diff --git a/package.json b/package.json index 125c4be..fc84a51 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "scripts": { "build": "yarn build:docs && yarn build:website && yarn build:packages", "build:packages": "yarn workspaces foreach -Apt --no-private run build", - "build:docs": "yarn workspace excubiae-docs build", + "build:docs": "yarn workspace excubiae-docs build && yarn workspace excubiae-contracts docs:forge", "build:website": "yarn workspace excubiae-website build", "compile:contracts": "yarn workspace excubiae-contracts compile", "test": "yarn test:contracts", diff --git a/packages/contracts/.gitignore b/packages/contracts/.gitignore index ec7554d..c908634 100644 --- a/packages/contracts/.gitignore +++ b/packages/contracts/.gitignore @@ -17,6 +17,7 @@ node_modules # solidity-coverage files /coverage /coverage.json +lcov.info # Hardhat Ignition ignition/deployments/chain-* diff --git a/packages/contracts/.solhint.json b/packages/contracts/.solhint.json index 27252e4..c9e5664 100644 --- a/packages/contracts/.solhint.json +++ b/packages/contracts/.solhint.json @@ -10,6 +10,7 @@ "func-visibility": ["error", { "ignoreConstructors": true }], "max-line-length": ["error", 120], "not-rely-on-time": "off", + "func-name-mixedcase": "off", "reason-string": ["warn", { "maxLength": 80 }] } } diff --git a/packages/contracts/contracts/src/Lock.sol b/packages/contracts/contracts/src/Lock.sol deleted file mode 100644 index ca5c91c..0000000 --- a/packages/contracts/contracts/src/Lock.sol +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.27; - -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; - -// Uncomment this line to use console.log -// import "hardhat/console.sol"; - -contract Lock is Ownable(msg.sender) { - uint256 public unlockTime; - - event Withdrawal(uint256 amount, uint256 when); - - constructor(uint256 _unlockTime) payable { - require(block.timestamp < _unlockTime, "Unlock time should be in the future"); - - unlockTime = _unlockTime; - } - - function withdraw() public onlyOwner { - // Uncomment this line, and the import of "hardhat/console.sol", to print a log in your terminal - // console.log("Unlock time is %o and block timestamp is %o", unlockTime, block.timestamp); - require(block.timestamp >= unlockTime, "You can't withdraw yet"); - - emit Withdrawal(address(this).balance, block.timestamp); - - payable(owner()).transfer(address(this).balance); - } -} diff --git a/packages/contracts/contracts/src/core/Excubia.sol b/packages/contracts/contracts/src/core/Excubia.sol new file mode 100644 index 0000000..697c812 --- /dev/null +++ b/packages/contracts/contracts/src/core/Excubia.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IExcubia} from "./IExcubia.sol"; + +/// @title Excubia. +/// @notice Abstract base contract which can be extended to implement a specific `Excubia`. +/// @dev Inherit from this contract and implement the `_pass` & `_check` methods to define +/// your custom gatekeeping logic. +abstract contract Excubia is IExcubia, Ownable(msg.sender) { + /// @notice The Excubia-protected contract address. + /// @dev The gate can be any contract address that requires a prior check to enable logic. + /// For example, the gate is a Semaphore group that requires the passerby + /// to meet certain criteria before joining. + address public gate; + + /// @dev Modifier to restrict function calls to only from the `gate` address. + modifier onlyGate() { + if (msg.sender != gate) revert GateOnly(); + _; + } + + /// @inheritdoc IExcubia + function trait() external pure virtual returns (string memory) { + return _trait(); + } + + /// @inheritdoc IExcubia + function setGate(address _gate) public virtual onlyOwner { + if (_gate == address(0)) revert ZeroAddress(); + if (gate != address(0)) revert GateAlreadySet(); + + gate = _gate; + + emit GateSet(_gate); + } + + /// @inheritdoc IExcubia + function pass(address passerby, bytes calldata data) external onlyGate { + _pass(passerby, data); + } + + /// @inheritdoc IExcubia + function check(address passerby, bytes calldata data) external view { + _check(passerby, data); + } + + /// @notice Internal function to define the trait of the Excubia contract. + /// @dev maintain consistency across `_pass` & `_check` definitions. + /// @return The specific trait of the Excubia contract (e.g., SemaphoreExcubia has trait Semaphore). + function _trait() internal pure virtual returns (string memory) {} + + /// @notice Internal function to enforce the custom `gate` passing logic. + /// @dev Calls the `_check` internal logic and emits the relative event if successful. + /// @param passerby The address of the entity attempting to pass the `gate`. + /// @param data Additional data required for the check (e.g., encoded token identifier). + function _pass(address passerby, bytes calldata data) internal virtual { + _check(passerby, data); + + emit GatePassed(passerby, gate); + } + + /// @notice Internal function to define the custom `gate` protection logic. + /// @dev Custom logic to determine if the passerby can pass the `gate`. + /// @param passerby The address of the entity attempting to pass the `gate`. + /// @param data Additional data that may be required for the check. + function _check(address passerby, bytes calldata data) internal view virtual {} +} diff --git a/packages/contracts/contracts/src/core/IExcubia.sol b/packages/contracts/contracts/src/core/IExcubia.sol new file mode 100644 index 0000000..269b4fc --- /dev/null +++ b/packages/contracts/contracts/src/core/IExcubia.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +/// @title IExcubia. +/// @notice Excubia contract interface. +interface IExcubia { + /// @notice Event emitted when someone passes the `gate` check. + /// @param passerby The address of those who have successfully passed the check. + /// @param gate The address of the excubia-protected contract address. + event GatePassed(address indexed passerby, address indexed gate); + + /// @notice Event emitted when the `gate` address is set. + /// @param gate The address of the contract set as the `gate`. + event GateSet(address indexed gate); + + /// @notice Error thrown when an address equal to zero is given. + error ZeroAddress(); + + /// @notice Error thrown when the `gate` address is not set. + error GateNotSet(); + + /// @notice Error thrown when the callee is not the `gate` contract. + error GateOnly(); + + /// @notice Error thrown when the `gate` address has been already set. + error GateAlreadySet(); + + /// @notice Error thrown when the passerby has already passed the `gate`. + error AlreadyPassed(); + + /// @notice Gets the trait of the Excubia contract. + /// @return The specific trait of the Excubia contract (e.g., SemaphoreExcubia has trait Semaphore). + function trait() external pure returns (string memory); + + /// @notice Sets the gate address. + /// @dev Only the owner can set the destination `gate` address. + /// @param _gate The address of the contract to be set as the gate. + function setGate(address _gate) external; + + /// @notice Enforces the custom `gate` passing logic. + /// @dev Must call the `check` to handle the logic of checking passerby for specific `gate`. + /// @param passerby The address of the entity attempting to pass the `gate`. + /// @param data Additional data required for the check (e.g., encoded token identifier). + function pass(address passerby, bytes calldata data) external; + + /// @dev Defines the custom `gate` protection logic. + /// @param passerby The address of the entity attempting to pass the `gate`. + /// @param data Additional data that may be required for the check. + function check(address passerby, bytes calldata data) external view; +} diff --git a/packages/contracts/contracts/src/extensions/FreeForAllExcubia.sol b/packages/contracts/contracts/src/extensions/FreeForAllExcubia.sol new file mode 100644 index 0000000..0d84eef --- /dev/null +++ b/packages/contracts/contracts/src/extensions/FreeForAllExcubia.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {Excubia} from "../core/Excubia.sol"; + +/// @title FreeForAll Excubia Contract. +/// @notice This contract extends the `Excubia` contract to allow free access through the `gate`. +/// This contract does not perform any checks and allows any `passerby` to pass the `gate`. +/// @dev The contract overrides the `_check` function to always return true. +contract FreeForAllExcubia is Excubia { + /// @notice Constructor for the FreeForAllExcubia contract. + constructor() {} + + /// @notice Mapping to track already passed passersby. + mapping(address => bool) public passedPassersby; + + /// @notice The trait of the `Excubia` contract. + function _trait() internal pure override returns (string memory) { + super._trait(); + + return "FreeForAll"; + } + + /// @notice Internal function to handle the `gate` passing logic. + /// @dev This function calls the parent `_pass` function and then tracks the `passerby`. + /// @param passerby The address of the entity passing the `gate`. + /// @param data Additional data required for the pass (not used in this implementation). + function _pass(address passerby, bytes calldata data) internal override { + // Avoiding passing the `gate` twice with the same address. + if (passedPassersby[passerby]) revert AlreadyPassed(); + + passedPassersby[passerby] = true; + + super._pass(passerby, data); + } + + /// @notice Internal function to handle the `gate` protection logic. + /// @dev This function always returns true, signaling that any `passerby` is able to pass the `gate`. + /// @param passerby The address of the entity attempting to pass the `gate`. + /// @param data Additional data required for the check (e.g., encoded attestation ID). + function _check(address passerby, bytes calldata data) internal view override { + super._check(passerby, data); + } +} diff --git a/packages/contracts/contracts/test/FreeForAllExcubia.t.sol b/packages/contracts/contracts/test/FreeForAllExcubia.t.sol new file mode 100644 index 0000000..572ca50 --- /dev/null +++ b/packages/contracts/contracts/test/FreeForAllExcubia.t.sol @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {Test} from "forge-std/src/Test.sol"; +import {FreeForAllExcubia} from "../src/extensions/FreeForAllExcubia.sol"; +import {FreeForAllExcubiaHarness} from "./wrappers/FreeForAllExcubiaHarness.sol"; + +contract FreeForAllExcubiaTest is Test { + FreeForAllExcubia internal freeForAllExcubia; + FreeForAllExcubiaHarness internal freeForAllExcubiaHarness; + + address public deployer = vm.addr(0x1); + address public gate = vm.addr(0x2); + address public passerby = vm.addr(0x3); + + event GateSet(address indexed gate); + event GatePassed(address indexed passerby, address indexed gate); + + error OwnableUnauthorizedAccount(address); + error ZeroAddress(); + error GateNotSet(); + error GateOnly(); + error GateAlreadySet(); + error AlreadyPassed(); + + function setUp() public virtual { + vm.startPrank(deployer); + + freeForAllExcubia = new FreeForAllExcubia(); + + freeForAllExcubiaHarness = new FreeForAllExcubiaHarness(); + freeForAllExcubiaHarness.setGate(gate); + + vm.stopPrank(); + } + + function test_trait_Internal() public view { + freeForAllExcubiaHarness.exposed__trait(); + } + + function test_trait_Match() external view { + assertEq(freeForAllExcubia.trait(), "FreeForAll"); + } + + function invariant_trait_AlwaysFreeForAll() public view { + assertEq( + freeForAllExcubia.trait(), + "FreeForAll", + "Invariant violated: the trait must match 'FreeForAll' string" + ); + } + + function test_setGate() external { + vm.expectEmit(true, true, false, false); + emit GateSet(gate); + + vm.prank(deployer); + freeForAllExcubia.setGate(gate); + } + + function test_setGate_RevertWhen_CallerIsNotOwner() external { + vm.expectRevert(abi.encodeWithSelector(OwnableUnauthorizedAccount.selector, address(0))); + vm.prank(address(0)); + freeForAllExcubia.setGate(gate); + } + + function test_setGate_RevertWhen_WithZeroAddress() external { + vm.expectRevert(ZeroAddress.selector); + vm.prank(deployer); + freeForAllExcubia.setGate(address(0)); + } + + function test_setGate_RevertWhen_AlreadySet() external { + vm.startPrank(deployer); + freeForAllExcubia.setGate(gate); + + vm.expectRevert(GateAlreadySet.selector); + freeForAllExcubia.setGate(gate); + + vm.stopPrank(); + } + + function testFuzz_setGate_Addresses(address theGate) public { + vm.assume(theGate != address(0)); + + vm.prank(deployer); + freeForAllExcubia.setGate(theGate); + + assertEq(freeForAllExcubia.gate(), theGate); + assert(freeForAllExcubia.gate() != address(0)); + assertEq(freeForAllExcubia.owner(), deployer); + } + + function testFuzz_setGate_RevertWhen_NotOwnerAddresses(address notOwner) public { + vm.assume(notOwner != deployer); + + vm.prank(notOwner); + vm.expectRevert(abi.encodeWithSelector(OwnableUnauthorizedAccount.selector, address(notOwner))); + freeForAllExcubia.setGate(gate); + + assertEq(freeForAllExcubia.gate(), address(0)); + assertEq(freeForAllExcubia.owner(), deployer); + } + + function test_pass() external { + vm.prank(deployer); + freeForAllExcubia.setGate(gate); + + vm.expectEmit(true, true, false, false); + emit GatePassed(passerby, gate); + + vm.prank(gate); + freeForAllExcubia.pass(passerby, "0x"); + + assertTrue(freeForAllExcubia.passedPassersby(passerby)); + } + + function test_pass_GateCanSelfPass() public { + vm.prank(deployer); + freeForAllExcubia.setGate(gate); + + vm.prank(gate); + freeForAllExcubia.pass(gate, "0x"); + + assertTrue(freeForAllExcubia.passedPassersby(gate)); + } + + function test_pass_Internal() public { + vm.expectEmit(true, true, false, false); + emit GatePassed(passerby, gate); + + freeForAllExcubiaHarness.exposed__pass(passerby, ""); + + assertTrue(freeForAllExcubiaHarness.passedPassersby(passerby)); + } + + function testGas_pass() public { + vm.prank(deployer); + freeForAllExcubia.setGate(gate); + + vm.prank(gate); + uint256 gasBefore = gasleft(); + + freeForAllExcubia.pass(passerby, "0x"); + + uint256 gasAfter = gasleft(); + uint256 gasUsed = gasBefore - gasAfter; + assert(gasUsed < 70_000); + } + + function test_pass_RevertWhen_GateNotSet() external { + vm.prank(gate); + vm.expectRevert(GateOnly.selector); + freeForAllExcubia.pass(passerby, ""); + + assertEq(freeForAllExcubia.gate(), address(0)); + } + + function test_pass_RevertWhen_NotGate() external { + vm.prank(deployer); + freeForAllExcubia.setGate(gate); + + vm.expectRevert(GateOnly.selector); + freeForAllExcubia.pass(passerby, ""); + + assert(freeForAllExcubia.gate() != address(0)); + } + + function test_pass_RevertIf_PassTwice() external { + vm.prank(deployer); + freeForAllExcubia.setGate(gate); + + vm.startPrank(gate); + freeForAllExcubia.pass(passerby, "0x"); + + vm.expectRevert(AlreadyPassed.selector); + freeForAllExcubia.pass(passerby, "0x"); + + vm.stopPrank(); + } + + function testFuzz_pass_AndCheck(address thePasserby, bytes calldata data) public { + vm.prank(deployer); + freeForAllExcubia.setGate(gate); + + vm.prank(gate); + freeForAllExcubia.pass(thePasserby, data); + + assertTrue(freeForAllExcubia.passedPassersby(thePasserby)); + assertEq(freeForAllExcubia.trait(), "FreeForAll"); + } + + function testFuzz_pass_Internal(address randomPasserby, bytes calldata randomData) public { + vm.expectEmit(true, true, false, false); + emit GatePassed(randomPasserby, gate); + + freeForAllExcubiaHarness.exposed__pass(randomPasserby, randomData); + + assertTrue(freeForAllExcubiaHarness.passedPassersby(randomPasserby)); + vm.expectRevert(AlreadyPassed.selector); + freeForAllExcubiaHarness.exposed__pass(randomPasserby, randomData); + } + + function testFuzz_pass_RevertWhen_AlreadyPassedAddresses(address thePasserby) public { + vm.prank(deployer); + freeForAllExcubia.setGate(gate); + + vm.startPrank(gate); + freeForAllExcubia.pass(thePasserby, "0x"); + + vm.expectRevert(AlreadyPassed.selector); + freeForAllExcubia.pass(thePasserby, "0x"); + + vm.stopPrank(); + + assertTrue(freeForAllExcubia.passedPassersby(thePasserby)); + assertEq(freeForAllExcubia.trait(), "FreeForAll"); + } + + function test_check_Internal() public view { + freeForAllExcubiaHarness.exposed__check(passerby, ""); + } + + function testFuzz_check_Addresses(address thePasserby, bytes calldata data) public { + vm.prank(deployer); + freeForAllExcubia.setGate(gate); + + freeForAllExcubia.check(thePasserby, data); + + vm.prank(gate); + freeForAllExcubia.pass(thePasserby, data); + + freeForAllExcubia.check(thePasserby, data); + } + + function testFuzz_check_Internal(address randomPasserby, bytes calldata randomData) public view { + freeForAllExcubiaHarness.exposed__check(randomPasserby, randomData); + } +} diff --git a/packages/contracts/contracts/test/Lock.t.sol b/packages/contracts/contracts/test/Lock.t.sol deleted file mode 100644 index ff3fcbd..0000000 --- a/packages/contracts/contracts/test/Lock.t.sol +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.25 <0.9.0; - -import {Test} from "forge-std/src/Test.sol"; -import {console2} from "forge-std/src/console2.sol"; - -import {Lock} from "../src/Lock.sol"; - -contract LockTest is Test { - Lock internal lock; - - function setUp() public virtual { - // Set unlock time to 1 hour from now - lock = new Lock{value: 1 ether}(block.timestamp + 1 hours); - } - - function test_CannotWithdrawYet() external { - // Attempt to withdraw before unlock time - vm.expectRevert("You can't withdraw yet"); - lock.withdraw(); - } -} diff --git a/packages/contracts/contracts/test/wrappers/FreeForAllExcubiaHarness.sol b/packages/contracts/contracts/test/wrappers/FreeForAllExcubiaHarness.sol new file mode 100644 index 0000000..9eb5f2a --- /dev/null +++ b/packages/contracts/contracts/test/wrappers/FreeForAllExcubiaHarness.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {FreeForAllExcubia} from "../../src/extensions/FreeForAllExcubia.sol"; + +// Deploy this contract then call its methods to test FreeForAllExcubia internal methods. +contract FreeForAllExcubiaHarness is FreeForAllExcubia { + function exposed__trait() public pure { + _trait(); + } + + function exposed__check(address passerby, bytes calldata data) public view { + _check(passerby, data); + } + + function exposed__pass(address passerby, bytes calldata data) public { + _pass(passerby, data); + } +} diff --git a/packages/contracts/ignition/modules/FreeForAllExcubia.ts b/packages/contracts/ignition/modules/FreeForAllExcubia.ts new file mode 100644 index 0000000..14914e6 --- /dev/null +++ b/packages/contracts/ignition/modules/FreeForAllExcubia.ts @@ -0,0 +1,9 @@ +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules" + +const FreeForAllExcubiaModule = buildModule("FreeForAllExcubiaModule", (m) => { + const freeForAllExcubia = m.contract("FreeForAllExcubia") + + return { freeForAllExcubia } +}) + +export default FreeForAllExcubiaModule diff --git a/packages/contracts/ignition/modules/Lock.ts b/packages/contracts/ignition/modules/Lock.ts deleted file mode 100644 index 34c4a93..0000000 --- a/packages/contracts/ignition/modules/Lock.ts +++ /dev/null @@ -1,20 +0,0 @@ -// This setup uses Hardhat Ignition to manage smart contract deployments. -// Learn more about it at https://hardhat.org/ignition - -import { buildModule } from "@nomicfoundation/hardhat-ignition/modules" - -const JAN_1ST_2030 = 1893456000 -const ONE_GWEI: bigint = 1_000_000_000n - -const LockModule = buildModule("LockModule", (m) => { - const unlockTime = m.getParameter("unlockTime", JAN_1ST_2030) - const lockedAmount = m.getParameter("lockedAmount", ONE_GWEI) - - const lock = m.contract("Lock", [unlockTime], { - value: lockedAmount - }) - - return { lock } -}) - -export default LockModule diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 56db7de..58dec6b 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -10,14 +10,15 @@ "compile": "yarn compile:hardhat && yarn compile:forge", "compile:hardhat": "hardhat compile", "compile:forge": "forge compile", - "deploy:lock": "hardhat ignition deploy ignition/modules/Lock.ts", - "deploy:lock-sepolia": "hardhat ignition deploy ignition/modules/Lock.ts --network sepolia", - "verify:lock-sepolia": "hardhat ignition verify sepolia-deployment", + "docs:forge": "forge doc", + "deploy:FreeForAllExcubia": "hardhat ignition deploy ignition/modules/FreeForAllExcubia.ts", + "deploy:FreeForAllExcubia-sepolia": "hardhat ignition deploy ignition/modules/FreeForAllExcubia.ts --network sepolia", + "verify:FreeForAllExcubia-sepolia": "hardhat ignition verify chain-11155111", "test": "yarn test:hardhat && yarn test:forge", "test:hardhat": "hardhat test", "test:forge": "forge test -vvv", - "test:report-gas": "REPORT_GAS=true yarn test && forge test --gas-report", - "test:coverage": "hardhat coverage && forge coverage", + "test:report-gas": "REPORT_GAS=true yarn test:hardhat && yarn test:forge --gas-report", + "test:coverage": "hardhat coverage && forge coverage --summary --report lcov", "typechain": "hardhat typechain", "format:forge": "forge fmt", "lint": "solhint 'contracts/**/*.sol'", diff --git a/packages/contracts/test/FreeForAllExcubia.test.ts b/packages/contracts/test/FreeForAllExcubia.test.ts new file mode 100644 index 0000000..f7ca6a1 --- /dev/null +++ b/packages/contracts/test/FreeForAllExcubia.test.ts @@ -0,0 +1,162 @@ +import { expect } from "chai" +import { ethers } from "hardhat" +import { Signer, ZeroAddress, ZeroHash } from "ethers" +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers" +import { FreeForAllExcubia, FreeForAllExcubia__factory } from "../typechain-types" + +describe("FreeForAllExcubia", () => { + async function deployFreeForAllExcubiaFixture() { + const [signer, gate, notOwner]: Signer[] = await ethers.getSigners() + const signerAddress: string = await signer.getAddress() + const gateAddress: string = await gate.getAddress() + const notOwnerAddress: string = await gate.getAddress() + + const FreeForAllExcubiaContract: FreeForAllExcubia__factory = + await ethers.getContractFactory("FreeForAllExcubia") + const freeForAllExcubia: FreeForAllExcubia = await FreeForAllExcubiaContract.deploy() + + // Fixtures can return anything you consider useful for your tests + return { + FreeForAllExcubiaContract, + freeForAllExcubia, + signer, + gate, + notOwner, + signerAddress, + gateAddress, + notOwnerAddress + } + } + + describe("constructor()", () => { + it("Should deploy the FreeForAllExcubia contract correctly", async () => { + const { freeForAllExcubia } = await loadFixture(deployFreeForAllExcubiaFixture) + + expect(freeForAllExcubia).to.not.eq(undefined) + }) + }) + + describe("trait()", () => { + it("should return the trait of the Excubia contract", async () => { + const { freeForAllExcubia } = await loadFixture(deployFreeForAllExcubiaFixture) + + expect(await freeForAllExcubia.trait()).to.be.equal("FreeForAll") + }) + }) + + describe("setGate()", () => { + it("should fail to set the gate when the caller is not the owner", async () => { + const { freeForAllExcubia, notOwner, gateAddress } = await loadFixture(deployFreeForAllExcubiaFixture) + + await expect(freeForAllExcubia.connect(notOwner).setGate(gateAddress)).to.be.revertedWithCustomError( + freeForAllExcubia, + "OwnableUnauthorizedAccount" + ) + }) + + it("should fail to set the gate when the gate address is zero", async () => { + const { freeForAllExcubia } = await loadFixture(deployFreeForAllExcubiaFixture) + + await expect(freeForAllExcubia.setGate(ZeroAddress)).to.be.revertedWithCustomError( + freeForAllExcubia, + "ZeroAddress" + ) + }) + + it("Should set the gate contract address correctly", async () => { + const { FreeForAllExcubiaContract, freeForAllExcubia, gateAddress } = + await loadFixture(deployFreeForAllExcubiaFixture) + + const tx = await freeForAllExcubia.setGate(gateAddress) + const receipt = await tx.wait() + const event = FreeForAllExcubiaContract.interface.parseLog( + receipt?.logs[0] as unknown as { topics: string[]; data: string } + ) as unknown as { + args: { + gate: string + } + } + + expect(receipt?.status).to.eq(1) + expect(event.args.gate).to.eq(gateAddress) + expect(await freeForAllExcubia.gate()).to.eq(gateAddress) + }) + + it("Should fail to set the gate if already set", async () => { + const { freeForAllExcubia, gateAddress } = await loadFixture(deployFreeForAllExcubiaFixture) + + await freeForAllExcubia.setGate(gateAddress) + + await expect(freeForAllExcubia.setGate(gateAddress)).to.be.revertedWithCustomError( + freeForAllExcubia, + "GateAlreadySet" + ) + }) + }) + + describe("check()", () => { + it("should check", async () => { + const { freeForAllExcubia, signerAddress } = await loadFixture(deployFreeForAllExcubiaFixture) + + // `data` parameter value can be whatever (e.g., ZeroHash default). + await expect(freeForAllExcubia.check(signerAddress, ZeroHash)).to.not.be.reverted + + // check does NOT change the state of the contract (see pass()). + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(await freeForAllExcubia.passedPassersby(signerAddress)).to.be.false + }) + }) + + describe("pass()", () => { + it("should throw when the callee is not the gate", async () => { + const { freeForAllExcubia, signer, signerAddress, gateAddress } = + await loadFixture(deployFreeForAllExcubiaFixture) + + await freeForAllExcubia.setGate(gateAddress) + + await expect( + // `data` parameter value can be whatever (e.g., ZeroHash default). + freeForAllExcubia.connect(signer).pass(signerAddress, ZeroHash) + ).to.be.revertedWithCustomError(freeForAllExcubia, "GateOnly") + }) + + it("should pass", async () => { + const { FreeForAllExcubiaContract, freeForAllExcubia, gate, signerAddress, gateAddress } = + await loadFixture(deployFreeForAllExcubiaFixture) + + await freeForAllExcubia.setGate(gateAddress) + + // `data` parameter value can be whatever (e.g., ZeroHash default). + const tx = await freeForAllExcubia.connect(gate).pass(signerAddress, ZeroHash) + const receipt = await tx.wait() + const event = FreeForAllExcubiaContract.interface.parseLog( + receipt?.logs[0] as unknown as { topics: string[]; data: string } + ) as unknown as { + args: { + passerby: string + gate: string + } + } + + expect(receipt?.status).to.eq(1) + expect(event.args.passerby).to.eq(signerAddress) + expect(event.args.gate).to.eq(gateAddress) + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(await freeForAllExcubia.passedPassersby(signerAddress)).to.be.true + }) + + it("should prevent to pass twice", async () => { + const { freeForAllExcubia, gate, signerAddress, gateAddress } = + await loadFixture(deployFreeForAllExcubiaFixture) + + await freeForAllExcubia.setGate(gateAddress) + + await freeForAllExcubia.connect(gate).pass(signerAddress, ZeroHash) + + await expect( + // `data` parameter value can be whatever (e.g., ZeroHash default). + freeForAllExcubia.connect(gate).pass(signerAddress, ZeroHash) + ).to.be.revertedWithCustomError(freeForAllExcubia, "AlreadyPassed") + }) + }) +}) diff --git a/packages/contracts/test/Lock.ts b/packages/contracts/test/Lock.ts deleted file mode 100644 index 61cf3ce..0000000 --- a/packages/contracts/test/Lock.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { time, loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers" -import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs" -import { expect } from "chai" -import hre from "hardhat" - -describe("Lock", () => { - // We define a fixture to reuse the same setup in every test. - // We use loadFixture to run this setup once, snapshot that state, - // and reset Hardhat Network to that snapshot in every test. - async function deployOneYearLockFixture() { - const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60 - const ONE_GWEI = 1_000_000_000 - - const lockedAmount = ONE_GWEI - const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS - - // Contracts are deployed using the first signer/account by default - const [owner, otherAccount] = await hre.ethers.getSigners() - - const Lock = await hre.ethers.getContractFactory("Lock") - const lock = await Lock.deploy(unlockTime, { value: lockedAmount }) - - return { lock, unlockTime, lockedAmount, owner, otherAccount } - } - - describe("Deployment", () => { - it("Should set the right unlockTime", async () => { - const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture) - - expect(await lock.unlockTime()).to.equal(unlockTime) - }) - - it("Should set the right owner", async () => { - const { lock, owner } = await loadFixture(deployOneYearLockFixture) - - expect(await lock.owner()).to.equal(owner.address) - }) - - it("Should receive and store the funds to lock", async () => { - const { lock, lockedAmount } = await loadFixture(deployOneYearLockFixture) - - expect(await hre.ethers.provider.getBalance(lock.target)).to.equal(lockedAmount) - }) - - it("Should fail if the unlockTime is not in the future", async () => { - // We don't use the fixture here because we want a different deployment - const latestTime = await time.latest() - const Lock = await hre.ethers.getContractFactory("Lock") - await expect(Lock.deploy(latestTime, { value: 1 })).to.be.revertedWith( - "Unlock time should be in the future" - ) - }) - }) - - describe("Withdrawals", () => { - describe("Validations", () => { - it("Should revert with the right error if called too soon", async () => { - const { lock } = await loadFixture(deployOneYearLockFixture) - - await expect(lock.withdraw()).to.be.revertedWith("You can't withdraw yet") - }) - - it("Should revert with the right error if called from another account", async () => { - const { lock, unlockTime, otherAccount } = await loadFixture(deployOneYearLockFixture) - - // We can increase the time in Hardhat Network - await time.increaseTo(unlockTime) - - // We use lock.connect() to send a transaction from another account - await expect(lock.connect(otherAccount).withdraw()).to.be.revertedWithCustomError( - lock, - "OwnableUnauthorizedAccount" - ) - }) - - it("Shouldn't fail if the unlockTime has arrived and the owner calls it", async () => { - const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture) - - // Transactions are sent using the first signer by default - await time.increaseTo(unlockTime) - - await expect(lock.withdraw()).not.to.be.reverted - }) - }) - - describe("Events", () => { - it("Should emit an event on withdrawals", async () => { - const { lock, unlockTime, lockedAmount } = await loadFixture(deployOneYearLockFixture) - - await time.increaseTo(unlockTime) - - await expect(lock.withdraw()).to.emit(lock, "Withdrawal").withArgs(lockedAmount, anyValue) // We accept any value as `when` arg - }) - }) - - describe("Transfers", () => { - it("Should transfer the funds to the owner", async () => { - const { lock, unlockTime, lockedAmount, owner } = await loadFixture(deployOneYearLockFixture) - - await time.increaseTo(unlockTime) - - await expect(lock.withdraw()).to.changeEtherBalances([owner, lock], [lockedAmount, -lockedAmount]) - }) - }) - }) -}) diff --git a/scripts/clean-packages.ts b/scripts/clean-packages.ts index c26cad6..fa8fa6e 100755 --- a/scripts/clean-packages.ts +++ b/scripts/clean-packages.ts @@ -14,7 +14,9 @@ const gitIgnored = [ "docs", "out", "coverage.json", - "lib" + "lib", + "lcov.info", + "docs" ] async function main() {