mirror of
https://github.com/luxfi/eoa-contracts.git
synced 2026-10-30 16:58:32 +00:00
448 lines
16 KiB
Solidity
448 lines
16 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity 0.8.17;
|
|
|
|
import {ISignatureValidator, ISignatureValidatorConstants} from "../interfaces/ISignatureValidator.sol";
|
|
import {Enum} from "../common/Enum.sol";
|
|
import {ReentrancyGuard} from "../common/ReentrancyGuard.sol";
|
|
import {Math} from "../libs/Math.sol";
|
|
|
|
struct Transaction {
|
|
address to;
|
|
Enum.Operation operation;
|
|
uint256 value;
|
|
bytes data;
|
|
uint256 targetTxGas;
|
|
}
|
|
|
|
struct FeeRefund {
|
|
uint256 baseGas;
|
|
uint256 gasPrice; //gasPrice or tokenGasPrice
|
|
uint256 tokenGasPriceFactor;
|
|
address gasToken;
|
|
address payable refundReceiver;
|
|
}
|
|
|
|
/**
|
|
* @notice Throws when the address that signed the data (restored from signature)
|
|
* differs from the address we expected to sign the data (i.e. some authorized address)
|
|
*/
|
|
error InvalidSignature();
|
|
|
|
/**
|
|
* @notice Throws if not enough gas is left at some point
|
|
* @param gasLeft how much gas left at the moment of a check
|
|
* @param gasRequired how much gas required to proceed
|
|
*/
|
|
error NotEnoughGasLeft(uint256 gasLeft, uint256 gasRequired);
|
|
|
|
/**
|
|
* @notice Throws if not able to estimate gas
|
|
* It can be when amount of gas and its price are both zero and at the same time
|
|
* transaction has failed to be executed
|
|
* @param targetTxGas gas required for target transaction
|
|
* @param gasPrice gas price passed in Refund Info
|
|
* @param success whether transaction has been executed successfully or not
|
|
*/
|
|
error CanNotEstimateGas(uint256 targetTxGas, uint256 gasPrice, bool success);
|
|
|
|
/**
|
|
* @notice Throws if transfer of tokens failed
|
|
* @param token token contract address
|
|
* @param dest token transfer receiver
|
|
* @param amount the amount of tokens in a failed transfer
|
|
*/
|
|
error TokenTransferFailed(address token, address dest, uint256 amount);
|
|
|
|
/**
|
|
* @notice Thrown when trying to use 0 as tokenGasPriceFactor
|
|
*/
|
|
error TokenGasPriceFactorCanNotBeZero();
|
|
|
|
/**
|
|
* @notice Throws when the transaction execution fails
|
|
*/
|
|
error ExecutionFailed();
|
|
|
|
interface IExecFromModule {
|
|
function execTransactionFromModule(
|
|
address to,
|
|
uint256 value,
|
|
bytes memory data,
|
|
Enum.Operation operation,
|
|
uint256 txGas
|
|
) external returns (bool success);
|
|
|
|
function execTransactionFromModule(
|
|
address to,
|
|
uint256 value,
|
|
bytes memory data,
|
|
Enum.Operation operation
|
|
) external returns (bool success);
|
|
}
|
|
|
|
contract ForwardFlowModule is ReentrancyGuard, ISignatureValidatorConstants {
|
|
// Domain Seperators keccak256("EIP712Domain(uint256 chainId,address verifyingContract)");
|
|
bytes32 internal constant DOMAIN_SEPARATOR_TYPEHASH =
|
|
0x47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218;
|
|
|
|
// solhint-disable-next-line
|
|
// 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;
|
|
|
|
uint256 private immutable CHAIN_ID;
|
|
|
|
mapping(uint256 => uint256) public nonces;
|
|
|
|
event AccountHandlePayment(bytes32 indexed txHash, uint256 indexed payment);
|
|
|
|
constructor() {
|
|
CHAIN_ID = block.chainid;
|
|
}
|
|
|
|
/**
|
|
* @dev Allows to estimate a transaction.
|
|
* @notice This method is for estimation only, it 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(
|
|
address smartAccount,
|
|
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);
|
|
if (
|
|
!IExecFromModule(smartAccount).execTransactionFromModule(
|
|
receiver,
|
|
payment,
|
|
"0x",
|
|
Enum.Operation.Call,
|
|
0
|
|
)
|
|
) {
|
|
revert TokenTransferFailed(address(0), receiver, payment);
|
|
}
|
|
} else {
|
|
uint256 payment = ((gasUsed + baseGas) * (gasPrice)) /
|
|
(tokenGasPriceFactor);
|
|
if (
|
|
!IExecFromModule(smartAccount).execTransactionFromModule(
|
|
gasToken,
|
|
0,
|
|
abi.encodeWithSignature(
|
|
"transfer(address,uint256)",
|
|
receiver,
|
|
payment
|
|
),
|
|
Enum.Operation.Call,
|
|
0
|
|
)
|
|
) {
|
|
revert TokenTransferFailed(gasToken, receiver, payment);
|
|
}
|
|
}
|
|
unchecked {
|
|
requiredGas = startGas - gasleft();
|
|
}
|
|
revert(string(abi.encodePacked(requiredGas)));
|
|
}
|
|
|
|
/**
|
|
* @dev Allows to estimate a transaction.
|
|
* This method is for estimation only, it will always revert and encode the result in the revert data.
|
|
* Call this method to get an estimate of the handlePayment costs that are deducted 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 smartAccount,
|
|
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 (
|
|
!IExecFromModule(smartAccount).execTransactionFromModule(
|
|
to,
|
|
value,
|
|
data,
|
|
operation
|
|
)
|
|
) revert ExecutionFailed();
|
|
// Convert response to string and return via error message
|
|
unchecked {
|
|
revert(string(abi.encodePacked(startGas - gasleft())));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @dev Safe (ex-Gnosis) style transaction with optional repay in native tokens or ERC20
|
|
* @dev Execute a transaction confirmed by required signature/s and then pays the account that submitted it.
|
|
* @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})
|
|
* Should be a signature over Typed Data Hash
|
|
* Use eth_signTypedData, not a personal_sign
|
|
*/
|
|
|
|
function execTransaction(
|
|
address smartAccount,
|
|
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(
|
|
// Smart Account to execute Transaction
|
|
smartAccount,
|
|
// Transaction info
|
|
_tx,
|
|
// Payment info
|
|
refundInfo,
|
|
// Signature info
|
|
nonces[1]++
|
|
);
|
|
|
|
txHash = keccak256(txHashData);
|
|
if (
|
|
ISignatureValidator(smartAccount).isValidSignature(
|
|
txHash,
|
|
signatures
|
|
) != EIP1271_MAGIC_VALUE
|
|
) {
|
|
revert InvalidSignature();
|
|
}
|
|
}
|
|
|
|
// We require some gas to emit the events (at least 2500) after the execution and some to
|
|
// perform code until the execution (7500 = call the external function + checks inside it)
|
|
// 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) + 7500
|
|
)
|
|
revert NotEnoughGasLeft(
|
|
gasleft(),
|
|
Math.max((_tx.targetTxGas << 6) / 63, _tx.targetTxGas + 2500) +
|
|
7500
|
|
);
|
|
// Use scope here to limit variable lifetime and prevent `stack too deep` errors
|
|
{
|
|
//we always provide targetTxGas to execution
|
|
success = IExecFromModule(smartAccount).execTransactionFromModule(
|
|
_tx.to,
|
|
_tx.value,
|
|
_tx.data,
|
|
_tx.operation,
|
|
_tx.targetTxGas
|
|
);
|
|
|
|
// If targetTxGas and gasPrice are both 0, the internal tx must succeed.
|
|
// Enables safe use of `estimateGas` by finding the minimum gas where the transaction doesn't revert
|
|
if (!success && _tx.targetTxGas == 0 && refundInfo.gasPrice == 0)
|
|
revert CanNotEstimateGas(
|
|
_tx.targetTxGas,
|
|
refundInfo.gasPrice,
|
|
success
|
|
);
|
|
|
|
// Transfer transaction costs to tx.origin to avoid intermediate contract payments.
|
|
uint256 payment;
|
|
if (refundInfo.gasPrice != 0) {
|
|
payment = _handlePayment(
|
|
smartAccount,
|
|
startGas - gasleft(),
|
|
refundInfo.baseGas,
|
|
refundInfo.gasPrice,
|
|
refundInfo.tokenGasPriceFactor,
|
|
refundInfo.gasToken,
|
|
refundInfo.refundReceiver
|
|
);
|
|
emit AccountHandlePayment(txHash, payment);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @dev Returns hash to be signed by owner.
|
|
* @param _nonce Transaction nonce.
|
|
* @param smartAccount Address of the Smart Account to execute the txn.
|
|
* @return Transaction hash.
|
|
*/
|
|
function getTransactionHash(
|
|
Transaction calldata _tx,
|
|
FeeRefund calldata refundInfo,
|
|
uint256 _nonce,
|
|
address smartAccount
|
|
) public view returns (bytes32) {
|
|
return
|
|
keccak256(
|
|
encodeTransactionData(smartAccount, _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(
|
|
address smartAccount,
|
|
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(smartAccount),
|
|
accountTxHash
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @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 domain separator for this contract, as defined in the EIP-712 standard.
|
|
* @return bytes32 The domain separator hash.
|
|
*/
|
|
function domainSeparator(
|
|
address smartAccount
|
|
) public view returns (bytes32) {
|
|
return
|
|
keccak256(
|
|
abi.encode(DOMAIN_SEPARATOR_TYPEHASH, CHAIN_ID, smartAccount)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @notice Returns the ID of the chain the contract is currently deployed on.
|
|
* @return CHAIN_ID The ID of the current chain as a uint256.
|
|
*/
|
|
function getChainId() public view returns (uint256) {
|
|
return CHAIN_ID;
|
|
}
|
|
|
|
/**
|
|
* @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(
|
|
address smartAccount,
|
|
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);
|
|
if (
|
|
!IExecFromModule(smartAccount).execTransactionFromModule(
|
|
receiver,
|
|
payment,
|
|
"0x",
|
|
Enum.Operation.Call,
|
|
0
|
|
)
|
|
) {
|
|
revert TokenTransferFailed(address(0), receiver, payment);
|
|
}
|
|
} else {
|
|
payment =
|
|
((gasUsed + baseGas) * (gasPrice)) /
|
|
(tokenGasPriceFactor);
|
|
if (
|
|
!IExecFromModule(smartAccount).execTransactionFromModule(
|
|
gasToken,
|
|
0,
|
|
abi.encodeWithSignature(
|
|
"transfer(address,uint256)",
|
|
receiver,
|
|
payment
|
|
),
|
|
Enum.Operation.Call,
|
|
0
|
|
)
|
|
) {
|
|
revert TokenTransferFailed(gasToken, receiver, payment);
|
|
}
|
|
}
|
|
}
|
|
}
|