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: 9/58
Findings: 3
Award: $2,976.36
🌟 Selected for report: 1
🚀 Solo Findings: 1
🌟 Selected for report: fatherOfBlocks
2732.5332 USDC - $2,732.53
In the _calcTotalClaimable() function it should be validated that perPeriodTotalFees[i] != 0 since otherwise it would generate a DoS in claimableRewards() and claimRewards(). This would be possible since if advanceEpoch() or kill() is executed by the InflationManager address, the epoch will go up without perPeriodTotalFees[newIndexEpoch] is 0. The negative of this is that every time the InflationManager executes these two methods (kill() and advanceEpoch()) DoS is generated until you run reportFees(). Another possible case is that kill() or advanceEpoch() are executed 2 times in a row and there is no way of a perPeriodTotalFees[epoch-1] updating its value, therefore it would be an irreversible DoS.
Generate a behavior for the case that perPeriodTotalFees[i] == 0.
#0 - GalloDaSballo
2022-06-18T00:33:07Z
The finding at face value is valid and it's impact is a DOS.
A division by zero in keeperRecords[beneficiary].feesInPeriod[i].scaledDiv(perPeriodTotalFees[i])
, will cause an instantaneous revert making it impossible to claim fees.
After more reading I believe that the finding is valid and can cause issues if there's one epoch without fees, if for any reason the protocol skips one epoch via advanceEpoch
, while no perPeriodTotalFees
are increased, then, because keeperRecords[beneficiary].nextEpochToClaim
will include the empty epoch, the beneficiary
will not be able to receive fees.
Given that setup is necessary for this to happen, I believe Medium Severity to be more appropriate
🌟 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
159.0051 USDC - $159.01
AddressProvider.sol
L93/117 - It is validated that pool != address(0) but actually pool is needed to put it inside the ILiquidityPool interface, therefore, the best thing would be to request the interface directly in the signature, like this: addPool(ILiquidityPool pool) and removePool(ILiquidityPool pool). In this way, it would be avoided to validate that pool != address(0), since address(0) does not comply with the ILiquidityPool interface.
L288 - An address is requested in the addStakerVault() function and as soon as the function starts it is put inside the IStakerVault interface, therefore, the best would be to request the interface directly in the signature, like this: addStakerVault(IStakerVault stakerVault). This way it would avoid starting to execute inside the function.
Controller.sol
L33 - The setInflationManager() function performs two validations, the second would not be necessary if an IInflationManager is requested directly in the signature (as is done in the constructor). This would also have the benefit of not being wrapear on line 36.
L39 - The addStakerVault() function has as its first validation, return false if this validation is true (!addressProvider.addStakerVault(stakerVault)), The problem with this validation is that in the implementation of AddressProvider it never returns false, therefore the validation is not necessary (it is also immutable, therefore it can only be modified in the deploy).
L121/123/127/129 - The code of the function getTotalEthRequiredForGas() would be much cleaner if the signature contains the creation of the variable (returns (uint256 totalEthRequired)), in this way the creation of the variable in line 123 and the final return would be avoided.
contracts/zaps/PoolMigrationZap.sol
contracts/BkdLocker.sol
contracts/tokenomics/FeeBurner.sol
L25 - The WETH address is hardcoded, this implies that this code is only usable in a single network. If it's on testnet, it can't be deployed on mainnet. If it's on mainnet, you can't test it.
L79 - When the targetUnderlying_ variable is created, it is never validated that it is != address(0), this is important, since otherwise when swapAll() is executed they would be burning WETH.
contracts/tokenomics/KeeperGauge.sol
contracts/StakerVault.sol
L156/157 - First it should be validated that the src has an amount to transfer and then check if it needs allowance or not.
L185 - If a malicious address is approved and if the src wants to change the approve it has, the spender could front run it to spend that approve you have and end up with more allowance.
contracts/tokenomics/InflationManager.sol
#0 - GalloDaSballo
2022-06-21T17:01:30Z
I don't believe the finding to be valid, unless the Address library by OZ (which btw is not used in this contract) is doing the check, then the warden advice will actually accept address(0) as valid.
You will get a revert at line 101, which may or may not be what you would want in case of a 0 address
Arguably useful as you'd still get a revert when calling getToken
I believe the warden is under the assumption that using an Interface as the type will perform a zero check, but that is not the case per the demo below.
contract GasTest is DSTest { GasTestStorageInMappingGood c0; GasTestStorageInMappingBad c1; function setUp() public { c0 = new GasTestStorageInMappingGood(); } function testGas() public { c0.doSmth(IStuff(0x0000000000000000000000000000000000000000)); } } interface IStuff { function doStuff() external view; } contract GasTestStorageInMappingGood { IStuff public storedThing; function doSmth(IStuff theThing) external{ storedThing = theThing; } }
I don't believe the findings related to casting to interface are valid as they seem to be based on invalid assumptions as well as a lack of code
Agree, there will never be a false return there, only reverts, the check can be removed
Honestly really not a big deal and subjective as some people prefer explicit return and it's the same for the compiler
Agree that an early return makes sense
I don't believe this to be an issue nor necessary as Solidity >= 0.8.0 will do a division by zero check
While the recommendation to use immutable can be entertained, the rest of the claims are highly subject as inlining is always the cheapest way of storing data, and testing can be done on a fork-mainnet
I agree that the code may need refactoring, however the case of address(0) is handled inswap
, personally I'd recommend the sponsor to consider refactoring the entire flow to offer less flexibility while giving stronger "good paths"
Because of the interesting food for thought, I think the finding is valuable
Considering that the code is not upgradeable I agree that the unused variable could just be refactored away
##Â L156/157 - First it should be validated that the src has an amount to transfer and then check if it needs allowance or not. I'm not convinced as this boils down to opinion, you ultimately need to do both checks, so why prefer one over the other?
The inflationManager keeps tracks of gauges and the token only let's the inflationManager mints, I think this finding needed further development to be actionable
#1 - GalloDaSballo
2022-06-21T17:03:03Z
I'm conflicted on this report, the warden is trying and has original thoughts. Some of the findings may come from invalid assumptions. Lastly, the formatting could be improved by adding links to the files and line on each of the L80 making the report a lot more convenient to verify and take action on
🌟 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
84.8218 USDC - $84.82
RoleManager.sol
AddressProvider.sol
L64/71/98/102/176/185/199/241/242/260/270/295/296/325/428/434 - The require are too expensive, it would be saved using the Errors custom + if, instead of requires. Another option could be to use a private view function.
L101/102/103/118/119/121/122/123 - The get calls to other contracts, being a view method, do not generate any cost, therefore, it would be less expensive not to create variables and call the getter directly.
Controller.sol
L49/50/51/66/69/71 - The get calls to other contracts, being a view method, do not generate any cost, therefore, it would be less expensive not to create variables and call the getter directly.
L66 - A variable is created and gas is consumed to fill a variable that will only be used if the if in line 68 is true, since it is only used within the if, it should be created within the if.
BkdToken.sol
libraries/UncheckedMath.sol
contracts/RewardHandler.sol
contracts/BkdLocker.sol
L91/92/137/139/254/301 - It is less expensive to validate that variable != 0, than to validate variable > 0.
L91/92/119/137/208 - Gas can be saved if instead of a require an if is used, with the custom error already created.
L140/144 - --i is less expensive than i = i - 1;
150/151 - A newTotal variable is created, to only be used in one place, it is not necessary and gas could be saved by not doing it.
contracts/tokenomics/FeeBurner.sol
contracts/tokenomics/LpGauge.sol
L59 - The if that validates: (amount <= 0) does not make much sense, since in uint there is no < 0, therefore the correct validation is: (amount == 0).
L68/114 - The if that validates param > 0, could become less expensive if instead of >, a != is used.
L111/115 - A currentRate variable is created, which is only used in one place, therefore it is not necessary and more gas is spent.
contracts/tokenomics/VestedEscrowRevocable.sol
L101 - The claim() function appears as nonReentrant, but there is no reason for reentrant to occur, since only a transferFrom is performed on line 146 at VestedEscrow.sol
L97/98 - It is not necessary to create a variable that will only be used once, it would save gas if it is used directly.
L103/104 - It is possible to create a variable in memory for revokedTime[msg.sender], in this way less gas would be used.
L52/53/54 - The modifier generates a lot of gas cost, it would be less expensive to use an if with a custom error or a private view function.
L54 - In the revoke() function the storage treasury address is used multiple times, one way to save gas is to create a variable in memory.
contracts/tokenomics/VestedEscrow.sol
L83/84 - It is not necessary to create a variable that will only be used once, it would save gas if it is used directly. Gas is also saved if instead of > 0 we use != 0.
L89 - It is not necessary for the function to be nonReentrant, since the transfers that occur are only in the rewardToken address that is set by the owner on deploy, so there shouldn't be a problem with reentry.
L134/135/155/156/168/169 - It is not necessary to create a variable that will only be used once, it would save gas if it is used directly.
contracts/utils/Preparable.sol
contracts/tokenomics/KeeperGauge.sol
L59/98 - ++epoch uses less gas than epoch++;
L39/78/82/126/140 - The requirements are very expensive, it could save gas if instead of a modifier it is an if with a custom error or a private view function.
L140 - Gas is saved, if instead of totalClaimable > 0 you can use totalClaimable != 0.
contracts/tokenomics/AmmGauge.sol
L63 - It should be amount == 0, it doesn't make sense for amount to be <= 0, since it is uint256.
L88/104/125/147 - Gas would be saved if instead of variable > 0 variable != 0 is used.
#0 - GalloDaSballo
2022-06-18T21:13:30Z
Saves deployment cost which is hard to quantify, in lack of details cannot give it any points.
Same deal, no Code = no points
Doing a call to a view function generates a STATICCALL
which costs 100 fixed gas plus the gas cost of the operations, this finding is invalid
Invalid per above
Agree, would save gas on the "bad path" hence hard to quantify
Disagree in lack of POC
Should save 5 gas
Saves 3 gas
Only for requires, L91/92/137/139
4 * 3 = 12
0 per above
Saves 5 gas 5 * 2 = 10
3 gas
Only for requires
3 gas
Only for requires
3 gas
Arguable as Slither would flag the event for potential reEntrancy, will not award points here
3
Saves 94 gas (100 - 3 - 3)
See above
97 + 97 + 94 gas saved
3
I'd keep the guard in lack of CEI pattern (and dodge the wrath of the spam c4 submissions)
Saves 9 gas
It's an internal function ser
5 gas
See above
3 (technically 6 but I already judged tons with 3)
Saves 3 gas
Only for requires, 2 * 3 = 6
Personally I think this type of report can be drastically improved by:
Total Gas Saved 453
#1 - GalloDaSballo
2022-06-18T21:16:06Z
Adding 24 gas from #33
477