Platform: Code4rena
Start Date: 13/12/2021
Pot Size: $75,000 USDC
Total HM: 11
Participants: 30
Period: 7 days
Judge: leastwood
Total Solo HM: 4
Id: 68
League: ETH
Rank: 10/30
Findings: 3
Award: $2,657.56
🌟 Selected for report: 3
🚀 Solo Findings: 0
gpersoon
The function joinPool() tries to prevent going over the cap by checking:
require( totalSupply.add(_amount) <= this.getCap(), "MAX_POOL_CAP_REACHED" );
However later on in the code, 2 mint actions take place (which increase the totalSupply). One is for "feeBeneficiaryShare" and the other is for "_amount". These 2 together are more than "_amount", so if the supply is close to the Cap it could slightly surpass the Cap, with feeBeneficiaryShare
function joinPool(uint256 _amount, uint16 _referral) external override noReentry { ... require( totalSupply.add(_amount) <= this.getCap(), "MAX_POOL_CAP_REACHED" ); .... if (feeBeneficiaryShare != 0) { LibERC20.mint(bs.feeBeneficiary, feeBeneficiaryShare); // mint an additional feeBeneficiaryShare } .... LibERC20.mint(msg.sender, _amount); // mints _amount
Move the require statement to the end of function joinPool() and change it to:
require( totalSupply <= this.getCap(), "MAX_POOL_CAP_REACHED" );
If this require doesn't succeed is will roll back all actions. Note: it will use slightly more gas when the Cap is reached and a revert takes place
#0 - loki-sama
2022-01-03T11:29:56Z
Duplicate #283
gpersoon
Some functions, like rebalance() in RebalanceManagerV3 use _deadline as a time limit for swapExactTokensForTokens() Other functions, like _joinTokenSingle() of SingleTokenJoinV2.sol and _exit() of SingleNativeTokenExitV2() use block.timestamp, although a deadline field is present in the struct.
Possibly the deadline fields should have been used.
function rebalance(UnderlyingTrade[] calldata _swapsV2, uint256 _deadline) external override onlyRebalanceManager { ... for (uint256 i; i < _swapsV2.length; i++) { ... for (uint256 j; j < trade.swaps.length; j++) { .. _swapUniswapV2(swap.exchange,input,0, swap.path,address(basket), _deadline );
function _swapUniswapV2(...) { basket.singleCall( exchange, abi.encodeWithSelector( IUniswapV2Router02(exchange).swapExactTokensForTokens.selector, quantity, minReturn, path, recipient, deadline ), 0 );
struct JoinTokenStructV2 { ... uint256 deadline; ... } function _joinTokenSingle(JoinTokenStructV2 calldata _joinTokenStruct) internal { ... for (uint256 j; j < trade.swaps.length; j++) { IPangolinRouter(swap.exchange).swapExactTokensForTokens( amountIn, 0, swap.path, address(this), block.timestamp ); } }
struct ExitTokenStructV2 { ... uint256 deadline; ... } function _exit(ExitTokenStructV2 calldata _exitTokenStruct) internal { ... for (uint256 i; i < _exitTokenStruct.trades.length; i++) { ... for (uint256 j; j < trade.swaps.length; j++) { ... IPangolinRouter(swap.exchange).swapExactTokensForTokens( IERC20(swap.path[0]).balanceOf(address(this)), 0, swap.path, address(this), block.timestamp ); } }
Check whether the deadline fields should have been used. If so replace block.timestamp with the appropriate deadline
#0 - loki-sama
2022-01-04T11:12:34Z
duplicate #47
#1 - 0xleastwood
2022-01-22T03:57:17Z
Marking as primary issue.
🌟 Selected for report: gpersoon
gpersoon
The function joinPool() and calcTokensForAmount() both calculate the tokenAmount , taking in account the entryFee. However both use a different formula. The result of the formulas is the same as far as I can see. However for maintenance purposes it is better to use the same formula.
Note1: exitPool() and calcTokensForAmountExit() do use the same formula. Note2: When using the same formula it is easier to optimize for gas, for example to take "_amount.add(feeAmount)" outside of the for loop.
function joinPool(uint256 _amount, uint16 _referral) external override noReentry { ... uint256 feeAmount = _amount.mul(bs.entryFee).div(10**18); for (uint256 i; i < bs.tokens.length; i++) { IERC20 token = bs.tokens[i]; uint256 tokenAmount = balance(address(token)).mul(_amount.add(feeAmount)).div( totalSupply );
function calcTokensForAmount(uint256 _amount) external view override returns (address[] memory tokens, uint256[] memory amounts) { ... for (uint256 i; i < bs.tokens.length; i++) { IERC20 token = bs.tokens[i]; uint256 tokenBalance = balance(address(token)); uint256 tokenAmount = tokenBalance.mul(_amount).div(totalSupply); tokenAmount = tokenAmount.add( tokenAmount.mul(bs.entryFee).div(10**18) );
In function calcTokensForAmount() use the same formula to calculate tokenAmount as in joinPool()
450.6605 USDC - $450.66
gpersoon
The function rebalance() of RebalanceManagerV3 calculates the amount of tokens received by subtracting oldBalance from the new balanceOf() If the new balanceOf() happens to be smaller than oldBalance (due to an error or a weird situation at the exchange) then the variable "input" would be negative. As the solidity version is 0.7.5 and input is an unsigned int, it would underflow and result in a very large value.
Assuming this happens at the last cycle of the inner for loop, then the following require would not revert because input is very large (although it should have reverted)
require(trade.minimumReturn <= input, "INSUFFICIENT_OUTPUT_AMOUNT");
This way you could receive less tokens than required with a revert, which is unwanted.
pragma solidity ^0.7.5; ... function rebalance(UnderlyingTrade[] calldata _swapsV2, uint256 _deadline) external override onlyRebalanceManager { ... for (uint256 j; j < trade.swaps.length; j++) { ... uint256 input = trade.quantity; ... for (uint256 j; j < trade.swaps.length; j++) { ... uint256 oldBalance = IERC20(targetToken).balanceOf( address(basket) ); ... //The output of this trade is the input for the next trade input = IERC20(targetToken).balanceOf(address(basket)) - oldBalance; // no safemath is used here } require(trade.minimumReturn <= input, "INSUFFICIENT_OUTPUT_AMOUNT"); }
Use safemath for the subtraction
#0 - MiniRoman
2021-12-21T19:12:26Z
If the new balanceOf() happens to be smaller than oldBalance (due to an error or a weird situation at the exchange)
This scenario is not really possible. After buying a token, it's rather impossible, that it's balance will be smaller after buying it, then before the swap.
#1 - 0xleastwood
2022-01-22T03:24:34Z
This issue would involve the rebalance manager setting a malicious or improper exchange which is approved to transfer out the target token. As the sponsor has stated, it is extremely unlikely that an exchange will become malicious and transfer out the target token. However, I think it is still worth considering as a preventative measure. Marking this as low
.