Platform: Code4rena
Start Date: 20/01/2022
Pot Size: $50,000 USDC
Total HM: 3
Participants: 35
Period: 7 days
Judge: GalloDaSballo
Total Solo HM: 2
Id: 77
League: ETH
Rank: 2/35
Findings: 2
Award: $1,779.97
🌟 Selected for report: 4
🚀 Solo Findings: 0
🌟 Selected for report: gzeon
1545.4995 USDC - $1,545.50
gzeon
The constant product k
is calculated by baseTokenReserve*quoteTokenReserve
, when this resolves to >= 2^256, some math in MathLib will overflow causing revert and all fund would be locked in the Exchange
until the balance decrease. There exists no mechanism like skim()
in UniswapV2 that remove excess token from the pool.
This is especially dangerous to elasticswap because rebase token can have their balance grow very quickly.
uint256 kLast; // as of the last add / rem liquidity event
internalBalances.kLast = internalBalances.baseTokenReserveQty * internalBalances.quoteTokenReserveQty;
qtyToReturn = (tokenASwapQtyLessFee * _tokenBReserveQty) / ((_tokenAReserveQty * BASIS_POINTS) + tokenASwapQtyLessFee);
Implement a skim()
function to remove excess token in the pool or otherwise make sure removeLiquidity
can be called even when baseTokenReserve*quoteTokenReserve
overflow.
Currently removeLiquidity
will revert when baseTokenReserve*quoteTokenReserve
overflow due to the calculation of kLast here:
https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/src/contracts/Exchange.sol#L231
internalBalances.kLast = internalBalances.baseTokenReserveQty * internalBalances.quoteTokenReserveQty;
unless the user remove enough token to make it not overflow
#0 - 0xean
2022-01-27T00:36:14Z
Adding of liquidity cannot cause the overflow, since it will revert before the overflow would occur when calculating K.
Let's take an example also to see how large this would have to be to occur. Let's take a token that has a total supply of 10 billion and 18 decimals. If the total supply of two of these tokens was in our contracts we would end up with a K as below:
100000000000000000000000000000000000000000000000000000000 The max uint256 is: 115792089237316195423570985008687907853269984665640564039457584007913129639935
Uniswap v2 also doesn't use uint256 for their variables.
Would recommend low or medium risk for this.
#1 - GalloDaSballo
2022-02-05T23:11:49Z
The warden is stating a fact, if the math goes above 2^256 it will revert.
The sponsor says that Adding of liquidity cannot cause the overflow
that is correct, because the MathLib
will calculate K and revert.
The sponsor example shows how it's incredibly unlikely that any token would cause a revert, I went and multiplied safemoons (9 decimals, trillions in circulation) and I still am 30 orders of magnitude away from a revert
<img width="510" alt="Screenshot 2022-02-06 at 00 10 52" src="https://user-images.githubusercontent.com/13383782/152662033-3608922d-2179-4e92-8781-0c8810edc41f.png">I think the observation is valid, but ultimately extremely unlikely.
I agree with a low severity and don't believe the sponsor has to mitigate
🌟 Selected for report: gzeon
106.5364 USDC - $106.54
gzeon
tokenBQty = (_tokenAQty * _tokenBReserveQty) / _tokenAReserveQty;
since _tokenAReserveQty > 0
the division is safe, we can do something like
assembly{ if iszero(eq(div(mul(_tokenAQty ,_tokenBReserveQty),_tokenAQty),_tokenBReserveQty)) {revert(0,0)} tokenBQty := div(mul(_tokenAQty ,_tokenBReserveQty),_tokenAReserveQty) }
or use the following function
// out = x * y unchecked{/} z function fmul(uint256 x, uint256 y, uint256 z) internal pure returns(uint256 out){ assembly{ if iszero(eq(div(mul(x,y),x),y)) {revert(0,0)} out := div(mul(x,y),z) } }
to save some gas
#0 - 0xean
2022-01-27T00:42:46Z
unsure if we want to got the assembly route, but non the less I am sure there are savings here.
#1 - GalloDaSballo
2022-02-04T01:12:57Z
If the division is safe, why not just do unchecked
?
Will mark as valid although would prefer if the warden showed actual gas savings
🌟 Selected for report: wuwe1
Also found by: Ruhum, WatchPug, csanuragjain, gzeon
13.9797 USDC - $13.98
gzeon
swapBaseTokenForQuoteToken
check for _baseTokenQty > 0 && _minQuoteTokenQty > 0
, but the same check also exists next line in MathLib.calculateQuoteTokenQty
function swapBaseTokenForQuoteToken( uint256 _baseTokenQty, uint256 _minQuoteTokenQty, uint256 _expirationTimestamp ) external nonReentrant() { isNotExpired(_expirationTimestamp); require( _baseTokenQty > 0 && _minQuoteTokenQty > 0, "Exchange: INSUFFICIENT_TOKEN_QTY" ); uint256 quoteTokenQty = MathLib.calculateQuoteTokenQty( _baseTokenQty, _minQuoteTokenQty, TOTAL_LIQUIDITY_FEE, internalBalances );
function calculateQuoteTokenQty( uint256 _baseTokenQty, uint256 _quoteTokenQtyMin, uint256 _liquidityFeeInBasisPoints, InternalBalances storage _internalBalances ) public returns (uint256 quoteTokenQty) { require( _baseTokenQty > 0 && _quoteTokenQtyMin > 0, "MathLib: INSUFFICIENT_TOKEN_QTY" );
#0 - GalloDaSballo
2022-02-04T01:13:42Z
Duplicate of #173
2.3145 USDC - $2.31
gzeon
> 0
is less gas efficient than != 0
for uint in require condition when optimizer is enabled
Ref: https://twitter.com/GalloDaSballo/status/1485430908165443590
require(this.totalSupply() > 0, "Exchange: INSUFFICIENT_LIQUIDITY");
#0 - GalloDaSballo
2022-02-04T22:32:34Z
For require statements, when using the optimizer, the finding is valid
🌟 Selected for report: gzeon
106.5364 USDC - $106.54
gzeon
Currently an Exchange is deployed by https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/src/contracts/ExchangeFactory.sol#L55
Exchange exchange = new Exchange( _name, _symbol, _baseToken, _quoteToken, address(this) ); exchangeAddressByTokenAddress[_baseToken][_quoteToken] = address( exchange ); isValidExchangeAddress[address(exchange)] = true; emit NewExchange(msg.sender, address(exchange));
which will generate a non-deterministic contract address for the pair. When a contract want to find the Exchange address for a specific pair, they would have to lookup exchangeAddressByTokenAddress[][]
costing a SLOAD. Instead, we can use CREATE2 to generate deterministic contract address for each pair.
bytes memory bytecode = type(Exchange).creationCode; bytes32 salt = keccak256(abi.encodePacked(_baseToken, _quoteToken)); assembly { exchange := create2(0, add(bytecode, 32), mload(bytecode), salt) }
To find the pair we can calculate the address, see https://github.com/sushiswap/sushiswap/blob/45da97206358039039883c4a99c005bb8a4bef0c/contracts/uniswapv2/libraries/UniswapV2Library.sol#L20
#0 - 0xean
2022-01-27T00:52:46Z
super interesting, will have to try this out. Thanks for the suggestion.
#1 - GalloDaSballo
2022-02-04T22:47:55Z
While the implementation would be some extra work, the warden gave good advice, so will mark as valid
5.0956 USDC - $5.10
gzeon
// We should ensure no possible overflow here. if (quoteTokenQtyToReturn > internalBalances.quoteTokenReserveQty) { internalBalances.quoteTokenReserveQty = 0; } else { internalBalances.quoteTokenReserveQty -= quoteTokenQtyToReturn; }
to
// We should ensure no possible overflow here. if (quoteTokenQtyToReturn > internalBalances.quoteTokenReserveQty) { internalBalances.quoteTokenReserveQty = 0; } else { unchecked{ internalBalances.quoteTokenReserveQty -= quoteTokenQtyToReturn; } }
#0 - 0xean
2022-01-31T13:54:38Z
dupe of #177