diff --git a/packages/contracts/contracts/src/AdvancedChecker.sol b/packages/contracts/contracts/src/AdvancedChecker.sol index f294989..3e09eff 100644 --- a/packages/contracts/contracts/src/AdvancedChecker.sol +++ b/packages/contracts/contracts/src/AdvancedChecker.sol @@ -51,15 +51,18 @@ abstract contract AdvancedChecker is IAdvancedChecker { /// @param evidence The evidence associated with the check. /// @param checkType The type of check to perform (PRE, MAIN, POST). function _check(address subject, bytes memory evidence, Check checkType) internal view returns (bool checked) { + if (skipPre && checkType == Check.PRE) revert PreCheckSkipped(); + if (skipPost && checkType == Check.POST) revert PostCheckSkipped(); + if (!skipPre && checkType == Check.PRE) { return _checkPre(subject, evidence); - } else if (!skipPost && checkType == Check.POST) { + } + + if (!skipPost && checkType == Check.POST) { return _checkPost(subject, evidence); - } else if (checkType == Check.MAIN) { - return _checkMain(subject, evidence); } - return false; + return _checkMain(subject, evidence); } /// @notice Internal method for performing pre-condition checks. diff --git a/packages/contracts/contracts/src/AdvancedPolicy.sol b/packages/contracts/contracts/src/AdvancedPolicy.sol index 9bbd1d4..1e32fb5 100644 --- a/packages/contracts/contracts/src/AdvancedPolicy.sol +++ b/packages/contracts/contracts/src/AdvancedPolicy.sol @@ -37,21 +37,45 @@ abstract contract AdvancedPolicy is IAdvancedPolicy, Policy { function _enforce(address subject, bytes calldata evidence, Check checkType) internal { bool checked = ADVANCED_CHECKER.check(subject, evidence, checkType); - if (!checked) revert UnsuccessfulCheck(); + if (!checked) { + revert UnsuccessfulCheck(); + } if (checkType == Check.PRE) { - if (ADVANCED_CHECKER.skipPre()) revert PreCheckSkipped(); - else if (enforced[msg.sender][subject].pre) revert AlreadyEnforced(); - else enforced[msg.sender][subject].pre = true; - } else if (checkType == Check.POST) { - if (ADVANCED_CHECKER.skipPost()) revert PostCheckSkipped(); - else if (enforced[msg.sender][subject].post) revert AlreadyEnforced(); - else enforced[msg.sender][subject].post = true; - } else if (checkType == Check.MAIN) { - if (!ADVANCED_CHECKER.allowMultipleMain() && enforced[msg.sender][subject].main > 0) { - revert MainCheckAlreadyEnforced(); + if (!ADVANCED_CHECKER.skipPost() && enforced[msg.sender][subject].pre) { + revert AlreadyEnforced(); + } else { + enforced[msg.sender][subject].pre = true; + } + } else { + if (checkType == Check.POST) { + if (enforced[msg.sender][subject].post) { + revert AlreadyEnforced(); + } else { + if (!ADVANCED_CHECKER.skipPre() && !enforced[msg.sender][subject].pre) { + revert PreCheckNotEnforced(); + } else { + if (enforced[msg.sender][subject].main == 0) { + revert MainCheckNotEnforced(); + } else { + enforced[msg.sender][subject].post = true; + } + } + } } else { - enforced[msg.sender][subject].main += 1; + if ( + checkType == Check.MAIN && + !ADVANCED_CHECKER.allowMultipleMain() && + enforced[msg.sender][subject].main > 0 + ) { + revert MainCheckAlreadyEnforced(); + } else { + if (checkType == Check.MAIN && !ADVANCED_CHECKER.skipPre() && !enforced[msg.sender][subject].pre) { + revert PreCheckNotEnforced(); + } else { + enforced[msg.sender][subject].main += 1; + } + } } } diff --git a/packages/contracts/contracts/src/BaseChecker.sol b/packages/contracts/contracts/src/BaseChecker.sol index 7d7312f..36268b5 100644 --- a/packages/contracts/contracts/src/BaseChecker.sol +++ b/packages/contracts/contracts/src/BaseChecker.sol @@ -9,8 +9,6 @@ import {IBaseChecker} from "./interfaces/IBaseChecker.sol"; /// It defines a method `check` that invokes a protected `_check` method, which must be implemented by derived /// contracts. abstract contract BaseChecker is IBaseChecker { - constructor() {} - /// @notice Checks the validity of the provided evidence for a given address. /// @param subject The address to be checked. /// @param evidence The evidence associated with the check. diff --git a/packages/contracts/contracts/src/interfaces/IAdvancedChecker.sol b/packages/contracts/contracts/src/interfaces/IAdvancedChecker.sol index 11f2c91..60a620e 100644 --- a/packages/contracts/contracts/src/interfaces/IAdvancedChecker.sol +++ b/packages/contracts/contracts/src/interfaces/IAdvancedChecker.sol @@ -15,6 +15,11 @@ enum Check { /// @title IAdvancedChecker. /// @notice AdvancedChecker contract interface. interface IAdvancedChecker { + /// @notice Error thrown when the PRE check is skipped. + error PreCheckSkipped(); + /// @notice Error thrown when the POST check is skipped. + error PostCheckSkipped(); + /// @dev Defines the custom `target` protection logic. /// @param subject The address of the entity attempting to interact with the `target`. /// @param evidence Additional data that may be required for the check. diff --git a/packages/contracts/contracts/src/interfaces/IAdvancedPolicy.sol b/packages/contracts/contracts/src/interfaces/IAdvancedPolicy.sol index 45d4957..36f4b80 100644 --- a/packages/contracts/contracts/src/interfaces/IAdvancedPolicy.sol +++ b/packages/contracts/contracts/src/interfaces/IAdvancedPolicy.sol @@ -7,14 +7,14 @@ import {Check} from "./IAdvancedChecker.sol"; /// @title IAdvancedPolicy /// @notice IAdvancedPolicy contract interface that extends the IPolicy interface. interface IAdvancedPolicy is IPolicy { - /// @notice Error thrown when the PRE check is skipped. - error PreCheckSkipped(); - /// @notice Error thrown when the MAIN check cannot be executed more than once. error MainCheckAlreadyEnforced(); - /// @notice Error thrown when the POST check is skipped. - error PostCheckSkipped(); + /// @notice Error thrown when the PRE check has not been enforced yet. + error PreCheckNotEnforced(); + + /// @notice Error thrown when the MAIN check has not been enforced yet. + error MainCheckNotEnforced(); /// @notice Event emitted when someone enforces the `target` check. /// @param subject The address of those who have successfully enforced the check. diff --git a/packages/contracts/contracts/src/test/advanced/AdvancedERC721Checker.sol b/packages/contracts/contracts/src/test/advanced/AdvancedERC721Checker.sol new file mode 100644 index 0000000..232e8e1 --- /dev/null +++ b/packages/contracts/contracts/src/test/advanced/AdvancedERC721Checker.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {AdvancedChecker} from "../../AdvancedChecker.sol"; +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +/** + * @title AdvancedERC721Checker + * @notice Implements advanced checks for ERC721 token requirements. + * @dev Extends AdvancedChecker to provide three-phase validation: + * - Pre-check: Basic token ownership verification. + * - Main check: Token balance threshold validation. + * - Post-check: Special token ID range verification. + */ +contract AdvancedERC721Checker is AdvancedChecker { + IERC721 public immutable NFT; + /// @notice Minimum token balance required for main check. + uint256 public immutable MIN_BALANCE; + /// @notice Minimum token ID allowed for post-check validation. + uint256 public immutable MIN_TOKEN_ID; + /// @notice Maximum token ID allowed for post-check validation. + uint256 public immutable MAX_TOKEN_ID; + + constructor( + IERC721 _nft, + uint256 _minBalance, + uint256 _minTokenId, + uint256 _maxTokenId, + bool _skipPre, + bool _skipPost, + bool _allowMultipleMain + ) AdvancedChecker(_skipPre, _skipPost, _allowMultipleMain) { + NFT = _nft; + MIN_BALANCE = _minBalance; + MIN_TOKEN_ID = _minTokenId; + MAX_TOKEN_ID = _maxTokenId; + } + + /** + * @notice Pre-check verifies basic token ownership. + * @dev Validates if the subject owns the specific tokenId provided in evidence. + * @param subject Address to check ownership for. + * @param evidence Encoded uint256 tokenId. + * @return True if subject owns the token, false otherwise. + */ + function _checkPre(address subject, bytes memory evidence) internal view override returns (bool) { + super._checkPre(subject, evidence); + + uint256 tokenId = abi.decode(evidence, (uint256)); + return NFT.ownerOf(tokenId) == subject; + } + + /** + * @notice Main check verifies minimum token balance. + * @dev Validates if the subject holds at least MIN_BALANCE tokens. + * @param subject Address to check balance for. + * @param evidence Not used in this check. + * @return True if subject meets minimum balance requirement. + */ + function _checkMain(address subject, bytes memory evidence) internal view override returns (bool) { + super._checkMain(subject, evidence); + + return NFT.balanceOf(subject) >= MIN_BALANCE; + } + + /** + * @notice Post-check verifies ownership of a token within specific ID range. + * @dev Validates if subject owns a token with ID between MIN_TOKEN_ID and MAX_TOKEN_ID. + * @param subject Address to check ownership for. + * @param evidence Encoded uint256 tokenId. + * @return True if subject owns a token in valid range. + */ + function _checkPost(address subject, bytes memory evidence) internal view override returns (bool) { + super._checkPost(subject, evidence); + + uint256 tokenId = abi.decode(evidence, (uint256)); + return tokenId >= MIN_TOKEN_ID && tokenId <= MAX_TOKEN_ID && NFT.ownerOf(tokenId) == subject; + } +} diff --git a/packages/contracts/contracts/src/test/advanced/AdvancedERC721Policy.sol b/packages/contracts/contracts/src/test/advanced/AdvancedERC721Policy.sol new file mode 100644 index 0000000..1a488ca --- /dev/null +++ b/packages/contracts/contracts/src/test/advanced/AdvancedERC721Policy.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {AdvancedPolicy} from "../../AdvancedPolicy.sol"; +import {AdvancedERC721Checker} from "./AdvancedERC721Checker.sol"; + +/** + * @title AdvancedERC721Policy + * @notice Policy contract implementing three-phase validation for ERC721 tokens. + * @dev Extends AdvancedPolicy to enforce ERC721-specific checks through AdvancedERC721Checker. + */ +contract AdvancedERC721Policy is AdvancedPolicy { + /// @notice Reference to the ERC721 checker contract implementing validation logic. + AdvancedERC721Checker public immutable CHECKER; + + /// @param _checker Address of the AdvancedERC721Checker contract. + constructor(AdvancedERC721Checker _checker) AdvancedPolicy(_checker) { + CHECKER = _checker; + } + + /// @notice Returns the trait identifier for this policy. + /// @return String identifying this as an ERC721-based policy. + function trait() external pure returns (string memory) { + return "AdvancedERC721"; + } +} diff --git a/packages/contracts/contracts/src/test/advanced/AdvancedVoting.sol b/packages/contracts/contracts/src/test/advanced/AdvancedVoting.sol new file mode 100644 index 0000000..eb3afa6 --- /dev/null +++ b/packages/contracts/contracts/src/test/advanced/AdvancedVoting.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {AdvancedPolicy} from "../../AdvancedPolicy.sol"; +import {Check} from "../../interfaces/IAdvancedPolicy.sol"; + +/** + * @title AdvancedVoting + * @notice Voting contract with three-phase validation and NFT rewards. + * @dev Uses pre-check for registration, main check for voting, and post-check for claiming NFT rewards. + */ +contract AdvancedVoting { + event Registered(address voter); + event Voted(address voter, uint8 option); + event RewardClaimed(address voter, uint256 rewardId); + + error NotRegistered(); + error NotVoted(); + error AlreadyClaimed(); + error InvalidOption(); + error NotOwnerOfReward(); + + /// @notice Policy contract handling validation checks. + AdvancedPolicy public immutable POLICY; + + /// @notice Tracks vote counts for each option. + mapping(uint8 => uint256) public voteCounts; + + constructor(AdvancedPolicy _policy) { + POLICY = _policy; + } + + /** + * @notice Register to participate in voting. + * @dev Validates NFT ownership through pre-check. + * @param tokenId Token ID to verify ownership. + */ + function register(uint256 tokenId) external { + bytes memory evidence = abi.encode(tokenId); + + POLICY.enforce(msg.sender, evidence, Check.PRE); + + emit Registered(msg.sender); + } + + /** + * @notice Cast vote after verifying registration. + * @dev Requires pre-check completion and validates voting power. + * @param option Voting option (0 or 1). + */ + function vote(uint8 option) external { + (bool pre, , ) = POLICY.enforced(address(this), msg.sender); + + if (!pre) revert NotRegistered(); + if (option >= 2) revert InvalidOption(); + + bytes memory evidence = abi.encode(option); + POLICY.enforce(msg.sender, evidence, Check.MAIN); + + unchecked { + voteCounts[option]++; + } + + emit Voted(msg.sender, option); + } + + /** + * @notice Claim NFT reward after voting. + * @dev Validates voting participation and transfers reward NFT. + * @param rewardId NFT ID to be claimed as reward. + */ + function reward(uint256 rewardId) external { + (bool pre, uint8 main, bool post) = POLICY.enforced(address(this), msg.sender); + + if (!pre) revert NotRegistered(); + if (main == 0) revert NotVoted(); + if (post) revert AlreadyClaimed(); + + bytes memory evidence = abi.encode(rewardId); + POLICY.enforce(msg.sender, evidence, Check.POST); + + emit RewardClaimed(msg.sender, rewardId); + } +} diff --git a/packages/contracts/contracts/src/test/wrappers/AdvancedERC721CheckerHarness.sol b/packages/contracts/contracts/src/test/wrappers/AdvancedERC721CheckerHarness.sol new file mode 100644 index 0000000..38a73be --- /dev/null +++ b/packages/contracts/contracts/src/test/wrappers/AdvancedERC721CheckerHarness.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {AdvancedERC721Checker} from "../advanced/AdvancedERC721Checker.sol"; +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {Check} from "../../interfaces/IAdvancedChecker.sol"; + +// This contract is a harness for testing the AdvancedERC721Checker contract. +// Deploy this contract and call its methods to test the internal methods of AdvancedERC721Checker. +contract AdvancedERC721CheckerHarness is AdvancedERC721Checker { + constructor( + IERC721 _nft, + uint256 _minBalance, + uint256 _minTokenId, + uint256 _maxTokenId, + bool _skipPre, + bool _skipPost, + bool _allowMultipleMain + ) AdvancedERC721Checker(_nft, _minBalance, _minTokenId, _maxTokenId, _skipPre, _skipPost, _allowMultipleMain) {} + + /// @notice Exposes the internal `_check` method for testing purposes. + /// @param subject The address to be checked. + /// @param evidence The data associated with the check. + /// @param checkType The type of check to perform (PRE, MAIN, POST). + function exposed__check(address subject, bytes calldata evidence, Check checkType) public view returns (bool) { + return _check(subject, evidence, checkType); + } + + /// @notice Exposes the internal `_checkPre` method for testing purposes. + /// @param subject The address to be checked. + /// @param evidence The data associated with the check. + function exposed__checkPre(address subject, bytes calldata evidence) public view returns (bool) { + return _checkPre(subject, evidence); + } + + /// @notice Exposes the internal `_checkMain` method for testing purposes. + /// @param subject The address to be checked. + /// @param evidence The data associated with the check. + function exposed__checkMain(address subject, bytes calldata evidence) public view returns (bool) { + return _checkMain(subject, evidence); + } + + /// @notice Exposes the internal `_checkPost` method for testing purposes. + /// @param subject The address to be checked. + /// @param evidence The data associated with the check. + function exposed__checkPost(address subject, bytes calldata evidence) public view returns (bool) { + return _checkPost(subject, evidence); + } +} diff --git a/packages/contracts/contracts/src/test/wrappers/AdvancedERC721PolicyHarness.sol b/packages/contracts/contracts/src/test/wrappers/AdvancedERC721PolicyHarness.sol new file mode 100644 index 0000000..63f4c06 --- /dev/null +++ b/packages/contracts/contracts/src/test/wrappers/AdvancedERC721PolicyHarness.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {AdvancedERC721Policy} from "../advanced/AdvancedERC721Policy.sol"; +import {AdvancedERC721Checker} from "../advanced/AdvancedERC721Checker.sol"; +import {Check} from "../../interfaces/IAdvancedChecker.sol"; + +// This contract is a harness for testing the AdvancedERC721Policy contract. +// Deploy this contract and call its methods to test the internal methods of AdvancedERC721Policy. +contract AdvancedERC721PolicyHarness is AdvancedERC721Policy { + constructor(AdvancedERC721Checker _checker) AdvancedERC721Policy(_checker) {} + + /// @notice Exposes the internal `_enforce` method for testing purposes. + /// @param subject The address of those who have successfully enforced the check. + /// @param evidence Additional data required for the check (e.g., encoded token identifier). + /// @param checkType The type of the check to be enforced for the subject with the given data. + function exposed__enforce(address subject, bytes calldata evidence, Check checkType) internal onlyTarget { + _enforce(subject, evidence, checkType); + } +} diff --git a/packages/contracts/test/Advanced.test.ts b/packages/contracts/test/Advanced.test.ts new file mode 100644 index 0000000..c48e9ee --- /dev/null +++ b/packages/contracts/test/Advanced.test.ts @@ -0,0 +1,1471 @@ +import { expect } from "chai" +import { ethers } from "hardhat" +import { AbiCoder, Signer, ZeroAddress, ZeroHash } from "ethers" +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers" +import { + AdvancedERC721Checker, + AdvancedERC721Checker__factory, + AdvancedERC721CheckerHarness, + AdvancedERC721CheckerHarness__factory, + AdvancedERC721Policy, + AdvancedERC721Policy__factory, + AdvancedERC721PolicyHarness, + AdvancedERC721PolicyHarness__factory, + AdvancedVoting, + AdvancedVoting__factory, + IERC721Errors, + NFT, + NFT__factory +} from "../typechain-types" + +describe("Advanced", () => { + describe("Checker", () => { + async function deployAdvancedCheckerFixture() { + 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 AdvancedERC721CheckerFactory: AdvancedERC721Checker__factory = + await ethers.getContractFactory("AdvancedERC721Checker") + const AdvancedERC721CheckerHarnessFactory: AdvancedERC721CheckerHarness__factory = + await ethers.getContractFactory("AdvancedERC721CheckerHarness") + + const nft: NFT = await NFTFactory.deploy() + + const checker: AdvancedERC721Checker = await AdvancedERC721CheckerFactory.connect(deployer).deploy( + await nft.getAddress(), + 1, + 0, + 10, + false, + false, + true + ) + + const checkerHarness: AdvancedERC721CheckerHarness = await AdvancedERC721CheckerHarnessFactory.connect( + deployer + ).deploy(await nft.getAddress(), 1, 0, 10, false, false, true) + + // 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, + deployer, + checkerHarness, + target, + subject, + subjectAddress, + notOwnerAddress, + validNFTId, + invalidNFTId + } + } + + describe("constructor()", () => { + it("Should deploy the checker contract correctly", async () => { + const { checker } = await loadFixture(deployAdvancedCheckerFixture) + + expect(checker).to.not.eq(undefined) + }) + }) + + describe("check()", () => { + describe("pre", () => { + it("should revert the check when the evidence is not meaningful", async () => { + const { nft, checker, target, subjectAddress, invalidNFTId } = + await loadFixture(deployAdvancedCheckerFixture) + + await expect( + checker.connect(target).check(subjectAddress, invalidNFTId, 0) + ).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(deployAdvancedCheckerFixture) + + expect(await checker.connect(target).check(notOwnerAddress, validNFTId, 0)).to.be.equal(false) + }) + + it("should check", async () => { + const { checker, target, subjectAddress, validNFTId } = + await loadFixture(deployAdvancedCheckerFixture) + + expect(await checker.connect(target).check(subjectAddress, validNFTId, 0)).to.be.equal(true) + }) + }) + describe("main", () => { + it("should return false when the subject does not satisfy the attributes", async () => { + const { checker, target, notOwnerAddress, validNFTId } = + await loadFixture(deployAdvancedCheckerFixture) + + expect(await checker.connect(target).check(notOwnerAddress, validNFTId, 1)).to.be.equal(false) + }) + + it("should check", async () => { + const { checker, target, subjectAddress, validNFTId } = + await loadFixture(deployAdvancedCheckerFixture) + + expect(await checker.connect(target).check(subjectAddress, validNFTId, 1)).to.be.equal(true) + }) + }) + describe("post", () => { + it("should revert the check when the evidence is not meaningful", async () => { + const { nft, checker, target, subjectAddress, invalidNFTId } = + await loadFixture(deployAdvancedCheckerFixture) + + await expect( + checker.connect(target).check(subjectAddress, invalidNFTId, 2) + ).to.be.revertedWithCustomError(nft, "ERC721NonexistentToken") + }) + + it("should return false when the subject does not satisfy the attributes", async () => { + const { checker, target, notOwnerAddress, validNFTId } = + await loadFixture(deployAdvancedCheckerFixture) + + expect(await checker.connect(target).check(notOwnerAddress, validNFTId, 2)).to.be.equal(false) + }) + + it("should check", async () => { + const { checker, target, subjectAddress, validNFTId } = + await loadFixture(deployAdvancedCheckerFixture) + + expect(await checker.connect(target).check(subjectAddress, validNFTId, 2)).to.be.equal(true) + }) + }) + }) + + describe("_check()", () => { + describe("pre", () => { + it("should revert the check when the evidence is not meaningful", async () => { + const { nft, checkerHarness, target, subjectAddress, invalidNFTId } = + await loadFixture(deployAdvancedCheckerFixture) + + await expect( + checkerHarness.connect(target).exposed__check(subjectAddress, invalidNFTId, 0) + ).to.be.revertedWithCustomError(nft, "ERC721NonexistentToken") + }) + + it("should return false when the subject is not the owner of the evidenced token", async () => { + const { checkerHarness, target, notOwnerAddress, validNFTId } = + await loadFixture(deployAdvancedCheckerFixture) + + expect( + await checkerHarness.connect(target).exposed__check(notOwnerAddress, validNFTId, 0) + ).to.be.equal(false) + }) + + it("should check", async () => { + const { checkerHarness, target, subjectAddress, validNFTId } = + await loadFixture(deployAdvancedCheckerFixture) + + expect( + await checkerHarness.connect(target).exposed__check(subjectAddress, validNFTId, 0) + ).to.be.equal(true) + }) + }) + describe("main", () => { + it("should return false when the subject does not satisfy the attributes", async () => { + const { checkerHarness, target, notOwnerAddress, validNFTId } = + await loadFixture(deployAdvancedCheckerFixture) + + expect( + await checkerHarness.connect(target).exposed__check(notOwnerAddress, validNFTId, 1) + ).to.be.equal(false) + }) + + it("should check", async () => { + const { checkerHarness, target, subjectAddress, validNFTId } = + await loadFixture(deployAdvancedCheckerFixture) + + expect( + await checkerHarness.connect(target).exposed__check(subjectAddress, validNFTId, 1) + ).to.be.equal(true) + }) + }) + describe("post", () => { + it("should revert the check when the evidence is not meaningful", async () => { + const { nft, checkerHarness, target, subjectAddress, invalidNFTId } = + await loadFixture(deployAdvancedCheckerFixture) + + await expect( + checkerHarness.connect(target).exposed__check(subjectAddress, invalidNFTId, 2) + ).to.be.revertedWithCustomError(nft, "ERC721NonexistentToken") + }) + + it("should return false when the subject does not satisfy the attributes", async () => { + const { checkerHarness, target, notOwnerAddress, validNFTId } = + await loadFixture(deployAdvancedCheckerFixture) + + expect( + await checkerHarness.connect(target).exposed__check(notOwnerAddress, validNFTId, 2) + ).to.be.equal(false) + }) + + it("should check", async () => { + const { checkerHarness, target, subjectAddress, validNFTId } = + await loadFixture(deployAdvancedCheckerFixture) + + expect( + await checkerHarness.connect(target).exposed__check(subjectAddress, validNFTId, 2) + ).to.be.equal(true) + }) + }) + }) + + describe("_checkPre", () => { + it("should revert the check when the evidence is not meaningful", async () => { + const { nft, checkerHarness, target, subjectAddress, invalidNFTId } = + await loadFixture(deployAdvancedCheckerFixture) + + await expect( + checkerHarness.connect(target).exposed__checkPre(subjectAddress, invalidNFTId) + ).to.be.revertedWithCustomError(nft, "ERC721NonexistentToken") + }) + + it("should return false when the subject is not the owner of the evidenced token", async () => { + const { checkerHarness, target, notOwnerAddress, validNFTId } = + await loadFixture(deployAdvancedCheckerFixture) + + expect(await checkerHarness.connect(target).exposed__checkPre(notOwnerAddress, validNFTId)).to.be.equal( + false + ) + }) + + it("should check", async () => { + const { checkerHarness, target, subjectAddress, validNFTId } = + await loadFixture(deployAdvancedCheckerFixture) + + expect(await checkerHarness.connect(target).exposed__checkPre(subjectAddress, validNFTId)).to.be.equal( + true + ) + }) + + it("should return true for valid token ownership during pre-check", async () => { + const { checkerHarness, subjectAddress, validNFTId } = await loadFixture(deployAdvancedCheckerFixture) + + const result = await checkerHarness.exposed__checkPre(subjectAddress, validNFTId) + expect(result).to.be.equal(true) + }) + + it("should return false for invalid token ownership during pre-check", async () => { + const { checkerHarness, notOwnerAddress, validNFTId } = await loadFixture(deployAdvancedCheckerFixture) + + const result = await checkerHarness.exposed__checkPre(notOwnerAddress, validNFTId) + expect(result).to.be.equal(false) + }) + }) + + describe("_checkMain", () => { + it("should return false when the subject does not satisfy the attributes", async () => { + const { checkerHarness, target, notOwnerAddress, validNFTId } = + await loadFixture(deployAdvancedCheckerFixture) + + expect( + await checkerHarness.connect(target).exposed__checkMain(notOwnerAddress, validNFTId) + ).to.be.equal(false) + }) + + it("should check", async () => { + const { checkerHarness, target, subjectAddress, validNFTId } = + await loadFixture(deployAdvancedCheckerFixture) + + expect(await checkerHarness.connect(target).exposed__checkMain(subjectAddress, validNFTId)).to.be.equal( + true + ) + }) + + it("should return true when subject has sufficient token balance during main-check", async () => { + const { checkerHarness, subjectAddress, validNFTId } = await loadFixture(deployAdvancedCheckerFixture) + + const result = await checkerHarness.exposed__checkMain(subjectAddress, validNFTId) + expect(result).to.be.equal(true) + }) + + it("should return false when subject has insufficient token balance during main-check", async () => { + const { checkerHarness, notOwnerAddress, validNFTId } = await loadFixture(deployAdvancedCheckerFixture) + + const result = await checkerHarness.exposed__checkMain(notOwnerAddress, validNFTId) + expect(result).to.be.equal(false) + }) + }) + + describe("_checkPost", () => { + it("should revert the check when the evidence is not meaningful", async () => { + const { nft, checkerHarness, target, subjectAddress, invalidNFTId } = + await loadFixture(deployAdvancedCheckerFixture) + + await expect( + checkerHarness.connect(target).exposed__checkPost(subjectAddress, invalidNFTId) + ).to.be.revertedWithCustomError(nft, "ERC721NonexistentToken") + }) + + it("should return false when the subject does not satisfy the attributes", async () => { + const { checkerHarness, target, notOwnerAddress, validNFTId } = + await loadFixture(deployAdvancedCheckerFixture) + + expect( + await checkerHarness.connect(target).exposed__checkPost(notOwnerAddress, validNFTId) + ).to.be.equal(false) + }) + + it("should check", async () => { + const { checkerHarness, target, subjectAddress, validNFTId } = + await loadFixture(deployAdvancedCheckerFixture) + + expect(await checkerHarness.connect(target).exposed__checkPost(subjectAddress, validNFTId)).to.be.equal( + true + ) + }) + + it("should return false when token ID is out of valid range during post-check", async () => { + const { checkerHarness, nft, deployer, subjectAddress } = + await loadFixture(deployAdvancedCheckerFixture) + + for (let i = 0; i < 20; i += 1) { + await nft.connect(deployer).mint(subjectAddress) + } + + const result = await checkerHarness.exposed__checkPost( + subjectAddress, + AbiCoder.defaultAbiCoder().encode(["uint256"], [12]) + ) + expect(result).to.be.equal(false) + }) + + it("should return true for valid token ownership and ID range during post-check", async () => { + const { checkerHarness, subjectAddress, validNFTId } = await loadFixture(deployAdvancedCheckerFixture) + + const result = await checkerHarness.exposed__checkPost(subjectAddress, validNFTId) + expect(result).to.be.equal(true) + }) + + it("should return false when subject does not own the token during post-check", async () => { + const { checkerHarness, notOwnerAddress, validNFTId } = await loadFixture(deployAdvancedCheckerFixture) + + const result = await checkerHarness.exposed__checkPost(notOwnerAddress, validNFTId) + expect(result).to.be.equal(false) + }) + }) + }) + + describe("Policy", () => { + async function deployAdvancedPolicyFixture() { + 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 AdvancedERC721CheckerFactory: AdvancedERC721Checker__factory = + await ethers.getContractFactory("AdvancedERC721Checker") + const AdvancedERC721PolicyFactory: AdvancedERC721Policy__factory = + await ethers.getContractFactory("AdvancedERC721Policy") + const AdvancedERC721PolicyHarnessFactory: AdvancedERC721PolicyHarness__factory = + await ethers.getContractFactory("AdvancedERC721PolicyHarness") + + const nft: NFT = await NFTFactory.deploy() + const iERC721Errors: IERC721Errors = await ethers.getContractAt("IERC721Errors", await nft.getAddress()) + + const checker: AdvancedERC721Checker = await AdvancedERC721CheckerFactory.connect(deployer).deploy( + await nft.getAddress(), + 1, + 0, + 10, + false, + false, + true + ) + + const checkerSkippedPrePostNoMultMain: AdvancedERC721Checker = await AdvancedERC721CheckerFactory.connect( + deployer + ).deploy(await nft.getAddress(), 1, 0, 10, true, true, false) + + const policy: AdvancedERC721Policy = await AdvancedERC721PolicyFactory.connect(deployer).deploy( + await checker.getAddress() + ) + const policySkipped: AdvancedERC721Policy = await AdvancedERC721PolicyFactory.connect(deployer).deploy( + await checkerSkippedPrePostNoMultMain.getAddress() + ) + const policyHarness: AdvancedERC721PolicyHarness = await AdvancedERC721PolicyHarnessFactory.connect( + deployer + ).deploy(await checker.getAddress()) + const policyHarnessSkipped: AdvancedERC721PolicyHarness = await AdvancedERC721PolicyHarnessFactory.connect( + deployer + ).deploy(await checkerSkippedPrePostNoMultMain.getAddress()) + + // mint 0 for subject. + await nft.connect(deployer).mint(subjectAddress) + + // encoded token ids. + const validEncodedNFTId = AbiCoder.defaultAbiCoder().encode(["uint256"], [0]) + const invalidEncodedNFTId = AbiCoder.defaultAbiCoder().encode(["uint256"], [1]) + + return { + iERC721Errors, + AdvancedERC721PolicyFactory, + nft, + checker, + policyHarness, + policyHarnessSkipped, + policy, + policySkipped, + subject, + deployer, + target, + notOwner, + subjectAddress, + notOwnerAddress, + validEncodedNFTId, + invalidEncodedNFTId + } + } + + describe("constructor()", () => { + it("Should deploy the checker contract correctly", async () => { + const { policy } = await loadFixture(deployAdvancedPolicyFixture) + + expect(policy).to.not.eq(undefined) + }) + }) + + describe("trait()", () => { + it("should return the trait of the policy contract", async () => { + const { policy } = await loadFixture(deployAdvancedPolicyFixture) + + expect(await policy.trait()).to.be.eq("AdvancedERC721") + }) + }) + + describe("setTarget()", () => { + it("should fail to set the target when the caller is not the owner", async () => { + const { policy, notOwner, target } = await loadFixture(deployAdvancedPolicyFixture) + + await expect( + policy.connect(notOwner).setTarget(await target.getAddress()) + ).to.be.revertedWithCustomError(policy, "OwnableUnauthorizedAccount") + }) + + it("should fail to set the target when the target address is zero", async () => { + const { policy, deployer } = await loadFixture(deployAdvancedPolicyFixture) + + await expect(policy.connect(deployer).setTarget(ZeroAddress)).to.be.revertedWithCustomError( + policy, + "ZeroAddress" + ) + }) + + it("Should set the target contract address correctly", async () => { + const { policy, target, AdvancedERC721PolicyFactory } = await loadFixture(deployAdvancedPolicyFixture) + const targetAddress = await target.getAddress() + + const tx = await policy.setTarget(targetAddress) + const receipt = await tx.wait() + const event = AdvancedERC721PolicyFactory.interface.parseLog( + receipt?.logs[0] as unknown as { topics: string[]; data: string } + ) as unknown as { + args: { + target: string + } + } + + expect(receipt?.status).to.eq(1) + expect(event.args.target).to.eq(targetAddress) + expect(await policy.getTarget()).to.eq(targetAddress) + }) + + it("Should fail to set the target if already set", async () => { + const { policy, target } = await loadFixture(deployAdvancedPolicyFixture) + const targetAddress = await target.getAddress() + + await policy.setTarget(targetAddress) + + await expect(policy.setTarget(targetAddress)).to.be.revertedWithCustomError(policy, "TargetAlreadySet") + }) + }) + + describe("enforce()", () => { + describe("pre", () => { + it("should throw when the callee is not the target", async () => { + const { policy, subject, target, subjectAddress } = await loadFixture(deployAdvancedPolicyFixture) + + await policy.setTarget(await target.getAddress()) + + await expect( + policy.connect(subject).enforce(subjectAddress, ZeroHash, 0) + ).to.be.revertedWithCustomError(policy, "TargetOnly") + }) + + it("should throw when the evidence is not correct", async () => { + const { iERC721Errors, policy, target, subjectAddress, invalidEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + + await policy.setTarget(await target.getAddress()) + + await expect( + policy.connect(target).enforce(subjectAddress, invalidEncodedNFTId, 0) + ).to.be.revertedWithCustomError(iERC721Errors, "ERC721NonexistentToken") + }) + + it("should throw when the check is skipped", async () => { + const { checker, policySkipped, target, subjectAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + + await policySkipped.setTarget(await target.getAddress()) + + await expect( + policySkipped.connect(target).enforce(subjectAddress, validEncodedNFTId, 0) + ).to.be.revertedWithCustomError(checker, "PreCheckSkipped") + }) + + it("should throw when the check returns false", async () => { + const { policy, target, notOwnerAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + + await policy.setTarget(await target.getAddress()) + + expect( + policy.connect(target).enforce(notOwnerAddress, validEncodedNFTId, 0) + ).to.be.revertedWithCustomError(policy, "UnsuccessfulCheck") + }) + + it("should enforce", async () => { + const { AdvancedERC721PolicyFactory, policy, target, subjectAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + const targetAddress = await target.getAddress() + + await policy.setTarget(targetAddress) + + const tx = await policy.connect(target).enforce(subjectAddress, validEncodedNFTId, 0) + const receipt = await tx.wait() + const event = AdvancedERC721PolicyFactory.interface.parseLog( + receipt?.logs[0] as unknown as { topics: string[]; data: string } + ) as unknown as { + args: { + subject: string + target: string + evidence: string + } + } + + expect(receipt?.status).to.eq(1) + expect(event.args.subject).to.eq(subjectAddress) + expect(event.args.target).to.eq(targetAddress) + expect(event.args.evidence).to.eq(validEncodedNFTId) + expect((await policy.enforced(targetAddress, subjectAddress))[0]).to.be.equal(true) + }) + + it("should prevent to enforce twice", async () => { + const { policy, target, subjectAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + + await policy.setTarget(await target.getAddress()) + + await policy.connect(target).enforce(subjectAddress, validEncodedNFTId, 0) + + await expect( + policy.connect(target).enforce(subjectAddress, validEncodedNFTId, 0) + ).to.be.revertedWithCustomError(policy, "AlreadyEnforced") + }) + }) + + describe("main", () => { + it("should throw when the subject does not satisfy the chain of checks", async () => { + const { policy, target, subjectAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + + await policy.setTarget(await target.getAddress()) + + expect( + policy.connect(target).enforce(subjectAddress, validEncodedNFTId, 1) + ).to.be.revertedWithCustomError(policy, "PreCheckNotEnforced") + }) + + it("should throw when the subject does not satisfy the attributes", async () => { + const { policy, target, notOwnerAddress, subjectAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + + await policy.setTarget(await target.getAddress()) + await policy.connect(target).enforce(subjectAddress, validEncodedNFTId, 0) + + expect( + policy.connect(target).enforce(notOwnerAddress, validEncodedNFTId, 1) + ).to.be.revertedWithCustomError(policy, "UnsuccessfulCheck") + }) + + it("should enforce", async () => { + const { AdvancedERC721PolicyFactory, policy, target, subjectAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + const targetAddress = await target.getAddress() + + await policy.setTarget(await target.getAddress()) + await policy.connect(target).enforce(subjectAddress, validEncodedNFTId, 0) + + const tx = await policy.connect(target).enforce(subjectAddress, validEncodedNFTId, 1) + const receipt = await tx.wait() + const event = AdvancedERC721PolicyFactory.interface.parseLog( + receipt?.logs[0] as unknown as { topics: string[]; data: string } + ) as unknown as { + args: { + subject: string + target: string + evidence: string + } + } + + expect(receipt?.status).to.eq(1) + expect(event.args.subject).to.eq(subjectAddress) + expect(event.args.target).to.eq(targetAddress) + expect(event.args.evidence).to.eq(validEncodedNFTId) + expect((await policy.enforced(targetAddress, subjectAddress))[1]).to.be.equal(1) + }) + + it("should enforce twice when allowed", async () => { + const { AdvancedERC721PolicyFactory, policy, target, subjectAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + const targetAddress = await target.getAddress() + await policy.setTarget(targetAddress) + + await policy.connect(target).enforce(subjectAddress, validEncodedNFTId, 0) + await policy.connect(target).enforce(subjectAddress, validEncodedNFTId, 1) + + const tx = await policy.connect(target).enforce(subjectAddress, validEncodedNFTId, 1) + const receipt = await tx.wait() + const event = AdvancedERC721PolicyFactory.interface.parseLog( + receipt?.logs[0] as unknown as { topics: string[]; data: string } + ) as unknown as { + args: { + subject: string + target: string + evidence: string + } + } + + expect(receipt?.status).to.eq(1) + expect(event.args.subject).to.eq(subjectAddress) + expect(event.args.target).to.eq(targetAddress) + expect(event.args.evidence).to.eq(validEncodedNFTId) + expect((await policy.enforced(targetAddress, subjectAddress))[1]).to.be.equal(2) + }) + + it("should prevent to enforce twice when not allowed", async () => { + const { policySkipped, target, notOwnerAddress, subjectAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + + await policySkipped.setTarget(await target.getAddress()) + await policySkipped.connect(target).enforce(subjectAddress, validEncodedNFTId, 1) + + expect( + policySkipped.connect(target).enforce(notOwnerAddress, validEncodedNFTId, 1) + ).to.be.revertedWithCustomError(policySkipped, "MainCheckAlreadyEnforced") + }) + }) + + describe("post", () => { + it("should throw when the subject does not satisfy the chain of checks", async () => { + const { policy, target, subjectAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + + await policy.setTarget(await target.getAddress()) + + expect( + policy.connect(target).enforce(subjectAddress, validEncodedNFTId, 2) + ).to.be.revertedWithCustomError(policy, "PreCheckNotEnforced") + + await policy.connect(target).enforce(subjectAddress, validEncodedNFTId, 0) + + expect( + policy.connect(target).enforce(subjectAddress, validEncodedNFTId, 2) + ).to.be.revertedWithCustomError(policy, "MainCheckNotEnforced") + }) + + it("should throw when the callee is not the target", async () => { + const { policy, subject, target, subjectAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + + await policy.setTarget(await target.getAddress()) + await policy.connect(target).enforce(subjectAddress, validEncodedNFTId, 0) + await policy.connect(target).enforce(subjectAddress, validEncodedNFTId, 1) + + await expect( + policy.connect(subject).enforce(subjectAddress, ZeroHash, 2) + ).to.be.revertedWithCustomError(policy, "TargetOnly") + }) + + it("should throw when the evidence is not correct", async () => { + const { iERC721Errors, policy, target, subjectAddress, validEncodedNFTId, invalidEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + + await policy.setTarget(await target.getAddress()) + await policy.connect(target).enforce(subjectAddress, validEncodedNFTId, 0) + await policy.connect(target).enforce(subjectAddress, validEncodedNFTId, 1) + + await expect( + policy.connect(target).enforce(subjectAddress, invalidEncodedNFTId, 2) + ).to.be.revertedWithCustomError(iERC721Errors, "ERC721NonexistentToken") + }) + + it("should throw when the check is skipped", async () => { + const { checker, policySkipped, target, subjectAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + + await policySkipped.setTarget(await target.getAddress()) + await policySkipped.connect(target).enforce(subjectAddress, validEncodedNFTId, 1) + + await expect( + policySkipped.connect(target).enforce(subjectAddress, validEncodedNFTId, 2) + ).to.be.revertedWithCustomError(checker, "PostCheckSkipped") + }) + + it("should throw when the check returns false", async () => { + const { policy, target, subjectAddress, notOwnerAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + + await policy.setTarget(await target.getAddress()) + await policy.connect(target).enforce(subjectAddress, validEncodedNFTId, 0) + await policy.connect(target).enforce(subjectAddress, validEncodedNFTId, 1) + + expect( + policy.connect(target).enforce(notOwnerAddress, validEncodedNFTId, 2) + ).to.be.revertedWithCustomError(policy, "UnsuccessfulCheck") + }) + + it("should enforce", async () => { + const { AdvancedERC721PolicyFactory, policy, target, subjectAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + const targetAddress = await target.getAddress() + + await policy.setTarget(targetAddress) + await policy.connect(target).enforce(subjectAddress, validEncodedNFTId, 0) + await policy.connect(target).enforce(subjectAddress, validEncodedNFTId, 1) + + const tx = await policy.connect(target).enforce(subjectAddress, validEncodedNFTId, 2) + const receipt = await tx.wait() + const event = AdvancedERC721PolicyFactory.interface.parseLog( + receipt?.logs[0] as unknown as { topics: string[]; data: string } + ) as unknown as { + args: { + subject: string + target: string + evidence: string + } + } + + expect(receipt?.status).to.eq(1) + expect(event.args.subject).to.eq(subjectAddress) + expect(event.args.target).to.eq(targetAddress) + expect(event.args.evidence).to.eq(validEncodedNFTId) + expect((await policy.enforced(targetAddress, subjectAddress))[2]).to.be.equal(true) + }) + + it("should prevent to enforce twice", async () => { + const { policy, target, subjectAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + + await policy.setTarget(await target.getAddress()) + await policy.connect(target).enforce(subjectAddress, validEncodedNFTId, 0) + await policy.connect(target).enforce(subjectAddress, validEncodedNFTId, 1) + await policy.connect(target).enforce(subjectAddress, validEncodedNFTId, 2) + + await expect( + policy.connect(target).enforce(subjectAddress, validEncodedNFTId, 2) + ).to.be.revertedWithCustomError(policy, "AlreadyEnforced") + }) + }) + }) + + describe("_enforce()", () => { + describe("_pre", () => { + it("should throw when the callee is not the target", async () => { + const { policyHarness, subject, target, subjectAddress } = + await loadFixture(deployAdvancedPolicyFixture) + + await policyHarness.setTarget(await target.getAddress()) + + await expect( + policyHarness.connect(subject).enforce(subjectAddress, ZeroHash, 0) + ).to.be.revertedWithCustomError(policyHarness, "TargetOnly") + }) + + it("should throw when the evidence is not correct", async () => { + const { iERC721Errors, policyHarness, target, subjectAddress, invalidEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + + await policyHarness.setTarget(await target.getAddress()) + + await expect( + policyHarness.connect(target).enforce(subjectAddress, invalidEncodedNFTId, 0) + ).to.be.revertedWithCustomError(iERC721Errors, "ERC721NonexistentToken") + }) + + it("should throw when the check is skipped", async () => { + const { checker, policyHarnessSkipped, target, subjectAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + + await policyHarnessSkipped.setTarget(await target.getAddress()) + + await expect( + policyHarnessSkipped.connect(target).enforce(subjectAddress, validEncodedNFTId, 0) + ).to.be.revertedWithCustomError(checker, "PreCheckSkipped") + }) + + it("should throw when the check returns false", async () => { + const { policyHarness, target, notOwnerAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + + await policyHarness.setTarget(await target.getAddress()) + + expect( + policyHarness.connect(target).enforce(notOwnerAddress, validEncodedNFTId, 0) + ).to.be.revertedWithCustomError(policyHarness, "UnsuccessfulCheck") + }) + + it("should enforce", async () => { + const { AdvancedERC721PolicyFactory, policyHarness, target, subjectAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + const targetAddress = await target.getAddress() + + await policyHarness.setTarget(targetAddress) + + const tx = await policyHarness.connect(target).enforce(subjectAddress, validEncodedNFTId, 0) + const receipt = await tx.wait() + const event = AdvancedERC721PolicyFactory.interface.parseLog( + receipt?.logs[0] as unknown as { topics: string[]; data: string } + ) as unknown as { + args: { + subject: string + target: string + evidence: string + } + } + + expect(receipt?.status).to.eq(1) + expect(event.args.subject).to.eq(subjectAddress) + expect(event.args.target).to.eq(targetAddress) + expect(event.args.evidence).to.eq(validEncodedNFTId) + expect((await policyHarness.enforced(targetAddress, subjectAddress))[0]).to.be.equal(true) + }) + + it("should prevent to enforce twice", async () => { + const { policyHarness, target, subjectAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + + await policyHarness.setTarget(await target.getAddress()) + + await policyHarness.connect(target).enforce(subjectAddress, validEncodedNFTId, 0) + + await expect( + policyHarness.connect(target).enforce(subjectAddress, validEncodedNFTId, 0) + ).to.be.revertedWithCustomError(policyHarness, "AlreadyEnforced") + }) + }) + + describe("main", () => { + it("should throw when the subject does not satisfy the chain of checks", async () => { + const { policyHarness, target, subjectAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + + await policyHarness.setTarget(await target.getAddress()) + + expect( + policyHarness.connect(target).enforce(subjectAddress, validEncodedNFTId, 1) + ).to.be.revertedWithCustomError(policyHarness, "PreCheckNotEnforced") + }) + + it("should throw when the subject does not satisfy the attributes", async () => { + const { policyHarness, target, notOwnerAddress, subjectAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + + await policyHarness.setTarget(await target.getAddress()) + await policyHarness.connect(target).enforce(subjectAddress, validEncodedNFTId, 0) + + expect( + policyHarness.connect(target).enforce(notOwnerAddress, validEncodedNFTId, 1) + ).to.be.revertedWithCustomError(policyHarness, "UnsuccessfulCheck") + }) + + it("should enforce", async () => { + const { AdvancedERC721PolicyFactory, policyHarness, target, subjectAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + const targetAddress = await target.getAddress() + + await policyHarness.setTarget(await target.getAddress()) + await policyHarness.connect(target).enforce(subjectAddress, validEncodedNFTId, 0) + + const tx = await policyHarness.connect(target).enforce(subjectAddress, validEncodedNFTId, 1) + const receipt = await tx.wait() + const event = AdvancedERC721PolicyFactory.interface.parseLog( + receipt?.logs[0] as unknown as { topics: string[]; data: string } + ) as unknown as { + args: { + subject: string + target: string + evidence: string + } + } + + expect(receipt?.status).to.eq(1) + expect(event.args.subject).to.eq(subjectAddress) + expect(event.args.target).to.eq(targetAddress) + expect(event.args.evidence).to.eq(validEncodedNFTId) + expect((await policyHarness.enforced(targetAddress, subjectAddress))[1]).to.be.equal(1) + }) + + it("should enforce twice when allowed", async () => { + const { AdvancedERC721PolicyFactory, policyHarness, target, subjectAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + const targetAddress = await target.getAddress() + await policyHarness.setTarget(targetAddress) + + await policyHarness.connect(target).enforce(subjectAddress, validEncodedNFTId, 0) + await policyHarness.connect(target).enforce(subjectAddress, validEncodedNFTId, 1) + + const tx = await policyHarness.connect(target).enforce(subjectAddress, validEncodedNFTId, 1) + const receipt = await tx.wait() + const event = AdvancedERC721PolicyFactory.interface.parseLog( + receipt?.logs[0] as unknown as { topics: string[]; data: string } + ) as unknown as { + args: { + subject: string + target: string + evidence: string + } + } + + expect(receipt?.status).to.eq(1) + expect(event.args.subject).to.eq(subjectAddress) + expect(event.args.target).to.eq(targetAddress) + expect(event.args.evidence).to.eq(validEncodedNFTId) + expect((await policyHarness.enforced(targetAddress, subjectAddress))[1]).to.be.equal(2) + }) + + it("should prevent to enforce twice when not allowed", async () => { + const { policyHarnessSkipped, target, notOwnerAddress, subjectAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + + await policyHarnessSkipped.setTarget(await target.getAddress()) + await policyHarnessSkipped.connect(target).enforce(subjectAddress, validEncodedNFTId, 1) + + expect( + policyHarnessSkipped.connect(target).enforce(notOwnerAddress, validEncodedNFTId, 1) + ).to.be.revertedWithCustomError(policyHarnessSkipped, "MainCheckAlreadyEnforced") + expect( + policyHarnessSkipped.connect(target).enforce(subjectAddress, validEncodedNFTId, 1) + ).to.be.revertedWithCustomError(policyHarnessSkipped, "MainCheckAlreadyEnforced") + }) + }) + + describe("post", () => { + it("should throw when the subject does not satisfy the chain of checks", async () => { + const { policyHarness, target, subjectAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + + await policyHarness.setTarget(await target.getAddress()) + + expect( + policyHarness.connect(target).enforce(subjectAddress, validEncodedNFTId, 2) + ).to.be.revertedWithCustomError(policyHarness, "PreCheckNotEnforced") + + await policyHarness.connect(target).enforce(subjectAddress, validEncodedNFTId, 0) + + expect( + policyHarness.connect(target).enforce(subjectAddress, validEncodedNFTId, 2) + ).to.be.revertedWithCustomError(policyHarness, "MainCheckNotEnforced") + }) + + it("should throw when the callee is not the target", async () => { + const { policyHarness, subject, target, subjectAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + + await policyHarness.setTarget(await target.getAddress()) + await policyHarness.connect(target).enforce(subjectAddress, validEncodedNFTId, 0) + await policyHarness.connect(target).enforce(subjectAddress, validEncodedNFTId, 1) + + await expect( + policyHarness.connect(subject).enforce(subjectAddress, ZeroHash, 2) + ).to.be.revertedWithCustomError(policyHarness, "TargetOnly") + }) + + it("should throw when the evidence is not correct", async () => { + const { + iERC721Errors, + policyHarness, + target, + subjectAddress, + validEncodedNFTId, + invalidEncodedNFTId + } = await loadFixture(deployAdvancedPolicyFixture) + + await policyHarness.setTarget(await target.getAddress()) + await policyHarness.connect(target).enforce(subjectAddress, validEncodedNFTId, 0) + await policyHarness.connect(target).enforce(subjectAddress, validEncodedNFTId, 1) + + await expect( + policyHarness.connect(target).enforce(subjectAddress, invalidEncodedNFTId, 2) + ).to.be.revertedWithCustomError(iERC721Errors, "ERC721NonexistentToken") + }) + + it("should throw when the check is skipped", async () => { + const { checker, policyHarnessSkipped, target, subjectAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + + await policyHarnessSkipped.setTarget(await target.getAddress()) + await policyHarnessSkipped.connect(target).enforce(subjectAddress, validEncodedNFTId, 1) + + await expect( + policyHarnessSkipped.connect(target).enforce(subjectAddress, validEncodedNFTId, 2) + ).to.be.revertedWithCustomError(checker, "PostCheckSkipped") + }) + + it("should throw when the check returns false", async () => { + const { policyHarness, target, subjectAddress, notOwnerAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + + await policyHarness.setTarget(await target.getAddress()) + await policyHarness.connect(target).enforce(subjectAddress, validEncodedNFTId, 0) + await policyHarness.connect(target).enforce(subjectAddress, validEncodedNFTId, 1) + + expect( + policyHarness.connect(target).enforce(notOwnerAddress, validEncodedNFTId, 2) + ).to.be.revertedWithCustomError(policyHarness, "UnsuccessfulCheck") + }) + + it("should enforce", async () => { + const { AdvancedERC721PolicyFactory, policyHarness, target, subjectAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + const targetAddress = await target.getAddress() + + await policyHarness.setTarget(targetAddress) + await policyHarness.connect(target).enforce(subjectAddress, validEncodedNFTId, 0) + await policyHarness.connect(target).enforce(subjectAddress, validEncodedNFTId, 1) + + const tx = await policyHarness.connect(target).enforce(subjectAddress, validEncodedNFTId, 2) + const receipt = await tx.wait() + const event = AdvancedERC721PolicyFactory.interface.parseLog( + receipt?.logs[0] as unknown as { topics: string[]; data: string } + ) as unknown as { + args: { + subject: string + target: string + evidence: string + } + } + + expect(receipt?.status).to.eq(1) + expect(event.args.subject).to.eq(subjectAddress) + expect(event.args.target).to.eq(targetAddress) + expect(event.args.evidence).to.eq(validEncodedNFTId) + expect((await policyHarness.enforced(targetAddress, subjectAddress))[2]).to.be.equal(true) + }) + + it("should prevent to enforce twice", async () => { + const { policyHarness, target, subjectAddress, validEncodedNFTId } = + await loadFixture(deployAdvancedPolicyFixture) + + await policyHarness.setTarget(await target.getAddress()) + await policyHarness.connect(target).enforce(subjectAddress, validEncodedNFTId, 0) + await policyHarness.connect(target).enforce(subjectAddress, validEncodedNFTId, 1) + await policyHarness.connect(target).enforce(subjectAddress, validEncodedNFTId, 2) + + await expect( + policyHarness.connect(target).enforce(subjectAddress, validEncodedNFTId, 2) + ).to.be.revertedWithCustomError(policyHarness, "AlreadyEnforced") + }) + }) + }) + }) + + describe("Voting", () => { + async function deployAdvancedVotingFixture() { + const [deployer, subject, 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 AdvancedERC721CheckerFactory: AdvancedERC721Checker__factory = + await ethers.getContractFactory("AdvancedERC721Checker") + const AdvancedERC721PolicyFactory: AdvancedERC721Policy__factory = + await ethers.getContractFactory("AdvancedERC721Policy") + const AdvancedVotingFactory: AdvancedVoting__factory = await ethers.getContractFactory("AdvancedVoting") + + const nft: NFT = await NFTFactory.deploy() + const iERC721Errors: IERC721Errors = await ethers.getContractAt("IERC721Errors", await nft.getAddress()) + + const checker: AdvancedERC721Checker = await AdvancedERC721CheckerFactory.connect(deployer).deploy( + await nft.getAddress(), + 1, + 0, + 10, + false, + false, + true + ) + + const policy: AdvancedERC721Policy = await AdvancedERC721PolicyFactory.connect(deployer).deploy( + await checker.getAddress() + ) + + const voting: AdvancedVoting = await AdvancedVotingFactory.connect(deployer).deploy( + await policy.getAddress() + ) + + // mint 0 for subject. + await nft.connect(deployer).mint(subjectAddress) + + // encoded token ids. + const validNFTId = 0 + const invalidNFTId = 1 + const validEncodedNFTId = AbiCoder.defaultAbiCoder().encode(["uint256"], [validNFTId]) + const invalidEncodedNFTId = AbiCoder.defaultAbiCoder().encode(["uint256"], [invalidNFTId]) + + return { + iERC721Errors, + AdvancedVotingFactory, + nft, + voting, + policy, + subject, + deployer, + notOwner, + subjectAddress, + notOwnerAddress, + validNFTId, + invalidNFTId, + validEncodedNFTId, + invalidEncodedNFTId + } + } + + describe("constructor()", () => { + it("Should deploy the voting contract correctly", async () => { + const { voting } = await loadFixture(deployAdvancedVotingFixture) + + expect(voting).to.not.eq(undefined) + }) + }) + + describe("register()", () => { + it("Should revert when the callee is not the target", async () => { + const { voting, policy, notOwner, validNFTId } = await loadFixture(deployAdvancedVotingFixture) + + await policy.setTarget(await notOwner.getAddress()) + + await expect(voting.connect(notOwner).register(validNFTId)).to.be.revertedWithCustomError( + policy, + "TargetOnly" + ) + }) + + it("Should revert when the evidence is not correct", async () => { + const { iERC721Errors, voting, policy, subject, invalidNFTId } = + await loadFixture(deployAdvancedVotingFixture) + + await policy.setTarget(await voting.getAddress()) + + await expect(voting.connect(subject).register(invalidNFTId)).to.be.revertedWithCustomError( + iERC721Errors, + "ERC721NonexistentToken" + ) + }) + + it("should throw when the registration check returns false", async () => { + const { voting, policy, notOwner, validNFTId } = await loadFixture(deployAdvancedVotingFixture) + + await policy.setTarget(await voting.getAddress()) + + await expect(voting.connect(notOwner).register(validNFTId)).to.be.revertedWithCustomError( + policy, + "UnsuccessfulCheck" + ) + }) + + it("should register", async () => { + const { AdvancedVotingFactory, voting, policy, subject, validNFTId, subjectAddress } = + await loadFixture(deployAdvancedVotingFixture) + const targetAddress = await voting.getAddress() + + await policy.setTarget(targetAddress) + + const tx = await voting.connect(subject).register(validNFTId) + const receipt = await tx.wait() + const event = AdvancedVotingFactory.interface.parseLog( + receipt?.logs[1] as unknown as { topics: string[]; data: string } + ) as unknown as { + args: { + voter: string + } + } + + expect(receipt?.status).to.eq(1) + expect(event.args.voter).to.eq(subjectAddress) + expect((await policy.enforced(targetAddress, subjectAddress))[0]).to.be.equal(true) + expect((await policy.enforced(targetAddress, subjectAddress))[1]).to.be.equal(0n) + expect(await voting.voteCounts(0)).to.be.equal(0) + expect(await voting.voteCounts(1)).to.be.equal(0) + }) + + it("should prevent to register twice", async () => { + const { voting, policy, subject, validNFTId } = await loadFixture(deployAdvancedVotingFixture) + const targetAddress = await voting.getAddress() + + await policy.setTarget(targetAddress) + + await voting.connect(subject).register(validNFTId) + + await expect(voting.connect(subject).register(validNFTId)).to.be.revertedWithCustomError( + policy, + "AlreadyEnforced" + ) + }) + }) + + describe("vote()", () => { + it("Should revert when the callee is not registered", async () => { + const { voting, policy, subject } = await loadFixture(deployAdvancedVotingFixture) + + await policy.setTarget(await voting.getAddress()) + + await expect(voting.connect(subject).vote(0)).to.be.revertedWithCustomError(voting, "NotRegistered") + }) + + it("Should revert when the option is not correct", async () => { + const { voting, policy, subject, validNFTId } = await loadFixture(deployAdvancedVotingFixture) + + await policy.setTarget(await voting.getAddress()) + await voting.connect(subject).register(validNFTId) + + await expect(voting.connect(subject).vote(3)).to.be.revertedWithCustomError(voting, "InvalidOption") + }) + + it("should vote", async () => { + const { AdvancedVotingFactory, voting, policy, subject, subjectAddress, validNFTId } = + await loadFixture(deployAdvancedVotingFixture) + const option = 0 + const targetAddress = await voting.getAddress() + + await policy.setTarget(targetAddress) + await voting.connect(subject).register(validNFTId) + + const tx = await voting.connect(subject).vote(option) + const receipt = await tx.wait() + const event = AdvancedVotingFactory.interface.parseLog( + receipt?.logs[1] as unknown as { topics: string[]; data: string } + ) as unknown as { + args: { + voter: string + option: number + } + } + + expect(receipt?.status).to.eq(1) + expect(event.args.voter).to.eq(subjectAddress) + expect(event.args.option).to.eq(option) + expect((await policy.enforced(targetAddress, subjectAddress))[0]).to.be.equal(true) + expect((await policy.enforced(targetAddress, subjectAddress))[1]).to.be.equal(1n) + expect(await voting.voteCounts(0)).to.be.equal(1) + expect(await voting.voteCounts(1)).to.be.equal(0) + }) + + it("should vote twice", async () => { + const { AdvancedVotingFactory, voting, policy, subject, subjectAddress, validNFTId } = + await loadFixture(deployAdvancedVotingFixture) + const option = 0 + const targetAddress = await voting.getAddress() + + await policy.setTarget(targetAddress) + await voting.connect(subject).register(validNFTId) + await voting.connect(subject).vote(option) + + const tx = await voting.connect(subject).vote(option) + const receipt = await tx.wait() + const event = AdvancedVotingFactory.interface.parseLog( + receipt?.logs[1] as unknown as { topics: string[]; data: string } + ) as unknown as { + args: { + voter: string + option: number + } + } + + expect(receipt?.status).to.eq(1) + expect(event.args.voter).to.eq(subjectAddress) + expect(event.args.option).to.eq(option) + expect((await policy.enforced(targetAddress, subjectAddress))[0]).to.be.equal(true) + expect((await policy.enforced(targetAddress, subjectAddress))[1]).to.be.equal(2n) + expect(await voting.voteCounts(0)).to.be.equal(2) + expect(await voting.voteCounts(1)).to.be.equal(0) + }) + }) + + describe("reward()", () => { + it("Should revert when the callee is not the target", async () => { + const { voting, policy, subject, notOwner, validNFTId } = await loadFixture(deployAdvancedVotingFixture) + + await policy.setTarget(await notOwner.getAddress()) + + await expect(voting.connect(subject).register(validNFTId)).to.be.revertedWithCustomError( + policy, + "TargetOnly" + ) + }) + + it("Should revert when the evidence is not correct", async () => { + const { iERC721Errors, voting, policy, subject, validNFTId, invalidNFTId } = + await loadFixture(deployAdvancedVotingFixture) + + await policy.setTarget(await voting.getAddress()) + await voting.connect(subject).register(validNFTId) + await voting.connect(subject).vote(0) + + await expect(voting.connect(subject).reward(invalidNFTId)).to.be.revertedWithCustomError( + iERC721Errors, + "ERC721NonexistentToken" + ) + }) + + it("should throw when the rewared check returns false", async () => { + const { nft, deployer, voting, policy, notOwner, subject, validNFTId } = + await loadFixture(deployAdvancedVotingFixture) + + await policy.setTarget(await voting.getAddress()) + await nft.connect(deployer).mint(notOwner) + await voting.connect(subject).register(validNFTId) + await voting.connect(subject).vote(0) + await voting.connect(notOwner).register(1) + await voting.connect(notOwner).vote(0) + + await expect(voting.connect(subject).reward(1)).to.be.revertedWithCustomError( + policy, + "UnsuccessfulCheck" + ) + }) + + it("Should revert when the callee is not registered", async () => { + const { voting, policy, notOwner, validNFTId } = await loadFixture(deployAdvancedVotingFixture) + + await policy.setTarget(await notOwner.getAddress()) + + await expect(voting.connect(notOwner).reward(validNFTId)).to.be.revertedWithCustomError( + voting, + "NotRegistered" + ) + }) + + it("Should revert when the callee has not voted", async () => { + const { voting, policy, subject, validNFTId } = await loadFixture(deployAdvancedVotingFixture) + + await policy.setTarget(await voting.getAddress()) + await voting.connect(subject).register(validNFTId) + + await expect(voting.connect(subject).reward(validNFTId)).to.be.revertedWithCustomError( + voting, + "NotVoted" + ) + }) + + it("should get the reward", async () => { + const { AdvancedVotingFactory, voting, policy, subject, subjectAddress, validNFTId } = + await loadFixture(deployAdvancedVotingFixture) + const targetAddress = await voting.getAddress() + + await policy.setTarget(targetAddress) + await voting.connect(subject).register(validNFTId) + await voting.connect(subject).vote(0) + + const tx = await voting.connect(subject).reward(validNFTId) + const receipt = await tx.wait() + const event = AdvancedVotingFactory.interface.parseLog( + receipt?.logs[1] as unknown as { topics: string[]; data: string } + ) as unknown as { + args: { + voter: string + } + } + + expect(receipt?.status).to.eq(1) + expect(event.args.voter).to.eq(subjectAddress) + expect((await policy.enforced(targetAddress, subjectAddress))[0]).to.be.equal(true) + expect((await policy.enforced(targetAddress, subjectAddress))[1]).to.be.equal(1n) + expect((await policy.enforced(targetAddress, subjectAddress))[2]).to.be.equal(true) + expect(await voting.voteCounts(0)).to.be.equal(1) + expect(await voting.voteCounts(1)).to.be.equal(0) + }) + + it("should prevent to reward twice", async () => { + const { voting, policy, subject, validNFTId } = await loadFixture(deployAdvancedVotingFixture) + + await policy.setTarget(await voting.getAddress()) + await voting.connect(subject).register(validNFTId) + await voting.connect(subject).vote(0) + await voting.connect(subject).reward(validNFTId) + + await expect(voting.connect(subject).reward(validNFTId)).to.be.revertedWithCustomError( + voting, + "AlreadyClaimed" + ) + }) + }) + describe("e2e", () => { + it("should submit a vote for each subject", async () => { + const [deployer]: Signer[] = await ethers.getSigners() + + const NFTFactory: NFT__factory = await ethers.getContractFactory("NFT") + const AdvancedERC721CheckerFactory: AdvancedERC721Checker__factory = + await ethers.getContractFactory("AdvancedERC721Checker") + const AdvancedERC721PolicyFactory: AdvancedERC721Policy__factory = + await ethers.getContractFactory("AdvancedERC721Policy") + const AdvancedVotingFactory: AdvancedVoting__factory = await ethers.getContractFactory("AdvancedVoting") + + const nft: NFT = await NFTFactory.deploy() + + const checker: AdvancedERC721Checker = await AdvancedERC721CheckerFactory.connect(deployer).deploy( + await nft.getAddress(), + 1, + 0, + 20, + false, + false, + true + ) + + const policy: AdvancedERC721Policy = await AdvancedERC721PolicyFactory.connect(deployer).deploy( + await checker.getAddress() + ) + + const voting: AdvancedVoting = await AdvancedVotingFactory.connect(deployer).deploy( + await policy.getAddress() + ) + + // set the target. + const targetAddress = await voting.getAddress() + await policy.setTarget(targetAddress) + + for (const [tokenId, voter] of (await ethers.getSigners()).entries()) { + const voterAddress = await voter.getAddress() + + // mint for voter. + await nft.connect(deployer).mint(voterAddress) + + // register. + await voting.connect(voter).register(tokenId) + + // vote. + await voting.connect(voter).vote(tokenId % 2) + + // reward. + await voting.connect(voter).reward(tokenId) + + expect((await policy.enforced(targetAddress, voterAddress))[0]).to.be.equal(true) + expect((await policy.enforced(targetAddress, voterAddress))[1]).to.be.equal(1) + expect((await policy.enforced(targetAddress, voterAddress))[2]).to.be.equal(true) + } + }) + }) + }) +})