Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(GasEstimate): new base contract for gas estimation #133

Merged
merged 28 commits into from
Mar 16, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
514306b
feat(GasEstimate): new base contract for gas estimation
re1ro Mar 13, 2024
557de0f
Update contracts/gas/GasEstimate.sol
re1ro Mar 13, 2024
c200bc4
Update contracts/interfaces/IGasEstimate.sol
re1ro Mar 13, 2024
42de430
feat(GasEstimate): using Optimism Ecotone gas model
re1ro Mar 13, 2024
a82bb15
feat(GasEstimate): using computed constants.
re1ro Mar 13, 2024
324c8f3
feat(GasEstimate): using computed constants.
re1ro Mar 13, 2024
f1b31f2
feat(GasEstimate): adding GMP params to the estimation
re1ro Mar 14, 2024
bac19e6
doc(GasEstimate): comment
re1ro Mar 14, 2024
e696cca
doc(GasEstimate): comment
re1ro Mar 14, 2024
824b7b2
test(GasEstimate): adding test contract
re1ro Mar 14, 2024
9ddb4b0
feat(IAxelarGasService): updating the interface with the latest
re1ro Mar 14, 2024
4b8eade
feat(IAxelarGasService): updating the interface with the latest
re1ro Mar 14, 2024
0249eb5
feat(IAxelarGasService): updating the interface with the latest
re1ro Mar 14, 2024
62a07f4
test(GasEstimate): adding test contract
re1ro Mar 14, 2024
8beb68b
feat(GasEstimate): adjusting Ecotone formula
re1ro Mar 14, 2024
cee8eae
style(solidity): prettier
re1ro Mar 14, 2024
626bc82
test(GasEstimate): adding test setup
re1ro Mar 15, 2024
520a82f
Update contracts/interfaces/IGasEstimate.sol
re1ro Mar 15, 2024
27150eb
Update contracts/interfaces/IGasEstimate.sol
re1ro Mar 15, 2024
e5b1ba3
Update contracts/interfaces/IGasEstimate.sol
re1ro Mar 15, 2024
aae5ca9
fix(GasEstimate): PR feedback
re1ro Mar 16, 2024
0822133
refactor(GasEstimate): axelarBaseFee
re1ro Mar 16, 2024
245ab99
Update contracts/gas/InterchainGasEstimation.sol
re1ro Mar 16, 2024
c2893da
Update test/gas/InterchainGasEstimation.js
re1ro Mar 16, 2024
c254fa9
Update contracts/gas/InterchainGasEstimation.sol
re1ro Mar 16, 2024
65dabad
fix(GasEstimate): PR feedback
re1ro Mar 16, 2024
fa4dadf
fix(GasEstimate): PR feedback
re1ro Mar 16, 2024
3c5e374
doc(GasEstimate): comments
re1ro Mar 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions contracts/gas/GasEstimate.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import { IGasEstimate } from '../interfaces/IGasEstimate.sol';

/**
* @title AxelarGasService
* @notice This contract manages gas payments and refunds for cross-chain communication on the Axelar network.
* @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 GasEstimate is IGasEstimate {
// keccak256('GasEstimate.Slot') - 1
bytes32 internal constant GAS_SERVICE_SLOT = 0x2fa150da4c9f4c3a28593398c65313dd42f63d0530ec6db4a2b46e6d837a3902;

struct GasServiceStorage {
mapping(string => GasInfo) gasPrices;
}

/**
* @notice Sets the gas price for a specific chain.
* @dev This function is called by the gas oracle to update the gas price for a specific chain.
* @param chain The name of the chain
* @param gasInfo The gas info for the chain
*/
function _setGasInfo(string calldata chain, GasInfo calldata gasInfo) internal {
milapsheth marked this conversation as resolved.
Show resolved Hide resolved
emit GasInfoUpdated(chain, gasInfo);

_gasServiceStorage().gasPrices[chain] = gasInfo;
}

/**
* @notice Estimates the gas fee for a contract call on a destination chain.
* @param destinationChain Axelar registered name of the destination chain
* param destinationAddress Destination contract address being called
* @param executionGasLimit The gas limit to be used for the destination contract execution,
* e.g. pass in 200k if your app consumes needs upto 200k for this contract call
* @return gasEstimate The cross-chain gas estimate, in terms of source chain's native gas token that should be forwarded to the gas service.
*/
function estimateGasFee(
string calldata destinationChain,
string calldata, /* destinationAddress */
bytes calldata payload,
uint256 executionGasLimit
) external view returns (uint256 gasEstimate) {
GasServiceStorage storage slot = _gasServiceStorage();
GasInfo storage gasInfo = slot.gasPrices[destinationChain];

gasEstimate = gasInfo.baseFee + (executionGasLimit * gasInfo.relativeGasPrice);

// if chain is L2, compute L1 data fee using L1 gas price info
if (gasInfo.extraFee != ExtraFeeType.None) {
gasEstimate += computeExtraFee(gasInfo.extraFee, payload, slot.gasPrices['ethereum'].relativeGasPrice);
}
}

/**
* @notice Computes the L1 to L2 fee for a contract call on a destination chain.
* @param feeType The type of extra fee
* @param payload The payload of the contract call
* @param l1GasPrice The gas price on the source chain
* @return l1DataFee The L1 to L2 data fee
*/
function computeExtraFee(
ExtraFeeType feeType,
bytes calldata payload,
uint256 l1GasPrice
) internal pure returns (uint256) {
if (feeType == ExtraFeeType.OptimismEcotone) {
return optimismEcotoneL1Fee(payload, l1GasPrice);
}

revert UnsupportedExtraFeeType(feeType);
}

/**
* @notice Computes the L1 to L2 fee for a contract call on the Optimism chain.
* @param payload The payload of the contract call
* @param l1GasPrice The base fee for L1 to L2
* @return l1DataFee The L1 to L2 data fee
*/
function optimismEcotoneL1Fee(bytes calldata payload, uint256 l1GasPrice)
internal
pure
returns (uint256 l1DataFee)
{
/* Optimism Ecotone gas model https://docs.optimism.io/stack/transactions/fees#ecotone
tx_compressed_size = ((count_zero_bytes(tx_data) * 4 + count_non_zero_bytes(tx_data) * 16)) / 16
weighted_gas_price = 16 * base_fee_scalar*base_fee + blob_base_fee_scalar * blob_base_fee
l1_data_fee = tx_compressed_size * weighted_gas_price

Reference implementation:
https://github.com/ethereum-optimism/optimism/blob/876e16ad04968f0bb641eb76f98eb77e7e1a3e16/packages/contracts-bedrock/src/L2/GasPriceOracle.sol#L138
*/

// The new base_fee_scalar is currently set to 0.001368
// We are setting it to un upper bound of 0.0015 to account for possible fluctuations
uint256 scalarPrecision = 10**6;
uint256 baseFeeScalar = 1500; // 1500 : 1e6 = 0.0015

// The blob_base_fee_scalar is currently set to 0.810949. Setting it to 0.9 as an upper bound
// https://eips.ethereum.org/EIPS/eip-4844
uint256 blobBaseFeeScalar = 900000; // 0.9 multiplied by scalarPrecision
uint256 blobBaseFee = 1;

// Calculating transaction size in bytes that will later be divided by 16 to compress the size
// 68 bytes for the TX RLP encoding overhead
uint256 txSize = 68 * 16;
// GMP executeWithToken call parameters
// 4 bytes for method selector, 32 bytes for the commandId, 96 bytes for the sourceChain, 128 bytes for the sourceAddress, 96 bytes for token symbol, 32 bytes for amount
// Expecting most of the calldata bytes to be zeroes. So multiplying by 8 as a weighted average of 4 and 16
txSize += (4 + 32 + 96 + 128 + 96 + 32) * 8;

for (uint256 i; i < payload.length; ++i) {
if (payload[i] == 0) {
txSize += 4; // 4 for each zero byte
} else {
txSize += 16; // 16 for each non-zero byte
}
}

uint256 weightedGasPrice = 16 * baseFeeScalar * l1GasPrice + blobBaseFeeScalar * blobBaseFee;
milapsheth marked this conversation as resolved.
Show resolved Hide resolved

l1DataFee = (weightedGasPrice * txSize) / (16 * scalarPrecision); // 16 for txSize compression and scalar precision conversion
milapsheth marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* @notice Get the storage slot for the GasServiceStorage struct
*/
function _gasServiceStorage() private pure returns (GasServiceStorage storage slot) {
assembly {
slot.slot := GAS_SERVICE_SLOT
}
}
}
19 changes: 14 additions & 5 deletions contracts/interfaces/IAxelarGasService.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@

pragma solidity ^0.8.0;

import { IUpgradable } from '../interfaces/IUpgradable.sol';
import { IGasEstimate } from './IGasEstimate.sol';
import { IUpgradable } from './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
*/
interface IAxelarGasService is IUpgradable {
error NothingReceived();
interface IAxelarGasService is IGasEstimate, IUpgradable {
error InvalidAddress();
error NotCollector();
error InvalidAmounts();
error InvalidGasUpdates();

event GasPaidForContractCall(
address indexed sourceAddress,
Expand Down Expand Up @@ -265,7 +266,7 @@ interface IAxelarGasService is IUpgradable {

/**
* @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 express execute a remote contract.
* @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
Expand All @@ -282,7 +283,7 @@ interface IAxelarGasService is IUpgradable {

/**
* @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 express execute a remote contract.
* @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
Expand Down Expand Up @@ -361,6 +362,14 @@ interface IAxelarGasService is IUpgradable {
address refundAddress
) external payable;

/**
* @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;

/**
* @notice Allows the gasCollector to collect accumulated fees from the contract.
* @dev Use address(0) as the token address for native currency.
Expand Down
46 changes: 46 additions & 0 deletions contracts/interfaces/IGasEstimate.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

/**
* @title IAxelarGasService Interface
* @notice This is an interface for the AxelarGasService contract which manages gas payments
milapsheth marked this conversation as resolved.
Show resolved Hide resolved
* and refunds for cross-chain communication on the Axelar network.
* @dev This interface inherits IUpgradable
*/
interface IGasEstimate {
milapsheth marked this conversation as resolved.
Show resolved Hide resolved
re1ro marked this conversation as resolved.
Show resolved Hide resolved
error UnsupportedExtraFeeType(ExtraFeeType feeType);

/**
* @notice Event emitted when the gas price for a specific chain is updated.
* @param chain The name of the chain
* @param info The gas info for the chain
*/
event GasInfoUpdated(string chain, GasInfo info);

enum ExtraFeeType {
None,
OptimismEcotone
}

struct GasInfo {
uint256 baseFee; // destination base_fee (in terms of src native gas token)
re1ro marked this conversation as resolved.
Show resolved Hide resolved
uint256 relativeGasPrice; // dest_gas_price * dest_token_market_price / src_token_market_price
ExtraFeeType extraFee; // additional fee type for L1 to L2
re1ro marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* @notice Estimates the gas fee for a cross-chain contract call.
* @param destinationChain Axelar registered name of the destination chain
* @param destinationAddress Destination contract address being called
* @param executionGasLimit The gas limit to be used for the destination contract execution,
* e.g. pass in 200k if your app consumes needs upto 200k for this contract call
* @return gasEstimate The cross-chain gas estimate, in terms of source chain's native gas token that should be forwarded to the gas service.
*/
function estimateGasFee(
string calldata destinationChain,
string calldata destinationAddress,
bytes calldata payload,
uint256 executionGasLimit
) external view returns (uint256 gasEstimate);
}
17 changes: 17 additions & 0 deletions contracts/test/gas/TestGasEstimate.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import { GasEstimate } from '../../gas/GasEstimate.sol';

contract TestGasEstimate is GasEstimate {
constructor() {
if (GAS_SERVICE_SLOT != bytes32(uint256(keccak256("GasEstimate.Slot")) - 1)) {

Check failure on line 9 in contracts/test/gas/TestGasEstimate.sol

View workflow job for this annotation

GitHub Actions / lint (18.x, ubuntu-latest)

Use single quotes for string literals
revert("TestGasEstimate: invalid slot");

Check failure on line 10 in contracts/test/gas/TestGasEstimate.sol

View workflow job for this annotation

GitHub Actions / lint (18.x, ubuntu-latest)

Use single quotes for string literals
}
milapsheth marked this conversation as resolved.
Show resolved Hide resolved
}

function updateGasInfo(string calldata chain, GasInfo calldata info) external {
_setGasInfo(chain, info);
}
}
35 changes: 35 additions & 0 deletions test/gas/GasEstimate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use strict';

const chai = require('chai');
const { expect } = chai;
const { ethers } = require('hardhat');

describe('GasEstimate', () => {
let gasEstimateFactory;
let gasEstimate;

let ownerWallet;

const sourceChain = 'ethereum';
const destinationChain = 'optimism';

before(async () => {
[ownerWallet] = await ethers.getSigners();

gasEstimateFactory = await ethers.getContractFactory('TestGasEstimate', ownerWallet);
gasEstimate = await gasEstimateFactory.deploy().then((d) =>
d.deployed(),
);

await gasEstimate.updateGasInfo(sourceChain, [0, 50000000000, 0]);
await gasEstimate.updateGasInfo(destinationChain, [0, 50000000000, 1]);
});

describe('GasEstimate', () => {
it('should revert when deployed with empty gateway', async () => {
const estimate = await gasEstimate.estimateGasFee(destinationChain, destinationChain, '0x2534d1533c9ffce84d3174c1f846a4041d07b56d1e7b5cb7138e06fb42086325', 0);

expect(estimate).to.equal(352800000264);
});
});
});
Loading