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);
}
This diff is collapsed.
This diff is collapsed.
// 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