Skip to content

Latest commit

 

History

History
100 lines (65 loc) · 5.26 KB

001-H.md

File metadata and controls

100 lines (65 loc) · 5.26 KB

asD contract owner can not withdraw interests due to incorrect scaling factor

Vulnerability details

Impact

  • Calculating maximumWithdrawable will underflow and revert due to incorrect scaling.

  • Contract owner won't be able to withdraw earned interests with the withdrawCarry() function.

Proof of Concept

Creators can create stablecoins pegged to $NOTE with this protocol and they can earn interest with their creations.

Here is how the minting/burning of these stablecoins and interest mechanism works:

  • Users call asD::mint() and transfer their $NOTE tokens to mint asD stablecoin

  • asD contract immediately deposits these $NOTE tokens to Canto Lending Market and mints cNOTE, which earns interest.

  • Users get a corresponding amount of asD stablecoin

The cNOTE tokens in the lending market earn interest according to exchangeRate which is always increasing.

The opposite happens when burning.

  • Users request their $NOTE tokens by calling asD::burn()

  • The asD contract redeems that amount of $NOTE token from the Canto Lending Market and burns cNOTE tokens according to that moment's exchange rate.

  • Because the exchange rate is increasing, the burned (redeemed) cNOTE amount is less than the minted amount, and the difference is the interest that the creator earns.

The asD contract creator can withdraw these earned interests by calling the asD::withdrawCarry() function:
https://github.com/code-423n4/2023-11-canto/blob/335930cd53cf9a137504a57f1215be52c6d67cb3/asD/src/asD.sol#L72

file:asD.sol
    function withdrawCarry(uint256 _amount) external onlyOwner {
-->     uint256 exchangeRate = CTokenInterface(cNote).exchangeRateCurrent(); // Scaled by 1 * 10^(18 - 8 + Underlying Token Decimals), i.e. 10^(28) in our case    //@audit-issue According to cToken source code it is scaled by 1e18 - NOT 1e28
        // The amount of cNOTE the contract has to hold (based on the current exchange rate which is always increasing) such that it is always possible to receive 1 NOTE when burning 1 asD
        uint256 maximumWithdrawable = (CTokenInterface(cNote).balanceOf(address(this)) * exchangeRate) /
-->         1e28 -
            totalSupply();
        if (_amount == 0) {
            _amount = maximumWithdrawable;
        } else {
            require(_amount <= maximumWithdrawable, "Too many tokens requested");
        }
       
        // skipped for brevity
    }

This function basically checks the current exchange rate, calculates the $NOTE value of the current cNOTE balances with this exchange rate, and finds the maximumWithdrawable amount as interest.

In this function, the exchange rate is assumed to be scaled by 1e28 as you can see in this comment "Scaled by 1 * 10^(18 - 8 + Underlying Token Decimals), i.e. 10^(28) in our case", this division.

This comment is true based on the compound docs. However, this is not the case in the actual source code. In the code itself, the returned exchange rate is always scaled by 1e18.

file: CToken.sol //compound
    /**
     * @notice Accrue interest then return the up-to-date exchange rate
     * @return Calculated exchange rate scaled by 1e18
     */
    function exchangeRateCurrent() override public nonReentrant returns (uint) {
        accrueInterest();
        return exchangeRateStored();
    } 

Canto Lending Market is a compound fork and it returns the exchange rate scaled by 1e18 too. It can also be checked by reading the contract in the Canto blockchain explorers.
cNOTE contract address is: 0xEe602429Ef7eCe0a13e4FfE8dBC16e101049504C
It can be checked here (function no 40): https://tuber.build/token/0xEe602429Ef7eCe0a13e4FfE8dBC16e101049504C?tab=read_contract

You can see that the live cNOTE contract returns the exchange rate with 18 decimals in the screenshot below:
https://user-images.githubusercontent.com/97894167/283773543-a4d64810-474d-4ff4-8b9d-3b60682f18e6.png

The maximumWithdrawable amount calculation will always underflow due to the exchange rate being 18 decimals but the calculation is made with a hardcoded 1e28 value.

uint256 maximumWithdrawable = (CTokenInterface(cNote).balanceOf(address(this)) * exchangeRate) /
-->         1e28 -
            totalSupply(); //@audit will underflow due to 1e8 - 1e18

Tools Used

Manual review

Recommended Mitigation Steps

I would recommend using 1e18 when scaling the exchange rate instead of 1e28


Note: The original submission can be found here.