-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(excubiae): add ERC721Excubia contract
- Loading branch information
Showing
3 changed files
with
237 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity >=0.8.0; | ||
|
||
import {Excubia} from "../Excubia.sol"; | ||
import {IERC721} from "@openzeppelin/contracts/interfaces/IERC721.sol"; | ||
|
||
/// @title ERC721 Excubia Contract. | ||
/// @notice This contract extends the Excubia contract to integrate with an ERC721 token. | ||
/// This contract checks the ownership of an ERC721 token to permit access through the gate. | ||
/// @dev The contract refers to a contract implementing the ERC721 standard to admit the owner of the token. | ||
contract ERC721Excubia is Excubia { | ||
/// @notice The ERC721 token contract interface. | ||
IERC721 public immutable NFT; | ||
|
||
/// @notice Mapping to track which token IDs have been registered by the contract to | ||
/// avoid double checks with the same token ID. | ||
mapping(uint256 => bool) public registeredTokenIds; | ||
|
||
/// @notice Error thrown when the passerby is not the owner of the token. | ||
error UnexpectedTokenOwner(); | ||
|
||
/// @notice Constructor to initialize with target ERC721 contract. | ||
/// @param _erc721 The address of the ERC721 contract. | ||
constructor(address _erc721) payable { | ||
if (_erc721 == address(0)) revert ZeroAddress(); | ||
|
||
NFT = IERC721(_erc721); | ||
} | ||
|
||
/// @notice Internal function to handle the passing logic with check. | ||
/// @dev Calls the parent `_pass` function and registers the NFT ID to avoid double checks. | ||
/// @param passerby The address of the entity attempting to pass the gate. | ||
/// @param data Additional data required for the check (e.g., encoded token ID). | ||
function _pass(address passerby, bytes calldata data) internal override { | ||
super._pass(passerby, data); | ||
|
||
uint256 tokenId = abi.decode(data, (uint256)); | ||
|
||
// Avoiding double check of the same token ID. | ||
if (registeredTokenIds[tokenId]) revert AlreadyRegistered(); | ||
|
||
registeredTokenIds[tokenId] = true; | ||
} | ||
|
||
/// @notice Internal function to handle the gate protection (token ownership check) logic. | ||
/// @dev Checks if the passerby is the owner of the token. | ||
/// @param passerby The address of the entity attempting to pass the gate. | ||
/// @param data Additional data required for the check (e.g., encoded token ID). | ||
/// @return True if the passerby owns the token, false otherwise. | ||
function _check(address passerby, bytes calldata data) internal view override returns (bool) { | ||
uint256 tokenId = abi.decode(data, (uint256)); | ||
|
||
// Check if the user owns the token. | ||
if (!(NFT.ownerOf(tokenId) == passerby)) revert UnexpectedTokenOwner(); | ||
|
||
return true; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity >=0.8.0; | ||
|
||
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; | ||
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; | ||
|
||
/// @title Mock ERC721 Token Contract. | ||
/// @notice This contract is a mock implementation of the ERC721 standard for testing purposes. | ||
/// @dev It simulates the behavior of a real ERC721 contract by providing basic minting functionality. | ||
contract MockERC721 is ERC721, Ownable(msg.sender) { | ||
/// @notice A counter to keep track of the token IDs. | ||
uint256 private _tokenIdCounter; | ||
|
||
/// @notice Constructor to initialize the mock contract with a name and symbol. | ||
constructor() payable ERC721("MockERC721Token", "MockERC721Token") {} | ||
|
||
/// @notice Mints a new token and assigns it to the specified recipient. | ||
/// @param recipient The address that will receive the minted token. | ||
function mintAndGiveToken(address recipient) public onlyOwner { | ||
_safeMint(recipient, _tokenIdCounter++); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
import { expect } from "chai" | ||
import { ethers } from "hardhat" | ||
import { AbiCoder, Signer, ZeroAddress } from "ethers" | ||
import { ERC721Excubia, ERC721Excubia__factory, MockERC721, MockERC721__factory } from "../typechain-types" | ||
|
||
describe("ERC721Excubia", function () { | ||
let MockERC721Contract: MockERC721__factory | ||
let ERC721ExcubiaContract: ERC721Excubia__factory | ||
let erc721Excubia: ERC721Excubia | ||
|
||
let signer: Signer | ||
let signerAddress: string | ||
|
||
let gate: Signer | ||
let gateAddress: string | ||
|
||
let anotherTokenOwner: Signer | ||
let anotherTokenOwnerAddress: string | ||
|
||
let mockERC721: MockERC721 | ||
let mockERC721Address: string | ||
|
||
const encodedValidTokenId = AbiCoder.defaultAbiCoder().encode(["uint256"], [0]) | ||
const encodedInvalidOwnerTokenId = AbiCoder.defaultAbiCoder().encode(["uint256"], [1]) | ||
const decodedValidTokenId = AbiCoder.defaultAbiCoder().decode(["uint256"], encodedValidTokenId) | ||
const decodedInvalidOwnerTokenId = AbiCoder.defaultAbiCoder().decode(["uint256"], encodedInvalidOwnerTokenId) | ||
|
||
before(async function () { | ||
;[signer, gate, anotherTokenOwner] = await ethers.getSigners() | ||
signerAddress = await signer.getAddress() | ||
gateAddress = await gate.getAddress() | ||
anotherTokenOwnerAddress = await anotherTokenOwner.getAddress() | ||
|
||
MockERC721Contract = await ethers.getContractFactory("MockERC721") | ||
mockERC721 = await MockERC721Contract.deploy() | ||
mockERC721Address = await mockERC721.getAddress() | ||
|
||
// assign to `signerAddress` token with id equal to `1`. | ||
await mockERC721.mintAndGiveToken(signerAddress) | ||
await mockERC721.mintAndGiveToken(anotherTokenOwnerAddress) | ||
|
||
ERC721ExcubiaContract = await ethers.getContractFactory("ERC721Excubia") | ||
erc721Excubia = await ERC721ExcubiaContract.deploy(mockERC721Address) | ||
}) | ||
|
||
describe("constructor()", function () { | ||
it("Should deploy the ERC721Excubia contract correctly", async function () { | ||
expect(erc721Excubia).to.not.eq(undefined) | ||
}) | ||
|
||
it("Should deploy the MockERC721 contract correctly", async function () { | ||
expect(mockERC721).to.not.eq(undefined) | ||
}) | ||
|
||
it("Should fail to deploy ERC721Excubia when erc721 parameter is not valid", async () => { | ||
await expect(ERC721ExcubiaContract.deploy(ZeroAddress)).to.be.revertedWithCustomError( | ||
erc721Excubia, | ||
"ZeroAddress" | ||
) | ||
}) | ||
}) | ||
|
||
describe("setGate()", function () { | ||
it("should fail to set the gate when the caller is not the owner", async () => { | ||
const [, notOwnerSigner] = await ethers.getSigners() | ||
|
||
await expect(erc721Excubia.connect(notOwnerSigner).setGate(gateAddress)).to.be.revertedWithCustomError( | ||
erc721Excubia, | ||
"OwnableUnauthorizedAccount" | ||
) | ||
}) | ||
|
||
it("should fail to set the gate when the gate address is zero", async () => { | ||
await expect(erc721Excubia.setGate(ZeroAddress)).to.be.revertedWithCustomError(erc721Excubia, "ZeroAddress") | ||
}) | ||
|
||
it("Should set the gate contract address correctly", async function () { | ||
const tx = await erc721Excubia.setGate(gateAddress) | ||
const receipt = await tx.wait() | ||
const event = ERC721ExcubiaContract.interface.parseLog( | ||
receipt?.logs[0] as unknown as { topics: string[]; data: string } | ||
) as unknown as { | ||
args: { | ||
gate: string | ||
} | ||
} | ||
|
||
expect(receipt?.status).to.eq(1) | ||
expect(event.args.gate).to.eq(gateAddress) | ||
expect(await erc721Excubia.gate()).to.eq(gateAddress) | ||
}) | ||
|
||
it("Should fail to set the gate if already set", async function () { | ||
await expect(erc721Excubia.setGate(gateAddress)).to.be.revertedWithCustomError( | ||
erc721Excubia, | ||
"GateAlreadySet" | ||
) | ||
}) | ||
}) | ||
|
||
describe("check()", function () { | ||
it("should throw when the token id is not owned by the correct recipient", async () => { | ||
expect(await mockERC721.ownerOf(encodedInvalidOwnerTokenId)).to.be.equal(anotherTokenOwnerAddress) | ||
|
||
await expect(erc721Excubia.check(signerAddress, encodedInvalidOwnerTokenId)).to.be.revertedWithCustomError( | ||
erc721Excubia, | ||
"UnexpectedTokenOwner" | ||
) | ||
}) | ||
|
||
it("should pass the check", async () => { | ||
const passed = await erc721Excubia.check(signerAddress, encodedValidTokenId) | ||
|
||
expect(passed).to.be.true | ||
// check does NOT change the state of the contract (see pass()). | ||
expect(await erc721Excubia.registeredTokenIds(decodedValidTokenId[0])).to.be.false | ||
}) | ||
}) | ||
|
||
describe("pass()", function () { | ||
it("should throw when the callee is not the gate", async () => { | ||
await expect( | ||
erc721Excubia.connect(signer).pass(signerAddress, encodedInvalidOwnerTokenId) | ||
).to.be.revertedWithCustomError(erc721Excubia, "GateOnly") | ||
}) | ||
|
||
it("should throw when the token id is not owned by the correct recipient", async () => { | ||
await expect( | ||
erc721Excubia.connect(gate).pass(signerAddress, encodedInvalidOwnerTokenId) | ||
).to.be.revertedWithCustomError(erc721Excubia, "UnexpectedTokenOwner") | ||
}) | ||
|
||
it("should pass the check", async () => { | ||
const tx = await erc721Excubia.connect(gate).pass(signerAddress, encodedValidTokenId) | ||
const receipt = await tx.wait() | ||
const event = ERC721ExcubiaContract.interface.parseLog( | ||
receipt?.logs[0] as unknown as { topics: string[]; data: string } | ||
) as unknown as { | ||
args: { | ||
passerby: string | ||
gate: string | ||
} | ||
} | ||
|
||
expect(receipt?.status).to.eq(1) | ||
expect(event.args.passerby).to.eq(signerAddress) | ||
expect(event.args.gate).to.eq(gateAddress) | ||
expect(await erc721Excubia.registeredTokenIds(decodedValidTokenId[0])).to.be.true | ||
}) | ||
|
||
it("should prevent to pass twice", async () => { | ||
await expect( | ||
erc721Excubia.connect(gate).pass(signerAddress, encodedValidTokenId) | ||
).to.be.revertedWithCustomError(erc721Excubia, "AlreadyRegistered") | ||
}) | ||
}) | ||
}) |