Backd Tokenomics contest - fatherOfBlocks's results

Maximize the power of your assets and start earning yield

General Information

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

Backd

Findings Distribution

Researcher Performance

Rank: 9/58

Findings: 3

Award: $2,976.36

🌟 Selected for report: 1

🚀 Solo Findings: 1

Findings Information

🌟 Selected for report: fatherOfBlocks

Labels

bug
2 (Med Risk)
disagree with severity
sponsor confirmed

Awards

2732.5332 USDC - $2,732.53

External Links

Lines of code

https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/KeeperGauge.sol#L151-L164

Vulnerability details

Impact

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

Awards

159.0051 USDC - $159.01

Labels

bug
QA (Quality Assurance)
resolved
sponsor confirmed

External Links

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

  • L24/25 - If when executing: newPool_.getUnderlying(), we get address(0) as response, it should not be correct to set underlyingNewPools[address(0)] and this is currently happening. The validation of line 26: if (underlying == address(0)) continue; it should be earlier.

contracts/BkdLocker.sol

  • L188 - It is not validated in the getShareOfTotalBoostedBalance() function that when the division is performed with totalLockedBoosted, totalLockedBoosted is != 0.

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

  • L77 - The function requests an extra variable that is not requested. There is a comment that says to add it so that the compiler don't throw warnings.

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

  • L80/81 - In the mint() function of the minter contract it can only be executed by the deployer of the BkdToken contract. Therefore, there is not much benefit in mintRewards() being executed for any address that returns true in the modifieronlyGauge().

#0 - GalloDaSballo

2022-06-21T17:01:30Z

L93/117 - It is validated that pool != address(0) but actually pool is needed to put it inside the ILiquidityPool interface,

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

L288 - An address is requested in the addStakerVault() function

Arguably useful as you'd still get a revert when calling getToken

L33 - The setInflationManager()

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

L39 - The addStakerVault()

Agree, there will never be a false return there, only reverts, the check can be removed

L121/123/127/129 - The code of the function getTotalEthRequiredForGas()

Honestly really not a big deal and subjective as some people prefer explicit return and it's the same for the compiler

L24/25 - If when executing: newPool_.getUnderlying()

Agree that an early return makes sense

L188 - It is not validated in the getShareOfTotalBoostedBalance()

I don't believe this to be an issue nor necessary as Solidity >= 0.8.0 will do a division by zero check

L25 - The WETH address is hardcoded

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

L79 - When the targetUnderlying_ variable is created, it is never validated that it is != address(0),

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

L77 - The function requests an extra variable that is not requested.

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?

L185 - If a malicious address is approved and if the

<img width="1498" alt="Screenshot 2022-06-21 at 18 55 16" src="https://user-images.githubusercontent.com/13383782/174855754-eda6f838-1da2-41c9-b754-3a7aece6e51f.png"> Per the discussion on the standard, it's up to the user to avoid that, not the ERC programmer

L80/81 - In the mint()

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

Awards

84.8218 USDC - $84.82

Labels

bug
G (Gas Optimization)
resolved
sponsor confirmed

External Links

RoleManager.sol

  • L27/46/112/113 - 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.

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

  • L31 - Gas can be saved if instead of a require an if is used, with the custom error already created.

libraries/UncheckedMath.sol

  • L7 - In the uncheckedInc() function, the gas could be further optimized, if instead of unchecked{a+1;} it was unchecked {++a;}

contracts/RewardHandler.sol

  • L63 - It is less expensive to validate that variable != 0, than to validate variable > 0.

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

  • L117 - It is less expensive to validate that variable != 0, than to validate variable > 0.

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

  • L28/29/86/98/110/111 - 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.

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

L27/46/112/113 - 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.

Saves deployment cost which is hard to quantify, in lack of details cannot give it any points.

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.

Same deal, no Code = no points

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.

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

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.

Invalid per above

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.

Agree, would save gas on the "bad path" hence hard to quantify

L31 - Gas can be saved if instead of a require an if is used, with the custom error already created.

Disagree in lack of POC

L7 - In the uncheckedInc() function, the gas could be further optimized, if instead of unchecked{a+1;} it was unchecked {++a;}

Should save 5 gas

L63 - It is less expensive to validate that variable != 0, than to validate variable > 0.

Saves 3 gas

L91/92/137/139/254/301 - It is less expensive to validate that variable != 0, than to validate variable > 0.

Only for requires, L91/92/137/139 4 * 3 = 12

L91/92/119/137/208 - Gas can be saved if instead of a require an if is used, with the custom error already created.

0 per above

L140/144 - --i is less expensive than i = i - 1;

Saves 5 gas 5 * 2 = 10

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.

3 gas

L117 - It is less expensive to validate that variable != 0, than to validate variable > 0.

Only for requires

L59 - The if that validates: (amount <= 0) does not make much sense, since in uint there is no < 0, therefore the correct validation is:

3 gas

L68/114 - The if that validates param > 0, could become less expensive if instead of >, a != is used.

Only for requires

L111/115 - A currentRate variable is created, which is only used in one place, therefore it is not necessary and more gas is spent.

3 gas

L101 - The claim() function appears as nonReentrant, but there is no reason for reentrant to occur, since only a transferFrom is performed

Arguable as Slither would flag the event for potential reEntrancy, will not award points here

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.

3

L103/104 - It is possible to create a variable in memory for revokedTime[msg.sender], in this way less gas would be used.

Saves 94 gas (100 - 3 - 3)

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.

See above

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.

97 + 97 + 94 gas saved

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.

3

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

I'd keep the guard in lack of CEI pattern (and dodge the wrath of the spam c4 submissions)

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.

Saves 9 gas

L28/29/86/98/110/111 - 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.

It's an internal function ser

L59/98 - ++epoch uses less gas than epoch++;

5 gas

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.

See above

L140 - Gas is saved, if instead of totalClaimable > 0 you can use totalClaimable != 0.

3 (technically 6 but I already judged tons with 3)

L63 - It should be amount == 0, it doesn't make sense for amount to be <= 0, since it is uint256.

Saves 3 gas

L88/104/125/147 - Gas would be saved if instead of variable > 0 variable != 0 is used.

Only for requires, 2 * 3 = 6

Personally I think this type of report can be drastically improved by:

  • Bulking the fix instead of grouping by file (humans think in problems and ideas, not in files)
  • Add the gas saved
  • Add POC for big statements ("use custom errors lol XD")

Total Gas Saved 453

#1 - GalloDaSballo

2022-06-18T21:16:06Z

Adding 24 gas from #33

477

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter