From 2b7443ffd96d1f3f2a046a359037a8cbb8015806 Mon Sep 17 00:00:00 2001 From: seun Date: Sun, 25 Aug 2024 16:50:36 -0700 Subject: [PATCH 1/4] feat:foundry test for LineaWorld --- .env.example | 1 + package.json | 3 +- remappings.txt | 12 +- src/mocks/MocksBrigedWorldID.sol | 54 ++++ src/mocks/MocksMessageService.sol | 114 ++++++++ src/mocks/MocksStateBridge.sol | 41 +++ src/mocks/MocksWorldIDIdentityManager.sol | 102 +++++++ test/LinearStateBridge.t.sol | 333 ++++++++++++++++++++++ test/placeholder.t.sol | 10 - 9 files changed, 655 insertions(+), 15 deletions(-) create mode 100644 src/mocks/MocksBrigedWorldID.sol create mode 100644 src/mocks/MocksMessageService.sol create mode 100644 src/mocks/MocksStateBridge.sol create mode 100644 src/mocks/MocksWorldIDIdentityManager.sol create mode 100644 test/LinearStateBridge.t.sol diff --git a/.env.example b/.env.example index b28fddc..8ec3845 100644 --- a/.env.example +++ b/.env.example @@ -14,4 +14,5 @@ export MSG_CLAIM_VALUE=0 export MSG_CLAIM_NONCE=0 export MSG_CLAIM_CALLDATA="YOUR_CALL_DATA" export MSG_CLAIM_FEE_RECIPIENT=0x0 +export MAINNET_RPC_URL=https://eth.llamarpc.com diff --git a/package.json b/package.json index b2b5308..4df7363 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "dependencies": { "@openzeppelin/contracts": "^4.9.6", "@openzeppelin/contracts-upgradeable": "^4.9.6", + "@prb/test": "^0.6.4", "@worldcoin/world-id-state-bridge": "github:worldcoin/world-id-state-bridge#729d234", "linea-contracts": "github:Consensys/linea-contracts" }, @@ -46,4 +47,4 @@ "test:coverage": "forge coverage", "test:coverage:report": "forge coverage --report lcov && genhtml lcov.info --branch-coverage --output-dir coverage" } -} +} \ No newline at end of file diff --git a/remappings.txt b/remappings.txt index 30f60db..801a664 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,5 +1,9 @@ @openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ -@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable -forge-std/=node_modules/forge-std/src -world-id-state-bridge=node_modules/@worldcoin/world-id-state-bridge/src -linea-contracts/=node_modules/linea-contracts/contracts +@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/ +forge-std/=node_modules/forge-std/src/ +world-id-state-bridge/=node_modules/@worldcoin/world-id-state-bridge/src/ +linea-contracts/=node_modules/linea-contracts/contracts/ +@prb/test/=node_modules/@prb/test/src/ +@eth-optimism/=node_modules/@eth-optimism/ +@worldcoin/=node_modules/@worldcoin/ +hardhat/=node_modules/hardhat/ \ No newline at end of file diff --git a/src/mocks/MocksBrigedWorldID.sol b/src/mocks/MocksBrigedWorldID.sol new file mode 100644 index 0000000..eb9d4eb --- /dev/null +++ b/src/mocks/MocksBrigedWorldID.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import { WorldIDBridge } from "world-id-state-bridge/abstract/WorldIDBridge.sol"; +import { Ownable2Step } from "@openzeppelin/contracts/access/Ownable2Step.sol"; + +/// @title LineaWorldID Mock +/// @author Worldcoin & IkemHood +/// @notice Mock of LineaWorldID in order to test functionality on a local chain +/// @custom:deployment deployed through make local-mock +contract MockBridgedWorldID is WorldIDBridge, Ownable2Step { + /////////////////////////////////////////////////////////////////////////////// + /// CONSTRUCTION /// + /////////////////////////////////////////////////////////////////////////////// + + /// @notice Initializes the contract the depth of the associated merkle tree. + /// + /// @param _treeDepth The depth of the WorldID Semaphore merkle tree. + constructor(uint8 _treeDepth) WorldIDBridge(_treeDepth) { } + + /////////////////////////////////////////////////////////////////////////////// + /// ROOT MIRRORING /// + /////////////////////////////////////////////////////////////////////////////// + + /// @notice This function is called by the state bridge contract when it forwards a new root to + /// the bridged WorldID. + /// @dev This function can revert if Optimism's CrossDomainMessenger stops processing proofs + /// or if OPLabs stops submitting them. Next iteration of Optimism's cross-domain messaging, will be + /// fully permissionless for message-passing, so this will not be an issue. + /// Sequencer needs to include changes to the CrossDomainMessenger contract on L1, + /// not economically penalized if messages are not included, however the fraud prover (Cannon) + /// can force the sequencer to include it. + /// + /// @param newRoot The value of the new root. + /// + /// @custom:reverts CannotOverwriteRoot If the root already exists in the root history. + /// @custom:reverts string If the caller is not the owner. + function receiveRoot(uint256 newRoot) public virtual onlyOwner { + _receiveRoot(newRoot); + } + + /////////////////////////////////////////////////////////////////////////////// + /// DATA MANAGEMENT /// + /////////////////////////////////////////////////////////////////////////////// + + /// @notice Sets the amount of time it takes for a root in the root history to expire. + /// + /// @param expiryTime The new amount of time it takes for a root to expire. + /// + /// @custom:reverts string If the caller is not the owner. + function setRootHistoryExpiry(uint256 expiryTime) public virtual override onlyOwner { + _setRootHistoryExpiry(expiryTime); + } +} \ No newline at end of file diff --git a/src/mocks/MocksMessageService.sol b/src/mocks/MocksMessageService.sol new file mode 100644 index 0000000..9c65b3a --- /dev/null +++ b/src/mocks/MocksMessageService.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.19 <=0.8.24; + +import { IMessageService } from "linea-contracts/interfaces/IMessageService.sol"; + +/** + * @title Mock Message Service + * @author Worldcoin & IkemHood + * @notice Mock implementation of the IMessageService interface for testing functionality on a local chain. + * @dev deployed through make mock and make local-mock + */ +contract MockMessageService is IMessageService { + /// @notice Tracks the original sender for mocking purposes. + address private _originalSender; + + /// @notice Counter to generate unique nonces for each message. + uint256 private _nonceCounter; + + /// @notice Mapping to keep track of claimed messages to prevent double claims. + mapping(bytes32 => bool) private _claimedMessages; + + /** + * @notice Sets the original sender address for mocking purposes. + * @param _sender The address to set as the original sender. + */ + function setOriginalSender(address _sender) external { + _originalSender = _sender; + } + + /** + * @notice Mocks the process of sending a message. + * @dev This function should be called with a msg.value = _value + _fee. The fee will be paid on the destination + * chain. + * @param _to The destination address on the destination chain. + * @param _fee The message service fee on the origin chain. + * @param _calldata The calldata used by the destination message service to call the destination contract. + */ + function sendMessage(address _to, uint256 _fee, bytes calldata _calldata) external payable override { + if (msg.value < _fee) { + revert FeeTooLow(); + } + if (msg.value < _fee + 1) { + // Assuming 1 wei as the minimum value for testing purposes + revert ValueSentTooLow(); + } + + // Generate a unique message hash based on the message parameters. + bytes32 messageHash = keccak256(abi.encodePacked(msg.sender, _to, _fee, msg.value, _nonceCounter, _calldata)); + + // Emit the MessageSent event with the calculated hash and other details. + emit MessageSent(msg.sender, _to, _fee, msg.value, _nonceCounter, _calldata, messageHash); + + // Increment the nonce counter for the next message. + _nonceCounter++; + } + + /** + * @notice Mocks the process of claiming a message on the destination chain. + * @param _from The msg.sender calling the origin message service. + * @param _to The destination address on the destination chain. + * @param _fee The message service fee on the origin chain. + * @param _value The value to be transferred to the destination address. + * @param _feeRecipient Address that will receive the fees. + * @param _calldata The calldata used by the destination message service to call/forward to the destination + * contract. + * @param _nonce Unique message number. + */ + function claimMessage( + address _from, + address _to, + uint256 _fee, + uint256 _value, + address payable _feeRecipient, + bytes calldata _calldata, + uint256 _nonce + ) + external + override + { + // Decode the message hash based on the provided parameters. + bytes32 messageHash = keccak256(abi.encodePacked(_from, _to, _fee, _value, _nonce, _calldata)); + + // Revert if the message has already been claimed to prevent double claims. + if (_claimedMessages[messageHash]) { + revert MessageSendingFailed(_to); + } + + // Mark the message as claimed. + _claimedMessages[messageHash] = true; + + // Emit the MessageClaimed event with the calculated hash. + emit MessageClaimed(messageHash); + + // Simulate sending the fee to the recipient. + (bool success,) = _feeRecipient.call{ value: _fee }(""); + if (!success) { + revert FeePaymentFailed(_feeRecipient); + } + + // Simulate sending the value to the recipient. + (success,) = _to.call{ value: _value }(""); + if (!success) { + revert MessageSendingFailed(_to); + } + } + + /** + * @notice Returns the original sender of the message on the origin layer. + * @return The original sender address. + */ + function sender() external view override returns (address) { + return _originalSender; + } +} \ No newline at end of file diff --git a/src/mocks/MocksStateBridge.sol b/src/mocks/MocksStateBridge.sol new file mode 100644 index 0000000..b8c55f4 --- /dev/null +++ b/src/mocks/MocksStateBridge.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import { MockBridgedWorldID } from "./MockBridgedWorldID.sol"; +import { IWorldIDIdentityManager } from "world-id-state-bridge/interfaces/IWorldIDIdentityManager.sol"; +import { Ownable2Step } from "@openzeppelin/contracts/access/Ownable2Step.sol"; + +/// @title Mock State Bridge +/// @author Worldcoin +/// @notice Mock of the StateBridge to test functionality on a local chain +/// @custom:deployment deployed through make local-mock +contract MockStateBridge is Ownable2Step { + /// @notice MockWorldIDIdentityManager contract which will hold a mock root + IWorldIDIdentityManager public worldID; + + /// @notice MockBridgedWorldID contract which will receive the root + MockBridgedWorldID public mockBridgedWorldID; + + /// @notice Emmited when the root is not a valid root in the canonical WorldID Identity Manager contract + error InvalidRoot(); + + /// @notice constructor + constructor(address _mockWorldID, address _mockBridgedWorldID) { + worldID = IWorldIDIdentityManager(_mockWorldID); + mockBridgedWorldID = MockBridgedWorldID(_mockBridgedWorldID); + } + + /// @notice Sends the latest WorldID Identity Manager root to the Bridged WorldID contract. + /// @dev Calls this method on the L1 Proxy contract to relay roots to WorldID supported chains. + function propagateRoot() public { + uint256 latestRoot = worldID.latestRoot(); + _sendRootToMockBridgedWorldID(latestRoot); + } + + // @notice Sends the latest WorldID Identity Manager root to all chains. + /// @dev Calls this method on the L1 Proxy contract to relay roots to WorldID supported chains. + /// @param root The latest WorldID Identity Manager root. + function _sendRootToMockBridgedWorldID(uint256 root) internal { + mockBridgedWorldID.receiveRoot(root); + } +} \ No newline at end of file diff --git a/src/mocks/MocksWorldIDIdentityManager.sol b/src/mocks/MocksWorldIDIdentityManager.sol new file mode 100644 index 0000000..ce984e3 --- /dev/null +++ b/src/mocks/MocksWorldIDIdentityManager.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import { IWorldIDIdentityManager } from "world-id-state-bridge/interfaces/IWorldIDIdentityManager.sol"; + +/// @title WorldID Identity Manager Mock +/// @author Worldcoin +/// @notice Mock of the WorldID Identity Manager contract (world-id-contracts) to test functionality on a local chain +/// @dev deployed through make mock and make local-mock +contract MockWorldIDIdentityManager is IWorldIDIdentityManager { + uint256 internal _latestRoot; + + /// @notice Represents the kind of change that is made to the root of the tree. + enum TreeChange { + Insertion, + Deletion + } + + /// @notice Emitted when the current root of the tree is updated. + /// + /// @param preRoot The value of the tree's root before the update. + /// @param kind Either "insertion" or "update", the kind of alteration that was made to the + /// tree. + /// @param postRoot The value of the tree's root after the update. + event TreeChanged(uint256 indexed preRoot, TreeChange indexed kind, uint256 indexed postRoot); + + constructor(uint256 initRoot) { + _latestRoot = initRoot; + } + + /// @notice Registers identities into the WorldID system. + /// @dev Can only be called by the identity operator. + /// @dev Registration is performed off-chain and verified on-chain via the `insertionProof`. + /// This saves gas and time over inserting identities one at a time. + /// + /// @param insertionProof The proof that given the conditions (`preRoot`, `startIndex` and + /// `identityCommitments`), insertion into the tree results in `postRoot`. Elements 0 and + /// 1 are the `x` and `y` coordinates for `ar` respectively. Elements 2 and 3 are the `x` + /// coordinate for `bs`, and elements 4 and 5 are the `y` coordinate for `bs`. Elements 6 + /// and 7 are the `x` and `y` coordinates for `krs`. + /// @param preRoot The value for the root of the tree before the `identityCommitments` have been + //// inserted. Must be an element of the field `Kr`. (already in reduced form) + /// @param startIndex The position in the tree at which the insertions were made. + /// @param identityCommitments The identities that were inserted into the tree starting at + /// `startIndex` and `preRoot` to give `postRoot`. All of the commitments must be + /// elements of the field `Kr`. + /// @param postRoot The root obtained after inserting all of `identityCommitments` into the tree + /// described by `preRoot`. Must be an element of the field `Kr`. (alread in reduced form) + /// + function registerIdentities( + uint256[8] calldata insertionProof, + uint256 preRoot, + uint32 startIndex, + uint256[] calldata identityCommitments, + uint256 postRoot + ) + public + { + _latestRoot = postRoot; + emit TreeChanged(preRoot, TreeChange.Insertion, postRoot); + } + + /// @notice Deletes identities from the WorldID system. + /// @dev Can only be called by the identity operator. + /// @dev Deletion is performed off-chain and verified on-chain via the `deletionProof`. + /// This saves gas and time over deleting identities one at a time. + /// + /// @param deletionProof The proof that given the conditions (`preRoot` and `packedDeletionIndices`), + /// deletion into the tree results in `postRoot`. Elements 0 and 1 are the `x` and `y` + /// coordinates for `ar` respectively. Elements 2 and 3 are the `x` coordinate for `bs`, + /// and elements 4 and 5 are the `y` coordinate for `bs`. Elements 6 and 7 are the `x` + /// and `y` coordinates for `krs`. + /// @param packedDeletionIndices The indices of the identities that were deleted from the tree. The batch size is + /// inferred from the length of this + //// array: batchSize = packedDeletionIndices / 4 + /// @param preRoot The value for the root of the tree before the corresponding identity commitments have + /// been deleted. Must be an element of the field `Kr`. + /// @param postRoot The root obtained after deleting all of `identityCommitments` into the tree + /// described by `preRoot`. Must be an element of the field `Kr`. + function deleteIdentities( + uint256[8] calldata deletionProof, + bytes calldata packedDeletionIndices, + uint256 preRoot, + uint256 postRoot + ) + public + { + _latestRoot = postRoot; + emit TreeChanged(preRoot, TreeChange.Deletion, postRoot); + } + + function insertRoot(uint256 postRoot) public { + uint256 preRoot = _latestRoot; + _latestRoot = postRoot; + + emit TreeChanged(preRoot, TreeChange.Insertion, postRoot); + } + + function latestRoot() external view returns (uint256) { + return _latestRoot; + } +} \ No newline at end of file diff --git a/test/LinearStateBridge.t.sol b/test/LinearStateBridge.t.sol new file mode 100644 index 0000000..d201c39 --- /dev/null +++ b/test/LinearStateBridge.t.sol @@ -0,0 +1,333 @@ +pragma solidity ^0.8.15; + +import { LineaStateBridge } from "../src/LineaStateBridge.sol"; +import { MockWorldIDIdentityManager } from "../src/mocks/MockWorldIDIdentityManager.sol"; +import { MockBridgedWorldID } from "../src/mocks/MockBridgedWorldID.sol"; +import { MockMessageService } from "../src/mocks/MockMessageService.sol"; + +import { PRBTest } from "@prb/test/PRBTest.sol"; +import { StdCheats } from "forge-std/StdCheats.sol"; + +/// @title Linea State Bridge Test +/// @author Worldcoin & IkemHood +/// @notice A test contract for LineaStateBridge.sol +contract LineaStateBridgeTest is PRBTest, StdCheats { + uint256 public mainnetFork; + string private MAINNET_RPC_URL = vm.envString("MAINNET_RPC_URL"); + + LineaStateBridge public lineaStateBridge; + MockWorldIDIdentityManager public mockWorldID; + + uint32 public lineaGasLimit; + + address public mockWorldIDAddress; + + address public owner; + + MockBridgedWorldID public mockLineaWorldID; + + /// @notice The address of the LineaWorldID contract + address public lineaWorldIDAddress; + + MockMessageService public lineaCrossDomainMessenger; + + /// @notice address for Linea Stack chain Ethereum mainnet L1CrossDomainMessenger contract + address public lineaCrossDomainMessengerAddress; + + uint256 public sampleRoot; + + /////////////////////////////////////////////////////////////////// + /// EVENTS /// + /////////////////////////////////////////////////////////////////// + + /// @notice Emitted when the ownership transfer of lineaStateBridge is started (OZ Ownable2Step) + event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner); + + /// @notice Emitted when the StateBridge changes authorized remote sender address + /// of the LineaWorldID contract to the WorldID Identity Manager contract + /// @param previousRemoteAddress The previous authorized remote sender for the LineaWorldID contract + /// @param remoteAddress The authorized remote sender address, cannot be empty + event UpdatedRemoteAddressLinea(address indexed previousRemoteAddress, address indexed remoteAddress); + + /// @notice Emitted when the StateBridge changes message service address + /// of the LineaWorldID contract + /// @param previousMessageService The previous message service address for the LineaWorldID contract + /// @param messageService The message service address, cannot be empty. + event UpdatedMessageServiceLinea(address indexed previousMessageService, address indexed messageService); + + /// @notice Emitted when the message service is set or updated + /// @param oldMessageService The old message service address. + /// @param newMessageService The new message service address. + event MessageServiceUpdated(address indexed oldMessageService, address indexed newMessageService); + + /// @notice Emitted when the StateBridge sends a root to the LineaWorldID contract + /// @param root The root sent to the LineaWorldID contract on Linea + event RootPropagated(uint256 root); + + /// @notice Emitted when the StateBridge sets the root history expiry for LineaWorldID + /// @param rootHistoryExpiry The new root history expiry + event SetRootHistoryExpiry(uint256 rootHistoryExpiry); + + /// @notice Emitted when the StateBridge sets the fee for propagateRoot + /// @param _lineaFee The new fee for propagateRoot + event SetFeePropagateRoot(uint256 _lineaFee); + + /// @notice Emitted when the StateBridge sets the fee for SetRootHistoryExpiry + /// @param _lineaFee The new fee for SetRootHistoryExpiry + event SetFeeSetRootHistoryExpiry(uint256 _lineaFee); + + /// @notice Emitted when the StateBridge sets the fee for transferOwnershipLinea + /// @param _lineaFee The new fee for transferOwnershipLinea + event SetFeeTransferOwnershipLinea(uint256 _lineaFee); + + /// @notice Emitted when the StateBridge sets the fee for setMessageService + /// @param _lineaFee The new fee for setMessageService + event SetFeeSetMessageService(uint256 _lineaFee); + + /////////////////////////////////////////////////////////////////// + /// ERRORS /// + /////////////////////////////////////////////////////////////////// + + /// @notice Emitted when an attempt is made to renounce ownership. + error CannotRenounceOwnership(); + + /// @notice Emitted when an attempt is made to set an address to zero + error AddressZero(); + + function setUp() public { + mainnetFork = vm.createFork(MAINNET_RPC_URL); + vm.selectFork(mainnetFork); + + // Deploy mock contracts + sampleRoot = uint256(0x111); + mockWorldID = new MockWorldIDIdentityManager(sampleRoot); + mockWorldIDAddress = address(mockWorldID); + + mockLineaWorldID = new MockBridgedWorldID(32); + lineaWorldIDAddress = address(mockLineaWorldID); + + lineaCrossDomainMessenger = new MockMessageService(); + lineaCrossDomainMessengerAddress = address(lineaCrossDomainMessenger); + + // Deploy LineaStateBridge + lineaStateBridge = + new LineaStateBridge(mockWorldIDAddress, lineaWorldIDAddress, lineaCrossDomainMessengerAddress); + + owner = lineaStateBridge.owner(); + } + + /////////////////////////////////////////////////////////////////// + /// SUCCEEDS /// + /////////////////////////////////////////////////////////////////// + + /// @notice select a specific fork + function test_canSelectFork_succeeds() public { + // select the fork + vm.selectFork(mainnetFork); + assertEq(vm.activeFork(), mainnetFork); + } + + function test_propagateRoot_suceeds() public { + vm.expectEmit(true, true, true, true); + emit RootPropagated(sampleRoot); + + lineaStateBridge.propagateRoot(); + + // Bridging is not emulated + } + + /// @notice Tests that the owner of the StateBridge contract can transfer ownership + /// using Ownable2Step transferOwnership + /// @param newOwner the new owner of the contract + function test_owner_transferOwnership_succeeds(address newOwner, bool isLocal) public { + vm.assume(newOwner != address(0)); + + vm.expectEmit(true, true, true, true); + + // OpenZeppelin Ownable2Step transferOwnershipStarted event + emit OwnershipTransferStarted(owner, newOwner); + + vm.prank(owner); + lineaStateBridge.transferOwnership(newOwner); + + vm.prank(newOwner); + lineaStateBridge.acceptOwnership(); + + assertEq(lineaStateBridge.owner(), newOwner); + } + + /// @notice tests whether the StateBridge contract can transfer ownership of the lineaWorldID contract + /// @param newOwner The new owner of the lineaWorldID contract (foundry fuzz) + /// @param isLocal Whether the ownership transfer is local (Linea EOA/contract) or an Ethereum EOA or contract + function test_owner_transferOwnershipLinea_succeeds(address newOwner, bool isLocal) public { + vm.assume(newOwner != address(0)); + vm.expectEmit(true, true, true, true); + + emit UpdatedRemoteAddressLinea(owner, newOwner); + + vm.prank(owner); + lineaStateBridge.transferOwnershipLinea(newOwner, isLocal); + } + + /// @notice tests whether the StateBridge contract can updates the message service + /// @param _messageService The new message service address + function test_owner_setMessageService_succeeds(address _messageService) public { + vm.assume(_messageService != address(0)); + vm.expectEmit(true, true, true, true); + + emit UpdatedMessageServiceLinea(owner, _messageService); + + vm.prank(owner); + lineaStateBridge.setMessageService(_messageService); + } + + /// @notice tests whether the StateBridge contract can set root history expiry on Linea + /// @param _rootHistoryExpiry The new root history expiry for LineaWorldID + function test_owner_setRootHistoryExpiry_succeeds(uint256 _rootHistoryExpiry) public { + vm.expectEmit(true, true, true, true); + emit SetRootHistoryExpiry(_rootHistoryExpiry); + + vm.prank(owner); + lineaStateBridge.setRootHistoryExpiry(_rootHistoryExpiry); + } + + /// @notice tests whether the StateBridge contract can set fee for propagateRoot + /// @param _lineaFee The new lineaFee for SetRootHistoryExpiry + function test_owner_setFeePropagateRoot_succeeds(uint32 _lineaFee) public { + vm.assume(_lineaFee != 0); + + vm.expectEmit(true, true, true, true); + + emit SetFeePropagateRoot(_lineaFee); + + vm.prank(owner); + lineaStateBridge.setFeePropagateRoot(_lineaFee); + } + + /// @notice tests whether the StateBridge contract can set fee for SetRootHistoryExpiry + /// @param _lineaFee The new lineaFee for SetRootHistoryExpiry + function test_owner_setFeeSetRootHistoryExpiry_succeeds(uint32 _lineaFee) public { + vm.assume(_lineaFee != 0); + + vm.expectEmit(true, true, true, true); + + emit SetFeeSetRootHistoryExpiry(_lineaFee); + + vm.prank(owner); + lineaStateBridge.setFeeSetRootHistoryExpiry(_lineaFee); + } + + /// @notice tests whether the StateBridge contract can set fee for SetRootHistoryExpiry + /// @param _lineaFee The new lineaFee for transferOwnershipLinea + function test_owner_setFeeTransferOwnershipLinea_succeeds(uint32 _lineaFee) public { + vm.assume(_lineaFee != 0); + + vm.expectEmit(true, true, true, true); + + emit SetFeeTransferOwnershipLinea(_lineaFee); + + vm.prank(owner); + lineaStateBridge.setFeeTransferOwnershipLinea(_lineaFee); + } + + /// @notice tests whether the StateBridge contract can set fee for SetRootHistoryExpiry + /// @param _lineaFee The new lineaFee for setMessageService + function test_owner_setFeeSetMessageService_succeeds(uint32 _lineaFee) public { + vm.assume(_lineaFee != 0); + + vm.expectEmit(true, true, true, true); + + emit SetFeeSetMessageService(_lineaFee); + + vm.prank(owner); + lineaStateBridge.setFeeSetMessageService(_lineaFee); + } + + /////////////////////////////////////////////////////////////////// + /// REVERTS /// + /////////////////////////////////////////////////////////////////// + + /// @notice Tests that the StateBridge constructor params can't be set to the zero address + function test_cannotInitializeConstructorWithZeroAddresses_reverts() public { + vm.expectRevert(AddressZero.selector); + lineaStateBridge = new LineaStateBridge(address(0), lineaWorldIDAddress, lineaCrossDomainMessengerAddress); + + vm.expectRevert(AddressZero.selector); + lineaStateBridge = new LineaStateBridge(mockWorldIDAddress, address(0), lineaCrossDomainMessengerAddress); + + vm.expectRevert(AddressZero.selector); + lineaStateBridge = new LineaStateBridge(mockWorldIDAddress, lineaWorldIDAddress, address(0)); + } + + /// @notice tests that the StateBridge contract's ownership can't be changed by a non-owner + /// @param newOwner The new owner of the StateBridge contract (foundry fuzz) + /// @param nonOwner An address that is not the owner of the StateBridge contract + function test_notOwner_transferOwnership_reverts(address nonOwner, address newOwner) public { + vm.assume(nonOwner != owner && nonOwner != address(0) && newOwner != address(0)); + + vm.expectRevert("Ownable: caller is not the owner"); + + vm.prank(nonOwner); + lineaStateBridge.transferOwnership(newOwner); + } + + /// @notice tests that the StateBridge contract's ownership can't be set to be the zero address + function test_owner_transferOwnershipLinea_toZeroAddress_reverts() public { + vm.expectRevert(AddressZero.selector); + + vm.prank(owner); + lineaStateBridge.transferOwnershipLinea(address(0), true); + } + + /// @notice tests that the StateBridge contract's ownership can't be changed by a non-owner + /// @param newOwner The new owner of the StateBridge contract (foundry fuzz) + function test_notOwner_transferOwnershipLinea_reverts(address nonOwner, address newOwner, bool isLocal) public { + vm.assume(nonOwner != owner && newOwner != address(0)); + + vm.expectRevert("Ownable: caller is not the owner"); + + vm.prank(nonOwner); + lineaStateBridge.transferOwnershipLinea(newOwner, isLocal); + } + + /// @notice tests whether the StateBridge contract can set root history expiry + /// @param _rootHistoryExpiry The new root history expiry + function test_notOwner_SetRootHistoryExpiry_reverts(address nonOwner, uint256 _rootHistoryExpiry) public { + vm.assume(nonOwner != owner && nonOwner != address(0) && _rootHistoryExpiry != 0); + + vm.expectRevert("Ownable: caller is not the owner"); + + vm.prank(nonOwner); + lineaStateBridge.setRootHistoryExpiry(_rootHistoryExpiry); + } + + /// @notice Tests that a nonPendingOwner can't accept ownership of StateBridge + /// @param newOwner the new owner of the contract + function test_notOwner_acceptOwnership_reverts(address newOwner, address randomAddress) public { + vm.assume(newOwner != address(0) && randomAddress != address(0) && randomAddress != newOwner); + + vm.prank(owner); + lineaStateBridge.transferOwnership(newOwner); + + vm.expectRevert("Ownable2Step: caller is not the new owner"); + + vm.prank(randomAddress); + lineaStateBridge.acceptOwnership(); + } + + /// @notice Tests that ownership can't be renounced + function test_owner_renounceOwnership_reverts() public { + vm.expectRevert(CannotRenounceOwnership.selector); + + vm.prank(owner); + lineaStateBridge.renounceOwnership(); + } + + /// @notice Tests that setMessageService reverts for address zero + function test_owner_setMessageService_reverts() public { + vm.expectRevert(AddressZero.selector); + + vm.prank(owner); + lineaStateBridge.setMessageService(address(0)); + } +} \ No newline at end of file diff --git a/test/placeholder.t.sol b/test/placeholder.t.sol index 5aa69b4..e69de29 100644 --- a/test/placeholder.t.sol +++ b/test/placeholder.t.sol @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.15; - -// TODO: remove this placeholder test after the actual tests are implemented - -contract PlaceholderTest { - function testAlwaysPasses() public pure returns (bool) { - return true; - } -} From dfd91723e2d0f491e77f1073a0560aa065efa793 Mon Sep 17 00:00:00 2001 From: seun Date: Sun, 25 Aug 2024 16:59:33 -0700 Subject: [PATCH 2/4] chore:forge fmt --- src/mocks/MocksBrigedWorldID.sol | 2 +- src/mocks/MocksMessageService.sol | 2 +- src/mocks/MocksStateBridge.sol | 2 +- src/mocks/MocksWorldIDIdentityManager.sol | 2 +- test/LinearStateBridge.t.sol | 2 +- test/placeholder.t.sol | 1 + 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/mocks/MocksBrigedWorldID.sol b/src/mocks/MocksBrigedWorldID.sol index eb9d4eb..603f88b 100644 --- a/src/mocks/MocksBrigedWorldID.sol +++ b/src/mocks/MocksBrigedWorldID.sol @@ -51,4 +51,4 @@ contract MockBridgedWorldID is WorldIDBridge, Ownable2Step { function setRootHistoryExpiry(uint256 expiryTime) public virtual override onlyOwner { _setRootHistoryExpiry(expiryTime); } -} \ No newline at end of file +} diff --git a/src/mocks/MocksMessageService.sol b/src/mocks/MocksMessageService.sol index 9c65b3a..0473308 100644 --- a/src/mocks/MocksMessageService.sol +++ b/src/mocks/MocksMessageService.sol @@ -111,4 +111,4 @@ contract MockMessageService is IMessageService { function sender() external view override returns (address) { return _originalSender; } -} \ No newline at end of file +} diff --git a/src/mocks/MocksStateBridge.sol b/src/mocks/MocksStateBridge.sol index b8c55f4..b6a4025 100644 --- a/src/mocks/MocksStateBridge.sol +++ b/src/mocks/MocksStateBridge.sol @@ -38,4 +38,4 @@ contract MockStateBridge is Ownable2Step { function _sendRootToMockBridgedWorldID(uint256 root) internal { mockBridgedWorldID.receiveRoot(root); } -} \ No newline at end of file +} diff --git a/src/mocks/MocksWorldIDIdentityManager.sol b/src/mocks/MocksWorldIDIdentityManager.sol index ce984e3..c2f949c 100644 --- a/src/mocks/MocksWorldIDIdentityManager.sol +++ b/src/mocks/MocksWorldIDIdentityManager.sol @@ -99,4 +99,4 @@ contract MockWorldIDIdentityManager is IWorldIDIdentityManager { function latestRoot() external view returns (uint256) { return _latestRoot; } -} \ No newline at end of file +} diff --git a/test/LinearStateBridge.t.sol b/test/LinearStateBridge.t.sol index d201c39..4ce0405 100644 --- a/test/LinearStateBridge.t.sol +++ b/test/LinearStateBridge.t.sol @@ -330,4 +330,4 @@ contract LineaStateBridgeTest is PRBTest, StdCheats { vm.prank(owner); lineaStateBridge.setMessageService(address(0)); } -} \ No newline at end of file +} diff --git a/test/placeholder.t.sol b/test/placeholder.t.sol index e69de29..8b13789 100644 --- a/test/placeholder.t.sol +++ b/test/placeholder.t.sol @@ -0,0 +1 @@ + From 3164a39c43f5c0b93332e77898b81afc2c38cf97 Mon Sep 17 00:00:00 2001 From: seun Date: Thu, 29 Aug 2024 09:05:22 -0700 Subject: [PATCH 3/4] feat:test LinearWorld --- remappings.txt | 7 +- test/LineaWorldID.t.sol | 218 +++++++++++++++++++++++++++++++++++ test/LinearStateBridge.t.sol | 4 - 3 files changed, 219 insertions(+), 10 deletions(-) create mode 100644 test/LineaWorldID.t.sol diff --git a/remappings.txt b/remappings.txt index 801a664..c59a627 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,9 +1,4 @@ -@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ -@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/ forge-std/=node_modules/forge-std/src/ world-id-state-bridge/=node_modules/@worldcoin/world-id-state-bridge/src/ linea-contracts/=node_modules/linea-contracts/contracts/ -@prb/test/=node_modules/@prb/test/src/ -@eth-optimism/=node_modules/@eth-optimism/ -@worldcoin/=node_modules/@worldcoin/ -hardhat/=node_modules/hardhat/ \ No newline at end of file +@prb/test/=node_modules/@prb/test/src/ \ No newline at end of file diff --git a/test/LineaWorldID.t.sol b/test/LineaWorldID.t.sol new file mode 100644 index 0000000..65cb4ee --- /dev/null +++ b/test/LineaWorldID.t.sol @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { LineaWorldID } from "../src/LineaWorldID.sol"; +import { IMessageService } from "linea-contracts/interfaces/IMessageService.sol"; +import { MockMessageService } from "../src/mocks/MockMessageService.sol"; + +import { PRBTest } from "@prb/test/PRBTest.sol"; +import { StdCheats } from "forge-std/StdCheats.sol"; + +/// @title Linea World ID Test +/// @notice A test contract for LineaWorldID.sol +contract LineaWorldIDTest is PRBTest, StdCheats { + LineaWorldID public lineaWorldID; + + MockMessageService public mockMessageService; + address public messageServiceAddress; + + uint8 constant TREE_DEPTH = 30; + address constant OWNER = address(0x1234); + + /////////////////////////////////////////////////////////////////// + /// ERRORS /// + /////////////////////////////////////////////////////////////////// + + /// @notice Emitted when attempting to validate a root that has expired. + error ExpiredRoot(); + + /// @notice Emitted when attempting to validate a root that has yet to be added to the root + /// history. + error NonExistentRoot(); + + /// @notice Emitted when attempting to update the timestamp for a root that already has one. + error CannotOverwriteRoot(); + + /// @notice Emitted if the latest root is requested but the bridge has not seen any roots yet. + error NoRootsSeen(); + + /// @notice Emitted when the caller is not the owner + error CallerIsNotOwner(); + + /////////////////////////////////////////////////////////////////////////////// + /// EVENTS /// + /////////////////////////////////////////////////////////////////////////////// + + /// @notice Emitted when a new root is received by the contract. + /// + /// @param root The value of the root that was added. + /// @param timestamp The timestamp of insertion for the given root. + event RootAdded(uint256 root, uint128 timestamp); + + /// @notice Emitted when the expiry time for the root history is updated. + /// + /// @param newExpiry The new expiry time. + event RootHistoryExpirySet(uint256 newExpiry); + + /// @notice Emits when ownership of the contract is transferred. Includes the + /// isLocal field in addition to the standard `Ownable` OwnershipTransferred event. + /// @param previousOwner The previous owner of the contract. + /// @param newOwner The new owner of the contract. + /// @param isLocal Configures the `isLocal` contract variable. + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner, bool isLocal); + + function setUp() public { + mockMessageService = new MockMessageService(); + messageServiceAddress = address(mockMessageService); + lineaWorldID = new LineaWorldID(TREE_DEPTH, messageServiceAddress); + lineaWorldID.transferOwnership(OWNER, true); + } + + /////////////////////////////////////////////////////////////////// + /// SUCCEEDS /// + /////////////////////////////////////////////////////////////////// + + /// @notice Tests the initial state of the LineaWorldID contract + function test_initialState_succeeds() public { + assertEq(lineaWorldID.getTreeDepth(), TREE_DEPTH); + assertEq(lineaWorldID.owner(), OWNER); + assertEq(address(lineaWorldID.messageService()), messageServiceAddress); + assertEq(lineaWorldID.isLocal(), true); + } + + /// @notice Tests that the owner can successfully receive a new root + /// @param newRoot The new root to be received + function test_owner_receiveRoot_succeeds(uint256 newRoot) public { + vm.assume(newRoot != 0); + + vm.expectEmit(true, true, true, true); + emit RootAdded(newRoot, uint128(block.timestamp)); + + vm.prank(OWNER); + lineaWorldID.receiveRoot(newRoot); + + assertEq(lineaWorldID.latestRoot(), newRoot); + } + + /// @notice Tests that the owner can successfully set the root history expiry + /// @param newExpiry The new expiry time for the root history + function test_owner_setRootHistoryExpiry_succeeds(uint256 newExpiry) public { + vm.expectEmit(true, true, true, true); + emit RootHistoryExpirySet(newExpiry); + + vm.prank(OWNER); + lineaWorldID.setRootHistoryExpiry(newExpiry); + + assertEq(lineaWorldID.rootHistoryExpiry(), newExpiry); + } + + /// @notice Tests that proof verification succeeds with valid inputs + /// @param root The root to verify against + /// @param signalHash The hash of the signal + /// @param nullifierHash The nullifier hash + /// @param externalNullifierHash The external nullifier hash + /// @param proof The zero-knowledge proof + function test_verifyProof_succeeds( + uint256 root, + uint256 signalHash, + uint256 nullifierHash, + uint256 externalNullifierHash, + uint256[8] memory proof + ) + public + { + vm.prank(OWNER); + lineaWorldID.receiveRoot(root); + + vm.mockCall( + address(lineaWorldID), + abi.encodeWithSignature( + "verifyProof(uint256,uint256,uint256,uint256,uint256[8])", + root, + signalHash, + nullifierHash, + externalNullifierHash, + proof + ), + abi.encode() + ); + + lineaWorldID.verifyProof(root, signalHash, nullifierHash, externalNullifierHash, proof); + } + + /// @notice Tests that ownership transfer succeeds when called by the owner + /// @param newOwner The address of the new owner + /// @param newIsLocal Boolean indicating if the new owner is local + function test_owner_transferOwnership_succeeds(address newOwner, bool newIsLocal) public { + vm.assume(newOwner != address(0)); + + vm.expectEmit(true, true, true, true); + emit OwnershipTransferred(OWNER, newOwner, newIsLocal); + + vm.prank(OWNER); + lineaWorldID.transferOwnership(newOwner, newIsLocal); + + assertEq(lineaWorldID.owner(), newOwner); + assertEq(lineaWorldID.isLocal(), newIsLocal); + } + + /////////////////////////////////////////////////////////////////// + /// REVERTS /// + /////////////////////////////////////////////////////////////////// + + /// @notice Tests that receiving a root reverts when called by a non-owner + /// @param nonOwner An address that is not the owner + /// @param newRoot The new root to be received + function test_notOwner_receiveRoot_reverts(address nonOwner, uint256 newRoot) public { + vm.assume(nonOwner != OWNER && nonOwner != address(0)); + + vm.expectRevert(CallerIsNotOwner.selector); + + vm.prank(nonOwner); + lineaWorldID.receiveRoot(newRoot); + } + + /// @notice Tests that setting root history expiry reverts when called by a non-owner + /// @param nonOwner An address that is not the owner + /// @param newExpiry The new expiry time for the root history + function test_notOwner_setRootHistoryExpiry_reverts(address nonOwner, uint256 newExpiry) public { + vm.assume(nonOwner != OWNER && nonOwner != address(0)); + + vm.expectRevert(CallerIsNotOwner.selector); + + vm.prank(nonOwner); + lineaWorldID.setRootHistoryExpiry(newExpiry); + } + + /// @notice Tests that receiving the same root twice reverts + /// @param newRoot The root to be received + function test_owner_receiveRootOverwrite_reverts(uint256 newRoot) public { + vm.assume(newRoot != 0); + + vm.startPrank(OWNER); + lineaWorldID.receiveRoot(newRoot); + + vm.expectRevert(CannotOverwriteRoot.selector); + lineaWorldID.receiveRoot(newRoot); + vm.stopPrank(); + } + + /// @notice Tests that verifying a proof with an invalid root reverts + /// @param root An invalid root + /// @param signalHash The hash of the signal + /// @param nullifierHash The nullifier hash + /// @param externalNullifierHash The external nullifier hash + /// @param proof The zero-knowledge proof + function test_verifyProofInvalidRoot_reverts( + uint256 root, + uint256 signalHash, + uint256 nullifierHash, + uint256 externalNullifierHash, + uint256[8] memory proof + ) + public + { + vm.expectRevert(NonExistentRoot.selector); + lineaWorldID.verifyProof(root, signalHash, nullifierHash, externalNullifierHash, proof); + } +} \ No newline at end of file diff --git a/test/LinearStateBridge.t.sol b/test/LinearStateBridge.t.sol index 4ce0405..8ef4a99 100644 --- a/test/LinearStateBridge.t.sol +++ b/test/LinearStateBridge.t.sol @@ -84,10 +84,6 @@ contract LineaStateBridgeTest is PRBTest, StdCheats { /// @param _lineaFee The new fee for setMessageService event SetFeeSetMessageService(uint256 _lineaFee); - /////////////////////////////////////////////////////////////////// - /// ERRORS /// - /////////////////////////////////////////////////////////////////// - /// @notice Emitted when an attempt is made to renounce ownership. error CannotRenounceOwnership(); From d12eaf978ddd6656da3ff6f6fadf3611e97e98a9 Mon Sep 17 00:00:00 2001 From: seun Date: Thu, 29 Aug 2024 10:00:10 -0700 Subject: [PATCH 4/4] feat:remapping text --- remappings.txt | 2 ++ test/LineaWorldID.t.sol | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/remappings.txt b/remappings.txt index c59a627..43cfc8c 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,3 +1,5 @@ +@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ +@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/ forge-std/=node_modules/forge-std/src/ world-id-state-bridge/=node_modules/@worldcoin/world-id-state-bridge/src/ linea-contracts/=node_modules/linea-contracts/contracts/ diff --git a/test/LineaWorldID.t.sol b/test/LineaWorldID.t.sol index 65cb4ee..e3cfe59 100644 --- a/test/LineaWorldID.t.sol +++ b/test/LineaWorldID.t.sol @@ -215,4 +215,4 @@ contract LineaWorldIDTest is PRBTest, StdCheats { vm.expectRevert(NonExistentRoot.selector); lineaWorldID.verifyProof(root, signalHash, nullifierHash, externalNullifierHash, proof); } -} \ No newline at end of file +}