Platform: Code4rena
Start Date: 27/05/2022
Pot Size: $75,000 USDC
Total HM: 20
Participants: 58
Period: 7 days
Judge: GalloDaSballo
Total Solo HM: 15
Id: 131
League: ETH
Rank: 3/58
Findings: 4
Award: $7,181.24
๐ Selected for report: 2
๐ Solo Findings: 1
4098.7998 USDC - $4,098.80
The actual total supply of the token is random and depends on when _executeInflationRateUpdate
is executed.
The README
and tokenomic documentation clearly states that โThe token supply is limited to a total ofย 268435456ย tokens.โ. However when executing _executeInflationRateUpdate
, it first uses the current inflation rate to update the total available before checking if it needs to be reduced.
Therefore if no one mints or calls executeInflationRateUpdate
for some time around the decay point, the inflation will be updated using the previous rate so the totalAvailableToNow
will grow too much.
You should do
totalAvailableToNow += (currentTotalInflation * (block.timestamp - lastEvent));
Only if the condition block.timestamp >= lastInflationDecay + _INFLATION_DECAY_PERIOD
is false.
Otherwise you should do
totalAvailableToNow += (currentTotalInflation * (lastInflationDecay + _INFLATION_DECAY_PERIOD - lastEvent));โจ
Then update the rates, then complete with
totalAvailableToNow += (currentTotalInflation * (block.timestamp - lastInflationDecay + _INFLATION_DECAY_PERIOD));โจ
Note that as all these variables are either constants either already loaded in memory this is super cheap to do.
#0 - danhper
2022-06-06T15:18:32Z
I believe this should actually be high severity
#1 - GalloDaSballo
2022-06-19T16:55:10Z
The warden has identified the lack of an upper bound on the inflation math which would make it so that more than the expected supply cap of the token could be minted.
The sponsor agree that this should be of High Severity.
Because this breaks the protocol stated invariant of a specific cap of 268435456
tokens, I agree with High Severity
๐ Selected for report: Picodes
2732.5332 USDC - $2,732.53
https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/Minter.sol#L220 https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/tokenomics/InflationManager.sol#L559 https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/tokenomics/InflationManager.sol#L572 https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/tokenomics/InflationManager.sol#L586
When updating pool inflation rates, other pools see their currentRate
being modified without having poolCheckpoint
called, which leads to false computations.
This will lead to either users losing a part of their claims, but can also lead to too many tokens could be distributed, preventing some users from claiming due to the totalAvailableToNow
requirement in Minter
.
Imagine you have 2 AMM pools A and B, both with an ammPoolWeight
of 100, where poolCheckpoint
has not been called for a moment. Then, imagine calling executeAmmTokenWeight
to reduce the weight of A to 0.
Only A is checkpointed here, so when B will be checkpointed it will call getAmmRateForToken
, which will see a pool weight of 100 and a total weight of 100 over the whole period since the last checkpoint of B, which is false, therefore it will distribute too many tokens. This is critical has the minter expects an exact or lower than expected distribution due to the requirement of totalAvailableToNow
.
In the opposite direction, when increasing weights, it will lead to less tokens being distributed in some pools than planned, leading to a loss for users.
Checkpoint every LpStakerVault
, KeeperGauge
or AmmGauge
when updating the weights of one of them.
#0 - chase-manning
2022-06-07T13:13:28Z
We think this should be Medium as impact is quite minor
#1 - GalloDaSballo
2022-06-19T15:39:39Z
Because getAmmRateForToken
uses the totalAmmTokenWeight
, then all Gauges will need to be poolCheckpoint
ed at the time the weight is changed-
This is because for systems that accrue points over time, any time the multiplier changes, the growth of points has changed, and as such a new accrual needs to be performed.
I believe there's 3 ways to judge this finding:
poolCheckpoint
, as such if a true concern for fairness is made, anyone can front-run and back-run the update of weights with the goal of offering the most accurate math possibleGiven those considerations, I want to emphasize that not calling poolCheckpoint
can cause potentially drastic leaks of value, in a way similar to the original Sushi MasterChef's lack of massUpdatePools
could.
That said, I believe the finding is of medium severity as any individual could setup a bot to monitor and prevent this and it would require the weights to be changed by governance and impact can be quite minor as long as the pools are used
๐ Selected for report: IllIllI
Also found by: 0x1f8b, 0x29A, 0xNazgul, 0xf15ers, BowTiedWardens, Chom, Funen, Kaiziron, Kumpa, MiloTruck, Picodes, Ruhum, SecureZeroX, Sm4rty, SmartSek, StyxRave, WatchPug, Waze, asutorufos, bardamu, berndartmueller, c3phas, catchup, cccz, codexploder, cryptphi, defsec, delfin454000, dipp, fatherOfBlocks, gzeon, hake, hansfriese, hyh, masterchief, oyc_109, sach1r0, sashik_eth, shenwilly, simon135, unforgiven
119.8232 USDC - $119.82
Minter
, currentInflationAmountLp
, currentInflationAmountKeeper
, currentInflationAmountAmm
could be made internal.There are getters like getLpInflationRate
for these variables, so there is no need to make them public as it just creates duplicates view functions.
#0 - GalloDaSballo
2022-06-20T00:29:56Z
Agree with the advice, nice finding, wish the warden sent more
๐ Selected for report: IllIllI
Also found by: 0x1f8b, 0x29A, 0xKitsune, 0xNazgul, 0xf15ers, 0xkatana, Chom, Dravee, Fitraldys, Funen, Kaiziron, MiloTruck, Picodes, Randyyy, RoiEvenHaim, SecureZeroX, Sm4rty, SmartSek, StyxRave, Tadashi, Tomio, Waze, asutorufos, berndartmueller, c3phas, catchup, csanuragjain, defsec, delfin454000, djxploit, fatherOfBlocks, gzeon, hake, hansfriese, oyc_109, robee, sach1r0, sashik_eth, scaraven, simon135
230.0919 USDC - $230.09
Gauges
could mint directly to Minter
Currently all mint action pass through the InflationManager
which checks the access control using the mapping gauges
and then forward the call to the Minter
: see this code. It would be way more gas efficient considering the frequency of mints versus gauges
mapping updates to maintain identical mappings in InflationManager
and Minter
and to mint directly to Minter
.
This would just imply using an internal function in InflationManager
to make sure each time gauges
is updated it is also updated in Minter
.
Minter
, token
could be made immutableIn Minter
, token
is not immutable but can only be set once. I assume this is for simplicity as you want to deploy Minter
, BkdToken
and then set the address of BkdToken
in Minter
. But this overhead could easily be avoided by pre-computing the deployment address of BkdToken
so it could be set in the constructor of Minter
and be immutable. This would save a lot of gas during the whole contract lifecycle.
To precompute deployment addresses, you can use the CREATE2 opcode: check https://docs.openzeppelin.com/cli/2.8/deploying-with-create2 or https://medium.com/coinmonks/pre-compute-contract-deployment-address-using-create2-8c01e80ab7da.
This also applies to other places in the code like https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/tokenomics/InflationManager.sol#L56
Minter
, currentInflationAmountLp
, currentInflationAmountKeeper
, currentInflationAmountAmm
could be made internal.There are getters like getLpInflationRate
for these variables, so there is no need to make them public as it just creates duplicates view functions.
Minter
and BkdToken
could be mergedTo save expensive external calls, why not merging Minter
and BkdToken
into a single contract ? I think it doable from a contract size point of view, and references to one another are immutable so it would totally make sense to merge them.
VestedEscrow
, totalTime
could be made immutable.totalTime
is not changed during the contract lifetime so could be made immutable !
Minter
, there is no need to inherit ReentrancyGuard
In Minter
the keyword nonReentrant
is used only once here, but it is useless as there is only an external call to a trusted contract: the token, and no crucial states updates after this call.
#0 - GalloDaSballo
2022-06-17T22:32:56Z
Great idea, however in lack of POC and before / after I can't do anything but give it 0 gas saved
Agree with caching token
in Minter
, will save at least one cold SLOAD = 2100
Disagree with second comment
Will save deployment cost while removing certain functions / interface
I think the ideas are really good, but you're gonna have to code a POC for these to be considered
Similar to 3. Saves 2.1k
I'm going to disagree in lack of detailed POC, especially given the un-CEIness of https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/Minter.sol#L217
#1 - GalloDaSballo
2022-06-17T22:34:05Z
Total Gas Saved: 4200
To be honest had the warden written some POC for the other refactorings this may have been the shortest yet most effective report of the contest