diff --git a/ERCS/erc-7521.md b/ERCS/erc-7521.md index d2904ac86f..0b2821e4d8 100644 --- a/ERCS/erc-7521.md +++ b/ERCS/erc-7521.md @@ -12,7 +12,7 @@ created: 2023-09-19 ## Abstract -A generalized intent specification entry point contract which enables support for a multitude of intent standards as they evolve over time. Instead of smart contract wallets having to constantly upgrade to provide support for new intent standards as they pop up, a single entry point contract is trusted to handle signature verification which then passes off the low level intent data handling and defining to other contracts specified by users at intent sign time. These signed messages, called a `UserIntent`, are gossipped around any host of mempool strategies for MEV searchers to look through and combine with their own `UserIntent` into an object called an `IntentSolution`. MEV searchers then package up an `IntentSolution` object they build into a transaction making a `handleIntents` call to a special contract. This transaction then goes through the typical MEV channels to eventually be included in a block. +A generalized intent specification entry point contract which enables support for a multitude of intent standards as they evolve over time. Instead of smart contract wallets having to constantly upgrade to provide support for new intent standards as they pop up, a single entry point contract is trusted to handle signature verification which then passes off the low level intent data handling and defining to other contracts specified by users at intent sign time. These signed messages, called a `UserIntent`, are gossipped around any host of mempool strategies for MEV searchers to look through and combine with their own `UserIntent` into an object called an `IntentSolution`. MEV searchers then package up an `IntentSolution` object they build into a transaction making a `handleIntents` call to the special contract entry point contract. This transaction then goes through the typical MEV channels to eventually be included in a block. ## Motivation @@ -34,13 +34,13 @@ This proposal uses the same entry point contract idea to enable a single interfa Users package up intents they want their wallet to participate in, in an ABI-encoded struct called a `UserIntent`: -| Field | Type | Description | -| ------------ | --------- | ------------------------------------------------------------------------------------ | -| `sender` | `address` | The wallet making the intent | -| `intentData` | `bytes[]` | Data defined by the intent standard broken down into multiple segments for execution | -| `signature` | `bytes` | Data passed into the wallet along with the nonce during the verification step | +| Field | Type | Description | +| ------------ | --------- | ------------------------------------------------------------- | +| `sender` | `address` | The wallet making the intent | +| `segments` | `bytes[]` | Data defined by multiple segments of varying intent standards | +| `signature` | `bytes` | Data passed into the wallet during the verification step | -The `intentData` parameter is an array of arbitrary bytes whose use is defined by an intent standard. Each item in this array is referred to as an **intent segment**. The first 32 bytes of each segment is used to specify the **intent standard ID** to which the segment data belongs. Users send `UserIntent` objects to any mempool strategy that works best for the intent standards being used. A specialized class of MEV searchers called **solvers** look for these intents and ways that they can be combined with other intents (including their own) to create an ABI-encoded struct called an `IntentSolution`: +The `segments` parameter is an array of arbitrary bytes whose use is defined by an intent standard. Each item in this array is referred to as an **intent segment**. The first 32 bytes of each segment is used to specify the **intent standard ID** to which the segment data belongs. Users send `UserIntent` objects to any mempool strategy that works best for the intent standards being used. A specialized class of MEV searchers called **solvers** look for these intents and ways that they can be combined with other intents (including their own) to create an ABI-encoded struct called an `IntentSolution`: | Field | Type | Description | | ----------- | -------------- | --------------------------------------------- | @@ -59,26 +59,26 @@ function handleIntents function validateIntent (UserIntent calldata intent) - external view; + external; function registerIntentStandard (IIntentStandard intentStandard) external returns (bytes32); -function verifyExecutingIntentForStandard +function verifyExecutingIntentSegmentForStandard (IIntentStandard intentStandard) - external returns (bool); + external view returns (bool); ``` The core interface required for an intent standard to have is: ```solidity -function validateUserIntent - (UserIntent calldata intent) - external; +function validateIntentSegment + (bytes calldata segmentData) + external pure; -function executeUserIntent - (IntentSolution calldata solution, uint256 executionIndex, uint256 segmentIndex, bytes memory context) +function executeIntentSegment + (IntentSolution calldata solution, uint256 executionIndex, uint256 segmentIndex, bytes calldata context) external returns (bytes memory); ``` @@ -87,11 +87,11 @@ The core interface required for a wallet to have is: ```solidity function validateUserIntent (UserIntent calldata intent, bytes32 intentHash) - external view returns (address); + external; function generalizedIntentDelegateCall (bytes memory data) - external returns (bool); + external; ``` ### Required entry point contract functionality @@ -103,40 +103,38 @@ In the verification loop, the `handleIntents` call must perform the following st - **Validate `timestamp` value on the `IntentSolution`** by making sure it is within an acceptable range of `block.timestamp` or some time before it. - **Call `validateUserIntent` on the wallet**, passing in the `UserIntent` and the hash of the intent. The wallet should verify the intent's signature. If any `validateUserIntent` call fails, `handleIntents` must skip execution of at least that intent, and may revert entirely. -In the execution loop, the `handleIntents` call must perform the following steps for all **segments** on the `intentData` bytes array parameter on each `UserIntent`: +In the execution loop, the `handleIntents` call must perform the following steps for all **segments** on the `segments` bytes array parameter on each `UserIntent`: -- **Call `executeUserIntent` on the intent standard**, specified by the first 32 bytes of the `intentData` (the intent standard ID). This call passes in the entire `IntentSolution` as well as the current `executionIndex` (the number of times this function has already been called for any standard or intent before this), `segmentIndex` (index in the `intentData` array to execute for) and `context` data. The `executeUserIntent` function returns arbitrary bytes per intent which must be remembered and passed into the next `executeUserIntent` call for the same intent. +- **Call `executeIntentSegment` on the intent standard**, specified by the first 32 bytes of the `segments` (the intent standard ID). This call passes in the entire `IntentSolution` as well as the current `executionIndex` (the number of times this function has already been called for any standard or intent before this), `segmentIndex` (index in the `segments` array to execute for) and `context` data. The `executeIntentSegment` function returns arbitrary bytes per intent which must be remembered and passed into the next `executeIntentSegment` call for the same intent. -It's up to the intent standard to choose how to parse the `intentData` segment bytes and utilize the `context` data blob that persists across intent execution. +It's up to the intent standard to choose how to parse the `segments` bytes and utilize the `context` data blob that persists across intent execution. -The order of execution for `UserIntent` segments in the `intentData` array always follows the same order defined on the `intentData` parameter. However, the order of execution for segments between `UserIntent` objects can be specified by the `order` parameter of the `IntentSolution` object. For example, an `order` array of `[1,1,0,1]` would result in the second intent being executed twice (segments 1 and 2 on intent 2), then the first intent would be executed (segment 1 on intent 1), followed by the second intent being executed a third time (segment 3 on intent 2). If no ordering is specified in the solution, or all segments have not been processed for all intents after getting to the end of the order array, a default ordering will be used. This default ordering loops from the first intent to the last as many times as necessary until all intents have had all their segments executed. If the ordering calls for an intent to be executed after it's already been executed for all its segments, then the `executeUserIntent` call is simply skipped and execution across all intents continues. +The order of execution for `UserIntent` segments in the `segments` array always follows the same order defined on the `segments` parameter. However, the order of execution for segments between `UserIntent` objects can be specified by the `order` parameter of the `IntentSolution` object. For example, an `order` array of `[1,1,0,1]` would result in the second intent being executed twice (segments 1 and 2 on intent 2), then the first intent would be executed (segment 1 on intent 1), followed by the second intent being executed a third time (segment 3 on intent 2). If no ordering is specified in the solution, or all segments have not been processed for all intents after getting to the end of the order array, a default ordering will be used. This default ordering loops from the first intent to the last as many times as necessary until all intents have had all their segments executed. If the ordering calls for an intent to be executed after it's already been executed for all its segments, then the `executeIntentSegment` call is simply skipped and execution across all intents continues. Before accepting a `UserIntent`, solvers must use an RPC method to locally call the `validateIntent` function of the entry point, which verifies that the signature and data formatting is correct; see the [Intent validation section below](#solver-intent-validation) for details. #### Registering new entry point intent standards -The entry point's `registerIntentStandard` function must allow for permissionless registration of new intent standard contracts. During the registration process, the entry point contract must verify the contract is meant to be registered by calling the `isIntentStandardForEntryPoint` function on the intent standard contract. This function passes in the entry point contract address which the intent standard can then verify and return true or false. If the intent standard contract returns true, then the entry point registers it and gives it a **standard ID** which is unique to the intent standard contract, entry point contract and chain ID. +The entry point's `registerIntentStandard` function must allow for permissionless registration of new intent standard contracts. During the registration process, the entry point gives it a **standard ID** which is unique to the intent standard contract, entry point contract and chain ID. ### Intent standard behavior executing an intent -The intent standard's `executeUserIntent` function is given access to a wide set of data, including the entire `IntentSolution` in order to allow it to be able to implement any kind of logic that may be seen as useful in the future. Each intent standard contract is expected to parse the `UserIntent` objects `intentData` parameter and use that to validate any constraints or perform any actions relevant to the standard. Intent standards can also take advantage of the `context` data it can return at the end of the `executeUserIntent` function. This data is kept by the entry point and passed in as a parameter to the `executeUserIntent` function the next time it is called for an event. This gives intent standards access to a persistent data store as other intents are executed in between others. One example of a use case for this is an intent standard that is looking for a change in state during intent execution (like releasing tokens and expecting to be given other tokens). +The intent standard's `executeIntentSegment` function is given access to a wide set of data, including the entire `IntentSolution` in order to allow it to implement any kind of logic that may be seen as useful in the future. Each intent standard contract is expected to parse the `UserIntent` objects `segments` parameter and use that to validate any constraints or perform any actions relevant to the standard. Intent standards can also take advantage of the `context` data it can return at the end of the `executeIntentSegment` function. This data is kept by the entry point and passed in as a parameter to the `executeIntentSegment` function the next time it is called for an intent. This gives intent standards access to a persistent data store as other intents are executed in between others. One use case for this is an intent standard that is looking for a change in state during intent execution (like releasing tokens and expecting to be given other tokens). ### Smart contract wallet behavior executing an intent -The entry point does not expect anything from the smart contract wallets after validation and during intent execution. However, intent standards may wish for the smart contract wallet to perform some action during execution. The smart contract wallet `generalizedIntentDelegateCall` function must perform a delegate call with the given calldata at the calling intent standard. In order for the wallet to trust making the delegate call it must call the `verifyExecutingIntentForStandard` function on the entry point contract to verify both of the following: +The entry point does not expect anything from the smart contract wallets after validation and during intent execution. However, intent standards may wish for the smart contract wallet to perform some action during execution. The smart contract wallet `generalizedIntentDelegateCall` function must perform a delegate call with the given calldata at the calling intent standard. In order for the wallet to trust making the delegate call it must call the `verifyExecutingIntentSegmentForStandard` function on the entry point contract to verify both of the following: -- The `msg.sender` for `generalizedIntentDelegateCall` on the wallet is the intent standard contract that the entry point is currently calling `executeUserIntent` on. -- The smart contract wallet is the `sender` on the `UserIntent` that the entry point is currently calling `executeUserIntent` for. +- The `msg.sender` for `generalizedIntentDelegateCall` on the wallet is the intent standard contract that the entry point is currently calling `executeIntentSegment` on. +- The smart contract wallet is the `sender` on the `UserIntent` that the entry point is currently calling `executeIntentSegment` for. ### Smart contract wallet behavior validating an intent -The entry point calls `validateUserIntent` for each intent on the smart contract wallet specified in the `sender` field of each `UserIntent`. This function provides the entire `UserIntent` object as well as the precomputed hash of the intent. The smart contract wallet is then expected to analyze this data to ensure it was actually sent from the specified `sender`. If the intent is not valid, the smart contract wallet should throw an error in the `validateUserIntent` function. It should be noted that `validateUserIntent` is restricted to `view` only. Any kind of updates to state for things like nonce management, should be done in an individual segment on the intent itself. This allows for maximum customization in the way users define their intents while enshrining only the minimum verification within the entry point needed to ensure intents cannot be forged. - -The function `validateUserIntent` also has an optional `address` return value for the smart contract wallet to return if the validation failed but could have been validated by a signature aggregation contract earlier. In this case, the smart contract wallet would return the address of the trusted signature aggregation smart contract; see the [Extension: signature aggregation](#extension-signature-aggregation) section below for details. If there were no issues during validation, the smart contract wallet should just return `address(0)`. +The entry point calls `validateUserIntent` for each intent on the smart contract wallet specified in the `sender` field of each `UserIntent`. This function provides the entire `UserIntent` object as well as the precomputed hash of the intent. The smart contract wallet is then expected to analyze this data to ensure it was actually sent from the specified `sender`. If the intent is not valid, the smart contract wallet should throw an error in the `validateUserIntent` function. It should be noted that although `validateUserIntent` is not restricted as `view`, updates to state for things like nonce management, should be done in an individual segment on the intent itself. This allows for maximum customization in the way users define their intents while enshrining only the minimum verification within the entry point needed to ensure intents cannot be forged. ### Solver intent validation -To validate a `UserIntent`, the solver makes a view call to `validateIntent(intent)` on the entry point. This function checks that the signature passes validation and that the segments on the intent are properly formatted. If the call reverts with any error, the solver should reject the `UserIntent`. +To validate a `UserIntent`, the solver makes a view call to `validateIntent` on the entry point. This function checks that the signature passes validation and that the segments on the intent are properly formatted. If the call reverts with any error, the solver should reject the `UserIntent`. ### Simulation @@ -146,39 +144,9 @@ Solvers are expected to handle simulation in typical MEV workflows. This most li The entry point contract may enable additional functionality to reduce gas costs for common scenarios. -#### Extension: signature aggregation - -We add the additional function `handleIntentsAggregated` to the entry point contract that allows an aggregated signature to be provided in place of verifying signatures for intents individually. Additionally, we introduce a new interface for a contract acting as the **signature aggregator** that handles all logic for aggregated signature verification. - -The core interface required for the entry point to have is: - -```solidity -handleIntentsAggregated( - IntentSolution[] calldata solutions, - IAggregator aggregator, - bytes32 intentsToAggregate, - bytes calldata signature - ) external; -``` - -The `handleIntentsAggregated` function takes in a list of solutions, the address of the aggregation contract, a bitfield indicating which intents the aggregate signature represents (1 for included, 0 for excluded) and lastly, the aggregated signature itself. The entry point contract will call to the aggregator contract to verify the aggregated signature for the involved intents. Then, during normal validation, the entry point contract verifies that the smart contract wallets that sent the intents in the aggregated signature all return the address of the signature aggregator contract that was used; see the [Smart contract wallet behavior validating an intent](#smart-contract-wallet-behavior-validating-an-intent) section above. - -The core interface required for an aggregator to have is: - -```solidity -function validateSignatures - (UserIntent[] calldata intents, bytes calldata signature) - external view; - -function aggregateSignatures - (UserIntent[] calldata intents) - external view returns (bytes memory aggregatedSignature); -``` -The `validateSignatures` function serves as the main function for the entry point contract to call to verify an aggregated signature. The `aggregateSignatures` function can be used by solvers off-chain to calculate the aggregated signature if they do not already have optimized custom code to perform the aggregation. - #### Extension: embedded intent standards -We extend the entry point logic to include the logic of several identified **common intent standards**. These standards are registered with their own standard ID at entry point contract creation time. The functions `validateUserIntent` and `executeUserIntent` for these standards are included as part of the entry point contracts code in order to reduce external calls and save gas. +We extend the entry point logic to include the logic of several identified **common intent standards**. These standards are registered with their own standard ID at entry point contract creation time. The functions `validateUserIntent` and `executeIntentSegment` for these standards are included as part of the entry point contracts code in order to reduce external calls and save gas. #### Extension: handle multi @@ -190,13 +158,13 @@ We add the functions `getNonce(address sender, uint256 key)` and `setNonce(uint2 #### Extension: data blobs -We enable the entry point contract to skip the validation of `UserIntent` objects with either a `sender` field of `address(0)` or an empty `intentData` field (rather than fail validation). Similarly, they are skipped during execution. The `intentData` field or `sender` field is then free to be treated as a way to inject any arbitrary data into intent execution. This data could be useful in solving an intent that has an intent standard which requires some secret to be known and proven to it, or an intent whose behavior can change according to what other intents are around it. For example, an intent standard that signals a smart contract wallet to transfer some tokens to the sender of the intent that is next in line for the execution process. +We enable the entry point contract to skip the validation of `UserIntent` objects with either a `sender` field of `address(0)` or an empty `segments` field (rather than fail validation). Similarly, they are skipped during execution. The `segments` field or `sender` field is then free to be treated as a way to inject any arbitrary data into intent execution. This data could be useful in solving an intent that has an intent standard which requires some secret to be known and proven to it, or an intent whose behavior can change according to what other intents are around it. For example, an intent standard that signals a smart contract wallet to transfer some tokens to the sender of the intent that is next in line for the execution process. ## Rationale The main challenge with a generalized intent standard is being able to adapt to the evolving world of intents. Users need to have a way to express their intents in a seamless way without having to make constant updates to their smart contract wallets. -In this proposal, we expect wallets to have a `validateUserIntent` function that takes as input a `UserIntent`, and verifies the signature. A trusted entry point contract uses this function to validate the signature and forwards the intent handling logic to the intent standard contracts specified in the first 32 bytes of each segment in the `intentData` array field on the `UserIntent`. The wallet is then expected to have a `generalizedIntentDelegateCall` function that allows it to perform intent related actions from the intent standard contracts, using the `verifyExecutingIntentForStandard` function on the entry point for security. +In this proposal, we expect wallets to have a `validateUserIntent` function that takes as input a `UserIntent`, and verifies the signature. A trusted entry point contract uses this function to validate the signature and forwards the intent handling logic to the intent standard contracts specified in the first 32 bytes of each segment in the `segments` array field on the `UserIntent`. The wallet is then expected to have a `generalizedIntentDelegateCall` function that allows it to perform intent related actions from the intent standard contracts, using the `verifyExecutingIntentSegmentForStandard` function on the entry point for security. The entry point based approach allows for a clean separation between verification and intent execution, and prevents wallets from having to constantly update to support the latest intent standard composition that a user wants to use. The alternative would involve developers of new intent standards having to convince wallet software developers to support their new intent standards. This proposal moves the core definition of an intent into the hands of users at signing time. @@ -210,25 +178,31 @@ Solvers will rely on gossiping networks and solution algorithms that are to be d Wallets are encouraged to be DELEGATECALL forwarding contracts for gas efficiency and to allow wallet upgradability. The wallet code is expected to hard-code the entry point into their code for gas efficiency. If a new entry point is introduced, whether to add new functionality, improve gas efficiency, or fix a critical security bug, users can self-call to replace their wallet's code address with a new code address containing code that points to a new entry point. During an upgrade process, it's expected that intent standard contracts will also have to be re-registered to the new entry point. +Another option would be for wallets to not hard-code the entry point and instead validate signatures from any entry point. When a signature is validated, the wallet can note the entry point in transient storage and then use that to ensure security when accepting `generalizedIntentDelegateCall` function calls. There is an example of this in the [reference implementation](#reference-implementation). + #### Intent standard upgrading Because intent standards are not hardcoded into the wallet, users do not need to perform any operation to use any newly registered intent standards. A user can simply sign an intent with the new intent standard. +### Signature aggregation + +Signature aggregation should be handled by the smart contract wallets directly during the signature validation process. This removes complexity from the entry point and allows developers to be creative with solutions. The [reference implementation](#reference-implementation) includes an example for how to accomplish this through the use of a wallet trusted aggregation contract which uses transient storage to report back to individual wallets that an intent was already validated via an aggregated signature earlier in the transaction call stack. + ## Backwards Compatibility This ERC does not change the consensus layer, so there are no backwards compatibility issues for Ethereum as a whole. There is a little more difficulty when trying to integrate with existing smart contract wallets. If the wallet already has support for [ERC-4337](./eip-4337.md), then implementing a `validateUserIntent` function should be very similar to the `validateUserOp` function, but would require an upgrade by the user. ## Reference Implementation -See `https://github.com/essential-contributions/ERC-7521` +See `https://github.com/essential-contributions/ERC7521` ## Security Considerations -The entry point contract will need to be very heavily audited and formally verified, because it will serve as a central trust point for _all_ [ERC-7521](./eip-7521.md) supporting wallets. In total, this architecture reduces auditing and formal verification load for the ecosystem, because the amount of work that individual _wallets_ have to do becomes much smaller (they need only verify the `validateUserIntent` function and its "check signature" logic) and gate any calls to `generalizedIntentDelegateCall` by checking with the entry point using the `verifyExecutingIntentForStandard` function. The concentrated security risk in the entry point contract, however, needs to be verified to be very robust since it is so highly concentrated. +The entry point contract will need to be very heavily audited and formally verified, because it will serve as a central trust point for _all_ [ERC-7521](./eip-7521.md) supporting wallets. In total, this architecture reduces auditing and formal verification load for the ecosystem, because the amount of work that individual _wallets_ have to do becomes much smaller (they need only verify the `validateUserIntent` function and its "check signature" logic) and gate any calls to `generalizedIntentDelegateCall` by checking with the entry point using the `verifyExecutingIntentSegmentForStandard` function. The concentrated security risk in the entry point contract, however, needs to be verified to be very robust since it is so highly concentrated. Verification would need to cover one primary claim (not including claims needed to protect solvers, and intent standard related infrastructure): -- **Safety against arbitrary hijacking**: The entry point only returns true for `verifyExecutingIntentForStandard` when it has successfully validated the signature of the `UserIntent` and is currently in the middle of calling `executeUserIntent` on the `standard` specified in the `intentData` field of a `UserIntent` which also has the same `sender` as the `msg.sender` wallet calling the function. +- **Safety against arbitrary hijacking**: The entry point only returns true for `verifyExecutingIntentSegmentForStandard` when it has successfully validated the signature of the `UserIntent` and is currently in the middle of calling `executeIntentSegment` on the `standard` specified in the `segments` field of a `UserIntent` which also has the same `sender` as the `msg.sender` wallet calling the function. Additional heavy auditing and formal verification will also need to be done for any intent standard contracts a user decides to interact with.