Platform: Code4rena
Start Date: 04/11/2022
Pot Size: $42,500 USDC
Total HM: 9
Participants: 88
Period: 4 days
Judge: 0xean
Total Solo HM: 2
Id: 180
League: ETH
Rank: 20/88
Findings: 2
Award: $190.87
🌟 Selected for report: 2
🚀 Solo Findings: 0
🌟 Selected for report: neko_nyaa
Also found by: 0x52, 0xSmartContract, 0xc0ffEE, Josiah, KingNFT, Lambda, R2, RaymondFam, Ruhum, TomJ, Trust, TwelveSec, __141345__, c7e7eff, cccz, cryptostellar5, fs0c, hansfriese, horsefacts, ladboy233, minhtrng, pashov, rvierdiiev, sashik_eth, tonisives, wagmi
11.1039 USDC - $11.10
https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L163 https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L96-L105
The following report describes two issues with how the SizeSealed
contract incorrectly handles several so-called "weird ERC20" tokens, in which the token's balance can change unexpectedly:
Let us first note how the contract attempts to handle sudden balance change to the baseToken
:
https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L96-L105
uint256 balanceBeforeTransfer = ERC20(auctionParams.baseToken).balanceOf(address(this)); SafeTransferLib.safeTransferFrom( ERC20(auctionParams.baseToken), msg.sender, address(this), auctionParams.totalBaseAmount ); uint256 balanceAfterTransfer = ERC20(auctionParams.baseToken).balanceOf(address(this)); if (balanceAfterTransfer - balanceBeforeTransfer != auctionParams.totalBaseAmount) { revert UnexpectedBalanceChange(); }
The effect is that the operation will revert with the error UnexpectedBalanceChange()
if the received amount is different from what was transferred.
Unlike base tokens, there is no such check when transferring the quoteToken
from the bidder:
https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L163
SafeTransferLib.safeTransferFrom(ERC20(a.params.quoteToken), msg.sender, address(this), quoteAmount);
Since line 150 stores the quoteAmount
as a state variable, which will be used later on during outgoing transfers (see following lines), this results in incorrect state handling.
It is worth noting that this will effect all functions with outgoing transfers, due to reliance on storage values.
Consider the following scenario, describing the issue with both token types:
1e18
quote tokens. This quote token turns out to be a fee-on-transfer token, and the contract actually received 1e18 - fee
.For Alice's situation, she is able to recover her original amount. However, since aTokens increases one's own balance over time, the "interest" amount is permanently locked in the contract.
Bob's situation is somewhat more recoverable, as he can simply send more tokens to the contract itself, so that the contract's balance is sufficient to refund Bob his tokens. However, given that Bob now has to incur more transfer fees to get his own funds back, I consider this a leakage of value.
It is worth noting that similar impacts will happen to successful auctions as well, although it is a little more complicated, and that it varies in the matter of who is affected.
For the base token:
For the quote token:
withdraw()
will fail due to insufficient balance.
While technically two separate issues are described, they do have many overlappings, and both comes down to correct handling of unusual ERC20 tokens, hence I have decided to combine these into a single report.
Another intention was to highlight the similarities and differences between balance handling of base tokens and quote tokens, which actually has given rise to part of the issue itself.
Manual review
For both issues:
For the base token issue:
amountToTransferIn
for the amount to be pulled in, and auctionParams.totalBaseAmount
for the actual amount received, then check the received amount is appropriate.
auctionParams.totalBaseAmount
instead of exact, and possibly transferring the surplus amount back to the creator.For the quote token issue:
quoteAmount
to be correctly transferred, or check the actual amount transferred in the contract instead of using the function parameter, or something similar to the base token's mitigation.Additionally, consider using a separate internal function for pulling tokens, to ensure correct transfers.
#0 - c4-judge
2022-11-09T15:56:07Z
0xean marked the issue as duplicate
#1 - midori-fuse
2022-11-29T15:42:07Z
Why was this not the selected report for this issue?
Quoting the judge on the original issue #255
using this issue to gather all of the various manifestations of "special" ERC20 tokens not being supported. While rebasing tokens would need to be handled differently than fee on transfer, the underlying issue is close enough to not separate them all out
The thing is that the issue covers both fee-on-transfer, as well as rebasing tokens, also covers both base and quote tokens. I believe this was the only report that has covered all said aspects of the main issue.
#2 - 0xean
2022-12-02T14:04:20Z
@midori-fuse - thanks for raising the comment I do believe your issue does a better job than what I had originally selected.
#3 - c4-judge
2022-12-02T14:04:29Z
0xean marked the issue as selected for report
#4 - C4-Staff
2023-06-26T19:02:58Z
captainmangoC4 marked the issue as selected for report
179.769 USDC - $179.77
https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L163
When bidding, the contract pulls the quote token from the bidder to itself.
https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L163
SafeTransferLib.safeTransferFrom(ERC20(a.params.quoteToken), msg.sender, address(this), quoteAmount);
However, since the contract uses Solmate's SafeTransferLib
/// @dev Note that none of the functions in this library check that a token has code at all! That responsibility is delegated to the caller.
Therefore if the token address is empty, the transfer will succeed silently, but not crediting the contract with any tokens.
This error opens up room for a honeypot attack similar to the Qubit Finance hack in January 2022.
In particular, it has became popular for protocols to deploy their token across multiple networks using the same deploying address, so that they can control the address nonce and ensuring a consistent token address across different networks.
E.g. 1INCH has the same token address on Ethereum and BSC, and GEL token has the same address on Ethereum, Fantom and Polygon. There are other protocols have same contract addresses across different chains, and it's not hard to imagine such thing for their protocol token too, if any.
Assuming that Alice is the attacker, Bob is the victim. Alice has two accounts, namely Alice1 and Alice2. Denote token Q as the quote token.
baseToken
, and a token Q as the quoteToken
.1000e18
quote tokens.
1001e18
quote tokens.1000e18
refund bid back.As a result, the contract's Q token balance is 1e18
, Alice gets away with all her base tokens and 1000e18
Q tokens that are Bob's. Alice has stolen Bob's funds.
Manual review
Consider using OpenZeppelin's SafeERC20 instead, which has checks that an address does indeed have a contract.
#0 - c4-judge
2022-11-09T15:17:37Z
0xean marked the issue as duplicate
#1 - c4-judge
2022-11-24T13:15:41Z
0xean marked the issue as selected for report
#2 - c4-judge
2022-11-24T21:24:55Z
0xean changed the severity to 2 (Med Risk)
#3 - c4-judge
2022-11-28T22:25:21Z
0xean marked the issue as satisfactory
#4 - C4-Staff
2023-06-26T19:03:04Z
captainmangoC4 marked the issue as selected for report