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: 3/32
Findings: 10
Award: $7,426.18
🌟 Selected for report: 6
🚀 Solo Findings: 6
🌟 Selected for report: thank_you
Also found by: 0x0x0x, Koustre, Meta0xNull, WatchPug, cmichel, defsec, harleythedog, hyh, leastwood, pauliax, pmerkleplant, tabish, xYrYuYx
leastwood
The purchaseAndBurn
function in LiquidityExtension
is called by the Auction
contract when users purchase arbitrage tokens. An attacker can monitor the blockchain for calls to this function and launch a sandwich attack to steal funds, assuming the attacker is a well-funded actor.
A malicious user is potentially able to steal rewards from users. As such, this issue should be considered medium risk as this results in loss of funds, but requires the attacker to be well-funded.
Consider the following scenario:
purchaseArbitrageTokens
.As shown above, attackers are able to siphon funds from users overtime.
https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Auction.sol#L184 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/LiquidityExtension.sol#L124 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/DexHandlers/UniswapHandler.sol#L131-L158
Manual code review Referenced Mochi review.
Consider performing slippage checks when users purchase arbitrage tokens by calling purchaseArbitrageTokens
. This can be done by simply performing a lower bounds check on the output amount received when exchanging rewards tokens for Malt tokens. This change would need to be added into the purchaseAndBurn
function in LiquidityExtension
.
#0 - 0xScotch
2021-12-10T00:21:59Z
#219
#1 - GalloDaSballo
2022-01-24T13:32:15Z
Duplicate of #219
leastwood
The notSameBlock
modifier is added to sensitive functions where flashloans may put the protocol at risk. Malt Finance has preemptively addressed these concerns by limiting msg.sender
from calling a set of functions multiple times within a single block. However, this can be bypassed if a flashloan attacker uses multiple contracts to act as separate EOA accounts. Therefore, notSameBlock
provides security by obscurity and should be replaced with a fully fledged solution.
https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Permissions.sol#L135-L141
Manual code review.
Consider checking that tx.origin == msg.sender
in order to prevent contracts from calling such sensitive functions. However, careful consideration needs to be made as smart wallets won't be able to call the affected functions. It is also important to note that tx.origin
can add additional security concerns if not implemented correctly.
#0 - 0xScotch
2021-12-08T11:39:52Z
#195
#1 - GalloDaSballo
2022-01-25T01:16:37Z
#195
leastwood
The permissionless stabilize
function in StabilizerNode
is called to correct deviations in the Malt token price. When the price of Malt has appreciated above its peg, the function simply distributes rewards to LP token holders, effectively diluting the total Malt token supply. As the _distributeSupply
function interacts with a Uniswap pool without performing slippage checks, an attacker can call this function and launch a sandwich attack in combination with a flash loan to steal funds.
A malicious user is potentially able to steal rewards that would otherwise be distributed to the protocol's users. As such, this issue should be considered high risk as this results in loss of funds.
Consider the following scenario:
stabilize
function in StabilizerNode
.StabilizerNode
contract buys rewards tokens at an inflated price.As shown above, attackers are able to siphon funds from users overtime.
https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/StabilizerNode.sol#L236 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/DexHandlers/UniswapHandler.sol#L160-L183
Manual code review Referenced Mochi review.
Consider performing slippage checks within the _distributeSupply
function. This can be done by simply performing a lower bounds check on the output amount received when exchanging Malt tokens for rewards tokens.
#0 - 0xScotch
2021-12-08T12:27:28Z
#56
#1 - GalloDaSballo
2022-01-23T15:46:22Z
Duplicate of #56
🌟 Selected for report: leastwood
1103.7569 USDC - $1,103.76
leastwood
purchaseArbitrageTokens
enables users to commit collateral tokens and in return receive arbitrage tokens which are redeemable in the future for Malt tokens. Each auction specifies a commitment cap which when reached, prevents users from participating in the auction. However, realCommitment
can be ignored by directly sending the LiquidityExtension
contract collateral tokens and subsequently calling purchaseArbitrageTokens
.
Consider the following scenario:
LiquidityExtension
contract.purchaseArbitrageTokens
with amount 0
.purchaseAndBurn
call returns a positive purchased
amount which is subsequently used in auction calculations.As a result, a user could effectively influence the average malt price used throughout the Auction
contract.
https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Auction.sol#L177-L214 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/LiquidityExtension.sol#L117-L128
Manual code review.
Consider adding a check to ensure that realCommitment != 0
in purchaseArbitrageTokens
.
#0 - GalloDaSballo
2022-01-24T13:41:01Z
The warden has identified a way to side-step the cap on commitments.
Because the commitments are used for calculating limits, but maltPurchased
is used to calculate rewards, an exploiter can effectively use an auction to purchase as many arbitrage tokens as they desire.
Using any amount
greater than zero will eventually allow to end the auction, however, by using 0 this process can be repeated continuously.
Agree with the finding and severity
🌟 Selected for report: leastwood
1103.7569 USDC - $1,103.76
leastwood
_startAuction
utilises the SwingTrader
contract to purchase Malt. If SwingTrader
has insufficient capital to return the price of Malt back to its target price, an auction is triggered with the remaining amount. However, no auction is triggered if the current auction exists, but msg.sender
is still rewarded for their call to stabilize
.
_shouldAdjustSupply
initially checks if the current auction is active, however, it does not check if the current auction exists. There is a key distinction between the auctionActive
and auctionExists
functions which are not used consistently. Hence, an auction which is inactive but exists would satisfy the edge case and result in triggerAuction
simply returning.
https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Auction.sol#L382-L386 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Auction.sol#L268-L272 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/StabilizerNode.sol#L342-L344 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Auction.sol#L873-L888
Manual code review.
Consider using auctionExists
and auctionActive
consistently in StabilizerNode
and Auction
to ensure this edge case cannot be abused.
#0 - GalloDaSballo
2022-01-24T13:55:10Z
The warden found a way to effectively mint free malt by spamming the caller incentive by abusing a specific edge case.
🌟 Selected for report: leastwood
1103.7569 USDC - $1,103.76
leastwood
_calculateMaltRequiredForExit
in AuctionEscapeHatch
currently uses Malt's spot price to calculate the quantity to return to the exiting user. This spot price simply tracks the Uniswap pool's reserves which can easily be manipulated via a flash loan attack to extract funds from the protocol.
https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/AuctionEscapeHatch.sol#L65-L92 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/AuctionEscapeHatch.sol#L193 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/DexHandlers/UniswapHandler.sol#L80-L109 https://shouldiusespotpriceasmyoracle.com/
Manual code review.
Consider implementing/integrating a TWAP oracle to track the price of Malt.
#0 - GalloDaSballo
2022-01-25T01:15:19Z
I feel like this issue highlights the design challenge that the sponsor will have to solve, on one hand the protocol is meant to stabilize the price of malt in specific pools (impossible to block / control every pool due to permissionless nature). At the same time in order to determine which direction to move the price to, they need to refer to the pricing of the underlying pools (in this case a UniV2Pool, most likely from QuickSwap)
Personally I understand the finding and think it's valid, however I don't believe there's easy answers as to how the sponsor should address this.
Whenever there's excess value there will be entities trying to seize it and perhaps through such a harsh environment this protocol can truly find a way to be sustainable.
That said, I'll mark the finding as valid, but believe this specific issue underlines the challenges that await the sponsor in making the protocol succesful
🌟 Selected for report: leastwood
1103.7569 USDC - $1,103.76
leastwood
addLiquidity
is called when users reinvest their tokens through bonding events. The RewardReinvestor
first transfers Malt and rewards tokens before adding liquidity to the token pool. addLiquidity
provides protections against slippage by a margin of 5%, and any dust token amounts are transferred back to the caller. In this instance, the caller is the RewardReinvestor
contract which further distributes the dust token amounts to the protocol's treasury. However, the token approval for this outcome is not handled properly. Dust approval amounts can accrue over time, leading to large Uniswap approval amounts by the UniswapHandler
contract.
https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/DexHandlers/UniswapHandler.sol#L212-L214 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/DexHandlers/UniswapHandler.sol#L216-L218
Manual code review.
Consider resetting the approval amount if either maltUsed < maltBalance
or rewardUsed < rewardBalance
in addLiquidity
.
#0 - GalloDaSballo
2022-01-25T01:32:08Z
The UniV2Router will first calculate the amounts and then pull them from the msg.sender
This means that approvals may not be fully utilized, leaving traces of approvals here and there. This can cause issues with certain tokens (USDT comes to mind), and will also not trigger gas refunds.
🌟 Selected for report: leastwood
1103.7569 USDC - $1,103.76
leastwood
_distributeRewards
attempts to reward LP token holders when the price of Malt exceeds its price target. Malt Finance is able to being Malt back to its peg by selling Malt and distributing rewards tokens to LP token holders. An external call to Auction
is made via the allocateArbRewards
function. Prior to this call, the StabilizerNode
approves the contract for a fixed amount of tokens, however, the allocateArbRewards
function does not necessarily utilise this entire amount. Hence, dust token approval amounts may accrue from within the StabilizerNode
contract.
https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/StabilizerNode.sol#L252-L253 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Auction.sol#L809-L871
Manual code review
Consider resetting the approval amount if the input rewarded
amount to allocateArbRewards
is less than the output amount.
#0 - GalloDaSballo
2022-01-25T01:36:59Z
Finding is valid, incorrect approvals can cause reverts (USDT being an example), and not fully resetting will also negate gas refunds
🌟 Selected for report: leastwood
1103.7569 USDC - $1,103.76
leastwood
MaltDataLab
integrates several MovingAverage
contracts to fetch sensitive data for the Malt protocol. Primary data used by the protocol consists of the real value for LP tokens, the average price for Malt and average reserve ratios. trackMaltPrice
, trackPoolReserves
and trackPool
are called by a restricted role denoted as the UPDATER_ROLE
and represented by an EOA account and not another contract. Hence, the EOA account must consistently update the aforementioned functions to ensure the most up-to-date values. However, miners can censor calls to MaltDataLab
and effectively extract value from other areas of the protocol which use stale values.
Consider the following attack vector:
stabilize
can be called by any user._stabilityWindowOverride
function is satisfied, hence the function will execute.exchangeRate
, queries maltPriceAverage
which may use an outdated exchange rate._startAuction
is executed which rewards msg.sender
with 100 Malt as an incentive for triggering an auction.stabilize
on every fastAveragePeriod
interval to extract incentive payments.UPDATER_ROLE
is willing to pay to call trackMaltPrice
, a user is able to sustain this attack.This threatens the overall stability of the protocol and should be properly handled to prevent such attacks. However, the fact that MaltDataLab
uses a series of spot price data points to calculate the MovingAverage
also creates an area of concern as well-funded actors could still manipulate the MovingAverage
contract by sandwiching calls to trackMaltPrice
, trackPool
and trackPoolReserves
.
trackMaltPrice
, trackPool
, and trackPoolReserves
should be added to the following areas of the code where applicable.
https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Bonding.sol#L159
https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Bonding.sol#L173
https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Bonding.sol#L177
https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Auction.sol#L881
https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Auction.sol#L710
https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/StabilizerNode.sol#L156
https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/StabilizerNode.sol#L190
https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/ImpliedCollateralService.sol#L105
Manual code review.
Consider adding calls to trackMaltPrice
, trackPoolReserves
and trackPool
wherever the values are impacted by the protocol. This should ensure the protocol is tracking the most up-to-date values. Assuming the cumulative values are used in the MovingAverage
contracts, then sensitive calls utilising MaltDataLab
should be protected from flashloan attacks. However, currently this is not the case, rather MovingAverage
consists of a series of spot price data points which can be manipulated by well-funded actors or via a flashloan. Therefore, there needs to be necessary changes made to MaltDataLab
to use cumulative price updates as its moving average instead of spot price.
#0 - 0xScotch
2021-12-08T11:35:09Z
Gas issues were the reason updates weren't inlined into paths that update critical values. However based on some thoughts from the team over the past few weeks and suggestions in other audit findings I think we can reduce gas enough to make it viable to inline it
#1 - GalloDaSballo
2022-01-22T14:40:01Z
However, the target chain for the deployment is Polygon, so unless they were to create custom validator code, and collude 1-1 with each validator (no flashbots on Polygon, yet) then the attack is increasingly more complex.
The attack is reliant on the externalities of the validator + somebody incentivized enough to exploit this.
I agree with he finding and believe after stabilize the system should auto-update the prices Because of the external requirements am downgrading to medium severity
🌟 Selected for report: cmichel
Also found by: 0x1f8b, Meta0xNull, WatchPug, defsec, jayjonah8, leastwood, stonesandtrees
leastwood
The Malt Finance smart contract suite incorporates a two-step deployment process whereby all contracts are first deployed and later setup via the initialize
function. However, it is possible for malicious actors to frontrun the initialization process to either take over the ownership of a given contract or disrupt the entire deployment process. The latter option results in significant and unrecoverable gas costs to the deployer and can be repeated as a form of DOS attack on the protocol.
0xScotch confirmed that the current testnet deployment is similar to how Malt Finance's contracts will be deployed on mainnet.
https://github.com/code-423n4/2021-11-malt/blob/main/src/scripts/2_testnet_deploy.ts
Manual code review.
Consider restricting the initialize
function to the original deployer of the contract. It may also be useful to revoke their role after the contract has been initialized. This should ensure that contracts cannot be compromised by malicious actors during the deployment process.
#0 - 0xScotch
2021-12-08T13:02:13Z
#245
#1 - GalloDaSballo
2022-01-22T14:24:40Z
Duplicate of #245
leastwood
ERC20Permit
uses block.chainId
in its DOMAIN_SEPERATOR
. This is only defined once from within the constructor and not recalculated on each call to ERC20Permit
. Hence, if a hard fork happens after contract deployment, the domain could become invalid due to the changed block.chainId
.
https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/ERC20Permit.sol#L23-L35
Previous audit contests.
A solution is provided by Sushiswap as an example to how this issue might be mitigated. https://github.com/sushiswap/trident/blob/concentrated/contracts/pool/concentrated/TridentNFT.sol#L47-L62
#0 - 0xScotch
2021-12-08T15:38:43Z
#349
#1 - GalloDaSballo
2022-01-24T00:42:11Z
Duplicate of #349