Platform: Code4rena
Start Date: 04/02/2022
Pot Size: $30,000 USDC
Total HM: 3
Participants: 37
Period: 3 days
Judge: leastwood
Id: 84
League: ETH
Rank: 21/37
Findings: 2
Award: $401.09
🌟 Selected for report: 1
🚀 Solo Findings: 0
82.0104 USDC - $82.01
The contract comments nor the submission text detail what happens when the user submits more tokenIn
tokens than are required to recieve N tokenOut
tokens, but not enough for N+1 tokens. The contract as written takes any excess contributions for itself.
Due to loss of precision, the number of tokenOut
tokens receieved is rounded down and the user loses any excess contributions, and also has to deal with any tax implications, due to this undocumented and surprising behavior. If amount of funds lost is directly proportional to the ratio of the tokenOutPrice
to tokenOut.decimals()
.
If tokenOutPrice
is greater than 10**tokenOut.decimals()
, loss of precision due to tokenOutAmount_
not being a floating type, will mean that the caller will lose out on fractions of tokenOut
tokens. The buy()
function does not return any excess tokenIn
tokens, and does not keep track of fractions of tokens partially paid for, and instead keeps them for itself.
buy()
calls getAmountOut()
:
tokenOutAmount_ = getAmountOut(_tokenInAmount); boughtAmounts[msg.sender] = boughtAmountTillNow + tokenOutAmount_; daoCommitments[_daoId] += tokenOutAmount_; totalTokenIn += _tokenInAmount; totalTokenOutBought += tokenOutAmount_;
And getAmount()
doesn't keep track of partial tokenOutAmount_
s
function getAmountOut(uint256 _tokenInAmount) public view returns (uint256 tokenOutAmount_) { tokenOutAmount_ = (_tokenInAmount * 10**tokenOut.decimals()) / tokenOutPrice; }
Based on this conversion comment
/// eg. 1 WBTC (8 decimals) = 40,000 CTDL ==> price = 10^8 / 40,000
If tokenOut
were something like a BTC with only 2 decimals and tokenIn
were USD, the comment would read:
/// eg. 40,000 USD (2 decimals) = 1 BTC ==> price = (40,000 * 10^2) / 1
which is 4,000,000. With this tokenOutPrice
, if someone submitted $79k USDC, they'd get int((79000 * 10**2)/(4000000)
which is only 1 BTC, when they spent almost 2 BTC's worth of USDC.
Code inspection
The code should follow the "principle of least surprise" and require()
that tokenOutPrice
be less than or equal to 10**tokenOut.decimals()
, or keep track of partial amounts, or refund excess contributions.
#0 - GalloDaSballo
2022-02-10T02:20:24Z
Ultimately the rounding issue is a problem with integer division, I don't think the finding as dramatic as it sounds
#1 - 0xleastwood
2022-03-14T11:26:19Z
Duplicate of #50
#2 - 0xleastwood
2022-03-21T08:32:13Z
After further thought, I actually think this is more of an issue of state handling. Users can mis-use this function to receive slightly lower amounts than expected due to the imprecise nature of arithmetic in Solidity. I'll mark this as low
to reflect this.
#3 - CloudEllie
2022-03-25T03:26:36Z
Since this issue was downgraded to a QA level, and the warden did not submit a separate QA report, we've renamed this one to "QA report" for consistency.
The original title, for the record, was "Loss of excess funds due to loss of precision."
319.0827 USDC - $319.08
#0 - CloudEllie
2022-02-18T22:27:50Z
This submission was sent via email on Feb 6, 2022; unfortunately I missed it until now. My apologies.
#1 - GalloDaSballo
2022-02-18T22:59:06Z
All findings are valid and appreciated, would recommend the warden to just post the specific gas differences instead of the whole table as there's 10s of thousands of lines in the readme, but the only lines that count are very few.
That said, this may be the best report I've ever seen, as while it's a lot of superfluous information (again just delete the redundant lines), the information is all there and verifiable.
I think the work from the warden is commendable and it is really appreciated.
A breath of fresh air compared to the usual copy paste "use != instead of >"