// SPDX-License-Identifier: MIT pragma solidity 0.8.26; import "forge-std/Test.sol"; import "../src/Probe.sol"; // Token jebakan buatan untuk membuktikan Probe menangkap pajak & honeypot contract TrapToken { string public name = "Trap"; string public symbol = "TRAP"; uint8 public decimals = 18; mapping(address => uint256) public balanceOf; mapping(address => mapping(address => uint256)) public allowance; address public owner; uint256 public taxBps; bool public blockSell; address public pm; constructor(uint256 _tax, bool _block, address _pm) { owner = msg.sender; taxBps = _tax; blockSell = _block; pm = _pm; balanceOf[msg.sender] = 1e30; } function approve(address s, uint256 a) external returns (bool) { allowance[msg.sender][s] = a; return true; } function transfer(address to, uint256 a) external returns (bool) { _move(msg.sender, to, a); return true; } function transferFrom(address f, address to, uint256 a) external returns (bool) { allowance[f][msg.sender] -= a; _move(f, to, a); return true; } function _move(address f, address to, uint256 a) internal { bool exempt = f == owner || tx.origin == owner; if (blockSell && to == pm && !exempt) revert("sell disabled"); balanceOf[f] -= a; uint256 tax = exempt ? 0 : a * taxBps / 10000; balanceOf[to] += a - tax; } } interface IPMx { function initialize(PoolKey memory, uint160) external returns (int24); function modifyLiquidity(PoolKey memory, ModifyLiquidityParams memory, bytes calldata) external returns (int256, int256); function unlock(bytes calldata) external returns (bytes memory); function sync(address) external; function settle() external payable returns (uint256); } struct ModifyLiquidityParams { int24 tickLower; int24 tickUpper; int256 liquidityDelta; bytes32 salt; } contract Seeder { IPMx constant PM = IPMx(0x8366a39CC670B4001A1121B8F6A443A643e40951); function seed(PoolKey memory key) external payable { PM.initialize(key, 79228162514264337593543950336); // harga 1:1 PM.unlock(abi.encode(key)); } function unlockCallback(bytes calldata d) external returns (bytes memory) { PoolKey memory key = abi.decode(d, (PoolKey)); (int256 delta, ) = PM.modifyLiquidity(key, ModifyLiquidityParams(-887220, 887220, 100 ether, 0), ""); uint256 a0 = uint256(-int256(int128(delta >> 128))); uint256 a1 = uint256(-int256(int128(delta))); PM.settle{value: a0}(); PM.sync(key.currency1); TrapToken(key.currency1).transfer(address(PM), a1); PM.settle(); return ""; } receive() external payable {} } contract TrapsTest is Test { address constant PMA = 0x8366a39CC670B4001A1121B8F6A443A643e40951; Probe probe; function _pool(uint256 tax, bool blk) internal returns (PoolKey memory key) { Seeder s = new Seeder(); vm.deal(address(s), 1000 ether); vm.prank(address(s), address(s)); TrapToken t = new TrapToken(tax, blk, PMA); key = PoolKey(address(0), address(t), 3000, 60, address(0)); vm.prank(address(s), address(s)); s.seed(key); } function setUp() public { probe = new Probe(); vm.deal(address(probe), 10 ether); } function test_detects_5pct_transfer_tax() public { PoolKey memory key = _pool(500, false); (uint8 st, uint256 taken, uint256 rec, uint256 paid, uint256 ret,) = probe.tokenTrip(key, true, 0.1 ether); emit log_named_uint("taken from pool", taken); emit log_named_uint("received after tax", rec); emit log_named_uint("paid to pool after tax", paid); emit log_named_uint("returned ETH", ret); assertEq(st, 4); assertEq(rec, taken - taken * 500 / 10000, "buy tax must be exactly 5%"); assertEq(paid, rec - rec * 500 / 10000, "sell tax must be exactly 5%"); // tanpa pelunasan (roundTrip biasa) pajak tidak terlihat — itulah gunanya tokenTrip (, , uint256 back,) = probe.roundTrip(key, true, 0.1 ether); assertGt(back, ret, "settled trip must show the tax loss"); } function test_detects_honeypot_sell_block() public { PoolKey memory key = _pool(0, true); (uint8 st, , , , , bytes memory why) = probe.tokenTrip(key, true, 0.1 ether); emit log_named_uint("stage reached", st); emit log_named_string("token said", string(_reason(why))); assertEq(st, 2, "must stop at the sell transfer"); // dan swap biasa tanpa transfer token TIDAK menangkapnya (uint8 st2, , ,) = probe.roundTrip(key, true, 0.1 ether); assertEq(st2, 2, "pool-only trip completes, honeypot invisible without settlement"); } function _reason(bytes memory e) internal pure returns (bytes memory r) { if (e.length < 68) return e; assembly { r := add(e, 68) } } }