Skip to content

Add upper bound validation for integrator_fee_amount_bps in swap_exact_token_to#12

Open
RogerZoe wants to merge 11 commits into
avnu-labs:mainfrom
RogerZoe:arif-shadow-review
Open

Add upper bound validation for integrator_fee_amount_bps in swap_exact_token_to#12
RogerZoe wants to merge 11 commits into
avnu-labs:mainfrom
RogerZoe:arif-shadow-review

Conversation

@RogerZoe

@RogerZoe RogerZoe commented Mar 2, 2026

Copy link
Copy Markdown

Summary

This PR introduces validation for integrator_fee_amount_bps in swap_exact_token_to.

Currently, integrator_fee_amount_bps is not bounded. Under FeeOnBuy policy, large values can inflate internal_buy_token_amount, causing _swap_exact_token_to to revert due to exceeding sell_token_max_amount.

While this does not lead to fund loss, it degrades swap liveness and introduces a griefing vector if the fee parameter is externally controlled (e.g., via integrator backend or relayer).


Problem

For FeeOnBuy:

integrator_fee = buy_token_amount * bps / 10000
internal_buy_token_amount = buy_token_amount + integrator_fee

If bps is excessively large (e.g., 50,000 = 500%), the router attempts to acquire an unrealistic buy amount.

This inflated target propagates into _swap_exact_token_to, where the iterative swap loop attempts to pull additional sell tokens until the target is met. Eventually, the required sell amount exceeds sell_token_max_amount, causing the transaction to revert with:

Insufficient token from amount

This ties an unbounded economic parameter directly to execution feasibility and allows economically nonsensical inputs to disrupt swap execution.


Proof of Concept

The following test demonstrates the issue:

#[test]
#[available_gas(20000000)]
#[should_panic(expected: ('Insufficient token from amount', 'ENTRYPOINT_FAILED'))]
fn poc_integrator_fee_bps_unbounded_causes_revert_feeonbuy() {
    // Given: Normal swap setup
    let (exchange, ownable, fee) = deploy_exchange();
    let beneficiary = contract_address_const::<0x12345>();
    let sell_token = deploy_mock_token(beneficiary, 120000, 1);
    let sell_token_address = sell_token.contract_address;
    let buy_token = deploy_mock_token(beneficiary, 0, 2);
    let buy_token_address = buy_token.contract_address;
    
    // Configure fee policy to FeeOnBuy (default: no special config needed)
    set_contract_address(ownable.get_owner());
    let fees_recipient = contract_address_const::<0x1111>();
    fee.set_fees_recipient(fees_recipient);
    
    let sell_token_max_amount = u256 { low: 120000, high: 0 };
    let sell_token_amount = u256 { low: 8000, high: 0 };
    let buy_token_amount = u256 { low: 100000, high: 0 }; // User expects 100k buy tokens
    
    let mut routes = ArrayTrait::new();
    routes.append(
        Route {
            sell_token: sell_token_address,
            buy_token: buy_token_address,
            swap: RouteSwap::Direct(
                DirectSwap {
                    exchange_address: contract_address_const::<0x12>(),
                    percent: 100 * ROUTE_PERCENT_FACTOR,
                    additional_swap_params: ArrayTrait::new(),
                },
            ),
        },
    );
    
    set_contract_address(beneficiary);
    sell_token.approve(exchange.contract_address, sell_token_max_amount);
    
    // MALICIOUS INPUT: 50,000 BPS = 500% integrator fee
    // For FeeOnBuy: integrator_fee = 100000 * 50000 / 10000 = 5,00,000
    // internal_buy_token_amount = 100000 + 500000 = 600000
    // This inflated target is passed to _swap_exact_token_to, causing the 
    // iterative loop to request far more sell tokens than sell_token_max_amount allows
    let integrator_fee_recipient = contract_address_const::<0x9999>();
    let integrator_fee_bps = 50000_u128; // 500% - ECONOMICALLY NONSENSICAL

    // When: Attacker submits swap with excessive integrator fee
    exchange.swap_exact_token_to(
        sell_token_address,
        sell_token_amount,
        sell_token_max_amount,
        buy_token_address,
        buy_token_amount,
        beneficiary,
        integrator_fee_bps, // ← Unbounded input
        integrator_fee_recipient,
        routes,
    );
    
    // Then: Transaction reverts inside _swap_exact_token_to loop with 
    // 'Insufficient token from amount' because the inflated target requires
    // more sell tokens than the user approved (sell_token_max_amount)
    // This is a DoS vector: attacker can grief any swap by inflating BPS
}

With:

  • buy_token_amount = 100,000
  • integrator_fee_bps = 50,000

The computed values become:

integrator_fee = 100000 * 50000 / 10000 = 500000
internal_buy_token_amount = 600000

The swap loop attempts to reach 600,000 buy tokens, eventually exceeding sell_token_max_amount and reverting.


Impact

  • No direct fund loss (transaction reverts atomically)
  • Swap liveness can be disrupted
  • If integrator_fee_amount_bps is externally controlled, an attacker can grief swaps by forcing predictable reverts
  • Router robustness depends on implicit assumptions about BPS sanity

Severity: Informational → Low (depending on parameter control surface)


Mitigation Options

Option 1 — Hard Cap at 100%

Enforce:

assert(integrator_fee_amount_bps <= 10000, 'Integrator fee too high');

This ensures fees cannot exceed 100% of the reference amount.


Option 2 — Protocol-Level Maximum

Define a protocol constant:

const MAX_INTEGRATOR_FEE_BPS: u128 = 1000; // example: 10%

Then enforce:

assert(integrator_fee_amount_bps <= MAX_INTEGRATOR_FEE_BPS, 'Integrator fee too high');

This enforces economically reasonable limits aligned with protocol policy.


Option 3 — Defensive Target Bounding (Alternative Approach)

Instead of only bounding BPS, cap the internal target:

internal_buy_token_amount <= buy_token_amount + max_expected_slippage

This decouples execution feasibility from fee amplification.


Documented a vulnerability related to unbounded integrator fee amounts in the swap process, outlining the root cause, impact, and recommended mitigation strategies.
Add test for unbounded integrator fee causing transaction revert due to excessive fee input.
Add test for unbounded integrator fee causing revert in swap.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant