Skip to content

Commit

Permalink
style: consistent natspec style
Browse files Browse the repository at this point in the history
  • Loading branch information
jaypaik committed Mar 12, 2024
1 parent 73ddc7c commit 8dac3c6
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 243 deletions.
172 changes: 66 additions & 106 deletions src/LightAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,36 +16,28 @@ import {PackedUserOperation} from "account-abstraction/interfaces/PackedUserOper
import {BaseLightAccount} from "./common/BaseLightAccount.sol";
import {CustomSlotInitializable} from "./common/CustomSlotInitializable.sol";

/**
* @title A simple ERC-4337 compatible smart contract account with a designated owner account
* @dev Like eth-infinitism's `SimpleAccount`, but with the following changes:
*
* 1. Instead of the default storage slots, uses namespaced storage to avoid
* clashes when switching implementations.
*
* 2. Ownership can be transferred via `transferOwnership`, similar to the
* behavior of an `Ownable` contract. This is a simple single-step operation,
* so care must be taken to ensure that the ownership is being transferred to
* the correct address.
*
* 3. Supports [ERC-1271](https://eips.ethereum.org/EIPS/eip-1271) signature
* validation for both validating the signature on user operations and in
* exposing its own `isValidSignature` method. This only works when the owner of
* `LightAccount` also support ERC-1271.
*
* ERC-4337's bundler validation rules limit the types of contracts that can be
* used as owners to validate user operation signatures. For example, the
* contract's `isValidSignature` function may not use any forbidden opcodes
* such as `TIMESTAMP` or `NUMBER`, and the contract may not be an ERC-1967
* proxy as it accesses a constant implementation slot not associated with
* the account, violating storage access rules. This also means that the
* owner of a `LightAccount` may not be another `LightAccount` if you want to
* send user operations through a bundler.
*
* 4. Event `SimpleAccountInitialized` renamed to `LightAccountInitialized`.
*
* 5. Uses custom errors.
*/
/// @title A simple ERC-4337 compatible smart contract account with a designated owner account.
/// @dev Like eth-infinitism's SimpleAccount, but with the following changes:
///
/// 1. Instead of the default storage slots, uses namespaced storage to avoid clashes when switching implementations.
///
/// 2. Ownership can be transferred via `transferOwnership`, similar to the behavior of an `Ownable` contract. This is
/// a simple single-step operation, so care must be taken to ensure that the ownership is being transferred to the
/// correct address.
///
/// 3. Supports [ERC-1271](https://eips.ethereum.org/EIPS/eip-1271) signature validation for both validating the
/// signature on user operations and in exposing its own `isValidSignature` method. This only works when the owner of
/// LightAccount also support ERC-1271.
///
/// ERC-4337's bundler validation rules limit the types of contracts that can be used as owners to validate user
/// operation signatures. For example, the contract's `isValidSignature` function may not use any forbidden opcodes
/// such as `TIMESTAMP` or `NUMBER`, and the contract may not be an ERC-1967 proxy as it accesses a constant
/// implementation slot not associated with the account, violating storage access rules. This also means that the
/// owner of a LightAccount may not be another LightAccount if you want to send user operations through a bundler.
///
/// 4. Event `SimpleAccountInitialized` renamed to `LightAccountInitialized`.
///
/// 5. Uses custom errors.
contract LightAccount is BaseLightAccount, CustomSlotInitializable {
using ECDSA for bytes32;
using MessageHashUtils for bytes32;
Expand All @@ -65,69 +57,53 @@ contract LightAccount is BaseLightAccount, CustomSlotInitializable {
address owner;
}

/**
* @notice Emitted when this account is first initialized
* @param entryPoint The entry point
* @param owner The initial owner
*/
/// @notice Emitted when this account is first initialized.
/// @param entryPoint The entry point.
/// @param owner The initial owner.
event LightAccountInitialized(IEntryPoint indexed entryPoint, address indexed owner);

/**
* @notice Emitted when this account's owner changes. Also emitted once at
* initialization, with a `previousOwner` of 0.
* @param previousOwner The previous owner
* @param newOwner The new owner
*/
/// @notice Emitted when this account's owner changes. Also emitted once at initialization, with a
/// `previousOwner` of 0.
/// @param previousOwner The previous owner.
/// @param newOwner The new owner.
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

/**
* @dev The new owner is not a valid owner (e.g., `address(0)`, the
* account itself, or the current owner).
*/
/// @dev The new owner is not a valid owner (e.g., `address(0)`, the account itself, or the current owner).
error InvalidOwner(address owner);

constructor(IEntryPoint anEntryPoint) CustomSlotInitializable(_INITIALIZABLE_STORAGE_POSITION) {
_ENTRY_POINT = anEntryPoint;
constructor(IEntryPoint entryPoint_) CustomSlotInitializable(_INITIALIZABLE_STORAGE_POSITION) {
_ENTRY_POINT = entryPoint_;
_disableInitializers();
}

/**
* @notice Called once as part of initialization, either during initial deployment or when first upgrading to
* this contract.
* @dev The _ENTRY_POINT member is immutable, to reduce gas consumption. To upgrade EntryPoint,
* a new implementation of LightAccount must be deployed with the new EntryPoint address, then upgrading
* the implementation by calling `upgradeTo()`
* @param anOwner The initial owner of the account
*/
function initialize(address anOwner) public virtual initializer {
_initialize(anOwner);
/// @notice Called once as part of initialization, either during initial deployment or when first upgrading to
/// this contract.
/// @dev The `_ENTRY_POINT` member is immutable, to reduce gas consumption. To update the entry point address, a new
/// implementation of LightAccount must be deployed with the new entry point address, and then `upgradeToAndCall`
/// must be called to upgrade the implementation.
/// @param owner_ The initial owner of the account.
function initialize(address owner_) public virtual initializer {
_initialize(owner_);
}

/**
* @notice Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner or from the entry point via a
* user operation signed by the current owner.
* @param newOwner The new owner
*/
/// @notice Transfers ownership of the contract to a new account (`newOwner`). Can only be called by the current
/// owner or from the entry point via a user operation signed by the current owner.
/// @param newOwner The new owner.
function transferOwnership(address newOwner) external virtual onlyOwner {
if (newOwner == address(0) || newOwner == address(this)) {
revert InvalidOwner(newOwner);
}
_transferOwnership(newOwner);
}

/**
* @notice Return the current owner of this account
* @return The current owner
*/
/// @notice Return the current owner of this account.
/// @return The current owner.
function owner() public view returns (address) {
return _getStorage().owner;
}

/**
* @notice Returns the domain separator for this contract, as defined in the EIP-712 standard.
* @return bytes32 The domain separator hash.
*/
/// @notice Returns the domain separator for this contract, as defined in the EIP-712 standard.
/// @return bytes32 The domain separator hash.
function domainSeparator() public view returns (bytes32) {
return keccak256(
abi.encode(
Expand All @@ -140,34 +116,26 @@ contract LightAccount is BaseLightAccount, CustomSlotInitializable {
);
}

/**
* @notice Returns the pre-image of the message hash
* @param message Message that should be encoded.
* @return Encoded message.
*/
/// @notice Returns the pre-image of the message hash.
/// @param message Message that should be encoded.
/// @return Encoded message.
function encodeMessageData(bytes memory message) public view returns (bytes memory) {
bytes32 messageHash = keccak256(abi.encode(_LA_MSG_TYPEHASH, keccak256(message)));
return abi.encodePacked("\x19\x01", domainSeparator(), messageHash);
}

/**
* @notice Returns hash of a message that can be signed by owners.
* @param message Message that should be hashed.
* @return Message hash.
*/
/// @notice Returns hash of a message that can be signed by owners.
/// @param message Message that should be hashed.
/// @return Message hash.
function getMessageHash(bytes memory message) public view returns (bytes32) {
return keccak256(encodeMessageData(message));
}

/**
* @inheritdoc IERC1271
* @dev The signature is valid if it is signed by the owner's private key
* (if the owner is an EOA) or if it is a valid ERC-1271 signature from the
* owner (if the owner is a contract). Note that unlike the signature
* validation used in `validateUserOp`, this does **not** wrap the digest in
* an "Ethereum Signed Message" envelope before checking the signature in
* the EOA-owner case.
*/
/// @inheritdoc IERC1271
/// @dev The signature is valid if it is signed by the owner's private key (if the owner is an EOA) or if it is a
/// valid ERC-1271 signature from the owner (if the owner is a contract). Note that unlike the signature validation
/// used in `validateUserOp`, this does **not** wrap the digest in an "Ethereum Signed Message" envelope before
/// checking the signature in the EOA-owner case.
function isValidSignature(bytes32 hash, bytes memory signature) public view override returns (bytes4) {
bytes32 messageHash = getMessageHash(abi.encode(hash));
if (SignatureChecker.isValidSignatureNow(owner(), messageHash, signature)) {
Expand All @@ -176,19 +144,15 @@ contract LightAccount is BaseLightAccount, CustomSlotInitializable {
return 0xffffffff;
}

function _initialize(address anOwner) internal virtual {
if (anOwner == address(0)) {
function _initialize(address owner_) internal virtual {
if (owner_ == address(0)) {
revert InvalidOwner(address(0));
}
_getStorage().owner = anOwner;
emit LightAccountInitialized(_ENTRY_POINT, anOwner);
emit OwnershipTransferred(address(0), anOwner);
_getStorage().owner = owner_;
emit LightAccountInitialized(_ENTRY_POINT, owner_);
emit OwnershipTransferred(address(0), owner_);
}

/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Internal function without access restriction.
*/
function _transferOwnership(address newOwner) internal virtual {
LightAccountStorage storage _storage = _getStorage();
address oldOwner = _storage.owner;
Expand All @@ -199,13 +163,9 @@ contract LightAccount is BaseLightAccount, CustomSlotInitializable {
emit OwnershipTransferred(oldOwner, newOwner);
}

/*
* Implement template method of BaseAccount.
*
* Uses a modified version of `SignatureChecker.isValidSignatureNow` in
* which the digest is wrapped with an "Ethereum Signed Message" envelope
* for the EOA-owner case but not in the ERC-1271 contract-owner case.
*/
/// @dev Implement template method of BaseAccount.
/// Uses a modified version of `SignatureChecker.isValidSignatureNow` in which the digest is wrapped with an
/// "Ethereum Signed Message" envelope for the EOA-owner case but not in the ERC-1271 contract-owner case.
function _validateSignature(PackedUserOperation calldata userOp, bytes32 userOpHash)
internal
virtual
Expand Down
36 changes: 15 additions & 21 deletions src/LightAccountFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,24 @@ import {IEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol";

import {LightAccount} from "./LightAccount.sol";

/**
* @title A factory contract for LightAccount
* @dev A UserOperations "initCode" holds the address of the factory, and a method call (to createAccount, in this sample factory).
* The factory's createAccount returns the target account address even if it is already installed.
* This way, the entryPoint.getSenderAddress() can be called either before or after the account is created.
*/
/// @title A factory contract for LightAccount.
/// @dev A UserOperations "initCode" holds the address of the factory, and a method call (`createAccount`). The
/// factory's `createAccount` returns the target account address even if it is already installed. This way,
/// `entryPoint.getSenderAddress()` can be called either before or after the account is created.
contract LightAccountFactory {
LightAccount public immutable accountImplementation;

constructor(IEntryPoint _entryPoint) {
accountImplementation = new LightAccount(_entryPoint);
}

/**
* @notice Create an account, and return its address.
* Returns the address even if the account is already deployed.
* @dev During UserOperation execution, this method is called only if the account is not deployed.
* This method returns an existing account address so that entryPoint.getSenderAddress() would work even after account creation.
* @param owner The owner of the account to be created
* @param salt A salt, which can be changed to create multiple accounts with the same owner
* @return ret The address of either the newly deployed account or an existing account with this owner and salt
*/
/// @notice Create an account, and return its address. Returns the address even if the account is already deployed.
/// @dev During UserOperation execution, this method is called only if the account is not deployed. This method
/// returns an existing account address so that entryPoint.getSenderAddress() would work even after account
/// creation.
/// @param owner The owner of the account to be created.
/// @param salt A salt, which can be changed to create multiple accounts with the same owner.
/// @return ret The address of either the newly deployed account or an existing account with this owner and salt.
function createAccount(address owner, uint256 salt) public returns (LightAccount ret) {
address addr = getAddress(owner, salt);
uint256 codeSize = addr.code.length;
Expand All @@ -45,12 +41,10 @@ contract LightAccountFactory {
);
}

/**
* @notice Calculate the counterfactual address of this account as it would be returned by createAccount()
* @param owner The owner of the account to be created
* @param salt A salt, which can be changed to create multiple accounts with the same owner
* @return The address of the account that would be created with createAccount()
*/
/// @notice Calculate the counterfactual address of this account as it would be returned by `createAccount`.
/// @param owner The owner of the account to be created.
/// @param salt A salt, which can be changed to create multiple accounts with the same owner.
/// @return The address of the account that would be created with `createAccount`.
function getAddress(address owner, uint256 salt) public view returns (address) {
return Create2.computeAddress(
bytes32(salt),
Expand Down
Loading

0 comments on commit 8dac3c6

Please sign in to comment.