Puzzle #22
Stake Frens
Author
0xb49bf876be26435b6fae1ef42c3c82c5867fa149
chainlight.io
SoliditySolidity's logo.Puzzle
Curtacallsverify()
1
// SPDX-License-Identifier: MIT
2
3
pragma solidity ^0.8.13;
4
5
import {IPuzzle} from "curta/src/interfaces/IPuzzle.sol";
6
import {IDepositEvents, IDepositContract} from "./interfaces/IDepositContract.sol";
7
import {IReliquary} from "relic-sdk/packages/contracts/interfaces/IReliquary.sol";
8
import {IReliquary} from "relic-sdk/packages/contracts/interfaces/IReliquary.sol";
9
import {IProver} from "relic-sdk/packages/contracts/interfaces/IProver.sol";
10
import {Fact, FactSignature} from "relic-sdk/packages/contracts/lib/Facts.sol";
11
import {FactSigs} from "relic-sdk/packages/contracts/lib/FactSigs.sol";
12
import {CoreTypes} from "relic-sdk/packages/contracts/lib/CoreTypes.sol";
13
14
import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol";
15
import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
16
17
contract StakeFrens is IPuzzle {
18
IReliquary public constant RELIQUARY =
19
IReliquary(0x5E4DE6Bb8c6824f29c44Bd3473d44da120387d08);
20
FrenCoin public immutable frenCoin;
21
22
mapping (address => FrenPool) public pools;
23
24
constructor() {
25
frenCoin = new FrenCoin(this);
26
}
27
28
function name() external pure returns (string memory) {
29
return "Stake Frens";
30
}
31
32
function generate(address seed) external view returns (uint256) {
33
return uint256(uint160(seed));
34
}
35
36
function verify(uint256 seed, uint256 solution) external view returns (bool) {
37
address challenger = address(uint160(seed));
38
require(
39
solution == uint256(uint128(uint256(keccak256(abi.encode(seed))))),
40
"invalid solution"
41
);
42
43
return frenCoin.balanceOf(challenger) == solution;
44
}
45
46
function createPool() external payable returns (FrenPool pool) {
47
pool = new FrenPool(msg.sender, RELIQUARY);
48
pools[msg.sender] = pool;
49
}
50
51
function joinPool(address creator, address prover, FrenPool.DepositProof calldata proof) external payable {
52
FrenPool pool = pools[creator];
53
require(address(pool) != address(0), "pool doesn't exist");
54
pool.join{value: msg.value}(msg.sender, prover, proof);
55
}
56
}
57
58
contract FrenPool is Ownable, IDepositEvents {
59
struct DepositProof {
60
uint256 blockNum;
61
uint256 txIdx;
62
uint256 logIdx;
63
bytes32 expectedRoot;
64
bytes proof;
65
}
66
67
bytes1 constant ETH1_ADDRESS_WITHDRAWAL_PREFIX = hex"01";
68
IDepositContract constant ETH_DEPOSIT_CONTRACT =
69
IDepositContract(0x00000000219ab540356cBB839Cbe05303d7705Fa);
70
uint256 constant FAIR_DEPOSIT_AMOUNT = 16 ether;
71
72
IReliquary public immutable reliquary;
73
bytes32 immutable DEPOSIT_AMOUNT_KECCAK;
74
75
address fren;
76
mapping(address => uint256) collected;
77
78
function to_little_endian_64(
79
uint64 value
80
) internal pure returns (bytes memory ret) {
81
ret = new bytes(8);
82
bytes8 bytesValue = bytes8(value);
83
// Byteswapping during copying to bytes.
84
ret[0] = bytesValue[7];
85
ret[1] = bytesValue[6];
86
ret[2] = bytesValue[5];
87
ret[3] = bytesValue[4];
88
ret[4] = bytesValue[3];
89
ret[5] = bytesValue[2];
90
ret[6] = bytesValue[1];
91
ret[7] = bytesValue[0];
92
}
93
94
constructor(address owner, IReliquary _reliquary) Ownable(owner) {
95
reliquary = _reliquary;
96
DEPOSIT_AMOUNT_KECCAK = keccak256(
97
to_little_endian_64(uint64(FAIR_DEPOSIT_AMOUNT / 1 gwei))
98
);
99
}
100
101
function eth1WithdrawalCredentials()
102
public
103
view
104
returns (bytes memory creds)
105
{
106
return
107
abi.encodePacked(
108
ETH1_ADDRESS_WITHDRAWAL_PREFIX,
109
uint248(uint160(address(this)))
110
);
111
}
112
113
function verifyProver(address prover) internal view {
114
// check that it's a valid Relic prover
115
IReliquary.ProverInfo memory info = reliquary.provers(prover);
116
require(info.version > 0 && !info.revoked, "Invalid prover provided");
117
}
118
119
function verifyDeposit(
120
address prover,
121
DepositProof calldata proof
122
) internal returns (bytes memory pubkey) {
123
Fact memory fact = IProver(prover).prove(proof.proof, false);
124
FactSignature expected = FactSigs.logFactSig(
125
proof.blockNum,
126
proof.txIdx,
127
proof.logIdx
128
);
129
require(
130
FactSignature.unwrap(fact.sig) == FactSignature.unwrap(expected),
131
"fact signature is incorrect"
132
);
133
CoreTypes.LogData memory logData = abi.decode(
134
fact.data,
135
(CoreTypes.LogData)
136
);
137
require(
138
logData.Topics[0] == DepositEvent.selector,
139
"incorrect log event proven"
140
);
141
DepositEventData memory eventData = abi.decode(
142
logData.Data,
143
(DepositEventData)
144
);
145
require(
146
keccak256(eventData.amount) == DEPOSIT_AMOUNT_KECCAK,
147
"incorrect deposit amount"
148
);
149
require(
150
keccak256(eventData.withdrawal_credentials) ==
151
keccak256(eth1WithdrawalCredentials()),
152
"incorrect withdrawal credentials"
153
);
154
require(
155
IDepositContract(fact.account).get_deposit_root() ==
156
proof.expectedRoot,
157
"unexpected deposit root, potential frontrun"
158
);
159
return eventData.pubkey;
160
}
161
162
function computeUnsignedDepositRoot(
163
bytes memory pubkey,
164
bytes memory withdrawal_credentials,
165
uint256 deposit_amount
166
) internal pure returns (bytes32 result) {
167
assert(deposit_amount % 1 gwei == 0);
168
bytes memory amount = to_little_endian_64(uint64(deposit_amount / 1 gwei));
169
bytes32 pubkey_root = sha256(abi.encodePacked(pubkey, bytes16(0)));
170
bytes32 zeroNode = sha256(abi.encodePacked(bytes32(0), bytes32(0)));
171
bytes32 signature_root = sha256(abi.encodePacked(zeroNode, zeroNode));
172
result = sha256(abi.encodePacked(
173
sha256(abi.encodePacked(pubkey_root, withdrawal_credentials)),
174
sha256(abi.encodePacked(amount, bytes24(0), signature_root))
175
));
176
}
177
178
// will you be my fren?
179
function join(
180
address newFren,
181
address prover,
182
DepositProof calldata proof
183
) external payable {
184
require(fren == address(0), "already have a fren");
185
require(msg.value >= FAIR_DEPOSIT_AMOUNT, "frens should pay their fair share");
186
fren = newFren;
187
188
verifyProver(prover);
189
bytes memory pubkey = verifyDeposit(prover, proof);
190
bytes memory credentials = eth1WithdrawalCredentials();
191
bytes32 deposit_data_root = computeUnsignedDepositRoot(pubkey, credentials, msg.value);
192
193
ETH_DEPOSIT_CONTRACT.deposit{value: msg.value}(
194
pubkey,
195
eth1WithdrawalCredentials(),
196
new bytes(96),
197
deposit_data_root
198
);
199
}
200
201
function collect() external {
202
require(
203
msg.sender == owner() || msg.sender == fren,
204
"only the owner or their fren can call"
205
);
206
address other = msg.sender == owner() ? fren : owner();
207
uint256 diff = collected[msg.sender] - collected[other];
208
uint256 amount = (address(this).balance - diff) / 2;
209
collected[msg.sender] += amount;
210
(bool success, ) = msg.sender.call{value: address(this).balance}("");
211
require(success, "transfer failed");
212
}
213
}
214
215
contract FrenCoin is ERC20 {
216
StakeFrens public immutable stakeFrens;
217
218
bytes32 constant TOO_FRENLY =
219
keccak256("DepositContract: deposit value too high");
220
221
uint256 constant HUGE = 1<<128;
222
223
constructor(StakeFrens _stakeFrens) ERC20("FrenCoin", "FREN") {
224
stakeFrens = _stakeFrens;
225
}
226
227
function showFrenship(
228
address creator,
229
address prover,
230
FrenPool.DepositProof calldata proof
231
) external payable {
232
try stakeFrens.joinPool{value: msg.value}(creator, prover, proof) {
233
_mint(msg.sender, 1);
234
} catch Error(string memory reason) {
235
if (keccak256(abi.encodePacked(reason)) == TOO_FRENLY) {
236
// too frenly
237
_mint(msg.sender, HUGE);
238
}
239
240
// refund sender
241
(bool success, ) = msg.sender.call{value: msg.value}("");
242
assert(success);
243
}
244
}
245
}
246
247
interface IDepositEvents {
248
/// @notice A processed deposit event.
249
event DepositEvent(
250
bytes pubkey,
251
bytes withdrawal_credentials,
252
bytes amount,
253
bytes signature,
254
bytes index
255
);
256
257
/// @notice A struct with identical data to the deposit event.
258
struct DepositEventData {
259
bytes pubkey;
260
bytes withdrawal_credentials;
261
bytes amount;
262
bytes signature;
263
bytes index;
264
}
265
}
266
267
// This interface is designed to be compatible with the Vyper version.
268
/// @notice This is the Ethereum 2.0 deposit contract interface.
269
/// For more information see the Phase 0 specification under https://github.com/ethereum/eth2.0-specs
270
interface IDepositContract is IDepositEvents {
271
/// @notice Submit a Phase 0 DepositData object.
272
/// @param pubkey A BLS12-381 public key.
273
/// @param withdrawal_credentials Commitment to a public key for withdrawals.
274
/// @param signature A BLS12-381 signature.
275
/// @param deposit_data_root The SHA-256 hash of the SSZ-encoded DepositData object.
276
/// Used as a protection against malformed input.
277
function deposit(
278
bytes calldata pubkey,
279
bytes calldata withdrawal_credentials,
280
bytes calldata signature,
281
bytes32 deposit_data_root
282
) external payable;
283
284
/// @notice Query the current deposit root hash.
285
/// @return The deposit root hash.
286
function get_deposit_root() external view returns (bytes32);
287
288
/// @notice Query the current deposit count.
289
/// @return The deposit count encoded as a little endian 64-bit number.
290
function get_deposit_count() external view returns (bytes memory);
291
}
292
First Blood
billh.eth
08:48:48
8
Time Left

Solve locally (WIP)

  1. Clone GitHub repo + install deps
git clone https://github.com/waterfall-mkt/curta-puzzles.git && cd curta-puzzles && forge install
  1. Set RPC_URL_MAINNET in .env
.env
RPC_URL_MAINNET=""
  1. Write solution + run script
forge script <PATH_TO_PUZZLE> -f mainnet -vvv
This is still WIP.
Waterfall