Skip to content

Commit

Permalink
test(contracts): add harness for checker and policy to hardhat testing
Browse files Browse the repository at this point in the history
  • Loading branch information
0xjei committed Nov 28, 2024
1 parent 0b07847 commit 39b1b30
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 20 deletions.
10 changes: 1 addition & 9 deletions packages/contracts/.solcover.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
module.exports = {

Check warning on line 1 in packages/contracts/.solcover.js

View workflow job for this annotation

GitHub Actions / style

File ignored by default.
istanbulFolder: "../../coverage/contracts",
skipFiles: [
"test/Base.t.sol",
"test/base/BaseERC721Checker.sol",
"test/base/BaseERC721Policy.sol",
"test/base/BaseVoting.sol",
"test/utils/NFT.sol",
"test/wrappers/BaseERC721CheckerHarness.sol",
"test/wrappers/BaseERC721PolicyHarness.sol"
]
skipFiles: ["test"]
}
4 changes: 3 additions & 1 deletion packages/contracts/contracts/src/BaseChecker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ 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.
Expand All @@ -19,5 +21,5 @@ abstract contract BaseChecker is IBaseChecker {
/// @notice Internal method to perform the actual check logic.
/// @param subject The address to be checked.
/// @param evidence The evidence associated with the check.
function _check(address subject, bytes memory evidence) internal view virtual returns (bool checked);
function _check(address subject, bytes memory evidence) internal view virtual returns (bool checked) {}
}
21 changes: 18 additions & 3 deletions packages/contracts/contracts/src/test/base/BaseERC721Checker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,33 @@ pragma solidity 0.8.27;
import {BaseChecker} from "../../../src/BaseChecker.sol";
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";

/**
* @title BaseERC721Checker
* @notice Implements basic token ownership validation for ERC721 tokens.
* @dev Extends BaseChecker to provide simple ownership verification.
*/
contract BaseERC721Checker is BaseChecker {
IERC721 public immutable NFT;

/**
* @notice Initializes the checker with an ERC721 token contract.
* @param _nft Address of the ERC721 contract to check against.
*/
constructor(IERC721 _nft) {
NFT = IERC721(_nft);
}

/**
* @notice Checks if the subject owns the specified token.
* @dev Validates if the subject is the owner of the 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 _check(address subject, bytes memory evidence) internal view override returns (bool) {
// Decode the tokenId from the evidence.
uint256 tokenId = abi.decode(evidence, (uint256));
super._check(subject, evidence);

// Return true if the subject is the owner of the tokenId, false otherwise.
uint256 tokenId = abi.decode(evidence, (uint256));
return NFT.ownerOf(tokenId) == subject;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@ pragma solidity 0.8.27;
import {BasePolicy} from "../../../src/BasePolicy.sol";
import {BaseERC721Checker} from "./BaseERC721Checker.sol";

/**
* @title BaseERC721Policy
* @notice Policy contract for basic ERC721 token validation.
* @dev Extends BasePolicy to enforce NFT ownership checks.
*/
contract BaseERC721Policy is BasePolicy {
/// @notice Reference to the checker contract for token validation.
BaseERC721Checker public immutable CHECKER;

constructor(BaseERC721Checker _checker) BasePolicy(_checker) {
CHECKER = BaseERC721Checker(_checker);
}

/// @notice Returns the trait identifier for this policy.
function trait() external pure returns (string memory) {
return "BaseERC721";
}
Expand Down
18 changes: 12 additions & 6 deletions packages/contracts/contracts/src/test/base/BaseVoting.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ pragma solidity 0.8.27;

import {BaseERC721Policy} from "./BaseERC721Policy.sol";

/**
* @title BaseVoting
* @notice Basic voting contract with NFT-based access control.
* @dev Uses BaseERC721Policy for voter validation.
*/
contract BaseVoting {
event Registered(address voter);
event Voted(address voter, uint8 option);
Expand All @@ -11,34 +16,35 @@ contract BaseVoting {
error AlreadyVoted();
error InvalidOption();

/// @notice Policy contract for voter validation.
BaseERC721Policy public immutable POLICY;

// Mapping to track if an address has voted
/// @notice Tracks if an address has voted.
mapping(address => bool) public hasVoted;
// Mapping to count votes for each option
/// @notice Counts votes for each option.
mapping(uint8 => uint256) public voteCounts;

constructor(BaseERC721Policy _policy) {
POLICY = _policy;
}

// Function to register a voter using a the policy enforcement.
/// @notice Register voter using NFT ownership verification.
/// @param tokenId Token ID to verify ownership.
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.
/// @notice Cast vote for given option.
/// @param option Voting option (0 or 1).
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ contract BaseERC721PolicyHarness is BaseERC721Policy {
/// @notice Exposes the internal `_enforce` method for testing purposes.
/// @param subject The address to be checked.
/// @param evidence The data associated with the check.
function exposed__enforce(address subject, bytes calldata evidence) public {
function exposed__enforce(address subject, bytes calldata evidence) public onlyTarget {
_enforce(subject, evidence);
}
}
118 changes: 118 additions & 0 deletions packages/contracts/test/Base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import {
BaseERC721Checker__factory,
BaseERC721Policy,
BaseERC721Policy__factory,
BaseERC721CheckerHarness,
BaseERC721CheckerHarness__factory,
BaseERC721PolicyHarness,
BaseERC721PolicyHarness__factory,
NFT,
NFT__factory,
IERC721Errors,
Expand All @@ -24,11 +28,16 @@ describe("Base", () => {
const NFTFactory: NFT__factory = await ethers.getContractFactory("NFT")
const BaseERC721CheckerFactory: BaseERC721Checker__factory =
await ethers.getContractFactory("BaseERC721Checker")
const BaseERC721CheckerHarnessFactory: BaseERC721CheckerHarness__factory =
await ethers.getContractFactory("BaseERC721CheckerHarness")

const nft: NFT = await NFTFactory.deploy()
const checker: BaseERC721Checker = await BaseERC721CheckerFactory.connect(deployer).deploy(
await nft.getAddress()
)
const checkerHarness: BaseERC721CheckerHarness = await BaseERC721CheckerHarnessFactory.connect(
deployer
).deploy(await nft.getAddress())

// mint 0 for subject.
await nft.connect(deployer).mint(subjectAddress)
Expand All @@ -40,6 +49,7 @@ describe("Base", () => {
return {
nft,
checker,
checkerHarness,
target,
subjectAddress,
notOwnerAddress,
Expand Down Expand Up @@ -79,6 +89,35 @@ describe("Base", () => {
expect(await checker.connect(target).check(subjectAddress, validNFTId)).to.be.equal(true)
})
})

describe("_check()", () => {
it("should revert the check when the evidence is not meaningful", async () => {
const { nft, checkerHarness, target, subjectAddress, invalidNFTId } =
await loadFixture(deployBaseCheckerFixture)

await expect(
checkerHarness.connect(target).exposed__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 { checkerHarness, target, notOwnerAddress, validNFTId } =
await loadFixture(deployBaseCheckerFixture)

expect(await checkerHarness.connect(target).exposed__check(notOwnerAddress, validNFTId)).to.be.equal(
false
)
})

it("should check", async () => {
const { checkerHarness, target, subjectAddress, validNFTId } =
await loadFixture(deployBaseCheckerFixture)

expect(await checkerHarness.connect(target).exposed__check(subjectAddress, validNFTId)).to.be.equal(
true
)
})
})
})

describe("Policy", () => {
Expand All @@ -92,6 +131,8 @@ describe("Base", () => {
await ethers.getContractFactory("BaseERC721Checker")
const BaseERC721PolicyFactory: BaseERC721Policy__factory =
await ethers.getContractFactory("BaseERC721Policy")
const BaseERC721PolicyHarnessFactory: BaseERC721PolicyHarness__factory =
await ethers.getContractFactory("BaseERC721PolicyHarness")

const nft: NFT = await NFTFactory.deploy()
const iERC721Errors: IERC721Errors = await ethers.getContractAt("IERC721Errors", await nft.getAddress())
Expand All @@ -102,6 +143,9 @@ describe("Base", () => {
const policy: BaseERC721Policy = await BaseERC721PolicyFactory.connect(deployer).deploy(
await checker.getAddress()
)
const policyHarness: BaseERC721PolicyHarness = await BaseERC721PolicyHarnessFactory.connect(
deployer
).deploy(await checker.getAddress())

// mint 0 for subject.
await nft.connect(deployer).mint(subjectAddress)
Expand All @@ -114,6 +158,7 @@ describe("Base", () => {
iERC721Errors,
BaseERC721PolicyFactory,
nft,
policyHarness,
policy,
subject,
deployer,
Expand Down Expand Up @@ -261,6 +306,79 @@ describe("Base", () => {
).to.be.revertedWithCustomError(policy, "AlreadyEnforced")
})
})

describe("_enforce()", () => {
it("should throw when the callee is not the target", async () => {
const { policyHarness, subject, target, subjectAddress } = await loadFixture(deployBasePolicyFixture)

await policyHarness.setTarget(await target.getAddress())

await expect(
policyHarness.connect(subject).exposed__enforce(subjectAddress, ZeroHash)
).to.be.revertedWithCustomError(policyHarness, "TargetOnly")
})

it("should throw when the evidence is not correct", async () => {
const { iERC721Errors, policyHarness, target, subjectAddress, invalidEncodedNFTId } =
await loadFixture(deployBasePolicyFixture)

await policyHarness.setTarget(await target.getAddress())

await expect(
policyHarness.connect(target).exposed__enforce(subjectAddress, invalidEncodedNFTId)
).to.be.revertedWithCustomError(iERC721Errors, "ERC721NonexistentToken")
})

it("should throw when the check returns false", async () => {
const { policyHarness, target, notOwnerAddress, validEncodedNFTId } =
await loadFixture(deployBasePolicyFixture)

await policyHarness.setTarget(await target.getAddress())

expect(
policyHarness.connect(target).exposed__enforce(notOwnerAddress, validEncodedNFTId)
).to.be.revertedWithCustomError(policyHarness, "UnsuccessfulCheck")
})

it("should enforce", async () => {
const { BaseERC721PolicyFactory, policyHarness, target, subjectAddress, validEncodedNFTId } =
await loadFixture(deployBasePolicyFixture)
const targetAddress = await target.getAddress()

await policyHarness.setTarget(await target.getAddress())

const tx = await policyHarness.connect(target).exposed__enforce(subjectAddress, validEncodedNFTId)
const receipt = await tx.wait()
const event = BaseERC721PolicyFactory.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)).to.be.equal(true)
})

it("should prevent to enforce twice", async () => {
const { policyHarness, target, subjectAddress, validEncodedNFTId } =
await loadFixture(deployBasePolicyFixture)

await policyHarness.setTarget(await target.getAddress())

await policyHarness.connect(target).exposed__enforce(subjectAddress, validEncodedNFTId)

await expect(
policyHarness.connect(target).enforce(subjectAddress, validEncodedNFTId)
).to.be.revertedWithCustomError(policyHarness, "AlreadyEnforced")
})
})
})

describe("Voting", () => {
Expand Down

0 comments on commit 39b1b30

Please sign in to comment.