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_MAINNETin.env
.env
RPC_URL_MAINNET=""- Write solution + run script
forge script <PATH_TO_PUZZLE> -f mainnet -vvvThis is still WIP.