xanoutas/solidity-security-findings huggingface dataset:
Malicious User Can `transfer_share_and_rewards` Allows Self-Transfer of Shares and Rewards
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#L326


# Vulnerability details

## Vulnerability Description
`transfer_share_and_rewards` function facilitates the transfer of shares and associated rewards between two accounts within the specified pool.

it iterates through the rewards associated with the shares being transferred and distributes them accordingly to the receiving account (other). 

It then updates the share balances for both the initiating account (who) and the receiving account (other) within the specified pool.

Th problem arises because `transfer_share_and_rewards` function lacks a crucial check to prevent self-transfers, enabling an account to transfer shares and associated rewards to itself without restriction. 

It allows malicious actors to illegitimately inflate their holdings by self share. 

## Vulnerability Impact

- Malicious actors can provide his own account as other account to increase their shares and rewards within the system by self share.

## Vulnerability Proof Of Concept
Here's a step by step detailed how this attack could be executed :


   - Alice has a certain amount of shares and associated rewards in the system.


   - Alice identifies the missing check in the `transfer_share_and_rewards` function.


   - Alice calls the `transfer_share_and_rewards` function with her own account ID (`Alice`) as both `who` and `other`.
   - She specifies an arbitrary amount of shares (`move_share`) to transfer, along with associated rewards.


   - The function does not detect that `who` and `other` are the same account, thus proceeding with the transfer as instructed by Alice.


   - The function calculates the proportion of rewards corresponding to the transferred shares and updates both the transferring and receiving balances.
   - Since `who` and `other` are the same, the transferred shares and rewards are effectively added to Alice's holdings.


   - Alice successfully transfers shares and rewards to herself without any checks or restrictions.
   - This action increases Alice's share and reward holdings in the system without legitimate activity.

   - Alice can exploit this vulnerability repeatedly, further inflating her holdings within the system.

## Recommended Mitigation
To mitigate this vulnerability, the following steps are recommended:
- Implement a check within the `transfer_share_and_rewards` function to ensure that `who` and `other` are not the same account. This prevents self-transfers and mitigates the risk of abuse.

```diff
/// Splits share into two parts.
	///
	/// `move_share` - amount of share to remove and put into `other` share
	/// `other` - new account who will own new share
	///
	/// Similar too claim and add 2 shares later, but does not requires pool
	/// inflation and is more efficient.
	pub fn transfer_share_and_rewards(
		who: &T::AccountId,
		pool: &T::PoolId,
		move_share: T::Share,
		other: &T::AccountId,
An attacker can steal other users rewards
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#L191-L252


# 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#L191-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 currency
						withdrawn_rewards
							.iter_mut()
							.for_each(|(reward_currency, withdrawn_reward)| {
								let withdrawn_reward_to_remove: T::Balance = removing_share
									.saturating_mul(withdrawn_reward.to_owned().saturated_into::<u128>().into())
									.checked_div(share.saturated_into::<u128>().into())
									.unwrap_or_default()
									.as_u128()
									.saturated_into();

								if let Some((total_reward, total_withdrawn_reward)) =
									pool_info.rewards.get_mut(reward_currency)
								{
									*total_reward = total_reward.saturating_sub(withdrawn_reward_to_remove);
									*total_withdrawn_reward =
										total_withdrawn_reward.saturating_sub(withdrawn_reward_to_remove);

									// remove if all reward is withdrawn
									if total_reward.is_zero() {
										pool_info.rewards.remove(reward_currency);
									}
								}
								*withdrawn_reward = withdrawn_reward.saturating_sub(withdrawn_reward_to_remove);
							});

						if !pool_info.total_shares.is_zero() {
							*maybe_pool_info = Some(pool_info);
						}
					}
				});

				share = share.saturating_sub(remove_amount);
				if !share.is_zero() {
					*share_info = Some((share, withdrawn_rewards));
				}
			}
		});
	}

```

This function is used to remove shares, would be key to note that, this function could be passed any amount of shares asides `0`, this is enforced [here](https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/orml/rewards/src/lib.rs#L192-L195).

But before removing the shares there is always an attempt to claim the rewards for users, which is where the problem is, see https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/orml/rewards/src/lib.rs#L263-L292

```rust
	pub fn claim_rewards(who: &T::AccountId, pool: &T::PoolId) {
		SharesAndWithdra
The first user of pool gets all of the pool rewards
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#L159


# Vulnerability details

## Impact
The first user of pool gets all of the pool rewards

## Proof of Concept
In the reward module, when a user adds add share, the current `reward_inflation` of the user will be recorded according to the previous `total_reward` and `total_share` in the pool.

```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 {
						//@audit reward_inflation = add_amount * total_reward / initial_total_shares(total_shares)
						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| {
                                //@audit Record the user's withdrawn_reward
								*withdrawn_reward = withdrawn_reward.saturating_add(reward_inflation);
							})
							.or_insert(reward_inflation);
					});
			});
		});
```

If total_reward or total_share is 0, the user's reward_inflation is 0:

```rust
	pub fn add_share(who: &T::AccountId, pool: &T::PoolId, add_amount: T::Share) {
        ......
        let reward_inflation = if initial_total_shares.is_zero() {
            Zero::zero()
        }
        ......
    }
```

When calculating the user's rewards, the share is calculated based on the user's share amount and then subtracted from reward_inflation:

```rust
	fn reward_to_withdraw(
		share: T::Share,
		total_reward: T::Balance,
		total_shares: U256,
		withdrawn_reward: T::Balance,
		total_withdrawn_reward: T::Balance,
	) -> T::Balance {
		// (share * total_reward / total_shares) -  withdrawn_reward
		let total_reward
There is no check in the `payout_reward_and_reaccumulate_reward` function to ensure that the `who` account is the same as the account that owns the shares for the pool
Audit: 2024-03-acala-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The `payout_reward_and_reaccumulate_reward` function takes in a `who` parameter which is the `AccountId` of the user who is claiming the rewards. The function then transfers the reward amount from the `Self::account_id()` account to the `who` account and updates the rewards records in the `orml_rewards` pallet.

However, there is no check in the function to ensure that the `who` account is the same as the account that owns the shares for the pool. This means that any user can potentially claim rewards for a pool that they do not own shares in, causing a loss of funds.



## Proof of Concept

The `payout_reward_and_reaccumulate_reward` function transfers rewards from the `Self::account_id()` account to the `who` account, but there is no check to ensure that the `who` account is the same as the account that owns the shares for the pool. This could allow an attacker to claim rewards for a pool that they do not own shares in, causing a loss of funds or incorrect reward calculations.

```rs
	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(())
	}
}

```
We can create a pool and add some shares to it using a legitimate account. Then, we can attempt to claim rewards using a different account that does not own any shares in the pool. 
The `payout_reward_and_reaccumulate_reward` function does not perform any checks to verify the ownership of the shares, the attacker will be able to successfully claim rewards that they are not entitled to.

## Tools Used
Manual Review

## Recommended Mitigation Steps

It is recommended to add checks to the `payout_reward_and_reaccumulate_reward` function to ensure that the who account is the same as the account that owns the shares for the pool. This can be done by adding a check to the function that retrieves the share balance for the who account and compares it to the total share balance for the pool. If the share balance for the who account is less than the minimum share balance required to claim rewards, the function should return an error and not proceed with the reward payout.



## Assessed type

Access Control
Unbond_instant function reverts if amount to be instantly unbonded is close to  the total bonded amount
Audit: 2024-03-acala-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The `unbond_instant` function in the earning module is responsible for allowing users to instantly unbond a specified amount by paying a fee.

Here is a simple  break down of its functionality

1. Any amount can be unbonded as long as it is not below the MinBond Threshold.

2. If the amount specified to be instantly unbonded is greater than the amount bonded, the fee is subtracted from the bonded amount, and then the entire bonded amount is unbonded

3. If the amount specified to be instantly unbonded is less than the amount bonded and does not fall below the MinBond Threshold, the fee is subtracted from the amount to be instantly unbonded, and then the specified amount is unbonded.

The vulnerability lies in the case where the amount to be instantly unbonded is close to, but not  below the MinBond Threshold of the total bonded amount




## Proof of Concept
 ```
fn poc() {
   ExtBuilder::default().build().execute_with(|| {

        // Bond 500 tokens for ALICE
assert_ok!(Earning::bond(RuntimeOrigin::signed(ALICE), 500));
       
 // Assert that the Bonded event was emitted correctly
        System::assert_last_event(
            Event::Bonded {
                who: ALICE,
                amount: 500,
            }
            .into(),
        );
       
 // Assert that the OnBonded hook was called with the correct parameters
        OnBonded::assert_eq_and_clear(vec![(ALICE, 500)]);
     
  System::set_block_number(5); // Set the block number to 5
        
// Attempt to instantly unbond 450 tokens for ALICE
        assert_ok!(Earning::unbond_instant(RuntimeOrigin::signed(ALICE), 450));
        
// Assert that the InstantUnbonded event was emitted correctly
        System::assert_last_event(
            Event::InstantUnbonded {
                who: ALICE,
                amount: 405, //Expected final unbonded amount after deducting the fee
                fee: 45,     // Expected  fee paid for instant unbonding
            }
            .into(),
        );
       
 // Assert that the OnUnbonded hook was called with the correct parameters
        OnUnbonded::assert_eq_and_clear(vec![(ALICE, 405)]);
       

 // Assert that the OnUnstakeFee hook was called with the correct fee amount
      OnUnstakeFee::assert_eq_and_clear(vec![45]); 
});
}
```

Test
To run the exploit test:
cargo test --package module-earning --lib -- tests::poc 

```
---- tests::poc stdout ----
thread 'tests::poc' panicked at modules\earning\src\tests.rs:103:1:
Expected Ok(_). Got Err(
    Module(
        ModuleError {
            index: 2,
            error: [
                0,
                0,
                0,
                0,
            ],
            message: Some(
                "BelowMinBondThreshold",
            ),
        },
    ),
)
note: run with `RUST_BACKTRACE=1` environment variab
`transfer_share_and_rewards` allows for self transfer
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#L326


# Vulnerability details

## Impact
The rewards library holds the `transfer_share_and_rewards` allows for self transfer which can be used to double shares and rewards. 
Important to note that the function, for now is not in use by the in-scope contracts. However, I still believe it's worth pointing out.

## Proof of Concept
Copy and paste the below test into [tests.rs](https://github.com/code-423n4/2024-03-acala/blob/main/src/orml/rewards/src/tests.rs)
It shows how users Alice and Bob, by invoking this function, can increase their share/rewards balance.

```rust
fn transfer_to_self() {
	ExtBuilder::default().build().execute_with(|| {
		// Open a pool with bob holding 100 shares in the pool
		RewardsModule::add_share(&BOB, &DOT_POOL, 100);
		// Accumulate rewards
		assert_ok!(RewardsModule::accumulate_reward(&DOT_POOL, NATIVE_COIN, 100));
		// Alice deposits into the pool and gets shares, assert that it succeeds
		RewardsModule::add_share(&ALICE, &DOT_POOL, 100);
		// Assert that rewards still exist
		assert_ok!(RewardsModule::accumulate_reward(&DOT_POOL, NATIVE_COIN, 100));
		// Gets pools info 
		let pool_info = RewardsModule::pool_infos(DOT_POOL);
		// Ensures that reward transfer doesn't affect pool
		let new_pool_info = RewardsModule::pool_infos(DOT_POOL);
		assert_eq!(pool_info, new_pool_info, "reward transfer does not affect the pool");
		// Assert that Alice's share/rewards transfer to self succeeds 
		assert_ok!(RewardsModule::transfer_share_and_rewards(&ALICE, &DOT_POOL, 90, &ALICE));
		// Assert that alice's share/reward balance has now increased
		assert_eq!(
			RewardsModule::shares_and_withdrawn_rewards(DOT_POOL, ALICE),
			(190, vec![(NATIVE_COIN, 190)].into_iter().collect())
		);
		// Alice has discovered infinite money glitch? She tries again to be sure its not a fluke
		// Alice's transfers shares and rewards again to to self, assert that it succeeds 
		assert_ok!(RewardsModule::transfer_share_and_rewards(&ALICE, &DOT_POOL, 180, &ALICE));
		// Assert that her share/reward balance still increased
		assert_eq!(
			RewardsModule::shares_and_withdrawn_rewards(DOT_POOL, ALICE),
			(370, vec![(NATIVE_COIN, 370)].into_iter().collect())
		);
		// She transfers a some of her shares/rewards to Bob, while teaching him the glitch
		assert_ok!(RewardsModule::transfer_share_and_rewards(&ALICE, &DOT_POOL, 70, &BOB));
		assert_eq!(
			RewardsModule::shares_and_withdrawn_rewards(DOT_POOL, ALICE),
			(300, vec![(NATIVE_COIN, 300)].into_iter().collect())
		);
		assert_eq!(
			RewardsModule::shares_and_withdrawn_rewards(DOT_POOL, BOB),
			(170, vec![(NATIVE_COIN, 70)].into_iter().collect())
		);
		// Bob decides to try it out himself, Assert that it works
		assert_ok!(RewardsModule::transfer_share_and_rewards(&BOB, &DOT_POOL, 100, &BOB));
		assert_eq!(
			RewardsModule::shares_and_withdrawn_rewards(DOT_POOL, BO
Inconsistent Application of Reward Deduction Rates and Currencies
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#L421-L424


# Vulnerability details

## description 

the vulnerability is arise from how ClaimRewardDeductionRates and ClaimRewardDeductionCurrency are applied within the do_claim_rewards function, 
the logic does not consistently apply deduction rates based on the updated deduction currency settings. and this inconsistency can lead to scenarios where the deduction rate is either not applied or applied incorrectly,and this depending on the currency of the rewards being claimed versus the intended deduction currency for the pool
The do_claim_rewards function interacts directly with the storage mappings ClaimRewardDeductionRates and ClaimRewardDeductionCurrency, leading to potential misapplication of the deduction rates.
The reward payout calculation within do_claim_rewards might not accurately reflect the intended deduction policy, affecting the fairness and integrity of reward distributions.
```solidity
let deduction_rate = Self::claim_reward_deduction_rates(&pool_id);
let deduction_currency = ClaimRewardDeductionCurrency::<T>::get(pool_id);
```
## Impact

a user can claim rewards without the intended deduction rate being applied, due to an inconsistency in the deduction currency settings. This allows the user to exploit the system by receiving more rewards than they should, depleting the rewards pool at an unintended rate and disadvantaging other users

## Proof of Concept

i fuzz with scenario and as result :
```python
# Initial Setup
pools = {
    'DEX': {'rewards': 1000, 'deduction_rate': 0.1, 'deduction_currency': None},
    'Loans': {'rewards': 2000, 'deduction_rate': 0.05, 'deduction_currency': 'USD'}
}

users = {
    'Alice': {'DEX': 500, 'Loans': 1000},
    'Bob': {'DEX': 500, 'Loans': 1000}
}

# Represents the rewards source account balance
rewards_source = {'USD': 10000, 'ACALA': 10000}

# Function to simulate the claim_rewards process
def claim_rewards(user, pool_id):
    rewards = users[user][pool_id]  # User's share of the pool's rewards
    pool_info = pools[pool_id]
    
    # Inconsistency introduced: Deduction currency not considered properly
    reward_currency = 'ACALA' if pool_id == 'DEX' else 'USD'
    deduction_rate = pool_info['deduction_rate'] if pool_info['deduction_currency'] == reward_currency or pool_info['deduction_currency'] is None else 0
    
    actual_rewards = rewards - (rewards * deduction_rate)
    rewards_source[reward_currency] -= actual_rewards
    users[user][pool_id] = 0  # Reset the user's pool reward to 0 after claiming
    print(f"{user} claimed {actual_rewards} {reward_currency} from {pool_id} pool, deduction_rate applied: {deduction_rate}")

# Claim rewards simulation
print("Before claiming rewards:")
print(f"Rewards Source: {rewards_source}")
print(f"Users' Rewards: {users}\n")

claim_rewards('Alice', 'DEX')
claim_rewards('Bob', 'Loans')

# Adjusting deductio