#46888 [SC-High] account_transfer_partial: lack of input validation when working with signed integers
Submitted on Jun 5th 2025 at 22:12:47 UTC by @gln for IOP | Paradex
Report ID: #46888
Report Type: Smart Contract
Report severity: High
Target: https://github.com/tradeparadex/audit-competition-may-2025/tree/main/paraclear
Impacts:
Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
Description
Brief/Intro
The function account_transfer_partial() fails to validate signed integer parameter which represents an amount to transfer.
As a result, malicious user is able to steal funds from the contract.
Vulnerability Details
Let's look at the code from paraclear.cairo:
fn account_transfer_partial(
ref self: ContractState,
account: ContractAddress,
receiver: ContractAddress,
account_share: felt252,
amount_collateral: felt252,
) -> felt252 {
// Validate account share is between 0 and 1
assert!(
account_share.try_into().unwrap() > 0_i128
&& account_share.try_into().unwrap() <= ONE,
"AccountTransfer: account_share must be within [1,100000000]",
);
// detect transfer restriction
self
.token
._detect_account_transfer_restriction(account, receiver, account_share.into());
// Load account state and verify account is healthy
let account_state = self._load_account_v2(account);
let excess_balance = account_state.excess_balance(MARGIN_CHECK_MAINTENANCE);
assert!(excess_balance >= 0_i128, "AccountTransfer: account must be healthy");
// Get account value and settlement token info
let account_value = account_state.account_value();
// Standard transfer mode, % of both collateral and positions
if amount_collateral == 0 {
// Transfer each perpetual position
self._transfer_positions_internal(account_state, receiver, account_share, 0);
// Transfer proportional collateral
let token_transfer_share = mul_128(
account_value, account_share.try_into().unwrap(),
);
let token_transfer = div_128(
token_transfer_share, account_state.asset_data.settlement_token_price,
);
self
.token
.transfer_internal(
account, receiver, self.getSettlementTokenAsset(), token_transfer.into(), 1,
);
// Fast transfer mode, collateral only
} else {
1) self
.token
.transfer_internal(
account, receiver, self.getSettlementTokenAsset(), amount_collateral, 1,
);
}
...
}We asuume that amount_collateral is not zero, so transfer_internal() will be called.
Also note, that there is no any validations of amount_collateral parameter (it is felt252).
Now let's look at transfer_internal function from token.cairo:
Note that if 'amount' is signed integer (i128)
If 'amount' is negative, this check could be easily bypassed
Sender balance is updated, so if 'amount' is very small negative value, sender balance will be increased !
As a result, sender will be able to increase balance to arbitrary value and later drain all contract's funds.
Impact Details
Attacker will be able to drain all funds from the contract.
Proof of Concept
Proof of Concept
How to reproduce:
In this scenario we assume the Admin deposited some amount of funds to Paradex contract.
Now attack works like this:
Alice deposits very small amount of funds to contract
Alice calls account_transfer_partial() with crafted parameters
Now Alice is able withdraw all contract's funds
PoC reproduction:
apply patch (see gist link - https://gist.github.com/gln7/19536922bbd3a87d56557f82f446b9d7 )
copy mock_erc20.cairo to paraclear/src directory
run poc
As you can see, Alice was able to drain all USDC from contract.
Was this helpful?