// SPDX-License-Identifier: MIT pragma solidity =0.8.16; import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {ECDSAUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol"; import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol"; import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; import {IL1MessageQueue} from "../rollup/IL1MessageQueue.sol"; // solhint-disable reason-string contract EnforcedTxGateway is OwnableUpgradeable, ReentrancyGuardUpgradeable, PausableUpgradeable, EIP712Upgradeable { /********** * Events * **********/ /// @notice Emitted when owner updates fee vault contract. /// @param _oldFeeVault The address of old fee vault contract. /// @param _newFeeVault The address of new fee vault contract. event UpdateFeeVault(address _oldFeeVault, address _newFeeVault); /************* * Constants * *************/ // solhint-disable-next-line var-name-mixedcase bytes32 private constant _ENFORCED_TX_TYPEHASH = keccak256( "EnforcedTransaction(address sender,address target,uint256 value,uint256 gasLimit,bytes data,uint256 nonce,uint256 deadline)" ); /************* * Variables * *************/ /// @notice The address of L1MessageQueue contract. address public messageQueue; /// @notice The address of fee vault contract. address public feeVault; /// @notice Mapping from EOA address to current nonce. /// @dev Every successful call to `sendTransaction` with signature increases `_sender`'s nonce by one. /// This prevents a signature from being used multiple times. mapping(address => uint256) public nonces; /*************** * Constructor * ***************/ constructor() { _disableInitializers(); } function initialize(address _queue, address _feeVault) external initializer { OwnableUpgradeable.__Ownable_init(); ReentrancyGuardUpgradeable.__ReentrancyGuard_init(); PausableUpgradeable.__Pausable_init(); EIP712Upgradeable.__EIP712_init("EnforcedTxGateway", "1"); messageQueue = _queue; feeVault = _feeVault; } /************************* * Public View Functions * *************************/ /// @notice return the domain separator for the typed transaction // solhint-disable-next-line func-name-mixedcase function DOMAIN_SEPARATOR() external view returns (bytes32) { return _domainSeparatorV4(); } /***************************** * Public Mutating Functions * *****************************/ /// @notice Add an enforced transaction to L2. /// @dev The caller should be EOA only. /// @param _target The address of target contract to call in L2. /// @param _value The value passed /// @param _gasLimit The maximum gas should be used for this transaction in L2. /// @param _data The calldata passed to target contract. function sendTransaction( address _target, uint256 _value, uint256 _gasLimit, bytes calldata _data ) external payable whenNotPaused { // solhint-disable-next-line avoid-tx-origin require(msg.sender == tx.origin, "Only EOA senders are allowed to send enforced transaction"); _sendTransaction(msg.sender, _target, _value, _gasLimit, _data, msg.sender); } /// @notice Add an enforced transaction to L2. /// @dev The `_sender` should be EOA and match with the signature. /// @param _sender The address of sender who will initiate this transaction in L2. /// @param _target The address of target contract to call in L2. /// @param _value The value passed /// @param _gasLimit The maximum gas should be used for this transaction in L2. /// @param _data The calldata passed to target contract. /// @param _deadline The deadline of the signature. /// @param _signature The signature for the transaction. /// @param _refundAddress The address to refund exceeded fee. function sendTransaction( address _sender, address _target, uint256 _value, uint256 _gasLimit, bytes calldata _data, uint256 _deadline, bytes memory _signature, address _refundAddress ) external payable whenNotPaused { // solhint-disable-next-line not-rely-on-time require(block.timestamp <= _deadline, "signature expired"); uint256 _nonce = nonces[_sender]; bytes32 _structHash = keccak256( abi.encode(_ENFORCED_TX_TYPEHASH, _sender, _target, _value, _gasLimit, keccak256(_data), _nonce, _deadline) ); unchecked { nonces[_sender] = _nonce + 1; } bytes32 _hash = _hashTypedDataV4(_structHash); address _signer = ECDSAUpgradeable.recover(_hash, _signature); // no need to check `_signer != address(0)`, since it is checked in `recover`. require(_signer == _sender, "Incorrect signature"); _sendTransaction(_sender, _target, _value, _gasLimit, _data, _refundAddress); } /************************ * Restricted Functions * ************************/ /// @notice Update the address of fee vault. /// @param _newFeeVault The address to update. function updateFeeVault(address _newFeeVault) external onlyOwner { address _oldFeeVault = feeVault; feeVault = _newFeeVault; emit UpdateFeeVault(_oldFeeVault, _newFeeVault); } /// @notice Pause or unpause this contract. /// @param _status Pause this contract if it is true, otherwise unpause this contract. function setPause(bool _status) external onlyOwner { if (_status) { _pause(); } else { _unpause(); } } /********************** * Internal Functions * **********************/ /// @dev Internal function to charge fee and add enforced transaction. /// @param _sender The address of sender who will initiate this transaction in L2. /// @param _target The address of target contract to call in L2. /// @param _value The value passed /// @param _gasLimit The maximum gas should be used for this transaction in L2. /// @param _data The calldata passed to target contract. /// @param _refundAddress The address to refund exceeded fee. function _sendTransaction( address _sender, address _target, uint256 _value, uint256 _gasLimit, bytes calldata _data, address _refundAddress ) internal nonReentrant { address _messageQueue = messageQueue; // charge fee uint256 _fee = IL1MessageQueue(_messageQueue).estimateCrossDomainMessageFee(_gasLimit); require(msg.value >= _fee, "Insufficient value for fee"); if (_fee > 0) { (bool _success, ) = feeVault.call{value: _fee}(""); require(_success, "Failed to deduct the fee"); } // append transaction IL1MessageQueue(_messageQueue).appendEnforcedTransaction(_sender, _target, _value, _gasLimit, _data); // refund fee to `_refundAddress` unchecked { uint256 _refund = msg.value - _fee; if (_refund > 0) { (bool _success, ) = _refundAddress.call{value: _refund}(""); require(_success, "Failed to refund the fee"); } } } }