diff --git a/contracts/gas-service/AxelarGasService.sol b/contracts/gas-service/AxelarGasService.sol index 7558d0a2..ecce98ff 100644 --- a/contracts/gas-service/AxelarGasService.sol +++ b/contracts/gas-service/AxelarGasService.sol @@ -3,10 +3,11 @@ pragma solidity ^0.8.0; import { IERC20 } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IERC20.sol'; +import { IAxelarGasService } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol'; +import { InterchainGasEstimation } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/gas-estimation/InterchainGasEstimation.sol'; +import { Upgradable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/upgradable/Upgradable.sol'; import { SafeTokenTransfer, SafeTokenTransferFrom } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/libs/SafeTransfer.sol'; import { SafeNativeTransfer } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/libs/SafeNativeTransfer.sol'; -import { IAxelarGasService } from '../interfaces/IAxelarGasService.sol'; -import { Upgradable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/upgradable/Upgradable.sol'; /** * @title AxelarGasService @@ -14,11 +15,13 @@ import { Upgradable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/up * @dev The owner address of this contract should be the microservice that pays for gas. * @dev Users pay gas for cross-chain calls, and the gasCollector can collect accumulated fees and/or refund users if needed. */ -contract AxelarGasService is Upgradable, IAxelarGasService { +contract AxelarGasService is InterchainGasEstimation, Upgradable, IAxelarGasService { using SafeTokenTransfer for IERC20; using SafeTokenTransferFrom for IERC20; using SafeNativeTransfer for address payable; + error InvalidParams(); + address public immutable gasCollector; /** @@ -38,6 +41,44 @@ contract AxelarGasService is Upgradable, IAxelarGasService { _; } + /** + * @notice Pay for gas for any type of contract execution on a destination chain. + * @dev This function is called on the source chain before calling the gateway to execute a remote contract. + * @dev If estimateOnChain is true, the function will estimate the gas cost and revert if the payment is insufficient. + * @param sender The address making the payment + * @param destinationChain The target chain where the contract call will be made + * @param destinationAddress The target address on the destination chain + * @param payload Data payload for the contract call + * @param executionGasLimit The gas limit for the contract call + * @param estimateOnChain Flag to enable on-chain gas estimation + * @param refundAddress The address where refunds, if any, should be sent + * @param params Additional parameters for gas payment + */ + function payGas( + address sender, + string calldata destinationChain, + string calldata destinationAddress, + bytes calldata payload, + uint256 executionGasLimit, + bool estimateOnChain, + address refundAddress, + bytes calldata params + ) external payable override { + if (params.length > 0) { + revert InvalidParams(); + } + + if (estimateOnChain) { + uint256 gasEstimate = estimateGasFee(destinationChain, destinationAddress, payload, executionGasLimit, params); + + if (gasEstimate > msg.value) { + revert InsufficientGasPayment(gasEstimate, msg.value); + } + } + + emit NativeGasPaidForContractCall(sender, destinationChain, destinationAddress, keccak256(payload), msg.value, refundAddress); + } + /** * @notice Pay for gas using ERC20 tokens for a contract call on a destination chain. * @dev This function is called on the source chain before calling the gateway to execute a remote contract. @@ -348,6 +389,25 @@ contract AxelarGasService is Upgradable, IAxelarGasService { emit NativeExpressGasAdded(txHash, logIndex, msg.value, refundAddress); } + /** + * @notice Updates the gas price for a specific chain. + * @dev This function is called by the gas oracle to update the gas prices for a specific chains. + * @param chains Array of chain names + * @param gasUpdates Array of gas updates + */ + function updateGasInfo(string[] calldata chains, GasInfo[] calldata gasUpdates) external onlyCollector { + uint256 chainsLength = chains.length; + + if (chainsLength != gasUpdates.length) revert InvalidGasUpdates(); + + for (uint256 i; i < chainsLength; i++) { + string calldata chain = chains[i]; + GasInfo calldata gasUpdate = gasUpdates[i]; + + _setGasInfo(chain, gasUpdate); + } + } + /** * @notice Allows the gasCollector to collect accumulated fees from the contract. * @dev Use address(0) as the token address for native currency. diff --git a/contracts/interfaces/IAxelarGasService.sol b/contracts/interfaces/IAxelarGasService.sol index 98f3d3bb..13816067 100644 --- a/contracts/interfaces/IAxelarGasService.sol +++ b/contracts/interfaces/IAxelarGasService.sol @@ -2,375 +2,11 @@ pragma solidity ^0.8.0; -import { IUpgradable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IUpgradable.sol'; - /** * @title IAxelarGasService Interface * @notice This is an interface for the AxelarGasService contract which manages gas payments * and refunds for cross-chain communication on the Axelar network. - * @dev This interface inherits IUpgradable + * @dev This interface is re-exported from axelar-gmp-sdk-solidity package */ -interface IAxelarGasService is IUpgradable { - error InvalidAddress(); - error NotCollector(); - error InvalidAmounts(); - - event GasPaidForContractCall( - address indexed sourceAddress, - string destinationChain, - string destinationAddress, - bytes32 indexed payloadHash, - address gasToken, - uint256 gasFeeAmount, - address refundAddress - ); - - event GasPaidForContractCallWithToken( - address indexed sourceAddress, - string destinationChain, - string destinationAddress, - bytes32 indexed payloadHash, - string symbol, - uint256 amount, - address gasToken, - uint256 gasFeeAmount, - address refundAddress - ); - - event NativeGasPaidForContractCall( - address indexed sourceAddress, - string destinationChain, - string destinationAddress, - bytes32 indexed payloadHash, - uint256 gasFeeAmount, - address refundAddress - ); - - event NativeGasPaidForContractCallWithToken( - address indexed sourceAddress, - string destinationChain, - string destinationAddress, - bytes32 indexed payloadHash, - string symbol, - uint256 amount, - uint256 gasFeeAmount, - address refundAddress - ); - - event GasPaidForExpressCall( - address indexed sourceAddress, - string destinationChain, - string destinationAddress, - bytes32 indexed payloadHash, - address gasToken, - uint256 gasFeeAmount, - address refundAddress - ); - - event GasPaidForExpressCallWithToken( - address indexed sourceAddress, - string destinationChain, - string destinationAddress, - bytes32 indexed payloadHash, - string symbol, - uint256 amount, - address gasToken, - uint256 gasFeeAmount, - address refundAddress - ); - - event NativeGasPaidForExpressCall( - address indexed sourceAddress, - string destinationChain, - string destinationAddress, - bytes32 indexed payloadHash, - uint256 gasFeeAmount, - address refundAddress - ); - - event NativeGasPaidForExpressCallWithToken( - address indexed sourceAddress, - string destinationChain, - string destinationAddress, - bytes32 indexed payloadHash, - string symbol, - uint256 amount, - uint256 gasFeeAmount, - address refundAddress - ); - - event GasAdded(bytes32 indexed txHash, uint256 indexed logIndex, address gasToken, uint256 gasFeeAmount, address refundAddress); - - event NativeGasAdded(bytes32 indexed txHash, uint256 indexed logIndex, uint256 gasFeeAmount, address refundAddress); - - event ExpressGasAdded(bytes32 indexed txHash, uint256 indexed logIndex, address gasToken, uint256 gasFeeAmount, address refundAddress); - - event NativeExpressGasAdded(bytes32 indexed txHash, uint256 indexed logIndex, uint256 gasFeeAmount, address refundAddress); - - event Refunded(bytes32 indexed txHash, uint256 indexed logIndex, address payable receiver, address token, uint256 amount); - - /** - * @notice Pay for gas using ERC20 tokens for a contract call on a destination chain. - * @dev This function is called on the source chain before calling the gateway to execute a remote contract. - * @param sender The address making the payment - * @param destinationChain The target chain where the contract call will be made - * @param destinationAddress The target address on the destination chain - * @param payload Data payload for the contract call - * @param gasToken The address of the ERC20 token used to pay for gas - * @param gasFeeAmount The amount of tokens to pay for gas - * @param refundAddress The address where refunds, if any, should be sent - */ - function payGasForContractCall( - address sender, - string calldata destinationChain, - string calldata destinationAddress, - bytes calldata payload, - address gasToken, - uint256 gasFeeAmount, - address refundAddress - ) external; - - /** - * @notice Pay for gas using ERC20 tokens for a contract call with tokens on a destination chain. - * @dev This function is called on the source chain before calling the gateway to execute a remote contract. - * @param sender The address making the payment - * @param destinationChain The target chain where the contract call with tokens will be made - * @param destinationAddress The target address on the destination chain - * @param payload Data payload for the contract call with tokens - * @param symbol The symbol of the token to be sent with the call - * @param amount The amount of tokens to be sent with the call - * @param gasToken The address of the ERC20 token used to pay for gas - * @param gasFeeAmount The amount of tokens to pay for gas - * @param refundAddress The address where refunds, if any, should be sent - */ - function payGasForContractCallWithToken( - address sender, - string calldata destinationChain, - string calldata destinationAddress, - bytes calldata payload, - string calldata symbol, - uint256 amount, - address gasToken, - uint256 gasFeeAmount, - address refundAddress - ) external; - - /** - * @notice Pay for gas using native currency for a contract call on a destination chain. - * @dev This function is called on the source chain before calling the gateway to execute a remote contract. - * @param sender The address making the payment - * @param destinationChain The target chain where the contract call will be made - * @param destinationAddress The target address on the destination chain - * @param payload Data payload for the contract call - * @param refundAddress The address where refunds, if any, should be sent - */ - function payNativeGasForContractCall( - address sender, - string calldata destinationChain, - string calldata destinationAddress, - bytes calldata payload, - address refundAddress - ) external payable; - - /** - * @notice Pay for gas using native currency for a contract call with tokens on a destination chain. - * @dev This function is called on the source chain before calling the gateway to execute a remote contract. - * @param sender The address making the payment - * @param destinationChain The target chain where the contract call with tokens will be made - * @param destinationAddress The target address on the destination chain - * @param payload Data payload for the contract call with tokens - * @param symbol The symbol of the token to be sent with the call - * @param amount The amount of tokens to be sent with the call - * @param refundAddress The address where refunds, if any, should be sent - */ - function payNativeGasForContractCallWithToken( - address sender, - string calldata destinationChain, - string calldata destinationAddress, - bytes calldata payload, - string calldata symbol, - uint256 amount, - address refundAddress - ) external payable; - - /** - * @notice Pay for gas using ERC20 tokens for an express contract call on a destination chain. - * @dev This function is called on the source chain before calling the gateway to express execute a remote contract. - * @param sender The address making the payment - * @param destinationChain The target chain where the contract call will be made - * @param destinationAddress The target address on the destination chain - * @param payload Data payload for the contract call - * @param gasToken The address of the ERC20 token used to pay for gas - * @param gasFeeAmount The amount of tokens to pay for gas - * @param refundAddress The address where refunds, if any, should be sent - */ - function payGasForExpressCall( - address sender, - string calldata destinationChain, - string calldata destinationAddress, - bytes calldata payload, - address gasToken, - uint256 gasFeeAmount, - address refundAddress - ) external; - - /** - * @notice Pay for gas using ERC20 tokens for an express contract call with tokens on a destination chain. - * @dev This function is called on the source chain before calling the gateway to express execute a remote contract. - * @param sender The address making the payment - * @param destinationChain The target chain where the contract call with tokens will be made - * @param destinationAddress The target address on the destination chain - * @param payload Data payload for the contract call with tokens - * @param symbol The symbol of the token to be sent with the call - * @param amount The amount of tokens to be sent with the call - * @param gasToken The address of the ERC20 token used to pay for gas - * @param gasFeeAmount The amount of tokens to pay for gas - * @param refundAddress The address where refunds, if any, should be sent - */ - function payGasForExpressCallWithToken( - address sender, - string calldata destinationChain, - string calldata destinationAddress, - bytes calldata payload, - string calldata symbol, - uint256 amount, - address gasToken, - uint256 gasFeeAmount, - address refundAddress - ) external; - - /** - * @notice Pay for gas using native currency for an express contract call on a destination chain. - * @dev This function is called on the source chain before calling the gateway to execute a remote contract. - * @param sender The address making the payment - * @param destinationChain The target chain where the contract call will be made - * @param destinationAddress The target address on the destination chain - * @param payload Data payload for the contract call - * @param refundAddress The address where refunds, if any, should be sent - */ - function payNativeGasForExpressCall( - address sender, - string calldata destinationChain, - string calldata destinationAddress, - bytes calldata payload, - address refundAddress - ) external payable; - - /** - * @notice Pay for gas using native currency for an express contract call with tokens on a destination chain. - * @dev This function is called on the source chain before calling the gateway to execute a remote contract. - * @param sender The address making the payment - * @param destinationChain The target chain where the contract call with tokens will be made - * @param destinationAddress The target address on the destination chain - * @param payload Data payload for the contract call with tokens - * @param symbol The symbol of the token to be sent with the call - * @param amount The amount of tokens to be sent with the call - * @param refundAddress The address where refunds, if any, should be sent - */ - function payNativeGasForExpressCallWithToken( - address sender, - string calldata destinationChain, - string calldata destinationAddress, - bytes calldata payload, - string calldata symbol, - uint256 amount, - address refundAddress - ) external payable; - - /** - * @notice Add additional gas payment using ERC20 tokens after initiating a cross-chain call. - * @dev This function can be called on the source chain after calling the gateway to execute a remote contract. - * @param txHash The transaction hash of the cross-chain call - * @param logIndex The log index for the cross-chain call - * @param gasToken The ERC20 token address used to add gas - * @param gasFeeAmount The amount of tokens to add as gas - * @param refundAddress The address where refunds, if any, should be sent - */ - function addGas( - bytes32 txHash, - uint256 logIndex, - address gasToken, - uint256 gasFeeAmount, - address refundAddress - ) external; - - /** - * @notice Add additional gas payment using native currency after initiating a cross-chain call. - * @dev This function can be called on the source chain after calling the gateway to execute a remote contract. - * @param txHash The transaction hash of the cross-chain call - * @param logIndex The log index for the cross-chain call - * @param refundAddress The address where refunds, if any, should be sent - */ - function addNativeGas( - bytes32 txHash, - uint256 logIndex, - address refundAddress - ) external payable; - - /** - * @notice Add additional gas payment using ERC20 tokens after initiating an express cross-chain call. - * @dev This function can be called on the source chain after calling the gateway to express execute a remote contract. - * @param txHash The transaction hash of the cross-chain call - * @param logIndex The log index for the cross-chain call - * @param gasToken The ERC20 token address used to add gas - * @param gasFeeAmount The amount of tokens to add as gas - * @param refundAddress The address where refunds, if any, should be sent - */ - function addExpressGas( - bytes32 txHash, - uint256 logIndex, - address gasToken, - uint256 gasFeeAmount, - address refundAddress - ) external; - - /** - * @notice Add additional gas payment using native currency after initiating an express cross-chain call. - * @dev This function can be called on the source chain after calling the gateway to express execute a remote contract. - * @param txHash The transaction hash of the cross-chain call - * @param logIndex The log index for the cross-chain call - * @param refundAddress The address where refunds, if any, should be sent - */ - function addNativeExpressGas( - bytes32 txHash, - uint256 logIndex, - address refundAddress - ) external payable; - - /** - * @notice Allows the gasCollector to collect accumulated fees from the contract. - * @dev Use address(0) as the token address for native currency. - * @param receiver The address to receive the collected fees - * @param tokens Array of token addresses to be collected - * @param amounts Array of amounts to be collected for each respective token address - */ - function collectFees( - address payable receiver, - address[] calldata tokens, - uint256[] calldata amounts - ) external; - - /** - * @notice Refunds gas payment to the receiver in relation to a specific cross-chain transaction. - * @dev Only callable by the gasCollector. - * @dev Use address(0) as the token address to refund native currency. - * @param txHash The transaction hash of the cross-chain call - * @param logIndex The log index for the cross-chain call - * @param receiver The address to receive the refund - * @param token The token address to be refunded - * @param amount The amount to refund - */ - function refund( - bytes32 txHash, - uint256 logIndex, - address payable receiver, - address token, - uint256 amount - ) external; - - /** - * @notice Returns the address of the designated gas collector. - * @return address of the gas collector - */ - function gasCollector() external returns (address); -} +// solhint-disable-next-line no-unused-import +import { IAxelarGasService } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol'; diff --git a/package-lock.json b/package-lock.json index 8995c720..829df08e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "6.2.1", "license": "MIT", "dependencies": { - "@axelar-network/axelar-gmp-sdk-solidity": "5.6.4" + "@axelar-network/axelar-gmp-sdk-solidity": "^5.7.0" }, "devDependencies": { "@0xpolygonhermez/zkevm-commonjs": "github:0xpolygonhermez/zkevm-commonjs#v1.0.0", @@ -74,11 +74,11 @@ } }, "node_modules/@axelar-network/axelar-gmp-sdk-solidity": { - "version": "5.6.4", - "resolved": "https://registry.npmjs.org/@axelar-network/axelar-gmp-sdk-solidity/-/axelar-gmp-sdk-solidity-5.6.4.tgz", - "integrity": "sha512-PQjV+HeJynmSRMhyM3SexwnbFNruSaiRUeNCWjV8/7CkdPsDqypoqIXVRVU8Zk92DUUHeqZZzL/3qP2LYuvlnA==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@axelar-network/axelar-gmp-sdk-solidity/-/axelar-gmp-sdk-solidity-5.7.0.tgz", + "integrity": "sha512-JlokiWFxvR6bFQtDjdErtk0mZrr3GH1A8bKps1zVP/Bu4XOHR0WsrWGPVhWIbvT8a8Ag3dva4hskBYgdq+pLig==", "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/@babel/code-frame": { diff --git a/package.json b/package.json index 2efb5b1f..432e9adf 100644 --- a/package.json +++ b/package.json @@ -30,9 +30,10 @@ }, "homepage": "https://github.com/axelarnetwork/axelar-cgp-solidity#readme", "dependencies": { - "@axelar-network/axelar-gmp-sdk-solidity": "5.6.4" + "@axelar-network/axelar-gmp-sdk-solidity": "5.7.0" }, "devDependencies": { + "@0xpolygonhermez/zkevm-commonjs": "github:0xpolygonhermez/zkevm-commonjs#v1.0.0", "@axelar-network/axelar-chains-config": "^1.0.0", "@nomicfoundation/hardhat-toolbox": "^2.0.2", "chai": "^4.3.7", @@ -43,16 +44,15 @@ "ethers": "^5.7.2", "fs-extra": "^11.1.1", "hardhat": "^2.19.1", - "hardhat-storage-layout": "^0.1.7", "hardhat-contract-sizer": "^2.10.0", + "hardhat-storage-layout": "^0.1.7", "lodash": "^4.17.21", "mocha": "^10.2.0", "prettier": "^2.8.7", "prettier-plugin-solidity": "1.0.0-beta.19", "readline-sync": "^1.4.10", "solhint": "^3.4.1", - "solidity-coverage": "^0.8.4", - "@0xpolygonhermez/zkevm-commonjs": "github:0xpolygonhermez/zkevm-commonjs#v1.0.0" + "solidity-coverage": "^0.8.4" }, "engines": { "node": ">=16" diff --git a/test/gmp/AxelarGasService.js b/test/gmp/AxelarGasService.js index 6e5968f1..3fbea4e8 100644 --- a/test/gmp/AxelarGasService.js +++ b/test/gmp/AxelarGasService.js @@ -53,7 +53,7 @@ describe('AxelarGasService', () => { await testToken.mint(userWallet.address, 1e9).then((tx) => tx.wait()); }); - describe('gas receiver', () => { + describe('AxelarGasService', () => { it('should emit events when receives gas payment', async () => { const destinationChain = 'ethereum'; const destinationAddress = ownerWallet.address; @@ -629,5 +629,121 @@ describe('AxelarGasService', () => { expect(proxyBytecodeHash).to.be.equal(expected); }); + + describe('Gas Estimation', () => { + const chains = ['ethereum', 'optimism', 'base']; + const gasUpdates = [ + [0, '110227069355211', '278470919016084', '3800724', '1395265596'], + [1, '110898281163494', '278128885876991', '3066', '0'], + [1, '123127735536005', '279194214965138', '30593', '0'], + ]; + + it('should allow the collector to update gas info', async () => { + await expectRevert( + (gasOptions) => gasService.connect(userWallet).updateGasInfo(chains, gasUpdates, gasOptions), + gasService, + 'NotCollector', + ); + + await expectRevert( + (gasOptions) => gasService.connect(ownerWallet).updateGasInfo(chains, gasUpdates.slice(0, 2), gasOptions), + gasService, + 'InvalidGasUpdates', + ); + + await expect(gasService.connect(ownerWallet).updateGasInfo(chains, gasUpdates)) + .to.emit(gasService, 'GasInfoUpdated') + .withArgs(chains[0], gasUpdates[0]); + + for (let i = 0; i < chains.length; i++) { + const chain = chains[i]; + const gasInfo = gasUpdates[i]; + + let result = await gasService.getGasInfo(chain); + result = Array.from(result).map((x) => (x.toNumber ? x.toNumber().toString() : x)); + expect(result).to.be.deep.equal(gasInfo); + } + }); + + it('should allow paying gas with on-chain estimation', async () => { + const destinationChain = 'optimism'; + const destinationAddress = ownerWallet.address; + const payload = defaultAbiCoder.encode(['address', 'address'], [ownerWallet.address, userWallet.address]); + const executionGasLimit = 1000000; + const estimateOnChain = true; + const refundAddress = userWallet.address; + const params = '0x'; + + // Set up the gas info for the destination chain + await gasService.connect(ownerWallet).updateGasInfo(chains, gasUpdates); + + // Estimate the gas fee + const gasEstimate = await gasService.estimateGasFee( + destinationChain, + destinationAddress, + payload, + executionGasLimit, + params, + ); + + expect(gasEstimate).to.be.equal(111288142881657); + + await expectRevert( + (gasOptions) => + gasService + .connect(userWallet) + .payGas( + userWallet.address, + destinationChain, + destinationAddress, + payload, + executionGasLimit, + estimateOnChain, + refundAddress, + params, + { ...gasOptions, value: gasEstimate - 1 }, + ), + gasService, + 'InsufficientGasPayment', + ); + + await expectRevert( + (gasOptions) => + gasService + .connect(userWallet) + .payGas( + userWallet.address, + destinationChain, + destinationAddress, + payload, + executionGasLimit, + estimateOnChain, + refundAddress, + '0x11', + { ...gasOptions, value: gasEstimate }, + ), + gasService, + 'InvalidParams', + ); + + await expect( + gasService + .connect(userWallet) + .payGas( + userWallet.address, + destinationChain, + destinationAddress, + payload, + executionGasLimit, + estimateOnChain, + refundAddress, + params, + { value: gasEstimate }, + ), + ) + .to.emit(gasService, 'NativeGasPaidForContractCall') + .withArgs(userWallet.address, destinationChain, destinationAddress, keccak256(payload), gasEstimate, refundAddress); + }); + }); }); });