#47370 [SC-Critical] `account_transfer_partial` should not be enabled when `transfer_registry_address` is not configured.
Submitted on Jun 13th 2025 at 05:12:31 UTC by @shaflow1 for IOP | Paradex
Report ID: #47370
Report Type: Smart Contract
Report severity: Critical
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
When transfer_registry_address
is not yet configured, _detect_account_transfer_restriction
will skip the check. However, _detect_account_transfer_restriction
is the only mechanism for enforcing the account_transfer_partial
permission. If the check is skipped, it will result in unrestricted fund transfers. Therefore, _detect_account_transfer_restriction
should revert when transfer_registry_address
is not configured, instead of skipping the check.
Vulnerability Details
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());
account_transfer_partial
is used for fund withdrawals and vault closure fund transfers. The only permission control in this function is the _detect_account_transfer_restriction
function.
fn _detect_account_transfer_restriction(
self: @ComponentState<TContractState>,
sender: ContractAddress,
recipient: ContractAddress,
account_share: u256,
) {
// Read transfer registry address
let transfer_registry_address = self.Paraclear_transfer_registry.read();
// If transfer registry is set, check restrictions
if transfer_registry_address.is_non_zero() {
let registry_dispatcher = IRegistryDispatcher {
contract_address: transfer_registry_address,
};
let is_account_transfer_restricted: u8 = registry_dispatcher
.detect_account_transfer_restriction(sender, recipient, account_share);
assert!(
is_account_transfer_restricted == 0,
"Transfer: Account transfer is not allowed",
);
}
}
However, when transfer_registry_address
is not configured, _detect_account_transfer_restriction
skips the check, which causes the sole permission control for account_transfer_partial
to fail. As a result, anyone can transfer others’ positions and funds.
Therefore, when transfer_registry_address
is not configured, the function should revert instead of skipping the check.
Impact Details
When transfer_registry_address
is not configured, anyone can transfer the full amount of another user's funds.
References
https://github.com/tradeparadex/audit-competition-may-2025/blob/0eb81b26a67666c399b4e16b39a96c19848ab7fd/paraclear/src/token/token.cairo#L322
Proof of Concept
Proof of Concept
The contract has not yet enabled the vault and
transfer_registry_address
is not configured.Anyone can use the
account_transfer_partial
function to transfer another user's funds to their own account, thereby stealing funds.
Was this helpful?