Hands-on building a Money Market: Part 3
Welcome to the third and final part of the three-part series of building a Money Market on ICE which would allow anyone to supply cryptocurrencies to earn interest as well as borrow cryptocurrencies for loans.
In the previous two parts, we got acquainted with the basics of money market and the financial calculations used to calculate interest rates. In this part, we are going to set up the project, get insights on the working flow of the Solidity smart contracts, and finally, run the frontend to interact with dApp.
Contract Structure
.
├── configuration
│ └── AddressProvider.sol
├── lending-pool
│ ├── Config.sol
│ ├── LendingPool.sol
│ ├── LendingPoolCore.sol
│ ├── LendingPoolDataProvider.sol
│ └── ReserveInitializer.sol
├── oracle
│ └── Oracle.sol
├── tokens
│ ├── Asset.sol
│ ├── DToken.sol
│ ├── HToken.sol
└── utils
└── WadRayMath.sol
AddressProvider.sol
This contract stores the addresses of other contracts in the project, that are deployed to the chain.
Config.sol
This contract is a library. It stores the constants such as OPTIMAL_UTILIZATION_RATE, BASE_BORROW_RATE, SLOPE_RATE_1, and SLOPE_RATE_2. It also implements a method to update the cumulative indexes (both liquidity cumulative index and borrow cumulative index).
Note: If you have any confusion on the above terms, it is highly recommended to visit Part 2 of the article where we discussed them in detail.
LendingPool.sol
It is the originating point of transactions: deposit, borrow, redeem and repay. It gets data from LendingPoolDataProvider.sol and uses the current state to make various checks before the transactions are executed. After the checks are complete, it calls LendingPoolCore.sol to update the internal states and interest rates.
LendingPoolCore.sol
This contract holds all of the supported reserves and handles the internal calculations.
LendingPoolDataProvider.sol
This contract interfaces with other contracts to get overall data in aggregated form. It returns current user level and system level information.
ReserveInitializer.sol
This contract is used to initialize the reserve. When a new reserve has to be added, the owner can call this method to add tokens. This can be called by governance, if implemented later.
Oracle.sol
This contract is used to receive the current price of the asset in USD.
Asset.sol
This is an ERC20 base template. It can be used to create a new asset (token) which can later be added to the money market.
DToken.sol
This is an ERC20 token minted on borrowing of a particular asset. Therefore, for every asset, there is a DToken associated with it. It is deployed every time a new asset is initialized.
HToken.sol
This is an ERC20 token minted on the deposit of a particular asset. Similar to DToken, for every asset there is an HToken associated with it and is deployed every time a new asset is initialized.
WadRayMath.sol
This is a library used to represent and perform arithmetic on fixed-point decimal numbers.
Wad is a decimal number with 18 digits of precision
Ray is a decimal number with 27 digits of precision
Since the Solidity compiler doesn’t support fixed-point mathematics yet, we use WadRayMath. Andy Milenius has written a wonderful article here describing the necessity of WadRayMath.
Implementation
Before moving on with the implementation of each major method in smart contracts, we’ll see how the functions that we discussed in Part 2 are implemented.
Interest Rate Calculations
The interest rates as mentioned earlier are calculated in LendingPoolCore.sol via the calculateInterestRate function.
Note: WadRayMath is used in the calculations.
Interest Rates and Timestamp Updates
As we mentioned in the first and second part of the article series, with each transaction there is a change in total borrows and total liquidity, which in turn changes the utilization rate and consequently the interest rates. The updates required are also performed in the LendingPoolCore.sol.
Cumulated Indexes Updates
The calculation for the cumulated liquidity index Cᵗ and cumulated borrow index Bᵗ is performed on the Config.sol.
Deposit
When we want to make a deposit, we have to supply two parameters to the LendingPool contract:
- _reserve: Contract address of the asset that we want to deposit.
- _amount: The amount of asset we want to deposit.
The contract then performs the following tasks:
- Check if the reserve is active, if not, revert the transaction.
- Check if the _amount is greater than zero. If not, revert the transaction.
- Update the interest rates and the cumulative indexes.
- Transfer the deposited token to the reserve.
- Mint equivalent hToken to the user wallet.
- Emit Deposit event.
Various smart contracts involved in the call of this method are shown in the sequence diagram below.
Note: We have to first approve the LendingPoolCore contract address in the Asset contract so that the LendingPoolCore contract can transfer the asset from the actor to itself via the transferToReserve function on behalf of the actor when the deposit method is called.
Borrow
When we want to make a borrow, like in the case of a deposit, we pass on two parameters to the LendingPool contract:
- _reserve: Contract address of the asset that we want to borrow.
- _amount: The amount of asset we want to borrow.
The contract then performs the following tasks:
- Checks if the reserve is active. If not, revert the transaction.
- Checks if the _amount is greater than zero. If not, revert the transaction.
- Checks if there is enough liquidity. If not, revert the transaction.
- Checks if the user can be liquidated. If yes, revert the transaction.
- Checks if the user has supplied enough collateral earlier to borrow the required _amount. If not, revert the transaction.
- Updates the state on borrow. This mints equivalent dToken to the user wallet as well as updates cumulative indexes, interest rates, and timestamps.
- Transfers the borrowed amount to the user’s wallet.
- Emits Borrow event.
Various smart contracts involved in the calls of this method are shown in the sequence diagram below.
Redeem
When we want to redeem the amount deposited we pass a single parameter to the redeem method on hToken contract of the particular reserve. The parameter is:
- _amount: The amount we want to redeem
The hToken contract then performs the following tasks:
- Checks if the user can redeem the _amount. If not, revert the transaction.
- Burns the _amount of hToken.
It further calls the redeem method on LendingPoolContract with three parameters:
- _reserve: Contract address of the asset it wants to redeem.
- _user: Address of the user that has called the redeem method.
- _amount: Amount the user wants to redeem.
The LendingPoolContract then performs the following tasks:
- Checks if the reserve is active. If not, revert the transaction.
- Checks if the _amount is greater than zero. If not, revert the transaction.
- Checks if the right hToken address has called the method. If not, revert the transaction.
- Checks if there is enough liquidity. If not, revert the transaction.
- Updates the state on redeem. This updates the cumulative indexes, interest rates, and timestamps.
- Transfers the redeemed amount to the user’s wallet.
- Emits Redeem event.
Repay
When we want to repay the borrowed amount, we pass on two parameters to the LendingPool contract:
- _reserve: Contract address of the asset that we had borrowed.
- _amount: The amount of asset we want to repay.
The contract then performs the following tasks:
- Checks if the _amount is greater than zero. If not, revert the transaction.
- Checks if the user has some borrowed amount.
- Calculates the amount to return to the user and amount to repay to the LendingPoolCore contract. If the amount provided by user is greater than the total amount to repay, amount to return is greater than zero. Otherwise amount to return is zero.
- Updates the state on borrow. This updates the cumulative indexes, interest rates and timestamps. This action also burns the _amount of dTokens.
- Transfers the amount to return back to the user.
- Transfers the amount to repay to the core.
- Emits Repay event.
Note: We have to first approve the LendingPoolCore contract address in the Asset contract so that LendingPoolCore contract can transfer the asset from the actor to itself via transferToReserve function on behalf of the actor when deposit method is called.
Deploying Contracts and Running the frontend
Now let us deploy the contract explained above and then interact with it through the UI.
We will be using hardhat to deploy smart contracts to the Arctic testnet.
First of all, fork this github repo, and then we can start with installing the dependencies and deploying the contract to Arctic testnet.
NOTE: You can also follow the instruction from here for deploying the smart contracts.
- Checkout to the main branch
git checkout main
- Install necessary dependencies
npm install
Now let’s add the private key of the address that we are going to use to deploy the contracts.
- create a file .env in the root
touch .env
- Create a variable ICE_PRIVATE_KEY in .env file and paste your private key here.
ICE_PRIVATE_KEY=6d557dc854b9854c4c67134a6545197d03c6e40c4af3d452743cd89ce668174f
We have already configured hardhat-config.js for the deployment of contracts in the Arctic testnet.
Compile the contracts
In the same directory, run
npx hardhat compile
The above command utilizes the Solidity compiler to generate the contract’s bytecode and interface (ABI) in the artifacts folder in the same directory.
Deploy the contracts
Run the following commands to deploy the contracts to Arctic testnet
npx hardhat run scripts/deploy.js --network arctic
This command runs the script in the file deploy.js which is in the scripts directory.
It initializes the system, deploys three assets and mints all three tokens to deployer address, and deposit some of it. It also copies all contract addresses to frontend/src/consts/contractAddresses.json.
After deploying all the contracts successfully, you will get output similar to this:
When we compile the project, a directory, artifacts will be made. Copy the contracts directory in the artifacts folder to the frontend/src folder.
so in same directory, run
cp -r artifacts/contracts frontend/src
This command copies ABI of all contracts to the frontend folder.
Running the frontend
Navigate to the frontend folder, install required npm dependencies, and start the client using following commands:
cd frontend
npm install
npm start
The IMM (ICE Money Market) frontend will launch and will be accessible via any browser on http://localhost:3000.
We have also deployed our frontend here which you can visit.
Note: Make sure you have configured your metamask with the Arctic testnet before building the UI since we have deployed our contract there. You can make the configuration following the instructions here.
Let’s take a look at the UI
NOTE: We have already built a UI for interacting with the main smart contract methods which is inspired by OMM. Also, the frontend UI may contain some minor bugs.
As we can see in the UI, it consists of two side navigation menu; Home and Transfer. The Home page gives us the information of supplied/borrowed tokens, etc. And Transfer page lets us transfer our specific tokens to any account address.
Now let’s interact with smart contract methods via the UI.
Deposit
To deposit your token, you have to click on the token name, which will open a modal where you can adjust the amount using slider as shown in example below
Here we have deposited 199 BTC tokens, so that at the end total supply of the BTC token would be 399 BTC as shown in the example above. This action pops the metamask two times; first time is to approve the lendingPoolCore and second call is to actually deposit the tokens.
Borrow
Here we have borrowed 50 ETH tokens, which is clearly shown in You Borrowed column. We can choose the borrow amount by adjusting the slider as shown in example above. This action will pop out metamask to confirm the transaction for borrow the token.
Similarly we can Redeem the deposited tokens as well as Repay the borrowed tokens.
Summary
That is the end the of three part series of building Money Market On ICE. In this part, we got acquainted with the smart contracts and the explanations of the main methods. We also deployed the smart contracts on the Arctic testnet using hardhat. Finally, we run the frontend to make some deposit and borrowed some token as well.