mirror of
https://github.com/luxfi/eoa-contracts.git
synced 2026-10-30 16:58:32 +00:00
824 lines
33 KiB
Solidity
824 lines
33 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity 0.8.17;
|
|
|
|
import {BaseSmartAccount, IEntryPoint, Transaction, FeeRefund, Enum, UserOperation} from "./BaseSmartAccountV1.sol";
|
|
import {ModuleManagerV1} from "./ModuleManagerV1.sol";
|
|
import {FallbackManagerV1} from "./FallbackManagerV1.sol";
|
|
import {SignatureDecoder} from "../../../common/SignatureDecoder.sol";
|
|
import {SecuredTokenTransfer} from "../../../common/SecuredTokenTransfer.sol";
|
|
import {LibAddress} from "../../../libs/LibAddress.sol";
|
|
import {ISignatureValidator} from "../../../interfaces/ISignatureValidator.sol";
|
|
import {Math} from "../../../libs/Math.sol";
|
|
import {IERC165} from "../../../interfaces/IERC165.sol";
|
|
import {ReentrancyGuard} from "../../../common/ReentrancyGuard.sol";
|
|
import {SmartAccountErrorsV1} from "./ErrorsV1.sol";
|
|
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
|
|
import {IModule} from "./IModuleV1.sol";
|
|
|
|
/**
|
|
* @title SmartAccount - EIP-4337 compatible smart contract wallet.
|
|
* @dev This contract is the base for the Smart Account functionality.
|
|
* - It provides the functionality to execute both gnosis-style txns and AA (EIP-4337) userOps
|
|
* - 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]>
|
|
*/
|
|
contract SmartAccountV1 is
|
|
BaseSmartAccount,
|
|
ModuleManagerV1,
|
|
FallbackManagerV1,
|
|
SignatureDecoder,
|
|
SecuredTokenTransfer,
|
|
IERC165,
|
|
ReentrancyGuard,
|
|
SmartAccountErrorsV1,
|
|
ISignatureValidator
|
|
{
|
|
using ECDSA for bytes32;
|
|
using LibAddress for address;
|
|
|
|
// Storage Version
|
|
string public constant VERSION = "1.0.0";
|
|
|
|
// Domain Seperators keccak256("EIP712Domain(uint256 chainId,address verifyingContract)");
|
|
bytes32 internal constant DOMAIN_SEPARATOR_TYPEHASH =
|
|
0x47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218;
|
|
|
|
// keccak256(
|
|
// "AccountTx(address to,uint256 value,bytes data,uint8 operation,uint256 targetTxGas,uint256 baseGas,uint256 gasPrice,uint256 tokenGasPriceFactor,address gasToken,address refundReceiver,uint256 nonce)"
|
|
// );
|
|
bytes32 internal constant ACCOUNT_TX_TYPEHASH =
|
|
0xda033865d68bf4a40a5a7cb4159a99e33dba8569e65ea3e38222eb12d9e66eee;
|
|
|
|
// Owner storage
|
|
address public owner;
|
|
|
|
// changed to 2D nonce below
|
|
// @notice there is no _nonce
|
|
mapping(uint256 => uint256) public nonces;
|
|
|
|
// AA immutable storage
|
|
IEntryPoint private immutable _entryPoint;
|
|
uint256 private immutable _chainId;
|
|
address private immutable _self;
|
|
|
|
// Events
|
|
|
|
event ImplementationUpdated(
|
|
address indexed oldImplementation,
|
|
address indexed newImplementation
|
|
);
|
|
event EOAChanged(
|
|
address indexed _scw,
|
|
address indexed _oldEOA,
|
|
address indexed _newEOA
|
|
);
|
|
event AccountHandlePayment(bytes32 indexed txHash, uint256 indexed payment);
|
|
event SmartAccountReceivedNativeToken(
|
|
address indexed sender,
|
|
uint256 indexed value
|
|
);
|
|
|
|
/// modifiers
|
|
/**
|
|
* @dev Modifier to allow only the owner to call the function.
|
|
* Reverts with CallerIsNotOwner if the caller is not the owner.
|
|
*/
|
|
modifier onlyOwner() {
|
|
if (msg.sender != owner) revert CallerIsNotOwner(msg.sender);
|
|
_;
|
|
}
|
|
/**
|
|
* @dev Modifier to allow only the owner or the contract itself to call the function.
|
|
* Reverts with MixedAuthFail if the caller is not the owner or the contract itself.
|
|
*/
|
|
modifier mixedAuth() {
|
|
if (msg.sender != owner && msg.sender != address(this))
|
|
revert MixedAuthFail(msg.sender);
|
|
_;
|
|
}
|
|
|
|
/**
|
|
* @dev Constructor that sets the owner of the contract and the entry point contract.
|
|
* @param anEntryPoint The address of the entry point contract.
|
|
*/
|
|
constructor(IEntryPoint anEntryPoint) {
|
|
_self = address(this);
|
|
// By setting the owner it is not possible to call init anymore,
|
|
// so we create an account with fixed non-zero owner.
|
|
// This is an unusable account, perfect for the singleton
|
|
owner = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE);
|
|
if (address(anEntryPoint) == address(0))
|
|
revert EntryPointCannotBeZero();
|
|
_entryPoint = anEntryPoint;
|
|
_chainId = block.chainid;
|
|
}
|
|
|
|
/// Getters
|
|
/**
|
|
* @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())
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @dev Allows to change the owner of the smart account by current owner or self-call (modules)
|
|
* @param _newOwner Address of the new signatory
|
|
*/
|
|
function setOwner(address _newOwner) public mixedAuth {
|
|
if (_newOwner == address(0)) revert OwnerCannotBeZero();
|
|
if (_newOwner == address(this)) revert OwnerCanNotBeSelf();
|
|
if (_newOwner == owner) revert OwnerProvidedIsSame();
|
|
address oldOwner = owner;
|
|
assembly {
|
|
sstore(owner.slot, _newOwner)
|
|
}
|
|
emit EOAChanged(address(this), oldOwner, _newOwner);
|
|
}
|
|
|
|
/**
|
|
* @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 mixedAuth {
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* @dev 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(
|
|
DOMAIN_SEPARATOR_TYPEHASH,
|
|
block.chainid,
|
|
address(this)
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @notice Returns the ID of the chain the contract is currently deployed on.
|
|
* @return _chainId The ID of the current chain as a uint256.
|
|
*/
|
|
function getChainId() public view returns (uint256) {
|
|
return _chainId;
|
|
}
|
|
|
|
/**
|
|
* @dev returns a value from the nonces 2d mapping
|
|
* @param batchId : the key of the user's batch being queried
|
|
* @return nonce : the number of transactions made within said batch
|
|
*/
|
|
function getNonce(uint256 batchId) public view virtual returns (uint256) {
|
|
return nonces[batchId];
|
|
}
|
|
|
|
/**
|
|
* @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 _entryPoint;
|
|
}
|
|
|
|
/**
|
|
* @dev Initialize the Smart Account with required states
|
|
* @param _owner Signatory of the Smart Account
|
|
* @param _handler Default fallback handler provided in Smart Account
|
|
* @notice devs need to make sure it is only callble once by initiazer or state check restrictions
|
|
* @notice any further implementations that introduces a new state must have a reinit method
|
|
* @notice init is prevented here by setting owner in the constructor and checking here for address(0)
|
|
*/
|
|
function init(address _owner, address _handler) external virtual override {
|
|
if (owner != address(0)) revert AlreadyInitialized();
|
|
if (_owner == address(0)) revert OwnerCannotBeZero();
|
|
owner = _owner;
|
|
_setFallbackHandler(_handler);
|
|
_setupModules(address(0), bytes(""));
|
|
}
|
|
|
|
/**
|
|
* @dev Gnosis style transaction with optional repay in native tokens OR ERC20
|
|
* @dev Allows to execute a transaction confirmed by required signature/s and then pays the account that submitted the transaction.
|
|
* @dev Function name optimized to have hash started with zeros to make this function calls cheaper
|
|
* @notice The fees are always transferred, even if the user transaction fails.
|
|
* @param _tx Smart Account transaction
|
|
* @param refundInfo Required information for gas refunds
|
|
* @param signatures Packed signature/s data ({bytes32 r}{bytes32 s}{uint8 v})
|
|
*/
|
|
function execTransaction_S6W(
|
|
Transaction memory _tx,
|
|
FeeRefund memory refundInfo,
|
|
bytes memory signatures
|
|
) public payable virtual nonReentrant returns (bool success) {
|
|
uint256 startGas = gasleft();
|
|
bytes32 txHash;
|
|
// Use scope here to limit variable lifetime and prevent `stack too deep` errors
|
|
{
|
|
bytes memory txHashData = encodeTransactionData(
|
|
// Transaction info
|
|
_tx,
|
|
// Payment info
|
|
refundInfo,
|
|
// Signature info
|
|
nonces[1]++
|
|
);
|
|
txHash = keccak256(txHashData);
|
|
checkSignatures(txHash, signatures);
|
|
}
|
|
|
|
// We require some gas to emit the events (at least 2500) after the execution and some to perform code until the execution (500)
|
|
// We also include the 1/64 in the check that is not send along with a call to counteract potential shortings because of EIP-150
|
|
// Bitshift left 6 bits means multiplying by 64, just more gas efficient
|
|
if (
|
|
gasleft() <
|
|
Math.max((_tx.targetTxGas << 6) / 63, _tx.targetTxGas + 2500) + 500
|
|
)
|
|
revert NotEnoughGasLeft(
|
|
gasleft(),
|
|
Math.max((_tx.targetTxGas << 6) / 63, _tx.targetTxGas + 2500) +
|
|
500
|
|
);
|
|
// Use scope here to limit variable lifetime and prevent `stack too deep` errors
|
|
{
|
|
// If the gasPrice is 0 we assume that nearly all available gas can be used (it is always more than targetTxGas)
|
|
// We only substract 2500 (compared to the 3000 before) to ensure that the amount passed is still higher than targetTxGas
|
|
success = _execute(
|
|
_tx.to,
|
|
_tx.value,
|
|
_tx.data,
|
|
_tx.operation,
|
|
refundInfo.gasPrice == 0 ? (gasleft() - 2500) : _tx.targetTxGas
|
|
);
|
|
// If no targetTxGas and no gasPrice was set (e.g. both are 0), then the internal tx is required to be successful
|
|
// This makes it possible to use `estimateGas` without issues, as it searches for the minimum gas where the tx doesn't revert
|
|
if (!success && _tx.targetTxGas == 0 && refundInfo.gasPrice == 0)
|
|
revert CanNotEstimateGas(
|
|
_tx.targetTxGas,
|
|
refundInfo.gasPrice,
|
|
success
|
|
);
|
|
// We transfer the calculated tx costs to the tx.origin to avoid sending it to intermediate contracts that have made calls
|
|
uint256 payment;
|
|
if (refundInfo.gasPrice != 0) {
|
|
payment = _handlePayment(
|
|
startGas - gasleft(),
|
|
refundInfo.baseGas,
|
|
refundInfo.gasPrice,
|
|
refundInfo.tokenGasPriceFactor,
|
|
refundInfo.gasToken,
|
|
refundInfo.refundReceiver
|
|
);
|
|
emit AccountHandlePayment(txHash, payment);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @dev Interface function with the standard name for execTransaction_S6W
|
|
*/
|
|
function execTransaction(
|
|
Transaction memory _tx,
|
|
FeeRefund memory refundInfo,
|
|
bytes memory signatures
|
|
) external payable virtual override returns (bool) {
|
|
return execTransaction_S6W(_tx, refundInfo, signatures);
|
|
}
|
|
|
|
/**
|
|
* @dev Handles the payment for a transaction refund from Smart Account to Relayer.
|
|
* @param gasUsed Gas used by the transaction.
|
|
* @param baseGas Gas costs that are independent of the transaction execution
|
|
* (e.g. base transaction fee, signature check, payment of the refund, emitted events).
|
|
* @param gasPrice Gas price / TokenGasPrice (gas price in the context of token using offchain price feeds)
|
|
* that should be used for the payment calculation.
|
|
* @param tokenGasPriceFactor factor by which calculated token gas price is already multiplied.
|
|
* @param gasToken Token address (or 0 if ETH) that is used for the payment.
|
|
* @return payment The amount of payment made in the specified token.
|
|
*/
|
|
function _handlePayment(
|
|
uint256 gasUsed,
|
|
uint256 baseGas,
|
|
uint256 gasPrice,
|
|
uint256 tokenGasPriceFactor,
|
|
address gasToken,
|
|
address payable refundReceiver
|
|
) private returns (uint256 payment) {
|
|
if (tokenGasPriceFactor == 0) revert TokenGasPriceFactorCanNotBeZero();
|
|
// solhint-disable-next-line avoid-tx-origin
|
|
address payable receiver = refundReceiver == address(0)
|
|
? payable(tx.origin)
|
|
: refundReceiver;
|
|
if (gasToken == address(0)) {
|
|
// For ETH we will only adjust the gas price to not be higher than the actual used gas price
|
|
payment =
|
|
(gasUsed + baseGas) *
|
|
(gasPrice < tx.gasprice ? gasPrice : tx.gasprice);
|
|
bool success;
|
|
assembly {
|
|
success := call(gas(), receiver, payment, 0, 0, 0, 0)
|
|
}
|
|
if (!success)
|
|
revert TokenTransferFailed(address(0), receiver, payment);
|
|
} else {
|
|
payment =
|
|
((gasUsed + baseGas) * (gasPrice)) /
|
|
(tokenGasPriceFactor);
|
|
if (!_transferToken(gasToken, receiver, payment))
|
|
revert TokenTransferFailed(gasToken, receiver, payment);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @dev Allows to estimate a transaction.
|
|
* @notice This method is only meant for estimation purpose, therefore the call will always revert and encode the result in the revert data.
|
|
* @notice Call this method to get an estimate of the handlePayment costs that are deducted with `execTransaction`
|
|
* @param gasUsed Gas used by the transaction.
|
|
* @param baseGas Gas costs that are independent of the transaction execution
|
|
* (e.g. base transaction fee, signature check, payment of the refund, emitted events).
|
|
* @param gasPrice Gas price / TokenGasPrice (gas price in the context of token using offchain price feeds)
|
|
* that should be used for the payment calculation.
|
|
* @param tokenGasPriceFactor factor by which calculated token gas price is already multiplied.
|
|
* @param gasToken Token address (or 0 if ETH) that is used for the payment.
|
|
* @return requiredGas Estimate of refunds
|
|
*/
|
|
function handlePaymentRevert(
|
|
uint256 gasUsed,
|
|
uint256 baseGas,
|
|
uint256 gasPrice,
|
|
uint256 tokenGasPriceFactor,
|
|
address gasToken,
|
|
address payable refundReceiver
|
|
) external returns (uint256 requiredGas) {
|
|
require(tokenGasPriceFactor != 0, "invalid tokenGasPriceFactor");
|
|
uint256 startGas = gasleft();
|
|
// solhint-disable-next-line avoid-tx-origin
|
|
address payable receiver = refundReceiver == address(0)
|
|
? payable(tx.origin)
|
|
: refundReceiver;
|
|
if (gasToken == address(0)) {
|
|
// For ETH we will only adjust the gas price to not be higher than the actual used gas price
|
|
uint256 payment = (gasUsed + baseGas) *
|
|
(gasPrice < tx.gasprice ? gasPrice : tx.gasprice);
|
|
bool success;
|
|
assembly {
|
|
success := call(gas(), receiver, payment, 0, 0, 0, 0)
|
|
}
|
|
if (!success)
|
|
revert TokenTransferFailed(address(0), receiver, payment);
|
|
} else {
|
|
uint256 payment = ((gasUsed + baseGas) * (gasPrice)) /
|
|
(tokenGasPriceFactor);
|
|
if (!_transferToken(gasToken, receiver, payment))
|
|
revert TokenTransferFailed(gasToken, receiver, payment);
|
|
}
|
|
unchecked {
|
|
requiredGas = startGas - gasleft();
|
|
}
|
|
revert(string(abi.encodePacked(requiredGas)));
|
|
}
|
|
|
|
/**
|
|
* @dev Checks whether the signature provided is valid for the provided data, hash. Will revert otherwise.
|
|
* @param dataHash Hash of the data (could be either a message hash or transaction hash)
|
|
* @param signatures Signature data that should be verified. Can be ECDSA signature, contract signature (EIP-1271) or approved hash.
|
|
*/
|
|
function checkSignatures(
|
|
bytes32 dataHash,
|
|
bytes memory signatures
|
|
) public view virtual {
|
|
require(signatures.length >= 65, "Invalid signatures length");
|
|
uint8 v;
|
|
bytes32 r;
|
|
bytes32 s;
|
|
address _signer;
|
|
(v, r, s) = _signatureSplit(signatures);
|
|
if (v == 0) {
|
|
// If v is 0 then it is a contract signature
|
|
// When handling contract signatures the address of the signer contract is encoded into r
|
|
_signer = address(uint160(uint256(r)));
|
|
|
|
// Check that signature data pointer (s) is not pointing inside the static part of the signatures bytes
|
|
// Here we check that the pointer is not pointing inside the part that is being processed
|
|
if (uint256(s) < 65)
|
|
revert WrongContractSignatureFormat(uint256(s), 0, 0);
|
|
|
|
// Check if the contract signature is in bounds: start of data is s + 32 and end is start + signature length
|
|
uint256 contractSignatureLen;
|
|
|
|
assembly {
|
|
contractSignatureLen := mload(add(add(signatures, s), 0x20))
|
|
}
|
|
if (uint256(s) + 32 + contractSignatureLen > signatures.length)
|
|
revert WrongContractSignatureFormat(
|
|
uint256(s),
|
|
contractSignatureLen,
|
|
signatures.length
|
|
);
|
|
|
|
// Check signature
|
|
bytes memory contractSignature;
|
|
|
|
assembly {
|
|
// The signature data for contract signatures is appended to the concatenated signatures and the offset is stored in s
|
|
contractSignature := add(add(signatures, s), 0x20)
|
|
}
|
|
if (
|
|
ISignatureValidator(_signer).isValidSignature(
|
|
dataHash,
|
|
contractSignature
|
|
) != EIP1271_MAGIC_VALUE
|
|
) revert WrongContractSignature(contractSignature);
|
|
} else if (v > 30) {
|
|
// If v > 30 then default va (27,28) has been adjusted for eth_sign flow
|
|
// To support eth_sign and similar we adjust v and hash the messageHash with the Ethereum message prefix before applying ecrecover
|
|
(_signer, ) = dataHash.toEthSignedMessageHash().tryRecover(
|
|
v - 4,
|
|
r,
|
|
s
|
|
);
|
|
} else {
|
|
(_signer, ) = dataHash.tryRecover(v, r, s);
|
|
}
|
|
if (_signer != owner) revert InvalidSignature();
|
|
}
|
|
|
|
/**
|
|
* @dev Allows to estimate a transaction.
|
|
* This method is only meant for estimation purpose, therefore the call will always revert and encode the result in the revert data.
|
|
* Since the `estimateGas` function includes refunds, call this method to get an estimated of the costs that are deducted from the wallet with `execTransaction`
|
|
* @param to Destination address of the transaction.
|
|
* @param value Ether value of transaction.
|
|
* @param data Data payload of transaction.
|
|
* @param operation Operation type of transaction.
|
|
* @return Estimate without refunds and overhead fees (base transaction and payload data gas costs).
|
|
*/
|
|
function requiredTxGas(
|
|
address to,
|
|
uint256 value,
|
|
bytes calldata data,
|
|
Enum.Operation operation
|
|
) external returns (uint256) {
|
|
uint256 startGas = gasleft();
|
|
// We don't provide an error message here, as we use it to return the estimate
|
|
if (!_execute(to, value, data, operation, gasleft()))
|
|
revert ExecutionFailed();
|
|
// Convert response to string and return via error message
|
|
unchecked {
|
|
revert(string(abi.encodePacked(startGas - gasleft())));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @dev Returns hash to be signed by owner.
|
|
* @param to Destination address.
|
|
* @param value Ether value.
|
|
* @param data Data payload.
|
|
* @param operation Operation type.
|
|
* @param targetTxGas Fas that should be used for the internal Smart Account transaction.
|
|
* @param baseGas Additional Gas costs for data used to trigger the transaction.
|
|
* @param gasPrice Maximum gas price/ token gas price that should be used for this transaction.
|
|
* @param tokenGasPriceFactor factor by which calculated token gas price is already multiplied.
|
|
* @param gasToken Token address (or 0 if ETH) that is used for the payment.
|
|
* @param refundReceiver Address of receiver of gas payment (or 0 if tx.origin).
|
|
* @param _nonce Transaction nonce.
|
|
* @return Transaction hash.
|
|
*/
|
|
function getTransactionHash(
|
|
address to,
|
|
uint256 value,
|
|
bytes calldata data,
|
|
Enum.Operation operation,
|
|
uint256 targetTxGas,
|
|
uint256 baseGas,
|
|
uint256 gasPrice,
|
|
uint256 tokenGasPriceFactor,
|
|
address gasToken,
|
|
address payable refundReceiver,
|
|
uint256 _nonce
|
|
) public view returns (bytes32) {
|
|
Transaction memory _tx = Transaction({
|
|
to: to,
|
|
value: value,
|
|
data: data,
|
|
operation: operation,
|
|
targetTxGas: targetTxGas
|
|
});
|
|
FeeRefund memory refundInfo = FeeRefund({
|
|
baseGas: baseGas,
|
|
gasPrice: gasPrice,
|
|
tokenGasPriceFactor: tokenGasPriceFactor,
|
|
gasToken: gasToken,
|
|
refundReceiver: refundReceiver
|
|
});
|
|
return keccak256(encodeTransactionData(_tx, refundInfo, _nonce));
|
|
}
|
|
|
|
/**
|
|
* @dev Returns the bytes that are hashed to be signed by owner.
|
|
* @param _tx The wallet transaction to be signed.
|
|
* @param refundInfo Required information for gas refunds.
|
|
* @param _nonce Transaction nonce.
|
|
* @return transactionHash bytes that are hashed to be signed by the owner.
|
|
*/
|
|
function encodeTransactionData(
|
|
Transaction memory _tx,
|
|
FeeRefund memory refundInfo,
|
|
uint256 _nonce
|
|
) public view returns (bytes memory) {
|
|
bytes32 accountTxHash = keccak256(
|
|
abi.encode(
|
|
ACCOUNT_TX_TYPEHASH,
|
|
_tx.to,
|
|
_tx.value,
|
|
keccak256(_tx.data),
|
|
_tx.operation,
|
|
_tx.targetTxGas,
|
|
refundInfo.baseGas,
|
|
refundInfo.gasPrice,
|
|
refundInfo.tokenGasPriceFactor,
|
|
refundInfo.gasToken,
|
|
refundInfo.refundReceiver,
|
|
_nonce
|
|
)
|
|
);
|
|
return
|
|
bytes.concat(
|
|
bytes1(0x19),
|
|
bytes1(0x01),
|
|
domainSeparator(),
|
|
accountTxHash
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dev Utility method to be able to transfer native tokens out of Smart Account
|
|
* @notice only owner/ signatory of Smart Account with enough gas to spend can call this method
|
|
* @notice While enabling multisig module and renouncing ownership this will not work
|
|
* @param dest Destination address
|
|
* @param amount Amount of native tokens
|
|
*/
|
|
function transfer(address payable dest, uint256 amount) external onlyOwner {
|
|
if (dest == address(0)) revert TransferToZeroAddressAttempt();
|
|
bool success;
|
|
assembly {
|
|
success := call(gas(), dest, amount, 0, 0, 0, 0)
|
|
}
|
|
if (!success) revert TokenTransferFailed(address(0), dest, amount);
|
|
}
|
|
|
|
/**
|
|
* @dev Utility method to be able to transfer ERC20 tokens out of Smart Account
|
|
* @notice only owner/ signatory of Smart Account with enough gas to spend can call this method
|
|
* @notice While enabling multisig module and renouncing ownership this will not work
|
|
* @param token Token address
|
|
* @param dest Destination/ Receiver address
|
|
* @param amount Amount of tokens
|
|
*/
|
|
function pullTokens(
|
|
address token,
|
|
address dest,
|
|
uint256 amount
|
|
) external onlyOwner {
|
|
if (dest == address(0)) revert TransferToZeroAddressAttempt();
|
|
if (!_transferToken(token, dest, amount))
|
|
revert TokenTransferFailed(token, dest, amount);
|
|
}
|
|
|
|
/**
|
|
* @dev Execute a transaction (called directly from owner, or 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 executeCall_s1m(
|
|
address dest,
|
|
uint256 value,
|
|
bytes calldata func
|
|
) public {
|
|
_requireFromEntryPointOrOwner();
|
|
_call(dest, value, func);
|
|
}
|
|
|
|
/**
|
|
* @dev Interface function with the standard name for executeCall_s1m
|
|
* @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 executeCall(
|
|
address dest,
|
|
uint256 value,
|
|
bytes calldata func
|
|
) external {
|
|
executeCall_s1m(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 executeBatchCall_4by(
|
|
address[] calldata dest,
|
|
uint256[] calldata value,
|
|
bytes[] calldata func
|
|
) public {
|
|
_requireFromEntryPointOrOwner();
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @dev Interface function with the standard name for executeBatchCall_4by
|
|
* @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 executeBatchCall(
|
|
address[] calldata dest,
|
|
uint256[] calldata value,
|
|
bytes[] calldata func
|
|
) external {
|
|
executeBatchCall_4by(dest, value, func);
|
|
}
|
|
|
|
/**
|
|
* Implementation of ISignatureValidator (see `interfaces/ISignatureValidator.sol`)
|
|
* @dev If owner is a smart-contract (other smart contract wallet or module, that controls
|
|
* signature verifications - like multisig), forward isValidSignature request to it.
|
|
* In case of multisig, _signature can be several concatenated signatures
|
|
* If owner is EOA, perform a regular ecrecover.
|
|
* @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) {
|
|
if (owner.code.length > 0) {
|
|
return
|
|
ISignatureValidator(owner).isValidSignature(
|
|
_dataHash,
|
|
_signature
|
|
);
|
|
}
|
|
if (owner == _dataHash.recover(_signature)) {
|
|
return EIP1271_MAGIC_VALUE;
|
|
}
|
|
return bytes4(0xffffffff);
|
|
}
|
|
|
|
/**
|
|
* @dev Check current account deposit in the entryPoint
|
|
*/
|
|
function getDeposit() public view returns (uint256) {
|
|
return entryPoint().balanceOf(address(this));
|
|
}
|
|
|
|
/**
|
|
* @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 onlyOwner {
|
|
entryPoint().withdrawTo(withdrawAddress, amount);
|
|
}
|
|
|
|
/**
|
|
* @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
|
|
}
|
|
|
|
/**
|
|
* @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 This function allows the owner or entry point to execute certain actions.
|
|
* If the caller is not authorized, the function will revert with an error message.
|
|
* @notice This modifier is marked as internal and can only be called within the contract itself.
|
|
*/
|
|
function _requireFromEntryPointOrOwner() internal view {
|
|
if (msg.sender != address(entryPoint()) && msg.sender != owner)
|
|
revert CallerIsNotEntryPointOrOwner(msg.sender);
|
|
}
|
|
|
|
/**
|
|
* @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 Implements the template method of BaseAccount and validates the user's signature for a given operation.
|
|
* @notice This function is marked as internal and virtual, and it overrides the BaseAccount function of the same name.
|
|
* @param userOp The user operation to be validated, provided as a `UserOperation` calldata struct.
|
|
* @param userOpHash The hashed version of the user operation, provided as a `bytes32` value.
|
|
*/
|
|
function _validateSignature(
|
|
UserOperation calldata userOp,
|
|
bytes32 userOpHash
|
|
) internal virtual override returns (uint256 validationData) {
|
|
// below changes need formal verification.
|
|
bytes calldata userOpData = userOp.callData;
|
|
if (userOpData.length > 0) {
|
|
bytes4 methodSig = bytes4(userOpData[:4]);
|
|
// If method to be called is executeCall then only check for module transaction
|
|
if (methodSig == this.executeCall.selector) {
|
|
(address _to, , ) = abi.decode(
|
|
userOpData[4:],
|
|
(address, uint, bytes)
|
|
);
|
|
if (address(_modules[_to]) != address(0))
|
|
return IModule(_to).validateSignature(userOp, userOpHash);
|
|
}
|
|
}
|
|
bytes32 hash = userOpHash.toEthSignedMessageHash();
|
|
if (owner != hash.recover(userOp.signature))
|
|
return SIG_VALIDATION_FAILED;
|
|
return 0;
|
|
}
|
|
}
|