mirror of
https://github.com/luxfi/eoa-contracts.git
synced 2026-10-30 16:58:32 +00:00
404 lines
14 KiB
Solidity
404 lines
14 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity 0.8.17;
|
|
|
|
import {BaseSmartAccount, IEntryPoint, UserOperation} from "./BaseSmartAccount.sol";
|
|
import {ModuleManager} from "./base/ModuleManager.sol";
|
|
import {FallbackManager} from "./base/FallbackManager.sol";
|
|
import {LibAddress} from "./libs/LibAddress.sol";
|
|
import {ISignatureValidator} from "./interfaces/ISignatureValidator.sol";
|
|
import {IERC165} from "./interfaces/IERC165.sol";
|
|
import {SmartAccountErrors} from "./common/Errors.sol";
|
|
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
|
|
import {IAuthorizationModule} from "./interfaces/IAuthorizationModule.sol";
|
|
|
|
/**
|
|
* @title SmartAccount - EIP-4337 compatible smart contract wallet.
|
|
* @dev This contract is the base for the Smart Account functionality.
|
|
* - It is modular by nature. UserOp and txns validation happens in Authorization Modules.
|
|
* - It provides the functionality to execute AA (EIP-4337) userOps. Gnosis style txns removed to a module.
|
|
* - It allows to receive and manage assets.
|
|
* - It is responsible for managing the modules and fallbacks.
|
|
* - The Smart Account can be extended with modules, such as Social Recovery, Session Key and others.
|
|
* @author Chirag Titiya - <[email protected]>, Filipp Makarov - <[email protected]>
|
|
*/
|
|
contract SmartAccount is
|
|
BaseSmartAccount,
|
|
ModuleManager,
|
|
FallbackManager,
|
|
IERC165,
|
|
SmartAccountErrors,
|
|
ISignatureValidator
|
|
{
|
|
using ECDSA for bytes32;
|
|
using LibAddress for address;
|
|
|
|
// Storage Version
|
|
string public constant VERSION = "2.0.0";
|
|
|
|
// Owner storage. Deprecated. Left for storage layout compatibility
|
|
address public ownerDeprecated;
|
|
|
|
// changed to 2D nonce below
|
|
// @notice there is no _nonce
|
|
// Deprecated. Left for storage layout compatibility
|
|
mapping(uint256 => uint256) public noncesDeprecated;
|
|
|
|
// AA immutable storage
|
|
IEntryPoint private immutable ENTRY_POINT;
|
|
address private immutable SELF;
|
|
|
|
// Events
|
|
event ImplementationUpdated(
|
|
address indexed oldImplementation,
|
|
address indexed newImplementation
|
|
);
|
|
event SmartAccountReceivedNativeToken(
|
|
address indexed sender,
|
|
uint256 indexed value
|
|
);
|
|
|
|
/**
|
|
* @dev Constructor that sets the entry point contract.
|
|
* _modules[SENTINEL_MODULES] = SENTINEL_MODULES protects implementation from initialization
|
|
* @param anEntryPoint The address of the entry point contract.
|
|
*/
|
|
constructor(IEntryPoint anEntryPoint) {
|
|
SELF = address(this);
|
|
if (address(anEntryPoint) == address(0))
|
|
revert EntryPointCannotBeZero();
|
|
ENTRY_POINT = anEntryPoint;
|
|
_modules[SENTINEL_MODULES] = SENTINEL_MODULES;
|
|
}
|
|
|
|
/**
|
|
* @dev This function is a special fallback function that is triggered when the contract receives Ether.
|
|
* It logs an event indicating the amount of Ether received and the sender's address.
|
|
* @notice This function is marked as external and payable, meaning it can be called from external
|
|
* sources and accepts Ether as payment.
|
|
*/
|
|
receive() external payable {
|
|
if (address(this) == SELF) revert DelegateCallsOnly();
|
|
emit SmartAccountReceivedNativeToken(msg.sender, msg.value);
|
|
}
|
|
|
|
/**
|
|
* @dev Initialize the Smart Account with required states
|
|
* @param handler Default fallback handler provided in Smart Account
|
|
* @param moduleSetupContract Contract, that setups initial auth module for this smart account.
|
|
* It can be a module factory or a registry module that serves several smart accounts
|
|
* @param moduleSetupData modules setup data (a standard calldata for the module setup contract)
|
|
* @notice devs need to make sure it is only callable once by initializer or state check restrictions
|
|
* @notice any further implementations that introduces a new state must have a reinit method
|
|
* @notice reinitialization is not possible, as _initialSetupModules reverts if the account is already initialized
|
|
* which is when there is at least one enabled module
|
|
*/
|
|
function init(
|
|
address handler,
|
|
address moduleSetupContract,
|
|
bytes calldata moduleSetupData
|
|
) external virtual override returns (address) {
|
|
if (
|
|
_modules[SENTINEL_MODULES] != address(0) ||
|
|
getFallbackHandler() != address(0)
|
|
) revert AlreadyInitialized();
|
|
_setFallbackHandler(handler);
|
|
return _initialSetupModules(moduleSetupContract, moduleSetupData);
|
|
}
|
|
|
|
/**
|
|
* @dev Interface function with the standard name for execute_ncC
|
|
* @param dest Address of the contract to call
|
|
* @param value Amount of native tokens to send along with the transaction
|
|
* @param func Data of the transaction
|
|
*/
|
|
function execute(
|
|
address dest,
|
|
uint256 value,
|
|
bytes calldata func
|
|
) external {
|
|
execute_ncC(dest, value, func);
|
|
}
|
|
|
|
/**
|
|
* @dev Interface function with the standard name for executeBatch_y6U
|
|
* @param dest Addresses of the contracts to call
|
|
* @param value Amounts of native tokens to send along with the transactions
|
|
* @param func Data of the transactions
|
|
*/
|
|
function executeBatch(
|
|
address[] calldata dest,
|
|
uint256[] calldata value,
|
|
bytes[] calldata func
|
|
) external {
|
|
executeBatch_y6U(dest, value, func);
|
|
}
|
|
|
|
function validateUserOp(
|
|
UserOperation calldata userOp,
|
|
bytes32 userOpHash,
|
|
uint256 missingAccountFunds
|
|
) external virtual override returns (uint256 validationData) {
|
|
if (msg.sender != address(entryPoint()))
|
|
revert CallerIsNotAnEntryPoint(msg.sender);
|
|
|
|
(, address validationModule) = abi.decode(
|
|
userOp.signature,
|
|
(bytes, address)
|
|
);
|
|
if (address(_modules[validationModule]) != address(0)) {
|
|
validationData = IAuthorizationModule(validationModule)
|
|
.validateUserOp(userOp, userOpHash);
|
|
} else {
|
|
revert WrongValidationModule(validationModule);
|
|
}
|
|
// Check nonce requirement if any
|
|
_payPrefund(missingAccountFunds);
|
|
}
|
|
|
|
/**
|
|
* @dev Adds a module to the allowlist.
|
|
* @notice This can only be done via a userOp or a selfcall.
|
|
* @notice Enables the module `module` for the wallet.
|
|
* @param module Module to be allow-listed.
|
|
*/
|
|
function enableModule(address module) external virtual override {
|
|
_requireFromEntryPointOrSelf();
|
|
_enableModule(module);
|
|
}
|
|
|
|
/**
|
|
* @dev Setups module for this Smart Account and enables it.
|
|
* @notice This can only be done via userOp or a selfcall.
|
|
* @notice Enables the module `module` for the wallet.
|
|
*/
|
|
function setupAndEnableModule(
|
|
address setupContract,
|
|
bytes memory setupData
|
|
) external virtual override returns (address) {
|
|
_requireFromEntryPointOrSelf();
|
|
return _setupAndEnableModule(setupContract, setupData);
|
|
}
|
|
|
|
/**
|
|
* @dev Sets the fallback handler.
|
|
* @notice This can only be done via a UserOp sent by EntryPoint.
|
|
* @param handler Handler to be set.
|
|
*/
|
|
function setFallbackHandler(address handler) external virtual override {
|
|
_requireFromEntryPointOrSelf();
|
|
_setFallbackHandler(handler);
|
|
}
|
|
|
|
/**
|
|
* @dev Returns the address of the implementation contract associated with this contract.
|
|
* @notice The implementation address is stored in the contract's storage slot with index 0.
|
|
*/
|
|
function getImplementation()
|
|
external
|
|
view
|
|
returns (address _implementation)
|
|
{
|
|
assembly {
|
|
_implementation := sload(address())
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @notice Query if a contract implements an interface
|
|
* @param _interfaceId The interface identifier, as specified in ERC165
|
|
* @return `true` if the contract implements `_interfaceID`
|
|
*/
|
|
function supportsInterface(
|
|
bytes4 _interfaceId
|
|
) external view virtual override returns (bool) {
|
|
return _interfaceId == type(IERC165).interfaceId; // 0x01ffc9a7
|
|
}
|
|
|
|
/**
|
|
* @notice All the new implementations MUST have this method!
|
|
* @notice Updates the implementation of the base wallet
|
|
* @param _implementation New wallet implementation
|
|
*/
|
|
function updateImplementation(address _implementation) public virtual {
|
|
_requireFromEntryPointOrSelf();
|
|
require(_implementation != address(0), "Address cannot be zero");
|
|
if (!_implementation.isContract())
|
|
revert InvalidImplementation(_implementation);
|
|
address oldImplementation;
|
|
|
|
assembly {
|
|
oldImplementation := sload(address())
|
|
sstore(address(), _implementation)
|
|
}
|
|
emit ImplementationUpdated(oldImplementation, _implementation);
|
|
}
|
|
|
|
/* solhint-disable func-name-mixedcase */
|
|
|
|
/**
|
|
* @dev Execute a transaction (called by entryPoint)
|
|
* @notice Name is optimized for this method to be cheaper to be called
|
|
* @param dest Address of the contract to call
|
|
* @param value Amount of native tokens to send along with the transaction
|
|
* @param func Data of the transaction
|
|
*/
|
|
function execute_ncC(
|
|
address dest,
|
|
uint256 value,
|
|
bytes calldata func
|
|
) public {
|
|
_requireFromEntryPoint();
|
|
_call(dest, value, func);
|
|
}
|
|
|
|
/**
|
|
* @dev Execute a sequence of transactions
|
|
* @notice Name is optimized for this method to be cheaper to be called
|
|
* @param dest Addresses of the contracts to call
|
|
* @param value Amounts of native tokens to send along with the transactions
|
|
* @param func Data of the transactions
|
|
*/
|
|
function executeBatch_y6U(
|
|
address[] calldata dest,
|
|
uint256[] calldata value,
|
|
bytes[] calldata func
|
|
) public {
|
|
_requireFromEntryPoint();
|
|
if (
|
|
dest.length == 0 ||
|
|
dest.length != value.length ||
|
|
value.length != func.length
|
|
) revert WrongBatchProvided(dest.length, value.length, func.length, 0);
|
|
for (uint256 i; i < dest.length; ) {
|
|
_call(dest[i], value[i], func[i]);
|
|
unchecked {
|
|
++i;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* solhint-enable func-name-mixedcase */
|
|
|
|
/**
|
|
* @dev Deposit more funds for this account in the entryPoint
|
|
*/
|
|
function addDeposit() public payable {
|
|
entryPoint().depositTo{value: msg.value}(address(this));
|
|
}
|
|
|
|
/**
|
|
* @dev Withdraw value from the account's deposit
|
|
* @param withdrawAddress target to send to
|
|
* @param amount to withdraw
|
|
*/
|
|
function withdrawDepositTo(
|
|
address payable withdrawAddress,
|
|
uint256 amount
|
|
) public payable {
|
|
_requireFromEntryPointOrSelf();
|
|
entryPoint().withdrawTo(withdrawAddress, amount);
|
|
}
|
|
|
|
/**
|
|
* @dev Removes a module from the allowlist.
|
|
* @notice This can only be done via a wallet transaction.
|
|
* @notice Disables the module `module` for the wallet.
|
|
* @param prevModule Module that pointed to the module to be removed in the linked list
|
|
* @param module Module to be removed.
|
|
*/
|
|
function disableModule(address prevModule, address module) public virtual {
|
|
_requireFromEntryPointOrSelf();
|
|
_disableModule(prevModule, module);
|
|
}
|
|
|
|
/**
|
|
* @dev Returns the current entry point used by this account.
|
|
* @return EntryPoint as an `IEntryPoint` interface.
|
|
* @dev This function should be implemented by the subclass to return the current entry point used by this account.
|
|
*/
|
|
function entryPoint() public view virtual override returns (IEntryPoint) {
|
|
return ENTRY_POINT;
|
|
}
|
|
|
|
/**
|
|
* @dev Check current account deposit in the entryPoint
|
|
*/
|
|
function getDeposit() public view returns (uint256) {
|
|
return entryPoint().balanceOf(address(this));
|
|
}
|
|
|
|
/**
|
|
* Implementation of ISignatureValidator (see `interfaces/ISignatureValidator.sol`)
|
|
* @dev Forwards the validation to the module specified in the signature
|
|
* @param dataHash 32 bytes hash of the data signed on the behalf of address(msg.sender)
|
|
* @param signature Signature byte array associated with dataHash
|
|
* @return bytes4 value.
|
|
*/
|
|
function isValidSignature(
|
|
bytes32 dataHash,
|
|
bytes memory signature
|
|
) public view override returns (bytes4) {
|
|
(bytes memory moduleSignature, address validationModule) = abi.decode(
|
|
signature,
|
|
(bytes, address)
|
|
);
|
|
if (address(_modules[validationModule]) != address(0)) {
|
|
return
|
|
ISignatureValidator(validationModule).isValidSignature(
|
|
dataHash,
|
|
moduleSignature
|
|
);
|
|
} else {
|
|
revert WrongValidationModule(validationModule);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @dev internal method that fecilitates the extenral calls from SmartAccount
|
|
* @dev similar to execute() of Executor.sol
|
|
* @param target destination address contract/non-contract
|
|
* @param value amount of native tokens
|
|
* @param data function singature of destination
|
|
*/
|
|
function _call(address target, uint256 value, bytes memory data) internal {
|
|
assembly {
|
|
let success := call(
|
|
gas(),
|
|
target,
|
|
value,
|
|
add(data, 0x20),
|
|
mload(data),
|
|
0,
|
|
0
|
|
)
|
|
let ptr := mload(0x40)
|
|
returndatacopy(ptr, 0, returndatasize())
|
|
if iszero(success) {
|
|
revert(ptr, returndatasize())
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @dev This function allows entry point or SA itself to execute certain actions.
|
|
* If the caller is not authorized, the function will revert with an error message.
|
|
* @notice This function acts as modifier and is marked as internal to be be called
|
|
* within the contract itself only.
|
|
*/
|
|
function _requireFromEntryPointOrSelf() internal view {
|
|
if (msg.sender != address(entryPoint()) && msg.sender != address(this))
|
|
revert CallerIsNotEntryPointOrSelf(msg.sender);
|
|
}
|
|
|
|
/**
|
|
* @dev This function allows entry point to execute certain actions.
|
|
* If the caller is not authorized, the function will revert with an error message.
|
|
* @notice This function acts as modifier and is marked as internal to be be called
|
|
* within the contract itself only.
|
|
*/
|
|
function _requireFromEntryPoint() internal view {
|
|
if (msg.sender != address(entryPoint()))
|
|
revert CallerIsNotEntryPoint(msg.sender);
|
|
}
|
|
}
|