#46747 [SC-Insight] Self-Referral Vulnerability in Account Referral System
Submitted on Jun 4th 2025 at 07:21:29 UTC by @Catchme for IOP | Paradex
Report ID: #46747
Report Type: Smart Contract
Report severity: Insight
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
Summary
There is a critical vulnerability in the set_account_referral
function that allows an account to be set as its own referrer, creating a circular referral system that can be exploited to reduce trading fees unfairly.
Vulnerability Details
In AccountComponent::set_account_referral
:
fn set_account_referral(
ref self: ComponentState<TContractState>,
account: ContractAddress,
referrer: ContractAddress,
commission: felt252,
discount: felt252,
) {
self.assert_only_role(roles::CONFIGURATOR_ROLE);
self
.Paraclear_account_referral
.write(
account,
AccountReferral {
referrer: referrer, fee_commission: commission, fee_discount: discount,
},
);
self
.emit(
AccountReferralUpdate {
account: account,
referrer: referrer,
fee_commission: commission,
fee_discount: discount,
},
);
}
The function lacks validation to ensure that account != referrer
. This allows setting up self-referrals where an account can refer itself, creating a circular referral relationship.
Impact
Fee Manipulation: An account could receive both fee discounts as a referred account and fee commissions as a referrer for the same trades, allowing users to significantly reduce trading fees.
Economic Exploit: The system's fee calculation logic in
get_trade_fee_and_referral_commission
would apply both a discount and generate commissions for the same account:if self.referral.fee_discount != @0 { let one_minus_discount = math::MULTIPLIER - (*self.referral.fee_discount).try_into().unwrap(); let fee_after_discount = mul_128(fee, one_minus_discount); if self.referral.fee_commission != @0 { let fee_commission = mul_128( fee_after_discount, (*self.referral.fee_commission).try_into().unwrap(), ); return (fee_after_discount, *self.referral.referrer, fee_commission); } return (fee_after_discount, *self.referral.referrer, 0); }
Protocol Revenue Loss: This could result in reduced protocol revenue from trading fees.
Recommended Fix
Add a validation check to ensure that account
and referrer
cannot be the same address:
fn set_account_referral(
ref self: ComponentState<TContractState>,
account: ContractAddress,
referrer: ContractAddress,
commission: felt252,
discount: felt252,
) {
self.assert_only_role(roles::CONFIGURATOR_ROLE);
// Prevent self-referral
assert(account != referrer, 'Self-referral not allowed');
self
.Paraclear_account_referral
.write(
account,
AccountReferral {
referrer: referrer, fee_commission: commission, fee_discount: discount,
},
);
self
.emit(
AccountReferralUpdate {
account: account,
referrer: referrer,
fee_commission: commission,
fee_discount: discount,
},
);
}
Proof of Concept
Proof of Concept
A user with
CONFIGURATOR_ROLE
callsset_account_referral
with:account
= user's addressreferrer
= same user's addresscommission
=3000discount
= 2000
When the user trades, they receive both a 20% discount on fees and earn 30% commission on these already discounted fees.
Was this helpful?