diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..5d4e02a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std + \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index a35361d..38f20f3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,7 +3,7 @@ "editor.defaultFormatter": "NomicFoundation.hardhat-solidity" }, "[toml]": { - "editor.defaultFormatter": "tamasfe.even-better-toml" + "editor.defaultFormatter": "be5invis.toml" }, "solidity.formatter": "forge", "solidity.compileUsingRemoteVersion": "v0.8.15" diff --git a/foundry.toml b/foundry.toml index c48bced..acf2c51 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,55 +1,65 @@ # Full reference https://github.com/foundry-rs/foundry/tree/master/crates/config [profile.default] - auto_detect_solc = false - block_timestamp = 1_680_220_800 # March 31, 2023 at 00:00 GMT - bytecode_hash = "none" - evm_version = "shanghai" - fuzz = { runs = 1_000 } - gas_reports = ["*"] - optimizer = true - optimizer_runs = 10_000 - out = "out" - script = "script" - solc = "0.8.19" - src = "src" - test = "test" + auto_detect_solc = false +block_timestamp = 1_680_220_800 # March 31, 2023 at 00:00 GMT +bytecode_hash = "none" +evm_version = "shanghai" +fuzz = {runs = 1_000} +gas_reports = ["*"] +optimizer = true +optimizer_runs = 10_000 +out = "out" +script = "script" +solc = "0.8.19" +src = "src" +test = "test" + +libs = ["node_modules", "lib"] +remappings = [ + "forge-std/=lib/forge-std/src/", + "@openzeppelin/=node_modules/@openzeppelin/", + "@rari-capital/=node_modules/@rari-capital/", + "hardhat/=node_modules/hardhat/", + "linea-contracts/=node_modules/linea-contracts/contracts/", + "world-id-state-bridge/=node_modules/@worldcoin/world-id-state-bridge/src/", +] [profile.ci] - fuzz = { runs = 10_000 } + fuzz = {runs = 10_000} verbosity = 4 [etherscan] - arbitrum = { key = "${API_KEY_ARBISCAN}" } - avalanche = { key = "${API_KEY_SNOWTRACE}" } - base = { key = "${API_KEY_BASESCAN}" } - bnb_smart_chain = { key = "${API_KEY_BSCSCAN}" } - gnosis_chain = { key = "${API_KEY_GNOSISSCAN}" } - goerli = { key = "${API_KEY_ETHERSCAN}" } - mainnet = { key = "${API_KEY_ETHERSCAN}" } - optimism = { key = "${API_KEY_OPTIMISTIC_ETHERSCAN}" } - polygon = { key = "${API_KEY_POLYGONSCAN}" } - sepolia = { key = "${API_KEY_ETHERSCAN}" } +arbitrum = {key = "${API_KEY_ARBISCAN}"} +avalanche = {key = "${API_KEY_SNOWTRACE}"} +base = {key = "${API_KEY_BASESCAN}"} +bnb_smart_chain = {key = "${API_KEY_BSCSCAN}"} +gnosis_chain = {key = "${API_KEY_GNOSISSCAN}"} +goerli = {key = "${API_KEY_ETHERSCAN}"} +mainnet = {key = "${API_KEY_ETHERSCAN}"} +optimism = {key = "${API_KEY_OPTIMISTIC_ETHERSCAN}"} +polygon = {key = "${API_KEY_POLYGONSCAN}"} +sepolia = {key = "${API_KEY_ETHERSCAN}"} [fmt] - bracket_spacing = true - int_types = "long" - line_length = 120 - multiline_func_header = "all" - number_underscore = "thousands" - quote_style = "double" - tab_width = 4 - wrap_comments = true +bracket_spacing = true +int_types = "long" +line_length = 120 +multiline_func_header = "all" +number_underscore = "thousands" +quote_style = "double" +tab_width = 4 +wrap_comments = true [rpc_endpoints] - arbitrum = "https://arbitrum-mainnet.infura.io/v3/${API_KEY_INFURA}" - avalanche = "https://avalanche-mainnet.infura.io/v3/${API_KEY_INFURA}" - base = "https://mainnet.base.org" - bnb_smart_chain = "https://bsc-dataseed.binance.org" - gnosis_chain = "https://rpc.gnosischain.com" - goerli = "https://goerli.infura.io/v3/${API_KEY_INFURA}" - localhost = "http://localhost:8545" - mainnet = "https://eth-mainnet.g.alchemy.com/v2/${API_KEY_ALCHEMY}" - optimism = "https://optimism-mainnet.infura.io/v3/${API_KEY_INFURA}" - polygon = "https://polygon-mainnet.infura.io/v3/${API_KEY_INFURA}" - sepolia = "https://sepolia.infura.io/v3/${API_KEY_INFURA}" +arbitrum = "https://arbitrum-mainnet.infura.io/v3/${API_KEY_INFURA}" +avalanche = "https://avalanche-mainnet.infura.io/v3/${API_KEY_INFURA}" +base = "https://mainnet.base.org" +bnb_smart_chain = "https://bsc-dataseed.binance.org" +gnosis_chain = "https://rpc.gnosischain.com" +goerli = "https://goerli.infura.io/v3/${API_KEY_INFURA}" +localhost = "http://localhost:8545" +mainnet = "https://eth-mainnet.g.alchemy.com/v2/${API_KEY_ALCHEMY}" +optimism = "https://optimism-mainnet.infura.io/v3/${API_KEY_INFURA}" +polygon = "https://polygon-mainnet.infura.io/v3/${API_KEY_INFURA}" +sepolia = "https://sepolia.infura.io/v3/${API_KEY_INFURA}" \ No newline at end of file diff --git a/remappings.txt b/remappings.txt index ebf0402..f8c8301 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,5 +1,8 @@ -@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ -@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable -forge-std/=node_modules/forge-std/ -world-id-state-bridge=node_modules/@worldcoin/world-id-state-bridge/src -linea-contracts/=node_modules/linea-contracts/contracts +forge-std/=lib/forge-std/src/ +@openzeppelin/=node_modules/@openzeppelin/ +@rari-capital/=node_modules/@rari-capital/ +hardhat/=node_modules/hardhat/ +linea-contracts/=node_modules/linea-contracts/contracts/ +world-id-state-bridge/=node_modules/@worldcoin/world-id-state-bridge/src/ +@eth-optimism/=node_modules/@eth-optimism/ +@worldcoin/=node_modules/@worldcoin/ \ No newline at end of file diff --git a/test/LinearWorldID.t.sol b/test/LinearWorldID.t.sol new file mode 100644 index 0000000..e8d016b --- /dev/null +++ b/test/LinearWorldID.t.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import "forge-std/Test.sol"; +import "../src/LineaWorldID.sol"; +import "../src/interfaces/IMessageService.sol"; + +contract MockMessageService is IMessageService { + address public senderAddress; + + function setSender(address _sender) external { + senderAddress = _sender; + } + + function sender() external view returns (address) { + return senderAddress; + } + + function sendMessage(address, uint256, bytes calldata) external payable {} + function claimMessage(address, address, uint256, uint256, address payable, bytes calldata, uint256) external {} +} + +contract LineaWorldIDTest is Test { + LineaWorldID public lineaWorldID; + MockMessageService public mockMessageService; + uint8 constant TREE_DEPTH = 30; + address constant OWNER = address(0x1234); + + // Error selectors + bytes4 constant CallerIsNotOwner = bytes4(keccak256("Ownable: caller is not the owner")); + bytes4 constant NonExistentRoot = bytes4(keccak256("NonExistentRoot()")); + bytes4 constant CannotOverwriteRoot = bytes4(keccak256("CannotOverwriteRoot()")); + + event RootAdded(uint256 root, uint128 timestamp); + event RootHistoryExpirySet(uint256 newExpiry); + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner, bool isLocal); + event MessageServiceUpdated(address indexed oldMessageService, address indexed newMessageService); + + function setUp() public { + mockMessageService = new MockMessageService(); + lineaWorldID = new LineaWorldID(TREE_DEPTH, address(mockMessageService)); + lineaWorldID.transferOwnership(OWNER, true); + } + + function testInitialState() public { + assertEq(lineaWorldID.getTreeDepth(), TREE_DEPTH); + assertEq(lineaWorldID.owner(), OWNER); + assertEq(address(lineaWorldID.messageService()), address(mockMessageService)); + assertEq(lineaWorldID.isLocal(), true); + } + + function testReceiveRoot() public { + uint256 newRoot = 3 weeks; + + vm.prank(OWNER); + vm.expectEmit(true, true, true, true); + emit RootAdded(newRoot, uint128(block.timestamp)); + lineaWorldID.receiveRoot(newRoot); + + assertEq(lineaWorldID.latestRoot(), newRoot); + } + + function testReceiveRootNonOwner() public { + uint256 newRoot = 3 weeks; + + vm.expectRevert(CallerIsNotOwner); + lineaWorldID.receiveRoot(newRoot); + } + + function testSetRootHistoryExpiry() public { + uint256 newExpiry = 2 weeks; + + vm.prank(OWNER); + vm.expectEmit(true, true, true, true); + emit RootHistoryExpirySet(newExpiry); + lineaWorldID.setRootHistoryExpiry(newExpiry); + + assertEq(lineaWorldID.rootHistoryExpiry(), newExpiry); + } + + function testSetRootHistoryExpiryNonOwner() public { + uint256 newExpiry = 2 weeks; + + vm.expectRevert(CallerIsNotOwner); + lineaWorldID.setRootHistoryExpiry(newExpiry); + } + + function testVerifyProof() public { + uint256 root = 2 weeks; + uint256 signalHash = 987654321; + uint256 nullifierHash = 135792468; + uint256 externalNullifierHash = 246813579; + uint256[8] memory proof; + + 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); + } + + function testReceiveRootOverwrite() public { + uint256 newRoot = uint256(keccak256("newRoot")); + + vm.startPrank(OWNER); + lineaWorldID.receiveRoot(newRoot); + vm.expectRevert(CannotOverwriteRoot); + lineaWorldID.receiveRoot(newRoot); + vm.stopPrank(); + } + + function testVerifyProofInvalidRoot() public { + uint256 root = 2 weeks; + uint256 signalHash = 987654321; + uint256 nullifierHash = 135792468; + uint256 externalNullifierHash = 246813579; + uint256[8] memory proof; + + vm.expectRevert(NonExistentRoot); + lineaWorldID.verifyProof(root, signalHash, nullifierHash, externalNullifierHash, proof); + } + + function testTransferOwnership() public { + address newOwner = address(0x5678); + bool newIsLocal = false; + + vm.prank(OWNER); + vm.expectEmit(true, true, true, true); + emit OwnershipTransferred(OWNER, newOwner, newIsLocal); + lineaWorldID.transferOwnership(newOwner, newIsLocal); + + assertEq(lineaWorldID.owner(), newOwner); + assertEq(lineaWorldID.isLocal(), newIsLocal); + } + + function testSetMessagingService() public { + address newMessageService = address(0x9876); + + vm.prank(OWNER); + vm.expectEmit(true, true, true, true); + emit MessageServiceUpdated(address(mockMessageService), newMessageService); + lineaWorldID.setMessagingService(newMessageService); + + assertEq(address(lineaWorldID.messageService()), newMessageService); + } +} \ 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; - } -}