Skip to content

Commit f8bc490

Browse files
committed
refactor(excubiae): improve framework design to favor Excubia reuse; minor changes
developers need to define the _check and _pass internal methods logic support for solidity >=0.8.0 with a default compiler version to 0.8.20
1 parent 3b19355 commit f8bc490

File tree

7 files changed

+186
-90
lines changed

7 files changed

+186
-90
lines changed

packages/excubiae/contracts/Excubia.sol

+24-8
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
// SPDX-License-Identifier: MIT
2-
pragma solidity >=0.8.0 <0.9.0;
2+
pragma solidity >=0.8.0;
33

44
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
55
import {IExcubia} from "./IExcubia.sol";
66

77
/// @title Excubia.
88
/// @notice Abstract base contract which can be extended to implement a specific excubia.
9-
/// @dev Inherit from this contract and implement the `_check` method to define
10-
/// the custom gatekeeping logic.
9+
/// @dev Inherit from this contract and implement the `_pass` & `_check` methods to define
10+
/// your custom gatekeeping logic.
1111
abstract contract Excubia is IExcubia, Ownable(msg.sender) {
1212
/// @notice The excubia-protected contract address.
13-
/// @dev The gate can be any contract address that requires a prior `_check`.
13+
/// @dev The gate can be any contract address that requires a prior check to enable logic.
1414
/// For example, the gate is a Semaphore group that requires the passerby
1515
/// to meet certain criteria before joining.
1616
address public gate;
@@ -27,18 +27,34 @@ abstract contract Excubia is IExcubia, Ownable(msg.sender) {
2727
if (gate != address(0)) revert GateAlreadySet();
2828

2929
gate = _gate;
30+
31+
emit GateSet(_gate);
32+
}
33+
34+
/// @inheritdoc IExcubia
35+
function pass(address passerby, bytes calldata data) external onlyGate {
36+
_pass(passerby, data);
3037
}
3138

3239
/// @inheritdoc IExcubia
33-
function pass(address passerby, bytes calldata data) public virtual onlyGate {
40+
function check(address passerby, bytes calldata data) external view returns (bool) {
41+
return _check(passerby, data);
42+
}
43+
44+
/// @notice Internal function to enforce the custom gate passing logic.
45+
/// @dev Calls the `_check` internal logic and emits the relative event if successful.
46+
/// @param passerby The address of the entity attempting to pass the gate.
47+
/// @param data Additional data required for the check (e.g., encoded token identifier).
48+
function _pass(address passerby, bytes calldata data) internal virtual {
3449
if (!_check(passerby, data)) revert AccessDenied();
3550

3651
emit GatePassed(passerby, gate);
3752
}
3853

39-
/// @dev Abstract internal function to be implemented with custom logic to check if the passerby can pass the gate.
54+
/// @notice Internal function to define the custom gate protection logic.
55+
/// @dev Custom logic to determine if the passerby can pass the gate.
4056
/// @param passerby The address of the entity attempting to pass the gate.
4157
/// @param data Additional data that may be required for the check.
42-
/// @return True if the passerby passes the check, false otherwise.
43-
function _check(address passerby, bytes calldata data) internal virtual returns (bool);
58+
/// @return passed True if the passerby passes the check, false otherwise.
59+
function _check(address passerby, bytes calldata data) internal view virtual returns (bool passed) {}
4460
}

packages/excubiae/contracts/IExcubia.sol

+14-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
// SPDX-License-Identifier: MIT
2-
pragma solidity >=0.8.0 <0.9.0;
2+
pragma solidity >=0.8.0;
33

44
/// @title IExcubia.
55
/// @notice Excubia contract interface.
66
interface IExcubia {
7-
/// @notice Event emitted when someone passes the `_check` method.
7+
/// @notice Event emitted when someone passes the gate check.
88
/// @param passerby The address of those who have successfully passed the check.
99
/// @param gate The address of the excubia-protected contract address.
1010
event GatePassed(address indexed passerby, address indexed gate);
1111

12+
/// @notice Event emitted when the gate address is set.
13+
/// @param gate The address of the contract set as the gate.
14+
event GateSet(address indexed gate);
15+
1216
/// @notice Error thrown when an address equal to zero is given.
1317
error ZeroAddress();
1418

@@ -29,9 +33,15 @@ interface IExcubia {
2933
/// @param _gate The address of the contract to be set as the gate.
3034
function setGate(address _gate) external;
3135

32-
/// @notice Initiates the excubia's check and triggers the associated action if the check is passed.
33-
/// @dev Calls `_check` to handle the logic of checking for passing the gate.
36+
/// @notice Enforces the custom gate passing logic.
37+
/// @dev Must call the `check` to handle the logic of checking passerby for specific gate.
3438
/// @param passerby The address of the entity attempting to pass the gate.
3539
/// @param data Additional data required for the check (e.g., encoded token identifier).
3640
function pass(address passerby, bytes calldata data) external;
41+
42+
/// @dev Defines the custom gate protection logic.
43+
/// @param passerby The address of the entity attempting to pass the gate.
44+
/// @param data Additional data that may be required for the check.
45+
/// @return True if the passerby passes the check, false otherwise.
46+
function check(address passerby, bytes calldata data) external view returns (bool);
3747
}

packages/excubiae/contracts/README.md

+9-4
Original file line numberDiff line numberDiff line change
@@ -57,23 +57,28 @@ yarn add @zk-kit/excubiae
5757
To build your own Excubia:
5858

5959
1. Inherit from the [Excubia](./Excubia.sol) abstract contract that conforms to the [IExcubia](./IExcubia.sol) interface.
60-
2. Implement the `_check()` method to define your own gatekeeping logic and to prevent unwanted access (sybils, double checks).
60+
2. Implement the `_check()` and `_pass()` methods logic defining your own checks to prevent unwanted access (sybils, double checks).
6161

6262
```solidity
6363
// SPDX-License-Identifier: MIT
64-
pragma solidity >=0.8.0 <0.9.0;
64+
pragma solidity >=0.8.0;
6565
6666
import { Excubia } from "excubiae/contracts/Excubia.sol";
6767
6868
contract MyExcubia is Excubia {
6969
// ...
7070
71-
function _check(address passerby, bytes calldata data) internal override returns (bool) {
72-
// Implement custom access control logic here.
71+
function _pass(address passerby, bytes calldata data) internal override {
7372
// Implement your logic to prevent unwanted access here.
73+
}
74+
75+
function _check(address passerby, bytes calldata data) internal view override returns (bool) {
76+
// Implement custom access control logic here.
7477
7578
return true;
7679
}
80+
81+
// ...
7782
}
7883
```
7984

packages/excubiae/contracts/extensions/EASExcubia.sol

+16-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// SPDX-License-Identifier: MIT
2-
pragma solidity >=0.8.0 <0.9.0;
2+
pragma solidity >=0.8.0;
33

44
import {Excubia} from "../Excubia.sol";
55
import {IEAS} from "@ethereum-attestation-service/eas-contracts/contracts/IEAS.sol";
@@ -49,17 +49,28 @@ contract EASExcubia is Excubia {
4949
SCHEMA = _schema;
5050
}
5151

52-
/// @notice Overrides the `_check` function to validate against specific criteria.
52+
/// @notice Internal function to handle the passing logic with check.
53+
/// @dev Calls the parent `_pass` function and registers the attestation to avoid double checks.
5354
/// @param passerby The address of the entity attempting to pass the gate.
54-
/// @param data Encoded attestation ID.
55-
/// @return True if the attestation meets all criteria, revert otherwise.
56-
function _check(address passerby, bytes calldata data) internal override returns (bool) {
55+
/// @param data Additional data required for the check (e.g., encoded attestation ID).
56+
function _pass(address passerby, bytes calldata data) internal override {
57+
super._pass(passerby, data);
58+
5759
bytes32 attestationId = abi.decode(data, (bytes32));
5860

5961
// Avoiding double check of the same attestation.
6062
if (registeredAttestations[attestationId]) revert AlreadyRegistered();
6163

6264
registeredAttestations[attestationId] = true;
65+
}
66+
67+
/// @notice Internal function to handle the gate protection (attestation check) logic.
68+
/// @dev Checks if the attestation matches the schema, attester, recipient, and is not revoked.
69+
/// @param passerby The address of the entity attempting to pass the gate.
70+
/// @param data Additional data required for the check (e.g., encoded attestation ID).
71+
/// @return True if the attestation is valid and the passerby passes the check, false otherwise.
72+
function _check(address passerby, bytes calldata data) internal view override returns (bool) {
73+
bytes32 attestationId = abi.decode(data, (bytes32));
6374

6475
Attestation memory attestation = EAS.getAttestation(attestationId);
6576

packages/excubiae/contracts/test/MockEAS.sol

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// SPDX-License-Identifier: MIT
2-
pragma solidity ^0.8.0;
2+
pragma solidity >=0.8.0;
33

44
/* solhint-disable max-line-length */
55
import {IEAS, ISchemaRegistry, AttestationRequest, MultiAttestationRequest, DelegatedAttestationRequest, MultiDelegatedAttestationRequest, DelegatedRevocationRequest, RevocationRequest, MultiRevocationRequest, MultiDelegatedRevocationRequest} from "@ethereum-attestation-service/eas-contracts/contracts/IEAS.sol";

packages/excubiae/hardhat.config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { HardhatUserConfig } from "hardhat/config"
33

44
const hardhatConfig: HardhatUserConfig = {
55
solidity: {
6-
version: "0.8.23",
6+
version: "0.8.20",
77
settings: {
88
optimizer: {
99
enabled: true

packages/excubiae/test/EASExcubia.test.ts

+121-67
Original file line numberDiff line numberDiff line change
@@ -55,80 +55,134 @@ describe("EASExcubia", function () {
5555
})
5656

5757
describe("EASExcubia", function () {
58-
it("should fail to set the gate when the caller is not the owner", async () => {
59-
const [, notOwnerSigner] = await ethers.getSigners()
60-
61-
await expect(easExcubia.connect(notOwnerSigner).setGate(gateAddress)).to.be.revertedWithCustomError(
62-
easExcubia,
63-
"OwnableUnauthorizedAccount"
64-
)
65-
})
66-
67-
it("should fail to set the gate when the gate address is zero", async () => {
68-
await expect(easExcubia.setGate(ZeroAddress)).to.be.revertedWithCustomError(easExcubia, "ZeroAddress")
69-
})
70-
71-
it("Should set the gate contract address correctly", async function () {
72-
await easExcubia.setGate(gateAddress).then((tx) => tx.wait())
73-
74-
expect(await easExcubia.gate()).to.eq(gateAddress)
75-
})
76-
77-
it("Should fail to set the gate if already set", async function () {
78-
await expect(easExcubia.setGate(gateAddress)).to.be.revertedWithCustomError(easExcubia, "GateAlreadySet")
79-
})
80-
81-
it("should throw when the callee is not the gate", async () => {
82-
await expect(
83-
easExcubia.connect(signer).pass(signerAddress, invalidRecipientAttestationId)
84-
).to.be.revertedWithCustomError(easExcubia, "GateOnly")
85-
})
86-
87-
it("should throw when the attestation is not owned by the correct recipient", async () => {
88-
await expect(
89-
easExcubia.connect(gate).pass(signerAddress, invalidRecipientAttestationId)
90-
).to.be.revertedWithCustomError(easExcubia, "UnexpectedRecipient")
91-
})
92-
93-
it("should throw when the attestation has been revoked", async () => {
94-
await expect(
95-
easExcubia.connect(gate).pass(signerAddress, revokedAttestationId)
96-
).to.be.revertedWithCustomError(easExcubia, "RevokedAttestation")
97-
})
58+
describe("setGate()", function () {
59+
it("should fail to set the gate when the caller is not the owner", async () => {
60+
const [, notOwnerSigner] = await ethers.getSigners()
61+
62+
await expect(easExcubia.connect(notOwnerSigner).setGate(gateAddress)).to.be.revertedWithCustomError(
63+
easExcubia,
64+
"OwnableUnauthorizedAccount"
65+
)
66+
})
67+
68+
it("should fail to set the gate when the gate address is zero", async () => {
69+
await expect(easExcubia.setGate(ZeroAddress)).to.be.revertedWithCustomError(easExcubia, "ZeroAddress")
70+
})
71+
72+
it("Should set the gate contract address correctly", async function () {
73+
const tx = await easExcubia.setGate(gateAddress)
74+
const receipt = await tx.wait()
75+
const event = EASExcubiaContract.interface.parseLog(
76+
receipt?.logs[0] as unknown as { topics: string[]; data: string }
77+
) as unknown as {
78+
args: {
79+
gate: string
80+
}
81+
}
9882

99-
it("should throw when the attestation schema is not the one expected", async () => {
100-
await expect(
101-
easExcubia.connect(gate).pass(signerAddress, invalidSchemaAttestationId)
102-
).to.be.revertedWithCustomError(easExcubia, "UnexpectedSchema")
83+
expect(receipt?.status).to.eq(1)
84+
expect(event.args.gate).to.eq(gateAddress)
85+
expect(await easExcubia.gate()).to.eq(gateAddress)
86+
})
87+
88+
it("Should fail to set the gate if already set", async function () {
89+
await expect(easExcubia.setGate(gateAddress)).to.be.revertedWithCustomError(
90+
easExcubia,
91+
"GateAlreadySet"
92+
)
93+
})
10394
})
10495

105-
it("should throw when the attestation is not signed by the attestation owner", async () => {
106-
await expect(
107-
easExcubia.connect(gate).pass(signerAddress, invalidAttesterAttestationId)
108-
).to.be.revertedWithCustomError(easExcubia, "UnexpectedAttester")
96+
describe("check()", function () {
97+
it("should throw when the attestation is not owned by the correct recipient", async () => {
98+
await expect(
99+
easExcubia.check(signerAddress, invalidRecipientAttestationId)
100+
).to.be.revertedWithCustomError(easExcubia, "UnexpectedRecipient")
101+
})
102+
103+
it("should throw when the attestation has been revoked", async () => {
104+
await expect(easExcubia.check(signerAddress, revokedAttestationId)).to.be.revertedWithCustomError(
105+
easExcubia,
106+
"RevokedAttestation"
107+
)
108+
})
109+
110+
it("should throw when the attestation schema is not the one expected", async () => {
111+
await expect(easExcubia.check(signerAddress, invalidSchemaAttestationId)).to.be.revertedWithCustomError(
112+
easExcubia,
113+
"UnexpectedSchema"
114+
)
115+
})
116+
117+
it("should throw when the attestation is not signed by the attestation owner", async () => {
118+
await expect(
119+
easExcubia.check(signerAddress, invalidAttesterAttestationId)
120+
).to.be.revertedWithCustomError(easExcubia, "UnexpectedAttester")
121+
})
122+
123+
it("should pass the check", async () => {
124+
const passed = await easExcubia.check(signerAddress, validAttestationId)
125+
126+
expect(passed).to.be.true
127+
// check does NOT change the state of the contract (see pass()).
128+
expect(await easExcubia.registeredAttestations(validAttestationId)).to.be.false
129+
})
109130
})
110131

111-
it("should pass the check", async () => {
112-
const tx = await easExcubia.connect(gate).pass(signerAddress, validAttestationId)
113-
const receipt = await tx.wait()
114-
const event = EASExcubiaContract.interface.parseLog(
115-
receipt?.logs[0] as unknown as { topics: string[]; data: string }
116-
) as unknown as {
117-
args: {
118-
passerby: string
119-
gate: string
132+
describe("pass()", function () {
133+
it("should throw when the callee is not the gate", async () => {
134+
await expect(
135+
easExcubia.connect(signer).pass(signerAddress, invalidRecipientAttestationId)
136+
).to.be.revertedWithCustomError(easExcubia, "GateOnly")
137+
})
138+
139+
it("should throw when the attestation is not owned by the correct recipient", async () => {
140+
await expect(
141+
easExcubia.connect(gate).pass(signerAddress, invalidRecipientAttestationId)
142+
).to.be.revertedWithCustomError(easExcubia, "UnexpectedRecipient")
143+
})
144+
145+
it("should throw when the attestation has been revoked", async () => {
146+
await expect(
147+
easExcubia.connect(gate).pass(signerAddress, revokedAttestationId)
148+
).to.be.revertedWithCustomError(easExcubia, "RevokedAttestation")
149+
})
150+
151+
it("should throw when the attestation schema is not the one expected", async () => {
152+
await expect(
153+
easExcubia.connect(gate).pass(signerAddress, invalidSchemaAttestationId)
154+
).to.be.revertedWithCustomError(easExcubia, "UnexpectedSchema")
155+
})
156+
157+
it("should throw when the attestation is not signed by the attestation owner", async () => {
158+
await expect(
159+
easExcubia.connect(gate).pass(signerAddress, invalidAttesterAttestationId)
160+
).to.be.revertedWithCustomError(easExcubia, "UnexpectedAttester")
161+
})
162+
163+
it("should pass the check", async () => {
164+
const tx = await easExcubia.connect(gate).pass(signerAddress, validAttestationId)
165+
const receipt = await tx.wait()
166+
const event = EASExcubiaContract.interface.parseLog(
167+
receipt?.logs[0] as unknown as { topics: string[]; data: string }
168+
) as unknown as {
169+
args: {
170+
passerby: string
171+
gate: string
172+
}
120173
}
121-
}
122-
123-
expect(receipt?.status).to.eq(1)
124-
expect(event.args.passerby).to.eq(signerAddress)
125-
expect(event.args.gate).to.eq(gateAddress)
126-
})
127174

128-
it("should prevent to pass twice", async () => {
129-
await expect(
130-
easExcubia.connect(gate).pass(signerAddress, validAttestationId)
131-
).to.be.revertedWithCustomError(easExcubia, "AlreadyRegistered")
175+
expect(receipt?.status).to.eq(1)
176+
expect(event.args.passerby).to.eq(signerAddress)
177+
expect(event.args.gate).to.eq(gateAddress)
178+
expect(await easExcubia.registeredAttestations(validAttestationId)).to.be.true
179+
})
180+
181+
it("should prevent to pass twice", async () => {
182+
await expect(
183+
easExcubia.connect(gate).pass(signerAddress, validAttestationId)
184+
).to.be.revertedWithCustomError(easExcubia, "AlreadyRegistered")
185+
})
132186
})
133187
})
134188
})

0 commit comments

Comments
 (0)