CoinFabrik/scout-substrate huggingface dataset:
Audit finding: Incentive accumulation can be sandwiched with additional shares to gain advantage over long-term depositors
Project: Acala
Auditor: Code4Arena
Location: src/modules/incentives/src/lib.rs:188-217
Impact: Medium
Vulnerability class (audit): MEV
Vulnerability class (Scout): Business Logic

[M-02] Incentive accumulation can be sandwiched with additional shares to gain advantage over long-term depositors
Submitted by 0xTheC0der, also found by zhaojie, djxploit, and carrotsmuggler
Incentives are accumulated periodically in intervals of T::AccumulatePeriod blocks. A fixed incentive reward amount, which is set via update_incentive_rewards(...), is accumulated among all deposited shares of a respective pool. If only 1 share is deposited, it is entitled to all the rewards of this period; if N shares are deposited, the rewards are split among them.
To be eligible for rewards, it is sufficient to deposit (DEX) shares before accumulate_incentives(...) is called via the on_initialize hook. Afterward, rewards can be immediately claimed, and (DEX) shares can be withdrawn without any unbonding period or other block/time-related restrictions.

Consequences:
• Users can sandwich the incentive accumulation via deposit, claim, and withdraw.
• Adversaries can borrow vast amounts of (DEX) shares to crowd out honest users during incentive accumulation and gain an unfairly high share of the fixed rewards.

Proof of Concept:
The provided diff 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.

```diff
@@ -1171,10 +1171,11 @@ fn transfer_reward_and_update_rewards_stora
  assert_eq!(TokensModule::free_balance(AUSD, &VAULT);
  RewardsModule::add_share(&ALICE::get(), &PoolId::Loans);
+
  RewardsModule::add_share(&BOB::get(), &PoolId::Loans);
  assert_eq!(
    RewardsModule::pool_infos(PoolId::Loans(LDO)),
    PoolInfo {
-      total_shares: 1,
+      total_shares: 2,
      ..Default::default()
    }
  );
```

Recommended Mitigation Steps:
Track deposited shares in between accumulation intervals and scale the incentive rewards according to the actual deposit duration.

Assessed type: MEV
xlc (Acala) disputed and commented: To exploit this, an attacker would need to acquire a large number of DEX shares and wait for a block, which incurs costs. It is economically unfeasible to profit from this behavior.
Lambda (judge) commented: While external conditions (like liquidity availability) may not always align, the described scenario demonstrates a valid medium severity leak under reasonable assumptions.

Summary: Incentive rewards can be exploited by depositing and withdrawing large shares around accumulation periods, crowding out long-term depositors.
Audit finding: Unbond_instant removes incorrect amount of shares
Project: Acala
Auditor: Code4Arena
Location: src/modules/earning/src/lib.rs:188-196
Impact: Medium
Vulnerability class (Scout): Business Logic
Status: See the fix [here](https://github.com/AcalaNetwork/Acala/pull/2735).

[M-03] Unbond_instant removes incorrect amount of shares
Submitted by TheSchnilch, also found by Aymen0909
With unbond_instant, a user can unbond a bonded amount directly without having to wait. However, they must pay a fee for this:

let amount = change.change;
let fee = fee_ratio.mul_ceil(amount);
let final_amount = amount.saturating_sub(fee);
let unbalance = T::Currency::withdraw(&who, fee, WithdrawReasons::T);
T::OnUnstakeFee::on_unbalanced(unbalance);
T::OnUnbonded::happened(&(who.clone(), final_amount));

Here, the mistake is that T::OnUnbonded::happened is called with final_amount, meaning without a fee. As a result, a portion of the shares that were added as shares during bonding can no longer be removed. This, in turn, leads to these shares still receiving rewards that other users cannot receive.

If you insert a println! statement into the functions bond and unbond_instant to display the amounts with which OnBonded::happened and OnUnbonded::happened are called, you will see when you execute the following code that not all shares are removed when the bonded amount of a user is removed with unbond_instant:

For bond:
+ println!("change.change: {:?}", change.change);
T::OnBonded::happened(&(who.clone(), change.change));
Self::deposit_event(Event::Bonded { who, amount: change.change });

For unbond_instant:
+ println!("final_amount: {:?}", final_amount);
T::OnUnbonded::happened(&(who.clone(), final_amount));
Self::deposit_event(Event::InstantUnbonded { who, amount: final_amount, fee });

Code to test both functions:
#[test]
fn poc() {
  ExtBuilder::default().build().execute_with(|| {
    assert_ok!(Earning::bond(RuntimeOrigin::signed(...)));
    assert_ok!(Earning::unbond_instant(RuntimeOrigin::signed(...)));
  });
}

The code can be executed with the command: cargo test poc -- --nocapture.

Impact:
Every time a user calls unbond_instant, some shares will remain in the reward system and continue to accumulate rewards that other users who actually bond can no longer get. These shares can no longer be removed and are stuck. With a maximum fee of 20%, some shares could accumulate over time and continue to accumulate rewards. The owners of these stuck shares can still claim these rewards, causing an imbalance where one share is no longer equivalent to one underlying token.

Recommendation:
For unbond_instant, the final_amount should not be used to remove the shares, but change.change should be used instead.

xlc (Acala) confirmed and commented: See the fix here.

Summary: The unbond_instant function removes fewer shares than intended due to an incorrect fee deduction, causing stuck shares to accumulate rewards unfairly.
Audit finding: Storage can be bloated with low liquidity positions
Project: Acala
Auditor: Code4Arena
Location: src/modules/incentives/src/lib.rs:230
Impact: Medium
Vulnerability class (Scout): Denial of Service (DoS) and Spamming
Status: Fixed [here](https://github.com/open-web3-stack/open-runtime-module-library/pull/991).

[M-04] Storage can be bloated with low liquidity positions
Submitted by ZanyBonzy, also found by Bauchibred and carrotsmuggler
The deposit_dex_share function enforces no minimum amount that can be deposited into the pool, allowing for the creation of multiple low-liquidity positions. This enables a coordinated effort where users or attackers, for a relatively low cost, can create numerous low-liquidity positions, bloating the runtime storage.

This issue is significant as the Substrate framework requires storage optimization to prevent bloat, which can lead to high maintenance costs for the chain and potential denial-of-service (DoS) attacks.

Proof of Concept:
The test case below demonstrates how a user can create multiple 1 wei positions:

#[test]
fn open_low_liquidity_positions() {
  ExtBuilder::default().build().execute_with(|| {
    assert_ok!(TokensModule::deposit(BTC_AUSD_LP, &ALICE::get(), 1));
    assert_ok!(IncentivesModule::deposit_dex_share(RuntimeOrigin::signed(ALICE::get()), BTC_AUSD_LP, 1));
    assert_ok!(IncentivesModule::deposit_dex_share(RuntimeOrigin::signed(ALICE::get()), BTC_AUSD_LP, 1));
    assert_ok!(IncentivesModule::deposit_dex_share(RuntimeOrigin::signed(ALICE::get()), BTC_AUSD_LP, 1));
    assert_eq!(RewardsModule::pool_infos(PoolId::Dex(BTC_AUSD_LP)).total_shares, 3);
  });
}

The user creates three 1 wei positions, effectively bloating the pool's storage with minimal cost.

Recommended Mitigation Steps:
Introduce a minimum deposit amount to prevent the creation of low-liquidity positions.

xlc (Acala) confirmed and commented: Fixed here.

Summary: The deposit_dex_share function allows creating multiple low-liquidity positions, leading to storage bloat and potential denial-of-service risks.
Audit finding: Admin is a single point of failure
Project: Acala
Auditor: Code4Arena
Location: /src/modules/incentives/src/lib.rs:83-85
Impact: Low
Vulnerability class (Scout): Business Logic

[01] Admin is a single point of failure
Multiple instances in code where the admin logic could lead to a brick in normal functionality.
For example, take a look here.
 The origin which may update incentive related p
type UpdateOrigin: EnsureOrigin<Self::RuntimeOrigin
This is an instance where a somewhat admin logic has been applied, note that this is passed in the config’s constant paller. Now using this search command, we can see that there are three function calls where the expected caller is only accepted to be the update incentive related params.
Impact
Inaccess to update incentive related params if anything was to happen to the UpdateOrigin.
Recommended Mitigation Steps
Consider implementing a backdoor that the admins could use to change the incentive related params if anything were to happen to UpdateOrigin.

Summary: Admin logic creates a single point of failure, risking loss of access to update incentive-related parameters if the UpdateOrigin becomes inaccessible.
Audit finding: Consider adding better documentation
Project: Acala
Auditor: Code4Arena
Location: /src/orml/rewards/src/lib.rs:143-190
Impact: Low
Vulnerability class (Scout): Code Quality

[02] Consider adding better documentation
This is very rampant in code, for example, take a look here.
pub fn add_share(who: &T::AccountId, pool: &T::PoolId, add_
if add_amount.is_zero() {
return;
}
PoolInfos::<T>::mutate(pool, |pool_info| {
let initial_total_shares = pool_info.total_
pool_info.total_shares = pool_info.total_sh
let mut withdrawn_inflation = Vec
pool_info
.rewards
.iter_mut()
.for_each(|(reward_currency, (total
let reward_inflation =
Zero::zero
} else {
U256::from
.
.
.
.
.
};
*total_reward = total_rewar
*total_withdrawn_reward = t
withdrawn_inflation.
});
SharesAndWithdrawnRewards::<T>::mutate
*share = share.saturating_add
// update withdrawn inflation for e
withdrawn_inflation
.into_iter()
.for_each(|(reward_currency
withdrawn_rewards
.
.
})
.
});
});
});
}
Evidently, this function is somewhat complex, i.e. the type of mathematical operations attached with it. However, that isn’t the main case here. There are completely no documentations as to why some steps are taken, this heavily stalls the auditing process as we and other security researchers don’t know what the intended behaviour is and can’t really sit to break it.
Impact
Hard time understanding code for users/devs/auditors lead to bad integration.
Recommended Mitigation Steps
Consider adding better documentations to even if not all functions then at least the core/complex ones.

Summary: Lack of documentation in complex functions makes it difficult for developers, users, and auditors to understand the intended behavior, leading to potential misinterpretation and bad integration.
Audit finding: Setters should always have equality checkers
Project: Acala
Auditor: Code4Arena
Location: src/orml/rewards/src/lib.rs:252-262
src/modules/incentives/src/lib.rs:280-312,320
Impact: Low
Vulnerability class (Scout): Error Handling and Validation

[03] Setters should always have equality checkers
Take a look here.
pub fn set_share(who: &T::AccountId, pool: &T::PoolId, new_
let (share, _) = Self::shares_and_withdrawn_rewards
if new_share > share {
Self::add_share(who, pool, new_share.
} else {
Self::remove_share(who, pool, share.
}
}
This function sets a new share value for a pool, now there are no checks that new_share != share and as such whenever new_share == share the protocol unnecessarily (wrongly) attempts to remove via share.saturating_sub(new_share).
Another instance can be seen here, where the protocol in all instances of updating the amounts doesn’t check if the provided amount is not already the stored amount and as such unnecessarily updates v, see here.
pub fn update_incentive_rewards(
origin: OriginFor<T>,
updates: Vec<(PoolId, Vec<(CurrencyId, Bala
) -> DispatchResult {
T::UpdateOrigin::ensure_origin(origin)?;
for (pool_id, update_list) in updates {
if let PoolId::Dex(currency_id) = p
ensure!(currency_id.
}
for (currency_id, amount)
IncentiveRewardAmounts::<T>
let mut v = maybe_a
if amount != v {
v = amount;
Self
});
}
if v.is_zero
*maybe_amou
} else {
*maybe_amou
}
});
}
}
Ok(())
}
The same idea can also be applied to this update_claim_reward_deduction_rates() function.
Impact
Unnecessary code execution, flawed implementation.
Recommended Mitigation Steps
As a rule of thumb, all setters should always have equality checkers as passing an equal value to the already stored value hints a mistake and maybe this attempt to add/remove was meant for a different pool.

Summary: Missing equality checks in setters can lead to unnecessary code execution and flawed implementation when the new value is equal to the stored value.
Audit finding: OnUpdateLoan::happened() should check for when adjustment == 0
Project: Acala
Auditor: Code4Arena
Location: src/modules/incentives/src/lib.rs:578-587
Impact: Low
Vulnerability class (Scout): Error Handling and Validation

[04] OnUpdateLoan::happened() should check for when adjustment == 0
Take a look here.
fn happened(info: &(T::AccountId, CurrencyId, Amount, Balan
let (who, currency_id, adjustment, _previous_amount
let adjustment_abs = TryInto::<Balance>::
if adjustment.is_positive() {
<orml_rewards::Pallet<T>>::add_share
} else {
<orml_rewards::Pallet<T>>::remove_share
};
}
This function has been used to settle adjustments in the protocol. The main issue about this report is the fact that adding/removing the share is dependent on if the adjustment is positive or not. This is a somewhat flawed implementation, as the is_positive() getter would actually return false for when adjustment == 0 and as such lead to an unnecessary attempt of removing 0 shares.
Impact
Not enough input validation is applied, not the best code structure.
Recommended Mitigation Steps
Consider checking in the function to ensure that for instances where adjustment == 0, neither add_shares() nor remove_shares() is called.

Summary: The function does not handle cases where adjustment == 0, leading to unnecessary calls to add_shares() or remove_shares().
Audit finding: Protocol does not apply deadlines when dealing with critical operations
Project: Acala
Auditor: Code4Arena
Location: /src/modules/incentives/src/lib.rs:511-543
Impact: Low
Vulnerability class (Scout): Business Logic

[05] Protocol does not apply deadlines when dealing with critical operations
Consider the code here.
fn do_deposit_dex_share(who: &T::AccountId, lp_currency_id:
ensure!(lp_currency_id.is_dex_share_currency_id
T::Currency::transfer(lp_currency_id, who, &
<orml_rewards::Pallet<T>>::add_share(who, &PoolId::
Self::deposit_event(Event::DepositDexShare {
who: who.clone(),
dex_share_type: lp_currency_id,
deposit: amount,
});
Ok(())
}
fn do_withdraw_dex_share(who: &T::AccountId, lp_currency_id
ensure!(lp_currency_id.is_dex_share_currency_id
ensure!(
<orml_rewards::Pallet<T>>::shares_and_withd
Error::<T>::NotEnough,
);
T::Currency::transfer(lp_currency_id, &Self
<orml_rewards::Pallet<T>>::remove_share(who, &PoolI
Self::deposit_event(Event::WithdrawDexShare {
who: who.clone(),
dex_share_type: lp_currency_id,
withdraw: amount,
});
Ok(())
}
}
Both functions are used to either deposit/withdraw dex shares from the LPs by passing in the currency, which is in short just like a swap. The issue is that these functions do not include any deadline protection whatsoever. The transaction could be left hanging for a long time and end up being executed in unfavorable situations.
Impact
This is a pretty popular bug case, where no deadlines have been applied and could lead to users losing funds, sometimes in USD equivalent, since their transactions might execute under less favorable conditions than intended.
Recommended Mitigation Steps
Consider requesting a deadline to which a user would like their transaction to be hanging for, and ensure that if the deadline has passed, the function does not get executed.

Summary: Critical operations lack deadline protection, allowing transactions to hang and potentially execute under unfavorable conditions, leading to possible user losses.
Audit finding: Fix typos
Project: Acala
Auditor: Code4Arena
Location: /src/modules/earning/src/lib.rs:153-158
src/modules/incentives/src/lib.rs:258-269
Impact: Low
Vulnerability class (Scout): Code Quality

[06] Fix typos
Take a look here.
 Start unbonding tokens up to `amount`.
 If bonded amount is less than `amount`, then al
 unbonding. Token will finish unbonding after `U
#[pallet::call_index(1)]
#[pallet::weight(T::WeightInfo::unbond())]
pub fn unbond(origin: OriginFor<T>, #[pallet::compa
//..sip
The sentence: “Token will finish unbonding after UnbondingPeriod blocks.” should instead be: ”Tokens will finish unbonding after UnbondingPeriod blocks.”
Another instance can be seen here.
 Claim all available multi currencies rewards fo

 The dispatch origin of this call must be `Signe

 - `pool_id`: pool type
#[pallet::call_index(2)]
#[pallet::weight(<T as Config>::WeightInfo::claim_r
pub fn claim_rewards(origin: OriginFor<T>, pool_id:
let who = ensure_signed(origin)?;
Self::do_claim_rewards(who, pool_id)
}
The first line should be 'Claim all available multi currencies rewards for specific PoolId.' instead.

Summary: Typos in comments and documentation can lead to confusion and misinterpretation of intended functionality.
Audit finding: Consider using BST instead
Project: Acala
Auditor: Code4Arena
Location: src/orml/rewards/src/lib.rs:21-26
Impact: Low
Vulnerability class (Scout): Code Quality

[07] Consider using BST instead
Take a look here.
pub struct PoolInfo<Share: HasCompact, Balance: HasCompact, CurrencyId> {
    Total shares amount
    pub total_shares: Share,
    Reward infos <reward_currency, (total_reward, total_withdrawn)>
    pub rewards: BTreeMap<CurrencyId, (Balance, Balance)>,
}
Evidently, the reward infos are stored in a B-Tree map, which is, in short, an ordered map based on a B-Tree.
B-Trees represent a fundamental compromise between cache-efficiency and actually minimizing the amount of work performed in a search. In theory, a binary search tree (BST) is the optimal choice for a sorted map, as a perfectly balanced BST performs the theoretical minimum amount of comparisons necessary to find an element (log2n).
Impact:
Non-optimized method of finding elements.
Recommended Mitigation Steps:
Consider using binary search trees instead.

Summary: Non-optimized element lookup due to the use of a B-Tree map instead of a more efficient binary search tree (BST).
Audit finding: Consider not switching oﬀ important clippy protection methods
Project: Acala
Auditor: Code4Arena
Location: src/orml/rewards/src/lib.rs:1-3
Impact: Low
Vulnerability class (Scout): Code Quality

[08] Consider not switching off important clippy protection methods
Take a look here.
#![cfg_attr(not(feature = std), no_std)]
#![allow(clippy::unused_unit)]
#![allow(clippy::too_many_arguments)]
Protocol disallows clippy to run the auto tool to warn about unused units or too many arguments; whereas too many arguments could be understandable since it’s for the claim_one() function here. A justification can’t be made for not wanting to use clippy::unused_unit.
Impact:
Bad code structure.
Recommended Mitigation Steps:
In most cases, implementations that have unused units are mostly flawed and should be sorted out.
Lambda (judge) commented:
Some of the other ones are opinionated, but not necessarily invalid.
xlc (Acala) disputed and commented:
[01], [05], and [07] are completely invalid. Others are super minor.
[01] - is 100% not an issue. This is how Substrate pallet works.
[05] - doesn’t make sense. There is no swap happening. The LP tokens are minted already and this only stakes the already minted token.
[07] - doesn’t make sense and wouldn’t make any difference anyway for the given amount of expected items. It is a premature optimization.

Summary: The code disables important Clippy protection methods, allowing potential bad code structures due to unused units and excessive function arguments.
Audit finding: EVM runtime template waives XCM message delivery fees
Project: PolkadotRunetimeTemplates
Auditor: Security Research Labs
Location: evm-template/runtime/src/configs/xcm_config.rs
Impact: High
Vulnerability class (Scout): Denial of Service (DoS) and Spamming
Status: Fixed [6] [Online]. Available: https://github.com/OpenZeppelin/polkadot-runtime-templates/pull/239

3.2.1 EVM runtime template waives XCM message delivery fees

Attack scenario:
EVM runtime template waives XCM message delivery fee

Location:
evm-template/runtime/src/configs/xcm_config.rs

Attack impact:
An attacker can cause network congestion causing delay in message deliveries or discards and storage exhaustions

Severity:
High

Status:
Fixed [6]

To avoid congestion and spamming of messages in the network via XCM, a fee is charged for message delivery to ensure fairness and optimal message delivery time amongst all the participants. This fee is configured in the runtime construct through use of FeeManager type.

In the EVM template for XCM runtime configuration for OpenZeppelin, the FeeManager [7] is set to the Rust unit type "()". In the Polkadot-SDK, this implementation effectively waives the delivery fees [8].

Thereby all fee-based congestion control mechanisms become ineffective since the fees are not actually charged. An attacker can cause congestion, possibly leading to long delays in message delivery, storage exhaustion and/or dropping of messages.

We recommend implementing a fee for XCM message delivery and follow the fix implemented in the OpenZeppelin generic template [9].

Summary: XCM message delivery fees are waived, allowing attackers to cause network congestion and message delays.
Audit finding: Incorrect runtime weights for treasury pallet
Project: PolkadotRunetimeTemplates
Auditor: Security Research Labs
Location: generic-template/runtime/src/configs/mod.rs
Impact: High
Vulnerability class (Scout): Weight Management
Status: Fixed [10] [Online]. Available: https://github.com/OpenZeppelin/polkadot-runtime-templates/pull/240.

3.2.2 Incorrect runtime weights for treasury pallet

Attack scenario:
Incorrect runtime weights for treasury pallet

Location:
generic-template/runtime/src/configs/mod.rs

Attack impact:
Extrinsic weights in treasury pallet are not dependent on the actual runtime configuration, this could lead to underweight extrinsics

Severity:
High

Status:
Fixed [10]

OpenZeppelin’s runtime template constructs runtime logics with FRAME pallets in its configuration. Appropriate benchmarking is required for the pallets to ensure its actual runtime performance. This will effectively transform into charging adequate fee for extrinsics execution.

For example, the runtime weights for the pallet treasury [11] in the EVM runtime template are configured using the default SubstrateWeight.

As these pallet extrinsic weights are not dependent on the actual runtime configuration and using default Substrate weights could lead to underweight extrinsics. An attacker may spam and bloat network storage freely using these underweight extrinsics.

The WeightInfo of the treasury pallet should be configured using actual runtime benchmarks. A best practice example for configuring the weights of the treasury pallet can be found in the Kusama runtime implementation [12].

Summary: Incorrect runtime weights for the treasury pallet could lead to underweight extrinsics, allowing attackers to spam and bloat network storage.