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: 5/58
Findings: 5
Award: $6,380.59
🌟 Selected for report: 3
🚀 Solo Findings: 2
🌟 Selected for report: hansfriese
Also found by: csanuragjain, unforgiven
737.784 USDC - $737.78
https://github.com/code-423n4/2022-05-backd/tree/main/protocol/contracts/BkdLocker.sol#L70-L75 https://github.com/code-423n4/2022-05-backd/tree/main/protocol/contracts/BkdLocker.sol#L302-L322
Users can claim more fees than expected if governance migrates current rewardToken again by fault.
In the migrate() function, there is no requirement newRewardToken != rewardToken. If this function is called with the same "rewardToken" parameter, "_replacedRewardTokens" will contain the current "rewardToken" also. Then when the user claims fees, "userShares" will be added two times for the same token at L302-L305, L314-L317. It's because "curRewardTokenData.userFeeIntegrals[user]" is updated at L332 after the "userShares" calculation for past rewardTokens. So the user can get paid more fees than he should.
Solidity Visual Developer of VSCode
You need to add this require() at L71.
require(newRewardToken != rewardToken, Error.SAME_AS_CURRENT);
#0 - GalloDaSballo
2022-06-21T00:42:02Z
The warden has identified how a governance migration from and to the same token can cause the rewards to be double-counted.
Because the exploit is contingent on:
I believe Medium Severity to be appropriate
🌟 Selected for report: hansfriese
2732.5332 USDC - $2,732.53
https://github.com/code-423n4/2022-05-backd/tree/main/protocol/contracts/StakerVault.sol#L98-L102 https://github.com/code-423n4/2022-05-backd/tree/main/protocol/contracts/StakerVault.sol#L342-L346 https://github.com/code-423n4/2022-05-backd/tree/main/protocol/contracts/StakerVault.sol#L391-L395
StakerVault.unstake(), StakerVault.unstakeFor() would revert with a uint underflow error of StakerVault.strategiesTotalStaked, StakerVault._poolTotalStaked.
Currently it saves totalStaked for strategies and non-strategies separately. uint underflow error could be occured in these cases.
Scenario 1.
Scenario 2. There is a transfer between strategy and non-strategy using StakerVault.transfer(), StakerVault.transferFrom() functions. In this case, StakerVault.strategiesTotalStaked and StakerVault._poolTotalStaked must be changed accordingly but there is no such logic.
Solidity Visual Developer of VSCode
You need to modify 3 functions. StakerVault.addStrategy(), StakerVault.transfer(), StakerVault.transferFrom().
function addStrategy(address strategy) external override returns (bool) { require(msg.sender == address(inflationManager), Error.UNAUTHORIZED_ACCESS); require(!strategies[strategy], Error.ADDRESS_ALREADY_SET);
strategies[strategy] = true; _poolTotalStaked -= balances[strategy]; strategiesTotalStaked += balances[strategy]; return true;
}
if(strategies[msg.sender] != strategies[account]) { if(strategies[msg.sender]) { // from strategy to non-strategy _poolTotalStaked += amount; strategiesTotalStaked -= amount; } else { // from non-strategy to strategy _poolTotalStaked -= amount; strategiesTotalStaked += amount; } }
if(strategies[src] != strategies[dst]) { if(strategies[src]) { // from strategy to non-strategy _poolTotalStaked += amount; strategiesTotalStaked -= amount; } else { // from non-strategy to strategy _poolTotalStaked -= amount; strategiesTotalStaked += amount; } }
#0 - GalloDaSballo
2022-06-21T01:48:23Z
The warden has identified a way for funds to be stuck due to underflow.
While the odds of this happening are fairly low, the grief can be executed by frontrunning the addStrategy
call, as well as by mistake.
Additionally, a strategy doesn't seem to be removable making the loss of those hypothetical tokens permanent
For those reasons I believe Medium Severity to be appropriate
🌟 Selected for report: hansfriese
2732.5332 USDC - $2,732.53
https://github.com/code-423n4/2022-05-backd/tree/main/protocol/contracts/StakerVault.sol#L95 https://github.com/code-423n4/2022-05-backd/tree/main/protocol/contracts/tokenomics/LpGauge.sol#L52-L63
Strategy in StakerVault.sol can steal more rewards even though it's designed strategies shouldn't get rewards. Also there will be a problem with a rewarding system in LpGauge.sol so that some normal users wouldn't get rewards properly.
Inside LpGauge.userCheckPoint(), it's designed not to calculate LpGauge.perUserShare for strategy, but it will pass this condition because B is not a strategy. contracts\tokenomics\LpGauge.sol#L90
Furthermore, when calculate rewards, LpGauge.poolStakedIntegral will be calculated larger than a normal user stakes same amount. It's because StakerVault._poolTotalStaked wasn't updated when A transfers x amount to B so LpGauge.poolTotalStaked is less than correct value. contracts\tokenomics\LpGauge.sol#L113-L117
Finally B can get more rewards than he should and the reward system will pay more rewards than it's designed.
Solidity Visual Developer of VSCode
I think there will be two methods to fix. Method 1 is to forbid a transfer between strategy and non-strategy so that strategy can't move funds to non-strategy. Method 2 is to update StakerVault.strategiesTotalStaked and StakerVault._poolTotalStaked correctly so that strategy won't claim more rewards than he should even though he claims rewards using non-strategy.
Method 1. You need to modify two functions. StakerVault.transfer(), StakerVault.transferFrom().
You need to add this require() at L112 for transfer(). require(strategies[msg.sender] == strategies[account], Error.FAILED_TRANSFER);
You need to add this require() at L144 for transferFrom(). require(strategies[src] == strategies[dst], Error.FAILED_TRANSFER);
Method 2. I've explained about this method in my medium risk report "StakerVault.unstake(), StakerVault.unstakeFor() would revert with a uint underflow error of StakerVault.strategiesTotalStaked, StakerVault._poolTotalStaked" I will copy the same code for your convenience.
You need to modify 3 functions. StakerVault.addStrategy(), StakerVault.transfer(), StakerVault.transferFrom().
function addStrategy(address strategy) external override returns (bool) { Â Â require(msg.sender == address(inflationManager), Error.UNAUTHORIZED_ACCESS); Â Â require(!strategies[strategy], Error.ADDRESS_ALREADY_SET);
  strategies[strategy] = true;   _poolTotalStaked -= balances[strategy];   strategiesTotalStaked += balances[strategy];
  return true; }
if(strategies[msg.sender] != strategies[account]) {   if(strategies[msg.sender]) { // from strategy to non-strategy     _poolTotalStaked += amount;     strategiesTotalStaked -= amount;   }   else { // from non-strategy to strategy     _poolTotalStaked -= amount;     strategiesTotalStaked += amount;   } }
if(strategies[src] != strategies[dst]) {   if(strategies[src]) { // from strategy to non-strategy     _poolTotalStaked += amount;     strategiesTotalStaked -= amount;   }   else { // from non-strategy to strategy     _poolTotalStaked -= amount;     strategiesTotalStaked += amount;   } }
#0 - chase-manning
2022-06-07T12:42:47Z
We think this is a medium risk
#1 - GalloDaSballo
2022-06-22T16:47:41Z
I believe there's validity to the finding but at the same time I believe the impact is a loss of yield more so than an unfair gain of yield for a strategy.
Specifically the POC is reliant on Depositing as a user, then transferring tokens to a strategy.
I believe this will break the accounting per the POC shown (strategiesTotalStaked
will be incorrect)
Then the rewards will be claimable to the strategy as if it were a user, meaning that the extra checks to prevent strategies from gaining staking rewards will be sidestepped.
I believe those tokens will be lost unless all strategies have a way to sweep non-protected tokens.
Because the warden showed how to break accounting, I believe Medium Severity to be valid, that said I don't believe the warden has shown any meaningful economic attack beside end-users losing their own tokens and the rewards attached to them
🌟 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
It was easy to understand the logic for me because the codes have detailed explanations. I recommended adding some more require() for better performance.
i) contracts\BkdLocker.sol#L49 require(_govToken != address(0), Error.ZERO_ADDRESS_NOT_ALLOWED); require(_rewardToken != address(0), Error.ZERO_ADDRESS_NOT_ALLOWED);
ii) contracts\BkdLocker.sol#L60 "maxBoost" must be greater than "startBoost", otherwise L276-L278 will be revoked. require(maxBoost >= startBoost, Error.INVALID_AMOUNT);
iii) contracts\BkdLocker.sol#L60 "increasePeriod" must be positive, otherwise L276-L278 will be revoked. require(increasePeriod != 0, Error.INVALID_AMOUNT);
iv) contracts\BkdLocker.sol#L123 "amount" must be positive, otherwise the user can prepare unlock endlessly. require(amount != 0, Error.INVALID_AMOUNT);
v) contracts\BkdLocker.sol#L215 require(claimable != 0, Error.INVALID_AMOUNT);
vi) contracts\tokenomics\VestedEscrow.sol#L60 In this setFundAdmin() function at L75, it checks the first require() already. require(fundadmin_ != address(0), Error.ZERO_ADDRESS_NOT_ALLOWED); require(rewardToken_ != address(0), , Error.ZERO_ADDRESS_NOT_ALLOWED);
There are no "deposit" or "depositFor" functions in this contract. You need to write "lock" or "lockFor" instead.
contracts\tokenomics\InflationManager.sol#L151 contracts\tokenomics\InflationManager.sol#L241 contracts\tokenomics\InflationManager.sol#L326
With the above functions, the batch functions have the modifier "onlyRoles2(Roles.GOVERNANCE, Roles.INFLATION_MANAGER)". I think the above functions should have the same modifier also.
contracts\tokenomics\VestedEscrow.sol#L113-L115
Otherwise, you can change the function like this(same as VestedEscrowRevocable.sol).
function claim() external virtual override { Â Â claim(msg.sener); }
#0 - GalloDaSballo
2022-06-21T17:05:45Z
Valid
rest seem minor, personally the formatting takes away from what could be a pretty decent report.
🌟 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
57.93 USDC - $57.93
--length; if(i != length) { Â Â stashedWithdraws[i] = stashedWithdraws[length]; }
From the logic of the contract, "totalKeeperPoolWeight" will be always non-negative. Even if this value could be negative, it will be revoked with underflow error before.
Use != 0 instead of > 0 for uint variables contracts\BkdLocker.sol#L91 contracts\BkdLocker.sol#L92 contracts\BkdLocker.sol#L137 contracts\BkdLocker.sol#L139 contracts\BkdLocker.sol#L254 contracts\BkdLocker.sol#L301 contracts\tokenomics\AmmGauge.sol#L88 contracts\tokenomics\AmmGauge.sol#L104 contracts\tokenomics\AmmGauge.sol#L125 contracts\tokenomics\AmmGauge.sol#L147 contracts\tokenomics\FeeBurner.sol#L117 contracts\tokenomics\KeeperGauge.sol#L140 contracts\tokenomics\LpGauge.sol#L68 contracts\tokenomics\LpGauge.sol#L114 contracts\tokenomics\VestedEscrow.sol#L84 contracts\RewardHandler.sol#L63
Change storage to memory if possible contracts\BkdLocker.sol#L251
An array’s length should be cached to save gas in for-loops contracts\StakerVault.sol#L259 contracts\tokenomics\FeeBurner.sol#L56 contracts\tokenomics\InflationManager.sol#L116 contracts\tokenomics\VestedEscrow.sol#L94 contracts\zaps\PoolMigrationZap.sol#L22 contracts\zaps\PoolMigrationZap.sol#L39
Usage of unchecked can reduce the gas cost contracts\StakerVault.sol#L387
allowances[src][msg.sender] = allowance.uncheckedSub(unstaked);
#0 - GalloDaSballo
2022-06-18T21:23:28Z
I don't believe the suggestion will work, in lack of POC I'm not giving it points The suggested change goes out of bounds (last element of array is array.length - 1)
Saves 3 gas 3 * 3 = 9
Only for Requires, listed below
contracts\BkdLocker.sol#L91 contracts\BkdLocker.sol#L92 contracts\BkdLocker.sol#L137 contracts\tokenomics\AmmGauge.sol#L104 contracts\tokenomics\AmmGauge.sol#L125 contracts\tokenomics\KeeperGauge.sol#L140 contracts\tokenomics\VestedEscrow.sol#L84
3 * 7 = 21
In lack of POC I can't give it any points
3 per instance
3 * 6 = 18
Would save 20 gas
Total Gas Saved 68