Open code423n4 opened 2 years ago
Good catch , this issue is because Angle use the same function for claim veAsset and extra rewards
The warden has shown how Angle protocol will break certain invariants as the code assumes that claiming of veAsset
to always be separate from claiming of additionalRewards
.
Due to this any additional reward emitted by the Angle Gauge will be stuck in the claiming contract.
While impact is limited to loss of yield (loss of additional tokens), because the finding has broken the assumptions of the contract, meaning that Angle Protocol should not be integrated without a fix, I believe High Severity to be appropriate
Upon further review, we may raise the concern of the contract being out of scope.
However, given that:
I believe the finding is of High Severity
Lines of code
https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/Booster.sol#L495
Vulnerability details
Proof-of-Concept
In this example, assume the following Angle's gauge setup
To collect the gauge rewards, users would trigger the
Booster._earmarkRewards
function to claim veAsset and extra rewards from a gauge.Per the code logic, the function will attempt to execute the following two key operations:
1) First Operation - Claim the veAsset by calling
VoterProxy.claimVeAsset
. Call Flow as follow:VoterProxy.claimVeAsset() > IGauge(_gauge).claim_rewards()
. 2) Second Operation - Claim extra rewards by callingExtraRewardStashV3.claimRewards
. Call flow as follows:ExtraRewardStashV3.claimRewards > Booster.claimRewards > VoterProxy.claimRewards > IGauge(_gauge).claim_rewards()
.Note that
IGauge(_gauge).claim_rewards()
will claim all available reward tokens from the Angle's gauge.https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/Booster.sol#L495
First Operation - Claim the veAsset
Since this is a Angle Deployment, when the
VoterProxy.claimVeAsset
is triggered, it will go through the if-else logic (escrowModle == IVoteEscrow.EscrowModle.ANGLE
) and executeIGauge(_gauge).claim_rewards()
, and all rewards tokens will be sent toVoterProxy
contract. Assume that100 ANGLE
and100 DAI
were received.Note that in this example, we have two reward tokens (ANGLE and DAI). Additionally, gauge redirection was not configured on the gauge at this point, thus the gauge rewards will be sent to the caller, which is the
VoterProxy
contract.Subsequently, the code
IERC20(veAsset).safeTransfer(operator, _balance);
will be executed, and veAsset (100 ANGLE
) reward tokens will be transferred to theBooster
contract for distribution. However, the100 DAI
reward tokens will remain stuck in theVoterProxy
contract. As such, users will not be able to get any reward tokens (e.g. DAI, WETH) except veAsset (ANGLE) tokens from the gauges.https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/VoterProxy.sol#L224
Following is Angle's Gauge Contract for reference:
https://github.com/AngleProtocol/angle-core/blob/4d854e0d74be703a3707898f26ea2dd4166bc9b6/contracts/staking/LiquidityGaugeV4.vy#L344
(Mainnet Deployed Address: https://etherscan.io/address/0x8E2c0CbDa6bA7B65dbcA333798A3949B07638026)
Second Operation - Claim extra rewards
After the
IStaker(staker).claimVeAsset(gauge);
code within theBooster._earmarkRewards
function is executed,IStash(stash).claimRewards();
andIStash(stash).processStash();
functions will be executed next.stash
==ExtraRewardStashV3
.The
ExtraRewardStashV3.claimRewards
will call theBooster.setGaugeRedirect
first so that all the gauge rewards will be redirected toExtraRewardStashV3
stash contract. Subsequently,ExtraRewardStashV3.claimRewards
will triggerBooster.claimRewards
to claim the gauge rewards from the Angle's gauge.Note that this is the second time the contract attempts to claim gauge rewards from the gauge. Thus, no gauge rewards will be received since we already claimed them earlier. Next,
ExtraRewardStashV3
will attempt to process all the tokens stored in its contract and send them to the respective reward contracts for distribution to the users. However, the contract does not have any tokens stored in it because the earlier attempt to claim gauge rewards return nothing.As we can see, the DAI reward tokens are still stuck in the
VoterProxy
contract at this point.https://github.com/AngleProtocol/angle-core/blob/4d854e0d74be703a3707898f26ea2dd4166bc9b6/contracts/staking/LiquidityGaugeV4.vy#L332
https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/ExtraRewardStashV3.sol#L61
Impact
User's gauge rewards are frozen/stuck in
VoterProxy
contract. Additionally, there is no method to sweep/collect the reward tokens stuck in theVoterProxy
contract.Recommended Mitigation Steps
Consider triggering
Booster.setGaugeRedirect
during the deployment to set gauge redirection to stash contract (ExtraRewardStashV3
) so that the Angle's gauge rewards will not be redirected toVoterProxy
contract and get stuck there.Alternatively, update the
Booster._earmarkRewards
to as follows:There is no need to specifically call
VoterProxy.claimVeAsset
to fetch ANGLE for Angle Protocol because callingIStash(stash).claimRewards()
will fetch both ANGLE and other reward tokens from the gauge anyway. When the stash contract receives the ANGLE tokens, it will automatically transfer all of them back toBooster
contract whenIStash(stash).processStash()
is executed. TheIStash(stash).claimRewards()
function also performs a sanity check to ensure that the gauge redirection is pointing to itself before claiming the gauge rewards, and automatically configure them if it is not, so it will not cause the reward tokens to get stuck inVoterProxy
contract.Curve uses an older version of LiquidityGauge contract. Thus, two calls are needed (
Minter.mint
to claim CRV andLiquidityGauge.claim_rewards
to claim other rewards).Angle uses newer version of LiquidityGauge (V4) contract that just need one function call (
LiquidityGauge.claim_rewards
) to fetch both veAsset and other rewards.IDLE uses LiquidityGauge (V3) contract. veAsset (IDLE) is minted by calling
DistributorProxy.distribute
and gauge rewards are claimed by callingLiquidityGauge.claim_rewards
.Due to the discrepancies between different protocols in the reward claiming process, additional care must be taken to ensure that the flow of veAsset and gauge rewards are transferred to the appropriate contracts during integration. Otherwise, rewards will be stuck.
Lastly, I only see test cases written for claiming veAsset from the gauge. For completeness, it is recommended to also write test cases for claiming extra rewards from the gauge apart from veAsset.