xanoutas/solidity-security-findings huggingface dataset:
Protocol could lose funds currently in early `unbond_instant()` executions
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#L178-L206


# Vulnerability details


## Proof of Concept

Take a look at 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 is used to instantly make an unbond, a user would have to pay an instant unstake fee and then they wouldn't have to wait out the duration, issue is with how the "fee" being received is managed, there are no checks that the `fee` after being calculated is higher than the Existential Deposit, now protocol protects the `who` account by ensuring that it diesn't go below the `ED` so as not to get removed on-chain, but if the `fee` calculated is less than the `ED` and the recipient of the withdrawal has no value then the `fee` is effectively lost, cause even though the transaction would succeed, it wouldn't reflect on the recipient as it's state would not be on the chain, [source](https://wiki.acala.network/get-started/acala-network/acala-account#existential-deposit) essentially meaning that the fee has been lost, keep in mind that the fee being < ED is highly probable as it's been calculated in percentages _(And this is not the problem)_ but rather the fact that there are no assurances that the recipient has any tokens at all.

## Impact

Leak of value for protocol for `unbond_instant()` executions, since in these instances after the withdrawal it would be as if the recipient never received the tokens cause its state would be removed from the chain.

## Recommended Mitigation Steps

Consider ensuring that the calculated `fee` is higher than the `ED` at least in the first few instances of unbonding.



## Assessed type

Token-Transfer
Sandwich attack
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#L391


# Vulnerability details

## Impact
The attacker obtains pool rewards through the Sandwich attack

## Proof of Concept

Incentives module users can `deposit_dex_share` and `withdraw_dex_share` at any time,

earning module users can `bond` and `unbond` at any time. Although the token is still locked when unbonded, it will immediately `remove_share`.
`claim_rewards` is called when `remove_share`.

The reward module calculates rewards based on `user_share * total_reward / total_shares` :

```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_proportion: T::Balance = U256::from(share.saturated_into::<u128>())
			.saturating_mul(U256::from(total_reward.saturated_into::<u128>()))
			.checked_div(total_shares)
			.unwrap_or_default()
			.as_u128()
			.unique_saturated_into();
		total_reward_proportion
			.saturating_sub(withdrawn_reward)
			.min(total_reward.saturating_sub(total_withdrawn_reward))
	}
```

A malicious user can temporarily increase the share in the pool when `total_reward` increases, so that the user can get rewards, and then `remove_share` after claiming rewards.
An attacker can monitor on-chain transactions and find that when `total_reward` in the pool increases, he can increase his share by `front-running`.
After the target transaction is executed, the attacker `remove_share`, the attacker does not have any loses, and does not contribute anything to the pool, but gets rewards.


## Tools Used
vscode, manual

## Recommended Mitigation Steps
Add a time lock, or add more time to calculate rewards



## Assessed type

MEV
Users account balance can be negative after fee withdrawal
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


# Vulnerability details

## Impact
The impact of this vulnerability is that it could lead to erroneous token withdrawals, causing users' account balances to become negative. This could result in financial losses for users and disrupt the stability and integrity of the system.


## Proof of Concept
In the `unbond_instant` function of the provided contract, there's a potential vulnerability related to fee calculation. This vulnerability arises from the implementation of fee deduction logic, where a fee is subtracted from the amount being unbonded without proper validation checks. Consider the following code snippet:
```rs
let fee = fee_ratio.mul_ceil(amount);
let final_amount = amount.saturating_sub(fee);
```
The code calculates the fee (`fee`) based on a provided ratio (`fee_ratio`) and then subtracts this fee from the amount being unbonded to determine the final unbonded amount (`final_amount`). However, there's a risk that if the fee calculated exceeds the amount being unbonded, the resulting `final_amount` could be negative. This negative value could potentially lead to an incorrect withdrawal operation, causing the user's account balance to become negative, which is not intended behavior.


## Tools Used
Manual
## Recommended Mitigation Steps
Implement proper validation checks to ensure that the fee deduction does not result in a negative final amount.


## Assessed type

Context
Unexpected Final Unbond Amount Due to Direct Fee Deduction in unbond_instant
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#L187-L191


# Vulnerability details

## Description
in the unbond_instant function, an InstantUnstakeFee is calculated and directly deducted from the user's unbond amount, potentially leading to a final amount that could unexpectedly be lower than the user's initial intention. This direct deduction, without prior checks against the minimum bond threshold or explicit user acknowledgment of the final amount post-fee, could result in user dissatisfaction or unintended financial outcomes within the staking 
```rust
let fee = fee_ratio.mul_ceil(amount);
let final_amount = amount.saturating_sub(fee);
```
the bug is arise from  the direct deduction of an InstantUnstakeFee from the unbonding amount specified by the user without any pre-validation checks  

## Impact
Users intending to unbond tokens instantly may receive less than expected due to fee deductions, potentially affecting their staking strategy.
## Proof of Concept
i fuzz with scenario that 
Minimum Bond Threshold (min_bond): 1,000 tokens
Instant Unstake Fee Percentage: 5% of the unbonding amount
User's Total Bonded Amount: 1,050 tokens
in scenario that The user decides to instantly unbond 100 tokens.
- A 5% fee is applied to the unbonding amount Unbonding Amount: 100 tokens , Fee (5%): 5 tokens
- The unbonding amount is reduced by the fee final Unbonding Amount after 
Fee: 100 - 5 = 95 tokens
- The total bonded amount is reduced by the final unbonding amount:New Total Bonded Amount: 1,050 - 95 = 955 tokens
- as result the user's total bonded amount is 955 tokens, which is below the minimum bond threshold of 1,000 tokens

## Tools Used
manual review
## Recommended Mitigation Steps
add a check to the final unbonded amount post-fee deduction aligns with user expectations and system thresholds


## Assessed type

Other
Protocol implements a non-distinctive method of getting account Ids
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#L372-L375


# Vulnerability details


## Proof of Concept

Take a look at https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/incentives/src/lib.rs#L372-L375

```rust
	pub fn account_id() -> T::AccountId {
		T::PalletId::get().into_account_truncating()
	}

```

This function is used to get the account Id and is used in many instances in code, but it uses a not so safe method and would cause the Ids to be returned not distinct (achievable with other bytes), note that the `into_account_truncating` is gotten from the substrate pallet whose implementation is the below:

```rust

	fn into_account_truncating(&self) -> AccountId {
		self.into_sub_account_truncating(&())
	}

```

The docs around this function _`(into_sub_account_truncating)`_, indicates that not only does this function truncate the input bytes to provide a result, it also leads to duplicate accounts gotten from, to quote the comments aroung "into_sub_account_truncating"

```markdown
    /// NOTE: The account IDs from this and from `into_account` are *not* guaranteed to be distinct
    /// for any given value of `self`, nor are different invocations to this with different types `T`
```

Sources to the above statements can be seen from the official pallet docs: https://docs.rs/sp-runtime/34.0.0/sp_runtime/traits/trait.AccountIdConversion.html#tymethod.into_sub_account_truncating

Attached to the same docs we can see that a better account ids provider has been made avaliable in the pallet, i.e the [try_into_sub_account()](https://docs.rs/sp-runtime/34.0.0/sp_runtime/traits/trait.AccountIdConversion.html#tymethod.try_into_sub_account) method, which is the same just as the `into_sub_account_truncating()` but in this case it's ensured to always provide a distinct account, i.e the function uses all the bytes of the account’s seed when generating an account, meaning that there is no chance of clashing of accounts when different bytes are passed to the function unlike the `into_sub_account_truncating()`

Also this function is easily accessible as instead of calling `into_account_truncating()` from the pallet we can call the below instead:

```rust

	fn try_into_account(&self) -> Option<AccountId> {
		self.try_into_sub_account(&())
	}

```

Now, using this search command: [https://github.com/search?q=repo%3Acode-423n4%2F2024-03-acala%20account_id&type=code](https://github.com/search?q=repo%3Acode-423n4%2F2024-03-acala%20account_id&type=code) we can see that this method of getting the amount id is used in 4 different instances across the Incentives module.

## Impact

The way protocol's get an account id is not protected, as the account that it [gotten from account_id()](https://github.com/code-423n4/2024-03-acala/blob/9c71c05cf2d9f0a2603984c50f76fc8a315d4d65/src/modules/incentives/src/lib.rs#L372) can be gotte
Users can claim more than his accrued reward, by calling the functions in a specific order
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#L265
https://github.com/code-423n4/2024-03-acala/blob/main/src/modules/incentives/src/lib.rs#L248


# Vulnerability details

## Impact
A user stakes X amount of LP tokens by calling `deposit_dex_share` function of Incentive module. Rewards gets accumulated in the pool. The user then unstakes the X amount of LP tokens by calling `withdraw_dex_share`. He can then claim the accrued rewards by calling the `claim_rewards` function. 

But by changing the order of the calls a user can claim almost twice the accrued reward amount, **provided the deduction_rate is minimum i.e around 0.1**. So the lesser the deduction_rate, the more reward the user can claim for free.
So now user stakes X LP tokens. He waits untill the rewards gets accumulated in the pool. Now instead of the `withdraw_dex_share` call, he calls `claim_rewards` function. 
The `claim_rewards` function, updates the `pending_reward` with the amount of reward that the user should normally get. But a portion of this `pending_reward` is deducted (depends on the `deduction_rate`), and finally the remaining amount is transferred to the user, and `pending_reward` is marked as 0. On the other hand, the deducted amount is transferred to the pool as reward. Thus this increases the `total_reward` of the pool by an amount equal to the deducted amount.

And due to this change in the `total_reward` of the pool, when we now call the `withdraw_dex_share` function, `reward_to_withdraw` variable becomes non-zero, due to which it updates the `pending_reward` with a value that is approximately equal to the previously accumulated reward value. 

Hence now if we call the `claim_reward` function again, we will be able to claim the extra/free pending_reward again.

## Proof of Concept
Here is a scenerio :
1. User A call `deposit_dex_share` with X amount of LP token
2. He waits untill enough reward is accrued
3. User A now calls `claim_rewards` which will transfer him the accrued rewards
4. He then calls `withdraw_dex_share`, which will return his staked LP tokens, and create new pending_reward for him
5. Finally he calls `claim_rewards` again, to retrieve the remaining extra reward

## Tools Used
Manual review

## Recommended Mitigation Steps
It is recommended to record the deducted_amount sent to the pool, in a different storage and not in `total_reward`. Maybe it can be merged during the AccumulatePeriod


## Assessed type

Other
An attacker user can frontrun and stake a large amount of LP tokens before AccumulatePeriod, to claim the majority share of the reward pool
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#L230
https://github.com/code-423n4/2024-03-acala/blob/main/src/modules/incentives/src/lib.rs#L248


# Vulnerability details

## Impact
A malicious user can claim majority of the reward pool, by front-running and staking a large amount of LP tokens before the reward is accumulated (i.e accumulating period). Afterwards when the rewards gets accumulated and gets deposited to the pool, the malicious user can withdraw it. This guarentees that the malicious user will receive a majority chunk of the reward, than the rest of previously staked users. The user can use this method, to claim majority chunk of the reward pool for every reward accumulate period.

It also impacts the rest of the staked user. As they will lose maximum percent of their share of the reward.

## Proof of Concept
Consider the scenerio below, and for simplicity, we will consider reward accumulated in single reward_currency

1. Attacker saw that AccumulatePeriod will start. So either he stakes before that or he front-runs the `accumulate_incentives` function, and then stake
2. Attacker stakes a very large amount (compared to other staked users) of LP tokens, by calling `deposit_dex_share`, just before the AccumulatePeriod
3. This will increase the `reward_inflation` variable by a significant amount, which in-turn will increase the `total_reward` and `total_withdrawn_reward` variable. Furthermore `withdrawn_reward` is also increased.
4. Now the AccumulatePeriod starts, and rewards gets deposited. This will increase the `total_reward` variable of the pool by the reward amount accumulated.
5. Afterwards, we will call the `withdraw_dex_share` function, with our staked LP token amount. This will return our staked LP token, along with the majority share of the reward, that was accumulated.

Hence a direct POC is :
deposit_dex_share(origin, lp_currency_id, amount) - where amount is a very large amount of LP tokens
AccumulatePeriod starts and rewards get accumulated. total_reward of the pool increased
withdraw_dex_share(origin, lp_currency_id, amount) - where amount is the staked LP token

User will now receive a larger share of the reward. I was able to get a 90% share of the reward using this vulnerability. 

## Tools Used
Manual review

## Recommended Mitigation Steps
It is recommended to implement delays while staking and unstaking 


## Assessed type

Other
Fee Exceeds Available Balance in `unbond_instant` Function
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#L178-L205


# Vulnerability details

The fee calculation and subtraction process in the `unbond_instant` function may lead to negative or insufficient balances if the fee is not properly handled when subtracting it from the user's account balance.

## Impact
 Transaction failure and unexpected behavior due to insufficient balances after fee deduction.

## Proof of Concept
 Analyze the fee calculation and balance subtraction logic in the `unbond_instant` function to ensure that the fee amount is deducted correctly without causing 

## Tools Used

Manual Review 

## Recommended Mitigation Steps
 Implement checks to ensure that the fee deduction process does not result in negative or insufficient balances for users. Consider using safe arithmetic operations and handling edge cases where the fee may exceed the available balance.


## Assessed type

Math
Inconsistent Reward Claim Amount Calculation 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-L292
https://github.com/code-423n4/2024-03-acala/blob/main/src/orml/rewards/src/lib.rs#L292-L317


# Vulnerability details

The issue arises from potential inaccuracies in reward claim amounts due to integer operations and conversions within the `claim_rewards` and `claim_reward` functions. This can lead to minor discrepancies in the withdrawn reward amounts compared to actual expected values.


## Impact
The impact is low as it may result in slightly incorrect reward withdrawals, but it does not pose a significant security threat or financial risk.

## Proof of Concept
N/A

## Tools Used

Manual Review

## Recommended Mitigation Steps



## Assessed type

Under/Overflow
Inconsistent Deduction Currency Handling
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#L350-L367


# Vulnerability details

The `update_claim_reward_deduction_currency` function in the Incentives Module allows updating the deduction currency for claim rewards. However, there is a risk of inconsistent handling when updating the deduction currency for specific pools.

```
impl<T: Config> IncentivesManager<T::AccountId, Balance, CurrencyId, PoolId> for Pallet<T> {
    // Other code...

    fn update_claim_reward_deduction_currency(
        origin: OriginFor<T>,
        pool_id: PoolId,
        currency_id: Option<CurrencyId>,
    ) -> DispatchResult {
        T::UpdateOrigin::ensure_origin(origin)?;
        ClaimRewardDeductionCurrency::<T>::mutate_exists(pool_id, |c| *c = currency_id);
        Self::deposit_event(Event::ClaimRewardDeductionCurrencyUpdated {
            pool: pool_id,
            currency: currency_id,
        });
        Ok(())
    }

    // Other code...
}
```

## Impact
The inconsistent handling of deduction currencies can lead to incorrect deduction amounts during reward claims, affecting user rewards and system stability.

## Proof of Concept

N/A

## Tools Used

Manual Review 

## Recommended Mitigation Steps

Should review more than this Review and refactor the `update_claim_reward_deduction_currency` function to ensure consistent handling of deduction currencies across all pools.


## Assessed type

Access Control
`do_claim_rewards::should_deduction_amount`  inconsistent deduction logic when calculating 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#L442-L448


# Vulnerability details

## Impact
The `do_claim_rewards` function in the Incentives module contains inconsistent deduction logic when calculating rewards.

The `payout_amount` and `deduction_amount` are calculated inconsistently. The `payout_amount` is calculated as the difference between the `pending_reward` and the `should_deduction_amount`, while the `should_deduction_amount` is calculated as the product of the `deduction_rate` and the `pending_reward`.

This means that the `payout_amount` is not necessarily equal to the `pending_reward` minus the actual deduction amount. Instead, it is equal to the `pending_reward` minus the `should_deduction_amount`, which may not be the same as the actual deduction amount.

For example, if the `deduction_rate` is set too high, the `should_deduction_amount` may exceed the `pending_reward`, resulting in a negative `payout_amount`. Alternatively, if the `deduction_rate` is set too low, the `should_deduction_amount` may be lower than the actual deduction amount, resulting in an overpayment of rewards.


## Proof of Concept

In the `do_claim_rewards` function, where the `should_deduction_amount` is calculated as the product of the `deduction_rate` and the `pending_reward`. However, the `payout_amount` is calculated as the difference between the `pending_reward` and the `should_deduction_amount`. This could lead to inconsistent deduction logic and discrepancies in reward calculations.

```rs

					let (payout_amount, deduction_amount) = {
						let should_deduction_amount = deduction_rate.saturating_mul_int(*pending_reward);
						(
							pending_reward.saturating_sub(should_deduction_amount),
							should_deduction_amount,
						)

```

## Tools Used

Manual Review

## Recommended Mitigation Steps

To fix this issue, the `payout_amount` could be calculated as the product of the `deduction_rate` and the `pending_reward`, subtracted from the `pending_reward`. 
Alternatively, the `deduction_amount` could be calculated as the difference between the `pending_reward` and the `payout_amount`, instead of the `should_deduction_amount`. This would ensure that the `payout_amount` and `deduction_amount` are calculated consistently and accurately.







## Assessed type

Other