├── foundry.toml ├── .gitignore ├── .gitmodules ├── script └── Deploy.s.sol ├── test └── Solution.t.sol ├── .github └── workflows │ └── test.yml └── src └── Challenge.sol /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | 6 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config 7 | 8 | [fuzz] 9 | runs = 1000000 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/solvm"] 5 | path = lib/solvm 6 | url = https://github.com/brockelmore/solvm 7 | [submodule "lib/memmove"] 8 | path = lib/memmove 9 | url = https://github.com/brockelmore/memmove 10 | -------------------------------------------------------------------------------- /script/Deploy.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.17; 3 | 4 | import "forge-std/Script.sol"; 5 | import "../src/Challenge.sol"; 6 | 7 | 8 | contract CounterScript is Script { 9 | function setUp() public {} 10 | 11 | function run() public { 12 | vm.broadcast(0x230d31EEC85F4063a405B0F95bdE509C0d0A8b5D); 13 | MiniMutant a = new MiniMutant(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/Solution.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.17; 3 | 4 | import "forge-std/Test.sol"; 5 | import "forge-std/console2.sol"; 6 | import "../src/Challenge.sol"; 7 | 8 | contract CurtaTest is Test { 9 | MiniMutant curta; 10 | function setUp() public { 11 | curta = new MiniMutant(); 12 | } 13 | 14 | function test_solution() public { 15 | 16 | } 17 | } -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /src/Challenge.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.17; 3 | 4 | import "memmove/Mapping.sol"; 5 | import "solvm/Evm.sol"; 6 | 7 | 8 | interface IPuzzle { 9 | /// @notice Returns the puzzle's name. 10 | /// @return The puzzle's name. 11 | function name() external pure returns (string memory); 12 | 13 | /// @notice Generates the puzzle's starting position based on a seed. 14 | /// @dev The seed is intended to be `msg.sender` of some wrapper function or 15 | /// call. 16 | /// @param _seed The seed to use to generate the puzzle. 17 | /// @return The puzzle's starting position. 18 | function generate(address _seed) external returns (uint256); 19 | 20 | /// @notice Verifies that a solution is valid for the puzzle. 21 | /// @dev `_start` is intended to be an output from {IPuzzle-generate}. 22 | /// @param _start The puzzle's starting position. 23 | /// @param _solution The solution to the puzzle. 24 | /// @return Whether the solution is valid. 25 | function verify(uint256 _start, uint256 _solution) external returns (bool); 26 | } 27 | 28 | struct Solution { 29 | uint8 stop; 30 | uint8 ctxSetting; 31 | bytes code; 32 | } 33 | 34 | contract MiniMutant is IPuzzle { 35 | using EvmLib for Evm; 36 | using MappingLib for Mapping; 37 | 38 | function name() external pure returns (string memory) { 39 | return "Mini Mutant"; 40 | } 41 | 42 | function generate(address _seed) external returns (uint256) { 43 | return uint256(keccak256(abi.encodePacked(_seed))); 44 | } 45 | 46 | function verify(uint256 _start, uint256 _solution) external returns (bool) { 47 | 48 | Solution memory sol = Solution({ 49 | stop: 0, 50 | ctxSetting: 0, 51 | code: abi.encodePacked(_solution) 52 | }); 53 | 54 | address origin = tx.origin; 55 | address caller = msg.sender; 56 | address execution_address = address(this); 57 | bytes memory calld = abi.encodePacked(_start); 58 | 59 | while (sol.stop == 0) { 60 | if (sol.ctxSetting == 0) { 61 | origin = tx.origin; 62 | caller = msg.sender; 63 | execution_address = address(this); 64 | } else if (sol.ctxSetting == 1) { 65 | origin = msg.sender; 66 | caller = address(this); 67 | } 68 | 69 | (bool succ, bytes memory ret) = this.run(origin, caller, execution_address, calld, sol.code); 70 | require(succ, "Script failed"); 71 | require(ret.length > 2, "Bad return length"); 72 | sol.stop = uint8(ret[0]); 73 | sol.ctxSetting = uint8(ret[1]); 74 | sol.code = slice(ret, 2, ret.length - 2); 75 | } 76 | 77 | (bool succ, bytes memory ret) = this.run(origin, caller, execution_address, calld, sol.code); 78 | require(succ, "Script failed"); 79 | uint256 result = abi.decode(ret, (uint256)); 80 | require(result == _start, "Fail"); 81 | return true; 82 | } 83 | 84 | function run( 85 | address origin, 86 | address caller, 87 | address execution_address, 88 | bytes calldata calld, 89 | bytes calldata executionCode 90 | ) external returns (bool, bytes memory) { 91 | uint256 fee; 92 | uint256 id; 93 | assembly("memory-safe") { 94 | fee := basefee() 95 | id := chainid() 96 | } 97 | EvmContext memory ctx = EvmContext({ 98 | origin: origin, 99 | caller: caller, 100 | execution_address: execution_address, 101 | callvalue: 0, 102 | coinbase: block.coinbase, 103 | timestamp: block.timestamp, 104 | number: block.number, 105 | gaslimit: block.gaslimit, 106 | difficulty: block.difficulty, 107 | chainid: id, 108 | basefee: fee, 109 | balances: MappingLib.newMapping(1), 110 | calld: calld 111 | }); 112 | Evm evm = EvmLib.newEvm(ctx); 113 | 114 | // NOTE: No frame switching opcodes are supported (i.e. Call, Create, etc.) 115 | return evm.evaluate(executionCode); 116 | } 117 | 118 | function slice( 119 | bytes memory _bytes, 120 | uint256 _start, 121 | uint256 _length 122 | ) 123 | internal 124 | pure 125 | returns (bytes memory) 126 | { 127 | require(_length + 31 >= _length, "slice_overflow"); 128 | require(_bytes.length >= _start + _length, "slice_outOfBounds"); 129 | 130 | bytes memory tempBytes; 131 | 132 | assembly { 133 | switch iszero(_length) 134 | case 0 { 135 | // Get a location of some free memory and store it in tempBytes as 136 | // Solidity does for memory variables. 137 | tempBytes := mload(0x40) 138 | 139 | // The first word of the slice result is potentially a partial 140 | // word read from the original array. To read it, we calculate 141 | // the length of that partial word and start copying that many 142 | // bytes into the array. The first word we copy will start with 143 | // data we don't care about, but the last `lengthmod` bytes will 144 | // land at the beginning of the contents of the new array. When 145 | // we're done copying, we overwrite the full first word with 146 | // the actual length of the slice. 147 | let lengthmod := and(_length, 31) 148 | 149 | // The multiplication in the next line is necessary 150 | // because when slicing multiples of 32 bytes (lengthmod == 0) 151 | // the following copy loop was copying the origin's length 152 | // and then ending prematurely not copying everything it should. 153 | let mc := add(add(tempBytes, lengthmod), mul(0x20, iszero(lengthmod))) 154 | let end := add(mc, _length) 155 | 156 | for { 157 | // The multiplication in the next line has the same exact purpose 158 | // as the one above. 159 | let cc := add(add(add(_bytes, lengthmod), mul(0x20, iszero(lengthmod))), _start) 160 | } lt(mc, end) { 161 | mc := add(mc, 0x20) 162 | cc := add(cc, 0x20) 163 | } { 164 | mstore(mc, mload(cc)) 165 | } 166 | 167 | mstore(tempBytes, _length) 168 | 169 | //update free-memory pointer 170 | //allocating the array padded to 32 bytes like the compiler does now 171 | mstore(0x40, and(add(mc, 31), not(31))) 172 | } 173 | //if we want a zero-length slice let's just return a zero-length array 174 | default { 175 | tempBytes := mload(0x40) 176 | //zero out the 32 bytes slice we are about to return 177 | //we need to do it because Solidity does not garbage collect 178 | mstore(tempBytes, 0) 179 | 180 | mstore(0x40, add(tempBytes, 0x20)) 181 | } 182 | } 183 | 184 | return tempBytes; 185 | } 186 | } --------------------------------------------------------------------------------