stake() and contributeBonus() use balance-diff to measure actual tokens received, correctly handling fee-on-transfer tokens. All outbound transfers (withdraw, claim*, sweep*) use direct safeTransfer(amount) without balance-diff. When the stake token deducts a transfer fee, recipients receive less than entitled. The shortfall accumulates in the contract as unbacked liabilities.
stake() measures actual receipt via balance-diff. If the token takes 1% on a 100-token deposit, the protocol credits 99 tokens:
Every outbound transfer sends the recorded amount without re-measuring:
When the token deducts a fee on the outgoing transfer, the user receives less than the protocol recorded as owed. The lost fee stays in the contract and is eventually swept to recoveryAddress — not returned to the staker.
The README states fee-on-transfer tokens are not supported. This finding is about the incomplete defense: the balance-diff on inbound creates a false expectation that the protocol handles such tokens, while outbound paths lack the corresponding protection.
Likelihood:
The factory owner adds a token to allowedStakeToken that charges a transfer fee — either at allowlist time or through a future proxy upgrade. The README disclaims this, but no on-chain check prevents it.
A fee-on-transfer token is used as the stake token. Every outbound transfer deducts a fee the protocol does not account for.
Impact:
Each staker receives less than their recorded eligibleStake or payout on every withdraw or claim. The shortfall accumulates in the contract and is swept to recoveryAddress, not returned to the staker.
The accounting variables (totalEligibleStake, claimedBonus) diverge from the actual token balance, making true solvency unverifiable through accounting alone.
The test deploys a pool with MockFeeOnTransferERC20 (1% fee on every transfer). Alice deposits 1000 tokens and immediately withdraws.
Timeline and math:
Alice deposits 1000 tokens into the pool.
The 1% fee deducts 10 tokens on the inbound transfer. The protocol's balance-diff correctly credits Alice with 990 tokens (eligibleStake[alice] == 990).
Alice calls withdraw(). The protocol calls stakeToken.safeTransfer(alice, 990).
The 1% fee deducts ~9.9 tokens on the outbound transfer. Alice receives ~980 tokens, not the 990 the protocol recorded.
Alice lost 1% on entry (990 credited instead of 1000) and 1% on exit (980 received instead of 990). The 9.9-token fee stays in the pool contract permanently — it can only be swept to recoveryAddress, not returned to Alice.
Both tests confirm: the protocol correctly credits 990 tokens on deposit, but Alice receives only ~980 on withdrawal — a double loss from the asymmetric fee handling.
Apply the same balance-diff pattern to outbound transfers. For withdraw():
Apply the same pattern to claimSurvived() and claimExpired() around the final safeTransfer. The sweepUnclaimedBonus() function already reads balanceOf(this) before the sweep and caps at freeBalance - reserved, so it naturally handles fee tokens.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
View preliminary resultsAppeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.