Skip to content

Commit

Permalink
feat(excubiae): add basic EASExcubia extension
Browse files Browse the repository at this point in the history
  • Loading branch information
0xjei committed Jun 14, 2024
1 parent 1a0882a commit 04a996e
Show file tree
Hide file tree
Showing 10 changed files with 405 additions and 3 deletions.
3 changes: 2 additions & 1 deletion packages/excubiae/.solcover.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
module.exports = {
istanbulFolder: "../../coverage/gatekeepers"
istanbulFolder: "../../coverage/excubiae",
skipFiles: ["test"]
}
2 changes: 1 addition & 1 deletion packages/excubiae/contracts/IExcubia.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ interface IExcubia {
/// @param gate The address of the excubia-protected contract address.
event GatePassed(address indexed passerby, address indexed gate);

/// @notice Error thrown when an address is zero.
/// @notice Error thrown when an address equal to zero is given.
error ZeroAddress();

/// @notice Error thrown when the gate address is not set.
Expand Down
Empty file.
86 changes: 86 additions & 0 deletions packages/excubiae/contracts/extensions/EASExcubia.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;

import {Excubia} from "../Excubia.sol";
import {IEAS} from "@ethereum-attestation-service/eas-contracts/contracts/IEAS.sol";
import {Attestation} from "@ethereum-attestation-service/eas-contracts/contracts/Common.sol";

/// @title EAS Excubia Contract.
/// @notice This contract extends the Excubia contract to integrate with the Ethereum Attestation Service (EAS).
/// This contract checks an EAS attestation to permit access through the gate.
/// @dev The contract uses a specific attestation schema & attester to admit the recipient of the attestation.
contract EASExcubia is Excubia {
/// @notice The Ethereum Attestation Service contract interface.
IEAS public immutable EAS;
/// @notice The specific schema ID that attestations must match to pass the gate.
bytes32 public immutable SCHEMA;
/// @notice The trusted attester address whose attestations are considered
/// the only ones valid to pass the gate.
address public immutable ATTESTER;

/// @notice Mapping to track which attestations have been registered by the contract to
/// avoid double checks with the same attestation.
mapping(bytes32 => bool) public registeredAttestations;

/// @notice Error thrown when the attestation has been already used to pass the gate.
error AlreadyRegistered();

/// @notice Error thrown when the attestation does not match the designed schema.
error UnexpectedSchema();

/// @notice Error thrown when the attestation does not match the designed trusted attester.
error UnexpectedAttester();

/// @notice Error thrown when the attestation does not match the passerby as recipient.
error UnexpectedRecipient();

/// @notice Error thrown when the attestation has been revoked.
error RevokedAttestation();

/// @notice Constructor to initialize with target EAS contract with specific attester and schema.
/// @param _eas The address of the EAS contract.
/// @param _attester The address of the trusted attester.
/// @param _schema The schema ID that attestations must match.
constructor(address _eas, address _attester, bytes32 _schema) {
if (_eas == address(0) || _attester == address(0)) revert ZeroAddress();

EAS = IEAS(_eas);
ATTESTER = _attester;
SCHEMA = _schema;
}

/// @notice Overrides the `_pass` function to register a correct attestation.
/// @param passerby The address of the entity attempting to pass the gate.
/// @param data Encoded attestation ID.
function _pass(address passerby, bytes calldata data) internal override {
super._pass(passerby, data);

registeredAttestations[decodeAttestationId(data)] = true;
}

/// @notice Overrides the `_check` function to validate the attestation against specific criteria.
/// @param passerby The address of the entity attempting to pass the gate.
/// @param data Encoded attestation ID.
/// @return True if the attestation meets all criteria, revert otherwise.
function _check(address passerby, bytes calldata data) internal view override returns (bool) {
bytes32 attestationId = decodeAttestationId(data);

if (registeredAttestations[attestationId]) revert AlreadyRegistered();

Attestation memory attestation = EAS.getAttestation(attestationId);

if (attestation.schema != SCHEMA) revert UnexpectedSchema();
if (attestation.attester != ATTESTER) revert UnexpectedAttester();
if (attestation.recipient != passerby) revert UnexpectedRecipient();
if (attestation.revocationTime != 0) revert RevokedAttestation();

return true;
}

/// @notice Decodes an EAS attestation identifier from the encoded form.
/// @param data Encoded attestation ID.
/// @return Decoded attestation ID.
function decodeAttestationId(bytes calldata data) private pure returns (bytes32) {
return abi.decode(data, (bytes32));
}
}
1 change: 1 addition & 0 deletions packages/excubiae/contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"access": "public"
},
"dependencies": {
"@ethereum-attestation-service/eas-contracts": "^1.7.1",
"@openzeppelin/contracts": "^5.0.2"
}
}
172 changes: 172 additions & 0 deletions packages/excubiae/contracts/test/MockEAS.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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

/// @title Mock Ethereum Attestation Service (EAS) Contract.
/// @notice This contract is a mock implementation of the IEAS interface for testing purposes.
/// @dev It simulates the behavior of a real EAS contract by providing predefined mocked attestations.
contract MockEAS is IEAS {
/// @notice A mock schema registry, represented simply as an address.
ISchemaRegistry public override getSchemaRegistry;

/// @notice A mapping to store mocked attestations by their unique identifiers.
mapping(bytes32 => Attestation) private mockedAttestations;

/// MOCKS ///

/// @notice Constructor to initialize the mock contract with predefined attestations.
/// @param _recipient The recipient address used in mocked attestations.
/// @param _attester The attester address used in mocked attestations.
/// @param _schema The schema identifier used in mocked attestations.
constructor(address _recipient, address _attester, bytes32 _schema) {
getSchemaRegistry = ISchemaRegistry(address(1));

Attestation memory valid = Attestation({
uid: bytes32("0x01"),
schema: _schema,
time: 0,
expirationTime: 0,
revocationTime: 0,
refUID: bytes32("0x01"),
recipient: _recipient,
attester: _attester,
revocable: true,
data: bytes("")
});

Attestation memory revoked = Attestation({
uid: bytes32("0x02"),
schema: _schema,
time: 0,
expirationTime: 0,
revocationTime: 1,
refUID: bytes32("0x01"),
recipient: _recipient,
attester: _attester,
revocable: true,
data: bytes("")
});

Attestation memory invalidSchema = Attestation({
uid: bytes32("0x03"),
schema: bytes32("0x01"),
time: 0,
expirationTime: 0,
revocationTime: 0,
refUID: bytes32("0x01"),
recipient: _recipient,
attester: _attester,
revocable: true,
data: bytes("")
});

Attestation memory invalidRecipient = Attestation({
uid: bytes32("0x04"),
schema: _schema,
time: 0,
expirationTime: 0,
revocationTime: 0,
refUID: bytes32("0x01"),
recipient: address(1),
attester: _attester,
revocable: true,
data: bytes("")
});

Attestation memory invalidAttester = Attestation({
uid: bytes32("0x05"),
schema: _schema,
time: 0,
expirationTime: 0,
revocationTime: 0,
refUID: bytes32("0x000000000000000000000000000001"),
recipient: _recipient,
attester: address(1),
revocable: true,
data: bytes("")
});

mockedAttestations[bytes32("0x01")] = valid;
mockedAttestations[bytes32("0x02")] = revoked;
mockedAttestations[bytes32("0x03")] = invalidSchema;
mockedAttestations[bytes32("0x04")] = invalidRecipient;
mockedAttestations[bytes32("0x05")] = invalidAttester;
}

/// @notice Retrieves a mocked attestation by its unique identifier.
/// @param uid The unique identifier of the attestation.
/// @return The mocked attestation associated with the given identifier.
function getAttestation(bytes32 uid) external view override returns (Attestation memory) {
return mockedAttestations[uid];
}

/// STUBS ///
// The following functions are stubs and do not perform any meaningful operations.
// They are placeholders to comply with the IEAS interface.
function attest(AttestationRequest calldata /*request*/) external payable override returns (bytes32) {
return bytes32(0);
}

function attestByDelegation(
DelegatedAttestationRequest calldata /*delegatedRequest*/
) external payable override returns (bytes32) {
return bytes32(0);
}

function multiAttest(
MultiAttestationRequest[] calldata multiRequests
) external payable override returns (bytes32[] memory) {
return new bytes32[](multiRequests.length);
}

function multiAttestByDelegation(
MultiDelegatedAttestationRequest[] calldata multiDelegatedRequests
) external payable override returns (bytes32[] memory) {
return new bytes32[](multiDelegatedRequests.length);
}

function revoke(RevocationRequest calldata request) external payable override {}

function revokeByDelegation(DelegatedRevocationRequest calldata delegatedRequest) external payable override {}

function multiRevoke(MultiRevocationRequest[] calldata multiRequests) external payable override {}

function multiRevokeByDelegation(
MultiDelegatedRevocationRequest[] calldata multiDelegatedRequests
) external payable override {}

function timestamp(bytes32 /*data*/) external view override returns (uint64) {
return uint64(block.timestamp);
}

function multiTimestamp(bytes32[] calldata /*data*/) external view override returns (uint64) {
return uint64(block.timestamp);
}

function revokeOffchain(bytes32 /*data*/) external view override returns (uint64) {
return uint64(block.timestamp);
}

function multiRevokeOffchain(bytes32[] calldata /*data*/) external view override returns (uint64) {
return uint64(block.timestamp);
}

function isAttestationValid(bytes32 uid) external view override returns (bool) {
return mockedAttestations[uid].uid != bytes32(0);
}

function getTimestamp(bytes32 /*data*/) external view override returns (uint64) {
return uint64(block.timestamp);
}

function getRevokeOffchain(address /*revoker*/, bytes32 /*data*/) external view override returns (uint64) {
return uint64(block.timestamp);
}

function version() external pure returns (string memory) {
return string("");
}
}
1 change: 1 addition & 0 deletions packages/excubiae/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"typescript": "^5.3.3"
},
"dependencies": {
"@ethereum-attestation-service/eas-contracts": "^1.7.1",
"@openzeppelin/contracts": "^5.0.2"
}
}
Empty file removed packages/excubiae/test/.gitkeep
Empty file.
Loading

0 comments on commit 04a996e

Please sign in to comment.