#47313 [SC-Insight] Transfer(...) function doesn't account for current USDC price
Submitted on Jun 12th 2025 at 14:46:43 UTC by @Kalogerone for IOP | Paradex
Report ID: #47313
Report Type: Smart Contract
Report severity: Insight
Target: https://github.com/tradeparadex/audit-competition-may-2025/tree/main/paraclear
Impacts:
Contract fails to deliver promised returns, but doesn't lose value
Description
Brief/Intro
The Paraclear contract implements the transfer(...)
function that allows users to transfer collateral balance to other accounts. However, it doesn't account for the current USDC/collateral price at the moment of the transfer.
Vulnerability Details
This is the implementation of the function:
fn transfer(
ref self: ContractState,
recipient: ContractAddress,
token_address: ContractAddress,
amount: felt252,
) -> felt252 {
let sender = get_caller_address();
self.account._add_new_account_if_not_exists(recipient);
self.token._transfer(sender, recipient, token_address, amount);
let account_state = self._load_account_v2(sender);
let free_balance: i128 = account_state.free_balance().try_into().unwrap();
@> assert!(free_balance >= 0, "Transfer: Sender is unhealthy after transfer");
return 1;
}
As we can see, the only assertion after the transfer is that the free balance
is greater than 0. However, it is possible that for a timestamp that USDC price is $1.1, that free balance
is greater than 0 but when USDC comes back to $1, then without anything else changing the free balance
would fall under 0.
This check is done correctly in the withdraw(...)
function:
fn withdraw(
ref self: ContractState, token_address: ContractAddress, amount: felt252,
) -> felt252 {
self.token.assert_withdrawals_allowed();
let caller = get_caller_address();
let account_state = self._load_account_v2(caller);
let free_balance: i128 = account_state.free_balance().try_into().unwrap();
let amount_128: i128 = amount.try_into().unwrap();
let socialized_loss_factor: i128 = self.getSocializedLossFactor().try_into().unwrap();
let amount_settlement_full: i128 = mul_128(
amount_128, account_state.asset_data.settlement_token_price.into(),
)
.into();
@> assert!(
free_balance >= amount_settlement_full,
"Withdraw: Insufficient free balance {free_balance}, amount: {amount_settlement_full}",
);
...
Here it takes the amount of tokens and applies the USDC price at the current time to compare with the full balance
. This ensures that any price deviation of USDC won't affect the health of the account in the future.
The same should be done with the transfer(...)
function. The value of the collateral transferred is what matters to a healthy position check.
Impact Details
Users who want to withdraw more collateral than they are allowed to can wait for the collateral price to rise, transfer the collateral to another account while keeping their initial account "healthy" and withdraw from the second account. Also unsuspected users will see their accounts' free balance become negative as collateral prices go back to normal.
References
https://github.com/tradeparadex/audit-competition-may-2025/blob/main/paraclear/src/paraclear/paraclear.cairo#L1315
https://github.com/tradeparadex/audit-competition-may-2025/blob/main/paraclear/src/paraclear/paraclear.cairo#L1280
Proof of Concept
Proof of Concept
Bob wants to withdraw certain amount of USDC but withdraw function doesn't let him as he doesn't have enough free balance
Bob creates another address and
transfers(...)
his required USDC there at a moment where USDC has increased in priceBob withdraws the USDC from his other address
Was this helpful?