Platform: Code4rena
Start Date: 20/10/2021
Pot Size: $30,000 ETH
Total HM: 5
Participants: 15
Period: 3 days
Judge: 0xean
Total Solo HM: 3
Id: 44
League: ETH
Rank: 2/15
Findings: 3
Award: $4,928.01
🌟 Selected for report: 4
🚀 Solo Findings: 0
🌟 Selected for report: WatchPug
Also found by: harleythedog
harleythedog
Suppose that swapByQuote is called with zrxSellTokenAddress == zrxBuyTokenAddress, and neither of these addresses "signifiesETHOrZero". The contract first transfers amountToSell of these tokens from the sender's account into the contract and updates the allowance. Then, fillZrxQuote is called, and the erc20Delta variable is calculated as
(amount of the buy token after the zrxTo.call) - (amount of the buy token before the zrxTo.call)
So, if the zrxTo.call resulted in 0 output (trades amountToSell buy tokens for amountToSell buy tokens), then erc20Delta will be calculated as 0. This will throw an error due to the require statement on line 219, so the swap will not proceed.
There are 2 trains of thought here:
It shouldn't be the case that zrxSellTokenAddress == zrxBuyTokenAddress anyways, so it is okay that this errors. If this is the case, I believe adding a require(zrxSellTokenAddress != zrxBuyTokenAddress) at the start of the swapByQuote function would be beneficial, as there is currently no explicit checks for this, and this would save the gas of having to go through a lot of the code before erroring out.
This is a valid issue, and users should be able to, for example, trade xyz tokens for xyz tokens using swapByQuote. In this case, the calculation of erc20Delta should instead be
(amount of buy token after zrxTo.call) - (amount of buy token before transferring the sell tokens)
Side note: In case 2, the scenario where a user wants to trade unwrapped eth for unwrapped eth also needs to be handled differently, since address(this).balance is incremented with msg.value before swapByQuote even begins executing.
fillZrxQuote here: https://github.com/code-423n4/2021-10-tally/blob/main/contracts/swap/Swap.sol#:~:text=function-,fillZrxQuote,-(
Recommend setting originalERC20Balance at the start of swapByQuote instead, and then calculate erc20Delta in swapByQuote as well using the logic described above.
Or, if this behaviour is not intended, explicitly check that zrxSellTokenAddress != zrxBuyTokenAddress in swapByQuote to make this more obvious and potentially save gas on these cases.
Manual inspection.
Handle case where sell token and buy token are equal as described above (or make this more explicitly disallowed if this is not expected behaviour).
#0 - Shadowfiend
2021-11-04T17:03:47Z
Duplicate of #34, I believe.
🌟 Selected for report: harleythedog
367.2631 USDC - $367.26
harleythedog
In the sweepFees function, address(this).balance is being used as the "amount" in the SweepFees event immediately after a transfer. So, the amount in the event on line 258 will always be 0, but it should be what address(this).balance was before the transfer. This has implications on overall functionality, tools that are monitoring this event will receive incorrect information. A fix is to store the value before calling the transfer.
referenced lines in sweepFees function: https://github.com/code-423n4/2021-10-tally/blob/main/contracts/swap/Swap.sol#:~:text=feeRecipient.transfer(address,this).balance%2C%20feeRecipient)%3B
A more correct implementation would be:
uint256 amount = address(this).balance; feeRecipient.transfer(address(this).balance); emit FeesSwept(address(0), amount, feeRecipient);
Manual inspection
Store balance before calling transfer, as above.
🌟 Selected for report: harleythedog
Also found by: pants
35.9883 USDC - $35.99
harleythedog
In the sweepFees function, the storage variable feeRecipient is read multiple times and is read in the main loop as well. Each of these reads will be an expensive SLOAD operation. It would save a significant amount of gas if you were to store a memory copy of feeRecipient at the start of the function, and read this value instead (using the cheaper MLOAD operation instead).
sweepFees function here: https://github.com/code-423n4/2021-10-tally/blob/main/contracts/swap/Swap.sol#:~:text=function-,sweepFees,-(
A much more gas efficient function would look like this:
function sweepFees( address[] calldata tokens ) external nonReentrant { address payable recipient = feeRecipient; // read from this variable instead require( recipient != address(0), "Swap::withdrawAccruedFees: recipient is not initialized" ); for (uint8 i = 0; i<tokens.length; i++) { uint256 balance = IERC20(tokens[i]).balanceOf(address(this)); if (balance > 0) { IERC20(tokens[i]).safeTransfer(recipient , balance); emit FeesSwept(tokens[i], balance, recipient); } } recipient .transfer(address(this).balance); emit FeesSwept(address(0), address(this).balance, recipient); }
Manual inspection.
Change code to read from memory instead of storage as described above.
🌟 Selected for report: harleythedog
816.1402 USDC - $816.14
harleythedog
None of the events in swap.sol are indexed, so it is not easy for off-chain tools to efficiently filter these events. I would recommend adding indices to the SwappedTokens and FeesSwept events. For SwappedTokens, I would recommend adding an index on the tokenSold and tokenBought fields. For FeesSwept, I would recommend adding an index on the token and recipient fields.
See event declarations here: https://github.com/code-423n4/2021-10-tally/blob/main/contracts/swap/Swap.sol#:~:text=event-,SwappedTokens,-(
Manual inspection
Added indices as described above.
🌟 Selected for report: harleythedog
Also found by: pants
35.9883 USDC - $35.99
harleythedog
In the setFeeRecipient and the constructor, the newFeeRecipient event is emitted. In both of these function, the value used is the feeRecipient storage variable. It would save a small amount of gas if you instead used the feeRecipient_ memory variable (less cost for MLOAD vs SLOAD) as this variable is equal to feeRecipient due to the previous line before the emit statement. This logic also applies to the NewSwapFee event emitted in the constructor.
setFeeRecipient here: https://github.com/code-423n4/2021-10-tally/blob/main/contracts/swap/Swap.sol#:~:text=function-,setFeeRecipient,-(address%20payable%20feeRecipient_
and constructor here: https://github.com/code-423n4/2021-10-tally/blob/main/contracts/swap/Swap.sol#:~:text=for%20each%20swap.-,constructor,-(address%20owner_%2C%20address
My suggestion is to change the lines:
emit NewFeeRecipient(feeRecipient);
to be:
emit NewFeeRecipient(feeRecipient_);
to save a little bit of gas. The same logic can be applied for the swapFee in the constructor.
Manual inspection.
Change to use the memory variables as described above.
#0 - 0xean
2021-11-06T12:17:44Z
duplicate of #58
#1 - 0xean
2021-11-06T12:19:10Z
Going to use this issue as canonical since it contains both places this occurs