Skip to content

Commit

Permalink
Merge pull request #2 from ethereum-attestation-service/offchain-veri…
Browse files Browse the repository at this point in the history
…fication

Add an example for an offchain attestation verification contract (+ lots of refactoring)
  • Loading branch information
lbeder authored Nov 25, 2023
2 parents 264e7b3 + 4628b5b commit 8b33ef0
Show file tree
Hide file tree
Showing 15 changed files with 1,395 additions and 627 deletions.
21 changes: 10 additions & 11 deletions components/Contracts.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { ContractFactory, Signer } from 'ethers';
import { ethers } from 'hardhat';
import {
Attester__factory,
EAS__factory,
ExampleAttester__factory,
ExampleUintResolver__factory,
LogResolver__factory,
OffchainAttestationVerifier__factory,
SchemaRegistry__factory
} from '../typechain-types';

Expand All @@ -12,14 +13,13 @@ export * from '../typechain-types';
type AsyncReturnType<T extends (...args: any) => any> = T extends (...args: any) => Promise<infer U>
? U
: T extends (...args: any) => infer U
? U
: any;
? U
: any;

type Contract<F extends ContractFactory> = AsyncReturnType<F['deploy']>;

export interface ContractBuilder<F extends ContractFactory> {
metadata: {
contractName: string;
bytecode: string;
};
deploy(...args: Parameters<F['deploy']>): Promise<Contract<F>>;
Expand All @@ -33,13 +33,11 @@ export type FactoryConstructor<F extends ContractFactory> = {
};

export const deployOrAttach = <F extends ContractFactory>(
contractName: string,
FactoryConstructor: FactoryConstructor<F>,
initialSigner?: Signer
): ContractBuilder<F> => {
return {
metadata: {
contractName,
bytecode: FactoryConstructor.bytecode
},
deploy: async (...args: Parameters<F['deploy']>): Promise<Contract<F>> => {
Expand Down Expand Up @@ -67,10 +65,11 @@ export const attachOnly = <F extends ContractFactory>(
const getContracts = (signer?: Signer) => ({
connect: (signer: Signer) => getContracts(signer),

EAS: deployOrAttach('EAS', EAS__factory, signer),
ExampleAttester: deployOrAttach('ExampleAttester', ExampleAttester__factory, signer),
ExampleUintResolver: deployOrAttach('ExampleUintResolver', ExampleUintResolver__factory, signer),
SchemaRegistry: deployOrAttach('SchemaRegistry', SchemaRegistry__factory, signer)
EAS: deployOrAttach(EAS__factory, signer),
Attester: deployOrAttach(Attester__factory, signer),
LogResolver: deployOrAttach(LogResolver__factory, signer),
OffchainAttestationVerifier: deployOrAttach(OffchainAttestationVerifier__factory, signer),
SchemaRegistry: deployOrAttach(SchemaRegistry__factory, signer)
});
/* eslint-enable camelcase */

Expand Down
19 changes: 13 additions & 6 deletions contracts/ExampleAttester.sol → contracts/Attester.sol
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.21;
pragma solidity 0.8.22;

import { IEAS, AttestationRequest, AttestationRequestData } from "@ethereum-attestation-service/eas-contracts/contracts/IEAS.sol";
import { IEAS, AttestationRequest, AttestationRequestData, RevocationRequest, RevocationRequestData } from "@ethereum-attestation-service/eas-contracts/contracts/IEAS.sol";
import { NO_EXPIRATION_TIME, EMPTY_UID } from "@ethereum-attestation-service/eas-contracts/contracts/Common.sol";

/// @title ExampleAttester
/// @title Attester
/// @notice Ethereum Attestation Service - Example
contract ExampleAttester {
contract Attester {
error InvalidEAS();

// The address of the global EAS contract.
IEAS private immutable _eas;

/// @notice Creates a new ExampleAttester instance.
/// @notice Creates a new Attester instance.
/// @param eas The address of the global EAS contract.
constructor(IEAS eas) {
if (address(eas) == address(0)) {
Expand All @@ -27,7 +27,7 @@ contract ExampleAttester {
/// @param schema The schema UID to attest to.
/// @param input The uint256 value to pass to to the resolver.
/// @return The UID of the new attestation.
function attestUint(bytes32 schema, uint256 input) external returns (bytes32) {
function attest(bytes32 schema, uint256 input) external returns (bytes32) {
return
_eas.attest(
AttestationRequest({
Expand All @@ -43,4 +43,11 @@ contract ExampleAttester {
})
);
}

/// @notice Revokes an attestation of a schema that receives a uint256 parameter.
/// @param schema The schema UID to attest to.
/// @param uid The UID of the attestation to revoke.
function revoke(bytes32 schema, bytes32 uid) external {
_eas.revoke(RevocationRequest({ schema: schema, data: RevocationRequestData({ uid: uid, value: 0 }) }));
}
}
28 changes: 0 additions & 28 deletions contracts/ExampleUintResolver.sol

This file was deleted.

42 changes: 42 additions & 0 deletions contracts/LogResolver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.22;

import { IEAS, Attestation } from "@ethereum-attestation-service/eas-contracts/contracts/IEAS.sol";
import { SchemaResolver } from "@ethereum-attestation-service/eas-contracts/contracts/resolver/SchemaResolver.sol";

/// @title LogResolver
/// @notice A sample schema resolver that logs a uint256 input.
contract LogResolver is SchemaResolver {
/// @notice Emitted to log a uint256 value.
/// @param value The attested value.
event Attested(uint256 value);

/// @notice Emitted to log a uint256 value.
/// @param value The attested value.
event Revoked(uint256 value);

/// @notice Creates a new LogResolver instance.
constructor(IEAS eas) SchemaResolver(eas) {}

/// @notice An example resolver onAttest callback that decodes a uint256 value and just logs it.
/// @param attestation The new attestation.
/// @return Whether the attestation is valid.
function onAttest(Attestation calldata attestation, uint256 /*value*/) internal override returns (bool) {
uint256 value = abi.decode(attestation.data, (uint256));

emit Attested(value);

return true;
}

/// @notice An example resolver onRevoke fallthrough callback (which currently doesn't do anything).
/// @return Whether the attestation can be revoked.
function onRevoke(Attestation calldata attestation, uint256 /*value*/) internal override returns (bool) {
uint256 value = abi.decode(attestation.data, (uint256));

emit Revoked(value);

return true;
}
}
162 changes: 162 additions & 0 deletions contracts/OffchainAttestationVerifier.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.22;

import { IEAS } from "@ethereum-attestation-service/eas-contracts/contracts/IEAS.sol";
import { SchemaRecord } from "@ethereum-attestation-service/eas-contracts/contracts/ISchemaRegistry.sol";
import { EMPTY_UID, Signature } from "@ethereum-attestation-service/eas-contracts/contracts/Common.sol";

import { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";

import { EIP712Verifier } from "./eip712/EIP712Verifier.sol";

/// @title OffchainAttestationVerifier
/// @notice Offchain Attestation Verifier - Example
contract OffchainAttestationVerifier is EIP712Verifier {
/// @notice A struct representing an offchain attestation request.
struct OffchainAttestation {
uint16 version; // The version of the attestation.
address attester; // The attester of the attestation.
bytes32 schema; // The unique identifier of the schema.
address recipient; // The recipient of the attestation.
uint64 time; // The time when the attestation was signed.
uint64 expirationTime; // The time when the attestation expires (Unix timestamp).
bool revocable; // Whether the attestation is revocable.
bytes32 refUID; // The UID of the related attestation.
bytes data; // Custom attestation data.
Signature signature; // The ECDSA signature data.
}

// Offchain attestation versions.
uint16 private constant LEGACY = 0;
uint16 private constant VERSION1 = 1;

// The hash of the data type used to relay calls to the attest function. It's the value of
// keccak256("Attestation(bytes32 schema,address recipient,uint64 time,uint64 expirationTime,bool revocable,bytes32 refUID,bytes data)").
bytes32 private constant LEGACY_ATTEST_TYPEHASH =
0x2fcbc49c85ccde58f6986371b0828354351185c921aebbaace3e89e0e023b25d;

// The hash of the data type used to relay calls to the attest function. It's the value of
// keccak256("Attest(uint16 version,bytes32 schema,address recipient,uint64 time,uint64 expirationTime,bool revocable,bytes32 refUID,bytes data)").
bytes32 private constant VERSION1_ATTEST_TYPEHASH =
0x9a1ef129b3715afc513574bddcf4404e21b0296e3ca20fec532fe1ec8d0932ec;

// The address of the global EAS contract.
IEAS private immutable _eas;

/// @notice Creates a new Attester instance.
/// @param eas The address of the global EAS contract.
constructor(IEAS eas) EIP712Verifier("EAS Attestation", eas.version(), address(eas)) {
_eas = eas;
}

/// @notice Returns the EAS.
function getEAS() external view returns (IEAS) {
return _eas;
}

/// @notice Verify the offchain attestation.
/// @param attestation The offchain attestation to verify.
/// @return The status of the verification.
function verify(OffchainAttestation calldata attestation) external view returns (bool) {
if (attestation.attester == address(0)) {
return false;
}

// Verify that the version is known.
uint16 version = attestation.version;
if (version > VERSION1) {
return false;
}

// Verify that the time of the attestation isn't in the future.
if (attestation.time > _time()) {
return false;
}

// Verify that the schema exists.
SchemaRecord memory schemaRecord = _eas.getSchemaRegistry().getSchema(attestation.schema);
if (schemaRecord.uid == EMPTY_UID) {
return false;
}

// Verify that the referenced attestation exists.
if (attestation.refUID != EMPTY_UID && !_eas.isAttestationValid(attestation.refUID)) {
return false;
}

// Verify the EIP712/EIP1271 signature.
bytes32 hash;

// Derive the right typed data hash based on the offchain attestation version. Please note that due to previous
// checks, don't need a case for the unknown version below.
if (version == LEGACY) {
hash = _hashTypedDataLegacy(attestation);
} else if (version == VERSION1) {
hash = _hashTypedDataVersion1(attestation);
}

Signature memory signature = attestation.signature;
if (
!SignatureChecker.isValidSignatureNow(
attestation.attester,
hash,
abi.encodePacked(signature.r, signature.s, signature.v)
)
) {
return false;
}

return true;
}

/// @dev Returns the legacy (version 0) attestation typed data hash
/// @param attestation The offchain attestation to verify.
/// @return The typed data hash.
function _hashTypedDataLegacy(OffchainAttestation calldata attestation) private view returns (bytes32) {
return
_hashTypedDataV4(
keccak256(
abi.encode(
LEGACY_ATTEST_TYPEHASH,
attestation.schema,
attestation.recipient,
attestation.time,
attestation.expirationTime,
attestation.revocable,
attestation.refUID,
keccak256(attestation.data)
)
)
);
}

/// @dev Returns the legacy (version 0) attestation typed data hash
/// @param attestation The offchain attestation to verify.
/// @return The typed data hash.
function _hashTypedDataVersion1(OffchainAttestation calldata attestation) private view returns (bytes32) {
return
_hashTypedDataV4(
keccak256(
abi.encode(
VERSION1_ATTEST_TYPEHASH,
VERSION1,
attestation.schema,
attestation.recipient,
attestation.time,
attestation.expirationTime,
attestation.revocable,
attestation.refUID,
keccak256(attestation.data)
)
)
);
}

/// @dev Returns the current's block timestamp. This method is overridden during tests and used to simulate the
/// current block time.
function _time() internal view virtual returns (uint64) {
return uint64(block.timestamp);
}
}
Loading

0 comments on commit 8b33ef0

Please sign in to comment.