Platform: Code4rena
Start Date: 13/11/2023
Pot Size: $24,500 USDC
Total HM: 3
Participants: 120
Period: 4 days
Judge: 0xTheC0der
Id: 306
League: ETH
Rank: 16/120
Findings: 1
Award: $690.37
🌟 Selected for report: 0
🚀 Solo Findings: 0
690.3741 USDC - $690.37
Incorrect precision will result in profits being unable to be withdrawn normally.
Document address: https://docs.compound.finance/v2/#protocol-math wrote: The cToken Exchange Rate is scaled by the difference in decimals between the cToken and the underlying asset.
\Large \rm oneCTokenInUnderlying = exchangeRateCurrent / (1 * 10 ^ { (18 + underlyingDecimals - cTokenDecimals)})
The calculation in the code is as follows:
\Large \rm 1 * 10^{(18 - 8 + Underlying Token Decimals)} = 10^{(28)}
The specific code is as follows:
uint256 exchangeRate = CTokenInterface(cNote).exchangeRateCurrent(); // Scaled by 1 * 10^(18 - 8 + Underlying Token Decimals), i.e. 10^(28) in our case // 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();
It is considered that the precision of cNOTE is 8 and the precision of underlyingDecimals(Note) is 18
But the cNOTE precision deployed on the canto chain is 18
The deployment address is as follows(Already confirmed with sponsor): https://tuber.build/address/0xEe602429Ef7eCe0a13e4FfE8dBC16e101049504C?tab=read_contract#313ce567
So the correct precision is:
\Large \rm (1 * 10^ {(18 + underlyingDecimals - cTokenDecimals))} = 1*10^{ (18 + 18 - 18)} = 1*10^{18}
Because the precision is too high, maximumWithdrawable will be negative and cannot be positive until it reaches $10^{10}
$ times of totalSupply, resulting in the inability to withdraw profits normally.
POC: run forge install compound-finance/compound-protocol@master After installing the dependencies, it can run normally
// SPDX-License-Identifier: GPL-3.0-only pragma solidity >=0.8.0; import {asD} from "../asD.sol"; import {IERC20, ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "forge-std/Test.sol"; import "@compound-protocol/contracts/CErc20.sol"; import "@compound-protocol/contracts/CErc20Delegator.sol"; import "@compound-protocol/contracts/Comptroller.sol"; import "@compound-protocol/contracts/JumpRateModelV2.sol"; import "@compound-protocol/contracts/CErc20Delegate.sol"; import "@compound-protocol/contracts/CToken.sol"; contract MockERC20 is ERC20 { constructor(string memory symbol, string memory name) ERC20(symbol, name) {} function mint(address to, uint256 amount) public { _mint(to, amount); } } contract asD2Factory is Test { MockERC20 public note; CErc20Delegate public cnote; CErc20Delegator public cnoteDet; Comptroller public comptroller; JumpRateModelV2 public interestRateModel; uint public baseRatePerYear; uint public multiplierPerYear; uint public jumpMultiplierPerYear; uint initialExchangeRateMantissa; string asDName = "Test"; string asDSymbol = "TST"; asD asdToken; function setUp() public { cnote = new CErc20Delegate(); note = new MockERC20("note", "note"); // Underlying token decimals 18 comptroller = new Comptroller(); baseRatePerYear = 1e17; multiplierPerYear = 1e17; jumpMultiplierPerYear = 1e17; interestRateModel = new JumpRateModelV2(baseRatePerYear, multiplierPerYear, jumpMultiplierPerYear, 1, address(this)); initialExchangeRateMantissa = 1e18; // Initialize ExchangeRate 1e18 cnoteDet = new CErc20Delegator(address(note), comptroller, interestRateModel,initialExchangeRateMantissa, "cnote", "cnote", 18, // cnote decimals 18 payable(address(this)), address(cnote), bytes("")); comptroller._supportMarket(CToken(address(cnoteDet))); asdToken = new asD(asDName, asDSymbol, address(this), address(cnoteDet), address(this)); } function testexchangeRateCurrent() public { uint256 mintAmount = 10e18; note.mint(address(this), mintAmount); uint256 initialBalance = note.balanceOf(address(this)); note.approve(address(asdToken), mintAmount); asdToken.mint(mintAmount); assertEq(note.balanceOf(address(this)), initialBalance - mintAmount); assertEq(asdToken.balanceOf(address(this)), mintAmount); assertEq(note.balanceOf(address(cnoteDet)), mintAmount); assertEq(cnoteDet.totalSupply(), mintAmount); // Totalsupply is greater than 0, and will re -calculate the ExchangeErate uint256 exchange = cnoteDet.exchangeRateCurrent(); assertEq(exchange, 1e18); } }
VSCode Foundry
The correct writing should be changed to:
uint256 maximumWithdrawable = (CTokenInterface(cNote).balanceOf(address(this)) * exchangeRate) / 1e18 - totalSupply();
or
uint256 maximumWithdrawable = (CTokenInterface(cNote).balanceOfUnderlying(address(this)) - totalSupply();
Decimal
#0 - c4-pre-sort
2023-11-19T09:13:19Z
minhquanym marked the issue as duplicate of #227
#1 - c4-judge
2023-11-28T22:58:15Z
MarioPoneder marked the issue as satisfactory