xanoutas/solidity-security-findings huggingface dataset:
Attacker can bloat up the system by adding minute values of shares on multiple instances to different pools 
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#L142-L202


# Vulnerability details


## Proof of Concept

Take a look at https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/orml/rewards/src/lib.rs#L142-L202

```rust

	pub fn add_share(who: &T::AccountId, pool: &T::PoolId, add_amount: T::Share) {
		if add_amount.is_zero() {
			return;
		}

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

			let mut withdrawn_inflation = Vec::<(T::CurrencyId, T::Balance)>::new();

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

					withdrawn_inflation.push((*reward_currency, reward_inflation));
				});

			SharesAndWithdrawnRewards::<T>::mutate(pool, who, |(share, withdrawn_rewards)| {
				*share = share.saturating_add(add_amount);
				// update withdrawn inflation for each reward currency
				withdrawn_inflation
					.into_iter()
					.for_each(|(reward_currency, reward_inflation)| {
						withdrawn_rewards
							.entry(reward_currency)
							.and_modify(|withdrawn_reward| {
								*withdrawn_reward = withdrawn_reward.saturating_add(reward_inflation);
							})
							.or_insert(reward_inflation);
					});
			});
		});
	}
```

This function is used to add shaees to a pool, now see https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/orml/rewards/src/lib.rs#L202-L252

```rust

	pub fn remove_share(who: &T::AccountId, pool: &T::PoolId, remove_amount: T::Share) {
		if remove_amount.is_zero() {
			return;
		}

		// claim rewards firstly
		Self::claim_rewards(who, pool);

		SharesAndWithdrawnRewards::<T>::mutate_exists(pool, who, |share_info| {
			if let Some((mut share, mut withdrawn_rewards)) = share_info.take() {
				let remove_amount = remove_amount.min(share);

				if remove_amount.is_zero() {
					return;
				}

				PoolInfos::<T>::mutate_exists(pool, |maybe_pool_info| {
					if let Some(mut pool_info) = maybe_pool_info.take() {
						let removing_share = U256::from(remove_amount.saturated_into::<u128>());

						pool_info.total_shares = pool_info.total_shares.saturating_sub(remove_amount);

						// update withdrawn rewards for each reward cu
Storage can be bloated with low liquidtiy positions
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#L230


# Vulnerability details

## Impact

The `deposit_dex_share` function enforce no minimum amount that can be deposited into the pool allows for creating multiple pool positions. This causes that in a coordinated effort, for a pretty cheap cost, users/attackers can create multiple low liquidity positions to bloat the runtime storage. This is very important as substrate framework requires optimization of storage to prevent bloat which can lead to high maintenance costs for the chain and a potential DOS. A more in detail explanation can be found [here](https://docs.substrate.io/build/troubleshoot-your-code/#storage).

## Proof of Concept

The test case below shows how a user can create multiple 1 wei positions, and it can be added to [test.rs](https://github.com/code-423n4/2024-03-acala/blob/main/src/modules/incentives/src/tests.rs).
```rust
#[test]
fn open_low_liquidity_positions() {
	ExtBuilder::default().build().execute_with(|| {
		assert_ok!(TokensModule::deposit(BTC_AUSD_LP, &ALICE::get(), 1000000000));
		assert_eq!(TokensModule::free_balance(BTC_AUSD_LP, &ALICE::get()), 1000000000);
		assert_eq!(
			TokensModule::free_balance(BTC_AUSD_LP, &IncentivesModule::account_id()),
			0
		);
		assert_eq!(RewardsModule::pool_infos(PoolId::Dex(BTC_AUSD_LP)), PoolInfo::default(),);
		assert_eq!(
			RewardsModule::shares_and_withdrawn_rewards(PoolId::Dex(BTC_AUSD_LP), ALICE::get()),
			Default::default(),
		);
		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!(TokensModule::free_balance(BTC_AUSD_LP, &ALICE::get()), 999999997);
		assert_eq!(
			TokensModule::free_balance(BTC_AUSD_LP, &IncentivesModule::account_id()),
			3
		);
		assert_eq!(
			RewardsModule::pool_infos(PoolId::Dex(BTC_AUSD_LP)),
			PoolInfo {
				total_shares: 3,
				..Default::default()
			}
		);
		assert_eq!(
			RewardsModule::shares_and_withdrawn_rewards(PoolId::Dex(BTC_AUSD_LP), ALICE::get()),
			(3, Default::default())
		);
	});
}
```

## Tools Used
Manual code review

## Recommended Mitigation Steps
Introduce a minimum deposit amount.





## Assessed type

Other
transfer_share_and_rewards can be used to claim rewards from other users
Audit: 2024-03-acala-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Description

The orml-rewards module is designed to calculate how many rewards a user receives. The module tracks how many shares the user has deposited into the protocol and calculates how much the user will receive when they claim their rewards. It then records how much the user has claimed to prevent double claiming. Additionally, a user can transfer their shares to another user using the `transfer_share_and_rewards` function within this module.  If rewards have already been claimed for the shares being transferred, the amount of rewards claimed must also be recorded for the new owner of the shares to prevent them from claiming rewards again for the same shares. However, in the calculation, the amount of rewards already claimed for the tokens is rounded down:

```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>());
					let move_balance: Option<u128> = move_balance.try_into().ok();
					if let Some(move_balance) = move_balance {
						let move_balance: T::Balance = move_balance.unique_saturated_into();
						*balance = balance.saturating_sub(move_balance);
						increased_rewards
							.entry(*reward_currency)
							.and_modify(|increased_reward| {
								*increased_reward = increased_reward.saturating_add(move_balance);
							})
							.or_insert(move_balance);
					}
```

Here you can see that in the calculation of move_balance, zero can also result due to rounding. This leads to shares being transferred to the new owner without considering whether rewards have already been claimed for them. Consequently, the new owner of the shares can simply claim rewards again, resulting in another user receiving fewer rewards than they should. This can also be exploited by an attacker by transferring small amounts of shares so that move_balance is zero. The attacker could then easily transfer these shares to a second address also owned by him and claim rewards again. This process can be repeated arbitrarily, allowing the attacker to steal rewards from other users.

## POC

The POC can be inserted into the `orml/rewards/src/tests.rs`file and started with the following command: `cargo test poc -- --nocapture`

```rust
#[test]
fn poc() {
	ExtBuilder::default().build().execute_with(|| {
		RewardsModule::add_share(&ALICE, &DOT_POOL, 1500 * 1000); //Alice adds shares to the pool
		RewardsModule::add_share(&CAROL, &DOT_POOL, 100 * 1000); //Carol adds shares to the pool
		assert_ok!(RewardsModule::accumulate_reward(&DOT_POOL, NATIVE_COIN, 10 * 1000)); //Rewards are accumulated
		RewardsModule::claim_rewards(&ALICE, &DOT_POOL); //Alice claims her rewards

		assert_eq!(RewardsModule::shares_and_withdrawn_rewards(DOT_POOL, ALICE)
Unvalidated Reward Increment in accumulate_reward
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#L131-L138


# Vulnerability details

## Description

in the accumulate_reward function, this line that updates the reward pool without validating the input:

```solidity
pool_info.rewards.entry(reward_currency).and_modify(|(total_reward, _)| {
    *total_reward = total_reward.saturating_add(reward_increment);
}).or_insert((reward_increment, Zero::zero()));
```
The  function is interacts with the contract's state by modifying the PoolInfos storage, specifically updating the rewards mapping within a given pool's PoolInfo structure.
The vulnerability is from the lack of validation for the reward_increment parameter passed to the accumulate_reward function. This absence allows for arbitrary manipulation of the rewards total for any currency within a pool, with no checks against excessively large, small, or inappropriate values and it's allowing attackers to arbitrarily inflate the reward pool.

## Impact

the lack of validation for the reward_increment parameter in the accumulate_reward function, allowing attackers to manipulate the reward pool size arbitrarily.

## Proof of Concept
 i fuzz with first scenario :
```python 
class ContractSimulation:
    def __init__(self):
        self.pools = defaultdict(PoolInfo)
    
    def accumulate_reward(self, pool_id, reward_currency, reward_increment):
        pool = self.pools[pool_id]
        pool.total_rewards[reward_currency] += reward_increment
        
    def add_share(self, pool_id, add_amount, reward_currency='ACALA'):
        pool = self.pools[pool_id]
        initial_total_shares = pool.total_shares
        pool.total_shares += add_amount
        if initial_total_shares != 0:  # Prevent division by zero
            reward_inflation = pool.total_rewards[reward_currency] * add_amount / initial_total_shares
            pool.total_rewards[reward_currency] += reward_inflation

# Initialize the contract simulation and create a pool with initial values
contract = ContractSimulation()
pool_id = 'default_pool'
reward_currency = 'ACALA'
initial_reward = 10000  # Initial total reward
initial_shares = 100  # Initial total shares

# Setup initial pool state
contract.pools[pool_id].total_rewards[reward_currency] = initial_reward
contract.pools[pool_id].total_shares = initial_shares

# Simulate legitimate user adding shares
contract.add_share(pool_id, 50)  # Add 50 shares

# Simulate attack: Inject large reward increment
contract.accumulate_reward(pool_id, reward_currency, 1000000)  # Unrealistic large increment

# Post-Attack Analysis
total_rewards_post_attack = contract.pools[pool_id].total_rewards[reward_currency]
total_shares_post_attack = contract.pools[pool_id].total_shares

# Calculating the impact
legitimate_share_value_pre_attack = initial_reward / initial_shares
legitimate_share_value_post_attack = total_rewards_post_attack / total_shares_post_attack

total_rewards_post
Users can sandwich reward updates to game 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#L401-L414
https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/incentives/src/lib.rs#L495-L508


# Vulnerability details

## Impact

The rewards for the incentives module are updated atomically, i.e. rewards are incremented in a single transaction. This is primarily done by calling the `accumulate_reward` in the `rewards` module. The issue with atomic reward updates is that it allows users to frontrun the updates and sandwich them to extract large amounts of value from them at little to no risk.

The main tradeoff to rewards is that users take on risk. In this case, it is by holding and depositing LP tokens, and thus taking on the risk of impermanent loss. This risk is balanced by the rewards, as is done in all fair systems. However, if a user is able to access these rewards without taking on any risk, it is considered as exploiting the system.

In this contract, the `accumulate_reward` function is called in 2 instances: a regular time based update, and a redistribution of rewards when claiming them via deductions. Both of these updates lead to the amount of rewards being tracked in the `rewards` module increasing, and any stakeholder holding positions in the contract at the time of the update stand to directly benefit from this update.

The contract updates rewards in regular block intervals via the hook.

```rust
if now % T::AccumulatePeriod::get() == Zero::zero() {
    // other code...
                Self::accumulate_incentives(pool_id);
```

So, a user can time their transaction so that they deposit a large amount of LP tokens just before this block number is reached, stake those LP tokens in the incentives module, and then withdraw them immediately after the reward update. This way, they avoid any of the risk associated with the LP tokens and dont contribute anything to the system, but still receive the rewards. They can even use large amounts of funds since the risk is near zero, and thus out earn every other stakeholder in the system.

Similarly, whenever a user claims their rewards, a part of their reward is deducted and redistributed.

```rust
let should_deduction_amount = deduction_rate.saturating_mul_int(*pending_reward);
match Self::payout_reward_and_reaccumulate_reward(
    pool_id,
    &who,
    *currency_id,
    payout_amount,
    deduction_amount,
)
// ...

fn payout_reward_and_reaccumulate_reward(
    //...
) -> DispatchResult {
    <orml_rewards::Pallet<T>>::accumulate_reward(&pool_id, reward_currency_id, reaccumulate_amount)?; //@audit redistribute rewards
    T::Currency::transfer(reward_currency_id, &Self::account_id(), who, payout_amount)?; // Claim rewards
}
```

This means that other users stand to gain if any user decides to withdraw their rewards. So users can frontrun and sandwich attack large users claiming rewards, and claim most
Multiple storage bloat vectors can be used to make the parachain expensive
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#L511-L523
https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/earning/src/lib.rs#L180-L205


# Vulnerability details

## Impact

Parachains on polkadot are required to pay for the amount of storage used by the parachain. When using the substrate framework, it is one of the main goals of developers to prevent storage bloat. If storage can easily be bloated by users, this can lead to high costs for the maintainers of the chain and a potential DOS. A more detailed explanation can be found [here](https://docs.substrate.io/build/troubleshoot-your-code/#storage).

In the current codebase, multiple methods can be used to bloat the storage at little to no cost to the user.

In the earnings module, users can call `unbond_instant` to instantly remove their position from the contract by paying a fee. Users can also use the normal `unbond` function and then wait for a period and then call `withdraw_unbonded` to get back their position without a fee. The `withdraw_unbonded` function is also used to empty out storage when a user has no more funds in it.

```rust
if new_total == 0 {
    Self::Ledger::remove(who);
} else {
    Self::Ledger::insert(who, &ledger);
}
```

This is actually important in the polkadot ecosystem as this reduces the amount of storage used by the parachain. The contracts try to ensure only meaningful positions remain by using a minimum bond amount.

However, when a user calls `unbond`, they push an extra element in the unlocking array.

```rust
try_mutate(|unlocking| {
    // try merge if the last chunk unlock time is same
    if let Some(last) = unlocking.last_mut() {
        if last.unlock_at == unlock_at {
            last.value = last.value.saturating_add(amount);
            return;
        }
    }
    // or make a new one
    unlocking.push(UnlockChunk {
        value: amount,
        unlock_at,
    });
```

This push takes up more storage, until the user eventually calls `withdraw_unbonded` to free up this space. The `unbond` funciton has no minimum value checks, so users can order unbonds of as little as 1 wei.

So a user can deposit 1e18 tokens, and then order 1000s of unbonds of 1 wei each. This will take up an enormous amount of space, and will lead to high costs for maintaining the parachain. This is actually at no cost to the user since they can always claim back their funds in the end.

Similar things can be done via the incentives module. The incentive contract has no minimum deposit limit, so users can create multiple accounts and deposit 1 wei each with them to bloat the storage of the parachain.

## Proof of Concept

The first method can be used to bloat the storage of the parachain via the earnings modeule in the following steps.

1. Bond a significant amount of tokens (1e12).
2. Unbond 1 wei each block for a long time (10000 unbond
Incorrect reward distribution due to lack of reward availability check in claim_rewards and claim_reward functions
Audit: 2024-03-acala-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-acala/blob/main/src/orml/rewards/src/lib.rs#L263-L318


# Vulnerability details

## Description
The claim_rewards and claim_reward functions do not check if the requested reward amount exceeds the amount of rewards that is still available to be claimed. This can lead to incorrect reward distribution.

## Impact
Can cause users to receive more rewards than they should, which can lead to an unfair distribution of rewards among users. Moreover, if multiple users repeatedly call the claim_rewards or claim_reward functions for the same reward, they could potentially claim all the available rewards, leaving none for other users.

## Proof of concept 
This issue can be replicated by repeatedly calling the claim_rewards or claim_reward functions for the same reward. Here is an example scenario:

- There are 100 units of reward available in the reward pool.
- User A calls the claim_rewards or claim_reward function and receives 50 units of reward.
- User B calls the claim_rewards or claim_reward function for the same reward. Since the claim_rewards and claim_reward functions do not check for reward availability, User B will also receive 50 units of reward, even though only 50 units of reward are still available in the reward pool.
- Now, there are no more rewards available in the reward pool, but other users can still call the claim_rewards or claim_reward functions and receive 0 units of reward, even though there are no more rewards available.




## Assessed type

Other
No slippage protection in `withdraw_dex_share()` function
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#L240-L256
https://github.com/code-423n4/2024-03-acala/blob/main/src/modules/incentives/src/lib.rs#L525-L542


# Vulnerability details

## Impact

Without slippage check, If the price of the tokens changes significantly during the withdraw, it could result in a large slippage, causing users to lose a significant amount of funds.
## Proof of Concept

The function `withdraw_dex_share()` allows users to withdraw their shares. 
```solidity
        #[pallet::call_index(1)]
        #[pallet::weight(<T as Config>::WeightInfo::withdraw_dex_share())]
        pub fn withdraw_dex_share( 
            origin: OriginFor<T>,
            lp_currency_id: CurrencyId,
            #[pallet::compact] amount: Balance,
        ) -> DispatchResult {
            let who = ensure_signed(origin)?;
            Self::do_withdraw_dex_share(&who, lp_currency_id, amount)?;
            Ok(())
        }
```
`withdraw_dex_share()` -> `do_withdraw_dex_share()`:
```solidity
    fn do_withdraw_dex_share(who: &T::AccountId, lp_currency_id: CurrencyId, amount: Balance) -> DispatchResult {
        ensure!(lp_currency_id.is_dex_share_currency_id(), Error::<T>::InvalidCurrencyId);
        ensure!(
       <orml_rewards::Pallet<T>>::shares_and_withdrawn_rewards(&PoolId::Dex(lp_currency_id), &who).0 >= amount,
            Error::<T>::NotEnough,
        ); 

        T::Currency::transfer(lp_currency_id, &Self::account_id(), who, amount)?;
        <orml_rewards::Pallet<T>>::remove_share(who, &PoolId::Dex(lp_currency_id), amount.unique_saturated_into());

        Self::deposit_event(Event::WithdrawDexShare {
            who: who.clone(),
            dex_share_type: lp_currency_id,
            withdraw: amount,
        });
        Ok(())
    }
```

But this function lacks a mechanism for users to specify acceptable slippage levels, potentially resulting in unfavorable token withdrawal amounts due to market volatility.
Users may receive significantly less value for their withdrawn shares than expected, especially in volatile market conditions.
## Tools Used

Visual Studio Code

## Recommended Mitigation Steps

Introduce a `slippage` parameter in `withdraw_dex_share()`.


## Assessed type

MEV
Missing deadline when remove shares
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#L240-L256


# Vulnerability details

## Impact

Missing deadline checks allow pending transactions to be maliciously executed in the future. 
## Proof of Concept

The function `withdraw_dex_share()` has no deadline check:
```solidity
        #[pallet::call_index(1)]
        #[pallet::weight(<T as Config>::WeightInfo::withdraw_dex_share())]
        pub fn withdraw_dex_share( 
            origin: OriginFor<T>,
            lp_currency_id: CurrencyId,
            #[pallet::compact] amount: Balance,
        ) -> DispatchResult {
            let who = ensure_signed(origin)?;
            Self::do_withdraw_dex_share(&who, lp_currency_id, amount)?;
            Ok(())
        }
```

This parameter can provide the user an option to limit the execution of their pending transaction. 

Without deadline parameters, as a consequence, users can have their operations executed at unexpected times, when the market conditions are unfavorable, and they can lose funds from this.
## Tools Used

Visual Studio Code

## Recommended Mitigation Steps

Introduce a `deadline` parameter in `withdraw_dex_share()`.


## Assessed type

MEV