Platform: Code4rena
Start Date: 22/03/2024
Pot Size: $36,500 USDC
Total HM: 7
Participants: 17
Period: 14 days
Judge: Lambda
Id: 323
League: POLKADOT
Rank: 13/17
Findings: 2
Award: $517.79
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: 0xTheC0der
Also found by: carrotsmuggler, djxploit, zhaojie
475.8847 USDC - $475.88
Incentives are accumulated periodically in intervals of T::AccumulatePeriod blocks. Thereby, a fixed incentive reward amount, which is set via update_incentive_rewards(...), is accumulated among all deposited shares of a respective pool. This means if only 1
share is deposited, it is entitled for the all the rewards of this period, if N
shares are deposited, the rewards are split among them, etc. (see PoC).
Furthermore, to be eligible for rewards, it is sufficient to deposit (DEX) shares before accumulate_incentives(...) is called via the on_initialize
hook. This is also demonstrated in the transfer_reward_and_update_rewards_storage_atomically_when_accumulate_incentives_work() test case.
Afterwards, rewards can be immediately claimed and (DEX) shares can be withdrawn without any unbonding period or other block/time related restrictions.
This leads to the following consequences:
For reference, see also the earnings module, where an unbonding period is explicitly enforced.
The diff below modifies the existing test case transfer_reward_and_update_rewards_storage_atomically_when_accumulate_incentives_work() to demonstrate that the same total reward amount is accumulated even if twice as many total shares are deposited. Therefore, it's possible to crowd out honest users with great short-term share deposits.
diff --git a/src/modules/incentives/src/tests.rs b/src/modules/incentives/src/tests.rs index 1370d5b..fa16a08 100644 --- a/src/modules/incentives/src/tests.rs +++ b/src/modules/incentives/src/tests.rs @@ -1171,10 +1171,11 @@ fn transfer_reward_and_update_rewards_storage_atomically_when_accumulate_incenti assert_eq!(TokensModule::free_balance(AUSD, &VAULT::get()), 0); RewardsModule::add_share(&ALICE::get(), &PoolId::Loans(LDOT), 1); + RewardsModule::add_share(&BOB::get(), &PoolId::Loans(LDOT), 1); assert_eq!( RewardsModule::pool_infos(PoolId::Loans(LDOT)), PoolInfo { - total_shares: 1, + total_shares: 2, ..Default::default() } ); @@ -1188,7 +1189,7 @@ fn transfer_reward_and_update_rewards_storage_atomically_when_accumulate_incenti assert_eq!( RewardsModule::pool_infos(PoolId::Loans(LDOT)), PoolInfo { - total_shares: 1, + total_shares: 2, rewards: vec![(ACA, (30, 0)), (AUSD, (90, 0))].into_iter().collect() } ); @@ -1202,7 +1203,7 @@ fn transfer_reward_and_update_rewards_storage_atomically_when_accumulate_incenti assert_eq!( RewardsModule::pool_infos(PoolId::Loans(LDOT)), PoolInfo { - total_shares: 1, + total_shares: 2, rewards: vec![(ACA, (60, 0)), (AUSD, (90, 0))].into_iter().collect() } );
Manual review
The present issue scales with the size of T::AccumulatePeriod in terms of blocks. Therefore, it's recommended (for fairness) to also track deposited shares in-between the accumulation intervals and scale the incentive rewards according to the actual deposit duration.
MEV
#0 - c4-pre-sort
2024-04-06T14:20:05Z
DadeKuma marked the issue as duplicate of #10
#1 - c4-pre-sort
2024-04-07T13:27:04Z
DadeKuma marked the issue as not a duplicate
#2 - c4-pre-sort
2024-04-07T13:27:08Z
DadeKuma marked the issue as primary issue
#3 - c4-pre-sort
2024-04-07T13:29:07Z
DadeKuma marked the issue as high quality report
#4 - DadeKuma
2024-04-07T13:29:10Z
Has coded PoC, and overall it is the best report in the set of dups
#5 - xlc
2024-04-09T04:31:17Z
Furthermore, to be eligible for rewards, it is sufficient to deposit (DEX) shares before accumulate_incentives(...) is called via the on_initialize hook
It is not possible to do perform user triggered action before on_initialize
To exploit this, attacker will need to acquire a large number of dex share somehow, deposit it, what for a block, withdraw it, ????, and repeat the same thing on next minute. Firstly, it is not possible to borrow such amount of share without some payments, because the lender have the full incentives to deposit the shares and getting the rewards. It is also not a lost free action to mint such amount of dex share and redeem them due to potential price exposure and sandwich risk.
Therefore it is economically impossible to exploit this behavior and make a profit.
#6 - c4-sponsor
2024-04-09T04:31:20Z
xlc (sponsor) disputed
#7 - OpenCoreCH
2024-04-09T16:30:42Z
The described scenario has some stated assumptions with external requirements (availability of liquidity, possibility to front- & back-run the transactions risk free, etc...) that may not always hold in practice. But these are not very unreasonable assumptions and under these assumptions, a value leak is possible. This therefore fulfills the criteria of a valid medium according to the severity categorization.
#8 - c4-judge
2024-04-09T16:30:45Z
OpenCoreCH marked the issue as selected for report
#9 - c4-judge
2024-04-09T16:30:53Z
OpenCoreCH marked issue #47 as primary and marked this issue as a duplicate of 47
#10 - c4-judge
2024-04-09T16:31:50Z
OpenCoreCH marked the issue as selected for report
🌟 Selected for report: Bauchibred
Also found by: 0xTheC0der, AM, Cryptor, ZanyBonzy, n4nika, zhaojie
41.9121 USDC - $41.91
Incentives are accumulated periodically in intervals of T::AccumulatePeriod blocks.
This is not recommended since the block production time is not assured to be constant over time.
Consequently, inconsistent incentive reward amounts (per time interval) can be accumulated, which is perceived is unpredictable and unfair by users.
Recommendation:
Use timestamp based incentive accumulation for fixed time periods. See also: pallet-timestamp
Or, if the block number based approach is still desired, add a method which allows to change the accumulate period if necessary.
According to the README about the earning module:
The module implements a set of hooks that can be used by other modules (i.e. Incentives) to implement staking.
Hook names in earning module: OnBonded, OnUnbonded
Hook names in incentives module: OnEarningBonded, OnEarningUnbonded
Since those two modules are meant to be connected via these hooks, the inconsistent naming might be misleading and cause integration issues.
#0 - c4-pre-sort
2024-04-07T15:02:58Z
DadeKuma marked the issue as sufficient quality report
#1 - c4-judge
2024-04-09T17:06:04Z
OpenCoreCH marked the issue as grade-b