Puzzle #21
Submerged
Author
0xb49bf876be26435b6fae1ef42c3c82c5867fa149
chainlight.io

Write-up for Submerged

Overview

This challenge is based on an observation: by using single-use addresses, you can craft transactions such that a smart contract can verify the transaction hash of the current transaction. As a result, we can write a contract which simulates a hypothetical TXHASH opcode, which other contracts can build upon.

There are a few interesting use cases for this primitive, but this challenge was focused primarily on having challengers construct transactions that could be verified by the TxHashSimulator.

Solving the puzzle

By modifying the code from the challenge files, we can write a Foundry script which constructs the raw transaction, derives the sender, and tells us how much ETH to fund the sender with before broadcasting the raw transaction:

SoliditySolidity's logo.Solve.s.sol
1
function deriveRawTxAndSender(
2
bytes32 seed,
3
uint256 gasPrice,
4
uint256 gasLimit,
5
address to,
6
uint256 value,
7
bytes memory data
8
) public view returns(bytes memory rawTx, address sender) {
9
uint8 v = 27;
10
bytes32 r;
11
bytes32 s;
12
assembly {
13
mstore(0, seed)
14
r := keccak256(0, 32)
15
mstore(0, r)
16
s := keccak256(0, 32)
17
}
18
19
bytes[] memory txList = new bytes[](9);
20
txList[0] = RLP.encodeUint(0); // nonce
21
txList[1] = RLP.encodeUint(gasPrice); // gas price
22
txList[2] = RLP.encodeUint(gasLimit); // claimed gasLimit
23
txList[3] = RLP.encodeUint(uint256(uint160(to))); // to address
24
txList[4] = RLP.encodeUint(value); // tx value
25
txList[5] = RLP.encodeBytes(data); // tx data
26
txList[6] = RLP.encodeUint(uint256(v)); // v
27
txList[7] = RLP.encodeUint(uint256(r)); // r
28
txList[8] = RLP.encodeUint(uint256(s)); // s
29
rawTx = RLP.encodeList(txList);
30
31
// truncate the tx fields to exclude the signature data
32
assembly {
33
mstore(txList, 6)
34
}
35
bytes32 signingHash = keccak256(RLP.encodeList(txList));
36
sender = ecrecover(signingHash, v, r, s);
37
require(sender != address(0), "ecrecover failed");
38
}
39
40
function solve(address solver, Submerged submerged) public view {
41
TxHashSimulator simulator = submerged.simulator();
42
bytes32 seed = keccak256(abi.encode(
43
keccak256(abi.encode(solver)),
44
uint256(0)
45
));
46
uint256 gasPrice = 20 gwei; // TODO: configure based on current gas price
47
uint256 gasLimit = 120000;
48
uint256 value = 0;
49
(bytes memory rawTx, address sender) = deriveRawTxAndSender(
50
seed,
51
gasPrice,
52
gasLimit,
53
address(simulator),
54
value,
55
abi.encodePacked(
56
abi.encode(seed, gasLimit, address(submerged)),
57
abi.encodeWithSelector(Submerged.proveSubmergedTx.selector)
58
)
59
);
60
console2.log("Seed: ", vm.toString(seed));
61
console2.log("Sender: ", sender);
62
console2.log("Transfer amount: ", gasLimit * gasPrice + value);
63
console2.log("Broadcast raw tx:", vm.toString(rawTx));
64
}

After this, it's as simple as calling the proveSubmergedSeed method and passing in the raw transaction bytes.

Waterfall