Platform: Code4rena
Start Date: 25/11/2021
Pot Size: $80,000 USDC
Total HM: 35
Participants: 32
Period: 7 days
Judge: GalloDaSballo
Total Solo HM: 27
Id: 59
League: ETH
Rank: 5/32
Findings: 8
Award: $5,576.33
🌟 Selected for report: 8
🚀 Solo Findings: 4
🌟 Selected for report: thank_you
Also found by: 0x0x0x, Koustre, Meta0xNull, WatchPug, cmichel, defsec, harleythedog, hyh, leastwood, pauliax, pmerkleplant, tabish, xYrYuYx
harleythedog
The buyMalt
and sellMalt
functions in the UniswapHandler.sol do not have any slippage protection, which make them vulnerable to a sandwich attack. An attacker can strategically manipulate a uniswap pool before these functions are called, forcing the contract to buy/sell at an inflated price. The attacker can then undo their manipulation and end up with more tokens than they started with. This is a common issue and has been marked as a medium finding on previous C4 reports.
The fix for this is to set the amountOutMin parameter correctly in the uniswap calls, instead of just leaving it as 0.
Also, see M-01 on this previous C4 report - it is the exact same issue: https://ipfs.io/ipfs/bafybeicjla2h26q3wz4s344bsrtvhkxr3ypm44owvrzyorb2t6tcptlmem/C4%20Slingshot%20report.pdf
Inspection
Add slippage protection to mititgate sandwich attack thefts
#0 - 0xScotch
2021-12-10T00:21:34Z
#219
#1 - GalloDaSballo
2022-01-23T16:39:40Z
Duplicate of #219
🌟 Selected for report: 0x0x0x
Also found by: harleythedog, hyh
harleythedog
There is an admin function setRewardToken
in AbstractRewardMine.sol that allows changing the reward token of the contract. If there are unclaimed rewards in the contract when this function is called, then users should be transferred these rewards first before the new reward token is set. If this doesn't happen, then the users will miss out on their reward since the last time they called withdrawAll
. If the users have a lot of unclaimed reward, the worse this problem is.
See setRewardToken
here: https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/AbstractRewardMine.sol#L273
Notice that there is no consideration for the currently unclaimed rewards in the contract.
Inspection
Add logic to setRewardToken
to allow users to claim their owed old reward tokens before the new reward token is set.
#0 - 0xScotch
2021-12-08T12:44:55Z
#285
#1 - GalloDaSballo
2022-01-22T17:17:03Z
Duplicate of #285
🌟 Selected for report: harleythedog
1103.7569 USDC - $1,103.76
harleythedog
In AuctionParticipant.sol, the function setReplenishingIndex
is an admin function that allows manually setting replenishingIndex
. As I have shown in my two previous findings, I believe that this function could be called frequently. In my opinion (and Murphy's law would agree), this implies that eventually an admin will accidentally set replenishingIndex
incorrectly with this function.
Right now, setReplenishingIndex
does not allow the admin to set replenishingIndex
to a value smaller than it currently is. So, if an admin were to accidentally set this value too high, then it would be impossible to set it back to a lower value (the higher the value set, the worse this issue). All of the unclaimed tokens on auctions at smaller indices would be locked forever.
See code for setReplenishingIndex
here: https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/AuctionParticipant.sol#L132
Inspection
Remove the require statement on line 136, so that an admin can set the index to a smaller value.
#0 - GalloDaSballo
2022-01-13T01:04:47Z
Agree with the finding in that if the ADMIN
were to increase the replenishingIndex
then the unclaim tokens at auctions below the index wouldn't be claimable anymore.
I believe the warden properly highlighted what an hypothetical abuse of admin privilege
would look like.
As such I'll rate the finding with medium severity.
I don't necessarily agree with the warden recommended mitigation, it may actually be best to simply delete the setter, or force it to go up by one index at a time after checking that all tokens are claimed
🌟 Selected for report: harleythedog
1103.7569 USDC - $1,103.76
harleythedog
There are several ERC20 tokens that take a small fee on transfers/transferFroms (known as "fee-on-transfer" tokens). Most notably, USDT is an ERC20 token that has togglable transfer fees, but for now the fee is set to 0 (see the contract here: https://etherscan.io/address/0xdAC17F958D2ee523a2206206994597C13D831ec7#code). For these tokens, it should not be assumed that if you transfer x
tokens to an address, that the address actually receives x
tokens. In the current test environment, DAI is the only collateralToken
available, so there are no issues. However, it has been noted that more pools will be added in the future, so special care will need to be taken if fee-on-transfer tokens (like USDT) are planned to be used as collateralTokens
.
For example, consider the function purchaseArbitrageTokens
in Auction.sol. This function transfers realCommitment
amount of collateralToken
to the liquidityExtension, and then calls purchaseAndBurn(realCommitment)
on the liquidityExtension. The very first line of purchaseAndBurn(amount)
is require(collateralToken.balanceOf(address(this)) >= amount, "Insufficient balance");
. In the case of fee-on-transfer tokens, this line will revert due to the small fee taken. This means that all calls to purchaseArbitrageTokens
will fail, which would be very bad when the price goes below peg, since no one would be able to participate in this auction.
See purchaseArbitrageTokens
here: https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Auction.sol#L177
See purchaseAndBurn
here: https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/LiquidityExtension.sol#L117
Inspection
Add logic to transfers/transferFroms to calculate exactly how many tokens were actually sent to a specific address. In the example given with purchaseArbitrageTokens
, instead of calling purchaseAndBurn
with realCommitment
, the contract should use the difference in the liquidityExtension balance after the transfer minus the liquidityExtension balance before the transfer.
#0 - GalloDaSballo
2022-01-23T15:48:51Z
Agree with the finding, the system as a whole seem to not deal with feeOnTransfer Token Mitigation can be as simple as never using them, or refactoring to check for actual amounts received
🌟 Selected for report: harleythedog
1103.7569 USDC - $1,103.76
harleythedog
In AuctionParticpant.sol, every time purchaseArbitrageTokens
is called, the current auction is pushed to auctionIds
. If this function were to be called on the same auction multiple times, then the same auction id would be pushed multiple times into this array, and the claim
function would have issues with replenishingIndex
.
Specifically, even if replenishingIndex
was incremented once in claim
, it is still possible that the auction at the next index will never reward any more tokens to the participant, so the contract would need manual intervention to set replenishingIndex
(due to the if statement on lines 79-82 that does nothing if there is no claimable yield).
It is likely that purchaseArbitrageTokens
would be called multiple times on the same auction. In fact, the commented out code for handleDeficit
(in ImpliedCollateralService.sol) even suggests that the purchases might happen within the same transaction. So this issue will likely be an issue on most auctions and would require manual setting of replenishingIndex
.
NOTE: This is a separate issue from the one I just submitted previously relating to replenishingIndex
. The previous issue was related to an edge case where replenishingIndex
might need to be incremented by one if there are never going to be more claims, while this issue is due to duplicate auction ids.
See code for purchaseArbitrageTokens
here: https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/AuctionParticipant.sol#L40
Notice that currentAuction
is always appended to auctionIds
.
Inspection
Add a check to the function to purchaseArbitrageTokens
to ensure that duplicate ids are not added. For example, this can be achieved by changing auctionIds to a mapping instead of an array.
#0 - GalloDaSballo
2022-01-23T16:38:55Z
I agree with the finding, adding the same auctionId
can lead to undefined behaviour. This can break claiming of incentives. Because of that, I think medium severity to be appropriate
🌟 Selected for report: harleythedog
1103.7569 USDC - $1,103.76
harleythedog
In Bonding.sol, the internal function _unbondAndBreak
transfers a user's stake tokens to the dexHandler and then calls removeLiquidity
on the dexHandler. Within the Uniswap handler (which is the only handler so far) removeLiquidity
takes special care in the edge case where router.removeLiquidity
returns zero tokens. Specifically, the Uniswap handler has this code:
if (amountMalt == 0 || amountReward == 0) { liquidityBalance = lpToken.balanceOf(address(this)); lpToken.safeTransfer(msg.sender, liquidityBalance); return (amountMalt, amountReward); }
If this edge case does indeed happen (i.e. if something is preventing the Uniswap router from removing liquidity at the moment), then the Uniswap handler will transfer the LP tokens back to Bonding.sol. However, Bonding.sol does not have any logic to recognize that this happened, so the LP tokens will become stuck in the contract and the user will never get any of their value back. This could be very bad if the user unbonds a lot of LP and they don't get any of it back.
See _unbondAndBreak
here: https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Bonding.sol#L226
Notice how the edge case where amountMalt == 0 || amountReward == 0
is not considered in this function, but it is considered in the Uniswap handler's removeLiquidity
here: https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/DexHandlers/UniswapHandler.sol#L240
Inspection.
Add a similar edge case check to _unbondAndBreak
. In the case where LP tokens are transferred back to Bonding.sol instead of malt/reward, these LP tokens should be forwarded back to the user since the value is rightfully theirs.
#0 - GalloDaSballo
2022-01-26T13:36:16Z
In the then
case the tokens will be sent back to the Bonding.sol
contract, which has no way of rescuing or returning the tokens, probably reverting would be a better solution.
Because the warden identified a way for tokens to get stucked, I think Medium Severity to be appropriate
🌟 Selected for report: harleythedog
367.919 USDC - $367.92
harleythedog
In Auction.sol, there is a privileged function amendAccountParticipation
. Presumably, this function allows a trusted party to decrease a user's participation in an auction for acting maliciously (maybe there are other reasons for this function as well). However, if the auction is not currently finalized, then auction.finalPrice
will be 0, and amendAccountParticipation
will revert due to a division by zero on line 800. This means that the user's participation cannot be amended until the auction is finalized. This restricts the time frame that the admins can reduce the user's participation, which increases the chances that the user will get away with some vested reward tokens that are potentially malicious.
See amendAccountParticipation
here: https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Auction.sol#L786
Notice that there may be a division by zero on line 800 if the auction is not finalized, which will always revert.
Inspection
In the case where auction.finalPrice
is zero, just let the function return early, since unclaimedArbTokens
does not need to be altered in this case. This is already what is done in other functions, like userClaimableArbTokens
.
#0 - 0xScotch
2021-12-03T19:04:10Z
amendAccountParticipation
is only called by the auction escape hatch contract which enforces the auction is over. Adding a guard against finalPrice
being 0 is worth doing though as a fail safe.
#1 - GalloDaSballo
2022-01-23T15:56:46Z
I don't think the warden has identified any particular vulnerability. Just a hedge case for the logic. The logic is also inconsistent with other parts of the code where the 0 value is handled without a revert. For this reasons I think the finding is valid, but low severity (inconsistent code)
🌟 Selected for report: harleythedog
367.919 USDC - $367.92
harleythedog
In AuctionParticipant.sol, the claim
function is called to claim arb tokens from auctions the participant has entered. This is achieved through the global variable replenishingIndex
which keeps track of which auction claim
should be claiming from next. The logic for incrementing replenishingIndex
is at the end of claim.
I agree with the current logic at the end of the function. The comment on lines 96/97 says "Don't increment replenishingIndex if replenishingAuctionId == auctionId as claimable could be 0 due to the debt not being 100% replenished". Notice the keyword "could" - it is possible that replenishingAuctionId == auctionId but we will never be able to claim any more arb tokens from this contract, and in this case replenishingIndex
will NOT be incremented.
In this case, all subsequent calls to claim
will simply do nothing. Line 77 will have claimableTokens
be 0, and then the function will immediately return since it thinks it needs to wait longer to get more tokens, which will never happen. In this case, a manual intervention by an admin would be required to set replenishingIndex', which is obviously annoying and should be avoided. Since
claimis an external function, a malicious user/troll could intentionally call
claim` at the worst times to trigger this issue to happen. In this case, manual intervention would be required quite often.
The following logic should be added immediately after line 77 to account for this issue:
if (claimableTokens == 0 && replenishingId > auctionId) { // in this case, we will never receive any more tokens from this auction replenishingIndex = replenishingIndex + 1; auctionId = auctionIds[replenishingIndex]; }
// retry check for 0 claimable amount
See the code for claim
here: https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/AuctionParticipant.sol#L65
Other than manual intervention, the only place where replenishingIndex
is set is at the end of claim
.
Inspection
Add the code described above.
#0 - GalloDaSballo
2022-01-23T16:36:32Z
The warden has identified a situation where the system may break, fortunately the auction contract has a setter setAuctionReplenishId
This means that this loop can be broken by an admin call.
Because the warden didn't show a reliable way to get into this situation, and because the Sponsor can simply use the setter to fix it. I think Low Severity is more appropriate
🌟 Selected for report: ye0lde
Also found by: harleythedog
harleythedog
In Auction.sol, the storage variable currentAuctionId
is frequently used across the contract. In functions that read from this variable multiple times, it would save gas to copy it into a local memory variable first and then use the memory variable for calculations. This is because SLOAD operations are much more expensive than MLOAD operations.
For example, see _checkAuctionFinalization
here: https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Auction.sol#L746
It would save a lot of gas if currentAuctionId
was read from storage once at the start and then the local memory variable was used on subsequent reads.
Inspection
Cache currentAuctionId
reads when there are multiple reads in a function.
#0 - 0xScotch
2021-12-08T18:20:16Z
#89
#1 - GalloDaSballo
2021-12-27T22:37:40Z
Duplicate of #89
🌟 Selected for report: harleythedog
Also found by: pmerkleplant
25.436 USDC - $25.44
harleythedog
In ForfeitHandler.sol, there are two values swingTraderRewardCut
and treasuryRewardCut
, and these values always sum to 1000. Instead of having to go through all of the logic of setting these values independently and always ensuring that they sum to 1000, it would be simpler (and definitely save a lot of gas) if you simply removed everything related to treasuryRewardCut
and always just used 1000-swingTraderRewardCut
in its place.
This also is more similar to what is done in StabilizerNode.sol where treasuryCut
is simply what is left over after other components have taken their cut.
See ForfeitHandler.sol here: https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/ForfeitHandler.sol
Inspection
Simplify logic and save gas by removing treasuryRewardCut
.
#0 - GalloDaSballo
2021-12-31T17:04:08Z
Agree with finding, the variable is unused
🌟 Selected for report: harleythedog
56.5245 USDC - $56.52
harleythedog
In UniswapHandler.sol within the removeBuyer
function, there is a statement on line 308:
address buyer;
This variable is not used at all in the rest of the function, so this statement can be removed to save gas.
See statement here: https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/DexHandlers/UniswapHandler.sol#L308
Inspection
Remove unnecessary line to save gas
#0 - GalloDaSballo
2022-01-01T15:51:11Z
Agree that buyer
is unused