Platform: Code4rena
Start Date: 21/04/2022
Pot Size: $75,000 USDC
Total HM: 7
Participants: 45
Period: 7 days
Judge: 0xean
Total Solo HM: 5
Id: 111
League: ETH
Rank: 4/45
Findings: 1
Award: $9,375.00
🌟 Selected for report: 1
🚀 Solo Findings: 1
🌟 Selected for report: smiling_heretic
https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L214 https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L257 https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L248 https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L465-L469
The impact depends really on how gauges are used by other contracts.
The most obvious consequence I can imagine is that some other contract distributes rewards based on calculateGaugeAllocation
. However, because _getStoredWeight(_totalWeight, currentCycle)
is now larger than the real total sum of weights, all rewards are smaller than they should be (because of larger denominator total
).
There can be also (potentially large) leftover amount of rewards that is never distributed because now sum of calculateGaugeAllocation(gauge, quantity)
over all gauges with constant quantity
is less than quantity
. So value might be lost.
I added this test (modified testCalculateGaugeAllocation
) to ERC20GaugesTest.t.sol
and it passes.
function testExploit() public { token.mint(address(this), 100e18); token.setMaxGauges(2); token.addGauge(gauge1); require(token.incrementGauge(gauge1, 1e18) == 1e18); require(token.incrementGauge(gauge2, 1e18) == 2e18); // gauge added after incrementing... token.addGauge(gauge2); hevm.warp(3600); // warp 1 hour to store changes require(token.calculateGaugeAllocation(gauge1, 150e18) == 50e18); require(token.calculateGaugeAllocation(gauge2, 150e18) == 50e18); // expected value would be 2e18 require(token.totalWeight() == 3e18); require(token.incrementGauge(gauge2, 2e18) == 4e18); // ensure updates don't propagate until stored require(token.calculateGaugeAllocation(gauge1, 150e18) == 50e18); require(token.calculateGaugeAllocation(gauge2, 150e18) == 50e18); hevm.warp(7200); // warp another hour to store changes again require(token.calculateGaugeAllocation(gauge1, 125e18) == 25e18); require(token.calculateGaugeAllocation(gauge2, 125e18) == 75e18); // expected value would be 4e18 require(token.totalWeight() == 5e18); }
As we can see, we can call token.incrementGauge(gauge2, 1e18)
before token.addGauge(gauge2)
. This is because this check doesn't revert for gauges that were never added in the first place.
First time the total weight is incremented in _incrementUserAndGlobalWeights
and 2nd time here.
If corrupting state like this is adventurous for someone, he can frontrun token.addGauge
called by the admin with a call to incrementGauge
which is permissionless.
Foundry
Use condition _gauges.contains(gauge) && !_deprecatedGauges.contains(gauge)
to check if a gauge can be incremented instead of just !_deprecatedGauges.contains(gauge)
. There's a function isGauge
in the contract that does exactly this.
#0 - Joeysantoro
2022-04-27T03:16:09Z
#1 - 0xean
2022-05-19T23:19:32Z
I think this is different enough from #5 to warrant its own issue and stand alone. Happy to discuss further with sponsor if they are adamant it's a dupe.
#2 - thomas-waite
2022-07-14T16:54:07Z
This is caused by and is a unique symptom of the same underlying issue as M-03