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