From 6c842eed4937c6530cf698db22362f6435c054f7 Mon Sep 17 00:00:00 2001 From: ra-phael <10075759+ra-phael@users.noreply.github.com> Date: Mon, 3 Jul 2023 22:15:48 +0100 Subject: [PATCH 1/8] add initial draft --- EIPS/ethereum-access-token-eip.md | 187 ++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 EIPS/ethereum-access-token-eip.md diff --git a/EIPS/ethereum-access-token-eip.md b/EIPS/ethereum-access-token-eip.md new file mode 100644 index 00000000000000..47c1775f596690 --- /dev/null +++ b/EIPS/ethereum-access-token-eip.md @@ -0,0 +1,187 @@ +--- +title: Ethereum Access Token +description: A protocol for authorizing function calls from an off-chain service +author: Chris Chung (@0xpApaSmURf), Raphael Roullet (@ra-phael) +discussions-to: +status: Draft +type: Standards Track +category: ERC +created: 2023-07-03 +requires: EIP-712 +--- + +## Abstract + +An Ethereum Access Token (EAT) is an [EIP-712](./eip-712.md) conformant, signed message, used by off-chain services to grant Ethereum accounts access to specific on-chain resources. EATs share similarities with JWTs; both are used for short-lived authorizations. However Ethereum Access Tokens are specifically designed to be verified on-chain and tailored to authorize smart contract function calls. + +## Motivation + +While other proposals tackle authentication ([EIP-4361](./eip-4361.md)) or authorization in a more narrow way ([EIP-2612](./eip-2612.md)), this specification allows developers to add a layer of access control to any function they create with minimal changes. It is best suited for use cases where end users should only be able to access specific on-chain resources themselves directly, by way of sending a transaction, provided they have been granted authorization by an off-chain service first. Examples of such scenarios include an off-chain verifier assessing eligibility requirements (e.g by verifying verifiable credentials) to mint a token or to interact with a smart contract that requires a certain compliance status. +Therefore, this proposal enables off-chain systems to authenticate the controller of an Ethereum account in any way they want, before granting an authorization bound to said account. + +This specification is intended to improve interoperability in the Ethereum ecosystem, by providing a consistent machine-readable message format to achieve improved user experiences. + +EATs fill a void where access control requirements differ from current standard access control mechanisms (role-based access modifiers or checking that an address owns an NFT): +- Desired acccess is short-lived +- Criteria needs to be flexible/dynamic: updating the requirements for granting access doesn't require any update on chain +- When Soulbound or other on-chain token semantics are not desired. Using any kind of "on-chain registry" to grant authorization places a burden on the owner of such registry to keep it up-to-date at all time. Otherwise, someone might be wrongly granted access in the lapse of time where their on-chain status is incorrect. With EATs, on the contrary, users come to ask for an authorization which gives EAT issuers the opportunity to perform some checks and update their records before granting authorization. Additionally, relying purely on on-chain data comes with privacy concerns due to the public nature of most of current chains. When authorization needs to be granted based on sensitive or personally identifiable information, it is not recommended to store that information on-chain and perform a lookup. Ethereum Access Tokens provide an alternative which doesn't leak any PII on-chain. + +## Specification + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. + +### Overview + +An example flow integrated in a DeFi application is the following: +1. A user interacts with the DeFi's off-chain service, providing sufficient input for the off-chain service to ensure the user meets its criteria (for example, authenticates the user and/or make sure they possess valid credentials) +2. If authorization is granted, an EAT is issued to the user +3. The user then interacts with the gated smart contract function within the specified period of time passing the EAT as part of the transaction +4. The EAT is verified on-chain + +TODO: Add chart from assets + + +An Ethereum Access Token MUST guarantee granular access control by binding it to specific parameters upon issuance. Then, on-chain EAT verification ensures that: +- The function being called is the expected one +- The function parameters are the expected ones +- The function caller is the expected one +- The function is being called in the authorized timeframe (i.e checking that the EAT is not expired) +- The smart contract being called is the expected one +- The authorization has been given by a valid issuer, i.e the EAT has been signed by one of the expected issuers + + +### Structure of an Ethereum Access Token + +An Ethereum Access Token is composed of a signature and expiry. + +``` +{ + uint8 v, + bytes32 r, + bytes32 s, + uint256 expiry +} +``` + +The signature is obtained using the typed structured data hashing and signing standard (EIP-712), signing over the following EAT payload: + +``` +struct AccessToken { + uint256 expiry; + FunctionCall functionCall; +} + +struct FunctionCall { + bytes4 functionSignature; + address target; + address caller; + bytes parameters; +} +``` + +- **expiry**: unix timestamp, expected to be before `block.timestamp` + +`FunctionCall` parameters correspond to the following: + +- **functionSignature**: identifier for the function being called, expected to match `msg.sig` +- **target**: address of the target contract being called +- **caller**: address of the current caller - expected to match `msg.sender` +- **parameters**: `calldata` after stripping off the first parameters, namely `v`,`r`, `s` and `expiry` + + +### EAT Verification + +On chain, two contracts are necessary: an `AccessTokenConsumer` which is inherited by contracts needing to permission some of its functions and an `AccessTokenVerifier` which is responsible for verifying EATs. + +A modifier is added to functions that need to be permissioned with an EAT. +Given a function such as: + +``` +function transfer(address recipient, uint256 amount) +``` + +Its permissioned version, protected via an EAT becomes: +``` +function transfer( + uint8 v, + bytes32 r, + bytes32 s, + uint256 expiry, + address recipient, + uint256 amount + ) + requiresAuth(v, r, s, expiry) { + ... + } + +``` +The parameters `v`, `r`, `s`, `expiry`, which form an EAT, MUST be placed first, before any other parameters. + + +The `requiresAuth` modifier is inherited from `AccessTokenConsumer` which verifies the Ethereum Access Token and consumes it: + +``` + modifier requiresAuth( + uint8 v, + bytes32 r, + bytes32 s, + uint256 expiry + ) { + // VF -> Verification Failure + require(verify(v, r, s, expiry), "AccessToken: VF"); + _consumeAccessToken(v, r, s, expiry); + _; + } +``` + +The `verify` function calls the `AccessTokenVerifier` registered upon construction to verify the EAT's integrity. + +Optionally, implementers can decide to make EATs non-replayable. This what `_consumeAccessToken(...)` does. It marks an EAT as consumed to prevent its re-use: + +``` +function _consumeAccessToken( + uint8 v, + bytes32 r, + bytes32 s, + uint256 expiry + ) private { + bytes32 accessTokenHash = keccak256(abi.encodePacked(v, r, s, expiry)); + + _accessTokenUsed[accessTokenHash] = true; + } +``` + +## Rationale + +- Single-use. The reference implementation guarantees non-replayability of EATs. But other implementations might favor a different approach. + +- Use of EIP-712. By conforming to EIP-712, EATs are interoperable with existing Ethereum infrastructure, and developers can use them to create access controls with minimal modifications to their existing code. It also ensures that EATs issued are bound to a specific chain. + +- Zero-knowledge proofs. Using ZKPs comes at a cost, including added complexity. EATs are not much more than signed messages which are simpler to reason around. While `ecrecover` is available in any Ethereum smart contract out of the box, ZKPs come in different flavors which hinders interoperability. + +## Backwards Compatibility + +Any function can be gated with an EAT, apart from the special `receive` and `fallback` functions. + + +## Reference Implementation + +TODO: add from /assets + +## Security Considerations + +The security of the Ethereum Access Token (EAT) proposal depends on several factors: + +### Replay Attacks: +The implementation MAY ensure that an EAT cannot be reused after it has been consumed. This is achieved by marking the EAT as consumed in the `_consumeAccessToken` function. + +### Off-Chain Issuance: +The security of the off-chain service issuing EATs is critical since the security of EAT-gated functions depends on it. +If this service is compromised, malicious actors could be granted EATs giving them access to on-chain resources that they should not have access to. + +### Expiry Time Considerations: +The expiry time of the EAT must be set judiciously to balance usability and security. If the expiry time is set too long, it might increase the risk of EAT misuse. If it's too short, it might compromise the usability of the application. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). \ No newline at end of file From 13ed23a108770e6bab21f5c34743d2a09df0f8da Mon Sep 17 00:00:00 2001 From: ra-phael <10075759+ra-phael@users.noreply.github.com> Date: Mon, 3 Jul 2023 22:22:25 +0100 Subject: [PATCH 2/8] fix EIP walidator issues --- EIPS/ethereum-access-token-eip.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/EIPS/ethereum-access-token-eip.md b/EIPS/ethereum-access-token-eip.md index 47c1775f596690..f186bd9e605426 100644 --- a/EIPS/ethereum-access-token-eip.md +++ b/EIPS/ethereum-access-token-eip.md @@ -1,4 +1,5 @@ --- +eip: TBD title: Ethereum Access Token description: A protocol for authorizing function calls from an off-chain service author: Chris Chung (@0xpApaSmURf), Raphael Roullet (@ra-phael) @@ -7,16 +8,16 @@ status: Draft type: Standards Track category: ERC created: 2023-07-03 -requires: EIP-712 +requires: 712 --- ## Abstract -An Ethereum Access Token (EAT) is an [EIP-712](./eip-712.md) conformant, signed message, used by off-chain services to grant Ethereum accounts access to specific on-chain resources. EATs share similarities with JWTs; both are used for short-lived authorizations. However Ethereum Access Tokens are specifically designed to be verified on-chain and tailored to authorize smart contract function calls. +An Ethereum Access Token (EAT) is an [ERC-712](./eip-712.md) conformant, signed message, used by off-chain services to grant Ethereum accounts access to specific on-chain resources. EATs share similarities with JWTs; both are used for short-lived authorizations. However Ethereum Access Tokens are specifically designed to be verified on-chain and tailored to authorize smart contract function calls. ## Motivation -While other proposals tackle authentication ([EIP-4361](./eip-4361.md)) or authorization in a more narrow way ([EIP-2612](./eip-2612.md)), this specification allows developers to add a layer of access control to any function they create with minimal changes. It is best suited for use cases where end users should only be able to access specific on-chain resources themselves directly, by way of sending a transaction, provided they have been granted authorization by an off-chain service first. Examples of such scenarios include an off-chain verifier assessing eligibility requirements (e.g by verifying verifiable credentials) to mint a token or to interact with a smart contract that requires a certain compliance status. +While other proposals tackle authentication ([ERC-4361](./eip-4361.md)) or authorization in a more narrow way ([ERC-2612](./eip-2612.md)), this specification allows developers to add a layer of access control to any function they create with minimal changes. It is best suited for use cases where end users should only be able to access specific on-chain resources themselves directly, by way of sending a transaction, provided they have been granted authorization by an off-chain service first. Examples of such scenarios include an off-chain verifier assessing eligibility requirements (e.g by verifying verifiable credentials) to mint a token or to interact with a smart contract that requires a certain compliance status. Therefore, this proposal enables off-chain systems to authenticate the controller of an Ethereum account in any way they want, before granting an authorization bound to said account. This specification is intended to improve interoperability in the Ethereum ecosystem, by providing a consistent machine-readable message format to achieve improved user experiences. From 520fb22842048bf5563532b5203a81a8e7145958 Mon Sep 17 00:00:00 2001 From: ra-phael <10075759+ra-phael@users.noreply.github.com> Date: Tue, 4 Jul 2023 08:42:32 +0100 Subject: [PATCH 3/8] eip number --- EIPS/{ethereum-access-token-eip.md => eip-7272.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename EIPS/{ethereum-access-token-eip.md => eip-7272.md} (99%) diff --git a/EIPS/ethereum-access-token-eip.md b/EIPS/eip-7272.md similarity index 99% rename from EIPS/ethereum-access-token-eip.md rename to EIPS/eip-7272.md index f186bd9e605426..91a167cb8a3142 100644 --- a/EIPS/ethereum-access-token-eip.md +++ b/EIPS/eip-7272.md @@ -1,5 +1,5 @@ --- -eip: TBD +eip: 7272 title: Ethereum Access Token description: A protocol for authorizing function calls from an off-chain service author: Chris Chung (@0xpApaSmURf), Raphael Roullet (@ra-phael) From 05f51fec7e7ad9df957a2c94da7f7376202432cc Mon Sep 17 00:00:00 2001 From: ra-phael <10075759+ra-phael@users.noreply.github.com> Date: Tue, 4 Jul 2023 08:44:00 +0100 Subject: [PATCH 4/8] use eip for interface --- EIPS/eip-7272.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-7272.md b/EIPS/eip-7272.md index 91a167cb8a3142..6c3b59d6c3555e 100644 --- a/EIPS/eip-7272.md +++ b/EIPS/eip-7272.md @@ -13,7 +13,7 @@ requires: 712 ## Abstract -An Ethereum Access Token (EAT) is an [ERC-712](./eip-712.md) conformant, signed message, used by off-chain services to grant Ethereum accounts access to specific on-chain resources. EATs share similarities with JWTs; both are used for short-lived authorizations. However Ethereum Access Tokens are specifically designed to be verified on-chain and tailored to authorize smart contract function calls. +An Ethereum Access Token (EAT) is an [EIP-712](./eip-712.md) conformant, signed message, used by off-chain services to grant Ethereum accounts access to specific on-chain resources. EATs share similarities with JWTs; both are used for short-lived authorizations. However Ethereum Access Tokens are specifically designed to be verified on-chain and tailored to authorize smart contract function calls. ## Motivation From 14494d394052626a4d98394a284fd1ee72a36b02 Mon Sep 17 00:00:00 2001 From: ra-phael <10075759+ra-phael@users.noreply.github.com> Date: Tue, 4 Jul 2023 08:49:52 +0100 Subject: [PATCH 5/8] add missing blank lines --- EIPS/eip-7272.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/EIPS/eip-7272.md b/EIPS/eip-7272.md index 6c3b59d6c3555e..a44b9acb650d48 100644 --- a/EIPS/eip-7272.md +++ b/EIPS/eip-7272.md @@ -23,6 +23,7 @@ Therefore, this proposal enables off-chain systems to authenticate the controlle This specification is intended to improve interoperability in the Ethereum ecosystem, by providing a consistent machine-readable message format to achieve improved user experiences. EATs fill a void where access control requirements differ from current standard access control mechanisms (role-based access modifiers or checking that an address owns an NFT): + - Desired acccess is short-lived - Criteria needs to be flexible/dynamic: updating the requirements for granting access doesn't require any update on chain - When Soulbound or other on-chain token semantics are not desired. Using any kind of "on-chain registry" to grant authorization places a burden on the owner of such registry to keep it up-to-date at all time. Otherwise, someone might be wrongly granted access in the lapse of time where their on-chain status is incorrect. With EATs, on the contrary, users come to ask for an authorization which gives EAT issuers the opportunity to perform some checks and update their records before granting authorization. Additionally, relying purely on on-chain data comes with privacy concerns due to the public nature of most of current chains. When authorization needs to be granted based on sensitive or personally identifiable information, it is not recommended to store that information on-chain and perform a lookup. Ethereum Access Tokens provide an alternative which doesn't leak any PII on-chain. @@ -34,6 +35,7 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S ### Overview An example flow integrated in a DeFi application is the following: + 1. A user interacts with the DeFi's off-chain service, providing sufficient input for the off-chain service to ensure the user meets its criteria (for example, authenticates the user and/or make sure they possess valid credentials) 2. If authorization is granted, an EAT is issued to the user 3. The user then interacts with the gated smart contract function within the specified period of time passing the EAT as part of the transaction @@ -43,6 +45,7 @@ TODO: Add chart from assets An Ethereum Access Token MUST guarantee granular access control by binding it to specific parameters upon issuance. Then, on-chain EAT verification ensures that: + - The function being called is the expected one - The function parameters are the expected ones - The function caller is the expected one @@ -102,6 +105,7 @@ function transfer(address recipient, uint256 amount) ``` Its permissioned version, protected via an EAT becomes: + ``` function transfer( uint8 v, @@ -116,6 +120,7 @@ function transfer( } ``` + The parameters `v`, `r`, `s`, `expiry`, which form an EAT, MUST be placed first, before any other parameters. @@ -174,15 +179,18 @@ TODO: add from /assets The security of the Ethereum Access Token (EAT) proposal depends on several factors: ### Replay Attacks: + The implementation MAY ensure that an EAT cannot be reused after it has been consumed. This is achieved by marking the EAT as consumed in the `_consumeAccessToken` function. ### Off-Chain Issuance: + The security of the off-chain service issuing EATs is critical since the security of EAT-gated functions depends on it. If this service is compromised, malicious actors could be granted EATs giving them access to on-chain resources that they should not have access to. ### Expiry Time Considerations: + The expiry time of the EAT must be set judiciously to balance usability and security. If the expiry time is set too long, it might increase the risk of EAT misuse. If it's too short, it might compromise the usability of the application. ## Copyright -Copyright and related rights waived via [CC0](../LICENSE.md). \ No newline at end of file +Copyright and related rights waived via [CC0](../LICENSE.md). From c99b85499c5ee247b449e1d5e0ec109f90f4d087 Mon Sep 17 00:00:00 2001 From: ra-phael <10075759+ra-phael@users.noreply.github.com> Date: Wed, 5 Jul 2023 17:24:04 +0100 Subject: [PATCH 6/8] add url to eth magician thread --- EIPS/eip-7272.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-7272.md b/EIPS/eip-7272.md index a44b9acb650d48..d45dec2c6e285e 100644 --- a/EIPS/eip-7272.md +++ b/EIPS/eip-7272.md @@ -3,7 +3,7 @@ eip: 7272 title: Ethereum Access Token description: A protocol for authorizing function calls from an off-chain service author: Chris Chung (@0xpApaSmURf), Raphael Roullet (@ra-phael) -discussions-to: +discussions-to: https://ethereum-magicians.org/t/eip-7272-ethereum-access-token/14945 status: Draft type: Standards Track category: ERC From ed7863399c67e1af08f2b6a6935856295e733bbc Mon Sep 17 00:00:00 2001 From: ra-phael <10075759+ra-phael@users.noreply.github.com> Date: Sun, 9 Jul 2023 13:04:53 +0100 Subject: [PATCH 7/8] add diagram --- EIPS/eip-7272.md | 2 +- assets/eip-7272/EAT_transaction_auth_flow.jpeg | Bin 0 -> 76499 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 assets/eip-7272/EAT_transaction_auth_flow.jpeg diff --git a/EIPS/eip-7272.md b/EIPS/eip-7272.md index d45dec2c6e285e..fe6a2cff14b771 100644 --- a/EIPS/eip-7272.md +++ b/EIPS/eip-7272.md @@ -41,7 +41,7 @@ An example flow integrated in a DeFi application is the following: 3. The user then interacts with the gated smart contract function within the specified period of time passing the EAT as part of the transaction 4. The EAT is verified on-chain -TODO: Add chart from assets +![Transaction authorization flow using an EAT](../assets/eip-7272/EAT_transaction_auth_flow.jpeg) An Ethereum Access Token MUST guarantee granular access control by binding it to specific parameters upon issuance. Then, on-chain EAT verification ensures that: diff --git a/assets/eip-7272/EAT_transaction_auth_flow.jpeg b/assets/eip-7272/EAT_transaction_auth_flow.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..688c9c751c4103582caadfdbb8967d562455e71c GIT binary patch literal 76499 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 Date: Sun, 9 Jul 2023 14:14:08 +0100 Subject: [PATCH 8/8] add reference implementation --- EIPS/eip-7272.md | 6 +- assets/eip-7272/AccessTokenConsumer.sol | 103 +++++++++++++++++ assets/eip-7272/AccessTokenVerifier.sol | 136 +++++++++++++++++++++++ assets/eip-7272/IAccessTokenVerifier.sol | 30 +++++ 4 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 assets/eip-7272/AccessTokenConsumer.sol create mode 100644 assets/eip-7272/AccessTokenVerifier.sol create mode 100644 assets/eip-7272/IAccessTokenVerifier.sol diff --git a/EIPS/eip-7272.md b/EIPS/eip-7272.md index fe6a2cff14b771..054246efdfe83c 100644 --- a/EIPS/eip-7272.md +++ b/EIPS/eip-7272.md @@ -172,7 +172,11 @@ Any function can be gated with an EAT, apart from the special `receive` and `fal ## Reference Implementation -TODO: add from /assets +Here's a reference implementation of the different smart contracts making up the EAT system onchain: + +- [IAccessTokenVerifier.sol](../assets//eip-7272/IAccessTokenVerifier.sol) +- [AccessTokenVerifier.sol](../assets/eip-7272/AccessTokenVerifier.sol) +- [AccessTokenConsumer.sol](../assets/eip-7272/AccessTokenConsumer.sol) ## Security Considerations diff --git a/assets/eip-7272/AccessTokenConsumer.sol b/assets/eip-7272/AccessTokenConsumer.sol new file mode 100644 index 00000000000000..8167294d92ad8f --- /dev/null +++ b/assets/eip-7272/AccessTokenConsumer.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.13; + +import "./IAccessTokenVerifier.sol"; + +contract AccessTokenConsumer { + IAccessTokenVerifier private immutable _verifier; + mapping(bytes32 => bool) private _accessTokenUsed; + + constructor(address accessTokenVerifier) { + _verifier = IAccessTokenVerifier(accessTokenVerifier); + } + + modifier requiresAuth( + uint8 v, + bytes32 r, + bytes32 s, + uint256 expiry + ) { + // VF -> Verification Failure + require(verify(v, r, s, expiry), "AccessToken: VF"); + _consumeAccessToken(v, r, s, expiry); + _; + } + + function verify( + uint8 v, + bytes32 r, + bytes32 s, + uint256 expiry + ) internal view returns (bool) { + // AU -> Already Used + require(!_isAccessTokenUsed(v, r, s, expiry), "AccessToken: AU"); + + AccessToken memory token = constructToken(expiry); + return _verifier.verify(token, v, r, s); + } + + function constructToken(uint256 expiry) internal view returns (AccessToken memory token) { + FunctionCall memory functionCall; + functionCall.functionSignature = msg.sig; + functionCall.target = address(this); + functionCall.caller = msg.sender; + + functionCall.parameters = extractInputs(); + token.functionCall = functionCall; + token.expiry = expiry; + } + + // Takes calldata and extracts non-signature, non-expiry function inputs as a byte array + // Removes all references to the proof object except any offsets related to + // other inputs that are pushed by the proof + function extractInputs() public pure returns (bytes memory inputs) { + // solhint-disable-next-line no-inline-assembly + assembly { + // Allocate memory from free memory pointer + let ptr := mload(0x40) + // Copy calldata to memory + calldatacopy(ptr, 0, calldatasize()) + // Update free memory pointer to point to the end of the copied calldata + mstore(0x40, add(ptr, calldatasize())) + + // Set starting position after the first 4 bytes (function signature) + let startPos := 0x04 + // Add 128 bytes to the starting position to calculate the end position of the EAT params + // since each EAT param (v,r,s,expiry) takes 32 bytes + let endOfSigExp := add(startPos, 0x80) + // Compute the size of the remaining function parameters (end of calldata - end position of EAT params) + let totalInputSize := sub(calldatasize(), endOfSigExp) + + // Overwrite inputs pointer to free memory pointer + inputs := ptr + + // Store expected length of total byte array as first value + mstore(inputs, totalInputSize) + + // Copy bytes from end of signature and expiry section to end of calldata, + // right after the 32 bytes (0x20) reserved for totalInputSize + calldatacopy(add(inputs, 0x20), endOfSigExp, totalInputSize) + } + } + + function _isAccessTokenUsed( + uint8 v, + bytes32 r, + bytes32 s, + uint256 expiry + ) internal view returns (bool) { + bytes32 accessTokenHash = keccak256(abi.encodePacked(v, r, s, expiry)); + return _accessTokenUsed[accessTokenHash]; + } + + function _consumeAccessToken( + uint8 v, + bytes32 r, + bytes32 s, + uint256 expiry + ) private { + bytes32 accessTokenHash = keccak256(abi.encodePacked(v, r, s, expiry)); + + _accessTokenUsed[accessTokenHash] = true; + } +} diff --git a/assets/eip-7272/AccessTokenVerifier.sol b/assets/eip-7272/AccessTokenVerifier.sol new file mode 100644 index 00000000000000..1c69da594f8789 --- /dev/null +++ b/assets/eip-7272/AccessTokenVerifier.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.13; + +import "./IAccessTokenVerifier.sol"; + +contract AccessTokenVerifier is IAccessTokenVerifier { + bytes32 private constant EIP712DOMAIN_TYPEHASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + // solhint-disable max-line-length + bytes32 private constant FUNCTIONCALL_TYPEHASH = + keccak256("FunctionCall(bytes4 functionSignature,address target,address caller,bytes parameters)"); + + // solhint-disable max-line-length + bytes32 private constant TOKEN_TYPEHASH = + keccak256( + "AccessToken(uint256 expiry,FunctionCall functionCall)FunctionCall(bytes4 functionSignature,address target,address caller,bytes parameters)" + ); + + // Cache the domain separator as an immutable value, but also store the chain id that it corresponds to, in order to + // invalidate the cached domain separator if the chain id changes. + bytes32 private immutable _cachedDomainSeparator; + uint256 private immutable _cachedChainId; + address private immutable _cachedThis; + + constructor(address root) { + _cachedChainId = block.chainid; + _cachedDomainSeparator = _buildDomainSeparator(); + _cachedThis = address(this); + } + + function hash(EIP712Domain memory eip712Domain) internal pure returns (bytes32) { + return + keccak256( + abi.encode( + EIP712DOMAIN_TYPEHASH, + keccak256(bytes(eip712Domain.name)), + keccak256(bytes(eip712Domain.version)), + eip712Domain.chainId, + eip712Domain.verifyingContract + ) + ); + } + + function hash(FunctionCall calldata call) internal pure returns (bytes32) { + return + keccak256( + abi.encode( + FUNCTIONCALL_TYPEHASH, + call.functionSignature, + call.target, + call.caller, + keccak256(call.parameters) + ) + ); + } + + function hash(AccessToken calldata token) internal pure returns (bytes32) { + return keccak256(abi.encode(TOKEN_TYPEHASH, token.expiry, hash(token.functionCall))); + } + + function verifySignerOf( + AccessToken calldata token, + uint8 v, + bytes32 r, + bytes32 s + ) external view returns (address) { + return _retrieveSignerFromToken(token, v, r, s); + } + + function verify( + AccessToken calldata token, + uint8 v, + bytes32 r, + bytes32 s + ) public view override returns (bool) { + address signer = _retrieveSignerFromToken(token, v, r, s); + + // Verifies that the signer recovered from the token is a registered, active, expected + // issuer. How to register and manage signers onchain is likely out of scope in the context + // of EIP-7272. + return _isActiveIssuer[signer]; + } + + function _domainSeparator() internal view returns (bytes32) { + if (address(this) == _cachedThis && block.chainid == _cachedChainId) { + return _cachedDomainSeparator; + } else { + return _buildDomainSeparator(); + } + } + + function _buildDomainSeparator() private view returns (bytes32) { + return + hash( + EIP712Domain({ + name: "Ethereum Access Token", + version: "1", + chainId: block.chainid, + verifyingContract: address(this) + }) + ); + } + + function _retrieveSignerFromToken( + AccessToken calldata token, + uint8 v, + bytes32 r, + bytes32 s + ) internal view returns (address) { + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", _domainSeparator(), hash(token))); + + // HE -> Has Expired + require(token.expiry > block.timestamp, "AccessToken: HE"); + + if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { + // ISS -> Invalid Signature S + revert("AccessToken: ISS"); + } + + if (v != 27 && v != 28) { + // ISV -> Invalid Signature V + revert("AccessToken: ISV"); + } + + // If the signature is valid (and not malleable), return the signer address + address signer = ecrecover(digest, v, r, s); + + if (signer == address(0)) { + // IS -> Invalid Signature + revert("AccessToken: IS"); + } + + return signer; + } +} diff --git a/assets/eip-7272/IAccessTokenVerifier.sol b/assets/eip-7272/IAccessTokenVerifier.sol new file mode 100644 index 00000000000000..bc8aa7294601bb --- /dev/null +++ b/assets/eip-7272/IAccessTokenVerifier.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.13; + +struct EIP712Domain { + string name; + string version; + uint256 chainId; + address verifyingContract; +} + +struct FunctionCall { + bytes4 functionSignature; + address target; + address caller; + bytes parameters; +} + +struct AccessToken { + uint256 expiry; + FunctionCall functionCall; +} + +interface IAccessTokenVerifier { + function verify( + AccessToken calldata token, + uint8 v, + bytes32 r, + bytes32 s + ) external view returns (bool); +}