From b09ef8ca8f1d83f5803f685665c841756d7d5968 Mon Sep 17 00:00:00 2001 From: JoE11-y Date: Thu, 21 Aug 2025 18:05:28 +0100 Subject: [PATCH] chore: add updates --- contracts/L1/src/MerkleStateManager.sol | 583 --------------------- contracts/L1/src/ZeroXBridgeL1.sol | 18 +- contracts/L1/test/MerkleStateManager.t.sol | 548 ------------------- contracts/L1/test/ZeroXBridgeL1.t.sol | 305 +++++------ contracts/L1/test/ZeroXBridgeProof.t.sol | 87 --- 5 files changed, 166 insertions(+), 1375 deletions(-) delete mode 100644 contracts/L1/src/MerkleStateManager.sol delete mode 100644 contracts/L1/test/MerkleStateManager.t.sol delete mode 100644 contracts/L1/test/ZeroXBridgeProof.t.sol diff --git a/contracts/L1/src/MerkleStateManager.sol b/contracts/L1/src/MerkleStateManager.sol deleted file mode 100644 index 99c27f3..0000000 --- a/contracts/L1/src/MerkleStateManager.sol +++ /dev/null @@ -1,583 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "@openzeppelin/contracts/access/Ownable.sol"; -import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; -import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; -import "@openzeppelin/contracts/utils/Pausable.sol"; - -/** - * @title MerkleStateManager - * @dev Manages deposit commitments and synced withdrawal roots for ZeroXBridge protocol - * @notice Compatible with Alexandria Merkle Trees using deterministic leaf ordering and proper tree construction - */ -contract MerkleStateManager is Ownable, Pausable, ReentrancyGuard { - // Debug events - event DebugProofVerification(uint256 leafIndex, bytes32 computedHash, bytes32 expectedRoot, bool isValid); - - using MerkleProof for bytes32[]; - - bytes32 public depositRoot; - bytes32 public withdrawalRoot; - uint256 public depositRootIndex; - uint256 public withdrawalRootIndex; - - // Storage for intermediate tree nodes to optimize proof generation - // Indexed by [level][position] for efficient retrieval - mapping(uint256 => mapping(uint256 => bytes32)) public treeNodes; - uint256 public depositTreeDepth; - uint256 public depositLeafCount; - - mapping(uint256 => bytes32) public depositRootHistory; - mapping(uint256 => bytes32) public withdrawalRootHistory; - mapping(uint256 => uint256) public depositRootTimestamps; - mapping(uint256 => uint256) public withdrawalRootTimestamps; - mapping(address => bool) public approvedRelayers; - mapping(bytes32 => bool) public processedCommitments; - mapping(bytes32 => uint256) public commitmentToLeafIndex; - bytes32[] public leaves; - mapping(uint256 => bytes32) public merkleTree; - mapping(address => uint256) public lastOperationTime; - mapping(address => uint256) public operationCount; - uint256 private constant RATE_LIMIT_WINDOW = 15 seconds; - uint256 private constant MAX_OPERATIONS_PER_WINDOW = 10; - uint256 public constant MAX_TREE_DEPTH = 32; - uint256 public constant MAX_LEAF_COUNT = 2 ** 32 - 1; - - event DepositRootUpdated( - uint256 indexed index, - bytes32 newRoot, - bytes32 indexed commitment, - uint256 leafIndex, - uint256 timestamp, - uint256 blockNumber - ); - - event WithdrawalRootSynced( - uint256 indexed index, bytes32 newRoot, address indexed relayer, uint256 timestamp, uint256 blockNumber - ); - - event RelayerStatusChanged(address indexed relayer, bool status); - event CommitmentProcessed(bytes32 indexed commitment, uint256 leafIndex); - event EmergencyPause(address indexed admin, string reason); - event EmergencyUnpause(address indexed admin); - - /** - * @dev Constructor initializes the contract with genesis roots - * @param initialOwner The initial owner of the contract - * @param genesisDepositRoot The initial deposit root - * @param genesisWithdrawalRoot The initial withdrawal root - */ - constructor(address initialOwner, bytes32 genesisDepositRoot, bytes32 genesisWithdrawalRoot) - Ownable(initialOwner) - { - require(initialOwner != address(0), "MerkleStateManager: Invalid owner"); - require(genesisDepositRoot != bytes32(0), "MerkleStateManager: Invalid genesis deposit root"); - require(genesisWithdrawalRoot != bytes32(0), "MerkleStateManager: Invalid genesis withdrawal root"); - - depositRoot = genesisDepositRoot; - withdrawalRoot = genesisWithdrawalRoot; - depositRootHistory[0] = genesisDepositRoot; - withdrawalRootHistory[0] = genesisWithdrawalRoot; - depositRootTimestamps[0] = block.timestamp; - withdrawalRootTimestamps[0] = block.timestamp; - } - - // Using OpenZeppelin's built-in whenNotPaused modifier instead of custom implementation - - /** - * @dev Modifier to check rate limiting - */ - modifier rateLimited() { - _checkRateLimit(); - _; - _updateRateLimit(); - } - - /** - * @dev Modifier to ensure only approved relayers can call certain functions - */ - modifier onlyRelayer() { - require(approvedRelayers[msg.sender], "MerkleStateManager: Only approved relayers"); - _; - } - - /** - * @dev Updates deposit root from a new commitment using Alexandria tree structure - * @param commitment The commitment hash to add to the tree - */ - function updateDepositRootFromCommitment(bytes32 commitment) external whenNotPaused nonReentrant rateLimited { - require(commitment != bytes32(0), "MerkleStateManager: Invalid commitment"); - require(!processedCommitments[commitment], "MerkleStateManager: Commitment already processed"); - require(depositLeafCount < MAX_LEAF_COUNT, "MerkleStateManager: Tree capacity exceeded"); - - processedCommitments[commitment] = true; - uint256 leafIndex = depositLeafCount; - commitmentToLeafIndex[commitment] = leafIndex; - leaves.push(commitment); - bytes32 newRoot = _calculateNewDepositRoot(commitment); - depositRoot = newRoot; - depositRootIndex++; - depositLeafCount++; - uint256 newDepth = _calculateTreeDepth(depositLeafCount); - if (newDepth > depositTreeDepth) { - depositTreeDepth = newDepth; - } - depositRootHistory[depositRootIndex] = newRoot; - depositRootTimestamps[depositRootIndex] = block.timestamp; - - emit CommitmentProcessed(commitment, leafIndex); - emit DepositRootUpdated(depositRootIndex, newRoot, commitment, leafIndex, block.timestamp, block.number); - } - - /** - * @dev Batch update deposit roots for efficiency - * @param commitments Array of commitments to process - */ - function batchUpdateDepositRoots(bytes32[] calldata commitments) external whenNotPaused nonReentrant rateLimited { - require(commitments.length > 0 && commitments.length <= 100, "MerkleStateManager: Invalid batch size"); - require(depositLeafCount + commitments.length <= MAX_LEAF_COUNT, "MerkleStateManager: Batch exceeds capacity"); - uint256 startLeafIndex = depositLeafCount; - for (uint256 i = 0; i < commitments.length; i++) { - bytes32 commitment = commitments[i]; - require(commitment != bytes32(0), "MerkleStateManager: Invalid commitment in batch"); - require(!processedCommitments[commitment], "MerkleStateManager: Duplicate commitment in batch"); - processedCommitments[commitment] = true; - commitmentToLeafIndex[commitment] = startLeafIndex + i; - leaves.push(commitment); - emit CommitmentProcessed(commitment, startLeafIndex + i); - } - - // Update leaf count - depositLeafCount += commitments.length; - - // Calculate new root for the entire batch - bytes32 newRoot = _recalculateDepositRoot(); - - depositRoot = newRoot; - depositRootIndex++; - uint256 newDepth = _calculateTreeDepth(depositLeafCount); - if (newDepth > depositTreeDepth) { - depositTreeDepth = newDepth; - } - depositRootHistory[depositRootIndex] = newRoot; - depositRootTimestamps[depositRootIndex] = block.timestamp; - emit DepositRootUpdated( - depositRootIndex, - newRoot, - commitments[0], // First commitment as reference - startLeafIndex, - block.timestamp, - block.number - ); - } - - /** - * @dev Syncs withdrawal root from L2 - * @param newRoot The new withdrawal root from L2 - */ - function syncWithdrawalRootFromL2(bytes32 newRoot) external whenNotPaused nonReentrant onlyRelayer rateLimited { - require(newRoot != bytes32(0), "MerkleStateManager: Invalid root"); - require(newRoot != withdrawalRoot, "MerkleStateManager: Root unchanged"); - withdrawalRoot = newRoot; - withdrawalRootIndex++; - withdrawalRootHistory[withdrawalRootIndex] = newRoot; - withdrawalRootTimestamps[withdrawalRootIndex] = block.timestamp; - - emit WithdrawalRootSynced(withdrawalRootIndex, newRoot, msg.sender, block.timestamp, block.number); - } - - /** - * @dev Manages relayer approval status - * @param relayer The relayer address - * @param status The approval status - */ - function setRelayerStatus(address relayer, bool status) external onlyOwner { - require(relayer != address(0), "MerkleStateManager: Invalid relayer address"); - approvedRelayers[relayer] = status; - emit RelayerStatusChanged(relayer, status); - } - - /** - * @dev Emergency pause function - * @param reason The reason for pausing - */ - function emergencyPause(string calldata reason) external onlyOwner { - _pause(); - emit EmergencyPause(msg.sender, reason); - } - - /** - * @dev Unpause the contract - */ - function unpause() external onlyOwner { - _unpause(); - emit EmergencyUnpause(msg.sender); - } - - /** - * @dev Verifies a withdrawal proof against the current withdrawal root - * @param leaf The leaf to verify - * @param proof The Merkle proof - * @return True if the proof is valid - */ - function verifyWithdrawalProof(bytes32 leaf, bytes32[] calldata proof) external view returns (bool) { - return MerkleProof.verify(proof, withdrawalRoot, leaf); - } - - /** - * @dev Verifies a deposit proof - * @param leaf The leaf to verify - * @param leafIndex The index of the leaf - * @param proof The Merkle proof - * @return Whether the proof is valid - */ - function verifyDepositProof(bytes32 leaf, uint256 leafIndex, bytes32[] calldata proof) - external - view - returns (bool) - { - if (leafIndex >= depositLeafCount) { - return false; - } - - bytes32 computedHash = leaf; - uint256 currentIndex = leafIndex; - uint256 currentLevelNodes = depositLeafCount; - - for (uint256 i = 0; i < proof.length; i++) { - // Check if this is the last node at this level and it's odd - bool isLastNodeInOddLevel = (currentIndex == currentLevelNodes - 1) && (currentLevelNodes % 2 == 1); - - if (isLastNodeInOddLevel) { - // For the last node in an odd level, it's duplicated - // The node is just passed up to the next level without hashing - // No need to use the proof element for this level - } else { - // Normal case - use the provided proof element - bytes32 proofElement = proof[i]; - if (currentIndex % 2 == 0) { - // Current node is a left child - computedHash = keccak256(abi.encodePacked(computedHash, proofElement)); - } else { - // Current node is a right child - computedHash = keccak256(abi.encodePacked(proofElement, computedHash)); - } - } - - // Update for next level - currentIndex = currentIndex / 2; - currentLevelNodes = (currentLevelNodes + 1) / 2; // Calculate nodes at next level - } - - return computedHash == depositRoot; - } - - /** - * @dev Gets the Merkle proof for a deposit at a specific index - * @param leafIndex The index of the leaf - * @return The Merkle proof - */ - function getDepositProof(uint256 leafIndex) external view returns (bytes32[] memory) { - require(leafIndex < depositLeafCount, "MerkleStateManager: Invalid leaf index"); - - return _generateMerkleProof(leafIndex); - } - - /** - * @dev Gets deposit tree information - * @return depth The current tree depth - * @return leafCount The current leaf count - * @return root The current deposit root - */ - function getDepositTreeInfo() external view returns (uint256 depth, uint256 leafCount, bytes32 root) { - return (depositTreeDepth, depositLeafCount, depositRoot); - } - - /** - * @dev Gets withdrawal tree information - * @return index The current withdrawal root index - * @return root The current withdrawal root - */ - function getWithdrawalTreeInfo() external view returns (uint256 index, bytes32 root) { - return (withdrawalRootIndex, withdrawalRoot); - } - - /** - * @dev Calculates new deposit root using Alexandria tree construction - * @param newCommitment The new commitment to add - * @return The new Merkle root - */ - function _calculateNewDepositRoot(bytes32 newCommitment) internal returns (bytes32) { - uint256 newLeafCount = depositLeafCount + 1; - bytes32[] memory updatedLeaves = new bytes32[](newLeafCount); - - // Copy existing leaves - for (uint256 i = 0; i < depositLeafCount; i++) { - updatedLeaves[i] = leaves[i]; - } - - // Add new leaf - updatedLeaves[depositLeafCount] = newCommitment; - - // Calculate new root and store intermediate nodes - return _calculateMerkleRoot(updatedLeaves); - } - - /** - * @dev Recalculates the entire deposit root from current leaves - * @return The calculated Merkle root - */ - function _recalculateDepositRoot() internal returns (bytes32) { - return _calculateMerkleRoot(leaves); - } - - /** - * @dev Optimized Merkle root calculation using Alexandria-style approach - * @param leafArray The array of leaves - * @return The calculated Merkle root - */ - function _calculateMerkleRoot(bytes32[] memory leafArray) internal returns (bytes32) { - uint256 n = leafArray.length; - if (n == 0) return bytes32(0); - if (n == 1) return leafArray[0]; - - // Create a working array that we'll use to build the tree - uint256 maxSize = 2 * n; // Allocate enough space for all levels - bytes32[] memory nodes = new bytes32[](maxSize); - - // Copy leaves to the nodes array and store in treeNodes - for (uint256 i = 0; i < n; i++) { - nodes[i] = leafArray[i]; - treeNodes[0][i] = leafArray[i]; - } - - // Start building the tree level by level - uint256 levelSize = n; - uint256 level = 0; - - while (levelSize > 1) { - uint256 nextLevelSize = (levelSize + 1) / 2; // Ceiling division for odd numbers - - // Process pairs of nodes - for (uint256 i = 0; i < levelSize / 2; i++) { - bytes32 left = nodes[i * 2]; - bytes32 right = nodes[i * 2 + 1]; - nodes[n + i] = keccak256(abi.encodePacked(left, right)); - treeNodes[level + 1][i] = nodes[n + i]; // Store intermediate nodes - } - - // Handle odd node at the end if present - if (levelSize % 2 == 1) { - // Duplicate the last node - nodes[n + levelSize / 2] = nodes[levelSize - 1]; - treeNodes[level + 1][levelSize / 2] = nodes[levelSize - 1]; - } - - // Copy the next level back to the beginning of our working array - for (uint256 i = 0; i < nextLevelSize; i++) { - nodes[i] = nodes[n + i]; - } - - levelSize = nextLevelSize; - level++; - } - - return nodes[0]; // Root is at index 0 after the final iteration - } - - /** - * @dev Calculate next power of 2 for deterministic tree structure - */ - function _nextPowerOfTwo(uint256 n) internal pure returns (uint256) { - if (n <= 1) return 1; - uint256 power = 1; - while (power < n) { - power <<= 1; - } - return power; - } - - /** - * @dev Optimized proof generation using stored intermediate nodes - * @param leafIndex The index of the leaf - * @return The Merkle proof - */ - function _generateMerkleProof(uint256 leafIndex) internal view returns (bytes32[] memory) { - if (depositLeafCount <= 1) { - return new bytes32[](0); - } - - uint256 treeDepth = _getTreeDepth(depositLeafCount); - bytes32[] memory proof = new bytes32[](treeDepth); - uint256 currentIndex = leafIndex; - uint256 currentLevelNodes = depositLeafCount; - - for (uint256 level = 0; level < treeDepth; level++) { - // Check if this is the last node at an odd-numbered level - bool isLastNodeInOddLevel = (currentIndex == currentLevelNodes - 1) && (currentLevelNodes % 2 == 1); - - if (isLastNodeInOddLevel) { - // For the last node in an odd level, we should use the node itself as the proof element - // This is because when verifying, we'll pair the node with itself - bytes32 nodeValue; - if (level == 0) { - // At level 0, use the leaf directly - nodeValue = leaves[currentIndex]; - } else if (treeNodes[level][currentIndex] != bytes32(0)) { - nodeValue = treeNodes[level][currentIndex]; - } else { - nodeValue = _calculateNodeHash(level, currentIndex); - } - proof[level] = nodeValue; - } else { - // Normal case: get the sibling node - uint256 siblingIndex = currentIndex ^ 1; // XOR with 1 to get sibling index - bytes32 siblingNode; - if (level == 0) { - // At level 0, use the leaf directly - siblingNode = leaves[siblingIndex]; - } else if (treeNodes[level][siblingIndex] != bytes32(0)) { - siblingNode = treeNodes[level][siblingIndex]; - } else { - siblingNode = _calculateNodeHash(level, siblingIndex); - } - proof[level] = siblingNode; - } - - // Move to parent index for next level - currentIndex = currentIndex / 2; - currentLevelNodes = (currentLevelNodes + 1) / 2; // Calculate nodes at next level - } - - return proof; - } - - /** - * @dev Calculate log2 of a number - */ - function _log2(uint256 n) internal pure returns (uint256) { - uint256 result = 0; - while (n > 1) { - n >>= 1; - result++; - } - return result; - } - - /** - * @dev Calculate the tree depth based on the number of leaves - */ - function _getTreeDepth(uint256 leafCount) internal pure returns (uint256) { - if (leafCount <= 1) return 0; - return _log2(_nextPowerOfTwo(leafCount)); - } - - /** - * @dev Calculate a node hash recursively if not stored - * @param level The tree level of the node - * @param index The index of the node at that level - * @return The hash of the node - */ - function _calculateNodeHash(uint256 level, uint256 index) internal view returns (bytes32) { - // If we're at leaf level (level 0), return the leaf or zero if out of bounds - if (level == 0) { - if (index < depositLeafCount) { - return leaves[index]; - } else { - return bytes32(0); - } - } - - // Calculate the number of nodes at the child level - uint256 childLevel = level - 1; - uint256 nodesAtChildLevel; - - if (childLevel == 0) { - // At leaf level, it's the number of leaves - nodesAtChildLevel = depositLeafCount; - } else { - // For higher levels, calculate based on the number of nodes at the level below - uint256 leavesNeeded = depositLeafCount; - for (uint256 i = 0; i < childLevel; i++) { - leavesNeeded = (leavesNeeded + 1) / 2; - } - nodesAtChildLevel = leavesNeeded; - } - - // Calculate child indices - uint256 leftChildIndex = index * 2; - uint256 rightChildIndex = leftChildIndex + 1; - - // Get left child (from storage or calculate) - bytes32 leftChild; - if (leftChildIndex < nodesAtChildLevel) { - if (treeNodes[childLevel][leftChildIndex] != bytes32(0)) { - leftChild = treeNodes[childLevel][leftChildIndex]; - } else { - leftChild = _calculateNodeHash(childLevel, leftChildIndex); - } - } else { - return bytes32(0); // Out of bounds - } - - // Handle odd number of nodes at child level - if (rightChildIndex >= nodesAtChildLevel) { - // If this is the last node at this level and there are odd nodes below, - // duplicate the left child - return leftChild; - } - - // Get right child (from storage or calculate) - bytes32 rightChild; - if (treeNodes[childLevel][rightChildIndex] != bytes32(0)) { - rightChild = treeNodes[childLevel][rightChildIndex]; - } else { - rightChild = _calculateNodeHash(childLevel, rightChildIndex); - } - - // Hash the children together to get the parent - return keccak256(abi.encodePacked(leftChild, rightChild)); - } - - /** - * @dev Calculates the depth needed for a tree with given leaf count - * @param leafCount The number of leaves - * @return The required tree depth - */ - function _calculateTreeDepth(uint256 leafCount) internal pure returns (uint256) { - if (leafCount <= 1) return leafCount; - uint256 depth = 0; - uint256 temp = leafCount - 1; - while (temp > 0) { - temp >>= 1; - depth++; - } - return depth; - } - - /** - * @dev Checks rate limiting for the caller - */ - function _checkRateLimit() internal view { - uint256 currentTime = block.timestamp; - uint256 lastTime = lastOperationTime[msg.sender]; - if (currentTime < lastTime + RATE_LIMIT_WINDOW) { - require(operationCount[msg.sender] < MAX_OPERATIONS_PER_WINDOW, "MerkleStateManager: Rate limit exceeded"); - } - } - - /** - * @dev Updates rate limiting state for the caller - */ - function _updateRateLimit() internal { - uint256 currentTime = block.timestamp; - uint256 lastTime = lastOperationTime[msg.sender]; - if (currentTime >= lastTime + RATE_LIMIT_WINDOW) { - operationCount[msg.sender] = 1; - lastOperationTime[msg.sender] = currentTime; - } else { - operationCount[msg.sender]++; - } - } -} diff --git a/contracts/L1/src/ZeroXBridgeL1.sol b/contracts/L1/src/ZeroXBridgeL1.sol index 6d90da1..37896b9 100644 --- a/contracts/L1/src/ZeroXBridgeL1.sol +++ b/contracts/L1/src/ZeroXBridgeL1.sol @@ -79,6 +79,7 @@ contract ZeroXBridgeL1 is Ownable, Starknet, MerkleManager { address indexed user, uint256 nonce, uint256 commitmentHash, + uint256 leavesCount, bytes32 newRoot, uint256 elementCount ); @@ -240,11 +241,20 @@ contract ZeroXBridgeL1 is Ownable, Starknet, MerkleManager { keccak256(abi.encodePacked(depositId, userRecord[user], usdVal, nonce, block.timestamp)); // Append to Merkle tree - (bytes32 newRoot, uint256 count, uint256 elementCount) = appendDepositHash(commitmentHash); + (bytes32 newRoot, uint256 leavesCount, uint256 elementCount) = appendDepositHash(commitmentHash); // Emit deposit event emit DepositEvent( - depositId, tokenAddress, assetType, usdVal, user, nonce, uint256(commitmentHash), newRoot, elementCount + depositId, + tokenAddress, + assetType, + usdVal, + user, + nonce, + uint256(commitmentHash), + leavesCount, + newRoot, + elementCount ); return uint256(commitmentHash); @@ -274,7 +284,6 @@ contract ZeroXBridgeL1 is Ownable, Starknet, MerkleManager { uint256 timestamp = proofdata[3]; // Verify that commitmentHash matches expected format based on L2 standards - uint256 expectedCommitmentHash = uint256(keccak256(abi.encodePacked(starknetPubKey, usd_amount, nonce, timestamp))); @@ -288,9 +297,6 @@ contract ZeroXBridgeL1 is Ownable, Starknet, MerkleManager { // assert root hasn't been used require(!verifiedProofs[verifiedRoot], "ZeroXBridge: Proof has already been used"); - // pass verified root into merkle manager - // todo: implement merkle manager - // get user address from starknet pubkey address user = starkPubKeyRecord[starknetPubKey]; diff --git a/contracts/L1/test/MerkleStateManager.t.sol b/contracts/L1/test/MerkleStateManager.t.sol deleted file mode 100644 index 65664d6..0000000 --- a/contracts/L1/test/MerkleStateManager.t.sol +++ /dev/null @@ -1,548 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {Test} from "forge-std/Test.sol"; -import {console} from "forge-std/console.sol"; -import {MerkleStateManager} from "../src/MerkleStateManager.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; - -contract MerkleStateManagerTest is Test { - MerkleStateManager public merkleManager; - - address public owner = makeAddr("owner"); - address public relayer1 = makeAddr("relayer1"); - address public relayer2 = makeAddr("relayer2"); - address public user = makeAddr("user"); - address public unauthorizedUser = makeAddr("unauthorized"); - - bytes32 public constant GENESIS_DEPOSIT_ROOT = 0x27ae5ba08d7291c96c8cbddcc148bf48a6d68c7974b94356f53754ef6171d757; - bytes32 public constant GENESIS_WITHDRAWAL_ROOT = 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421; - bytes32 public constant USER_DEPOSIT_HASH = 0x2e99758548972a8e8822ad47fa1017ff72f06f3ff6a016851f45c398732bc50c; - bytes32 public constant L2_ROOT_UPDATE = 0x3f5a0b3b4e2c8d6e7a9b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f; - - event DepositRootUpdated( - uint256 indexed index, - bytes32 newRoot, - bytes32 indexed commitment, - uint256 leafIndex, - uint256 timestamp, - uint256 blockNumber - ); - - event WithdrawalRootSynced( - uint256 indexed index, bytes32 newRoot, address indexed relayer, uint256 timestamp, uint256 blockNumber - ); - - event RelayerStatusChanged(address indexed relayer, bool status); - event CommitmentProcessed(bytes32 indexed commitment, uint256 leafIndex); - event EmergencyPause(address indexed admin, string reason); - event EmergencyUnpause(address indexed admin); - - error OwnableUnauthorizedAccount(address account); - - function setUp() public { - vm.startPrank(owner); - merkleManager = new MerkleStateManager(owner, GENESIS_DEPOSIT_ROOT, GENESIS_WITHDRAWAL_ROOT); - merkleManager.setRelayerStatus(relayer1, true); - merkleManager.setRelayerStatus(relayer2, true); - vm.stopPrank(); - } - - function test_InitialState() public view { - assertEq(merkleManager.depositRoot(), GENESIS_DEPOSIT_ROOT); - assertEq(merkleManager.withdrawalRoot(), GENESIS_WITHDRAWAL_ROOT); - assertEq(merkleManager.depositRootIndex(), 0); - assertEq(merkleManager.withdrawalRootIndex(), 0); - assertEq(merkleManager.depositTreeDepth(), 0); - assertEq(merkleManager.depositLeafCount(), 0); - assertEq(merkleManager.owner(), owner); - } - - function test_RelayerSetup() public view { - assertTrue(merkleManager.approvedRelayers(relayer1)); - assertTrue(merkleManager.approvedRelayers(relayer2)); - assertFalse(merkleManager.approvedRelayers(unauthorizedUser)); - } - - function test_UpdateDepositRootFromCommitment() public { - // First, calculate the expected root value correctly - bytes32 expectedRoot = USER_DEPOSIT_HASH; // For the first leaf, the root is the leaf itself - - vm.expectEmit(true, true, false, true); - emit CommitmentProcessed(USER_DEPOSIT_HASH, 0); - - vm.expectEmit(true, true, true, true); - emit DepositRootUpdated(1, expectedRoot, USER_DEPOSIT_HASH, 0, block.timestamp, block.number); - - merkleManager.updateDepositRootFromCommitment(USER_DEPOSIT_HASH); - - assertEq(merkleManager.depositRootIndex(), 1); - assertEq(merkleManager.depositLeafCount(), 1); - assertTrue(merkleManager.processedCommitments(USER_DEPOSIT_HASH)); - } - - function test_RevertOnInvalidCommitment() public { - vm.expectRevert("MerkleStateManager: Invalid commitment"); - merkleManager.updateDepositRootFromCommitment(bytes32(0)); - } - - function test_RevertOnDuplicateCommitment() public { - merkleManager.updateDepositRootFromCommitment(USER_DEPOSIT_HASH); - - vm.expectRevert("MerkleStateManager: Commitment already processed"); - merkleManager.updateDepositRootFromCommitment(USER_DEPOSIT_HASH); - } - - function test_BatchDepositUpdates() public { - bytes32[] memory commitments = new bytes32[](3); - commitments[0] = keccak256("user_deposit_1"); - commitments[1] = keccak256("user_deposit_2"); - commitments[2] = keccak256("user_deposit_3"); - - merkleManager.batchUpdateDepositRoots(commitments); - - assertEq(merkleManager.depositRootIndex(), 1); - assertEq(merkleManager.depositLeafCount(), 3); - assertTrue(merkleManager.processedCommitments(commitments[0])); - assertTrue(merkleManager.processedCommitments(commitments[1])); - assertTrue(merkleManager.processedCommitments(commitments[2])); - } - - function test_BatchDepositUpdatesRevertOnInvalidSize() public { - bytes32[] memory emptyCommitments = new bytes32[](0); - vm.expectRevert("MerkleStateManager: Invalid batch size"); - merkleManager.batchUpdateDepositRoots(emptyCommitments); - - bytes32[] memory largeCommitments = new bytes32[](101); - vm.expectRevert("MerkleStateManager: Invalid batch size"); - merkleManager.batchUpdateDepositRoots(largeCommitments); - } - - function test_SyncWithdrawalRootFromL2() public { - vm.startPrank(relayer1); - - vm.expectEmit(true, false, true, true); - emit WithdrawalRootSynced(1, L2_ROOT_UPDATE, relayer1, block.timestamp, block.number); - - merkleManager.syncWithdrawalRootFromL2(L2_ROOT_UPDATE); - - assertEq(merkleManager.withdrawalRoot(), L2_ROOT_UPDATE); - assertEq(merkleManager.withdrawalRootIndex(), 1); - - vm.stopPrank(); - } - - function test_UnauthorizedCannotSyncWithdrawal() public { - vm.expectRevert("MerkleStateManager: Only approved relayers"); - merkleManager.syncWithdrawalRootFromL2(L2_ROOT_UPDATE); - } - - function test_CannotSyncWithZeroRoot() public { - vm.startPrank(relayer1); - vm.expectRevert("MerkleStateManager: Invalid root"); - merkleManager.syncWithdrawalRootFromL2(bytes32(0)); - vm.stopPrank(); - } - - function test_CannotSyncWithSameRoot() public { - vm.startPrank(relayer1); - vm.expectRevert("MerkleStateManager: Root unchanged"); - merkleManager.syncWithdrawalRootFromL2(GENESIS_WITHDRAWAL_ROOT); - vm.stopPrank(); - } - - function test_MultipleL2Syncs() public { - bytes32 l2Update1 = keccak256("l2_state_1"); - bytes32 l2Update2 = keccak256("l2_state_2"); - - vm.startPrank(relayer1); - - merkleManager.syncWithdrawalRootFromL2(l2Update1); - assertEq(merkleManager.withdrawalRoot(), l2Update1); - assertEq(merkleManager.withdrawalRootIndex(), 1); - - merkleManager.syncWithdrawalRootFromL2(l2Update2); - assertEq(merkleManager.withdrawalRoot(), l2Update2); - assertEq(merkleManager.withdrawalRootIndex(), 2); - - vm.stopPrank(); - } - - function test_SetRelayerStatus() public { - address newRelayer = makeAddr("newRelayer"); - - vm.startPrank(owner); - - vm.expectEmit(true, false, false, true); - emit RelayerStatusChanged(newRelayer, true); - - merkleManager.setRelayerStatus(newRelayer, true); - assertTrue(merkleManager.approvedRelayers(newRelayer)); - - vm.expectEmit(true, false, false, true); - emit RelayerStatusChanged(newRelayer, false); - - merkleManager.setRelayerStatus(newRelayer, false); - assertFalse(merkleManager.approvedRelayers(newRelayer)); - - vm.stopPrank(); - } - - function test_OnlyOwnerCanManageRelayers() public { - vm.expectRevert(abi.encodeWithSelector(OwnableUnauthorizedAccount.selector, unauthorizedUser)); - vm.startPrank(unauthorizedUser); - merkleManager.setRelayerStatus(relayer1, false); - vm.stopPrank(); - } - - function test_CannotSetZeroAddressAsRelayer() public { - vm.startPrank(owner); - vm.expectRevert("MerkleStateManager: Invalid relayer address"); - merkleManager.setRelayerStatus(address(0), true); - vm.stopPrank(); - } - - function test_VerifyWithdrawalProof() public view { - bytes32[] memory proof = new bytes32[](2); - proof[0] = keccak256("sibling_1"); - proof[1] = keccak256("sibling_2"); - - bytes32 leaf = keccak256("withdrawal_leaf"); - - bool result = merkleManager.verifyWithdrawalProof(leaf, proof); - // This will be false since we're testing against the withdrawal root without proper proof - assertFalse(result); - } - - function test_VerifyDepositProof() public { - // First add a commitment - merkleManager.updateDepositRootFromCommitment(USER_DEPOSIT_HASH); - - // Generate proof for the first leaf (index 0) - bytes32[] memory proof = merkleManager.getDepositProof(0); - - // Verify the proof - bool result = merkleManager.verifyDepositProof(USER_DEPOSIT_HASH, 0, proof); - assertTrue(result, "Single deposit proof verification failed"); - } - - function test_VerifyDepositProofWithTwoDeposits() public { - // Add first commitment - bytes32 firstDeposit = USER_DEPOSIT_HASH; - merkleManager.updateDepositRootFromCommitment(firstDeposit); - - // Add second commitment - bytes32 secondDeposit = keccak256("SECOND_DEPOSIT"); - merkleManager.updateDepositRootFromCommitment(secondDeposit); - - // Generate and verify proof for first leaf (index 0) - bytes32[] memory proofForFirst = merkleManager.getDepositProof(0); - bool resultFirst = merkleManager.verifyDepositProof(firstDeposit, 0, proofForFirst); - assertTrue(resultFirst, "First deposit proof verification failed with two deposits"); - - // Generate and verify proof for second leaf (index 1) - bytes32[] memory proofForSecond = merkleManager.getDepositProof(1); - bool resultSecond = merkleManager.verifyDepositProof(secondDeposit, 1, proofForSecond); - assertTrue(resultSecond, "Second deposit proof verification failed with two deposits"); - } - - function test_VerifyDepositProofWithMultipleDeposits() public { - // Add four deposits to create a deeper tree - bytes32[] memory deposits = new bytes32[](4); - deposits[0] = USER_DEPOSIT_HASH; - deposits[1] = keccak256("SECOND_DEPOSIT"); - deposits[2] = keccak256("THIRD_DEPOSIT"); - deposits[3] = keccak256("FOURTH_DEPOSIT"); - - for (uint256 i = 0; i < deposits.length; i++) { - merkleManager.updateDepositRootFromCommitment(deposits[i]); - } - - // Verify all deposits - for (uint256 i = 0; i < deposits.length; i++) { - bytes32[] memory proof = merkleManager.getDepositProof(i); - bool result = merkleManager.verifyDepositProof(deposits[i], i, proof); - assertTrue(result, string(abi.encodePacked("Deposit ", i + 1, " proof verification failed"))); - } - } - - function test_VerifyDepositProofWithOddNumberOfDeposits() public { - // Add three deposits to test odd number of nodes (requiring special handling) - bytes32[] memory deposits = new bytes32[](3); - deposits[0] = USER_DEPOSIT_HASH; - deposits[1] = keccak256("SECOND_DEPOSIT"); - deposits[2] = keccak256("THIRD_DEPOSIT"); - - for (uint256 i = 0; i < deposits.length; i++) { - merkleManager.updateDepositRootFromCommitment(deposits[i]); - } - - // Debug: Print the tree structure - debugPrintMerkleTree(deposits); - - // Get the deposit root - bytes32 root = merkleManager.depositRoot(); - console.log("Deposit root from contract:"); - console.logBytes32(root); - - // Verify deposits 0 and 1 (these should work) - for (uint256 i = 0; i < 2; i++) { - bytes32[] memory proof = merkleManager.getDepositProof(i); - console.log("\nLeaf index:", i); - console.log("Proof length:", proof.length); - for (uint256 j = 0; j < proof.length; j++) { - console.log("Proof element", j, ":"); - console.logBytes32(proof[j]); - } - bool result = merkleManager.verifyDepositProof(deposits[i], i, proof); - assertTrue( - result, - string(abi.encodePacked("Deposit ", i + 1, " proof verification failed with odd number of deposits")) - ); - } - - // Special handling for the last leaf (index 2) - uint256 lastIndex = 2; - bytes32[] memory lastProof = merkleManager.getDepositProof(lastIndex); - console.log("\nLast leaf index:", lastIndex); - console.log("Last leaf value:"); - console.logBytes32(deposits[lastIndex]); - console.log("Last proof length:", lastProof.length); - for (uint256 j = 0; j < lastProof.length; j++) { - console.log("Last proof element", j, ":"); - console.logBytes32(lastProof[j]); - } - - // Manual verification for the last leaf - bytes32 computedHash = deposits[lastIndex]; - console.log("\nManual verification for last leaf:"); - console.log("Starting with leaf:"); - console.logBytes32(computedHash); - - // Level 0: The last leaf is paired with itself - computedHash = keccak256(abi.encodePacked(computedHash, computedHash)); - console.log("After self-pairing at level 0:"); - console.logBytes32(computedHash); - - // Level 1: Hash with the first proof element (which should be the hash of leaves 0 & 1) - computedHash = keccak256(abi.encodePacked(lastProof[0], computedHash)); - console.log("After hashing with proof[0] at level 1:"); - console.logBytes32(computedHash); - - console.log("\nFinal computed hash:"); - console.logBytes32(computedHash); - console.log("Should match root:"); - console.logBytes32(root); - - // Now try the contract's verification - bool lastResult = merkleManager.verifyDepositProof(deposits[lastIndex], lastIndex, lastProof); - assertTrue(lastResult, "Deposit 3 proof verification failed with odd number of deposits"); - } - - function debugPrintMerkleTree(bytes32[] memory leaves) internal { - console.log("\n--- DEBUG: MERKLE TREE STRUCTURE ---"); - console.log("Total leaves:", leaves.length); - - // Level 0 (leaves) - console.log("Level 0 (leaves):"); - for (uint256 i = 0; i < leaves.length; i++) { - console.log(" Node", i, ":"); - console.logBytes32(leaves[i]); - } - - // Calculate higher levels - uint256 n = leaves.length; - bytes32[] memory nodes = new bytes32[](n); - for (uint256 i = 0; i < n; i++) { - nodes[i] = leaves[i]; - } - - uint256 level = 1; - while (n > 1) { - console.log("Level", level, ":"); - uint256 nextLevelSize = (n + 1) / 2; - - for (uint256 i = 0; i < n / 2; i++) { - bytes32 left = nodes[i * 2]; - bytes32 right = nodes[i * 2 + 1]; - bytes32 parent = keccak256(abi.encodePacked(left, right)); - console.log(" Node", i); - console.log(" from", i * 2, "&", i * 2 + 1); - console.logBytes32(parent); - nodes[i] = parent; - } - - // Handle odd node - if (n % 2 == 1) { - console.log(" Node", n / 2); - console.log(" duplicate of", n - 1); - console.logBytes32(nodes[n - 1]); - nodes[n / 2] = nodes[n - 1]; - n = n / 2 + 1; - } else { - n = n / 2; - } - - level++; - } - - console.log("Root:"); - console.logBytes32(nodes[0]); - console.log("--- END DEBUG ---\n"); - } - - function test_GetDepositProofRevertOnInvalidIndex() public { - vm.expectRevert("MerkleStateManager: Invalid leaf index"); - merkleManager.getDepositProof(0); // No leaves added yet - } - - function test_EmergencyPause() public { - vm.startPrank(owner); - - vm.expectEmit(true, false, false, true); - emit EmergencyPause(owner, "Security concern"); - - merkleManager.emergencyPause("Security concern"); - assertTrue(merkleManager.paused()); - - vm.stopPrank(); - } - - function test_Unpause() public { - vm.startPrank(owner); - - merkleManager.emergencyPause("Test pause"); - assertTrue(merkleManager.paused()); - - vm.expectEmit(true, false, false, true); - emit EmergencyUnpause(owner); - - merkleManager.unpause(); - assertFalse(merkleManager.paused()); - - vm.stopPrank(); - } - - function test_CannotUpdateWhenPaused() public { - vm.startPrank(owner); - merkleManager.emergencyPause("Emergency"); - vm.stopPrank(); - - // OpenZeppelin's Pausable uses a custom error with a specific selector - vm.expectRevert(bytes4(0xd93c0665)); - merkleManager.updateDepositRootFromCommitment(USER_DEPOSIT_HASH); - } - - function test_CannotSyncWhenPaused() public { - vm.startPrank(owner); - merkleManager.emergencyPause("Emergency"); - vm.stopPrank(); - - vm.startPrank(relayer1); - // OpenZeppelin's Pausable uses a custom error with a specific selector - vm.expectRevert(bytes4(0xd93c0665)); - merkleManager.syncWithdrawalRootFromL2(L2_ROOT_UPDATE); - vm.stopPrank(); - } - - function test_RateLimiting() public { - // First update should work - merkleManager.updateDepositRootFromCommitment(USER_DEPOSIT_HASH); - - // Make 9 more updates to reach the limit of 10 operations - for (uint256 i = 1; i < 10; i++) { - merkleManager.updateDepositRootFromCommitment(keccak256(abi.encodePacked("deposit_", i))); - } - - // Next update (11th) should fail due to exceeding rate limit - vm.expectRevert("MerkleStateManager: Rate limit exceeded"); - merkleManager.updateDepositRootFromCommitment(keccak256("exceeding_rate_limit")); - - // Fast forward time past rate limit window - vm.warp(block.timestamp + 16); // 15 seconds + 1 second - - // Now it should work - merkleManager.updateDepositRootFromCommitment(keccak256("another_deposit")); - } - - function test_GetTreeInfo() public { - merkleManager.updateDepositRootFromCommitment(USER_DEPOSIT_HASH); - - (uint256 depth, uint256 leafCount, bytes32 root) = merkleManager.getDepositTreeInfo(); - assertEq(depth, 1); // Tree depth should be 1 after first leaf - assertEq(leafCount, 1); - assertEq(root, merkleManager.depositRoot()); - - (uint256 index, bytes32 withdrawalRoot) = merkleManager.getWithdrawalTreeInfo(); - assertEq(index, 0); - assertEq(withdrawalRoot, GENESIS_WITHDRAWAL_ROOT); - } - - function testFuzz_ValidCommitmentUpdates(bytes32 commitment) public { - vm.assume(commitment != bytes32(0)); - vm.assume(!merkleManager.processedCommitments(commitment)); - - uint256 initialIndex = merkleManager.depositRootIndex(); - uint256 initialLeafCount = merkleManager.depositLeafCount(); - - merkleManager.updateDepositRootFromCommitment(commitment); - - assertEq(merkleManager.depositRootIndex(), initialIndex + 1); - assertEq(merkleManager.depositLeafCount(), initialLeafCount + 1); - assertTrue(merkleManager.processedCommitments(commitment)); - } - - function testFuzz_ValidL2RootSync(bytes32 newRoot) public { - vm.assume(newRoot != bytes32(0)); - vm.assume(newRoot != merkleManager.withdrawalRoot()); - - vm.startPrank(relayer1); - uint256 initialIndex = merkleManager.withdrawalRootIndex(); - merkleManager.syncWithdrawalRootFromL2(newRoot); - - assertEq(merkleManager.withdrawalRootIndex(), initialIndex + 1); - assertEq(merkleManager.withdrawalRoot(), newRoot); - vm.stopPrank(); - } - - function test_TreeCapacityLimit() public { - // We can't practically test the full capacity, but we can test the check exists - bytes32[] memory commitments = new bytes32[](1); - commitments[0] = keccak256("test"); - - // This should work normally - merkleManager.batchUpdateDepositRoots(commitments); - - // The capacity check is in place, verified by the require statement - assertEq(merkleManager.depositLeafCount(), 1); - } - - function test_EndToEndBridgeFlow() public { - // Add a deposit commitment - merkleManager.updateDepositRootFromCommitment(USER_DEPOSIT_HASH); - - // Sync withdrawal root from L2 - vm.startPrank(relayer1); - merkleManager.syncWithdrawalRootFromL2(L2_ROOT_UPDATE); - vm.stopPrank(); - - // Verify state - assertEq(merkleManager.depositRootIndex(), 1); - assertEq(merkleManager.withdrawalRootIndex(), 1); - assertEq(merkleManager.depositLeafCount(), 1); - assertTrue(merkleManager.processedCommitments(USER_DEPOSIT_HASH)); - - // Verify deposit proof - bytes32[] memory depositProof = merkleManager.getDepositProof(0); - bool depositResult = merkleManager.verifyDepositProof(USER_DEPOSIT_HASH, 0, depositProof); - assertTrue(depositResult); - - // Test withdrawal proof (will be false without proper setup but verifies the function works) - bytes32[] memory withdrawalProof = new bytes32[](1); - withdrawalProof[0] = keccak256("proof_data"); - bytes32 leaf = keccak256("withdrawal_data"); - - bool withdrawalResult = merkleManager.verifyWithdrawalProof(leaf, withdrawalProof); - assertFalse(withdrawalResult); // Expected to be false without proper proof - } -} diff --git a/contracts/L1/test/ZeroXBridgeL1.t.sol b/contracts/L1/test/ZeroXBridgeL1.t.sol index bb0b7a2..15f8085 100644 --- a/contracts/L1/test/ZeroXBridgeL1.t.sol +++ b/contracts/L1/test/ZeroXBridgeL1.t.sol @@ -48,6 +48,7 @@ contract ZeroXBridgeL1Test is Test { address indexed user, uint256 nonce, uint256 commitmentHash, + uint256 leavesCount, bytes32 newRoot, uint256 elementCount ); @@ -493,6 +494,8 @@ contract ZeroXBridgeL1Test is Test { (uint256 nextElementsCount, bytes32 nextRoot,) = StatelessMmr.appendWithPeaksRetrieval(_commitmentHash, currentPeaks, currentElementsCount, currentRoot); + // leavesCount += 1; + // 3. Return the predicted values return (nextRoot, nextElementsCount); } @@ -529,6 +532,7 @@ contract ZeroXBridgeL1Test is Test { user, nonce, uint256(expectedCommitmentHash), + 1, expectedNewRoot, expectedElementCount ); @@ -574,6 +578,7 @@ contract ZeroXBridgeL1Test is Test { user, nonce, uint256(expectedCommitmentHash), + 1, expectedNewRoot, expectedElementCount ); @@ -923,159 +928,157 @@ contract ZeroXBridgeL1Test is Test { (uint8 v, bytes32 r, bytes32 s) = vm.sign(ethAccountPrivateKey, digest); bytes memory starknetSig = abi.encodePacked(r, s); - // bridge.unlockFundsWithProof(ZeroXBridgeL1.AssetType.ERC20, address(dai), proofdata, commitmentHash, starknetSig); + bridge.unlockFundsWithProof(ZeroXBridgeL1.AssetType.ERC20, address(dai), proofdata, commitmentHash, starknetSig); + + // Step 9: Assertions + assertEq(bridge.tokenReserves(address(dai)), 0, "tokenReserves should be reduced after unlock"); + assertEq(dai.balanceOf(user), depositAmount, "User should receive full unlocked amount"); + } + + function testClaimReducesTokenReserveUSDC() public { + uint256 depositAmount = 100 * 1e6; + uint256 nonce = 0; + uint256 timestamp = block.timestamp; + + uint256 ethPrice = 2000 * 1e8; + uint256 daiPrice = 1e8; + uint256 usdcPrice = 1e8; + uint256 usdcDec = 6; + uint256 depositId = 14567; + + // Step 1: User registration and mint + vm.prank(user); + registerUser(user, starknetPubKey, ethAccountPrivateKey); + usdc.mint(user, depositAmount); + vm.prank(user); + usdc.approve(address(bridge), type(uint256).max); + + // Step 2: Mock price feed + vm.mockCall( + daiPriceFeed, + abi.encodeWithSelector(AggregatorV3Interface.latestRoundData.selector), + abi.encode(uint80(1), int256(daiPrice), uint256(0), uint256(0), uint80(0)) + ); + vm.mockCall( + ethPriceFeed, + abi.encodeWithSelector(AggregatorV3Interface.latestRoundData.selector), + abi.encode(uint80(1), int256(ethPrice), uint256(0), uint256(0), uint80(0)) + ); + vm.mockCall( + usdcPriceFeed, + abi.encodeWithSelector(AggregatorV3Interface.latestRoundData.selector), + abi.encode(uint80(1), int256(usdcPrice), uint256(0), uint256(0), uint80(0)) + ); + + uint256 usdRaw = (depositAmount * usdcPrice) / 1e8; + uint256 usdValue = (usdRaw * 1e18) / (10 ** usdcDec); + + // Step 3: Warp and deposit + vm.warp(timestamp); + vm.prank(user); + bridge.depositAsset(depositId, ZeroXBridgeL1.AssetType.ERC20, address(usdc), depositAmount, user); + + // Step 4: Compute usdValue and commitmentHash + uint256 commitmentHash = uint256(keccak256(abi.encodePacked(starknetPubKey, usdValue, nonce, timestamp))); + + // Step 5: Register proof + uint256 merkleRoot = uint256(keccak256("mock merkle")); + MockProofRegistry(address(proofRegistry)).registerWithdrawalProof(commitmentHash, merkleRoot); + + // Step 6: Build proof data + uint256[] memory proofdata = new uint256[](4); + proofdata[0] = starknetPubKey; + proofdata[1] = usdValue; + proofdata[2] = nonce; + proofdata[3] = timestamp; + + // Step 7: Generate valid signature + uint256 STARK_CURVE_ORDER = 361850278866613110698659328152149712041468702080126762623304950275186147821; + uint256 msgHash = commitmentHash % STARK_CURVE_ORDER; + bytes32 digest = bytes32(msgHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ethAccountPrivateKey, digest); + bytes memory starknetSig = abi.encodePacked(r, s); + + bridge.unlockFundsWithProof( + ZeroXBridgeL1.AssetType.ERC20, address(usdc), proofdata, commitmentHash, starknetSig + ); // // Step 9: Assertions - // assertEq(bridge.tokenReserves(address(dai)), 0, "tokenReserves should be reduced after unlock"); - // assertEq(dai.balanceOf(user), depositAmount, "User should receive full unlocked amount"); + assertEq(bridge.tokenReserves(address(usdc)), 0, "tokenReserves should be reduced after unlock"); + assertEq(usdc.balanceOf(user), depositAmount, "User should receive full unlocked amount"); } - // function testClaimReducesTokenReserveUSDC() public { - // uint256 depositAmount = 100 * 1e6; - // uint256 nonce = 0; - // uint256 timestamp = block.timestamp; - - // uint256 ethPrice = 2000 * 1e8; - // uint256 daiPrice = 1e8; - // uint256 usdcPrice = 1e8; - // uint256 usdcDec = 6; - - // // Step 1: User registration and mint - // vm.prank(user); - // registerUser(user, starknetPubKey, ethAccountPrivateKey); - // usdc.mint(user, depositAmount); - // vm.prank(user); - // usdc.approve(address(bridge), type(uint256).max); - - // // Step 2: Mock price feed - // vm.mockCall( - // daiPriceFeed, - // abi.encodeWithSelector(AggregatorV3Interface.latestRoundData.selector), - // abi.encode(uint80(1), int256(daiPrice), uint256(0), uint256(0), uint80(0)) - // ); - // vm.mockCall( - // ethPriceFeed, - // abi.encodeWithSelector(AggregatorV3Interface.latestRoundData.selector), - // abi.encode(uint80(1), int256(ethPrice), uint256(0), uint256(0), uint80(0)) - // ); - // vm.mockCall( - // usdcPriceFeed, - // abi.encodeWithSelector(AggregatorV3Interface.latestRoundData.selector), - // abi.encode(uint80(1), int256(usdcPrice), uint256(0), uint256(0), uint80(0)) - // ); - - // uint256 usdRaw = (depositAmount * usdcPrice) / 1e8; - // uint256 usdValue = (usdRaw * 1e18) / (10 ** usdcDec); - - // // Step 3: Warp and deposit - // vm.warp(timestamp); - // vm.prank(user); - // bridge.depositAsset(ZeroXBridgeL1.AssetType.ERC20, address(usdc), depositAmount, user); - - // // Step 4: Compute usdValue and commitmentHash - // uint256 commitmentHash = uint256( - // keccak256(abi.encodePacked(starknetPubKey, usdValue, nonce, timestamp)) - // ); - - // // Step 5: Register proof - // uint256 merkleRoot = uint256(keccak256("mock merkle")); - // MockProofRegistry(address(proofRegistry)).registerWithdrawalProof(commitmentHash, merkleRoot); - - // // Step 6: Build proof data - // uint256[] memory proofdata = new uint256[](4); - // proofdata[0] = starknetPubKey; - // proofdata[1] = usdValue; - // proofdata[2] = nonce; - // proofdata[3] = timestamp; - - // // Step 7: Generate valid signature - // uint256 STARK_CURVE_ORDER = 361850278866613110698659328152149712041468702080126762623304950275186147821; - // uint256 msgHash = commitmentHash % STARK_CURVE_ORDER; - // bytes32 digest = bytes32(msgHash); - // (uint8 v, bytes32 r, bytes32 s) = vm.sign(ethAccountPrivateKey, digest); - // bytes memory starknetSig = abi.encodePacked(r, s); - - // bridge.unlockFundsWithProof( - // ZeroXBridgeL1.AssetType.ERC20, - // address(usdc), - // proofdata, - // commitmentHash, - // starknetSig - // ); - - // // // Step 9: Assertions - // assertEq(bridge.tokenReserves(address(usdc)), 0, "tokenReserves should be reduced after unlock"); - // assertEq(usdc.balanceOf(user), depositAmount, "User should receive full unlocked amount"); - // } - - // function testClaimReducesTokenReserveETH() public { - // uint256 depositAmount = 1 ether; - // uint256 nonce = 0; - // uint256 timestamp = block.timestamp; - - // uint256 ethPrice = 2000 * 1e8; - // uint256 daiPrice = 1e8; - // uint256 usdcPrice = 1e8; - // uint8 ethDec = 18; - - // // Step 1: User registration - // vm.prank(user); - // registerUser(user, starknetPubKey, ethAccountPrivateKey); - // vm.deal(user, depositAmount); - - // // Step 2: Mock price feeds - // vm.mockCall( - // ethPriceFeed, - // abi.encodeWithSelector(AggregatorV3Interface.latestRoundData.selector), - // abi.encode(0, int256(ethPrice), 0, 0, 0) - // ); - // vm.mockCall( - // daiPriceFeed, - // abi.encodeWithSelector(AggregatorV3Interface.latestRoundData.selector), - // abi.encode(0, int256(daiPrice), 0, 0, 0) - // ); - // vm.mockCall( - // usdcPriceFeed, - // abi.encodeWithSelector(AggregatorV3Interface.latestRoundData.selector), - // abi.encode(0, int256(usdcPrice), 0, 0, 0) - // ); - - // // Step 3: Warp and deposit ETH - // vm.warp(timestamp); - // vm.prank(user); - // bridge.depositAsset{value: depositAmount}(ZeroXBridgeL1.AssetType.ETH, address(0), depositAmount, user); - - // // Step 4: Compute usd value and commitment hash - // uint256 usdRaw = (depositAmount * ethPrice) / 1e8; - // uint256 usdValue = (usdRaw * 1e18) / (10 ** ethDec); - // uint256 commitmentHash = uint256(keccak256(abi.encodePacked(starknetPubKey, usdValue, nonce, timestamp))); - - // // Step 5: Register proof - // uint256 merkleRoot = uint256(keccak256("mock_merkle")); - // proofRegistry.registerWithdrawalProof(commitmentHash, merkleRoot); - - // // Step 6: Build proof data - // uint256[] memory proofdata = new uint256[](4); - // proofdata[0] = starknetPubKey; - // proofdata[1] = usdValue; - // proofdata[2] = nonce; - // proofdata[3] = timestamp; - - // bytes memory dummySig = new bytes(64); // valid 64-byte placeholder signature - - // // mock verifyStarknetSignature to always return true - // bytes4 selector = bridge.verifyStarknetSignature.selector; - // vm.mockCall( - // address(bridge), - // abi.encodeWithSelector(selector, commitmentHash, dummySig, starknetPubKey), - // abi.encode(true) - // ); - - // // Step 8: Execute unlock - // vm.prank(relayer); - // bridge.unlockFundsWithProof(ZeroXBridgeL1.AssetType.ETH, address(0), proofdata, commitmentHash, dummySig); - // // Step 9: Assertions - // assertEq(bridge.tokenReserves(address(0)), 0, "tokenReserves should be reduced after unlock"); - // assertEq(user.balance, depositAmount, "User should receive full unlocked ETH amount"); - // } + function testClaimReducesTokenReserveETH() public { + uint256 depositAmount = 1 ether; + uint256 nonce = 0; + uint256 timestamp = block.timestamp; + + uint256 ethPrice = 2000 * 1e8; + uint256 daiPrice = 1e8; + uint256 usdcPrice = 1e8; + uint8 ethDec = 18; + uint256 depositId = 123246534; + + // Step 1: User registration + vm.prank(user); + registerUser(user, starknetPubKey, ethAccountPrivateKey); + vm.deal(user, depositAmount); + + // Step 2: Mock price feeds + vm.mockCall( + ethPriceFeed, + abi.encodeWithSelector(AggregatorV3Interface.latestRoundData.selector), + abi.encode(0, int256(ethPrice), 0, 0, 0) + ); + vm.mockCall( + daiPriceFeed, + abi.encodeWithSelector(AggregatorV3Interface.latestRoundData.selector), + abi.encode(0, int256(daiPrice), 0, 0, 0) + ); + vm.mockCall( + usdcPriceFeed, + abi.encodeWithSelector(AggregatorV3Interface.latestRoundData.selector), + abi.encode(0, int256(usdcPrice), 0, 0, 0) + ); + + // Step 3: Warp and deposit ETH + vm.warp(timestamp); + vm.prank(user); + bridge.depositAsset{value: depositAmount}( + depositId, ZeroXBridgeL1.AssetType.ETH, address(0), depositAmount, user + ); + + // Step 4: Compute usd value and commitment hash + uint256 usdRaw = (depositAmount * ethPrice) / 1e8; + uint256 usdValue = (usdRaw * 1e18) / (10 ** ethDec); + uint256 commitmentHash = uint256(keccak256(abi.encodePacked(starknetPubKey, usdValue, nonce, timestamp))); + + // Step 5: Register proof + uint256 merkleRoot = uint256(keccak256("mock_merkle")); + proofRegistry.registerWithdrawalProof(commitmentHash, merkleRoot); + + // Step 6: Build proof data + uint256[] memory proofdata = new uint256[](4); + proofdata[0] = starknetPubKey; + proofdata[1] = usdValue; + proofdata[2] = nonce; + proofdata[3] = timestamp; + + bytes memory dummySig = new bytes(64); // valid 64-byte placeholder signature + + // mock verifyStarknetSignature to always return true + bytes4 selector = bridge.verifyStarknetSignature.selector; + vm.mockCall( + address(bridge), + abi.encodeWithSelector(selector, commitmentHash, dummySig, starknetPubKey), + abi.encode(true) + ); + + // Step 8: Execute unlock + vm.prank(relayer); + bridge.unlockFundsWithProof(ZeroXBridgeL1.AssetType.ETH, address(0), proofdata, commitmentHash, dummySig); + // Step 9: Assertions + assertEq(bridge.tokenReserves(address(0)), 0, "tokenReserves should be reduced after unlock"); + assertEq(user.balance, depositAmount, "User should receive full unlocked ETH amount"); + } } diff --git a/contracts/L1/test/ZeroXBridgeProof.t.sol b/contracts/L1/test/ZeroXBridgeProof.t.sol deleted file mode 100644 index e9a2d38..0000000 --- a/contracts/L1/test/ZeroXBridgeProof.t.sol +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "forge-std/Test.sol"; -import "forge-std/console.sol"; -import {ZeroXBridgeL1} from "../src/ZeroXBridgeL1.sol"; -import {MockProofRegistry} from "./mocks/MockProofRegistry.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol"; -import {Starknet} from "../utils/Starknet.sol"; -import {MockERC20} from "./mocks/MockERC20.sol"; - -contract ZeroXBridgeTest is Test { - ZeroXBridgeL1 public bridge; - MockERC20 public dai; - MockERC20 public usdc; - address public ethPriceFeed; - address public daiPriceFeed; - address public usdcPriceFeed; - - address public owner = address(0x1); - address public admin; - - // Proof Generation - MockProofRegistry public proofRegistry; - address public user; - uint256 public amount; - uint256 public starknetPubKey; - uint256 public commitmentHash; - uint256 public ethAccountPrivateKey; - uint256 public blockHash; - - address public user2 = address(0x3); - address public relayer = address(0x4); - address public nonRelayer = address(0x5); - - event FundsUnlocked(address indexed user, uint256 amount, uint256 commitmentHash); - - event RelayerStatusChanged(address indexed relayer, bool status); - - event ClaimEvent(address indexed user, uint256 amount); - - error OwnableUnauthorizedAccount(address account); - - function setUp() public { - admin = address(0x123); - proofRegistry = new MockProofRegistry(); - - vm.startPrank(owner); - // Deploy the bridge contract - bridge = new ZeroXBridgeL1(admin, owner, address(proofRegistry)); - - // Setup approved relayer - bridge.setRelayerStatus(relayer, true); - - vm.stopPrank(); - - // Create a dummy commitment hash for tests involving unlock_funds_with_proof - user = 0xfc36a8C3f3FEC3217fa8bba11d2d5134e0354316; - amount = 100 ether; - starknetPubKey = 0x06ee7c7a561ae5c39e3a2866e8e208ed8ebe45da686e2929622102c80834b771; - ethAccountPrivateKey = 0x0b97274c3a8422119bc974361f370a03d022745a3be21c621b26226b2d6faf3a; - blockHash = 0x0123456; - commitmentHash = uint256(keccak256(abi.encodePacked(starknetPubKey, amount, blockHash))); - - // Deploy mock ERC20 tokens - dai = new MockERC20(18); // DAI with 18 decimals - usdc = new MockERC20(6); // USDC with 6 decimals - - // Assign mock price feed addresses - ethPriceFeed = address(1); - daiPriceFeed = address(2); - usdcPriceFeed = address(3); - - vm.startPrank(admin); - bridge.registerToken(ZeroXBridgeL1.AssetType.ETH, address(0), ethPriceFeed, 18); - bridge.registerToken(ZeroXBridgeL1.AssetType.ERC20, address(usdc), usdcPriceFeed, 6); - bridge.registerToken(ZeroXBridgeL1.AssetType.ERC20, address(dai), daiPriceFeed, 18); - - vm.stopPrank(); - } - - // Tests for Claim with Proofs - // ======================== - // Todo -}