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: 12/120
Findings: 2
Award: $691.74
🌟 Selected for report: 0
🚀 Solo Findings: 0
690.3741 USDC - $690.37
Incorrect assumptions regarding the cNOTE
token's exchange rate scaling cause asD.withdrawCarry()
to calculate a wrongly scaled maximumWithdrawable
value. This issue could potentially block owners of asD
tokens from withdrawing the correct amount of rewards, potentially leaving funds trapped in the contract indefinitely.
The core of the issue lies within the withdrawCarry
function of the asD
contract:
// ... uint256 maximumWithdrawable = (CTokenInterface(cNote).balanceOf(address(this)) * exchangeRate) / 1e28 - totalSupply(); // ...
The function assumes that the cNOTE
exchange rate is scaled by 1e28
, as inferred from the comments in the code. However, according to the actual cNOTE
smart contract, the exchange rate is scaled by 1e18
. This discrepancy can be verified by inspecting the exchangeRateStoredInternal
function and the expScale
constant in the cNOTE
contract source code:
https://tuber.build/token/0xEe602429Ef7eCe0a13e4FfE8dBC16e101049504C?tab=read_contract
(click Code
-> change from Proxy
to Implementation
, then you can use the search)
Using the actual exchange rate at the moment (1003956909210692900 wei
or 1.0039569092106929 ether
), the calculation within the withdrawCarry
function leads to an underflow when scaled by 1e28
, thus preventing the correct computation of withdrawable funds:
(100 ether * 1003956909210692900) / 1e28 - 100 ether == -99999999989960430908 // underflow // the example assumes that there are 100 cNOTE on the balance and 100 asD tokens in the total supply
With the correct scaling factor (1e18
):
(100 ether * 1003956909210692900) / 1e18 - 100 ether == 395690921069290000 // 0.39569092106929 ether, the correct amount of funds to withdraw
Manual review
Replace the erroneous 1e28
scaling factor with the correct 1e18
scaling factor in the withdrawCarry
function:
uint256 maximumWithdrawable = (CTokenInterface(cNote).balanceOf(address(this)) * exchangeRate) / - 1e28 - + 1e18 - totalSupply();
By making this correction, the function will calculate the maximum amount of funds withdrawable by the owner correctly, enabling the withdrawal of all accrued rewards without unintentionally trapping funds in the contract.
Math
#0 - c4-pre-sort
2023-11-18T05:10:13Z
minhquanym marked the issue as duplicate of #227
#1 - c4-judge
2023-11-28T22:55:20Z
MarioPoneder marked the issue as satisfactory
🌟 Selected for report: rvierdiiev
Also found by: 0x175, 0x3b, 0xMango, 0xarno, 0xpiken, Bauchibred, DarkTower, ElCid, Giorgio, HChang26, Kose, KupiaSec, Madalad, PENGUN, Pheonix, RaoulSchaffranek, SpicyMeatball, T1MOH, Tricko, Udsen, Yanchuan, aslanbek, ast3ros, bart1e, bin2chen, chaduke, d3e4, deepkin, developerjordy, glcanvas, inzinko, jasonxiale, jnforja, mahyar, max10afternoon, mojito_auditor, neocrao, nmirchev8, openwide, osmanozdemir1, peanuts, pep7siup, peritoflores, pontifex, rice_cooker, rouhsamad, t0x1c, tnquanghuy0512, turvy_fuzz, twcctop, ustas, vangrim, zhaojie, zhaojohnson
1.3743 USDC - $1.37
https://github.com/code-423n4/2023-11-canto/blob/df7362f601f64ccb8afaf73ccd885109f8217498/1155tech-contracts/src/Market.sol#L147-L169 https://github.com/code-423n4/2023-11-canto/blob/df7362f601f64ccb8afaf73ccd885109f8217498/1155tech-contracts/src/Market.sol#L171-L189
Implementation of the Market.buy()
and Market.sell()
functions without price slippage protection can lead to users experiencing losses due to front-running (sandwich attacks). Users with large approvals for Market
could unintentionally pay more or receive less due to the variable nature of the resulting share price after other users transact with higher gas prices.
The buy
function as depicted in the affected code allows a user to purchase shares at the current market price. However, due to the transaction being public on the mempool, an attacker can observe the intent to buy a certain amount of shares (e.g., Bob's transaction) and can perform a transaction with a higher gas price (Alice's transaction) to execute before Bob's. This can lead to an increase in share price right before Bob's purchase is executed, causing Bob to pay a higher price for the shares.
Similarly, the sell
function allows users to sell shares at the current price. An attacker can sell shares right before a seller's transaction, causing the share price to drop and thus the seller receives less.
Manual review
To mitigate this issue, implement maximum accepted price for buying and minimum accepted price for selling to protect users against undesirable price slippage. This change will also discourage malicious front-running activities.
For Market.buy()
:
function buy(uint256 _id, uint256 _amount, uint256 maxPrice) external { require(shareData[_id].creator != msg.sender, "Creator cannot buy"); (uint256 price, uint256 fee) = getBuyPrice(_id, _amount); // Reverts for non-existing ID require(price <= maxPrice, "Price exceeds max allowed"); ... }
For Market.sell()
:
function sell(uint256 _id, uint256 _amount, uint256 minPrice) external { (uint256 price, uint256 fee) = getSellPrice(_id, _amount); require(price >= minPrice, "Price below min allowed"); ... }
By implementing these changes, users will specify the maximum price they are willing to pay for each share bought and the minimum price they are willing to accept for each share sold, protecting them from potential front-running and sandwich attacks.
MEV
#0 - c4-pre-sort
2023-11-18T09:59:38Z
minhquanym marked the issue as duplicate of #12
#1 - c4-judge
2023-11-28T23:14:14Z
MarioPoneder changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-11-28T23:28:36Z
MarioPoneder marked the issue as satisfactory