#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

  1. 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.

  2. 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);
    }
  3. Protocol Revenue Loss: This could result in reduced protocol revenue from trading fees.

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

  1. A user with CONFIGURATOR_ROLE calls set_account_referral with:

    • account = user's address

    • referrer = same user's address

    • commission =3000

    • discount = 2000

  2. When the user trades, they receive both a 20% discount on fees and earn 30% commission on these already discounted fees.

Was this helpful?