xanoutas/solidity-security-findings huggingface dataset:
Precision loss may occur in `add_share` function
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#L143


# Vulnerability details

# Precision loss may occur in `add_share` function

## Impact
The conversion process could result in precision loss, which would lead to:
* Inaccurate Rewards
* Financial Loss
* Trust and Confidence
* Protocol Stability

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

## Proof of Concept
The function converts values to U256 or u128 before performing arithmetic operations. During this conversion, truncation may occur if the original value exceeds the range of the target type. For example, if the `add_amount`, `total_reward`, or `initial_total_shares` values are too large to be represented accurately as u128, the conversion process could result in precision loss.

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

## Tools Used
Manual review.

## Recommended Mitigation Steps
It's essential to carefully handle data conversions and perform arithmetic operations with appropriate data types and precision. Additionally, using libraries or functions that support arbitrary precision arithmetic can help ensure accurate calculations, especially when dealing with large or potentially imprecise values.
withdrawn
Audit: 2024-03-acala-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/README.md?plain=1#L16


# Vulnerability details

> withdrawn


## Assessed type

Error
Users can unbond transactions for amounts exceeding their bonded token balance
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

## Impact
The impact of this vulnerability is that it may allow users to initiate unbonding transactions for amounts exceeding their bonded token balance. Consequently, this could lead to financial discrepancies, negative balances, or disruption of the contract's intended functionality.


## Proof of Concept
The `unbond` function in the provided contract lacks proper input validation, specifically concerning whether the user has sufficient bonded tokens to unbond. Below is the relevant code snippet:
```rs
#[pallet::call_index(1)]
#[pallet::weight(T::WeightInfo::unbond())]
pub fn unbond(origin: OriginFor<T>, #[pallet::compact] amount: Balance) -> DispatchResult {
    let who = ensure_signed(origin)?;

    // Missing input validation for sufficient bonded tokens

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

```
Currently, the function allows users to initiate the unbonding process without verifying whether they possess enough bonded tokens. This oversight could lead to erroneous unbonding attempts, potentially resulting in negative balances or unexpected behavior within the contract.


## Tools Used
Manual
## Recommended Mitigation Steps
Implement input validation within the `unbond` function. Specifically, before proceeding with the unbonding process, the function should verify that the user possesses a sufficient amount of bonded tokens to fulfill the requested unbonding amount. 


## Assessed type

Invalid Validation
Lack of Proper State Update Before External Calls
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#L495-L508


# Vulnerability details

## Impact
The lack of proper state update before external calls can lead to reentrancy attacks. In this scenario, a malicious user could exploit the vulnerability to repeatedly call back into the `payout_reward_and_reaccumulate_reward` function before it completes, resulting in the unintended issuance of multiple reward payouts to the same user.


## Proof of Concept
The vulnerability arises due to the lack of proper state update before making external calls in the `payout_reward_and_reaccumulate_reward` function. This function is responsible for transferring rewards to users and re-accumulating rewards for the pool. However, it does not update the state variables before interacting with external contracts or functions, potentially exposing it to reentrancy attacks.

```rs
#[transactional]
fn payout_reward_and_reaccumulate_reward(
    pool_id: PoolId,
    who: &T::AccountId,
    reward_currency_id: CurrencyId,
    payout_amount: Balance,
    reaccumulate_amount: Balance,
) -> DispatchResult {
    // Vulnerable code: External call before updating state
    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(())
}
```
## Tools Used
Manul
## Recommended Mitigation Steps
Follow the checks-effects-interactions pattern. 


## Assessed type

Context
Multiple Currency Claims in `claim_rewards` Function Can Lead to Inflated Reward Payouts
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#L265


# Vulnerability details

## Impact
The impact of this vulnerability is that it enables a malicious actor to claim more rewards than they are entitled to receive based on their actual contributions. By exploiting the lack of checks in the `claim_rewards` function, the actor can artificially inflate their reward payout, potentially disrupting the fairness and integrity of the rewards system.

## Proof of Concept
In the `claim_rewards` function, rewards are claimed for all reward currencies associated with a specific pool for a given user. However, there is a possibility for a malicious actor to manipulate the system by repeatedly calling the `claim_rewards` function with different reward currencies. By doing so, the actor could potentially claim rewards multiple times for different currencies, leading to an inflated reward payout.
- The `claim_rewards` function iterates over all reward currencies associated with a specific pool for a given user and claims rewards for each currency. However, there are no checks to ensure that the same currency is not claimed multiple times within a single call.
- A malicious actor could exploit this by repeatedly calling the `claim_rewards` function with different reward currencies. For example, if the actor has pending rewards in two different currencies, they could call the function twice, claiming rewards for each currency separately.
- By repeatedly claiming rewards for different currencies, the malicious actor could inflate their total reward payout beyond what they are entitled to receive based on their actual contributions.
```rs
fn claim_rewards(origin: OriginFor<T>, pool_id: PoolId) -> DispatchResult {
    let who = ensure_signed(origin)?;

    // Claim rewards for all reward currencies associated with the pool
    <orml_rewards::Pallet<T>>::claim_rewards(&who, &pool_id);

    // Malicious actor could repeatedly call this function with different currencies
    // to inflate their total reward payout
    // This vulnerability allows the actor to claim more rewards than they should.

    Ok(())
}
```
## Tools Used
Manual
## Recommended Mitigation Steps
Enforce checks to ensure that rewards can only be claimed once per currency within a single call to the `claim_rewards` function. This could involve maintaining a flag or record for each user indicating whether rewards have already been claimed for a specific currency in the current call.


## Assessed type

Context
Lack of Validation in Deposit Function Allows Manipulation of Share Balances
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-L238
https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/incentives/src/lib.rs#L511-L523


# Vulnerability details

## Impact
This vulnerability could allow malicious actors to exploit the lack of validation and deposit an arbitrary amount of DEX shares into their account. By artificially inflating their share balance, they could gain an unfair advantage in the distribution of rewards within the incentives module.

## Proof of Concept
Let's examine the relevant part of the code:
```rs
fn deposit_dex_share(
    origin: OriginFor<T>,
    lp_currency_id: CurrencyId,
    #[pallet::compact] amount: Balance,
) -> DispatchResult {
    let who = ensure_signed(origin)?;
    Self::do_deposit_dex_share(&who, lp_currency_id, amount)?;
    Ok(())
}
```
In the `do_deposit_dex_share` function, the amount of DEX shares is added to the user's share balance without sufficient validation of the amount being deposited. Here's the relevant part of the code:
```rs
fn do_deposit_dex_share(who: &T::AccountId, lp_currency_id: CurrencyId, amount: Balance) -> DispatchResult {
    ensure!(lp_currency_id.is_dex_share_currency_id(), Error::<T>::InvalidCurrencyId);

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

    Self::deposit_event(Event::DepositDexShare {
        who: who.clone(),
        dex_share_type: lp_currency_id,
        deposit: amount,
    });
    Ok(())
}
```
The issue arises because the code does not verify whether the transferred amount of DEX shares corresponds to the actual balance of DEX shares held by the user. This could potentially allow a malicious actor to manipulate the transfer function to deposit an arbitrary amount of DEX shares, thereby gaining more shares than they should rightfully possess.

To exploit this vulnerability, a malicious actor could create a custom DEX token contract that allows them to mint arbitrary amounts of DEX shares. They could then use this contract to deposit a large amount of DEX shares into their account within the incentives module, artificially inflating their share balance and gaining an unfair advantage in the distribution of rewards.
## Tools Used
Manual
## Recommended Mitigation Steps
Include additional checks to ensure that the amount being deposited is valid and corresponds to the actual balance of DEX shares held by the user.


## Assessed type

Invalid Validation
A user would not be able to transfer his whole balance of share
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#L336


# Vulnerability details

## Impact
The `transfer_share_and_rewards` in orml-rewards module, is meant to transfer share balance from one user to another. But due to a vulnerability present in the function, it doesn't allow users to transfer all his shares

If user A had 5 shares as balance , and he want to transfer those 5 shares to user B, then he wouldn't be able to transfer all 5 shares. This happens because of a check implemented in the function : 
`ensure!(move_share < *share, Error::<T>::CanSplitOnlyLessThanShare);` 

Here move_share is the amount the user wants to transfer, and share is the balance of the user. So according to this, the user can at most transfer only 4 share, as move_share had to be less than user's share balance.

Thus due to this bug, there will always be 1 user share that he will not be able to transfer.

## Proof of Concept
https://github.com/code-423n4/2024-03-acala/blob/main/src/orml/rewards/src/lib.rs#L326C9-L326C35

```
SharesAndWithdrawnRewards::<T>::mutate_exists(pool, who, |share| {
	let (share, rewards) = share.as_mut().ok_or(Error::<T>::ShareDoesNotExist)?;
	ensure!(move_share < *share, Error::<T>::CanSplitOnlyLessThanShare);
	for (reward_currency, balance) in rewards {
```


## Tools Used
Manual review

## Recommended Mitigation Steps
It is recommended to change the check to use <= instead, as shown below
`ensure!(move_share <= *share, Error::<T>::CanSplitOnlyLessThanShare)`


## Assessed type

Other
Storage can be bloated by submitting dust amounts of tokens
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#L138
https://github.com/code-423n4/2024-03-acala/blob/main/src/primitives/src/bonding/controller.rs#L72


# Vulnerability details

## Impact
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 in detail explanation can be found in https://docs.substrate.io/build/troubleshoot-your-code/#storage

When a user locks token by calling `bond` function of earning module, the information of the user and the amount of tokens submitted is stored in the `Ledger` storage struct, as can be seen through the statement: 

  `Self::Ledger::insert(who, &ledger);`

So a malicious user can bloat the storage by submitting dust amounts of tokens. He only incurs the gas fee, when submitting tokens of dust amount (i.e very small amount). And if done enough, this can significantly bloat the storage leading to high cost for maintainers.

## Proof of Concept
https://github.com/code-423n4/2024-03-acala/blob/main/src/modules/earning/src/lib.rs#L138
https://github.com/code-423n4/2024-03-acala/blob/main/src/primitives/src/bonding/controller.rs#L72

## Tools Used
Manual review

## Recommended Mitigation Steps
Implement a minimum limit of tokens that can be submitted to the `bond` function. 


## Assessed type

Other
Unbond_instant removes incorrect amount of shares
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#L188-L196


# Vulnerability details

## Description

With `unbond_instant`, a user can unbond a bonded amount directly without having to wait. However, they must pay a fee for this:

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

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:

```diff
+ println!("change.change: {:?}", change.change);
144: T::OnBonded::happened(&(who.clone(), change.change));
145: Self::deposit_event(Event::Bonded {
146: 	who,
147: 	amount: change.change,
148: });
```

For unbond_instant:

```diff
+ println!("final_amount: {:?}", final_amount);
196: T::OnUnbonded::happened(&(who.clone(), final_amount));
197: Self::deposit_event(Event::InstantUnbonded {
198: 	who,
199: 	amount: final_amount,
200: 	fee,
201: });

```

Code that can be inserted into the file modules/earning/src/tests.rs to test both functions:

```rust
#[test]
fn poc() {
	ExtBuilder::default().build().execute_with(|| {
		assert_ok!(Earning::bond(RuntimeOrigin::signed(ALICE), 100)); //100 tokens are bonded. change.change is 100

		assert_ok!(Earning::unbond_instant(RuntimeOrigin::signed(ALICE), 100)); //All bonded tokens will be unbonded immediately but only 90 shares will be removed because of the fee amount.
	});
}
```

The code can be started with the command `cargo test poc -- --nocapture`

So 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 then no longer be removed and are stuck. With a maximum fee of 20%, some shares could accumulate over a period of time and these shares would accumulate rewards. The owners of the shares can still claim these rewards. In addition, one share is no longer the same as 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.


## Assessed type

Other
Anyone can sidestep waiting for a duration before unbonding
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


## Proof of Concept

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

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

Evidently, this function is used for unbonding tokens, in real sense the function "starts the unbonding process" and then the token will finish unbonding after `UnbondingPeriod` blocks, which is the contract's intended logic, i.e users having to wait for a duration before unbonding.

Keep in mind that from the attack ideas suggested by protocol during the contest seen here: https://github.com/code-423n4/2024-03-acala/tree/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65?tab=readme-ov-file#attack-ideas-where-to-look-for-bugs we can see that the first hinted place to look at is how one can sidestep waiting the bonding duration, i.e
> Find a way to unstake bypass the bonding duration

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

```rust
		#[pallet::call_index(2)]
		#[pallet::weight(T::WeightInfo::unbond_instant())]
		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(())
		}

```

This function however is used to unbound instantly, i.e there is no need to wait out the duration for the unbound to kick in, and this is allowed if a user is to pay the `InstantUnstakeFee` fee, case however is that from this snippet https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/earning/src/lib.rs#L44-L48

```rust
define_p