All incidents

Nimbus NBU Exploit via Referral-Fee Reserve Desync

Share
Sep 14, 2021 18:47 UTCAttackLoss: 0.27 NBU_WETH, 7,000.66 NBUManually checked1 exploit txWindow: Atomic
Estimated Impact
0.27 NBU_WETH, 7,000.66 NBU
Label
Attack
Exploit Tx
1
Addresses
1
Attack Window
Atomic
Sep 14, 2021 18:47 UTC → Sep 14, 2021 18:47 UTC

Exploit Transactions

TX 1Ethereum
0x5908622ce9670fdcd7954aa098aadb3e13882f198b795c6fea5ee6fc2c802d3c
Sep 14, 2021 18:47 UTCExplorer

Victim Addresses

0xa0ff0e694275023f4986dc3ca12a6eb5d6056c62Ethereum

Loss Breakdown

0.271812NBU_WETH
7,000.66NBU

Similar Incidents

Root Cause Analysis

Nimbus NBU Exploit via Referral-Fee Reserve Desync

1. Incident Overview TL;DR

An attacker EOA used a custom router to call the Nimbus NBU/NWETH pair with referral fees enabled, exploiting a reserve-accounting bug in NimbusPair.swap so that the pool's recorded reserves overstated its actual token balances and allowed large NBU and NBU_WETH outflows to attacker-related accounts for a net positive ETH-denominated profit.

NimbusPair.swap transfers referral fees after sampling token balances and then updates reserves using the stale pre-fee balances, breaking the constant-product and reserve-equals-balance invariants and enabling systematic draining of liquidity.

2. Key Background

  • Nimbus implements an AMM-style pair contract NimbusPair for token pairs such as NBU/NWETH, with constant-product pricing and reserve tracking similar to Uniswap V2.
  • NBU is the protocol token, and NBU_WETH is a wrapped ETH token with 1:1 deposit and withdraw semantics used as the second leg of the exploited pair.
  • NimbusReferralProgram integrates with NimbusPair.swap to collect a percentage of each swap input as a referral fee, accounting these amounts via internal recorded balances and fee mappings.
  • NimbusFactory deploys NimbusPair instances and holds configuration such as the referral program address and fee parameters for each pair.

3. Vulnerability Analysis & Root Cause Summary

4. Detailed Root Cause Analysis

// NimbusPair.swap core logic with referral fee and reserve update
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external override lock {
    require(amount0Out > 0 || amount1Out > 0, "Nimbus: INSUFFICIENT_OUTPUT_AMOUNT");
    (uint112 _reserve0, uint112 _reserve1,) = getReserves();
    require(amount0Out < _reserve0 && amount1Out < _reserve1, "Nimbus: INSUFFICIENT_LIQUIDITY");
    uint balance0; uint balance1; {
        address _token0 = token0; address _token1 = token1;
        require(to != _token0 && to != _token1, "Nimbus: INVALID_TO");
        if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out);
        if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out);
        if (data.length > 0) INimbusCallee(to).NimbusCall(msg.sender, amount0Out, amount1Out, data);
        balance0 = IERC20(_token0).balanceOf(address(this));
        balance1 = IERC20(_token1).balanceOf(address(this));
    }
    uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
    uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
    require(amount0In > 0 || amount1In > 0, "Nimbus: INSUFFICIENT_INPUT_AMOUNT");
    {
        address referralProgram = INimbusFactory(factory).nimbusReferralProgram();
        if (amount0In > 0) {
            address _token0 = token0;
            uint refFee = amount0In * 3 / 1994;
            _safeTransfer(_token0, referralProgram, refFee);
            INimbusReferralProgram(referralProgram).recordFee(_token0, to, refFee);
        }
        if (amount1In > 0) {
            uint refFee = amount1In * 3 / 1994;
            address _token1 = token1;
            _safeTransfer(_token1, referralProgram, refFee);
            INimbusReferralProgram(referralProgram).recordFee(_token1, to, refFee);
        }
    }
    {
        uint balance0Adjusted = balance0 * 10000 - amount0In * 15;
        uint balance1Adjusted = balance1 * 10000 - amount1In * 15;
        require(balance0Adjusted * balance1Adjusted >= uint(_reserve0) * _reserve1 * 1000**2, "Nimbus: K");
    }
    _update(balance0, balance1, _reserve0, _reserve1);
    emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}

Source: NimbusPair verified contract for pool 0xa0ff0e69…5c62; demonstrates that referral fees are transferred after sampling balances and before reserves are updated.

// ERC20 balance changes for exploit tx 0x5908…c802d3c (NBU_WETH and NBU)
{
  "chainid": 1,
  "txhash": "0x5908622ce9670fdcd7954aa098aadb3e13882f198b795c6fea5ee6fc2c802d3c",
  "native_balance_deltas": [
    {
      "address": "0x5676e585bf16387bc159fd4f82416434cda5f1a3",
      "before_wei": "1217282655090072657",
      "after_wei": "1189635062616385249",
      "delta_wei": "-27647592473687408"
    },
    {
      "address": "0xea674fdde714fd979de3edf0f56aa9716b898ec8",
      "before_wei": "3146683582440901850232",
      "after_wei": "3146684346061761850232",
      "delta_wei": "763620860000000"
    }
  ],
  "erc20_balance_deltas": [
    {
      "token": "0x0bcd83df58a1bfd25b1347f9c9da1b7118b648a6",
      "holder": "0xa0ff0e694275023f4986dc3ca12a6eb5d6056c62",
      "before": "272244859570035557",
      "after": "432976270416552",
      "delta": "-271811883299619005",
      "balances_slot": "3",
      "slot_key": "0x910b008eff8f9b7933f00d4652ad30c1b13537d18a4609c56ca8861cf3b936e0",
      "contract_name": "NBU_WETH"
    },
    {
      "token": "0x0bcd83df58a1bfd25b1347f9c9da1b7118b648a6",
      "holder": "0xe5ad1a7c9ecfd77c856c211fd5df26a04a72c365",
      "before": "3750501458924338092",
      "after": "3750603656649604278",
      "delta": "102197725266186",
      "balances_slot": "3",
      "slot_key": "0x96ca51d649ba4fac90c06d4c820edf70b51bbd8caa27414fa5e753507abaa29b",
      "contract_name": "NBU_WETH"
    },
    {
      "token": "0x0bcd83df58a1bfd25b1347f9c9da1b7118b648a6",
      "holder": "0x5676e585bf16387bc159fd4f82416434cda5f1a3",
      "before": "0",
      "after": "271709685574352819",
      "delta": "271709685574352819",
      "balances_slot": "3",
      "slot_key": "0xf116a045a9f6aaf1033ff02456f43c2c1f5d92e63da06dbe3c7219d696c65577",
      "contract_name": "NBU_WETH"
    },
    {
      "token": "0xeb58343b36c7528f23caae63a150240241310049",
      "holder": "0xa0ff0e694275023f4986dc3ca12a6eb5d6056c62",
      "before": "7011810487612862045579",
      "after": "11151533066888036027",
      "delta": "-7000658954545974009552",
      "balances_slot": "3",
      "slot_key": "0x910b008eff8f9b7933f00d4652ad30c1b13537d18a4609c56ca8861cf3b936e0",
      "contract_name": "NBU"
    },
    {
      "token": "0xeb58343b36c7528f23caae63a150240241310049",
      "holder": "0xe5ad1a7c9ecfd77c856c211fd5df26a04a72c365",
      "before": "38192307884100767421877",
      "after": "38194940040619584055801",
      "delta": "2632156518816633924",
      "balances_slot": "3",
      "slot_key": "0x96ca51d649ba4fac90c06d4c820edf70b51bbd8caa27414fa5e753507abaa29b",
      "contract_name": "NBU"
    },
    {
      "token": "0xeb58343b36c7528f23caae63a150240241310049",
      "holder": "0x5676e585bf16387bc159fd4f82416434cda5f1a3",
      "before": "2679828553611",
      "after": "6998026800706985929239",
      "delta": "6998026798027157375628",
      "balances_slot": "3",
      "slot_key": "0xf116a045a9f6aaf1033ff02456f43c2c1f5d92e63da06dbe3c7219d696c65577",
      "contract_name": "NBU"
    }
  ],
  "erc20_balance_delta_errors": [],
  "source_code": [
    {
      "layout_addr": "0x0bcd83df58a1bfd25b1347f9c9da1b7118b648a6",
      "path": "seed/1/0x0bcd83df58a1bfd25b1347f9c9da1b7118b648a6",
      "contract_name": "NBU_WETH"
    },
    {
      "layout_addr": "0xeb58343b36c7528f23caae63a150240241310049",
      "path": "seed/1/0xeb58343b36c7528f23caae63a150240241310049",
      "contract_name": "NBU"
    }
  ],
  "errors": []
}

Source: balance_diff.json for tx 0x5908…c802d3c; shows large outflows from NimbusPair and matching inflows to attacker EOA and NimbusReferralProgram.

5. Adversary Flow Analysis

6. Impact & Losses

During tx 0x5908622c...d3c, NimbusPair 0xa0ff0e69...5c62 transfers 271811883299619005 NBU_WETH and 7000658954545974009552 NBU out of the pool, primarily to attacker EOA 0x5676e585bf16387bc159fd4f82416434cda5f1a3 and NimbusReferralProgram 0xe5ad1a7c9ecfd77c856c211fd5df26a04a72c365, leaving significantly reduced pool balances and granting the attacker a net positive ETH-denominated gain via NBU_WETH after gas fees.

7. References

  • [1] Seed tx metadata and balance diff for 0x5908622c...d3c: artifacts/root_cause/seed/1/0x5908622ce9670fdcd7954aa098aadb3e13882f198b795c6fea5ee6fc2c802d3c/
  • [2] NimbusPair and NimbusFactory verified source: artifacts/root_cause/data_collector/iter_1/contract/1/0xa0ff0e694275023f4986dc3ca12a6eb5d6056c62/source/src/Contract.sol
  • [3] NimbusReferralProgram verified source: artifacts/root_cause/data_collector/iter_1/contract/1/0xe5ad1a7c9ecfd77c856c211fd5df26a04a72c365/source/src/Contract.sol
  • [4] PrestateTracer state diff for exploit tx: artifacts/root_cause/data_collector/iter_2/tx/1/0x5908622ce9670fdcd7954aa098aadb3e13882f198b795c6fea5ee6fc2c802d3c/state_diff.prestateTracer.json
  • [5] NBU token verified source: artifacts/root_cause/seed/1/0xeb58343b36c7528f23caae63a150240241310049/src/Contract.sol
  • [6] NBU_WETH token verified source: artifacts/root_cause/seed/1/0x0bcd83df58a1bfd25b1347f9c9da1b7118b648a6/src/Contract.sol