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: 4/15
Findings: 3
Award: $1,288.31
🌟 Selected for report: 4
🚀 Solo Findings: 0
cmichel
The address.transfer
function is used in sweepFees
and (and swapByQuote
) to send ETH to an account.
It is restricted to a low amount of GAS and might fail if GAS costs change in the future or if feeRecipient
is a smart contract and its fallback function handler implements anything non-trivial.
Consider using the lower-level .call{value: value}
instead and check it's success return value.
#0 - Shadowfiend
2021-11-04T16:22:12Z
Duplicate of #20.
cmichel
The Swap.constructor
allows arbitrary swap fees, even > 100%.
It should check that the fee is at most 100% as in setSwapFee
.
Setting swap fees > 100% will revert the contract on trades when trying to pay out more than received.
Consider adding a require(swapFee_ < SWAP_FEE_DIVISOR, "Swap::setSwapFee: Swap fee must not exceed 100%");
statement to the constructor.
#0 - Shadowfiend
2021-11-03T20:59:56Z
Duplicate of #25.
🌟 Selected for report: cmichel
816.1402 USDC - $816.14
cmichel
There exist ERC20 tokens that made certain customizations to their contract.
One type of these tokens is deflationary tokens that charge a certain fee for every transfer()
or transferFrom()
.
The Swap.swapByQuote
function performs a safeTransferFrom(msg.sender, this, amountToSell)
and then approves the same amountToSell
which will be more than what the Swap contract actually received due to fees.
The zrxAllowanceTarget
contract is allowed to transfer more than what was transferred to the contract post fees.
If the Swap
contract accumulated swap fees in this token, part of them (amountToSell - amountToSellPostFees
) can be stolen/are allowed to be transferred from the Swap contract.
One possible mitigation is to measure the asset change right before and after the asset-transferring IERC20(zrxSellTokenAddress).safeTransferFrom(msg.sender, address(this), amountToSell)
call, and then only approve the zrxAllowanceTarget
with the difference.
#0 - Shadowfiend
2021-11-04T16:51:21Z
We may tackle this as part of mitigating #37, since we're considering how to rework the allowance/transfer pattern here.
35.9883 USDC - $35.99
cmichel
The Math
library uses Solidity version 0.8 which comes with built-in overflow checks which cost gas.
The code already checks for underflows (a > b
before doing the division), and therefore the built-in checks can be disabled everywhere for improved gas cost.
pragma solidity ^0.8.0; library Math { function subOrZero(uint256 a, uint256 b) internal pure returns (uint256) { unchecked { return a > b ? a - b : 0; } } function subOrZero(uint128 a, uint128 b) internal pure returns (uint128) { unchecked { return a > b ? a - b : 0; } } function subOrZero(uint64 a, uint64 b) internal pure returns (uint64) { unchecked { return a > b ? a - b : 0; } } function subOrZero(uint32 a, uint32 b) internal pure returns (uint32) { unchecked { return a > b ? a - b : 0; } } }
10.4942 USDC - $10.49
cmichel
The Swap
contract uses Solidity version 0.8 which already implements overflow checks by default.
At the same time, it uses the SafeMath
library which is more gas expensive than the 0.8 overflow checks.
It should just use the built-in checks and remove SafeMath
from the dependencies:
// @audit can just normal arithmetic here uint256 toTransfer = SWAP_FEE_DIVISOR.sub(swapFee).mul(boughtERC20Amount).div(SWAP_FEE_DIVISOR); // uint256 toTransfer = (SWAP_FEE_DIVISOR - swapFee) * boughtERC20Amount / SWAP_FEE_DIVISOR; // same with many other computations
🌟 Selected for report: cmichel
Also found by: TomFrenchBlockchain
35.9883 USDC - $35.99
cmichel
The minimumAmountReceived
check in Swap.swapByQuote
is implemented like this:
require( ( !signifiesETHOrZero(zrxBuyTokenAddress) && boughtERC20Amount >= minimumAmountReceived ) || ( signifiesETHOrZero(zrxBuyTokenAddress) && boughtETHAmount >= minimumAmountReceived ), "Swap::swapByQuote: Minimum swap proceeds requirement not met" );
It can be simplified to this which performs less calls to signifiesETHOrZero
and less logical operators:
require( (signifiesETHOrZero(zrxBuyTokenAddress) ? boughtETHAmount : boughtERC20Amount) >= minimumAmountReceived, "...");
#0 - Shadowfiend
2021-11-04T16:36:21Z
And has the benefit of being clearer! Great find!