โ Supply and Redeem
Intro
TestCompoundErc20.sol
provides the following 4 functionalities:
supply()
(lender deposits collateral)redeem()
(lender withdraws collateral)borrow()
(borrower enters market and borrows loan)repay()
(borrower pays back loan)
In this section we are going to implement the lender's functions supply
and redeem
. The sample code is here:
Setup
In the constructor, we initialize the ERC20 token that we wish to use as collateral and cToken contract address:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./interfaces/compound.sol";
// supply()
// redeem()
// borrow()
// repay()
contract TestCompoundErc20 {
IERC20 public token;
CErc20 public cToken;
constructor(address _token, address _cToken) {
token = IERC20(_token);
cToken = CErc20(_cToken);
}
}
supply() - lender deposits collateral
Lender calls supply(uint _amount)
to deposit ERC20 token as collateral and get cToken back as "receipt token":
function supply(uint _amount) external {
// Step 1: transfer ERC20 token from lender's wallet to this contract
token.transferFrom(msg.sender, address(this), _amount);
// Step 2: approve cToken contract to spend the token we just transfered
token.approve(address(cToken), _amount);
// Step 3: mint cToken, equivalent to deposit collateral to Compound
// cToken.mint() returning 0 means function call succeeded
require(cToken.mint(_amount) == 0, "mint failed");
}
When calling cToken.mint()
, the ERC20 token in this contract will be transferred to the cToken
contract via transferFrom()
. This is why we have to do token.approve()
first.
redeem() - lender withdraws collateral
This function is the opposite of supply()
. Lender calls redeem()
to burn cToken and get ERC20 token back (interest is included in cToken price):
function redeem(uint _cTokenAmount) external {
// cToken.redeem() returning 0 means function call succeeded
require(cToken.redeem(_cTokenAmount) == 0, "redeem failed");
}
This is just a wrapper that calls cToken.redeem()
.
Utility Functions
We need a getter to query cToken balance:
function getCTokenBalance() external view returns (uint) {
return cToken.balanceOf(address(this));
}
Query exchange rate and supply rate:
// not view function
function getInfo() external returns (uint exchangeRate, uint supplyRate) {
// Exchange rate between cToken and underlying token
// For example, cETH <-> ETH begins at 0.02 and is increased by APR
exchangeRate = cToken.exchangeRateCurrent();
// Amount added to you supply balance this block
// This is just the interest rate
supplyRate = cToken.supplyRatePerBlock();
}
We can estimate underlying asset balance by the formula cTokenBal * exchangeRate
. And figure out some messy decimals things:
// not view function
function estimateBalanceOfUnderlying() external returns (uint) {
uint cTokenBal = cToken.balanceOf(address(this));
uint exchangeRate = cToken.exchangeRateCurrent();
uint decimals = 8; // WBTC = 8 decimals
uint cTokenDecimals = 8;
return (cTokenBal * exchangeRate) / 10**(18 + decimals - cTokenDecimals);
}
Official API for querying underlying asset balance:
// not view function
function balanceOfUnderlying() external returns (uint) {
// cToken.balanceOfUnderlying() returning 0 means function call succeeded
return cToken.balanceOfUnderlying(address(this));
}
Tests
In the setup, we create a whale user with each WBTC to run this demo:
beforeEach(async () => {
await sendEther(web3, accounts[0], WHALE, 1)
testCompound = await TestCompoundErc20.new(TOKEN, C_TOKEN)
token = await IERC20.at(TOKEN)
cToken = await CErc20.at(C_TOKEN)
const bal = await token.balanceOf(WHALE)
console.log(`whale balance: ${bal}`)
assert(bal.gte(DEPOSIT_AMOUNT), "bal < deposit")
})
Create a function snapshot()
that logs current states:
const snapshot = async (testCompound, token, cToken) => {
const { exchangeRate, supplyRate } = await testCompound.getInfo.call()
return {
exchangeRate,
supplyRate,
estimateBalance: await testCompound.estimateBalanceOfUnderlying.call(),
balanceOfUnderlying: await testCompound.balanceOfUnderlying.call(),
token: await token.balanceOf(testCompound.address),
cToken: await cToken.balanceOf(testCompound.address),
}
}
Test case:
it("should supply and redeem", async () => {
await token.approve(testCompound.address, DEPOSIT_AMOUNT, {
from: WHALE
})
let tx = await testCompound.supply(DEPOSIT_AMOUNT, {
from: WHALE,
})
let after = await snapshot(testCompound, token, cToken)
// for (const log of tx.logs) {
// console.log(log.event, log.args.message, log.args.val.toString())
// }
console.log("--- supply ---")
console.log(`exchange rate ${after.exchangeRate}`)
console.log(`supply rate ${after.supplyRate}`)
console.log(`estimate balance ${after.estimateBalance}`)
console.log(`balance of underlying ${after.balanceOfUnderlying}`)
console.log(`token balance ${after.token}`)
console.log(`c token balance ${after.cToken}`)
// accrue interest on supply
const block = await web3.eth.getBlockNumber()
await time.advanceBlockTo(block + 100)
after = await snapshot(testCompound, token, cToken)
console.log(`--- after some blocks... ---`)
console.log(`balance of underlying ${after.balanceOfUnderlying}`)
// test redeem
const cTokenAmount = await cToken.balanceOf(testCompound.address)
tx = await testCompound.redeem(cTokenAmount, {
from: WHALE,
})
after = await snapshot(testCompound, token, cToken)
console.log(`--- redeem ---`)
console.log(`balance of underlying ${after.balanceOfUnderlying}`)
console.log(`token balance ${after.token}`)
console.log(`c token balance ${after.cToken}`)
})
Last updated
Was this helpful?