Acala - 0xTheC0der's results

Building the liquidity layer of Web3 finance.

General Information

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

Acala Network

Findings Distribution

Researcher Performance

Rank: 13/17

Findings: 2

Award: $517.79

🌟 Selected for report: 1

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: 0xTheC0der

Also found by: carrotsmuggler, djxploit, zhaojie

Labels

bug
2 (Med Risk)
high quality report
primary issue
selected for report
sponsor disputed
edited-by-warden
:robot:_10_group
M-02

Awards

475.8847 USDC - $475.88

External Links

Lines of code

https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/incentives/src/lib.rs#L188-L217

Vulnerability details

Impact

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:

  • Users are not incentivized to keep (DEX) shares deposited, but rather to sandwich the incentive accumulation via deposit, claim & withdraw.
  • Since (DEX) shares are only required for a short period of time (> 1 block) to perform the above action, an adversary can borrow vast amounts of (DEX) shares (or the underlying assets to get them) to crowd out honest users during the incentive accumulation and therefore obtain an unfairly high share of the fixed rewards.

For reference, see also the earnings module, where an unbonding period is explicitly enforced.

Proof of Concept

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()
 			}
 		);

Tools Used

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.

Assessed type

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

Findings Information

🌟 Selected for report: Bauchibred

Also found by: 0xTheC0der, AM, Cryptor, ZanyBonzy, n4nika, zhaojie

Labels

bug
grade-b
QA (Quality Assurance)
sufficient quality report
edited-by-warden
Q-03

Awards

41.9121 USDC - $41.91

External Links

L-1: Reliance on block number instead of timestamp

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.

NC-1: Inconsistent hook naming among earning an incentives modules

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

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