Platform: Code4rena
Start Date: 11/05/2022
Pot Size: $150,000 USDC
Total HM: 23
Participants: 93
Period: 14 days
Judge: LSDan
Total Solo HM: 18
Id: 123
League: ETH
Rank: 6/93
Findings: 3
Award: $6,739.60
π Selected for report: 1
π Solo Findings: 1
π Selected for report: csanuragjain
Also found by: hyh, kirk-baird
1400.9659 USDC - $1,400.97
During AuraClaimZap._claimExtras()
if the option LockCvx
is set to false
then the contract will transfer CVX tokens from the msg.sender
to this
contract without forwarding them on to the user.
There is no way to retrieve these funds from the protocol and there they will be locked in the contract permanently.
In _claimExtra()
on line #224 there is a safeTransferFrom()
from the msg.sender
to this
for CVX. If LockCvx
is set to true
then this value will then be deposited in the AuraLocker on behalf of msg.sender
.
However, if LockCvx
the tokens are still transferred from the msg.sender
to this
but they are not forwarded to the AuraLocker contract. These tokens will remain in the AuraClaimZap and will be unable to be retrieved.
if (depositCvxMaxAmount > 0) { uint256 cvxBalance = IERC20(cvx).balanceOf(msg.sender).sub(removeCvxBalance); cvxBalance = AuraMath.min(cvxBalance, depositCvxMaxAmount); if (cvxBalance > 0) { //pull cvx IERC20(cvx).safeTransferFrom(msg.sender, address(this), cvxBalance); if (_checkOption(options, uint256(Options.LockCvx))) { IAuraLocker(locker).lock(msg.sender, cvxBalance); } } } } }
Consider changing the location of the transfer which pulls CVX to be inside of the if statement which checks if the funds should be locked e.g.
if (_checkOption(options, uint256(Options.LockCvx))) { //pull cvx IERC20(cvx).safeTransferFrom(msg.sender, address(this), cvxBalance); IAuraLocker(locker).lock(msg.sender, cvxBalance); }
#0 - 0xMaharishi
2022-05-28T11:39:21Z
#108
#1 - 0xahtle7
2022-05-29T16:58:27Z
#2 - dmvt
2022-06-20T17:34:05Z
Duplicate of #108
π Selected for report: kirk-baird
5188.7624 USDC - $5,188.76
https://github.com/code-423n4/2022-05-aura/blob/4989a2077546a5394e3650bf3c224669a0f7e690/contracts/AuraLocker.sol#L176-L177 https://github.com/code-423n4/2022-05-aura/blob/4989a2077546a5394e3650bf3c224669a0f7e690/contracts/AuraLocker.sol#L802-L814 https://github.com/code-423n4/2022-05-aura/blob/4989a2077546a5394e3650bf3c224669a0f7e690/contracts/AuraLocker.sol#L864
There is a potential overflow in the rewards calculations which would lead to updateReward()
always reverting.
The impact of this overflow is that all reward tokens will be permanently locked in the contract. User's will be unable to call any of the functions which have the updateReward()
modifier, that is:
lock()
getReward()
_processExpiredLocks()
_notifyReward()
As a result the contract will need to call shutdown()
and the users will only be able to receive their staked tokens via emergencyWithdraw()
, which does not transfer the users the reward tokens.
Note that if one reward token overflows this will cause a revert on all reward tokens due to the loop over reward tokens.
The overflow may occur due to the base of values in _rewardPerToken()
.
function _rewardPerToken(address _rewardsToken) internal view returns (uint256) { if (lockedSupply == 0) { return rewardData[_rewardsToken].rewardPerTokenStored; } return uint256(rewardData[_rewardsToken].rewardPerTokenStored).add( _lastTimeRewardApplicable(rewardData[_rewardsToken].periodFinish) .sub(rewardData[_rewardsToken].lastUpdateTime) .mul(rewardData[_rewardsToken].rewardRate) .mul(1e18) .div(lockedSupply) ); }
The return value of _rewardPerToken()
is in terms of
(now - lastUpdateTime) * rewardRate * 10**18 / totalSupply
Here (now - lastUpdateTime)
has a maximum value of rewardDuration = 6 * 10**5
.
Now rewardRate
is the _reward.div(rewardsDuration)
as seen in _notifyRewardAmount()
on line #864. Note that rewardDuration
is a constant 604,800.
rewardDuration = 6 * 10**5
Thus, if we have a rewards such as AURA or WETH (or most ERC20 tokens) which have units 10**18 we can transfer 1 WETH to the reward distributor which calls _notifyRewardAmount()
and sets the reward rate to,
rewardRate = 10**18 / (6 * 10**5) ~= 10**12
Finally, if this attack is run either by the first depositor they may lock()
a single token which would set totalSupply = 1
.
Therefore our equation in terms of units will become,
(now - lastUpdateTime) * rewardRate * 10**18 / totalSupply => 10**5 * 10**12 * 10**18 / 1 = 10**35
In since rewardPerTokenStored
is a uint96
it has a maximum value of 2**96 ~= 7.9 * 10**28
. Hence there will be an overflow in newRewardPerToken.to96()
. Since we are unable to add more total supply due to lock()
reverting there will be no way to circumvent this revert except to shutdown()
.
uint256 newRewardPerToken = _rewardPerToken(token); rewardData[token].rewardPerTokenStored = newRewardPerToken.to96();
Note this attack is described when we have a low totalSupply
. However it is also possible to apply this attack on a larger totalSupply
when there are reward tokens which have decimal places larger than 18 or tokens which such as SHIB which have small token value and so many of the tokens can be bought for cheap.
To mitigation this issue it is recommended to increase the size of the rewardPerTokenStored
. Since updating this value will require another slot to be used we recommend updating this to either uint256
or to update both rewardRate
and rewardPerTokenStored
to be uint224
.
#0 - liveactionllama
2022-05-25T00:25:43Z
Warden reached out via C4 help request (May 24 at 23:50 UTC) and asked that we add this additional information to their submission:
This issue was always be present if the staked token is one with a low number of decimal places such as USDC or USDT which have 6 decimal places. This is because the
totalSupply
will be limited in size by the decimal places of thestakingToken
.
#1 - 0xMaharishi
2022-05-28T11:35:20Z
Given that the staked token will have 18 decimals (it's the aura token) and there will be at least 1e21 units in there before any rewards come, it would take a number of tokens equal to 7.9e49 to be distributed to get this overflow.
I think that while this is certainly a possibility, given that that it would take an orchestrated governance attack and wouldn't necessarily put any funds at risk. That said, a solid mitigation would be to enforce rewardRate < 1e17
in the notifyRewardAmount, therefore it would never be possible for this to happen
#2 - 0xMaharishi
2022-05-28T11:35:55Z
IMO this should be a medium risk
#3 - 0xMaharishi
2022-05-30T21:02:38Z
#4 - dmvt
2022-06-20T17:55:22Z
Agree with sponsor about the downgrade to medium. This requires external factors to be an issue, including potential governance collusion in the attack.
π Selected for report: IllIllI
Also found by: 0x1f8b, 0x4non, 0xNazgul, 0xNineDec, 0xf15ers, 0xkatana, 242, AlleyCat, BouSalman, BowTiedWardens, CertoraInc, Chom, Cityscape, FSchmoede, Funen, GimelSec, Hawkeye, JC, JDeryl, Kaiziron, Kthere, Kumpa, MaratCerby, MiloTruck, Nethermind, NoamYakov, PPrieditis, QuantumBrief, Rolezn, Ruhum, SmartSek, SooYa, Tadashi, TerrierLover, WatchPug, Waze, _Adam, asutorufos, berndartmueller, bobirichman, c3phas, catchup, cccz, ch13fd357r0y3r, cryptphi, csanuragjain, cthulhu_cult, defsec, delfin454000, ellahi, fatherOfBlocks, hansfriese, hubble, hyh, jayjonah8, joestakey, kenta, kenzo, kirk-baird, mics, oyc_109, p_crypt0, reassor, robee, sach1r0, samruna, sashik_eth, sikorico, simon135, sorrynotsorry, sseefried, tintin, unforgiven, z3s, zmj
149.8668 USDC - $149.87
There is no upper bound on the size of the array rewardTokens
in AuraLocker
.
Since this array is iterated in multiple locations such as updateReward()
, getReward()
, claimableRewards()
it is possible to end up in a state where there are too many reward tokens such that the contract cannot be iterated over them within the block gas limit.
The impact is that no rewards will be able to be withdrawn from the contract since the array rewardTokens
is required to be iterated over in order to withdraw rewards.
This issue is rated Medium rather than High since addReward()
which increases the size of this array is controlled via the DAO.
There are no restrictions on the number of tokens that can be pushed to rewardTokens
in addReward()
.
function addReward(address _rewardsToken, address _distributor) external onlyOwner { require(rewardData[_rewardsToken].lastUpdateTime == 0, "Reward already exists"); require(_rewardsToken != address(stakingToken), "Cannot add StakingToken as reward"); rewardTokens.push(_rewardsToken); rewardData[_rewardsToken].lastUpdateTime = uint32(block.timestamp); rewardData[_rewardsToken].periodFinish = uint32(block.timestamp); rewardDistributors[_rewardsToken][_distributor] = true; }
Consider having a configurable upper bound that is set by the DAO to prevent accidental increase in the number of rewards tokens to a point where they cannot be iterated in the block gas limit.
#0 - dmvt
2022-06-24T23:53:00Z
Downgrading to QA. While this is a valid report, the ability to add reward tokens is tightly controlled making this highly unlikely.