xanoutas/solidity-security-findings huggingface dataset:
Early user can break pool via inflation attack due to no minimum liquidity check in the incentive contract
Audit: 2024-03-acala-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

The incentive contract does not enforce a minimum liquidity limit. This means users can have as little as 1 share in the pool. This can lead to inflation attacks as described below.

Let's imagine the state of the pool is as follows:
There is a single depositor, with 1000 shares deposited. Rewards have been accumulated up to 500 tokens.

The user can then withdraw 998 shares, leaving 2 shares. They will also claim the rewards, and leave 1 reward tokens in the pool. This is the setup for the inflation attack.

The user can then deposit 1 share. The inflation is calculated as shown below:

```rust
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()
```

Here `total_reward=1`, `add_amount=1`, `initial_total_shares=2`. So the result is calculated to 0. So the inflation is 0.

After this step, the `initial_total_shares` is updated to 3. Now the user can deposit 2 wei of shares without changing the inflation amount. Next iteration, they can deposit 4 shares. This way, the user can deposit 2\*\*n shares each iteration, and inflate the `initial_total_shares` without affecting the reward inflation. This leads to the situation where the `total_shares` keeps growing according to the deposit, but the entire reward inflation mechanism is broken. This lets users steal reward tokens from other users, and is a high severity issue.

In fact whenever the `total_reward` value is less than the `total_shares`, this issue can be triggered. This is because In those conditions, users can create deposits and have the `reward_inflation` evaluate to 0. 0 `reward_inflation` basically means later users can steal rewards of earlier users, as is outlined in the docs. However this donation attack is more effective the lower the `total_shares` in the system.

## Proof of Concept

The situation can be created via the following steps.

1. Since there is no minimum deposit, we can create a situation where the `total_reward < total_shares`, and `total_shares=2`. This also works for higher values of `total_share`, but is most potent in this stage.
2. User deposits 1 share, or any number of shares as long as `deposit*total_reward/total_shares` is less than 1. `reward_inflation` will be 0, while the user gets their shares accounted for.
3. Since `total_shares` has now increased, the user can deposit more shares now, and still have the `reward_inflation` be 0. This way, the user can keep depositing shares and increasing the `total_shares` without affecting the reward inflation.
4. Since `reward_inflation` and thus `total_reward` has not increased, but the `total_shares` have in
`transfer_share_and_rewards` can be used to transfer out shares without transferring reward debt due to rounding.
Audit: 2024-03-acala-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

The function `transfer_share_and_rewards` can be used to split up the position in a single account into multiple accounts. The contract sends some of the shares to be held by the second account, and similarly also updates the reward debt of the receiving account so that the receiver cannot take out more rewards than they deserve.

This is calculated in the following snippet. `move_balance` is the amount of the reward debt that is to be transferred to the receiver.

```rust
let move_balance = U256::from(balance.to_owned().saturated_into::<u128>())
    * U256::from(move_share.to_owned().saturated_into::<u128>())
    / U256::from(share.to_owned().saturated_into::<u128>());
```

Here we see the calculation is simple and by default is rounded down. So if `balance*move_share` is lower than `share`, `move_balance` evaluates to 0. So the receiving account's reward debt is not increased at all!

```rust
increased_rewards
    .entry(*reward_currency)
    .and_modify(|increased_reward| {
        *increased_reward = increased_reward.saturating_add(move_balance);
```

Since `move_balance` is 0, the `increased_reward` is not updated. This means the new account now has shares, but no reward debt. So the receiving account can claim rewards that were already claimed.

This can be done multiple times to drain the reward pool.

The criteria is that `balance*move_share` has to be lower than `share`. This can be achieved by sending a small fraction of the funds to the receiving account, such that `move_share` is much lower than `share`. Also, if `balance`, the reward debt of the sender is low, this facilitates the attack more.

## Proof of Concept

A short POC is shown here demonstrating the issue. The attacker sends to the receiver a small share of their total. The receiver is shown to have no reward debt, while the sender does have reward debt. This shows that the receiver can claim rewards already claimed by the sender.

```rust
#[test]
fn test_rounding() {
ExtBuilder::default().build().execute_with(|| {
    RewardsModule::add_share(&ALICE, &DOT_POOL, 1000);
    assert_ok!(RewardsModule::accumulate_reward(&DOT_POOL, NATIVE_COIN, 100));
    // RewardsModule::add_share(&BOB, &DOT_POOL, 100);
    RewardsModule::claim_rewards(&ALICE, &DOT_POOL);

    let user_stat = RewardsModule::shares_and_withdrawn_rewards(DOT_POOL, ALICE);
    println!("ALICE stat before transfer: {:?}", user_stat);
    let user_stat = RewardsModule::shares_and_withdrawn_rewards(DOT_POOL, BOB);
    println!("BOB stat before transfer: {:?}", user_stat);

    assert_ok!(RewardsModule::transfer_share_and_rewards(&ALICE, &DOT_POOL, 5, &BOB));

    let user_stat = RewardsModule::shares_and_withdrawn_rewards(DOT_POOL, ALICE);
    println!("ALICE stat after transfer: {:?}", user_stat);
    let user_stat = RewardsModule::shar
The user can get less penalty for the early withdrawal as the deduction amount goes again into rewards
Audit: 2024-03-acala-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-acala/blob/main/src/modules/incentives/src/lib.rs#L503


# Vulnerability details

## Impact

Inside of `incentives` pallet, there is a deduction rate that represents a penalty if the user decides to claim his rewards early. The problem is that if that's a partial removal of shares, the user can still get rewards for these shares but the deduction rate will go to the pool and the user will be able to get rewards again but for the remaining amount of shares.

## Proof of Concept

1. The user has 100 shares and decides to partially remove some of the shares and calls `withdraw_dex_share()`. This function, in its turn, calls `do_withdraw_dex_share()` where there is another call to the `rewards` pallet and `remove_share()` function where the rewards will be claimed:

https://github.com/code-423n4/2024-03-acala/blob/main/src/modules/incentives/src/lib.rs#L533
```
	<orml_rewards::Pallet<T>>::remove_share(who, &PoolId::Dex(lp_currency_id), amount.unique_saturated_into());
```

https://github.com/code-423n4/2024-03-acala/blob/main/src/orml/rewards/src/lib.rs#L197
```
Self::claim_rewards(who, pool);
```

Deduction rate that represents some form of a penalty is calculated and substracted from the withdrawal amount and then reaccumulated to the pool meaning that the user will still have the opportunity to claim part of this amount (not all of this as there are other users in the pool) but some of it making his penalty less than it should be:

https://github.com/code-423n4/2024-03-acala/blob/main/src/modules/incentives/src/lib.rs#L451-456
```
match Self::payout_reward_and_reaccumulate_reward(
						pool_id,
						&who,
						*currency_id,
						payout_amount,
						deduction_amount,
```

https://github.com/code-423n4/2024-03-acala/blob/main/src/modules/incentives/src/lib.rs#L503
```
<orml_rewards::Pallet<T>>::accumulate_reward(&pool_id, reward_currency_id, reaccumulate_amount)?;
```

Therefore, if deduction rate is set to 0.05% if the users claim before one month then this amount will be reaccumulated and the user may wait until the period and get the rewards without deduction rate for his remaining 50 shares + part of the amount that was reaccumulated (another part of it will go to the other people in the pool). It basically allows to get the less penalty than it should be 

## Tools Used

Manual review.

## Recommended Mitigation Steps

Change the reaccumulation mechanism.


## Assessed type

Other
Claiming rewards while the deduction rate is != 0, allows for repeated withdrawal of redistributed rewards
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#L451-L457


# Vulnerability details

## Impact
If a participant in a pool claims rewards while the deduction rate is != 0, the deducted rewards are redistributed between all the participants.
```Rust
fn payout_reward_and_reaccumulate_reward(
	pool_id: PoolId,
	who: &T::AccountId,
	reward_currency_id: CurrencyId,
	payout_amount: Balance,
	reaccumulate_amount: Balance,
) -> DispatchResult {
	if !reaccumulate_amount.is_zero() {
		<orml_rewards::Pallet<T>>::accumulate_reward(&pool_id, reward_currency_id, reaccumulate_amount)?;
	}
	T::Currency::transfer(reward_currency_id, &Self::account_id(), who, payout_amount)?;
	Ok(())
}

```
Since the deduction is redistributed between all participants (since `accumulate_reward` distributes to all participating users) including the one claiming the reward, they can repeatedly claim rewards and receive more of the rewards pool than they probably should.


## Proof of Concept
```Rust
#[test]
fn repeated_claiming() {
	ExtBuilder::default().build().execute_with(|| {
		assert_ok!(TokensModule::deposit(BTC, &VAULT::get(), 10000));

		assert_ok!(IncentivesModule::update_claim_reward_deduction_rates(
			RuntimeOrigin::signed(ROOT::get()),
			vec![(PoolId::Dex(BTC_AUSD_LP), Rate::saturating_from_rational(0, 100))]
		));

		assert_ok!(IncentivesModule::update_claim_reward_deduction_rates(
			RuntimeOrigin::signed(ROOT::get()),
			vec![(PoolId::Dex(BTC_AUSD_LP), Rate::saturating_from_rational(20, 100))]
		));

		// 40% deduction rate
		assert_ok!(IncentivesModule::update_claim_reward_deduction_rates(
			RuntimeOrigin::signed(ROOT::get()),
			vec![(PoolId::Dex(BTC_AUSD_LP), Rate::saturating_from_rational(40, 100))]
		));

		// give RewardsSource tokens
		TokensModule::deposit(BTC, &RewardsSource::get(), 10000);

		// add 11 participants with equal shares
		TokensModule::deposit(BTC_AUSD_LP, &ALICE::get(), 100);
		TokensModule::deposit(BTC_AUSD_LP, &BOB::get(), 100);


		// 1000 BTC as reward
		IncentivesModule::update_incentive_rewards(RuntimeOrigin::signed(ROOT::get()), vec![(PoolId::Dex(BTC_AUSD_LP), vec![(BTC, 1000)])]);
		
		// all 11 participants deposit their share
		assert_ok!(IncentivesModule::deposit_dex_share(RuntimeOrigin::signed(ALICE::get()), BTC_AUSD_LP, 100));
		assert_ok!(IncentivesModule::deposit_dex_share(RuntimeOrigin::signed(BOB::get()), BTC_AUSD_LP, 100));
		
		// first accumulation of rewards
		IncentivesModule::on_initialize(10);
		
		// ALICE claims their reward and should receive ((1000 // 2) * 0.6) = 300 BTC
		assert_ok!(IncentivesModule::claim_rewards(RuntimeOrigin::signed(ALICE::get()), PoolId::Dex(BTC_AUSD_LP)));
		let btc_before = TokensModule::total_balance(BTC, &ALICE::get());
		
		// here ALICE should have claimed all their rewards and not be able to claim anymore
		assert_ok!(IncentivesModule::claim_rewards(RuntimeOrigin::signed(ALICE::get()), PoolId::D
Native rewards will be lost after instant unbound `unbond_instant`
Audit: 2024-03-acala-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-acala/blob/main/src/modules/earning/src/lib.rs#L190-L196
https://github.com/code-423n4/2024-03-acala/blob/main/src/modules/incentives/src/lib.rs#L607-L619


# Vulnerability details

## Issue Description

When a user creates a bond for their native currency, the corresponding bond amount will be deposited as shares into the rewards contract after the `T::OnBonded` call, which invokes the `OnEarningBonded` function (adding shares to the rewards). 

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

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

    if let Some(change) = change {
        T::OnBonded::happened(&(who.clone(), change.change));
        Self::deposit_event(Event::Bonded {
            who,
            amount: change.change,
        });
    }
    Ok(())
}
```

```rust
pub struct OnEarningBonded<T>(sp_std::marker::PhantomData<T>);
impl<T: Config> Happened<(T::AccountId, Balance)> for OnEarningBonded<T> {
	fn happened((who, amount): &(T::AccountId, Balance)) {
		<orml_rewards::Pallet<T>>::add_share(who, &PoolId::Earning(T::NativeCurrencyId::get()), *amount);
	}
}
```

However, when a user decides to unbound instantly by calling `unbond_instant`, which frees and removes the user shares while imposing an unstake fee (penalty for unbounding early), there is a discrepancy in the removal of shares:

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

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

    if let Some(change) = change {
        T::OnBonded::happened(&(who.clone(), change.change));
        Self::deposit_event(Event::Bonded {
            who,
            amount: change.change,
        });
    }
    Ok(())
}
```

```rust
pub struct OnEarningBonded<T>(sp_std::marker::PhantomData<T>);
impl<T: Config> Happened<(T::AccountId, Balance)> for OnEarningBonded<T> {
	fn happened((who, amount): &(T::AccountId, Balance)) {
		<orml_rewards::Pallet<T>>::add_share(who, &PoolId::Earning(T::NativeCurrencyId::get()), *amount);
	}
}
```

The issue arises when attempting to remove the shares through the `T::OnUnbonded` call, which invokes `remove_share` under the hood. Only the amount minus the fee (`final_amount`) is removed from the shares, not the fully unbounded amount (`amount`). This means that the portion of shares corresponding to the fee will not be removed from the rewards pool and will remain in there without the possibility of removal later.

As a result, the native rewards total shares will contain a portion of shares that cannot be removed (redeemed), stemming from the unstaking fees when users do an instant unbounding. Consequently, this portion will result in a loss of a portion of the total rewards accumulated in the pool as they cannot be claimed by anyone.

## Impact

The unst
transfer_share_and_rewards do not check calculate the amount of rewards to be transferred
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#L326-L362


# Vulnerability details

## Impact
if the amount imbalance changes rapidly, users may not receive their fair share of the imbalance when transferring shares, prevents attackers from transferring more rewards than they actually have, thereby ensuring the integrity of the reward pool system.
based on the ratio of intervening shares to the total share account. However the implementation does not check whether the total account rewards are enough to cover the transferred rewards.

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


## Proof of Concept
first calculate the total account rewards by adding up the balances of all reward currencies. then calculates the amount of rewards to be transferred for each reward currency based on the ratio of shares transferred to total account shares. Before making a transfer check whether the total account rewards are sufficient to cover the transferred rewards by ensuring that the total rewards are greater than or equal to the amount of rewards transferred. If this check fails

```rust 
use super::*;
use frame_support::{assert_err, assert_ok};

   
    #[test]
    fn transfer_share_and_rewards_works() {
        new_test_ext().execute_with(|| {
            let who = 1;
            let pool = 1;
            let move_share = 10;
            let other = 2;

            // Set initial shares and rewards for 'who' account
            SharesAndWithdrawnRewards::<Test>::insert(
                &pool,
                &who,
                (100, btreemap! { 1 => 100 }),
            );

            // Execute the transfer
            assert_ok!(Pallet::<Test>::transfer_share_and_rewards(
                &who, &pool, move_share, &other
            ));

            // Check the updated shares and rewards for 'who' and 'other' accounts
            assert_eq!(
                SharesAndWithdrawnRewards::<Test>::get(&pool, &who),
                (90, btreemap! { 1 => 90 }),
            );
            assert_eq!(
                SharesAndWithdrawnRewards::<Test>::get(&pool, &other),
                (10, btreemap! { 1 => 10 }),
            );
        });
    }

    #[test]
    fn transfer_share_and_rewards_fails_with_insufficient_share() {
        new_test_ext().execute_with(|| {
            let who = 1;
            let pool = 1;
            let move_share = 110; // Trying to transfer more than available shares
            let other = 2;

            // Set initial shares and rewards for 'who' account
            SharesAndWithdrawnRewards::<Test>::insert(
                &pool,
                &who,
                (100, btreemap! { 1 => 100 }),
            );

            // Execute the transfer (should fail)
            assert_err!(
                Pallet::<Test>::transfer_share_and_rewards(&who, &pool, move
transfer_share_and_rewards do not check calculate the amount of rewards to be transferred
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#L326-L362


# Vulnerability details

## Impact
prevents attackers from transferring more rewards than they actually have, thereby ensuring the integrity of the reward pool system.
based on the ratio of intervening shares to the total share account. However the implementation does not check whether the total account rewards are enough to cover the transferred rewards.

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


## Proof of Concept
first calculate the total account rewards by adding up the balances of all reward currencies. then calculates the amount of rewards to be transferred for each reward currency based on the ratio of shares transferred to total account shares. Before making a transfer check whether the total account rewards are sufficient to cover the transferred rewards by ensuring that the total rewards are greater than or equal to the amount of rewards transferred. If this check fails

```rust 
use super::*;
use frame_support::{assert_err, assert_ok};
use frame_system::Pallet as System;

#[test]
fn test_transfer_share_and_rewards() {
    // Initialize test runtime
    let mut test_ext = sp_io::TestExternalities::new(test_client().into());
    test_ext.initialize();

    // Create new reward pool
    let pool_id = PoolId::default();
    assert_ok!(RewardPool::create_pool(Origin::signed(1), pool_id.clone()));

    // Add some shares and rewards to the pool
    let share = 100;
    let reward_currency = 1;
    let reward_amount = 1000;
    assert_ok!(RewardPool::add_share(Origin::signed(1), &pool_id, share));
    assert_ok!(RewardPool::accumulate_reward(
        &pool_id,
        reward_currency,
        reward_amount
    ));

    // Transfer some shares and rewards to another account
    let other = 2;
    let move_share = 50;
    assert_ok!(RewardPool::transfer_share_and_rewards(
        &Origin::signed(1),
        &pool_id,
        move_share,
        &other
    ));

    // Assert that the expected events were emitted
    let expected_events = vec![
        Event::RewardPool(crate::Event::ShareTransferred {
            who: 1,
            pool: pool_id,
            other,
            shares: move_share,
        }),
        Event::RewardPool(crate::Event::RewardTransferred {
            who: 1,
            pool: pool_id,
            other,
            currency: reward_currency,
            amount: reward_amount * move_share / share,
        }),
    ];
    assert_eq!(System::events(), expected_events);

    // Assert that the state of the reward pool was updated correctly
    let (total_shares, rewards) = RewardPool::shares_and_withdrawn_rewards(&pool_id, &1);
    assert_eq!(total_shares, share - move_share);
    assert_eq!(rewards[&reward_currency], reward_amount - reward_amount * move_share / share);

    let (other_tot