1// SPDX-License-Identifier: MIT2
3pragma solidity ^0.8.13;4
5import {IPuzzle} from "curta/src/interfaces/IPuzzle.sol";6import {IDepositEvents, IDepositContract} from "./interfaces/IDepositContract.sol";7import {IReliquary} from "relic-sdk/packages/contracts/interfaces/IReliquary.sol";8import {IReliquary} from "relic-sdk/packages/contracts/interfaces/IReliquary.sol";9import {IProver} from "relic-sdk/packages/contracts/interfaces/IProver.sol";10import {Fact, FactSignature} from "relic-sdk/packages/contracts/lib/Facts.sol";11import {FactSigs} from "relic-sdk/packages/contracts/lib/FactSigs.sol";12import {CoreTypes} from "relic-sdk/packages/contracts/lib/CoreTypes.sol";13
14import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol";15import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";16
17contract 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
58contract 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 value80 ) 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 public103 view104 returns (bytes memory creds)105 {106 return107 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 prover115 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 proof122 ) 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.logIdx128 );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_amount166 ) 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 proof183 ) 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_root198 );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
215contract 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 proof231 ) 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 frenly237 _mint(msg.sender, HUGE);238 }239
240 // refund sender241 (bool success, ) = msg.sender.call{value: msg.value}("");242 assert(success);243 }244 }245}246
247interface 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 index255 );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-specs270interface 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_root282 ) 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
Time Left
Solve locally (WIP)
- Clone GitHub repo + install deps
git clone https://github.com/waterfall-mkt/curta-puzzles.git && cd curta-puzzles && forge install
- Set
RPC_URL_MAINNET
in.env
.env
RPC_URL_MAINNET=""
- Write solution + run script
forge script <PATH_TO_PUZZLE> -f mainnet -vvv
This is still WIP.