DexLyn Disclosed Report

Critical: repay_flash_swap accepts arbitrary token types, enabling theft of pool assets.

Company
Created date
Sep 08 2025

Target

hidden

Vulnerability Details

Summary:

repay_flash_swap lack token type validation, pool accepts repayment with arbitrary token types, enabling theft of real assets.

Details:

The function in pool.move lines 1245-1275, repay_flash_swap only checks the numeric amounts of the supplied FungibleAsset matches expected repayment. Doesn't validate that the asset's metadata type equals the pool's configured tokens.

Because of that:

  • An attacker can borrow real tokens
  • Then, the attacker can repay with arbitrary tokens, including worthless ones
  • The pool's internal accounting is corrupted, it thinks the repayment occurred. But the real balances did not increase.

Impact

  • Direct theft of funds: An attacker can borrow real USDT via flash_swap and "repay" with equal-sized FAKE tokens, walking away with the USDT.
  • Pool insolvency: Accounting shows healthy balances, but real reserves are drained.
  • This undermines the core invariants of the AMM, breaks user trust, and causes permanent loss of funds from the pool.

Root cause

in pool.move lines 1245-1275, repay_flash_swap only checks the numeric amounts of the supplied FungibleAsset matches expected repayment. Doesn't validate that the asset's metadata type equals the pool's configured tokens.

1241     public fun repay_flash_swap(
1242         asset_a: FungibleAsset,
1243         asset_b: FungibleAsset,
1244         receipt: FlashSwapReceipt
1245     ) acquires Pool {
1246         let FlashSwapReceipt {
1247             pool_address,
1248             a2b,
1249             partner_name,
1250             pay_amount,
1251             ref_fee_amount
1252         } = receipt;
1253         let pool = borrow_global_mut<Pool>(pool_address);
1254         if (a2b) {
1255             assert!(fungible_asset::amount(&asset_a) == pay_amount, EAMOUNT_INCORRECT);
1256             // send ref fee to partner
1257             if (ref_fee_amount > 0) {
1258                 let ref_fee = fungible_asset::extract(&mut asset_a, ref_fee_amount);
1259                 partner::receive_ref_fee(partner_name, ref_fee, pool.asset_a_addr);
1260             };
1261             primary_fungible_store::deposit(pool_address, asset_a);
1262             fungible_asset::destroy_zero(asset_b);
1263             pool.asset_a = pool.asset_a + pay_amount - ref_fee_amount;
1264         } else {
1265             assert!(fungible_asset::amount(&asset_b) == pay_amount, EAMOUNT_INCORRECT);
1266             // send ref fee to partner
1267             if (ref_fee_amount > 0) {
1268                 let ref_fee = fungible_asset::extract(&mut asset_b, ref_fee_amount);
1269                 partner::receive_ref_fee(partner_name, ref_fee, pool.asset_b_addr);
1270             };
1271             primary_fungible_store::deposit(pool_address, asset_b);
1272             fungible_asset::destroy_zero(asset_a);
1273             pool.asset_b = pool.asset_b + pay_amount - ref_fee_amount;
1274         }
1275     }

Validation steps

Here is my PoC's high-level steps:

Setup Alice creates a USDC/USDT pool and deposits honest liquidity. Attacker: Bob mints a FAKE token.

Exploit of Flash Swap:

  • Borrow 4970 USDT with pool::flash_swap.
  • Required to repay 5000 USDC.
  • Instead, call pool::repay_flash_swap with 5000 FAKE tokens.
  • Result: Pool accepts FAKE, Bob keeps real USDT.

See run logs:

Bob borrowed USDT: 4970
Should repay USDC: 5000
EXPLOIT: Repaying with FAKE tokens instead of USDC
EXPLOIT SUCCESSFUL: Pool accepted fake tokens!
Bob's stolen USDT: 4970
Pool real USDC balance before/after: 5982 -> 5982
Pool real USDT balance before/after: 5982 -> 1012
cd CLMM_Dex/DexlynClmm
cp ~/Downloads/token_exploit.t.move CLMM_Dex/DexlynClmm/tests
aptos move test --dev --ignore-compile-warnings --skip-attribute-checks --filter token_exploit_test::test_token_type_validation_exploit

You can grab the test file here https://gist.githubusercontent.com/niafreu/183eaa7f6d89c92c3c934cfd953c517d/raw/10eba79f9681c2f8744600754c22ba1fd2cf1c88/token_exploit.t.move

Attachments

hidden
CommentsReport History
Comments on this report are hidden
Details
State
hidden
Severity
Critical
Bounty$1,855
Visibilitypartially
VulnerabilityBusiness Logic Errors
Participants
hidden