xanoutas/solidity-security-findings huggingface dataset:
Incentive accumulation can be sandwiched with additional shares to gain advantage over long-term depositors
Audit: 2024-03-acala-findings
Severity: MEDIUM

# 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](https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/incentives/src/lib.rs#L207) periodically in intervals of [T::AccumulatePeriod](https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/incentives/src/lib.rs#L190) blocks. Thereby, a **fixed** incentive reward amount, which is set via [update_incentive_rewards(...)](https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/incentives/src/lib.rs#L280-L311), 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](https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/incentives/src/lib.rs#L230-L238) (DEX) shares before [accumulate_incentives(...)](https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/incentives/src/lib.rs#L207) is called via the `on_initialize` hook. *This is also demonstrated in the [transfer_reward_and_update_rewards_storage_atomically_when_accumulate_incentives_work()](https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/incentives/src/tests.rs#L1173-L1194) test case.*  
Afterwards, rewards can be immediately [claimed](https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/incentives/src/lib.rs#L265-L269) and (DEX) shares can be [withdrawn](https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/incentives/src/lib.rs#L248-L256) **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](https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/incentives/src/lib.rs#L207) 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](https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/incentives/src/lib.rs#L207) and therefore obtain an unfairly high share of the fixed rewards.


For reference, see also the *earnings* module, where an unbonding
When initial_total_shares is zero, the checked_div operation will attempt to divide by zero, resulting in a panic.
Audit: 2024-03-acala-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/orml/rewards/src/lib.rs#L158-L167
https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/orml/rewards/src/lib.rs#L163


# Vulnerability details

## Summary
`add_share` function is responsible for updating user shares and pool information when a user adds liquidity to a pool. It calculates the reward inflation based on the user's added share and the total pool shares.

https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/orml/rewards/src/lib.rs#L158-L167

```rust
pub fn add_share(who: &T::AccountId, pool: &T::PoolId, add_amount: T::Share) {
    PoolInfos::<T>::mutate(pool, |pool_info| {
        let initial_total_shares = pool_info.total_shares;
        pool_info.total_shares = pool_info.total_shares.saturating_add(add_amount);
        
        pool_info.rewards.iter_mut().for_each(|(reward_currency, (total_reward, total_withdrawn_reward))| {
            let reward_inflation = if initial_total_shares.is_zero() {
                Zero::zero()
            } else {
                U256::from(add_amount.to_owned().saturated_into::<u128>())                          <
                    .saturating_mul(total_reward.to_owned().saturated_into::<u128>().into())        <
                    .checked_div(initial_total_shares.to_owned().saturated_into::<u128>().into())   <
                    .unwrap_or_default()                                                            <
                    .as_u128()                                                                      <
                    .saturated_into()                                                               <
            };
            *total_reward = total_reward.saturating_add(reward_inflation);
            *total_withdrawn_reward = total_withdrawn_reward.saturating_add(reward_inflation);
        });
        
        SharesAndWithdrawnRewards::<T>::mutate(pool, who, |(share, withdrawn_rewards)| {
            *share = share.saturating_add(add_amount);
            // ... (update withdrawn rewards for each currency)
        });
    });
}
```
The code checks if `initial_total_shares` is zero and returns zero reward inflation in that case. However, the `checked_div` operation is still executed unconditionally, even when `initial_total_shares` is zero.

[   The line of code responsible for the vulnerability is:](https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/orml/rewards/src/lib.rs#L163)
```rust
.checked_div(initial_total_shares.to_owned().saturated_into::<u128>().into())
```
If `initial_total_shares` is zero, the `checked_div` operation will attempt to divide by zero, resulting in a panic.

> When `initial_total_shares` is non-zero, the `add_share` function is expected to calculate the reward inflation correctly based on the user's added share and the total pool sha
Unauthorized access allows manipulation of critical token operations, risking economic stability.
Audit: 2024-03-acala-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/earning/src/lib.rs#L158-L173


# Vulnerability details

## Description
In the access control of the following public functions in the Acala Earning module:
- [`unbond()`](https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/earning/src/lib.rs#L158-L173)
- [`unbond_instant()`](https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/earning/src/lib.rs#L180-L205)
- [`rebond()`](https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/earning/src/lib.rs#L212-L227)
- [`withdraw_unbonded()`](https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/earning/src/lib.rs#L231-L246)

These functions are responsible for performing critical operations related to unbonding, instant unbonding, rebonding, and withdrawing unbonded funds.

[lib.rs#unbond](https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/earning/src/lib.rs#L158-L173)

```rust
pub fn unbond(origin, #[pallet::compact] amount: Balance) -> DispatchResult {
    let who = ensure_signed(origin)?;

    let unbond_at = frame_system::Pallet::<T>::block_number().saturating_add(T::UnbondingPeriod::get());
    let change = <Self as BondingController>::unbond(&who, amount, unbond_at)?;

    if let Some(change) = change {
        T::OnUnbonded::happened(&(who.clone(), change.change));
        Self::deposit_event(Event::Unbonded {
            who,
            amount: change.change,
        });
    }

    Ok(())
}
```

The access control for these functions relies on the `ensure_signed` function from the Substrate runtime, which checks that the origin of the transaction is a signed account.

If the access control is not properly implemented or configured, unauthorized users may be able to call the sensitive functions of the Earning module without proper permissions. This could lead to unintended behavior, such as:
- Unauthorized unbonding of tokens on behalf of other users.
- Instant unbonding of tokens without proper authorization.
- Rebonding of tokens without the necessary permissions.
- Unauthorized withdrawal of unbonded funds.

The actual behavior deviates from the expected behavior, as the access control mechanisms fail to restrict access to the functions based on the intended permissions and authorization levels.

## Vulnerability Details
The vulnerability arises if the access control for the Earning module's functions is not properly configured or enforced. If the Substrate runtime's permission system is not set up correctly, it could allow unauthorized users to call the sensitive functions of the Earning module.

For example, if the runtime allows any account to call the `unbond()` function without proper authorization checks, a malicious actor could poten
Unfair manipulation of unbonding process, leading to potential financial losses.
Audit: 2024-03-acala-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/earning/src/lib.rs#L158-L173
https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/earning/src/lib.rs#L231-L245


# Vulnerability details

## Description
The vulnerability affects the `unbond()` and `withdraw_unbonded()` functions in the Acala Earning module. 

[lib.rs#unbond](https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/earning/src/lib.rs#L158-L173), [lib.rs#withdraw_unbonded](https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/earning/src/lib.rs#L231-L245)

When a user submits an unbonding or withdrawal request, it should be processed fairly and in the order it was received. The user's unbonding share should be calculated based on the total bonded balance at the time of their request, and their withdrawal should be proportional to their unbonded share.

```rust
pub fn unbond(origin: OriginFor<T>, #[pallet::compact] amount: Balance) -> DispatchResult {
    let who = ensure_signed(origin)?;


    let unbond_at = frame_system::Pallet::<T>::block_number().saturating_add(T::UnbondingPeriod::get());
    let change = <Self as BondingController>::unbond(&who, amount, unbond_at)?;


    if let Some(change) = change {
        T::OnUnbonded::happened(&(who.clone(), change.change));
        Self::deposit_event(Event::Unbonded {
            who,
            amount: change.change,
        });
    }


    Ok(())
}

pub fn withdraw_unbonded(origin: OriginFor<T>) -> DispatchResult {
            let who = ensure_signed(origin)?;


            let change =
                <Self as BondingController>::withdraw_unbonded(&who, frame_system::Pallet::<T>::block_number())?;


            if let Some(change) = change {
                Self::deposit_event(Event::Withdrawn {
                    who,
                    amount: change.change,
                });
            }


            Ok(())
        }
```

> Users can initiate the unbonding process by specifying the amount of tokens they want to unbond. The function calculates the unbonding time by adding the `UnbondingPeriod` to the current block number and calls the `unbond()` function from the `BondingController` trait to perform the unbonding.

> The `withdraw_unbonded()` function allows users to withdraw their unbonded tokens once the unbonding period has elapsed. It calls the `withdraw_unbonded()` function from the `BondingController` trait to process the withdrawal.

## Vulnerability Details
_From the fact that transactions in a blockchain network are broadcasted to the network and can be seen by all participants before they are included in a block. An attacker can monitor the transaction pool and detect incoming unbonding or withdrawal requests._

By strategically placing their own transactions ahead of the victim's transaction, the attacker
Attackers can unbond tokens without paying fees or manipulate process.
Audit: 2024-03-acala-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/earning/src/lib.rs#L180-L205
https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/earning/src/lib.rs#L192-L194


# Vulnerability details

## Description
The `unbond_instant()` function is expected to withdraw the instant unstake fee from the user's account and transfer it to the treasury account. The user should receive the unbonded amount minus the fee, and the unbonding should be processed instantly.

The function enables users to instantly unbond their tokens by paying a fee. 

[lib.rs#pub fn unbond_instant](https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/earning/src/lib.rs#L180-L205)
```rust
pub fn unbond_instant(origin: OriginFor<T>, #[pallet::compact] amount: Balance) -> DispatchResult {
    let who = ensure_signed(origin)?;


    let fee_ratio = T::ParameterStore::get(InstantUnstakeFee).ok_or(Error::<T>::NotAllowed)?;


    let change = <Self as BondingController>::unbond_instant(&who, amount)?;


    if let Some(change) = change {
        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::TRANSFER, ExistenceRequirement::KeepAlive)?;
        T::OnUnstakeFee::on_unbalanced(unbalance);


        T::OnUnbonded::happened(&(who.clone(), final_amount));
        Self::deposit_event(Event::InstantUnbonded {
            who,
            amount: final_amount,
            fee,
        });
    }


    Ok(())
}
```
1. It retrieves the instant unstake fee ratio from the `ParameterStore`.
2. It calls the `unbond_instant()` function from the `BondingController` trait to perform the instant unbonding.
3. If the unbonding is successful, it calculates the fee based on the fee ratio and the unbonded amount.
4. It withdraws the fee from the user's account using the `Currency` trait and transfers it to the treasury account via the `OnUnstakeFee` hook.
5. It emits an `InstantUnbonded` event with the final unbonded amount and the fee.

## Vulnerability Details
If the Balances module which is used to withdraw the instant unstake fee, allows reentrancy. Reentrancy occurs when an external contract or module can call back into the original contract or module before the initial function execution is complete, potentially leading to unexpected behavior or exploitation.

We can see in this line: [lib.rs#192-L194](https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/earning/src/lib.rs#L192-L194)
```rust
let unbalance =
T::Currency::withdraw(&who, fee, WithdrawReasons::TRANSFER, ExistenceRequirement::KeepAlive)?;
T::OnUnstakeFee::on_unbalanced(unbalance);
```

If the `withdraw` function in the Balances module allows reentrancy, an attacker cou
Rewards remain unclaimed when withdrawing DEX shares
Audit: 2024-03-acala-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/incentives/src/lib.rs#L248-L256
https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/incentives/src/lib.rs#L525-L541


# Vulnerability details

## Description

DEX shares can be withdrawn from the *incentives* module via [withdraw_dex_share(...)](https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/incentives/src/lib.rs#L248-L257) (subsequently [do_withdraw_dex_share(...)](https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/incentives/src/lib.rs#L525-L541)) and rewards can be claimed via [claim_rewards(...)](https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/incentives/src/lib.rs#L265-L269) (subsequently [do_claim_rewards(...)](https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/incentives/src/lib.rs#L416-L491)).  
However, the methods involved in withdrawing DEX shares do **not** invoke the claiming of rewards.  
Therefore, users can easily miss out on [accumulated incentiives](https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/incentives/src/lib.rs#L207) when withdrawing shares, if they don't explicitly claim them within the same block of/before withdrawal.


## Tools Used
Manual review

## Recommended Mitigation Steps

Consider the following addition to [withdraw_dex_share(...)](https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/incentives/src/lib.rs#L248-L257):

```diff
diff --git a/src/modules/incentives/src/lib.rs b/src/modules/incentives/src/lib.rs
index 2cacb0f..4ff7e07 100644
--- a/src/modules/incentives/src/lib.rs
+++ b/src/modules/incentives/src/lib.rs
@@ -251,6 +251,7 @@ pub mod module {
 			#[pallet::compact] amount: Balance,
 		) -> DispatchResult {
 			let who = ensure_signed(origin)?;
+			Self::do_claim_rewards(who.clone(), PoolId::Dex(lp_currency_id))?;
 			Self::do_withdraw_dex_share(&who, lp_currency_id, amount)?;
 			Ok(())
 		}

```
*All tests are still passing after this addition.*


## Assessed type

Other
Unauthorized transfers and record manipulation lead to inaccurate reward distribution.
Audit: 2024-03-acala-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

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

## Impact
- Unauthorized transfer of rewards from the `RewardsSource` account to unintended recipients.
- Manipulation of pool reward records, leading to inaccurate reward tracking and distribution.


## Function Description
```rust
fn accumulate_incentives(pool_id: PoolId) {
    for (reward_currency_id, reward_amount) in IncentiveRewardAmounts::<T>::iter_prefix(pool_id) {
        if reward_amount.is_zero() {
            continue;
        }


        // ignore result so that failure will not block accumulate other type reward for the pool
        let _ =
            Self::transfer_rewards_and_update_records(pool_id, reward_currency_id, reward_amount).map_err(|e| {
                log::warn!(
                    target: "incentives",
                    "accumulate_incentives: failed to accumulate {:?} {:?} rewards for pool {:?} : {:?}",
                    reward_amount, reward_currency_id, pool_id, e
                );
            });
    }
}
```
The `accumulate_incentives` method is to accumulate and distribute incentive rewards for a given pool across multiple reward currencies. It takes a `pool_id` parameter and iterates over the `IncentiveRewardAmounts` storage map to retrieve the reward amounts for each currency associated with the pool. For each non-zero reward amount, it calls the `transfer_rewards_and_update_records` function to transfer the rewards from the configured `RewardsSource` account to the Incentives pallet's account and update the pool's reward records using the Rewards pallet.

## Vulnerability Details:
The method assumes that the `IncentiveRewardAmounts` storage map contains only valid pool IDs and currency IDs. However, if there is a way to manipulate or incorrectly set the values in `IncentiveRewardAmounts`, it could lead to invalid pools or tokens receiving incentives.

Specifically: [Here](https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/incentives/src/lib.rs#L381-L388)

```rust
for (reward_currency_id, reward_amount) in IncentiveRewardAmounts::<T>::iter_prefix(pool_id) {
    // ...
    let _ =
    Self::transfer_rewards_and_update_records(pool_id, reward_currency_id, reward_amount).map_err(|e| {
        // ...
    });
}
```
> The `accumulate_incentives` method should only process incentives for valid and approved pool IDs and currency IDs. It should ensure that the `pool_id` parameter represents a legitimate pool type (e.g., `Loans`, `Dex`) and that the `reward_currency_id` is a valid and supported currency for incentives.

`Instead, the `accumulate_incentives` method may process incentives for invalid or unintended pools and tokens. If an attacker can man
Unauthorized pools receive rewards, causing misallocation and manipulation of records.
Audit: 2024-03-acala-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

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

## Impact
- Distribution of incentives to invalid or unauthorized pools, resulting in the misallocation of rewards.
- Unintended transfer of rewards from the `RewardsSource` account to unauthorized recipients.
- Manipulation of pool reward records, leading to inconsistencies in reward tracking and distribution.

## Function Description
```rust
for (reward_currency_id, reward_amount) in IncentiveRewardAmounts::<T>::iter_prefix(pool_id) {
    if reward_amount.is_zero() {
        continue;
    }


    let _ =
        Self::transfer_rewards_and_update_records(pool_id, reward_currency_id, reward_amount).map_err(|e| {
```

The `accumulate_incentives` function is to accumulate and distribute incentive rewards for a given pool across multiple reward currencies. It takes a `pool_id` parameter and iterates over the `IncentiveRewardAmounts` storage map to retrieve the reward amounts for each currency associated with the pool. For each non-zero reward amount, it calls the `transfer_rewards_and_update_records` function to transfer the rewards from the configured `RewardsSource` account to the Incentives pallet's account and update the pool's reward records using the Rewards pallet.

## Vulnerability Details
The `pool_id` and `reward_currency_id` parameters are within the `accumulate_incentives` function. The function assumes that the `IncentiveRewardAmounts` storage map contains only valid pool IDs and currency IDs. With a way to manipulate or incorrectly set the values in `IncentiveRewardAmounts`, it could lead to the distribution of incentives to invalid pools or currencies.

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

```rust
for (reward_currency_id, reward_amount) in IncentiveRewardAmounts::<T>::iter_prefix(pool_id) {
    // ...
    let _ = 
         Self::transfer_rewards_and_update_records(pool_id, reward_currency_id, reward_amount).map_err(|e| {
        // ...
    });
}
```
> Should only process incentives for valid and approved pool IDs and currency IDs. It should ensure that the `pool_id` parameter represents a legitimate pool type (e.g., `Loans`, `Dex`) and that the `reward_currency_id` is a valid and supported currency for incentives.

`Instead, the `accumulate_incentives` function may process incentives for invalid or unintended pools and currencies if the `IncentiveRewardAmounts` storage is manipulated to contain invalid IDs.`

## Proof of Concept
Let's consider a scenario where an attacker discovers