From 9a6d052f07d0e606fa9d671f57b6e77c29e9ea22 Mon Sep 17 00:00:00 2001 From: Giacomo Date: Tue, 26 Nov 2024 18:39:24 +0100 Subject: [PATCH] test(contracts): add example implementation and tests for BaseChecker contract --- packages/contracts/.gitignore | 3 +- packages/contracts/contracts/package.json | 4 +- .../src/{core => }/AdvancedChecker.sol | 0 .../src/{core => }/AdvancedPolicy.sol | 0 .../contracts/src/{core => }/BaseChecker.sol | 0 .../contracts/src/{core => }/BasePolicy.sol | 0 .../contracts/src/{core => }/Policy.sol | 0 .../interfaces/IAdvancedChecker.sol | 0 .../{core => }/interfaces/IAdvancedPolicy.sol | 0 .../{core => }/interfaces/IBaseChecker.sol | 0 .../src/{core => }/interfaces/IBasePolicy.sol | 0 .../src/{core => }/interfaces/IPolicy.sol | 0 .../contracts/src/test/BaseERC721Checker.sol | 21 +++++ .../contracts/src/test/BaseERC721Policy.sol | 17 ++++ .../contracts/src/test/BaseVoting.sol | 44 ++++++++++ packages/contracts/contracts/src/test/NFT.sol | 15 ++++ packages/contracts/contracts/test/Base.t.sol | 87 +++++++++++++++++++ .../contracts/test/wrappers/.gitkeep | 0 .../wrappers/BaseERC721CheckerHarness.sol | 18 ++++ packages/contracts/hardhat.config.ts | 2 +- .../test => ignition/modules}/.gitkeep | 0 packages/contracts/test/.gitkeep | 0 packages/contracts/test/Base.test.ts | 70 +++++++++++++++ 23 files changed, 278 insertions(+), 3 deletions(-) rename packages/contracts/contracts/src/{core => }/AdvancedChecker.sol (100%) rename packages/contracts/contracts/src/{core => }/AdvancedPolicy.sol (100%) rename packages/contracts/contracts/src/{core => }/BaseChecker.sol (100%) rename packages/contracts/contracts/src/{core => }/BasePolicy.sol (100%) rename packages/contracts/contracts/src/{core => }/Policy.sol (100%) rename packages/contracts/contracts/src/{core => }/interfaces/IAdvancedChecker.sol (100%) rename packages/contracts/contracts/src/{core => }/interfaces/IAdvancedPolicy.sol (100%) rename packages/contracts/contracts/src/{core => }/interfaces/IBaseChecker.sol (100%) rename packages/contracts/contracts/src/{core => }/interfaces/IBasePolicy.sol (100%) rename packages/contracts/contracts/src/{core => }/interfaces/IPolicy.sol (100%) create mode 100644 packages/contracts/contracts/src/test/BaseERC721Checker.sol create mode 100644 packages/contracts/contracts/src/test/BaseERC721Policy.sol create mode 100644 packages/contracts/contracts/src/test/BaseVoting.sol create mode 100644 packages/contracts/contracts/src/test/NFT.sol create mode 100644 packages/contracts/contracts/test/Base.t.sol delete mode 100644 packages/contracts/contracts/test/wrappers/.gitkeep create mode 100644 packages/contracts/contracts/test/wrappers/BaseERC721CheckerHarness.sol rename packages/contracts/{contracts/test => ignition/modules}/.gitkeep (100%) delete mode 100644 packages/contracts/test/.gitkeep create mode 100644 packages/contracts/test/Base.test.ts diff --git a/packages/contracts/.gitignore b/packages/contracts/.gitignore index c908634..b178472 100644 --- a/packages/contracts/.gitignore +++ b/packages/contracts/.gitignore @@ -1,8 +1,9 @@ node_modules .env +/cache # Hardhat files -/cache +/cache-hh /artifacts # Forge files diff --git a/packages/contracts/contracts/package.json b/packages/contracts/contracts/package.json index 56c069c..33efd30 100644 --- a/packages/contracts/contracts/package.json +++ b/packages/contracts/contracts/package.json @@ -5,7 +5,9 @@ "license": "MIT", "files": [ "*.sol", - "**/*.sol" + "!test/*", + "README.md", + "LICENSE" ], "keywords": [ "blockchain", diff --git a/packages/contracts/contracts/src/core/AdvancedChecker.sol b/packages/contracts/contracts/src/AdvancedChecker.sol similarity index 100% rename from packages/contracts/contracts/src/core/AdvancedChecker.sol rename to packages/contracts/contracts/src/AdvancedChecker.sol diff --git a/packages/contracts/contracts/src/core/AdvancedPolicy.sol b/packages/contracts/contracts/src/AdvancedPolicy.sol similarity index 100% rename from packages/contracts/contracts/src/core/AdvancedPolicy.sol rename to packages/contracts/contracts/src/AdvancedPolicy.sol diff --git a/packages/contracts/contracts/src/core/BaseChecker.sol b/packages/contracts/contracts/src/BaseChecker.sol similarity index 100% rename from packages/contracts/contracts/src/core/BaseChecker.sol rename to packages/contracts/contracts/src/BaseChecker.sol diff --git a/packages/contracts/contracts/src/core/BasePolicy.sol b/packages/contracts/contracts/src/BasePolicy.sol similarity index 100% rename from packages/contracts/contracts/src/core/BasePolicy.sol rename to packages/contracts/contracts/src/BasePolicy.sol diff --git a/packages/contracts/contracts/src/core/Policy.sol b/packages/contracts/contracts/src/Policy.sol similarity index 100% rename from packages/contracts/contracts/src/core/Policy.sol rename to packages/contracts/contracts/src/Policy.sol diff --git a/packages/contracts/contracts/src/core/interfaces/IAdvancedChecker.sol b/packages/contracts/contracts/src/interfaces/IAdvancedChecker.sol similarity index 100% rename from packages/contracts/contracts/src/core/interfaces/IAdvancedChecker.sol rename to packages/contracts/contracts/src/interfaces/IAdvancedChecker.sol diff --git a/packages/contracts/contracts/src/core/interfaces/IAdvancedPolicy.sol b/packages/contracts/contracts/src/interfaces/IAdvancedPolicy.sol similarity index 100% rename from packages/contracts/contracts/src/core/interfaces/IAdvancedPolicy.sol rename to packages/contracts/contracts/src/interfaces/IAdvancedPolicy.sol diff --git a/packages/contracts/contracts/src/core/interfaces/IBaseChecker.sol b/packages/contracts/contracts/src/interfaces/IBaseChecker.sol similarity index 100% rename from packages/contracts/contracts/src/core/interfaces/IBaseChecker.sol rename to packages/contracts/contracts/src/interfaces/IBaseChecker.sol diff --git a/packages/contracts/contracts/src/core/interfaces/IBasePolicy.sol b/packages/contracts/contracts/src/interfaces/IBasePolicy.sol similarity index 100% rename from packages/contracts/contracts/src/core/interfaces/IBasePolicy.sol rename to packages/contracts/contracts/src/interfaces/IBasePolicy.sol diff --git a/packages/contracts/contracts/src/core/interfaces/IPolicy.sol b/packages/contracts/contracts/src/interfaces/IPolicy.sol similarity index 100% rename from packages/contracts/contracts/src/core/interfaces/IPolicy.sol rename to packages/contracts/contracts/src/interfaces/IPolicy.sol diff --git a/packages/contracts/contracts/src/test/BaseERC721Checker.sol b/packages/contracts/contracts/src/test/BaseERC721Checker.sol new file mode 100644 index 0000000..4d438ea --- /dev/null +++ b/packages/contracts/contracts/src/test/BaseERC721Checker.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {BaseChecker} from "../../src/BaseChecker.sol"; +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +contract BaseERC721Checker is BaseChecker { + IERC721 public immutable NFT; + + constructor(IERC721 _nft) { + NFT = IERC721(_nft); + } + + function _check(address subject, bytes memory evidence) internal view override returns (bool) { + // Decode the tokenId from the evidence. + uint256 tokenId = abi.decode(evidence, (uint256)); + + // Return true if the subject is the owner of the tokenId, false otherwise. + return NFT.ownerOf(tokenId) == subject; + } +} diff --git a/packages/contracts/contracts/src/test/BaseERC721Policy.sol b/packages/contracts/contracts/src/test/BaseERC721Policy.sol new file mode 100644 index 0000000..d99de07 --- /dev/null +++ b/packages/contracts/contracts/src/test/BaseERC721Policy.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {BasePolicy} from "../../src/BasePolicy.sol"; +import {BaseERC721Checker} from "./BaseERC721Checker.sol"; + +contract BaseERC721Policy is BasePolicy { + BaseERC721Checker public immutable CHECKER; + + constructor(BaseERC721Checker _checker) BasePolicy(_checker) { + CHECKER = BaseERC721Checker(_checker); + } + + function trait() external pure returns (string memory) { + return "BaseERC721"; + } +} diff --git a/packages/contracts/contracts/src/test/BaseVoting.sol b/packages/contracts/contracts/src/test/BaseVoting.sol new file mode 100644 index 0000000..e571bc0 --- /dev/null +++ b/packages/contracts/contracts/src/test/BaseVoting.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {BaseERC721Policy} from "./BaseERC721Policy.sol"; + +contract BaseVoting { + event Registered(address voter); + event Voted(address voter, uint8 option); + + error NotRegistered(); + error AlreadyVoted(); + error InvalidOption(); + + BaseERC721Policy public immutable POLICY; + + // Mapping to track if an address has voted + mapping(address => bool) public hasVoted; + // Mapping to count votes for each option + mapping(uint8 => uint256) public voteCounts; + + constructor(BaseERC721Policy _policy) { + POLICY = _policy; + } + + // Function to register a voter using a the policy enforcement. + function register(uint256 tokenId) external { + bytes memory evidence = abi.encode(tokenId); + POLICY.enforce(msg.sender, evidence); + + emit Registered(msg.sender); + } + + // Function to cast a vote for a given option. + function vote(uint8 option) external { + if (!POLICY.enforced(address(this), msg.sender)) revert NotRegistered(); + if (hasVoted[msg.sender]) revert AlreadyVoted(); + if (option >= 2) revert InvalidOption(); + + hasVoted[msg.sender] = true; + voteCounts[option]++; + + emit Voted(msg.sender, option); + } +} diff --git a/packages/contracts/contracts/src/test/NFT.sol b/packages/contracts/contracts/src/test/NFT.sol new file mode 100644 index 0000000..183d16d --- /dev/null +++ b/packages/contracts/contracts/src/test/NFT.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +contract NFT is ERC721 { + uint256 private _tokenIdCounter; + + constructor() ERC721("NFT", "NFT") {} + + function mint(address to) external { + _safeMint(to, _tokenIdCounter); + _tokenIdCounter++; + } +} diff --git a/packages/contracts/contracts/test/Base.t.sol b/packages/contracts/contracts/test/Base.t.sol new file mode 100644 index 0000000..33c75ba --- /dev/null +++ b/packages/contracts/contracts/test/Base.t.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {Test} from "forge-std/src/Test.sol"; +import {NFT} from "../src/test/NFT.sol"; +import {BaseERC721Checker} from "../src/test/BaseERC721Checker.sol"; +import {BaseERC721CheckerHarness} from "./wrappers/BaseERC721CheckerHarness.sol"; +import {IERC721Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; + +contract BaseChecker is Test { + NFT internal nft; + BaseERC721Checker internal checker; + BaseERC721CheckerHarness internal checkerHarness; + + address public deployer = vm.addr(0x1); + address public target = vm.addr(0x2); + address public subject = vm.addr(0x3); + address public notOwner = vm.addr(0x4); + + function setUp() public virtual { + vm.startPrank(deployer); + + nft = new NFT(); + checker = new BaseERC721Checker(nft); + checkerHarness = new BaseERC721CheckerHarness(nft); + + vm.stopPrank(); + } + + function test_check_internal_RevertWhen_ERC721NonexistentToken() public { + vm.startPrank(target); + + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, uint256(0))); + checkerHarness.exposed__check(subject, abi.encode(0)); + + vm.stopPrank(); + } + + function test_check_internal_return_False() public { + vm.startPrank(target); + + nft.mint(subject); + + assert(!checkerHarness.exposed__check(notOwner, abi.encode(0))); + + vm.stopPrank(); + } + + function test_check_Internal() public { + vm.startPrank(target); + + nft.mint(subject); + + assert(checkerHarness.exposed__check(subject, abi.encode(0))); + + vm.stopPrank(); + } + + function test_check_RevertWhen_ERC721NonexistentToken() public { + vm.startPrank(target); + + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, uint256(0))); + checker.check(subject, abi.encode(0)); + + vm.stopPrank(); + } + + function test_check_return_False() public { + vm.startPrank(target); + + nft.mint(subject); + + assert(!checker.check(notOwner, abi.encode(0))); + + vm.stopPrank(); + } + + function test_check() public { + vm.startPrank(target); + + nft.mint(subject); + + assert(checker.check(subject, abi.encode(0))); + + vm.stopPrank(); + } +} diff --git a/packages/contracts/contracts/test/wrappers/.gitkeep b/packages/contracts/contracts/test/wrappers/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/packages/contracts/contracts/test/wrappers/BaseERC721CheckerHarness.sol b/packages/contracts/contracts/test/wrappers/BaseERC721CheckerHarness.sol new file mode 100644 index 0000000..b3300ad --- /dev/null +++ b/packages/contracts/contracts/test/wrappers/BaseERC721CheckerHarness.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {BaseERC721Checker} from "../../src/test/BaseERC721Checker.sol"; +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +// This contract is a harness for testing the BaseERC721Checker contract. +// Deploy this contract and call its methods to test the internal methods of BaseERC721Checker. +contract BaseERC721CheckerHarness is BaseERC721Checker { + constructor(IERC721 _nft) BaseERC721Checker(_nft) {} + + /// @notice Exposes the internal `_check` method for testing purposes. + /// @param subject The address to be checked. + /// @param evidence The data associated with the check. + function exposed__check(address subject, bytes calldata evidence) public view returns (bool) { + return _check(subject, evidence); + } +} diff --git a/packages/contracts/hardhat.config.ts b/packages/contracts/hardhat.config.ts index da82453..5087219 100644 --- a/packages/contracts/hardhat.config.ts +++ b/packages/contracts/hardhat.config.ts @@ -23,7 +23,7 @@ const config: HardhatUserConfig = { paths: { sources: "./contracts/src", tests: "./test", - cache: "./cache", + cache: "./cache-hh", artifacts: "./artifacts" }, networks: { diff --git a/packages/contracts/contracts/test/.gitkeep b/packages/contracts/ignition/modules/.gitkeep similarity index 100% rename from packages/contracts/contracts/test/.gitkeep rename to packages/contracts/ignition/modules/.gitkeep diff --git a/packages/contracts/test/.gitkeep b/packages/contracts/test/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/packages/contracts/test/Base.test.ts b/packages/contracts/test/Base.test.ts new file mode 100644 index 0000000..781c123 --- /dev/null +++ b/packages/contracts/test/Base.test.ts @@ -0,0 +1,70 @@ +import { expect } from "chai" +import { ethers } from "hardhat" +import { AbiCoder, Signer } from "ethers" +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers" +import { BaseERC721Checker, BaseERC721Checker__factory, NFT, NFT__factory } from "../typechain-types" + +describe("BaseChecker", () => { + async function deployBaseCheckerFixture() { + const [deployer, subject, target, notOwner]: Signer[] = await ethers.getSigners() + const subjectAddress: string = await subject.getAddress() + const notOwnerAddress: string = await notOwner.getAddress() + + const NFTFactory: NFT__factory = await ethers.getContractFactory("NFT") + const BaseERC721CheckerFactory: BaseERC721Checker__factory = + await ethers.getContractFactory("BaseERC721Checker") + + const nft: NFT = await NFTFactory.deploy() + const checker: BaseERC721Checker = await BaseERC721CheckerFactory.connect(deployer).deploy( + await nft.getAddress() + ) + + // mint 0 for subject. + await nft.connect(deployer).mint(subjectAddress) + + // encoded token ids. + const validNFTId = AbiCoder.defaultAbiCoder().encode(["uint256"], [0]) + const invalidNFTId = AbiCoder.defaultAbiCoder().encode(["uint256"], [1]) + + return { + nft, + checker, + target, + subjectAddress, + notOwnerAddress, + validNFTId, + invalidNFTId + } + } + + describe("constructor()", () => { + it("Should deploy the checker contract correctly", async () => { + const { checker } = await loadFixture(deployBaseCheckerFixture) + + expect(checker).to.not.eq(undefined) + }) + }) + + describe("check()", () => { + it("should revert the check when the evidence is not meaningful", async () => { + const { nft, checker, target, subjectAddress, invalidNFTId } = await loadFixture(deployBaseCheckerFixture) + + await expect(checker.connect(target).check(subjectAddress, invalidNFTId)).to.be.revertedWithCustomError( + nft, + "ERC721NonexistentToken" + ) + }) + + it("should return false when the subject is not the owner of the evidenced token", async () => { + const { checker, target, notOwnerAddress, validNFTId } = await loadFixture(deployBaseCheckerFixture) + + expect(await checker.connect(target).check(notOwnerAddress, validNFTId)).to.be.equal(false) + }) + + it("should check", async () => { + const { checker, target, subjectAddress, validNFTId } = await loadFixture(deployBaseCheckerFixture) + + expect(await checker.connect(target).check(subjectAddress, validNFTId)).to.be.equal(true) + }) + }) +})