diff --git a/contracts/ZkLink.sol b/contracts/ZkLink.sol index 2d2cc8d..993258c 100644 --- a/contracts/ZkLink.sol +++ b/contracts/ZkLink.sol @@ -7,6 +7,8 @@ import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/U import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {AddressAliasHelper} from "./zksync/l1-contracts/vendor/AddressAliasHelper.sol"; import {IZkLink} from "./interfaces/IZkLink.sol"; import {IL2Gateway} from "./interfaces/IL2Gateway.sol"; @@ -35,6 +37,7 @@ contract ZkLink is PausableUpgradeable { using UncheckedMath for uint256; + using SafeERC20 for IERC20; /// @dev The forward request type hash bytes32 public constant FORWARD_REQUEST_TYPE_HASH = @@ -45,9 +48,6 @@ contract ZkLink is /// @dev The length of withdraw message sent to secondary chain uint256 private constant L2_WITHDRAW_MESSAGE_LENGTH = 108; - /// @dev Whether eth is the gas token - bool public immutable IS_ETH_GAS_TOKEN; - /// @notice The gateway is used for communicating with L1 IL2Gateway public gateway; /// @notice List of permitted validators @@ -148,8 +148,7 @@ contract ZkLink is _; } - constructor(bool _isEthGasToken) { - IS_ETH_GAS_TOKEN = _isEthGasToken; + constructor() { _disableInitializers(); } @@ -275,7 +274,7 @@ contract ZkLink is address _refundRecipient ) external payable nonReentrant whenNotPaused returns (bytes32 canonicalTxHash) { // Disable l2 value if eth is not the gas token - if (!IS_ETH_GAS_TOKEN) { + if (!gateway.isEthGasToken()) { require(_l2Value == 0, "Not allow l2 value"); } // Change the sender address if it is a smart contract to prevent address collision between L1 and L2. @@ -365,7 +364,6 @@ contract ZkLink is bytes calldata _message, bytes32[] calldata _merkleProof ) external nonReentrant { - require(IS_ETH_GAS_TOKEN, "Not allow eth withdraw"); require(!isEthWithdrawalFinalized[_l2BatchNumber][_l2MessageIndex], "jj"); L2Message memory l2ToL1Message = L2Message({ @@ -461,7 +459,9 @@ contract ZkLink is bytes32 _l2LogsRootHash, uint256 _forwardEthAmount ) external payable onlyGateway { - require(msg.value == _forwardEthAmount, "Invalid forward amount"); + if (gateway.isEthGasToken()) { + require(msg.value == _forwardEthAmount, "Invalid forward amount"); + } // Allows repeated sending of the forward amount of the batch if (_batchNumber > totalBatchesExecuted) { totalBatchesExecuted = _batchNumber; @@ -477,7 +477,9 @@ contract ZkLink is uint256 _forwardEthAmount ) external payable onlyGateway { require(_toBatchNumber >= _fromBatchNumber, "Invalid range"); - require(msg.value == _forwardEthAmount, "Invalid forward amount"); + if (gateway.isEthGasToken()) { + require(msg.value == _forwardEthAmount, "Invalid forward amount"); + } bytes32 range = keccak256(abi.encodePacked(_fromBatchNumber, _toBatchNumber)); rangeBatchRootHashes[range] = _rangeBatchRootHash; emit SyncRangeBatchRoot(_fromBatchNumber, _toBatchNumber, _rangeBatchRootHash, _forwardEthAmount); @@ -644,12 +646,16 @@ contract ZkLink is /// @notice Transfer ether from the contract to the receiver /// @dev Reverts only if the transfer call failed function _withdrawFunds(address _to, uint256 _amount) internal { - bool callSuccess; - // Low-level assembly call, to avoid any memory copying (save gas) - assembly { - callSuccess := call(gas(), _to, _amount, 0, 0, 0, 0) + if (gateway.isEthGasToken()) { + bool callSuccess; + // Low-level assembly call, to avoid any memory copying (save gas) + assembly { + callSuccess := call(gas(), _to, _amount, 0, 0, 0, 0) + } + require(callSuccess, "pz"); + } else { + SafeERC20.safeTransfer(gateway.ethToken(), _to, _amount); } - require(callSuccess, "pz"); } function hashForwardL2Request(ForwardL2Request memory _request) internal pure returns (bytes32) { diff --git a/contracts/dev-contracts/DummyZkLink.sol b/contracts/dev-contracts/DummyZkLink.sol index 4888416..8ba24be 100644 --- a/contracts/dev-contracts/DummyZkLink.sol +++ b/contracts/dev-contracts/DummyZkLink.sol @@ -9,7 +9,6 @@ import {IL2Gateway} from "../interfaces/IL2Gateway.sol"; import {IZkLink} from "../interfaces/IZkLink.sol"; contract DummyZkLink is IZkLink, OwnableUpgradeable, UUPSUpgradeable, ReentrancyGuardUpgradeable { - bool public immutable IS_ETH_GAS_TOKEN; IL2Gateway public gateway; event ReceiveBatchRoot(uint256 batchNumber, bytes32 l2LogsRootHash, uint256 forwardEthAmount); @@ -26,8 +25,7 @@ contract DummyZkLink is IZkLink, OwnableUpgradeable, UUPSUpgradeable, Reentrancy _; } - constructor(bool _isEthGasToken) { - IS_ETH_GAS_TOKEN = _isEthGasToken; + constructor() { _disableInitializers(); } diff --git a/contracts/gateway/L2BaseGateway.sol b/contracts/gateway/L2BaseGateway.sol index fc37d0d..21924d1 100644 --- a/contracts/gateway/L2BaseGateway.sol +++ b/contracts/gateway/L2BaseGateway.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; import {IL2Gateway} from "../interfaces/IL2Gateway.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; abstract contract L2BaseGateway is IL2Gateway { /// @notice The zkLink contract @@ -23,4 +24,12 @@ abstract contract L2BaseGateway is IL2Gateway { constructor(address _zkLink) { ZKLINK = _zkLink; } + + function isEthGasToken() external pure virtual returns (bool) { + return true; + } + + function ethToken() external pure virtual returns (IERC20) { + return IERC20(address(0)); + } } diff --git a/contracts/gateway/optimism/MantleL2Gateway.sol b/contracts/gateway/optimism/MantleL2Gateway.sol new file mode 100644 index 0000000..79eed0f --- /dev/null +++ b/contracts/gateway/optimism/MantleL2Gateway.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.0; + +import {OptimismL2Gateway} from "./OptimismL2Gateway.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +contract MantleL2Gateway is OptimismL2Gateway { + using SafeERC20 for IERC20; + + /// @dev The ETH token deployed on Mantle + IERC20 private constant BVM_ETH = IERC20(0xdEAddEaDdeadDEadDEADDEAddEADDEAddead1111); + + constructor(address _zkLink) OptimismL2Gateway(_zkLink) { + _disableInitializers(); + } + + function isEthGasToken() external pure override returns (bool) { + return false; + } + + function ethToken() external pure override returns (IERC20) { + return BVM_ETH; + } + + function claimMessageCallback( + uint256 _ethValue, + bytes calldata _callData + ) external payable override onlyMessageService onlyRemoteGateway { + if (_ethValue > 0) { + // Mantle L2CrossDomainMessenger will approve l2 gateway before the callback in `relayMessage` + SafeERC20.safeTransferFrom(BVM_ETH, address(MESSAGE_SERVICE), address(ZKLINK), _ethValue); + } + // solhint-disable-next-line avoid-low-level-calls + (bool success, ) = ZKLINK.call{value: 0}(_callData); + require(success, "Call zkLink failed"); + } +} diff --git a/contracts/gateway/optimism/OptimismL2Gateway.sol b/contracts/gateway/optimism/OptimismL2Gateway.sol index 1f0a232..029178a 100644 --- a/contracts/gateway/optimism/OptimismL2Gateway.sol +++ b/contracts/gateway/optimism/OptimismL2Gateway.sol @@ -33,7 +33,7 @@ contract OptimismL2Gateway is L2BaseGateway, OptimismGateway { function claimMessageCallback( uint256 _value, bytes calldata _callData - ) external payable onlyMessageService onlyRemoteGateway { + ) external payable virtual onlyMessageService onlyRemoteGateway { require(msg.value == _value, "Invalid value"); // solhint-disable-next-line avoid-low-level-calls diff --git a/contracts/interfaces/IL2Gateway.sol b/contracts/interfaces/IL2Gateway.sol index 8035b0a..8063434 100644 --- a/contracts/interfaces/IL2Gateway.sol +++ b/contracts/interfaces/IL2Gateway.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; import {IGateway} from "./IGateway.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; interface IL2Gateway is IGateway { /// @notice Emit when sending a message @@ -11,4 +12,8 @@ interface IL2Gateway is IGateway { /// @param _value The msg value /// @param _callData The call data function sendMessage(uint256 _value, bytes calldata _callData) external payable; + + function isEthGasToken() external view returns (bool); + + function ethToken() external view returns (IERC20); } diff --git a/script/ChainConfig.json b/script/ChainConfig.json index 5a21c83..66d3b17 100644 --- a/script/ChainConfig.json +++ b/script/ChainConfig.json @@ -1,9 +1,7 @@ { "ETHEREUM": { - "eth": true }, "ZKSYNC": { - "eth": true, "l2Gateway": { "contractName": "ZkSyncL2Gateway", "constructParams": [], @@ -17,7 +15,6 @@ } }, "SCROLL": { - "eth": true, "l2Gateway": { "contractName": "ScrollL2Gateway", "constructParams": ["0x781e90f1c8Fc4611c9b7497C3B47F99Ef6969CbC"], @@ -31,7 +28,6 @@ } }, "LINEA": { - "eth": true, "l2Gateway": { "contractName": "LineaL2Gateway", "constructParams": ["0x508Ca82Df566dCD1B0DE8296e70a96332cD644ec"], @@ -45,7 +41,6 @@ } }, "ZKPOLYGON": { - "eth": true, "l2Gateway": { "contractName": "ZkPolygonL2Gateway", "constructParams": ["0x2a3DD3EB832aF982ec71669E178424b10Dca2EDe"], @@ -59,7 +54,6 @@ } }, "ARBITRUM": { - "eth": true, "l2Gateway": { "contractName": "ArbitrumL2Gateway", "constructParams": [], @@ -73,7 +67,6 @@ } }, "OPTIMISM": { - "eth": true, "l2Gateway": { "contractName": "OptimismL2Gateway", "constructParams": [], @@ -87,7 +80,6 @@ } }, "MANTA": { - "eth": true, "l2Gateway": { "contractName": "OptimismL2Gateway", "constructParams": [], @@ -101,9 +93,8 @@ } }, "MANTLE": { - "eth": false, "l2Gateway": { - "contractName": "OptimismL2Gateway", + "contractName": "MantleL2Gateway", "constructParams": [], "initializeParams": [] }, @@ -115,7 +106,6 @@ } }, "BLAST": { - "eth": true, "l2Gateway": { "contractName": "OptimismL2Gateway", "constructParams": [], @@ -129,7 +119,6 @@ } }, "BASE": { - "eth": true, "l2Gateway": { "contractName": "OptimismL2Gateway", "constructParams": [], @@ -143,7 +132,6 @@ } }, "METIS": { - "eth": false, "l2Gateway": { "contractName": "OptimismL2Gateway", "constructParams": [], @@ -156,14 +144,9 @@ "initializeParams": [] } }, - "GOERLI": { - "eth": true - }, "SEPOLIA": { - "eth": true }, "ZKSYNCTEST": { - "eth": true, "l2Gateway": { "contractName": "ZkSyncL2Gateway", "constructParams": [], @@ -177,7 +160,6 @@ } }, "SCROLLTEST": { - "eth": true, "l2Gateway": { "contractName": "ScrollL2Gateway", "constructParams": ["0xBa50f5340FB9F3Bd074bD638c9BE13eCB36E603d"], @@ -191,7 +173,6 @@ } }, "LINEATEST": { - "eth": true, "l2Gateway": { "contractName": "LineaL2Gateway", "constructParams": ["0x971e727e956690b9957be6d51Ec16E73AcAC83A7"], @@ -205,7 +186,6 @@ } }, "ZKPOLYGONTEST": { - "eth": true, "l2Gateway": { "contractName": "ZkPolygonL2Gateway", "constructParams": ["0xF6BEEeBB578e214CA9E23B0e9683454Ff88Ed2A7"], @@ -219,7 +199,6 @@ } }, "ARBITRUMTEST": { - "eth": true, "l2Gateway": { "contractName": "ArbitrumL2Gateway", "constructParams": [], @@ -233,7 +212,6 @@ } }, "OPTIMISMTEST": { - "eth": true, "l2Gateway": { "contractName": "OptimismL2Gateway", "constructParams": [], @@ -247,7 +225,6 @@ } }, "MANTATEST": { - "eth": true, "l2Gateway": { "contractName": "OptimismL2Gateway", "constructParams": [], @@ -261,9 +238,8 @@ } }, "MANTLETEST": { - "eth": false, "l2Gateway": { - "contractName": "OptimismL2Gateway", + "contractName": "MantleL2Gateway", "constructParams": [], "initializeParams": [] }, @@ -275,7 +251,6 @@ } }, "BLASTTEST": { - "eth": true, "l2Gateway": { "contractName": "OptimismL2Gateway", "constructParams": [], @@ -289,7 +264,6 @@ } }, "BASETEST": { - "eth": true, "l2Gateway": { "contractName": "OptimismL2Gateway", "constructParams": [], diff --git a/script/deploy_log_name.js b/script/deploy_log_name.js index d1113e6..06125a9 100644 --- a/script/deploy_log_name.js +++ b/script/deploy_log_name.js @@ -5,7 +5,6 @@ const DEPLOY_LOG_GOVERNOR = 'governor'; const DEPLOY_LOG_ZKLINK_TARGET = 'zkLinkTarget'; const DEPLOY_LOG_ZKLINK_TARGET_VERIFIED = 'zkLinkTargetVerified'; const DEPLOY_LOG_ZKLINK_PROXY = 'zkLinkProxy'; -const DEPLOY_LOG_ZKLINK_IS_ETH_GAS_TOKEN = 'isETHGasToken'; const DEPLOY_LOG_ZKLINK_PROXY_VERIFIED = 'zkLinkProxyVerified'; const DEPLOY_LOG_DEPLOY_TX_HASH = 'deployTxHash'; const DEPLOY_LOG_DEPLOY_BLOCK_NUMBER = 'deployBlockNumber'; @@ -61,7 +60,6 @@ module.exports = { DEPLOY_LOG_ZKLINK_TARGET_VERIFIED, DEPLOY_LOG_ZKLINK_PROXY, DEPLOY_LOG_ZKLINK_PROXY_VERIFIED, - DEPLOY_LOG_ZKLINK_IS_ETH_GAS_TOKEN, DEPLOY_LOG_DEPLOY_TX_HASH, DEPLOY_LOG_DEPLOY_BLOCK_NUMBER, DEPLOY_L1_GATEWAY_LOG_PREFIX, diff --git a/script/deploy_zklink.js b/script/deploy_zklink.js index 0efcf9d..7aa0071 100644 --- a/script/deploy_zklink.js +++ b/script/deploy_zklink.js @@ -33,8 +33,6 @@ task('deployZkLink', 'Deploy zkLink') console.log('current net not support'); return; } - const isEthGasToken = chainInfo.eth; - console.log(`is eth the gas token of ${netName}?`, isEthGasToken); const contractDeployer = new ChainContractDeployer(hardhat); await contractDeployer.init(); @@ -49,11 +47,10 @@ task('deployZkLink', 'Deploy zkLink') if (!(logName.DEPLOY_LOG_ZKLINK_PROXY in deployLog) || force) { console.log('deploy zkLink...'); const contractName = getZkLinkContractName(dummy); - const contract = await contractDeployer.deployProxy(contractName, [], [isEthGasToken]); + const contract = await contractDeployer.deployProxy(contractName, [], []); const transaction = await getDeployTx(contract); zkLinkAddr = await contract.getAddress(); deployLog[logName.DEPLOY_LOG_ZKLINK_PROXY] = zkLinkAddr; - deployLog[logName.DEPLOY_LOG_ZKLINK_IS_ETH_GAS_TOKEN] = isEthGasToken; deployLog[logName.DEPLOY_LOG_DEPLOY_TX_HASH] = transaction.hash; deployLog[logName.DEPLOY_LOG_DEPLOY_BLOCK_NUMBER] = transaction.blockNumber; fs.writeFileSync(deployLogPath, JSON.stringify(deployLog, null, 2)); @@ -75,7 +72,7 @@ task('deployZkLink', 'Deploy zkLink') // verify target contract if ((!(logName.DEPLOY_LOG_ZKLINK_TARGET_VERIFIED in deployLog) || force) && !skipVerify) { - await verifyContractCode(hardhat, zkLinkTargetAddr, [isEthGasToken]); + await verifyContractCode(hardhat, zkLinkTargetAddr, []); deployLog[logName.DEPLOY_LOG_ZKLINK_TARGET_VERIFIED] = true; fs.writeFileSync(deployLogPath, JSON.stringify(deployLog, null, 2)); } @@ -110,12 +107,6 @@ task('upgradeZkLink', 'Upgrade zkLink') return; } console.log('zkLink old target', oldContractTargetAddr); - const isETHGasToken = deployLog[logName.DEPLOY_LOG_ZKLINK_IS_ETH_GAS_TOKEN]; - if (isETHGasToken === undefined) { - console.log('is eth gas token not exist'); - return; - } - console.log('is eth gas token?', isETHGasToken); const contractDeployer = new ChainContractDeployer(hardhat); await contractDeployer.init(); @@ -123,7 +114,7 @@ task('upgradeZkLink', 'Upgrade zkLink') console.log('upgrade zkLink...'); const contractName = getZkLinkContractName(dummy); - const contract = await contractDeployer.upgradeProxy(contractName, contractAddr, [isETHGasToken]); + const contract = await contractDeployer.upgradeProxy(contractName, contractAddr, []); const tx = await getDeployTx(contract); console.log('upgrade tx', tx.hash); const newContractTargetAddr = await getImplementationAddress(hardhat.ethers.provider, contractAddr); @@ -133,7 +124,7 @@ task('upgradeZkLink', 'Upgrade zkLink') // verify target contract if (!skipVerify) { - await verifyContractCode(hardhat, newContractTargetAddr, [isETHGasToken]); + await verifyContractCode(hardhat, newContractTargetAddr, []); deployLog[logName.DEPLOY_LOG_ZKLINK_TARGET_VERIFIED] = true; fs.writeFileSync(deployLogPath, JSON.stringify(deployLog, null, 2)); } @@ -154,8 +145,6 @@ task('deployZkLinkTarget', 'Deploy zkLink target') console.log('current net not support'); return; } - const isEthGasToken = chainInfo.eth; - console.log(`is eth the gas token of ${netName}?`, isEthGasToken); const contractDeployer = new ChainContractDeployer(hardhat); await contractDeployer.init(); @@ -165,18 +154,17 @@ task('deployZkLinkTarget', 'Deploy zkLink target') // deploy zkLink target console.log('deploy zkLink target...'); const contractName = getZkLinkContractName(dummy); - const contract = await contractDeployer.deployContract(contractName, [isEthGasToken]); + const contract = await contractDeployer.deployContract(contractName, []); const transaction = await getDeployTx(contract); console.log('deploy tx hash', transaction.hash); const zkLinkTargetAddr = await contract.getAddress(); console.log('zkLink target', zkLinkTargetAddr); - deployLog[logName.DEPLOY_LOG_ZKLINK_IS_ETH_GAS_TOKEN] = isEthGasToken; deployLog[logName.DEPLOY_LOG_ZKLINK_TARGET] = zkLinkTargetAddr; fs.writeFileSync(deployLogPath, JSON.stringify(deployLog, null, 2)); // verify target contract if (!skipVerify) { - await verifyContractCode(hardhat, zkLinkTargetAddr, [isEthGasToken]); + await verifyContractCode(hardhat, zkLinkTargetAddr, []); deployLog[logName.DEPLOY_LOG_ZKLINK_TARGET_VERIFIED] = true; fs.writeFileSync(deployLogPath, JSON.stringify(deployLog, null, 2)); }