This is Ricochet project for managing fixed-rate collateral-backed lending on Ethereum. This repository contains the core smart contracts and the tests written in Typescript using the framework hardhat and the ethers library. A role-based access is now used in the Rex Bank. The modifiers in the reserveDeposit and reserveWithdrawCollateral functions have been commented, because the old tests were written when the access was simple (admin and users).
The linter points out some code improvements:
- some error messages are too long.
- block.timestamp and related functions should not be used to make time calculations.
- Mark visibility of state variables in BankStorage.sol.
Two test cases are failing:
- should not allow user to withdraw collateral from vault if undercollateralized
- should add origination fee to a vault's borrowed amount
Both with the same message error: Error: VM Exception while processing transaction: reverted with reason string 'NOT ENOUGH COLLATERAL'
The 'should calculate correct collateralization ratio for a user's vault' and 'should liquidate undercollateralized vault' test cases are commented, because they interact with the oracle and it has to be mocked. However, the code has been rewritten in ethers, except for the web3.eth.sendTransaction function.
For faster runs of your tests and scripts, consider skipping ts-node's type checking by setting the environment variable TS_NODE_TRANSPILE_ONLY
to 1
in hardhat's environment. For more details see the documentation.
-
Transparency: The contract code is simple enough to understand by anyone familiar with Solidity
-
Flexibility: The type of collateral, debt, and rates can all be configured
-
Easy of Use: Deployment and configuration is simple enough for anyone familiar with Ethereum and web programming
On deployment, the bank owner specifies the following parameters:
- Debt Token: This is the token users borrow from the bank (i.e. USDC)
- Collateral Token: This is the token the bank accepts as collateral (i.e. TRB)
- Interest Rate: The annual interest rate the bank charges borrowers
- Origination Fee: The fixed fee charged to borrowers
- Collateralization Ratio: The loan-to-value amount borrowers must maintain to avoid a liquidation
- Liquidation Penalty: The fixed fee charged to borrowers who get liquidated
- Period: The period for calculating interest in seconds
Once deployed, the bank owner must deposit some debt tokens into the bank's reserve. After depositing debt tokens, users can deposit collateral tokens and borrow the bank's debt tokens. During the borrow, the borrower is charged an origination fee and then interest will accumulate until they repay what they've borrowed plus interest and fees. If at anytime the price of the collateral falls, then the bank owner will liquidate the borrowers collateral to repay their debt.
As of December 2021, the project was migrated from Truffle to hardhat version 2.8.0. and waffle Solidity version changed from 0.5.0 to >=0.8.4.
Tools used for testing
- node version 16.6.1 and npm version 7.2.0.3.
- All the tests are written in Typescript 4.5.2.
- ethers version 5.5.2 and Chai matchers.
npx hardhat compile
npx hardhat test
npm run build
For deployment on localhost, testnet or mainnet, edit the parameters you're interested in using. An example configuration:
let daiAddress = "0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa";
let trbAddress = "0xfe41cb708cd98c5b20423433309e55b53f79134a";
let tellorOracleAddress = "0xFe41Cb708CD98C5B20423433309E55b53F79134a";
let interestRate = 12;
let originationFee = 1;
let collateralizationRatio = 150;
let liquidationPenalty = 20;
let period = 86400;
let trbusdRequestId = 50;
let daiusdRequestId = 39;
let initialPrice = 1000000;
let priceGranularity = 1000000;
await deployer.deploy(Bank, interestRate, originationFee, collateralizationRatio, liquidationPenalty, period,
trbAddress, trbusdRequestId, initialPrice, priceGranularity,
daiAddress, daiusdRequestId, initialPrice, priceGranularity,
tellorOracleAddress);
Replace the values with those you wish to use for your bank deployment and visit the Bank.sol
constructor for more details about these parameters.
First, truffle migrate
the contract to deploy to Ganache, then setup the contract using truffle console
.
From the console, approve and deposit debt tokens (i.e. USDToken
) into the bank's reserve.
let bank = await Bank.deployed()
let dt = await USDToken.deployed()
let accounts = await web3.eth.getAccounts()
await dt.approve(bank.address, web3.utils.toWei("1000", "ether"), {from: accounts[0]})
await bank.reserveDeposit(web3.utils.toWei("1000", "ether"), {from: accounts[0]})
You can start the DApp using npm:
export PORT=3000
npm run dev
Initialize the oracle objects and get accounts:
let oracle = await TellorMaster.deployed()
let oracleAddress = (web3.utils.toChecksumAddress(oracle.address))
let oracle2 = await new web3.eth.Contract(Tellor.abi, oracleAddress)
let accounts = await web3.eth.getAccounts()
Then make a request to the oracle:
await web3.eth.sendTransaction({to: oracleAddress, from: accounts[0], gas: 4000000, data: oracle2.methods.requestData("USDT","USDT/USD",1000,0).encodeABI()})
Next, submit 5 values through mining:
await web3.eth.sendTransaction({to: oracle.address, from: accounts[1],gas:4000000, data: oracle2.methods.submitMiningSolution("nonce", 1, 1000000).encodeABI()})
await web3.eth.sendTransaction({to: oracle.address, from: accounts[2],gas:4000000, data: oracle2.methods.submitMiningSolution("nonce", 1, 1000000).encodeABI()})
await web3.eth.sendTransaction({to: oracle.address, from: accounts[3],gas:4000000, data: oracle2.methods.submitMiningSolution("nonce", 1, 1000000).encodeABI()})
await web3.eth.sendTransaction({to: oracle.address, from: accounts[4],gas:4000000, data: oracle2.methods.submitMiningSolution("nonce", 1, 1000000).encodeABI()})
await web3.eth.sendTransaction({to: oracle.address, from: accounts[5],gas:4000000, data: oracle2.methods.submitMiningSolution("nonce", 1, 1000000).encodeABI()})
Because the Bank contract is UsingTellor, you can get the current data from the oracle using:
let vars = await bank.getCurrentValue.call(1)
And the price will be contained in vars[1]
.
And you can update the price with:
await bank.updatePrice({from: accounts[0]})
There are unit tests for the smart contract functionality which you can run using:
truffle test
In addition to the unit tests, you can run these tests manually after the contract has been deployed to confirm everything works correctly through the DApp:
- Update the debt and collateral token prices
- As the owner, deposit debt tokens
- As a borrower, deposit collateral and withdraw some debt
- - Borrow and repay debt
- - Add and remove collateral
- - Repay all the debt and withdraw all collateral
- With a borrower undercollateralized, liquidate the borrower
- As the owner, withdraw collateral and debt