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: 11/30
Findings: 3
Award: $2,492.08
🌟 Selected for report: 0
🚀 Solo Findings: 0
hyh
Griefing attack is possible for all basket joining contracts, making them fail all the time, disabling the join functionality for them altogether.
Basket join will be permanently failing if join contract balance grew by any amount, which is what any existing basket token holder can achieve. An attacker can send tiny amount of basket tokens to join contracts and joining the basket will be failing all the time. This will be the permanent state as there is no way to empty the balance of join contract.
The reason is strict equality as success condition of _joinTokenSingle functions of SingleTokenJoin and SingleTokenJoinV2 contracts. As EthSingleTokenJoin and EthSingleTokenJoinV2 use the affected _joinTokenSingle functions, all joining mechanics will be disabled. The mentioned contracts then to be fixed and redeployed.
The condition used for successful obtaining of required set of tokens is strict equality: https://github.com/code-423n4/2021-12-amun/blob/main/contracts/basket/contracts/singleJoinExit/SingleTokenJoin.sol#L134
It can be easily manipulated to failing by sending any amount of basket to the join contract.
This is possible as basket ERC20 do not censor transfers: https://github.com/code-423n4/2021-12-amun/blob/main/contracts/basket/contracts/facets/ERC20/ERC20Facet.sol#L138
a. Either perform balance check twice, before and after all the operations, and require the difference to be equal to _joinTokenStruct.outputAmount
b. Or, for gas saving and clarity, replace the condition with inequality, so that a user joining will get at least required amount of basket tokens. Now:
require(outputAmount == _joinTokenStruct.outputAmount, "FAILED_OUTPUT_AMOUNT");
To be:
require(outputAmount >= _joinTokenStruct.outputAmount, "FAILED_OUTPUT_AMOUNT");
#0 - loki-sama
2022-01-03T09:26:56Z
Duplicate #81
811.1888 USDC - $811.19
hyh
Exit swaps in each trade are restricted to (basket_token_i, ..., WETH) sequences as the output token is read from the first trade. This way any other correct trade structures will fail the function because of the output token amount check.
SingleNativeTokenExitV2.exit assumes that all trades will have the same output token and reads the output token from the first trade: https://github.com/code-423n4/2021-12-amun/blob/main/contracts/basket/contracts/singleJoinExit/SingleNativeTokenExitV2.sol#L93
When trades are more complicated than that due to liquidity or suitability considerations, for example, user wants to swap all the basket tokens to WETH, then WETH to USDC and withdraw it, or when basket is (t1, t2, WBTC) user may want to swap t1 to WBTC, t2 to WETH, then WBTC to WETH, and withdraw it, current logic fails and function will be reverted because of the final output token amount check: https://github.com/code-423n4/2021-12-amun/blob/main/contracts/basket/contracts/singleJoinExit/SingleNativeTokenExitV2.sol#L100
Fix the index to locate the last token in the swap sequence:
Now:
address[] calldata path = _exitTokenStruct .trades[0] .swaps[_exitTokenStruct.trades[0].swaps.length - 1] .path;
To be:
uint tradesLastIndex_ = _exitTokenStruct.trades.length - 1; address[] calldata path = _exitTokenStruct .trades[tradesLastIndex_] .swaps[_exitTokenStruct.trades[tradesLastIndex_].swaps.length - 1] .path;
Also, the comment looks like it is temporary, can it be clarified? https://github.com/code-423n4/2021-12-amun/blob/main/contracts/basket/contracts/singleJoinExit/SingleNativeTokenExitV2.sol#L96
#0 - loki-sama
2021-12-29T12:28:53Z
That is the intention of the contract to swap everything to a single token.
#1 - 0xleastwood
2022-01-23T02:32:26Z
I think this is a duplicate of #176
hyh
Ether being accidently sent to the contracts will be lost as it isn't used by these basket exit contracts and there is no retrieval mechanics in place. Correct behavior here looks to be rejecting any incoming native token transactions.
There is no business logic related need to have an ability to receive ETH for exit contracts.
SingleNativeTokenExit.receive https://github.com/code-423n4/2021-12-amun/blob/main/contracts/basket/contracts/singleJoinExit/SingleNativeTokenExit.sol#L37
SingleNativeTokenExitV2.receive https://github.com/code-423n4/2021-12-amun/blob/main/contracts/basket/contracts/singleJoinExit/SingleNativeTokenExitV2.sol#L48
Remove the receive()
function from the SingleNativeTokenExit and SingleNativeTokenExitV2 contracts
#0 - 0xleastwood
2022-01-23T05:35:07Z
Duplicate of #253
hyh
User balance will increase by lesser amount that user had requested as minimum accepted amount on calling SingleNativeTokenExitV2's exit
Minimum amount requested is checked before transfer, so a user will obtain less than stated minimum when a token uses transfer fees: https://github.com/code-423n4/2021-12-amun/blob/main/contracts/basket/contracts/singleJoinExit/SingleNativeTokenExitV2.sol#L100
If fee on transfer tokens will be allowed as output token in basket exiting, user's balance with the output token queries before and after the operations could be added to obtain as their difference and then verify the amount actually received by a user.
#0 - 0xleastwood
2022-01-23T05:26:39Z
Duplicate of #220
hyh
The minimum amount checks doesn't account for token decimals. For example, USDC minimum amount now is 1 USDC.
MIN_AMOUNT is hard coded to be 10^6 https://github.com/code-423n4/2021-12-amun/blob/main/contracts/basket/contracts/facets/Basket/BasketFacet.sol#L18
And used for basket tokens, that have decimals fixed to 18: https://github.com/code-423n4/2021-12-amun/blob/main/contracts/basket/contracts/facets/Basket/BasketFacet.sol#L229
And for tokens in a basket, that can have arbitrary decimals: https://github.com/code-423n4/2021-12-amun/blob/main/contracts/basket/contracts/facets/Basket/BasketFacet.sol#L33 https://github.com/code-423n4/2021-12-amun/blob/main/contracts/basket/contracts/facets/Basket/BasketFacet.sol#L209
Add token decimals variables and scale minimum amount according to it
#0 - 0xleastwood
2022-01-23T05:22:06Z
Duplicate of #5
🌟 Selected for report: pmerkleplant
Also found by: hyh
hyh
Comment doesn't match actual behavior
The comment says amount of exit fee that goes to the pool itself
for exitFeeBeneficiaryShare
:
https://github.com/code-423n4/2021-12-amun/blob/main/contracts/basket/contracts/facets/Basket/LibBasketStorage.sol#L18
While actually behavior for entryFeeBeneficiaryShare
and exitFeeBeneficiaryShare
is the same, they just mint the corresponding share to the feeBeneficiary
:
entryFeeBeneficiaryShare https://github.com/code-423n4/2021-12-amun/blob/main/contracts/basket/contracts/facets/Basket/BasketFacet.sol#L173
exitFeeBeneficiaryShare https://github.com/code-423n4/2021-12-amun/blob/main/contracts/basket/contracts/facets/Basket/BasketFacet.sol#L218
Update either the comment or the behavior
#0 - 0xleastwood
2022-01-23T05:40:32Z
This looks like its intended.
#1 - 0xleastwood
2022-01-23T05:54:46Z
I misread this issue. Actually a duplicate of #79
hyh
These contracts set configuration variables via initialize function without access controls, so whenever initialize is run not atomically with contract creation it can be front run by an attacker. The fix is to redeploy the contracts.
MintableERC20 https://github.com/code-423n4/2021-12-amun/blob/main/contracts/bridge/contracts/amunBasketBridge/MintableERC20.sol#L12
PolygonERC20Wrapper https://github.com/code-423n4/2021-12-amun/blob/main/contracts/bridge/contracts/amunBasketBridge/PolygonERC20Wrapper.sol#L16
a. Either set access roles in the constructor and restrict initialize access rights
b. Or run initialize atomically along with contract construction each time
#0 - 0xleastwood
2022-01-23T05:36:23Z
Duplicate of #185