✅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?
