This proposal defines a standard interface for intent-centric smart accounts. It enables externally owned accounts (EOAs) to delegate contract code to a smart account implementation, allowing them to sign intents. These intents can then be executed by solvers (or relayers) on behalf of the account owner, streamlining interactions and expanding the capabilities of EOAs.
Motivation
Account Abstraction (AA) is a highly discussed topic in the blockchain industry, as it enhances the programmability of accounts, enabling features such as:
Batch Execution
Gas Sponsorship
Access Control
The introduction of ERC-4337 established a permissionless standard for AA, unlocking a wide range of powerful features. However, ERC-4337 has several limitations:
Complexity: The standard requires multiple interdependent components, including the Account, EntryPoint, Paymaster, Bundler, and additional plugins (ERC-6900, ERC-7579. Running a bundler demands significant engineering expertise and introduces operational overhead.
Compatibility: Component dependencies make upgrades cumbersome, often requiring multiple smart contracts to be updated simultaneously. This creates fragmentation within the ecosystem.
one version update, also divides the ecosystem.
Cost: Processing UserOperation transactions consumes a high amount of gas.
Trust Assumption: Despite being designed as a permissionless standard, ERC-4337 still relies on centralized entities. Paymasters, for instance, are typically centralized, as they must either trust account owners to reimburse gas costs or manage external funding sources. Similarly, bundlers operate within a miner extractable value (MEV) environment, requiring users to trust them for transaction inclusion.
ERC-7521 introduced a smart contract account (SCA) solution with an intent-centric design. It allows solvers to fulfill account owners’ intents while maintaining flexibility for custom execution logic and ensuring forward compatibility.
With the introduction of SET_CODE_TX_TYPE=0x04, EOAs can now set contract code dynamically, granting them programmability similar to SCAs. This presents an opportunity to develop a new standard that extends AA capabilities to EOAs while addressing the aforementioned challenges.
By simplifying execution, improving efficiency, and enhancing user experience, this proposal aims to accelerate the adoption of intent-centric account abstraction smart contracts.
Solvers, Relayers, Paymasters, and Bundlers—All in One
In an intent-centric system, solvers play a crucial role in fulfilling user intents and are rewarded accordingly. This proposal introduces an open execution model, where any solver can participate, fostering a competitive environment that benefits users.
With integrated gas abstraction, solvers can cover gas fees using native tokens while receiving other tokens from the EOA account as compensation. Additionally, solvers can further optimize costs by bundling multiple intent executions into a single blockchain transaction.
Each solver is free to develop its own strategies for maximizing profitability. This proposal does not impose restrictions on how solvers execute intents, ensuring flexibility and adaptability in diverse execution scenarios.
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.
UserIntent schema
Each intent is a packed data structure containing sufficient information about the operations the account owner wants to execute. The core structure of a UserIntent object is as follows:
Field
Type
Description
sender
address
The address of the account initiating the intent.
standard
address
The IStandard implementation responsible for validating and parsing the UserIntent
header
bytes
Metadata associated with the UserIntent, interpreted by standard. Stored as bytes for flexibility.
instructions
bytes[]
The execution details of the UserIntent, interpreted by standard. Stored as bytes[] to allow flexibility.
signatures
bytes[]
Validatable signatures required for execution, interpreted by standard.
Fields Explanations
header: The bytes header can carry information about how to validate the intent or how to prevent
double-spending. For example, header can contain an uint256 nonce to check if the nonce is used already.
instructions: These bytes instructions can just be concatenated (address,value,calldata) or can be
standardized values, for example (erc20TokenAddress,1000) means the instructions can use up to 1000 of the
specified ERC-20 token. It is NOT REQUIRED that all instructions MUST be provided by the EOA owner to allow dynamically carry out other operations during intent executions, but the IStandard design needs to carefully handle this case.
signatures: The bytes signatures field can support different signing methods. It is NOT REQUIRED that
all signatures MUST be provided by the EOA owner, some of them MAY be provided by solver, relayer or anyone else.
Pack UserIntent as Bytes
The UserIntent object is packed and encoded into bytes calldata userIntent. There is no strict schema requirement for the data structure. Each IAccount and IStandard implementation can define its own encoding and decoding methods for handling the bytes data.
Here is an example of packed-encoded format:
Section
Value Type
Description
userIntent[0:20]
address
sender
userIntent[20:40]
address
standard
userIntent[40:42]
uint16
Length of header
userIntent[42:44]
uint16
Length of instructions
userIntent[44:46]
uint16
Length of signatures
Next headerLength bytes
bytes
The actual header data
Next instructionsLength bytes
bytes
The actual instructions data
Next signatureLength bytes
bytes
The actual signatures data
Remaining bytes
bytes
Extra data, such as nested intents for further execution
IStandard Interface
Each standard defines how to parse and validate a UserIntent. Implementations of standard must conform to the IStandard interface:
interfaceIStandard{/**
* Validate user's intent
*
* @dev returning validation result, the type uses bytes4 for extensibility purpose
* @return result values representing validation outcomes
*/functionvalidateUserIntent(bytescalldataintent)externalviewreturns(bytes4result);/**
* Unpack user's intent, it is RECOMMENDED to validate intent while unpacking to save gas
*
* @dev returning unpacked result, the type uses bytes for extensibility purpose
* @return result unpacked result status
* @return operations unpacked operations that can be executed by the IAccount, NOT REQUIRED to match UserIntent.instructions
*/functionunpackOperations(bytescalldataintent)externalviewreturns(bytes4result,bytes[]memoryoperations);}
The IStandard interface is responsible for defining and enforcing the validation logic for UserIntent objects.
It operates similarly to the EntryPoint in ERC-4337 and ERC-7521.
The extensibility of bytes4 return types allows future upgrades without modifying the function signatures.
IAccount Interface
On the account side, IAccount provides the interface for executing bytes calldata intent:
interfaceIAccount{/**
* Execute user's intent
*
* @dev returning execution result, the type uses bytes for extensibility purpose
* @return result values representing execution outcomes
*/functionexecuteUserIntent(bytescalldataintent)externalreturns(bytesmemory);}
Using SET_CODE_TX_TYPE=0x04, EOAs can delegate contract code to an IAccount implementation, enabling them to function as smart accounts. A single account implementation can be shared across multiple EOAs, meaning:
It only needs to be deployed and audited once.
Each EOA owner is responsible for delegating their account to a secure IAccount implementation.
It is RECOMMENDED that each account leverages IStandard to validate and unpack operations, check Reference
Implementation for examples. Account smart contract can be stateless to avoid sharing storage space with other delegated contracts.
Rationale
Usage of Bytes
Defining UserIntent object as a struct would improve readability and make it easier to work with in Solidity. For example:
Mandating all IAccount and IStandard implementations to follow this specific struct format reduces flexibility.
The use of bytes[] introduces additional gas costs due to Solidity’s dynamic array encoding.
Since all objects within the UserIntent structure are optional and their usage depends on IStandard and IAccount implementations, the bytes format ensures maximum flexibility while preserving compatibility.
Execution in EOA Contract Code
With SET_CODE_TX_TYPE=0x04, EOAs gain the ability to execute contract code. Executing transactions directly from an EOA provides several key benefits:
Preserves EOA Control: Execution remains fully controlled by the account owner. If needed, the EOA owner can easily disable all smart contract functionalities by un-delegating the contract code.
Consistent msg.sender Behavior: Since the execution originates from an EOA, msg.sender always resolves to the EOA address, simplifying authentication and permission checks.
Stateless Execution: The execution logic can be designed to be stateless, allowing the IAccount implementation to avoid storing persistent data, reducing storage costs.
If an EOA does not require smart contract execution, or if executing an intent is too expensive, the owner can still use the account as a regular EOA without any modifications.
Validation in the Standard Contract
Validation logic often relies on contract state. For example, a weighted multi-owner signature scheme needs to track the weight assigned to each signer. Keeping intent validation entirely within IStandard offers multiple advantages:
Simplified Implementation: By mirroring the EntryPoint concept from ERC-4337 but in a simpler form, IStandard focuses solely on validation.
Easier Auditing and Maintenance: Since IStandard is responsible only for validation, it becomes easier for contract engineers to implement, audit, and maintain.
Modular Validation: The IStandard interface is inherently modular, allowing for more complex validation mechanisms. For instance, a “compound” standard could decompose an intent into smaller components, validate each separately, and then combine the results.
Gas Abstraction
This design enables gasless transactions by allowing any address to initiate a transaction on behalf of the intent’s sender.
The sender can specify how and what to pay in the intent’s header or instructions.
Payments can be made in any token from the sender’s account.
The transaction cost can be covered by transferring tokens from the sender’s account to tx.origin (the address submitting the transaction).
No re-entry protection enforced
This proposal does not enforce built-in re-entry protection mechanisms such as nonces. The rationale behind this decision is that certain intents are inherently designed to be executed multiple times.
Instead of a global re-entry protection mechanism, each standard should define its own protection rules based on its intended use case. Implementers are encouraged to:
Backwards Compatibility
This IAccount standard shares the same backwards compatibility considerations as the introduction of EOA contract code execution (SET_CODE_TX_TYPE=0x04).
Reference Implementation
Helper Library
This PackedIntent is a library to decode (address sender, address standard, uint16 headerLength, uint16 instructionsLength, uint16 signaturesLength) from a packed encoded intent. The following IAccount and IStandrd implementations both follow PackedIntent schema.
/// @title PackedIntent
/// @notice This is a library that packs metadata of intent (sender, standard, lengths) into bytes
/// @dev the packed intent data schema is defined as follows:
/// @dev 1. sender: address, 20-bytes
/// @dev 2. standard: address, 20-bytes
/// @dev 3. headerLength: uint16, 2-bytes
/// @dev 4. instructionLength: uint16, 2-bytes
/// @dev 5. signatureLength: uint16, 2-bytes
libraryPackedIntent{/// @notice getSenderAndStandard is a function that gets the sender and standard from the intent
/// @param intent The intent to get the sender and standard from
/// @return sender The sender of the intent
/// @return standard The standard of the intent
functiongetSenderAndStandard(bytescalldataintent)externalpurereturns(address,address){require(intent.length>=40,"Intent too short");return(address(bytes20(intent[:20])),address(bytes20(intent[20:40])));}/// @notice getLengths is a function that gets the lengths from the intent
/// @param intent The intent to get the lengths from
/// @return headerLength The length of the header
/// @return instructionLength The length of the instructions
/// @return signatureLength The length of the signature
functiongetLengths(bytescalldataintent)externalpurereturns(uint256,uint256,uint256){require(intent.length>=46,"Missing length section");return(uint256(uint16(bytes2(intent[40:42]))),uint256(uint16(bytes2(intent[42:44]))),uint256(uint16(bytes2(intent[44:46]))));}/// @notice getSignatureLength is a function that gets the signature length from the intent
/// @param intent The intent to get the signature length from
/// @return signatureLength The length of the signature
functiongetSignatureLength(bytescalldataintent)externalpurereturns(uint256){require(intent.length>=46,"Missing length section");returnuint256(uint16(bytes2(intent[44:46])));}/// @notice getIntentLength is a function that gets the intent length from the intent
/// @param intent The intent to get the intent length from
/// @return result The sum of header, instruction and signature lengths
functiongetIntentLength(bytescalldataintent)externalpurereturns(uint256){require(intent.length>=46,"Missing length section");uint256headerLength=uint256(uint16(bytes2(intent[40:42])));uint256instructionLength=uint256(uint16(bytes2(intent[42:44])));uint256signatureLength=uint256(uint16(bytes2(intent[44:46])));returnheaderLength+instructionLength+signatureLength+46;}/// @notice getIntentLengthFromSection is a function that gets the intent length from the length section
/// @param lengthSection The length section to get the intent length from
/// @return result The sum of header, instruction and signature lengths
functiongetIntentLengthFromSection(bytes6lengthSection)externalpurereturns(uint16result){assembly{letvalue:=lengthSectionleta:=shr(240,value)// Extract first 2 bytes
letb:=and(shr(224,value),0xFFFF)// Extract next 2 bytes
letc:=and(shr(208,value),0xFFFF)// Extract last 2 bytes
result:=add(add(add(a,b),c),46)}}}
Relayed Execution Standard
This RelayedExecutionStandard allows relayer to execute the operations on chain and take ERC-20 token from the intent sender, thus achieve a gas-less experience for the sender.
import{MessageHashUtils}import{ECDSA}import{IERC20}import{IStandard}import{IAccount}import{PackedIntent}/// @title ERC7806Constants
/// @notice This is a library that defines the constants for the ERC7806 standard
libraryERC7806Constants{/// @notice VALIDATION_DENIED is the magic value of denied intent
bytes4publicconstantVALIDATION_DENIED=0x00000000;/// @notice VALIDATION_APPROVED is the magic value of validated intent
bytes4publicconstantVALIDATION_APPROVED=0x00000001;}abstractcontractHashGatedStandardisIStandard{eventHashUsed(addresssender,uint256hash);mapping(bytes32=>bool)internal_hashes;functioncheckHash(addresssender,uint256hash)externalviewreturns(bool){bytes32compositeKey=keccak256(abi.encode(sender,hash));return_hashes[compositeKey];}functionmarkHash(uint256hash)external{bytes32compositeKey=keccak256(abi.encode(msg.sender,hash));_hashes[compositeKey]=true;emitHashUsed(msg.sender,hash);}}/*
RelayedExecutionStandard
This standard allows sender to define a list of execution instructions and asks the relayer to execute
on chain on behalf of the sender. It is hash and time gated means the intent can only be executed before
a timestamp and can only be executed once.
The first 20 bytes of the `intent` is sender address.
The next 20 bytes of the `intent` is the standard address, which should be equal to address of this standard.
The following is the length section, containing 3 uint16 defining header length, instructions length and signature length.
The header is either 8 bytes long or 28 bytes long.
The 8-byte part is the timestamp in epoch seconds.
The optional 20-byte defines the assigned relayer address if the sender only wants a specific relayer to execute.
The instructions contains 2 main part.
The first 36 bytes is a packed encoded (address, uint128) pair representing the 'payment' that the sender will pay to the
relayer. It should be an ERC20 token.
The following 1-byte is an uint8 defining the number of instructions to execute.
The instructions are concatenated together, the first 2 bytes (uint16) defines the length of each instruction, the following
is the instruction body. Instructions should be abi.encode(address, uint256, bytes) which can directly be executed by
the sender account.
The signature field is always 65 bytes long. It contains the signed bytes.concat(header, instructions).
*/contractRelayedExecutionStandardisHashGatedStandard{usingECDSAforbytes32;stringpublicconstantICS_NUMBER="ICS1";stringpublicconstantDESCRIPTION="Timed Hashed Relayed Execution Standard";stringpublicconstantVERSION="0.0.0";stringpublicconstantAUTHOR="hellohanchen";functionvalidateUserIntent(bytescalldataintent)externalviewreturns(bytes4){(addresssender,addressstandard)=PackedIntent.getSenderAndStandard(intent);require(standard==address(this),"Not this standard");(uint256headerLength,uint256instructionsLength,uint256signatureLength)=PackedIntent.getLengths(intent);require(headerLength==28||headerLength==8,"Invalid header length");require(instructionsLength>=36,"Instructions too short");require(signatureLength==65,"Invalid signature length");// end of instructions
uint256instructionsEndIndex=46+headerLength+instructionsLength;require(instructionsLength+signatureLength==intent.length,"Invalid intent length");// validate signature
uint256hash=_validateSignatures(sender,intent,instructionsEndIndex);require(!this.checkHash(sender,hash),"Hash is already executed");// header contains expiration timestamp and assigned relayer (optional)
require(uint256(uint64(bytes8(intent[46:54])))>=block.timestamp,"Intent expired");// assignedRelayerAddress = address(intent[54:74]) [optional]
// end of header section / begin of instruction section
uint256headerEndIndex=46+headerLength;// first 20 bytes of instruction is out token address
addressoutTokenAddress=address(bytes20(intent[headerEndIndex:headerEndIndex+20]));// out token amount, use uint128 to shorten the intent
uint256outTokenAmount=uint256(uint128(bytes16(intent[headerEndIndex+20:headerEndIndex+36])));if(outTokenAddress!=address(0)){(boolsuccess,bytesmemorydata)=outTokenAddress.staticcall(abi.encodeWithSelector(IERC20.balanceOf.selector,sender));if(!success||data.length!=32){revert("Not ERC20 token");}require(abi.decode(data,(uint256))>=outTokenAmount,"Insufficient token balance");}else{require(sender.balance>=outTokenAmount,"Insufficient eth balance");}// end of outToken instruction
uint256numExecutions=uint256(uint8(bytes1(intent[headerEndIndex+36:headerEndIndex+37])));// instruction index
uint256instructionIndex=0;// begin of the first instruction
uint256instructionStart;uint256instructionEnd=headerEndIndex+37;while(instructionIndex<numExecutions){instructionStart=instructionEnd;require(instructionStart+2<=instructionsEndIndex,"Intent too short: instruction length");// end of this execution instruction
instructionEnd=instructionStart+2+uint256(uint16(bytes2(intent[instructionStart:instructionStart+2])));require(instructionEnd<=instructionsEndIndex,"Intent too short: single instruction");instructionIndex+=1;}require(instructionEnd==instructionsEndIndex,"Intent length doesn't match");returnERC7806Constants.VALIDATION_APPROVED;}functionunpackOperations(bytescalldataintent)externalviewreturns(bytes4code,bytes[]memoryunpackedInstructions){(addresssender,addressstandard)=PackedIntent.getSenderAndStandard(intent);require(standard==address(this),"Not this standard");(uint256headerLength,uint256instructionsLength,uint256signatureLength)=PackedIntent.getLengths(intent);require(headerLength==28||headerLength==8,"Invalid header length");require(instructionsLength>=36,"Instructions too short");require(signatureLength==65,"Invalid signature length");// end of instructions
uint256instructionsEndIndex=46+headerLength+instructionsLength;require(instructionsLength+signatureLength==intent.length,"Invalid intent length");// fetch header content (timestamp, relayer address [optional])
require(uint256(uint64(bytes8(intent[46:54])))>=block.timestamp,"Intent expired");if(headerLength==28){// assigned relayer
require(tx.origin==address(bytes20(intent[54:74])),"Invalid relayer");}uint256intentHash=_validateSignatures(sender,intent,instructionsEndIndex);require(!this.checkHash(sender,intentHash),"Hash is already executed");// begin of instructions
uint256headerEndIndex=headerLength+46;// total instructions = mark hash + transfer token to relayer + executions
// the first 36 bytes defines the payment to relayer
// the next 1 byte defines the number of execution instructions
unpackedInstructions=newbytes[](2+uint8(bytes1(intent[headerEndIndex+36:headerEndIndex+37])));// first instruction is mark hash to prevent re-entry attack
unpackedInstructions[0]=abi.encode(address(this),0,abi.encodeWithSelector(this.markHash.selector,intentHash));// the first 20 bytes of instructions is the out token address
addressoutTokenAddress=address(bytes20(intent[headerEndIndex:headerEndIndex+20]));// amount
uint256outTokenAmount=uint256(uint128(bytes16(intent[headerEndIndex+20:headerEndIndex+36])));// out token instruction
if(outTokenAddress==address(0)){unpackedInstructions[1]=abi.encode(address(tx.origin),outTokenAmount,"");}else{unpackedInstructions[1]=abi.encode(outTokenAddress,uint256(0),abi.encodeWithSelector(IERC20.transfer.selector,address(tx.origin),outTokenAmount));}// instruction index
uint256instructionIndex=2;uint256instructionEndIndex=headerEndIndex+37;uint256instructionStartIndex;while(instructionIndex<unpackedInstructions.length){// start of next execution instruction
instructionStartIndex=instructionEndIndex;require(instructionStartIndex+2<=instructionEndIndex,"Intent too short: instruction length");// end of next execution instruction
instructionEndIndex=instructionStartIndex+2+uint256(uint16(bytes2(intent[instructionStartIndex:instructionStartIndex+2])));require(instructionEndIndex<=instructionsEndIndex,"Intent too short: single instruction");unpackedInstructions[instructionIndex]=intent[instructionStartIndex+2:instructionEndIndex];instructionIndex+=1;}require(instructionEndIndex==instructionsEndIndex,"Intent length doesn't match");return(ERC7806Constants.VALIDATION_APPROVED,unpackedInstructions);}function_validateSignatures(addresssender,bytescalldataintent,uint256sigStartIndex)internalviewreturns(uint256){bytes32intentHash=keccak256(abi.encode(intent[46:sigStartIndex],address(this),block.chainid));bytes32messageHash=MessageHashUtils.toEthSignedMessageHash(intentHash);require(sender==messageHash.recover(intent[sigStartIndex:sigStartIndex+65]),"Invalid sender signature");returnuint256(intentHash);}// -------------
// The following methods will be removed after testing
// -------------
functionsampleIntent(addresssender,addressrelayer,addressoutTokenAddress,uint128outAmount,bytes[]memoryexecutions)externalviewreturns(bytesmemoryintent,bytes32intentHash){bytesmemoryheader=relayer==address(0)?abi.encodePacked(uint64((block.timestamp+31536000)&0xFFFFFFFFFFFFFFFF)):abi.encodePacked(uint64((block.timestamp+31536000)&0xFFFFFFFFFFFFFFFF),relayer);bytesmemoryinstructions=bytes.concat(bytes20(outTokenAddress),bytes16(outAmount),bytes1(uint8(executions.length)));for(uint256i=0;i<executions.length;i++){uint16length=uint16(executions[i].length);instructions=bytes.concat(instructions,bytes2(length),executions[i]);}bytesmemorytoSign=bytes.concat(header,instructions);intentHash=keccak256(abi.encode(toSign,address(this),block.chainid));intent=bytes.concat(bytes20(sender),bytes20(address(this)),bytes2(uint16(header.length)),bytes2(uint16(instructions.length)),bytes2(uint16(65)),toSign);return(intent,intentHash);}functionsampleERC20Execution(addresstoken,addressreceiver,uint256amount)externalpurereturns(bytesmemory){if(token==address(0)){returnabi.encode(receiver,amount,"");}returnabi.encode(token,uint256(0),abi.encodeWithSelector(IERC20.transfer.selector,address(receiver),amount));}functionexecuteUserIntent(bytescalldataintent)externalreturns(bytesmemory){(addresssender,)=PackedIntent.getSenderAndStandard(intent);bytesmemoryexecuteCallData=abi.encodeWithSelector(IAccount.executeUserIntent.selector,intent);(,bytesmemoryresult)=sender.call{value:0,gas:gasleft()}(executeCallData);returnresult;}}
Sample Account
The following IAccount implementation uses a StandardRegistry to maintain allowlist of standards and just batch execute
all operations returned from IStandard.unpackOperations.
import{MessageHashUtils}from"@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";import{ECDSA}from"@openzeppelin/contracts/utils/cryptography/ECDSA.sol";/// @title StandardRegistry
/// @notice This is a registry for standards, determining whether an account accepts a standard
/// @dev EIP-712 is used for signature verification
contractStandardRegistry{usingECDSAforbytes32;/// @notice The event emitted when a standard is registered
eventStandardRegistered(addressindexedsigner,addressindexedstandard);/// @notice The event emitted when a standard is unregistered
eventStandardUnregistered(addressindexedsigner,addressindexedstandard);/// @notice The domain separator of this contract
bytes32publicimmutableDOMAIN_SEPARATOR;/// @notice The type hash of the signed data of this contract
bytes32publicimmutableSIGNED_DATA_TYPEHASH;/// @notice The mapping of nonces
mapping(bytes32nonce=>boolused)private_nonces;/// @notice The mapping of registrations
mapping(bytes32standard=>boolregistered)private_registrations;/// @notice The constructor of this contract
constructor(){DOMAIN_SEPARATOR=keccak256(abi.encode(keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),keccak256(bytes("StandardRegistry")),// Contract name
keccak256(bytes("2")),// Version
block.chainid,// Chain ID
address(this)// Contract address
));SIGNED_DATA_TYPEHASH=keccak256("Permission(bool registering,address standard,uint256 nonce)");}/// @notice The function to permit a standard, allowing a relayer to register or unregister a standard for a user
/// @param registering Whether registering or unregistering
/// @param signer The signer of the permission
/// @param standard The standard to permit
/// @param nonce The nonce of the permission
/// @param signature The signature of the permission
functionpermit(boolregistering,addresssigner,addressstandard,uint256nonce,bytescalldatasignature)external{bytes32compositeKey=keccak256(abi.encodePacked(signer,nonce));require(!_nonces[compositeKey],"Invalid nonce");// validate signature
bytes32structHash=keccak256(abi.encode(SIGNED_DATA_TYPEHASH,registering,standard,nonce));bytes32digest=MessageHashUtils.toTypedDataHash(DOMAIN_SEPARATOR,structHash);require(signer==digest.recover(signature),"Invalid signature");_process(registering,signer,standard,nonce);}/// @notice The function to update a standard registration directly
/// @param registering Whether registering or unregistering
/// @param standard The standard to update
/// @param nonce The nonce of the update
functionupdate(boolregistering,addressstandard,uint256nonce)external{addresssigner=msg.sender;bytes32compositeKey=keccak256(abi.encodePacked(signer,nonce));require(!_nonces[compositeKey],"Invalid nonce");_process(registering,signer,standard,nonce);}/// @notice The function to check if a nonce is used
/// @param signer The signer of the nonce
/// @param nonce The nonce to check
/// @return result true if the nonce is used
functionisNonceUsed(addresssigner,uint256nonce)externalviewreturns(bool){bytes32compositeKey=keccak256(abi.encodePacked(signer,nonce));return_nonces[compositeKey];}/// @notice The function to check if a standard is registered
/// @param signer The signer of the standard
/// @param standard The standard to check
/// @return result true if the standard is registered
functionisRegistered(addresssigner,addressstandard)externalviewreturns(bool){bytes32compositeKey=keccak256(abi.encodePacked(signer,standard));return_registrations[compositeKey];}/// @notice The function to process a standard registration or unregistration
/// @param registering Whether registering or unregistering
/// @param signer The signer of the registration
/// @param standard The standard to process
/// @param nonce The nonce of the registration
function_process(boolregistering,addresssigner,addressstandard,uint256nonce)internal{bytes32compositeKey=keccak256(abi.encodePacked(signer,standard));if(registering){_registrations[compositeKey]=true;emitStandardRegistered(signer,standard);}else{_registrations[compositeKey]=false;emitStandardUnregistered(signer,standard);}compositeKey=keccak256(abi.encodePacked(signer,nonce));_nonces[compositeKey]=true;}}
contractAccountImplV0{stringpublicconstantDESCRIPTION="Account with Batch Execution, Standard Registry";stringpublicconstantVERSION="0.0.0";stringpublicconstantAUTHOR="hellohanchen";StandardRegistrypublicconstantREGISTRY=StandardRegistry(address());bytes4publicconstantVALIDATION_APPROVED=0x00000001;bytes4publicconstantVALIDATION_DENIED=0x00000000;functionexecuteOtherIntent(bytescalldataintent)overrideinternalreturns(bytesmemory){(addresssender,addressstandard)=PackedIntent.getSenderAndStandard(intent);require(sender==address(this),"Intent is not from this account");require(REGISTRY.isRegistered(address(this),standard),"Standard not registered");// standard validation and unpack
(bytes4validationCode,bytes[]memoryinstructions)=IStandard(standard).unpackOperations(intent);require(validationCode==VALIDATION_APPROVED,"Validation failed");// batch execute
for(uint256i=0;i<instructions.length;i++){(addressdest,uint256value,bytesmemorydata)=abi.decode(instructions[i],(address,uint256,bytes));(boolsuccess,)=dest.call{value:value,gas:gasleft()}(data);if(!success){revertSelfExecutableAccount.ExecutionError();}}returnnewbytes(0);}receive()externalpayable{}}
As shown above, the implementation of IAccount is stateless and simple, so that it can be compatible with different IStandard.
While the IStandard implementation is complex because it needs to define its own schema. But both contracts will be public
and audited, to ensure the security of intent execution.
Security Considerations
The security of this standard primarily depends on the implementation of both IStandard and IAccount. Each component must ensure that user intents are validated and executed safely. Additionally, solvers are responsible for securing their own execution environments to prevent unintended exploits.
Auditability of both Validation and Execution
To ensure security and maintain ecosystem integrity, it is critical that both the standard (IStandard) and account (IAccount) implementations are:
Publicly auditable: Open access to contract code allows security researchers to identify potential vulnerabilities.
Well-reviewed and shared: Public discussions and peer reviews help strengthen security assumptions.
Secure against compatibility risks: Ensuring compatibility between different standard and account implementations can prevent unintended interactions that may lead to exploits.
Delegated Contract Storage Risks
If an IAccount implementation maintains state (instead of being stateless), it could:
Interfere with other delegated contracts sharing the same storage.
Be manipulated by unauthorized users if storage is not properly protected.
Strongly RECOMMEND stateless execution to prevent storage conflicts.