Commit 9c3d03d6 authored by lbeder's avatar lbeder Committed by Mark Tyneway

Add EAS contracts

parent 984fbf5d
...@@ -8,6 +8,7 @@ ignore: ...@@ -8,6 +8,7 @@ ignore:
- "op-bindings/bindings/*.go" - "op-bindings/bindings/*.go"
- "packages/contracts-bedrock/contracts/vendor/WETH9.sol" - "packages/contracts-bedrock/contracts/vendor/WETH9.sol"
- "packages/contracts-bedrock/contracts/cannon" # tested through Go tests - "packages/contracts-bedrock/contracts/cannon" # tested through Go tests
- 'packages/contracts-bedrock/contracts/EAS/**/*.sol'
coverage: coverage:
status: status:
patch: patch:
......
...@@ -31,5 +31,7 @@ ...@@ -31,5 +31,7 @@
"StandardBridge", "StandardBridge",
"CrossDomainMessenger", "CrossDomainMessenger",
"MIPS", "MIPS",
"PreimageOracle" "PreimageOracle",
"EAS",
"SchemaRegistry"
] ]
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// A representation of an empty/uninitialized UID.
bytes32 constant EMPTY_UID = 0;
// A zero expiration represents an non-expiring attestation.
uint64 constant NO_EXPIRATION_TIME = 0;
error AccessDenied();
error InvalidEAS();
error InvalidLength();
error InvalidSignature();
error NotFound();
/**
* @dev A struct representing EIP712 signature data.
*/
struct EIP712Signature {
uint8 v; // The recovery ID.
bytes32 r; // The x-coordinate of the nonce R.
bytes32 s; // The signature data.
}
/**
* @dev A struct representing a single attestation.
*/
struct Attestation {
bytes32 uid; // A unique identifier of the attestation.
bytes32 schema; // The unique identifier of the schema.
uint64 time; // The time when the attestation was created (Unix timestamp).
uint64 expirationTime; // The time when the attestation expires (Unix timestamp).
uint64 revocationTime; // The time when the attestation was revoked (Unix timestamp).
bytes32 refUID; // The UID of the related attestation.
address recipient; // The recipient of the attestation.
address attester; // The attester/sender of the attestation.
bool revocable; // Whether the attestation is revocable.
bytes data; // Custom attestation data.
}
// Maximum upgrade forward-compatibility storage gap.
uint32 constant MAX_GAP = 50;
/**
* @dev A helper function to work with unchecked iterators in loops.
*
* @param i The index to increment.
*
* @return j The incremented index.
*/
function uncheckedInc(uint256 i) pure returns (uint256 j) {
unchecked {
j = i + 1;
}
}
/**
* @dev A helper function that converts a string to a bytes32.
*
* @param str The string to convert.
*
* @return The converted bytes32.
*/
function stringToBytes32(string memory str) pure returns (bytes32) {
bytes32 result;
assembly {
result := mload(add(str, 32))
}
return result;
}
/**
* @dev A helper function that converts a bytes32 to a string.
*
* @param data The bytes32 data to convert.
*
* @return The converted string.
*/
function bytes32ToString(bytes32 data) pure returns (string memory) {
bytes memory byteArray = new bytes(32);
uint256 length = 0;
for (uint256 i = 0; i < 32; i = uncheckedInc(i)) {
bytes1 char = data[i];
if (char == 0x00) {
break;
}
byteArray[length] = char;
length = uncheckedInc(length);
}
bytes memory terminatedBytes = new bytes(length);
for (uint256 j = 0; j < length; j = uncheckedInc(j)) {
terminatedBytes[j] = byteArray[j];
}
return string(terminatedBytes);
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import { Address } from "@openzeppelin/contracts/utils/Address.sol";
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import { Semver } from "../universal/Semver.sol";
import { EIP712Verifier } from "./eip712/EIP712Verifier.sol";
import { ISchemaResolver } from "./resolver/ISchemaResolver.sol";
// prettier-ignore
import {
AccessDenied,
EMPTY_UID,
EIP712Signature,
InvalidLength,
MAX_GAP,
NotFound,
NO_EXPIRATION_TIME,
uncheckedInc
} from "./Common.sol";
// prettier-ignore
import {
Attestation,
AttestationRequest,
AttestationRequestData,
DelegatedAttestationRequest,
DelegatedRevocationRequest,
IEAS,
MultiAttestationRequest,
MultiDelegatedAttestationRequest,
MultiDelegatedRevocationRequest,
MultiRevocationRequest,
RevocationRequest,
RevocationRequestData
} from "./IEAS.sol";
import { ISchemaRegistry, SchemaRecord } from "./ISchemaRegistry.sol";
struct AttestationsResult {
uint256 usedValue; // Total ETH amount that was sent to resolvers.
bytes32[] uids; // UIDs of the new attestations.
}
/**
* @title EAS - Ethereum Attestation Service
*/
contract EAS is IEAS, Semver, Initializable, EIP712Verifier {
using Address for address payable;
error AlreadyRevoked();
error AlreadyRevokedOffchain();
error AlreadyTimestamped();
error InsufficientValue();
error InvalidAttestation();
error InvalidAttestations();
error InvalidExpirationTime();
error InvalidOffset();
error InvalidRegistry();
error InvalidRevocation();
error InvalidRevocations();
error InvalidSchema();
error InvalidVerifier();
error Irrevocable();
error NotPayable();
error WrongSchema();
// The global schema registry.
ISchemaRegistry private immutable _schemaRegistry;
// The global mapping between attestations and their UIDs.
mapping(bytes32 uid => Attestation attestation) private _db;
// The global mapping between data and their timestamps.
mapping(bytes32 data => uint64 timestamp) private _timestamps;
// The global mapping between data and their revocation timestamps.
mapping(address revoker => mapping(bytes32 data => uint64 timestamp)) private _revocationsOffchain;
// Upgrade forward-compatibility storage gap
uint256[MAX_GAP - 3] private __gap;
/**
* @dev Creates a new EAS instance.
*
* @param registry The address of the global schema registry.
*/
constructor(ISchemaRegistry registry) Semver(1, 0, 0) EIP712Verifier("EAS", "1.0.0") {
if (address(registry) == address(0)) {
revert InvalidRegistry();
}
_schemaRegistry = registry;
}
/**
* @dev Initializes the contract and its parents.
*/
function initialize() external initializer {
__EAS_init();
}
// solhint-disable func-name-mixedcase
/**
* @dev Upgradeable initialization.
*/
function __EAS_init() internal onlyInitializing {
__EAS_init_unchained();
}
/**
* @dev Upgradeable initialization.
*/
function __EAS_init_unchained() internal onlyInitializing {}
// solhint-enable func-name-mixedcase
/**
* @inheritdoc IEAS
*/
function getSchemaRegistry() external view returns (ISchemaRegistry) {
return _schemaRegistry;
}
/**
* @inheritdoc IEAS
*/
function attest(AttestationRequest calldata request) external payable returns (bytes32) {
AttestationRequestData[] memory requests = new AttestationRequestData[](1);
requests[0] = request.data;
return _attest(request.schema, requests, msg.sender, msg.value, true).uids[0];
}
/**
* @inheritdoc IEAS
*/
function attestByDelegation(
DelegatedAttestationRequest calldata delegatedRequest
) external payable returns (bytes32) {
_verifyAttest(delegatedRequest);
AttestationRequestData[] memory data = new AttestationRequestData[](1);
data[0] = delegatedRequest.data;
return _attest(delegatedRequest.schema, data, delegatedRequest.attester, msg.value, true).uids[0];
}
/**
* @inheritdoc IEAS
*/
function multiAttest(MultiAttestationRequest[] calldata multiRequests) external payable returns (bytes32[] memory) {
// Since a multi-attest call is going to make multiple attestations for multiple schemas, we'd need to collect
// all the returned UIDs into a single list.
bytes32[][] memory totalUids = new bytes32[][](multiRequests.length);
uint256 totalUidsCount = 0;
// We are keeping track of the total available ETH amount that can be sent to resolvers and will keep deducting
// from it to verify that there isn't any attempt to send too much ETH to resolvers. Please note that unless
// some ETH was stuck in the contract by accident (which shouldn't happen in normal conditions), it won't be
// possible to send too much ETH anyway.
uint availableValue = msg.value;
for (uint256 i = 0; i < multiRequests.length; i = uncheckedInc(i)) {
// The last batch is handled slightly differently: if the total available ETH wasn't spent in full and there
// is a remainder - it will be refunded back to the attester (something that we can only verify during the
// last and final batch).
bool last;
unchecked {
last = i == multiRequests.length - 1;
}
// Process the current batch of attestations.
MultiAttestationRequest calldata multiRequest = multiRequests[i];
AttestationsResult memory res = _attest(
multiRequest.schema,
multiRequest.data,
msg.sender,
availableValue,
last
);
// Ensure to deduct the ETH that was forwarded to the resolver during the processing of this batch.
availableValue -= res.usedValue;
// Collect UIDs (and merge them later).
totalUids[i] = res.uids;
unchecked {
totalUidsCount += res.uids.length;
}
}
// Merge all the collected UIDs and return them as a flatten array.
return _mergeUIDs(totalUids, totalUidsCount);
}
/**
* @inheritdoc IEAS
*/
function multiAttestByDelegation(
MultiDelegatedAttestationRequest[] calldata multiDelegatedRequests
) external payable returns (bytes32[] memory) {
// Since a multi-attest call is going to make multiple attestations for multiple schemas, we'd need to collect
// all the returned UIDs into a single list.
bytes32[][] memory totalUids = new bytes32[][](multiDelegatedRequests.length);
uint256 totalUidsCount = 0;
// We are keeping track of the total available ETH amount that can be sent to resolvers and will keep deducting
// from it to verify that there isn't any attempt to send too much ETH to resolvers. Please note that unless
// some ETH was stuck in the contract by accident (which shouldn't happen in normal conditions), it won't be
// possible to send too much ETH anyway.
uint availableValue = msg.value;
for (uint256 i = 0; i < multiDelegatedRequests.length; i = uncheckedInc(i)) {
// The last batch is handled slightly differently: if the total available ETH wasn't spent in full and there
// is a remainder - it will be refunded back to the attester (something that we can only verify during the
// last and final batch).
bool last;
unchecked {
last = i == multiDelegatedRequests.length - 1;
}
MultiDelegatedAttestationRequest calldata multiDelegatedRequest = multiDelegatedRequests[i];
AttestationRequestData[] calldata data = multiDelegatedRequest.data;
// Ensure that no inputs are missing.
if (data.length == 0 || data.length != multiDelegatedRequest.signatures.length) {
revert InvalidLength();
}
// Verify EIP712 signatures. Please note that the signatures are assumed to be signed with increasing nonces.
for (uint256 j = 0; j < data.length; j = uncheckedInc(j)) {
_verifyAttest(
DelegatedAttestationRequest({
schema: multiDelegatedRequest.schema,
data: data[j],
signature: multiDelegatedRequest.signatures[j],
attester: multiDelegatedRequest.attester
})
);
}
// Process the current batch of attestations.
AttestationsResult memory res = _attest(
multiDelegatedRequest.schema,
data,
multiDelegatedRequest.attester,
availableValue,
last
);
// Ensure to deduct the ETH that was forwarded to the resolver during the processing of this batch.
availableValue -= res.usedValue;
// Collect UIDs (and merge them later).
totalUids[i] = res.uids;
unchecked {
totalUidsCount += res.uids.length;
}
}
// Merge all the collected UIDs and return them as a flatten array.
return _mergeUIDs(totalUids, totalUidsCount);
}
/**
* @inheritdoc IEAS
*/
function revoke(RevocationRequest calldata request) external payable {
RevocationRequestData[] memory requests = new RevocationRequestData[](1);
requests[0] = request.data;
_revoke(request.schema, requests, msg.sender, msg.value, true);
}
/**
* @inheritdoc IEAS
*/
function revokeByDelegation(DelegatedRevocationRequest calldata delegatedRequest) external payable {
_verifyRevoke(delegatedRequest);
RevocationRequestData[] memory data = new RevocationRequestData[](1);
data[0] = delegatedRequest.data;
_revoke(delegatedRequest.schema, data, delegatedRequest.revoker, msg.value, true);
}
/**
* @inheritdoc IEAS
*/
function multiRevoke(MultiRevocationRequest[] calldata multiRequests) external payable {
// We are keeping track of the total available ETH amount that can be sent to resolvers and will keep deducting
// from it to verify that there isn't any attempt to send too much ETH to resolvers. Please note that unless
// some ETH was stuck in the contract by accident (which shouldn't happen in normal conditions), it won't be
// possible to send too much ETH anyway.
uint availableValue = msg.value;
for (uint256 i = 0; i < multiRequests.length; i = uncheckedInc(i)) {
// The last batch is handled slightly differently: if the total available ETH wasn't spent in full and there
// is a remainder - it will be refunded back to the attester (something that we can only verify during the
// last and final batch).
bool last;
unchecked {
last = i == multiRequests.length - 1;
}
MultiRevocationRequest calldata multiRequest = multiRequests[i];
// Ensure to deduct the ETH that was forwarded to the resolver during the processing of this batch.
availableValue -= _revoke(multiRequest.schema, multiRequest.data, msg.sender, availableValue, last);
}
}
/**
* @inheritdoc IEAS
*/
function multiRevokeByDelegation(
MultiDelegatedRevocationRequest[] calldata multiDelegatedRequests
) external payable {
// We are keeping track of the total available ETH amount that can be sent to resolvers and will keep deducting
// from it to verify that there isn't any attempt to send too much ETH to resolvers. Please note that unless
// some ETH was stuck in the contract by accident (which shouldn't happen in normal conditions), it won't be
// possible to send too much ETH anyway.
uint availableValue = msg.value;
for (uint256 i = 0; i < multiDelegatedRequests.length; i = uncheckedInc(i)) {
// The last batch is handled slightly differently: if the total available ETH wasn't spent in full and there
// is a remainder - it will be refunded back to the attester (something that we can only verify during the
// last and final batch).
bool last;
unchecked {
last = i == multiDelegatedRequests.length - 1;
}
MultiDelegatedRevocationRequest memory multiDelegatedRequest = multiDelegatedRequests[i];
RevocationRequestData[] memory data = multiDelegatedRequest.data;
// Ensure that no inputs are missing.
if (data.length == 0 || data.length != multiDelegatedRequest.signatures.length) {
revert InvalidLength();
}
// Verify EIP712 signatures. Please note that the signatures are assumed to be signed with increasing nonces.
for (uint256 j = 0; j < data.length; j = uncheckedInc(j)) {
_verifyRevoke(
DelegatedRevocationRequest({
schema: multiDelegatedRequest.schema,
data: data[j],
signature: multiDelegatedRequest.signatures[j],
revoker: multiDelegatedRequest.revoker
})
);
}
// Ensure to deduct the ETH that was forwarded to the resolver during the processing of this batch.
availableValue -= _revoke(
multiDelegatedRequest.schema,
data,
multiDelegatedRequest.revoker,
availableValue,
last
);
}
}
/**
* @inheritdoc IEAS
*/
function timestamp(bytes32 data) external returns (uint64) {
uint64 time = _time();
_timestamp(data, time);
return time;
}
/**
* @inheritdoc IEAS
*/
function revokeOffchain(bytes32 data) external returns (uint64) {
uint64 time = _time();
_revokeOffchain(msg.sender, data, time);
return time;
}
/**
* @inheritdoc IEAS
*/
function multiRevokeOffchain(bytes32[] calldata data) external returns (uint64) {
uint64 time = _time();
uint256 length = data.length;
for (uint256 i = 0; i < length; i = uncheckedInc(i)) {
_revokeOffchain(msg.sender, data[i], time);
}
return time;
}
/**
* @inheritdoc IEAS
*/
function multiTimestamp(bytes32[] calldata data) external returns (uint64) {
uint64 time = _time();
uint256 length = data.length;
for (uint256 i = 0; i < length; i = uncheckedInc(i)) {
_timestamp(data[i], time);
}
return time;
}
/**
* @inheritdoc IEAS
*/
function getAttestation(bytes32 uid) external view returns (Attestation memory) {
return _db[uid];
}
/**
* @inheritdoc IEAS
*/
function isAttestationValid(bytes32 uid) public view returns (bool) {
return _db[uid].uid != 0;
}
/**
* @inheritdoc IEAS
*/
function getTimestamp(bytes32 data) external view returns (uint64) {
return _timestamps[data];
}
/**
* @inheritdoc IEAS
*/
function getRevokeOffchain(address revoker, bytes32 data) external view returns (uint64) {
return _revocationsOffchain[revoker][data];
}
/**
* @dev Attests to a specific schema.
*
* @param schema // the unique identifier of the schema to attest to.
* @param data The arguments of the attestation requests.
* @param attester The attesting account.
* @param availableValue The total available ETH amount that can be sent to the resolver.
* @param last Whether this is the last attestations/revocations set.
*
* @return The UID of the new attestations and the total sent ETH amount.
*/
function _attest(
bytes32 schema,
AttestationRequestData[] memory data,
address attester,
uint256 availableValue,
bool last
) private returns (AttestationsResult memory) {
uint256 length = data.length;
AttestationsResult memory res;
res.uids = new bytes32[](length);
// Ensure that we aren't attempting to attest to a non-existing schema.
SchemaRecord memory schemaRecord = _schemaRegistry.getSchema(schema);
if (schemaRecord.uid == EMPTY_UID) {
revert InvalidSchema();
}
Attestation[] memory attestations = new Attestation[](length);
uint256[] memory values = new uint256[](length);
for (uint256 i = 0; i < length; i = uncheckedInc(i)) {
AttestationRequestData memory request = data[i];
// Ensure that either no expiration time was set or that it was set in the future.
if (request.expirationTime != NO_EXPIRATION_TIME && request.expirationTime <= _time()) {
revert InvalidExpirationTime();
}
// Ensure that we aren't trying to make a revocable attestation for a non-revocable schema.
if (!schemaRecord.revocable && request.revocable) {
revert Irrevocable();
}
Attestation memory attestation = Attestation({
uid: EMPTY_UID,
schema: schema,
refUID: request.refUID,
time: _time(),
expirationTime: request.expirationTime,
revocationTime: 0,
recipient: request.recipient,
attester: attester,
revocable: request.revocable,
data: request.data
});
// Look for the first non-existing UID (and use a bump seed/nonce in the rare case of a conflict).
bytes32 uid;
uint32 bump = 0;
while (true) {
uid = _getUID(attestation, bump);
if (_db[uid].uid == EMPTY_UID) {
break;
}
unchecked {
++bump;
}
}
attestation.uid = uid;
_db[uid] = attestation;
if (request.refUID != 0) {
// Ensure that we aren't trying to attest to a non-existing referenced UID.
if (!isAttestationValid(request.refUID)) {
revert NotFound();
}
}
attestations[i] = attestation;
values[i] = request.value;
res.uids[i] = uid;
emit Attested(request.recipient, attester, uid, schema);
}
res.usedValue = _resolveAttestations(schemaRecord, attestations, values, false, availableValue, last);
return res;
}
/**
* @dev Revokes an existing attestation to a specific schema.
*
* @param schema The unique identifier of the schema to attest to.
* @param data The arguments of the revocation requests.
* @param revoker The revoking account.
* @param availableValue The total available ETH amount that can be sent to the resolver.
* @param last Whether this is the last attestations/revocations set.
*
* @return Returns the total sent ETH amount.
*/
function _revoke(
bytes32 schema,
RevocationRequestData[] memory data,
address revoker,
uint256 availableValue,
bool last
) private returns (uint256) {
// Ensure that a non-existing schema ID wasn't passed by accident.
SchemaRecord memory schemaRecord = _schemaRegistry.getSchema(schema);
if (schemaRecord.uid == EMPTY_UID) {
revert InvalidSchema();
}
uint256 length = data.length;
Attestation[] memory attestations = new Attestation[](length);
uint256[] memory values = new uint256[](length);
for (uint256 i = 0; i < length; i = uncheckedInc(i)) {
RevocationRequestData memory request = data[i];
Attestation storage attestation = _db[request.uid];
// Ensure that we aren't attempting to revoke a non-existing attestation.
if (attestation.uid == EMPTY_UID) {
revert NotFound();
}
// Ensure that a wrong schema ID wasn't passed by accident.
if (attestation.schema != schema) {
revert InvalidSchema();
}
// Allow only original attesters to revoke their attestations.
if (attestation.attester != revoker) {
revert AccessDenied();
}
// Please note that also checking of the schema itself is revocable is unnecessary, since it's not possible to
// make revocable attestations to an irrevocable schema.
if (!attestation.revocable) {
revert Irrevocable();
}
// Ensure that we aren't trying to revoke the same attestation twice.
if (attestation.revocationTime != 0) {
revert AlreadyRevoked();
}
attestation.revocationTime = _time();
attestations[i] = attestation;
values[i] = request.value;
emit Revoked(attestation.recipient, revoker, request.uid, attestation.schema);
}
return _resolveAttestations(schemaRecord, attestations, values, true, availableValue, last);
}
/**
* @dev Resolves a new attestation or a revocation of an existing attestation.
*
* @param schemaRecord The schema of the attestation.
* @param attestation The data of the attestation to make/revoke.
* @param value An explicit ETH amount to send to the resolver.
* @param isRevocation Whether to resolve an attestation or its revocation.
* @param availableValue The total available ETH amount that can be sent to the resolver.
* @param last Whether this is the last attestations/revocations set.
*
* @return Returns the total sent ETH amount.
*/
function _resolveAttestation(
SchemaRecord memory schemaRecord,
Attestation memory attestation,
uint256 value,
bool isRevocation,
uint256 availableValue,
bool last
) private returns (uint256) {
ISchemaResolver resolver = schemaRecord.resolver;
if (address(resolver) == address(0)) {
// Ensure that we don't accept payments if there is no resolver.
if (value != 0) {
revert NotPayable();
}
return 0;
}
// Ensure that we don't accept payments which can't be forwarded to the resolver.
if (value != 0 && !resolver.isPayable()) {
revert NotPayable();
}
// Ensure that the attester/revoker doesn't try to spend more than available.
if (value > availableValue) {
revert InsufficientValue();
}
// Ensure to deduct the sent value explicitly.
unchecked {
availableValue -= value;
}
if (isRevocation) {
if (!resolver.revoke{ value: value }(attestation)) {
revert InvalidRevocation();
}
} else if (!resolver.attest{ value: value }(attestation)) {
revert InvalidAttestation();
}
if (last) {
_refund(availableValue);
}
return value;
}
/**
* @dev Resolves multiple attestations or revocations of existing attestations.
*
* @param schemaRecord The schema of the attestation.
* @param attestations The data of the attestations to make/revoke.
* @param values Explicit ETH amounts to send to the resolver.
* @param isRevocation Whether to resolve an attestation or its revocation.
* @param availableValue The total available ETH amount that can be sent to the resolver.
* @param last Whether this is the last attestations/revocations set.
*
* @return Returns the total sent ETH amount.
*/
function _resolveAttestations(
SchemaRecord memory schemaRecord,
Attestation[] memory attestations,
uint256[] memory values,
bool isRevocation,
uint256 availableValue,
bool last
) private returns (uint256) {
uint256 length = attestations.length;
if (length == 1) {
return _resolveAttestation(schemaRecord, attestations[0], values[0], isRevocation, availableValue, last);
}
ISchemaResolver resolver = schemaRecord.resolver;
if (address(resolver) == address(0)) {
// Ensure that we don't accept payments if there is no resolver.
for (uint256 i = 0; i < length; i = uncheckedInc(i)) {
if (values[i] != 0) {
revert NotPayable();
}
}
return 0;
}
uint256 totalUsedValue = 0;
for (uint256 i = 0; i < length; i = uncheckedInc(i)) {
uint256 value = values[i];
// Ensure that we don't accept payments which can't be forwarded to the resolver.
if (value != 0 && !resolver.isPayable()) {
revert NotPayable();
}
// Ensure that the attester/revoker doesn't try to spend more than available.
if (value > availableValue) {
revert InsufficientValue();
}
// Ensure to deduct the sent value explicitly and add it to the total used value by the batch.
unchecked {
availableValue -= value;
totalUsedValue += value;
}
}
if (isRevocation) {
if (!resolver.multiRevoke{ value: totalUsedValue }(attestations, values)) {
revert InvalidRevocations();
}
} else if (!resolver.multiAttest{ value: totalUsedValue }(attestations, values)) {
revert InvalidAttestations();
}
if (last) {
_refund(availableValue);
}
return totalUsedValue;
}
/**
* @dev Calculates a UID for a given attestation.
*
* @param attestation The input attestation.
* @param bump A bump value to use in case of a UID conflict.
*
* @return Attestation UID.
*/
function _getUID(Attestation memory attestation, uint32 bump) private pure returns (bytes32) {
return
keccak256(
abi.encodePacked(
attestation.schema,
attestation.recipient,
attestation.attester,
attestation.time,
attestation.expirationTime,
attestation.revocable,
attestation.refUID,
attestation.data,
bump
)
);
}
/**
* @dev Refunds remaining ETH amount to the attester.
*
* @param remainingValue The remaining ETH amount that was not sent to the resolver.
*/
function _refund(uint256 remainingValue) private {
if (remainingValue > 0) {
// Using a regular transfer here might revert, for some non-EOA attesters, due to exceeding of the 2300
// gas limit which is why we're using call instead (via sendValue), which the 2300 gas limit does not
// apply for.
payable(msg.sender).sendValue(remainingValue);
}
}
/**
* @dev Timestamps the specified bytes32 data.
*
* @param data The data to timestamp.
* @param time The timestamp.
*/
function _timestamp(bytes32 data, uint64 time) private {
if (_timestamps[data] != 0) {
revert AlreadyTimestamped();
}
_timestamps[data] = time;
emit Timestamped(data, time);
}
/**
* @dev Timestamps the specified bytes32 data.
*
* @param data The data to timestamp.
* @param time The timestamp.
*/
function _revokeOffchain(address revoker, bytes32 data, uint64 time) private {
mapping(bytes32 data => uint64 timestamp) storage revocations = _revocationsOffchain[revoker];
if (revocations[data] != 0) {
revert AlreadyRevokedOffchain();
}
revocations[data] = time;
emit RevokedOffchain(revoker, data, time);
}
/**
* @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);
}
/**
* @dev Merges lists of UIDs.
*
* @param uidLists The provided lists of UIDs.
* @param uidsCount Total UIDs count.
*
* @return A merged and flatten list of all the UIDs.
*/
function _mergeUIDs(bytes32[][] memory uidLists, uint256 uidsCount) private pure returns (bytes32[] memory) {
bytes32[] memory uids = new bytes32[](uidsCount);
uint256 currentIndex = 0;
for (uint256 i = 0; i < uidLists.length; i = uncheckedInc(i)) {
bytes32[] memory currentUids = uidLists[i];
for (uint256 j = 0; j < currentUids.length; j = uncheckedInc(j)) {
uids[currentIndex] = currentUids[j];
unchecked {
++currentIndex;
}
}
}
return uids;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import { ISchemaRegistry } from "./ISchemaRegistry.sol";
import { Attestation, EIP712Signature } from "./Common.sol";
/**
* @dev A struct representing the arguments of the attestation request.
*/
struct AttestationRequestData {
address recipient; // The recipient of the attestation.
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.
uint256 value; // An explicit ETH amount to send to the resolver. This is important to prevent accidental user errors.
}
/**
* @dev A struct representing the full arguments of the attestation request.
*/
struct AttestationRequest {
bytes32 schema; // The unique identifier of the schema.
AttestationRequestData data; // The arguments of the attestation request.
}
/**
* @dev A struct representing the full arguments of the full delegated attestation request.
*/
struct DelegatedAttestationRequest {
bytes32 schema; // The unique identifier of the schema.
AttestationRequestData data; // The arguments of the attestation request.
EIP712Signature signature; // The EIP712 signature data.
address attester; // The attesting account.
}
/**
* @dev A struct representing the full arguments of the multi attestation request.
*/
struct MultiAttestationRequest {
bytes32 schema; // The unique identifier of the schema.
AttestationRequestData[] data; // The arguments of the attestation request.
}
/**
* @dev A struct representing the full arguments of the delegated multi attestation request.
*/
struct MultiDelegatedAttestationRequest {
bytes32 schema; // The unique identifier of the schema.
AttestationRequestData[] data; // The arguments of the attestation requests.
EIP712Signature[] signatures; // The EIP712 signatures data. Please note that the signatures are assumed to be signed with increasing nonces.
address attester; // The attesting account.
}
/**
* @dev A struct representing the arguments of the revocation request.
*/
struct RevocationRequestData {
bytes32 uid; // The UID of the attestation to revoke.
uint256 value; // An explicit ETH amount to send to the resolver. This is important to prevent accidental user errors.
}
/**
* @dev A struct representing the full arguments of the revocation request.
*/
struct RevocationRequest {
bytes32 schema; // The unique identifier of the schema.
RevocationRequestData data; // The arguments of the revocation request.
}
/**
* @dev A struct representing the arguments of the full delegated revocation request.
*/
struct DelegatedRevocationRequest {
bytes32 schema; // The unique identifier of the schema.
RevocationRequestData data; // The arguments of the revocation request.
EIP712Signature signature; // The EIP712 signature data.
address revoker; // The revoking account.
}
/**
* @dev A struct representing the full arguments of the multi revocation request.
*/
struct MultiRevocationRequest {
bytes32 schema; // The unique identifier of the schema.
RevocationRequestData[] data; // The arguments of the revocation request.
}
/**
* @dev A struct representing the full arguments of the delegated multi revocation request.
*/
struct MultiDelegatedRevocationRequest {
bytes32 schema; // The unique identifier of the schema.
RevocationRequestData[] data; // The arguments of the revocation requests.
EIP712Signature[] signatures; // The EIP712 signatures data. Please note that the signatures are assumed to be signed with increasing nonces.
address revoker; // The revoking account.
}
/**
* @title EAS - Ethereum Attestation Service interface.
*/
interface IEAS {
/**
* @dev Emitted when an attestation has been made.
*
* @param recipient The recipient of the attestation.
* @param attester The attesting account.
* @param uid The UID the revoked attestation.
* @param schema The UID of the schema.
*/
event Attested(address indexed recipient, address indexed attester, bytes32 uid, bytes32 indexed schema);
/**
* @dev Emitted when an attestation has been revoked.
*
* @param recipient The recipient of the attestation.
* @param attester The attesting account.
* @param schema The UID of the schema.
* @param uid The UID the revoked attestation.
*/
event Revoked(address indexed recipient, address indexed attester, bytes32 uid, bytes32 indexed schema);
/**
* @dev Emitted when a data has been timestamped.
*
* @param data The data.
* @param timestamp The timestamp.
*/
event Timestamped(bytes32 indexed data, uint64 indexed timestamp);
/**
* @dev Emitted when a data has been revoked.
*
* @param revoker The address of the revoker.
* @param data The data.
* @param timestamp The timestamp.
*/
event RevokedOffchain(address indexed revoker, bytes32 indexed data, uint64 indexed timestamp);
/**
* @dev Returns the address of the global schema registry.
*
* @return The address of the global schema registry.
*/
function getSchemaRegistry() external view returns (ISchemaRegistry);
/**
* @dev Attests to a specific schema.
*
* @param request The arguments of the attestation request.
*
* Example:
*
* attest({
* schema: "0facc36681cbe2456019c1b0d1e7bedd6d1d40f6f324bf3dd3a4cef2999200a0",
* data: {
* recipient: "0xdEADBeAFdeAdbEafdeadbeafDeAdbEAFdeadbeaf",
* expirationTime: 0,
* revocable: true,
* refUID: "0x0000000000000000000000000000000000000000000000000000000000000000",
* data: "0xF00D",
* value: 0
* }
* })
*
* @return The UID of the new attestation.
*/
function attest(AttestationRequest calldata request) external payable returns (bytes32);
/**
* @dev Attests to a specific schema via the provided EIP712 signature.
*
* @param delegatedRequest The arguments of the delegated attestation request.
*
* Example:
*
* attestByDelegation({
* schema: '0x8e72f5bc0a8d4be6aa98360baa889040c50a0e51f32dbf0baa5199bd93472ebc',
* data: {
* recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
* expirationTime: 1673891048,
* revocable: true,
* refUID: '0x0000000000000000000000000000000000000000000000000000000000000000',
* data: '0x1234',
* value: 0
* },
* signature: {
* v: 28,
* r: '0x148c...b25b',
* s: '0x5a72...be22'
* },
* attester: '0xc5E8740aD971409492b1A63Db8d83025e0Fc427e'
* })
*
* @return The UID of the new attestation.
*/
function attestByDelegation(
DelegatedAttestationRequest calldata delegatedRequest
) external payable returns (bytes32);
/**
* @dev Attests to multiple schemas.
*
* @param multiRequests The arguments of the multi attestation requests. The requests should be grouped by distinct
* schema ids to benefit from the best batching optimization.
*
* Example:
*
* multiAttest([{
* schema: '0x33e9094830a5cba5554d1954310e4fbed2ef5f859ec1404619adea4207f391fd',
* data: [{
* recipient: '0xdEADBeAFdeAdbEafdeadbeafDeAdbEAFdeadbeaf',
* expirationTime: 1673891048,
* revocable: true,
* refUID: '0x0000000000000000000000000000000000000000000000000000000000000000',
* data: '0x1234',
* value: 1000
* },
* {
* recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
* expirationTime: 0,
* revocable: false,
* refUID: '0x480df4a039efc31b11bfdf491b383ca138b6bde160988222a2a3509c02cee174',
* data: '0x00',
* value: 0
* }],
* },
* {
* schema: '0x5ac273ce41e3c8bfa383efe7c03e54c5f0bff29c9f11ef6ffa930fc84ca32425',
* data: [{
* recipient: '0xdEADBeAFdeAdbEafdeadbeafDeAdbEAFdeadbeaf',
* expirationTime: 0,
* revocable: true,
* refUID: '0x75bf2ed8dca25a8190c50c52db136664de25b2449535839008ccfdab469b214f',
* data: '0x12345678',
* value: 0
* },
* }])
*
* @return The UIDs of the new attestations.
*/
function multiAttest(MultiAttestationRequest[] calldata multiRequests) external payable returns (bytes32[] memory);
/**
* @dev Attests to multiple schemas using via provided EIP712 signatures.
*
* @param multiDelegatedRequests The arguments of the delegated multi attestation requests. The requests should be
* grouped by distinct schema ids to benefit from the best batching optimization.
*
* Example:
*
* multiAttestByDelegation([{
* schema: '0x8e72f5bc0a8d4be6aa98360baa889040c50a0e51f32dbf0baa5199bd93472ebc',
* data: [{
* recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
* expirationTime: 1673891048,
* revocable: true,
* refUID: '0x0000000000000000000000000000000000000000000000000000000000000000',
* data: '0x1234',
* value: 0
* },
* {
* recipient: '0xdEADBeAFdeAdbEafdeadbeafDeAdbEAFdeadbeaf',
* expirationTime: 0,
* revocable: false,
* refUID: '0x0000000000000000000000000000000000000000000000000000000000000000',
* data: '0x00',
* value: 0
* }],
* signatures: [{
* v: 28,
* r: '0x148c...b25b',
* s: '0x5a72...be22'
* },
* {
* v: 28,
* r: '0x487s...67bb',
* s: '0x12ad...2366'
* }],
* attester: '0x1D86495b2A7B524D747d2839b3C645Bed32e8CF4'
* }])
*
* @return The UIDs of the new attestations.
*/
function multiAttestByDelegation(
MultiDelegatedAttestationRequest[] calldata multiDelegatedRequests
) external payable returns (bytes32[] memory);
/**
* @dev Revokes an existing attestation to a specific schema.
*
* Example:
*
* revoke({
* schema: '0x8e72f5bc0a8d4be6aa98360baa889040c50a0e51f32dbf0baa5199bd93472ebc',
* data: {
* uid: '0x101032e487642ee04ee17049f99a70590c735b8614079fc9275f9dd57c00966d',
* value: 0
* }
* })
*
* @param request The arguments of the revocation request.
*/
function revoke(RevocationRequest calldata request) external payable;
/**
* @dev Revokes an existing attestation to a specific schema via the provided EIP712 signature.
*
* Example:
*
* revokeByDelegation({
* schema: '0x8e72f5bc0a8d4be6aa98360baa889040c50a0e51f32dbf0baa5199bd93472ebc',
* data: {
* uid: '0xcbbc12102578c642a0f7b34fe7111e41afa25683b6cd7b5a14caf90fa14d24ba',
* value: 0
* },
* signature: {
* v: 27,
* r: '0xb593...7142',
* s: '0x0f5b...2cce'
* },
* revoker: '0x244934dd3e31bE2c81f84ECf0b3E6329F5381992'
* })
*
* @param delegatedRequest The arguments of the delegated revocation request.
*/
function revokeByDelegation(DelegatedRevocationRequest calldata delegatedRequest) external payable;
/**
* @dev Revokes existing attestations to multiple schemas.
*
* @param multiRequests The arguments of the multi revocation requests. The requests should be grouped by distinct
* schema ids to benefit from the best batching optimization.
*
* Example:
*
* multiRevoke([{
* schema: '0x8e72f5bc0a8d4be6aa98360baa889040c50a0e51f32dbf0baa5199bd93472ebc',
* data: [{
* uid: '0x211296a1ca0d7f9f2cfebf0daaa575bea9b20e968d81aef4e743d699c6ac4b25',
* value: 1000
* },
* {
* uid: '0xe160ac1bd3606a287b4d53d5d1d6da5895f65b4b4bab6d93aaf5046e48167ade',
* value: 0
* }],
* },
* {
* schema: '0x5ac273ce41e3c8bfa383efe7c03e54c5f0bff29c9f11ef6ffa930fc84ca32425',
* data: [{
* uid: '0x053d42abce1fd7c8fcddfae21845ad34dae287b2c326220b03ba241bc5a8f019',
* value: 0
* },
* }])
*/
function multiRevoke(MultiRevocationRequest[] calldata multiRequests) external payable;
/**
* @dev Revokes existing attestations to multiple schemas via provided EIP712 signatures.
*
* @param multiDelegatedRequests The arguments of the delegated multi revocation attestation requests. The requests should be
* grouped by distinct schema ids to benefit from the best batching optimization.
*
* Example:
*
* multiRevokeByDelegation([{
* schema: '0x8e72f5bc0a8d4be6aa98360baa889040c50a0e51f32dbf0baa5199bd93472ebc',
* data: [{
* uid: '0x211296a1ca0d7f9f2cfebf0daaa575bea9b20e968d81aef4e743d699c6ac4b25',
* value: 1000
* },
* {
* uid: '0xe160ac1bd3606a287b4d53d5d1d6da5895f65b4b4bab6d93aaf5046e48167ade',
* value: 0
* }],
* signatures: [{
* v: 28,
* r: '0x148c...b25b',
* s: '0x5a72...be22'
* },
* {
* v: 28,
* r: '0x487s...67bb',
* s: '0x12ad...2366'
* }],
* revoker: '0x244934dd3e31bE2c81f84ECf0b3E6329F5381992'
* }])
*
*/
function multiRevokeByDelegation(
MultiDelegatedRevocationRequest[] calldata multiDelegatedRequests
) external payable;
/**
* @dev Timestamps the specified bytes32 data.
*
* @param data The data to timestamp.
*
* @return The timestamp the data was timestamped with.
*/
function timestamp(bytes32 data) external returns (uint64);
/**
* @dev Timestamps the specified multiple bytes32 data.
*
* @param data The data to timestamp.
*
* @return The timestamp the data was timestamped with.
*/
function multiTimestamp(bytes32[] calldata data) external returns (uint64);
/**
* @dev Revokes the specified bytes32 data.
*
* @param data The data to timestamp.
*
* @return The timestamp the data was revoked with.
*/
function revokeOffchain(bytes32 data) external returns (uint64);
/**
* @dev Revokes the specified multiple bytes32 data.
*
* @param data The data to timestamp.
*
* @return The timestamp the data was revoked with.
*/
function multiRevokeOffchain(bytes32[] calldata data) external returns (uint64);
/**
* @dev Returns an existing attestation by UID.
*
* @param uid The UID of the attestation to retrieve.
*
* @return The attestation data members.
*/
function getAttestation(bytes32 uid) external view returns (Attestation memory);
/**
* @dev Checks whether an attestation exists.
*
* @param uid The UID of the attestation to retrieve.
*
* @return Whether an attestation exists.
*/
function isAttestationValid(bytes32 uid) external view returns (bool);
/**
* @dev Returns the timestamp that the specified data was timestamped with.
*
* @param data The data to query.
*
* @return The timestamp the data was timestamped with.
*/
function getTimestamp(bytes32 data) external view returns (uint64);
/**
* @dev Returns the timestamp that the specified data was timestamped with.
*
* @param data The data to query.
*
* @return The timestamp the data was timestamped with.
*/
function getRevokeOffchain(address revoker, bytes32 data) external view returns (uint64);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import { ISchemaResolver } from "./resolver/ISchemaResolver.sol";
/**
* @title A struct representing a record for a submitted schema.
*/
struct SchemaRecord {
bytes32 uid; // The unique identifier of the schema.
ISchemaResolver resolver; // Optional schema resolver.
bool revocable; // Whether the schema allows revocations explicitly.
string schema; // Custom specification of the schema (e.g., an ABI).
}
/**
* @title The global schema registry interface.
*/
interface ISchemaRegistry {
/**
* @dev Emitted when a new schema has been registered
*
* @param uid The schema UID.
* @param registerer The address of the account used to register the schema.
*/
event Registered(bytes32 indexed uid, address registerer);
/**
* @dev Submits and reserves a new schema
*
* @param schema The schema data schema.
* @param resolver An optional schema resolver.
* @param revocable Whether the schema allows revocations explicitly.
*
* @return The UID of the new schema.
*/
function register(string calldata schema, ISchemaResolver resolver, bool revocable) external returns (bytes32);
/**
* @dev Returns an existing schema by UID
*
* @param uid The UID of the schema to retrieve.
*
* @return The schema data members.
*/
function getSchema(bytes32 uid) external view returns (SchemaRecord memory);
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import { Semver } from "../universal/Semver.sol";
import { ISchemaResolver } from "./resolver/ISchemaResolver.sol";
import { EMPTY_UID, MAX_GAP } from "./Common.sol";
import { ISchemaRegistry, SchemaRecord } from "./ISchemaRegistry.sol";
/**
* @title The global schema registry.
*/
contract SchemaRegistry is ISchemaRegistry, Semver, Initializable {
error AlreadyExists();
// The global mapping between schema records and their IDs.
mapping(bytes32 uid => SchemaRecord schemaRecord) private _registry;
// Upgrade forward-compatibility storage gap
uint256[MAX_GAP - 1] private __gap;
/**
* @dev Creates a new SchemaRegistry instance.
*/
constructor() Semver(1, 0, 0) {}
/**
* @dev Initializes the contract and its parents.
*/
function initialize() external initializer {
__SchemaRegistry_init();
}
// solhint-disable func-name-mixedcase
/**
* @dev Upgradeable initialization.
*/
function __SchemaRegistry_init() internal onlyInitializing {
__SchemaRegistry_init_unchained();
}
/**
* @dev Upgradeable initialization.
*/
function __SchemaRegistry_init_unchained() internal onlyInitializing {}
// solhint-enable func-name-mixedcase
/**
* @inheritdoc ISchemaRegistry
*/
function register(string calldata schema, ISchemaResolver resolver, bool revocable) external returns (bytes32) {
SchemaRecord memory schemaRecord = SchemaRecord({
uid: EMPTY_UID,
schema: schema,
resolver: resolver,
revocable: revocable
});
bytes32 uid = _getUID(schemaRecord);
if (_registry[uid].uid != EMPTY_UID) {
revert AlreadyExists();
}
schemaRecord.uid = uid;
_registry[uid] = schemaRecord;
emit Registered(uid, msg.sender);
return uid;
}
/**
* @inheritdoc ISchemaRegistry
*/
function getSchema(bytes32 uid) external view returns (SchemaRecord memory) {
return _registry[uid];
}
/**
* @dev Calculates a UID for a given schema.
*
* @param schemaRecord The input schema.
*
* @return schema UID.
*/
function _getUID(SchemaRecord memory schemaRecord) private pure returns (bytes32) {
return keccak256(abi.encodePacked(schemaRecord.schema, schemaRecord.resolver, schemaRecord.revocable));
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol";
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
// prettier-ignore
import {
AttestationRequest,
AttestationRequestData,
DelegatedAttestationRequest,
DelegatedRevocationRequest,
RevocationRequest,
RevocationRequestData
} from "../IEAS.sol";
import { EIP712Signature, InvalidSignature, MAX_GAP, stringToBytes32, bytes32ToString } from "../Common.sol";
/**
* @title EIP712 typed signatures verifier for EAS delegated attestations.
*/
abstract contract EIP712Verifier is EIP712 {
// The hash of the data type used to relay calls to the attest function. It's the value of
// keccak256("Attest(bytes32 schema,address recipient,uint64 expirationTime,bool revocable,bytes32 refUID,bytes data,uint256 nonce)").
bytes32 private constant ATTEST_TYPEHASH = 0xdbfdf8dc2b135c26253e00d5b6cbe6f20457e003fd526d97cea183883570de61;
// The hash of the data type used to relay calls to the revoke function. It's the value of
// keccak256("Revoke(bytes32 schema,bytes32 uid,uint256 nonce)").
bytes32 private constant REVOKE_TYPEHASH = 0xa98d02348410c9c76735e0d0bb1396f4015ac2bb9615f9c2611d19d7a8a99650;
// The user readable name of the signing domain.
bytes32 private immutable _name;
// Replay protection nonces.
mapping(address => uint256) private _nonces;
// Upgrade forward-compatibility storage gap
uint256[MAX_GAP - 1] private __gap;
/**
* @dev Creates a new EIP712Verifier instance.
*
* @param version The current major version of the signing domain
*/
constructor(string memory name, string memory version) EIP712(name, version) {
_name = stringToBytes32(name);
}
/**
* @dev Returns the domain separator used in the encoding of the signatures for attest, and revoke.
*/
function getDomainSeparator() external view returns (bytes32) {
return _domainSeparatorV4();
}
/**
* @dev Returns the current nonce per-account.
*
* @param account The requested account.
*
* @return The current nonce.
*/
function getNonce(address account) external view returns (uint256) {
return _nonces[account];
}
/**
* Returns the EIP712 type hash for the attest function.
*/
function getAttestTypeHash() external pure returns (bytes32) {
return ATTEST_TYPEHASH;
}
/**
* Returns the EIP712 type hash for the revoke function.
*/
function getRevokeTypeHash() external pure returns (bytes32) {
return REVOKE_TYPEHASH;
}
/**
* Returns the EIP712 name.
*/
function getName() external view returns (string memory) {
return bytes32ToString(_name);
}
/**
* @dev Verifies delegated attestation request.
*
* @param request The arguments of the delegated attestation request.
*/
function _verifyAttest(DelegatedAttestationRequest memory request) internal {
AttestationRequestData memory data = request.data;
EIP712Signature memory signature = request.signature;
uint256 nonce;
unchecked {
nonce = _nonces[request.attester]++;
}
bytes32 digest = _hashTypedDataV4(
keccak256(
abi.encode(
ATTEST_TYPEHASH,
request.schema,
data.recipient,
data.expirationTime,
data.revocable,
data.refUID,
keccak256(data.data),
nonce
)
)
);
if (ECDSA.recover(digest, signature.v, signature.r, signature.s) != request.attester) {
revert InvalidSignature();
}
}
/**
* @dev Verifies delegated revocation request.
*
* @param request The arguments of the delegated revocation request.
*/
function _verifyRevoke(DelegatedRevocationRequest memory request) internal {
RevocationRequestData memory data = request.data;
EIP712Signature memory signature = request.signature;
uint256 nonce;
unchecked {
nonce = _nonces[request.revoker]++;
}
bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(REVOKE_TYPEHASH, request.schema, data.uid, nonce)));
if (ECDSA.recover(digest, signature.v, signature.r, signature.s) != request.revoker) {
revert InvalidSignature();
}
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import { Attestation } from "../Common.sol";
/**
* @title The interface of an optional schema resolver.
*/
interface ISchemaResolver {
/**
* @dev Returns whether the resolver supports ETH transfers.
*/
function isPayable() external pure returns (bool);
/**
* @dev Processes an attestation and verifies whether it's valid.
*
* @param attestation The new attestation.
*
* @return Whether the attestation is valid.
*/
function attest(Attestation calldata attestation) external payable returns (bool);
/**
* @dev Processes multiple attestations and verifies whether they are valid.
*
* @param attestations The new attestations.
* @param values Explicit ETH amounts which were sent with each attestation.
*
* @return Whether all the attestations are valid.
*/
function multiAttest(
Attestation[] calldata attestations,
uint256[] calldata values
) external payable returns (bool);
/**
* @dev Processes an attestation revocation and verifies if it can be revoked.
*
* @param attestation The existing attestation to be revoked.
*
* @return Whether the attestation can be revoked.
*/
function revoke(Attestation calldata attestation) external payable returns (bool);
/**
* @dev Processes revocation of multiple attestation and verifies they can be revoked.
*
* @param attestations The existing attestations to be revoked.
* @param values Explicit ETH amounts which were sent with each revocation.
*
* @return Whether the attestations can be revoked.
*/
function multiRevoke(
Attestation[] calldata attestations,
uint256[] calldata values
) external payable returns (bool);
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import { Semver } from "../../universal/Semver.sol";
import { IEAS, Attestation } from "../IEAS.sol";
import { InvalidEAS, uncheckedInc } from "../Common.sol";
import { ISchemaResolver } from "./ISchemaResolver.sol";
/**
* @title A base resolver contract
*/
abstract contract SchemaResolver is ISchemaResolver, Semver {
error AccessDenied();
error InsufficientValue();
error NotPayable();
// The global EAS contract.
IEAS internal immutable _eas;
/**
* @dev Creates a new resolver.
*
* @param eas The address of the global EAS contract.
*/
constructor(IEAS eas) Semver(1, 0, 0) {
if (address(eas) == address(0)) {
revert InvalidEAS();
}
_eas = eas;
}
/**
* @dev Ensures that only the EAS contract can make this call.
*/
modifier onlyEAS() {
_onlyEAS();
_;
}
/**
* @inheritdoc ISchemaResolver
*/
function isPayable() public pure virtual returns (bool) {
return false;
}
/**
* @dev ETH callback.
*/
receive() external payable virtual {
if (!isPayable()) {
revert NotPayable();
}
}
/**
* @inheritdoc ISchemaResolver
*/
function attest(Attestation calldata attestation) external payable onlyEAS returns (bool) {
return onAttest(attestation, msg.value);
}
/**
* @inheritdoc ISchemaResolver
*/
function multiAttest(
Attestation[] calldata attestations,
uint256[] calldata values
) external payable onlyEAS returns (bool) {
uint256 length = attestations.length;
// We are keeping track of the remaining ETH amount that can be sent to resolvers and will keep deducting
// from it to verify that there isn't any attempt to send too much ETH to resolvers. Please note that unless
// some ETH was stuck in the contract by accident (which shouldn't happen in normal conditions), it won't be
// possible to send too much ETH anyway.
uint256 remainingValue = msg.value;
for (uint256 i = 0; i < length; i = uncheckedInc(i)) {
// Ensure that the attester/revoker doesn't try to spend more than available.
uint256 value = values[i];
if (value > remainingValue) {
revert InsufficientValue();
}
// Forward the attestation to the underlying resolver and revert in case it isn't approved.
if (!onAttest(attestations[i], value)) {
return false;
}
unchecked {
// Subtract the ETH amount, that was provided to this attestation, from the global remaining ETH amount.
remainingValue -= value;
}
}
return true;
}
/**
* @inheritdoc ISchemaResolver
*/
function revoke(Attestation calldata attestation) external payable onlyEAS returns (bool) {
return onRevoke(attestation, msg.value);
}
/**
* @inheritdoc ISchemaResolver
*/
function multiRevoke(
Attestation[] calldata attestations,
uint256[] calldata values
) external payable onlyEAS returns (bool) {
uint256 length = attestations.length;
// We are keeping track of the remaining ETH amount that can be sent to resolvers and will keep deducting
// from it to verify that there isn't any attempt to send too much ETH to resolvers. Please note that unless
// some ETH was stuck in the contract by accident (which shouldn't happen in normal conditions), it won't be
// possible to send too much ETH anyway.
uint256 remainingValue = msg.value;
for (uint256 i = 0; i < length; i = uncheckedInc(i)) {
// Ensure that the attester/revoker doesn't try to spend more than available.
uint256 value = values[i];
if (value > remainingValue) {
revert InsufficientValue();
}
// Forward the revocation to the underlying resolver and revert in case it isn't approved.
if (!onRevoke(attestations[i], value)) {
return false;
}
unchecked {
// Subtract the ETH amount, that was provided to this attestation, from the global remaining ETH amount.
remainingValue -= value;
}
}
return true;
}
/**
* @dev A resolver callback that should be implemented by child contracts.
*
* @param attestation The new attestation.
* @param value An explicit ETH amount that was sent to the resolver. Please note that this value is verified in
* both attest() and multiAttest() callbacks EAS-only callbacks and that in case of multi attestations, it'll
* usually hold that msg.value != value, since msg.value aggregated the sent ETH amounts for all the attestations
* in the batch.
*
* @return Whether the attestation is valid.
*/
function onAttest(Attestation calldata attestation, uint256 value) internal virtual returns (bool);
/**
* @dev Processes an attestation revocation and verifies if it can be revoked.
*
* @param attestation The existing attestation to be revoked.
* @param value An explicit ETH amount that was sent to the resolver. Please note that this value is verified in
* both revoke() and multiRevoke() callbacks EAS-only callbacks and that in case of multi attestations, it'll
* usually hold that msg.value != value, since msg.value aggregated the sent ETH amounts for all the attestations
* in the batch.
*
* @return Whether the attestation can be revoked.
*/
function onRevoke(Attestation calldata attestation, uint256 value) internal virtual returns (bool);
/**
* @dev Ensures that only the EAS contract can make this call.
*/
function _onlyEAS() private view {
if (msg.sender != address(_eas)) {
revert AccessDenied();
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment