Skip to content

Complete Orderbook.sol executeWithdrawal verification #38

Description

@freesig

Complete Orderbook.sol executeWithdrawal Verification

Description

Implement full merkle proof verification in the executeWithdrawal function. The user submits both a ZK proof (proving the state root) and a merkle proof (proving the burn exists in that state) in a single transaction. State roots are never stored on-chain.

Tasks

  • Implement hashLeaf() and hashPair() helper functions
  • Implement computeBurnedKeyIndex() function
  • Implement verifyBurnProof() function for merkle verification
  • Update executeWithdrawal() to accept both ZK proof and merkle proof
  • Add tests for withdrawal verification

Technical Details

File to Modify

  • contracts/Orderbook.sol

Constants to Add

uint256 constant TREE_HEIGHT = 255;

executeWithdrawal Flow

  1. User submits withdrawal data + merkle siblings + ZK proof (imageId, journal, seal)
  2. Contract verifies ZK proof using IRiscZeroVerifier
  3. Contract extracts state root from journal (first 32 bytes)
  4. Contract verifies merkle proof against extracted state root
  5. If both pass, transfer tokens

Note: State roots are NOT stored on-chain. Both proofs are verified in the same transaction.

verifyBurnProof Function

Verifies a merkle proof for a burned withdrawal:

  • Computes leaf data matching Rust encode_burned_value format
  • Computes burned key index using owner and nonce
  • Walks up the tree using siblings
  • Returns true if computed root matches expected root

executeWithdrawal Updates

  • Requires state root to be in verifiedStateRoots mapping
  • Verifies merkle proof against the verified state root
  • Existing nonce and chainId checks remain

Data Format Matching

Burned key index computation must match Rust:

bytes memory key = abi.encodePacked(
    uint8(0x05),  // BURNED prefix
    uint8(0x00),  // Ethereum address type
    owner,        // 20 bytes
    bytes12(0),   // Padding to 32 bytes
    nonce         // 8 bytes
);
return keccak256(key);

Leaf data must match Rust encode_burned_value:

bytes memory leafData = abi.encodePacked(
    amount,       // 32 bytes
    uint8(0x00),  // Ethereum address type
    token,        // 20 bytes
    bytes12(0),   // Padding
    chainId       // 32 bytes
);

Updated executeWithdrawal Signature

function executeWithdrawal(
    Withdrawal calldata withdrawal,
    bytes32 imageId,
    bytes calldata journal,
    bytes calldata seal
) external {
    require(withdrawal.burn.user == msg.sender, "Invalid user");
    require(withdrawal.burn.chainId == block.chainid, "Invalid chain ID");
    require(!usedNonces[withdrawal.burn.user][withdrawal.burn.nonce], "Nonce already used");

    // 1. Verify ZK proof
    require(address(verifier) != address(0), "Verifier not set");
    bytes32 journalDigest = sha256(journal);
    bytes memory sealWithSelector = abi.encodePacked(verifierSelector, seal);
    verifier.verify(sealWithSelector, imageId, journalDigest);

    // 2. Extract state root from journal (first 32 bytes)
    require(journal.length >= 32, "Invalid journal");
    bytes32 stateRoot;
    assembly {
        stateRoot := calldataload(journal.offset)
    }

    // 3. Verify merkle proof against ZK-proven state root
    require(verifyBurnProof(
        withdrawal.burn.user,
        withdrawal.burn.nonce,
        withdrawal.burn.token,
        withdrawal.burn.amount,
        withdrawal.burn.chainId,
        withdrawal.siblings,
        stateRoot
    ), "Invalid merkle proof");

    // 4. Mark nonce as used and transfer
    usedNonces[withdrawal.burn.user][withdrawal.burn.nonce] = true;
    IERC20(withdrawal.burn.token).safeTransfer(msg.sender, withdrawal.burn.amount);
}

Security Considerations

  • ZK proof verified in same transaction as merkle proof (no stored state roots)
  • Nonce tracking prevents double-withdrawals
  • chainId check prevents cross-chain replay attacks
  • Only the burn owner can execute their withdrawal
  • imageId should be validated against expected guest program

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions