From e3905aea5037f430427a123eef4e54d2c9eabb05 Mon Sep 17 00:00:00 2001 From: Roy Yang Date: Fri, 15 Sep 2023 03:52:16 +1200 Subject: [PATCH] feat: Calculate ccm gas limit (#3935) Co-authored-by: Daniel Co-authored-by: Albert Llimos <53186777+albert-llimos@users.noreply.github.com> Co-authored-by: albert Co-authored-by: Alastair Holmes --- .cargo/config.toml | 2 +- Cargo.lock | 1 + bouncer/run.sh | 2 +- bouncer/shared/eth_abis.ts | 2 +- bouncer/shared/gaslimit_ccm.ts | 373 ++++++++++-------- bouncer/shared/send_eth.ts | 11 +- engine/src/eth/retry_rpc.rs | 37 +- engine/src/eth/rpc.rs | 6 +- .../CFTester.json | 0 .../Deposit_bytecode.json | 0 .../IAddressChecker.json | 0 .../IFLIP.json | 0 .../IKeyManager.json | 0 .../IStateChainGateway.json | 0 .../IVault.json | 0 localnet/docker-compose.yml | 4 +- state-chain/cf-integration-tests/Cargo.toml | 1 + .../cf-integration-tests/src/swapping.rs | 98 ++++- state-chain/chains/src/btc/api.rs | 1 + state-chain/chains/src/dot/api.rs | 1 + state-chain/chains/src/eth.rs | 13 +- state-chain/chains/src/eth/api.rs | 8 + state-chain/chains/src/evm.rs | 2 +- state-chain/chains/src/evm/api.rs | 7 + .../src/evm/api/execute_x_swap_and_call.rs | 11 +- state-chain/chains/src/lib.rs | 7 + .../pallets/cf-ingress-egress/src/lib.rs | 31 +- .../pallets/cf-ingress-egress/src/tests.rs | 23 +- state-chain/pallets/cf-swapping/Cargo.toml | 2 + state-chain/pallets/cf-swapping/src/lib.rs | 15 +- state-chain/pallets/cf-swapping/src/tests.rs | 5 + state-chain/runtime/src/chainflip.rs | 68 ++-- state-chain/traits/src/lib.rs | 6 +- state-chain/traits/src/mocks/api_call.rs | 3 + .../traits/src/mocks/egress_handler.rs | 8 +- 35 files changed, 500 insertions(+), 248 deletions(-) rename eth-contract-abis/{perseverance-0.9-rc6 => v0.8.0}/CFTester.json (100%) rename eth-contract-abis/{perseverance-0.9-rc6 => v0.8.0}/Deposit_bytecode.json (100%) rename eth-contract-abis/{perseverance-0.9-rc6 => v0.8.0}/IAddressChecker.json (100%) rename eth-contract-abis/{perseverance-0.9-rc6 => v0.8.0}/IFLIP.json (100%) rename eth-contract-abis/{perseverance-0.9-rc6 => v0.8.0}/IKeyManager.json (100%) rename eth-contract-abis/{perseverance-0.9-rc6 => v0.8.0}/IStateChainGateway.json (100%) rename eth-contract-abis/{perseverance-0.9-rc6 => v0.8.0}/IVault.json (100%) diff --git a/.cargo/config.toml b/.cargo/config.toml index b521701afd1..24d6858f7ee 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,6 +1,6 @@ [env] CF_ETH_CONTRACT_ABI_ROOT = { value = "eth-contract-abis", relative = true } -CF_ETH_CONTRACT_ABI_TAG = "perseverance-0.9-rc6" +CF_ETH_CONTRACT_ABI_TAG = "v0.8.0" # Note: If you just want to save typing command commands, you can install tab completions for most shells. Type diff --git a/Cargo.lock b/Cargo.lock index f3f75e80301..4d82d916dd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1169,6 +1169,7 @@ dependencies = [ "pallet-authorship", "pallet-cf-account-roles", "pallet-cf-broadcast", + "pallet-cf-chain-tracking", "pallet-cf-emissions", "pallet-cf-environment", "pallet-cf-flip", diff --git a/bouncer/run.sh b/bouncer/run.sh index 509d16990c2..6f89a3bbb3c 100755 --- a/bouncer/run.sh +++ b/bouncer/run.sh @@ -2,8 +2,8 @@ set -e ./commands/observe_block.ts 5 ./commands/setup_vaults.ts ./commands/setup_swaps.ts -./tests/all_concurrent_tests.ts ./tests/gaslimit_ccm.ts +./tests/all_concurrent_tests.ts ./tests/rotates_through_btc_swap.ts if [[ $LOCALNET == false ]]; then diff --git a/bouncer/shared/eth_abis.ts b/bouncer/shared/eth_abis.ts index 36e794a274b..2aaa2ce6425 100644 --- a/bouncer/shared/eth_abis.ts +++ b/bouncer/shared/eth_abis.ts @@ -14,7 +14,7 @@ function loadContractCached(abiPath: string) { return cached; }; } -const CF_ETH_CONTRACT_ABI_TAG = 'perseverance-0.9-rc6'; +const CF_ETH_CONTRACT_ABI_TAG = 'v0.8.0'; export const getErc20abi = loadContractCached('../eth-contract-abis/IERC20.json'); export const getGatewayAbi = loadContractCached( `../eth-contract-abis/${CF_ETH_CONTRACT_ABI_TAG}/IStateChainGateway.json`, diff --git a/bouncer/shared/gaslimit_ccm.ts b/bouncer/shared/gaslimit_ccm.ts index f02edd07088..1bfd5ff37a8 100644 --- a/bouncer/shared/gaslimit_ccm.ts +++ b/bouncer/shared/gaslimit_ccm.ts @@ -1,6 +1,6 @@ import Web3 from 'web3'; import { Asset, Assets } from '@chainflip-io/cli'; -import { newCcmMetadata, prepareSwap, testSwap } from './swapping'; +import { newCcmMetadata, prepareSwap } from './swapping'; import { getChainflipApi, observeCcmReceived, @@ -12,20 +12,28 @@ import { import { requestNewSwap } from './perform_swap'; import { send } from './send'; import { BtcAddressType } from './new_btc_address'; +import { signAndSendTxEthSilent } from './send_eth'; // This test uses the CFTester contract as the receiver for a CCM call. The contract will consume approximately -// the gas amount specified in the CCM message with an error margin. On top of that, the gas overhead of the -// CCM call itself is ~115k with some variability depending on the parameters. Overrall, the transaction should -// be broadcasted up to ~220k according to contract tests with some margin (~20k). 210k is a safe bet for a call -// being broadcasted, 230k shouldn't be. -const MAXIMUM_GAS_RECEIVED = 210000; -const GAS_BUDGET_MARGIN = 20000; -const tagSuffix = ' CcmGasLimit'; +// the gasLimitBudget amount specified in the CCM message with an error margin. On top of that, the gasLimitBudget overhead of the +// CCM call itself is ~115k with some variability depending on the parameters. We also add extra gasLimitBudget depending +// on the lenght of the message. +const MIN_BASE_GAS_OVERHEAD = 100000; +const BASE_GAS_OVERHEAD_BUFFER = 20000; +const ETHEREUM_BASE_FEE_MULTIPLIER = 2; +const CFE_GAS_LIMIT_CAP = 10000000; +// Arbitrary gas consumption values for testing. The total default gas used is then ~360-380k depending on the parameters. +let DEFAULT_GAS_CONSUMPTION = 260000; +const MIN_TEST_GAS_CONSUMPTION = 200000; +const MAX_TEST_GAS_CONSUMPTION = 4000000; +// The base overhead increases with message lenght. This is an approximation => BASE_GAS_OVERHEAD + messageLength * gasPerByte +const GAS_PER_BYTE = 16; let stopObservingCcmReceived = false; function gasTestCcmMetadata(sourceAsset: Asset, gasToConsume: number, gasBudgetFraction?: number) { const web3 = new Web3(process.env.ETH_ENDPOINT ?? 'http://127.0.0.1:8545'); + return newCcmMetadata( sourceAsset, web3.eth.abi.encodeParameters(['string', 'uint256'], ['GasTest', gasToConsume]), @@ -36,34 +44,25 @@ function gasTestCcmMetadata(sourceAsset: Asset, gasToConsume: number, gasBudgetF async function testGasLimitSwap( sourceAsset: Asset, destAsset: Asset, - gasToConsume: number, + testTag?: string, + gasToConsume?: number, gasBudgetFraction?: number, addressType?: BtcAddressType, ) { const chainflipApi = await getChainflipApi(); - const messageMetadata = gasTestCcmMetadata(sourceAsset, gasToConsume, gasBudgetFraction); + // Increase the gas consumption to make sure all the messages are unique + const gasConsumption = gasToConsume ?? DEFAULT_GAS_CONSUMPTION++; + const messageMetadata = gasTestCcmMetadata(sourceAsset, gasConsumption, gasBudgetFraction); const { destAddress, tag } = await prepareSwap( sourceAsset, destAsset, addressType, messageMetadata, - tagSuffix, + ` GasLimit${testTag || ''}`, ); - const ccmReceivedFailure = observeCcmReceived( - sourceAsset, - destAsset, - destAddress, - messageMetadata, - undefined, - () => stopObservingCcmReceived, - ).then((event) => { - if (event) - throw new Error(`${tag} CCM event emitted. Transaction should not have been broadcasted!`); - }); - const { depositAddress, channelId } = await requestNewSwap( sourceAsset, destAsset, @@ -73,180 +72,228 @@ async function testGasLimitSwap( ); // If sourceAsset is ETH then deposited gasAmount won't be swapped, so we need to observe the principal swap - // instead. In any other scenario, including when destAsset is ETH, both principal and gas are being swapped. - let egressGasAmount; - if (sourceAsset !== Assets.ETH) { - const swapScheduledHandle = observeSwapScheduled( + // instead. In any other scenario, including when destAsset is ETH, both principal and gasLimitBudget are being swapped. + let swapScheduledHandle; + if (sourceAsset === Assets.ETH) { + swapScheduledHandle = observeSwapScheduled( sourceAsset, - Assets.ETH, // Native destChain asset + destAsset, channelId, - SwapType.CcmGas, + SwapType.CcmPrincipal, ); + } else { + swapScheduledHandle = observeSwapScheduled(sourceAsset, Assets.ETH, channelId, SwapType.CcmGas); + } - // SwapExecuted is emitted at the same time as swapScheduled so we can't wait for swapId to be known. - const swapIdToEgressAmount: { [key: string]: string } = {}; - let swapScheduledObserved = false; - const swapExecutedHandle = observeEvent( - 'swapping:SwapExecuted', - chainflipApi, - (event) => { - swapIdToEgressAmount[event.data.swapId] = event.data.egressAmount; - return false; - }, - () => swapScheduledObserved, - ); - await send(sourceAsset, depositAddress); + // SwapExecuted is emitted at the same time as swapScheduled so we can't wait for swapId to be known. + const swapIdToEgressAmount: { [key: string]: string } = {}; + let swapScheduledObserved = false; + const swapExecutedHandle = observeEvent( + 'swapping:SwapExecuted', + chainflipApi, + (event) => { + swapIdToEgressAmount[event.data.swapId] = event.data.egressAmount; + return false; + }, + () => swapScheduledObserved, + ); + const swapIdToEgressId: { [key: string]: string } = {}; + const swapEgressHandle = observeEvent( + 'swapping:SwapEgressScheduled', + chainflipApi, + (event) => { + swapIdToEgressId[event.data.swapId] = event.data.egressId; + return false; + }, + () => swapScheduledObserved, + ); + const egressIdToBroadcastId: { [key: string]: string } = {}; + const ccmBroadcastHandle = observeEvent( + 'ethereumIngressEgress:CcmBroadcastRequested', + chainflipApi, + (event) => { + egressIdToBroadcastId[event.data.egressId] = event.data.broadcastId; + return false; + }, + () => swapScheduledObserved, + ); + await send(sourceAsset, depositAddress); - const { - data: { swapId }, - } = await swapScheduledHandle; + const { + data: { swapId }, + } = await swapScheduledHandle; - while (!(swapId in swapIdToEgressAmount)) { - await sleep(3000); - } - swapScheduledObserved = true; - await swapExecutedHandle; - egressGasAmount = Number(swapIdToEgressAmount[swapId].replace(/,/g, '')); - } else { - const swapScheduledHandle = observeSwapScheduled( - sourceAsset, - destAsset, - channelId, - SwapType.CcmPrincipal, - ); - Promise.all([send(sourceAsset, depositAddress), swapScheduledHandle]); - egressGasAmount = messageMetadata.gasBudget; + while ( + !( + swapId in swapIdToEgressAmount && + swapId in swapIdToEgressId && + swapIdToEgressId[swapId] in egressIdToBroadcastId + ) + ) { + await sleep(3000); } + swapScheduledObserved = true; + await Promise.all([swapExecutedHandle, swapEgressHandle, ccmBroadcastHandle]); + + console.log( + `${tag} swapId: ${swapId} broadcastId: ${egressIdToBroadcastId[swapIdToEgressId[swapId]]}`, + ); + + const egressBudgetAmount = + sourceAsset !== Assets.ETH + ? Number(swapIdToEgressAmount[swapId].replace(/,/g, '')) + : messageMetadata.gasBudget; const ethTrackedData = ( await observeEvent('ethereumChainTracking:ChainStateUpdated', chainflipApi) ).data.newChainState.trackedData; - const baseFee = Number(ethTrackedData.baseFee); - const priorityFee = Number(ethTrackedData.priorityFee); + const baseFee = Number(ethTrackedData.baseFee.replace(/,/g, '')); + const priorityFee = Number(ethTrackedData.priorityFee.replace(/,/g, '')); + + // Standard gasLimitBudget estimation for now. + const maxFeePerGas = ETHEREUM_BASE_FEE_MULTIPLIER * baseFee + priorityFee; - // Standard gas estimation => In the statechain we might do a less conservative estimation, otherwise - // a good amount of gas might end up being unused (gasLimit too low). - const maxFeePerGas = 2 * baseFee + priorityFee; + // On the state chain the gasLimit is calculated from the egressBudget and the MaxFeePerGas + // TODO: We should could consider doing the following, potentially adjusting that multiplier depending on the + // total gas limit amount + // gasLimitBudget = egressBudgetAmount / (1.25 * baseFee + priorityFee) + const gasLimitBudget = egressBudgetAmount / maxFeePerGas; - // Max Gas Limit budget - const gasLimitBudget = egressGasAmount / maxFeePerGas; + const byteLength = Web3.utils.hexToBytes(messageMetadata.message).length; - // TODO: Add a check for the gasLimitBudget once mainnet logic is implemented. For now just logging it. console.log( - `${tag} egressGasAmount: ${egressGasAmount}, baseFee: ${baseFee}, priorityFee: ${priorityFee}, gasLimitBudget: ${gasLimitBudget}`, + `${tag} egressBudgetAmount: ${egressBudgetAmount}, baseFee: ${baseFee}, priorityFee: ${priorityFee}, gasLimitBudget: ${gasLimitBudget}`, ); - await ccmReceivedFailure; + const minGasLimitRequired = gasConsumption + MIN_BASE_GAS_OVERHEAD + byteLength * GAS_PER_BYTE; + + // This is a very rough approximation for the gas limit required. A buffer is added to account for that. + if (minGasLimitRequired + BASE_GAS_OVERHEAD_BUFFER >= gasLimitBudget) { + observeCcmReceived( + sourceAsset, + destAsset, + destAddress, + messageMetadata, + undefined, + () => stopObservingCcmReceived, + ).then((event) => { + if (event !== undefined) { + throw new Error(`${tag} CCM event emitted. Transaction should not have been broadcasted!`); + } + }); + // Expect Broadcast Aborted + console.log(`${tag} Gas budget is too low. Expecting BroadcastAborted event.`); + await observeEvent( + 'ethereumBroadcaster:BroadcastAborted', + await getChainflipApi(), + (event) => event.data.broadcastId === egressIdToBroadcastId[swapIdToEgressId[swapId]], + ); + stopObservingCcmReceived = true; + console.log( + `${tag} Broadcast Aborted found! broadcastId: ${ + egressIdToBroadcastId[swapIdToEgressId[swapId]] + }`, + ); + } else if (minGasLimitRequired < gasLimitBudget) { + const ccmReceived = await observeCcmReceived( + sourceAsset, + destAsset, + destAddress, + messageMetadata, + ); + if (ccmReceived?.returnValues.ccmTestGasUsed < gasConsumption) { + throw new Error(`${tag} CCM event emitted. Gas consumed is less than expected!`); + } + const web3 = new Web3(process.env.ETH_ENDPOINT ?? 'http://127.0.0.1:8545'); + const receipt = await web3.eth.getTransactionReceipt(ccmReceived?.txHash as string); + const tx = await web3.eth.getTransaction(ccmReceived?.txHash as string); + const gasUsed = receipt.gasUsed; + const gasPrice = tx.gasPrice; + const totalFee = gasUsed * Number(gasPrice); + if (Math.trunc(tx.gas) !== Math.min(Math.trunc(gasLimitBudget), CFE_GAS_LIMIT_CAP)) { + throw new Error(`${tag} Gas limit in the transaction is different than the one expected!`); + } + // This should not happen by definition, as maxFeePerGas * gasLimit < egressBudgetAmount + if (totalFee > egressBudgetAmount) { + throw new Error(`${tag} Transaction fee paid is higher than the budget paid by the user!`); + } + console.log(`${tag} Swap success! TxHash: ${ccmReceived?.txHash as string}!`); + } +} + +// Spamming to raise Ethereum's fee, otherwise it will get stuck at almost zero fee (~7 wei) +let spam = true; +async function spamEthereum() { + while (spam) { + signAndSendTxEthSilent('0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', '1'); + await sleep(500); + } +} + +const usedNumbers = new Set(); + +function getRandomGasConsumption(): number { + const range = MAX_TEST_GAS_CONSUMPTION - MIN_TEST_GAS_CONSUMPTION + 1; + let randomInt = Math.floor(Math.random() * range) + MIN_TEST_GAS_CONSUMPTION; + while (usedNumbers.has(randomInt)) { + randomInt = Math.floor(Math.random() * range) + MIN_TEST_GAS_CONSUMPTION; + } + usedNumbers.add(randomInt); + return randomInt; } -// NOTE: In localnet the gasPrice is is extremely low (~7wei) so the gasBudget needed is very small. export async function testGasLimitCcmSwaps() { console.log('=== Testing GasLimit CCM swaps ==='); - // As of now, these won't ne broadcasted regardless of the gasBudget provided because the gas consumed on the egress is > 400k. - // However, with the final solution, in localnet this might be broadcasted (1% of the amount might be enough gas budget since - // gasPrice is extremely low). - // TODO: For final solution, make swaps that consume more gas than the gasBudget. - const gasLimitSwapsAborted = [ - testGasLimitSwap('DOT', 'FLIP', MAXIMUM_GAS_RECEIVED + GAS_BUDGET_MARGIN), - testGasLimitSwap('ETH', 'USDC', MAXIMUM_GAS_RECEIVED + GAS_BUDGET_MARGIN), - testGasLimitSwap('FLIP', 'ETH', MAXIMUM_GAS_RECEIVED + GAS_BUDGET_MARGIN), - testGasLimitSwap('BTC', 'ETH', MAXIMUM_GAS_RECEIVED + GAS_BUDGET_MARGIN), + // Spam ethereum with transfers to increase the gasLimitBudget price + const spamming = spamEthereum(); + + // The default gas budgets should allow for almost any reasonable gas consumption + const gasLimitSwapsDefault = [ + testGasLimitSwap('DOT', 'FLIP', undefined, getRandomGasConsumption()), + testGasLimitSwap('ETH', 'USDC', undefined, getRandomGasConsumption()), + testGasLimitSwap('FLIP', 'ETH', undefined, getRandomGasConsumption()), + testGasLimitSwap('BTC', 'ETH', undefined, getRandomGasConsumption()), ]; - // This amount of gas will be swapped into very little destination gas. Not into zero as that will cause a debug_assert to - // panic when not in release due to zero swap intput amount. So for now we provide the minimum so it gets swapped to just > 0. - // As of now this will be broadcasted anyway because the gasBudget is not checked (hardcoded to < 400k) and this is within budget. - // However, this shouldn't be broadcasted for mainnet. - const gasLimitSwapsInsufBudget = [ - // ~ 410 wei for gasBudget (after CcmGas swap) - testSwap( - 'DOT', - 'FLIP', - undefined, - gasTestCcmMetadata('DOT', MAXIMUM_GAS_RECEIVED, 10 ** 6), - tagSuffix + ' InsufficientGasBudget', - ), - // ~ 500 wei for gasBudget (no CcmGas swap executed) - testSwap( - 'ETH', - 'USDC', - undefined, - gasTestCcmMetadata('ETH', MAXIMUM_GAS_RECEIVED, 10 ** 17), - tagSuffix + ' InsufficientGasBudget', - ), - // ~ 450 wei for gasBudget (after CcmGas swap) - testSwap( - 'FLIP', - 'ETH', - undefined, - gasTestCcmMetadata('FLIP', MAXIMUM_GAS_RECEIVED, 2 * 10 ** 6), - tagSuffix + ' InsufficientGasBudget', - ), + // reducing gas budget input amount used for gas to achieve a gasLimitBudget ~= 4-500k, which is enough for the CCM broadcast. + const gasLimitSwapsSufBudget = [ + testGasLimitSwap('DOT', 'FLIP', ' sufBudget', undefined, 750), + testGasLimitSwap('ETH', 'USDC', ' sufBudget', undefined, 7500), + testGasLimitSwap('FLIP', 'ETH', ' sufBudget', undefined, 6000), + testGasLimitSwap('BTC', 'ETH', ' sufBudget', undefined, 750), ]; - // As of now this is broadcasted regardless of the gas budget and even when the final solution is implemented - // this should be broadcasted, since the gas budget should be enough, since by default gasBudget is 1% of the - // principal and the gasPrice is very low in localnet (~7wei). - const gasLimitSwapsBroadcasted = [ - testSwap( - 'DOT', - 'FLIP', - undefined, - gasTestCcmMetadata('DOT', MAXIMUM_GAS_RECEIVED), - tagSuffix + ' SufficientGasBudget', - ), - testSwap( - 'ETH', - 'USDC', - undefined, - gasTestCcmMetadata('ETH', MAXIMUM_GAS_RECEIVED), - tagSuffix + ' SufficientGasBudget', - ), - testSwap( - 'FLIP', - 'ETH', - undefined, - gasTestCcmMetadata('FLIP', MAXIMUM_GAS_RECEIVED), - tagSuffix + ' SufficientGasBudget', - ), - testSwap( - 'BTC', - 'ETH', - undefined, - gasTestCcmMetadata('BTC', MAXIMUM_GAS_RECEIVED), - tagSuffix + ' SufficientGasBudget', - ), + // None of this should be broadcasted as the gasLimitBudget is not enough + const gasLimitSwapsInsufBudget = [ + testGasLimitSwap('DOT', 'FLIP', ' insufBudget', undefined, 10 ** 4), + testGasLimitSwap('ETH', 'USDC', ' insufBudget', undefined, 10 ** 5), + testGasLimitSwap('FLIP', 'ETH', ' insufBudget', undefined, 10 ** 5), + testGasLimitSwap('BTC', 'ETH', ' insufBudget', undefined, 10 ** 4), ]; - let broadcastAborted = 0; - let stopObserveAborted = false; - const observeBroadcastAborted = observeEvent( - 'ethereumBroadcaster:BroadcastAborted', - await getChainflipApi(), - (_) => { - ++broadcastAborted; - if (broadcastAborted === gasLimitSwapsAborted.length) { - stopObservingCcmReceived = true; - } - if (broadcastAborted > gasLimitSwapsAborted.length) { - throw new Error('Broadcast Aborted Unexpected'); - } - // Continue observing for unexpected BroadcastAborted events - return false; - }, - () => stopObserveAborted, - ); + // This amount of gasLimitBudget will be swapped into very little gasLimitBudget. Not into zero as that will cause a debug_assert to + // panic when not in release due to zero swap intput amount. So for now we provide the minimum so it gets swapped to just > 0. + const gasLimitSwapsNoBudget = [ + testGasLimitSwap('DOT', 'FLIP', ' noBudget', undefined, 10 ** 6), + testGasLimitSwap('ETH', 'USDC', ' noBudget', undefined, 10 ** 8), + testGasLimitSwap('FLIP', 'ETH', ' noBudget', undefined, 10 ** 6), + testGasLimitSwap('BTC', 'ETH', ' noBudget', undefined, 10 ** 5), + ]; await Promise.all([ - ...gasLimitSwapsAborted, - ...gasLimitSwapsBroadcasted, + ...gasLimitSwapsSufBudget, ...gasLimitSwapsInsufBudget, + ...gasLimitSwapsDefault, + ...gasLimitSwapsNoBudget, ]); - stopObserveAborted = true; - await observeBroadcastAborted; + spam = false; + await spamming; + + // Make sure all the spamming has stopped to avoid triggering connectivity issues when running the next test. + await sleep(10000); console.log('=== GasLimit CCM test completed ==='); } diff --git a/bouncer/shared/send_eth.ts b/bouncer/shared/send_eth.ts index ed2b4e40b18..021e1601e02 100644 --- a/bouncer/shared/send_eth.ts +++ b/bouncer/shared/send_eth.ts @@ -26,7 +26,12 @@ export async function getNextEthNonce( }); } -export async function signAndSendTxEth(to: string, value?: string, data?: string, gas = 2000000) { +export async function signAndSendTxEthSilent( + to: string, + value?: string, + data?: string, + gas = 2000000, +) { const ethEndpoint = process.env.ETH_ENDPOINT ?? 'http://127.0.0.1:8545'; const web3 = new Web3(ethEndpoint); @@ -46,7 +51,11 @@ export async function signAndSendTxEth(to: string, value?: string, data?: string } }, ); + return receipt; +} +export async function signAndSendTxEth(to: string, value?: string, data?: string, gas = 2000000) { + const receipt = await signAndSendTxEthSilent(to, value, data, gas); console.log( 'Transaction complete, tx_hash: ' + receipt.transactionHash + diff --git a/engine/src/eth/retry_rpc.rs b/engine/src/eth/retry_rpc.rs index b79e2140354..440adbd756a 100644 --- a/engine/src/eth/retry_rpc.rs +++ b/engine/src/eth/retry_rpc.rs @@ -2,10 +2,7 @@ pub mod address_checker; use ethers::{ prelude::*, - types::{ - transaction::{eip2718::TypedTransaction, eip2930::AccessList}, - TransactionReceipt, - }, + types::{transaction::eip2930::AccessList, TransactionReceipt}, }; use utilities::task_scope::Scope; @@ -114,6 +111,8 @@ impl EthersRetryRpcApi for EthersRetryRpcClient { &self, tx: cf_chains::evm::Transaction, ) -> anyhow::Result { + // We arbitrarily set the MAX_GAS_LIMIT we are willing broadcast to 10M. + const MAX_GAS_LIMIT: u128 = 10_000_000; let log = RequestLog::new("broadcast_transaction".to_string(), Some(format!("{tx:?}"))); self.rpc_retry_client .request_with_limit( @@ -128,24 +127,36 @@ impl EthersRetryRpcApi for EthersRetryRpcClient { value: Some(tx.value), max_fee_per_gas: tx.max_fee_per_gas, max_priority_fee_per_gas: tx.max_priority_fee_per_gas, - gas: tx.gas_limit, + // geth uses the latest block gas limit as an upper bound + gas: None, access_list: AccessList::default(), from: Some(client.address()), nonce: None, }; let estimated_gas = client - .estimate_gas(&TypedTransaction::Eip1559(transaction_request.clone())) + .estimate_gas(&transaction_request) .await .context("Failed to estimate gas")?; - // increase the estimate by 50% - transaction_request.gas = Some( - estimated_gas - .saturating_mul(U256::from(3u64)) - .checked_div(U256::from(2u64)) - .unwrap(), - ); + transaction_request.gas = Some(match tx.gas_limit { + Some(gas_limit) => + if estimated_gas > gas_limit { + return Err(anyhow::anyhow!( + "Estimated gas is greater than the gas limit" + )) + } else { + gas_limit.min(MAX_GAS_LIMIT.into()) + }, + None => { + // increase the estimate by 33% for normal transactions + estimated_gas + .saturating_mul(U256::from(4u64)) + .checked_div(U256::from(3u64)) + .unwrap() + .min(MAX_GAS_LIMIT.into()) + }, + }); client .send_transaction(transaction_request) diff --git a/engine/src/eth/rpc.rs b/engine/src/eth/rpc.rs index 10d1b16da8a..bf353e94f7d 100644 --- a/engine/src/eth/rpc.rs +++ b/engine/src/eth/rpc.rs @@ -100,7 +100,7 @@ impl EthRpcClient { pub trait EthRpcApi: Send { fn address(&self) -> H160; - async fn estimate_gas(&self, req: &TypedTransaction) -> Result; + async fn estimate_gas(&self, req: &Eip1559TransactionRequest) -> Result; async fn send_transaction(&self, tx: Eip1559TransactionRequest) -> Result; @@ -131,8 +131,8 @@ impl EthRpcApi for EthRpcClient { self.signer.address() } - async fn estimate_gas(&self, req: &TypedTransaction) -> Result { - Ok(self.signer.estimate_gas(req, None).await?) + async fn estimate_gas(&self, req: &Eip1559TransactionRequest) -> Result { + Ok(self.signer.estimate_gas(&TypedTransaction::Eip1559(req.clone()), None).await?) } async fn send_transaction(&self, mut tx: Eip1559TransactionRequest) -> Result { diff --git a/eth-contract-abis/perseverance-0.9-rc6/CFTester.json b/eth-contract-abis/v0.8.0/CFTester.json similarity index 100% rename from eth-contract-abis/perseverance-0.9-rc6/CFTester.json rename to eth-contract-abis/v0.8.0/CFTester.json diff --git a/eth-contract-abis/perseverance-0.9-rc6/Deposit_bytecode.json b/eth-contract-abis/v0.8.0/Deposit_bytecode.json similarity index 100% rename from eth-contract-abis/perseverance-0.9-rc6/Deposit_bytecode.json rename to eth-contract-abis/v0.8.0/Deposit_bytecode.json diff --git a/eth-contract-abis/perseverance-0.9-rc6/IAddressChecker.json b/eth-contract-abis/v0.8.0/IAddressChecker.json similarity index 100% rename from eth-contract-abis/perseverance-0.9-rc6/IAddressChecker.json rename to eth-contract-abis/v0.8.0/IAddressChecker.json diff --git a/eth-contract-abis/perseverance-0.9-rc6/IFLIP.json b/eth-contract-abis/v0.8.0/IFLIP.json similarity index 100% rename from eth-contract-abis/perseverance-0.9-rc6/IFLIP.json rename to eth-contract-abis/v0.8.0/IFLIP.json diff --git a/eth-contract-abis/perseverance-0.9-rc6/IKeyManager.json b/eth-contract-abis/v0.8.0/IKeyManager.json similarity index 100% rename from eth-contract-abis/perseverance-0.9-rc6/IKeyManager.json rename to eth-contract-abis/v0.8.0/IKeyManager.json diff --git a/eth-contract-abis/perseverance-0.9-rc6/IStateChainGateway.json b/eth-contract-abis/v0.8.0/IStateChainGateway.json similarity index 100% rename from eth-contract-abis/perseverance-0.9-rc6/IStateChainGateway.json rename to eth-contract-abis/v0.8.0/IStateChainGateway.json diff --git a/eth-contract-abis/perseverance-0.9-rc6/IVault.json b/eth-contract-abis/v0.8.0/IVault.json similarity index 100% rename from eth-contract-abis/perseverance-0.9-rc6/IVault.json rename to eth-contract-abis/v0.8.0/IVault.json diff --git a/localnet/docker-compose.yml b/localnet/docker-compose.yml index f784e878dc6..bcb8a24ba11 100644 --- a/localnet/docker-compose.yml +++ b/localnet/docker-compose.yml @@ -1,7 +1,7 @@ version: "3.9" services: init: - image: ghcr.io/chainflip-io/chainflip-eth-contracts/localnet-initial-state:perseverance-0.9-rc6-1-node + image: ghcr.io/chainflip-io/chainflip-eth-contracts/localnet-initial-state:v0.8.0-1-node container_name: init platform: linux/amd64 volumes: @@ -16,7 +16,7 @@ services: - "/tmp/chainflip/data/redis-data:/data" geth: - image: ghcr.io/chainflip-io/chainflip-eth-contracts/geth:perseverance-0.9-rc6-1-node + image: ghcr.io/chainflip-io/chainflip-eth-contracts/geth:v0.8.0-1-node container_name: geth platform: linux/amd64 ports: diff --git a/state-chain/cf-integration-tests/Cargo.toml b/state-chain/cf-integration-tests/Cargo.toml index 247c28565da..f8fe9f57242 100644 --- a/state-chain/cf-integration-tests/Cargo.toml +++ b/state-chain/cf-integration-tests/Cargo.toml @@ -32,6 +32,7 @@ chainflip-node = { path = '../node' } pallet-authorship = { git = "https://github.com/chainflip-io/substrate.git", tag = "chainflip-monthly-2023-08+2" } pallet-cf-account-roles = { path = '../pallets/cf-account-roles' } pallet-cf-broadcast = { path = '../pallets/cf-broadcast' } +pallet-cf-chain-tracking = { path = '../pallets/cf-chain-tracking' } pallet-cf-emissions = { path = '../pallets/cf-emissions' } pallet-cf-environment = { path = '../pallets/cf-environment' } pallet-cf-flip = { path = '../pallets/cf-flip' } diff --git a/state-chain/cf-integration-tests/src/swapping.rs b/state-chain/cf-integration-tests/src/swapping.rs index 26f17b31b02..656f1dd2245 100644 --- a/state-chain/cf-integration-tests/src/swapping.rs +++ b/state-chain/cf-integration-tests/src/swapping.rs @@ -5,8 +5,10 @@ use cf_amm::{ }; use cf_chains::{ address::{AddressConverter, AddressDerivationApi, EncodedAddress}, - CcmChannelMetadata, CcmDepositMetadata, Chain, Ethereum, ForeignChain, ForeignChainAddress, - SwapOrigin, + assets::eth::Asset as EthAsset, + eth::{api::EthereumApi, EthereumTrackedData}, + CcmChannelMetadata, CcmDepositMetadata, Chain, ChainState, Ethereum, ExecutexSwapAndCall, + ForeignChain, ForeignChainAddress, SwapOrigin, TransactionBuilder, TransferAssetParams, }; use cf_primitives::{AccountId, AccountRole, Asset, AssetAmount, STABLE_ASSET}; use cf_test_utilities::{assert_events_eq, assert_events_match}; @@ -18,10 +20,15 @@ use frame_support::{ use pallet_cf_ingress_egress::DepositWitness; use pallet_cf_pools::{OrderId, RangeOrderSize}; use pallet_cf_swapping::CcmIdCounter; +use sp_core::U256; use state_chain_runtime::{ - chainflip::{address_derivation::AddressDerivation, ChainAddressConverter}, - AccountRoles, EthereumInstance, LiquidityPools, LiquidityProvider, Runtime, RuntimeCall, - RuntimeEvent, RuntimeOrigin, Swapping, System, Timestamp, Validator, Weight, Witnesser, + chainflip::{ + address_derivation::AddressDerivation, ChainAddressConverter, EthEnvironment, + EthTransactionBuilder, + }, + AccountRoles, EthereumChainTracking, EthereumInstance, LiquidityPools, LiquidityProvider, + Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, Swapping, System, Timestamp, Validator, + Weight, Witnesser, }; const DORIS: AccountId = AccountId::new([0x11; 32]); @@ -628,3 +635,84 @@ fn failed_swaps_are_rolled_back() { ); }); } + +#[test] +fn ethereum_ccm_can_calculate_gas_limits() { + super::genesis::default().build().execute_with(|| { + let current_epoch = Validator::current_epoch(); + let chain_state = ChainState:: { + block_height: 1, + tracked_data: EthereumTrackedData { + base_fee: 1_000_000u128, + priority_fee: 500_000u128, + }, + }; + + for node in Validator::current_authorities() { + assert_ok!(Witnesser::witness_at_epoch( + RuntimeOrigin::signed(node), + Box::new(RuntimeCall::EthereumChainTracking( + pallet_cf_chain_tracking::Call::update_chain_state { + new_chain_state: chain_state.clone(), + } + )), + current_epoch + )); + } + assert_eq!(EthereumChainTracking::chain_state(), Some(chain_state)); + + let make_ccm_call = |gas_budget: u128| { + as ExecutexSwapAndCall>::new_unsigned( + (ForeignChain::Ethereum, 1), + TransferAssetParams:: { + asset: EthAsset::Flip, + amount: 1_000, + to: Default::default(), + }, + ForeignChain::Ethereum, + None, + gas_budget, + vec![], + ) + .unwrap() + }; + + // Each unit of gas costs 2 * 1_000_000 + 500_000 = 2_500_000 + assert_eq!( + EthTransactionBuilder::calculate_gas_limit(&make_ccm_call(2_499_999)), + Some(U256::from(0)) + ); + assert_eq!( + EthTransactionBuilder::calculate_gas_limit(&make_ccm_call(2_500_000)), + Some(U256::from(1)) + ); + // 1_000_000_000_000 / (2 * 1_000_000 + 500_000) = 400_000 + assert_eq!( + EthTransactionBuilder::calculate_gas_limit(&make_ccm_call(1_000_000_000_000u128)), + Some(U256::from(400_000)) + ); + + // Can handle divide by zero case. Practically this should never happen. + let chain_state = ChainState:: { + block_height: 2, + tracked_data: EthereumTrackedData { base_fee: 0u128, priority_fee: 0u128 }, + }; + + for node in Validator::current_authorities() { + assert_ok!(Witnesser::witness_at_epoch( + RuntimeOrigin::signed(node), + Box::new(RuntimeCall::EthereumChainTracking( + pallet_cf_chain_tracking::Call::update_chain_state { + new_chain_state: chain_state.clone(), + } + )), + current_epoch + )); + } + + assert_eq!( + EthTransactionBuilder::calculate_gas_limit(&make_ccm_call(1_000_000_000u128)), + Some(U256::from(0)) + ); + }); +} diff --git a/state-chain/chains/src/btc/api.rs b/state-chain/chains/src/btc/api.rs index 83d3c684f9f..a02573dae5f 100644 --- a/state-chain/chains/src/btc/api.rs +++ b/state-chain/chains/src/btc/api.rs @@ -109,6 +109,7 @@ impl> ExecutexSwapAndCall for Bitc _transfer_param: TransferAssetParams, _source_chain: ForeignChain, _source_address: Option, + _gas_budget: ::ChainAmount, _message: Vec, ) -> Result { Err(DispatchError::Other("Bitcoin's ExecutexSwapAndCall is not supported.")) diff --git a/state-chain/chains/src/dot/api.rs b/state-chain/chains/src/dot/api.rs index 9acfda46552..de5173a7374 100644 --- a/state-chain/chains/src/dot/api.rs +++ b/state-chain/chains/src/dot/api.rs @@ -121,6 +121,7 @@ where _transfer_param: TransferAssetParams, _source_chain: ForeignChain, _source_address: Option, + _gas_budget: ::ChainAmount, _message: Vec, ) -> Result { Err(DispatchError::Other("Not implemented")) diff --git a/state-chain/chains/src/eth.rs b/state-chain/chains/src/eth.rs index d5429930bf0..163e52edced 100644 --- a/state-chain/chains/src/eth.rs +++ b/state-chain/chains/src/eth.rs @@ -17,7 +17,7 @@ pub use ethabi::{ Address, Hash as TxHash, Token, Uint, Word, }; use evm::api::EvmReplayProtection; -use frame_support::sp_runtime::RuntimeDebug; +use frame_support::sp_runtime::{FixedPointNumber, FixedU64, RuntimeDebug}; use scale_info::TypeInfo; use serde::{Deserialize, Serialize}; use sp_std::{cmp::min, convert::TryInto, str}; @@ -65,6 +65,17 @@ pub struct EthereumTrackedData { pub priority_fee: ::ChainAmount, } +impl EthereumTrackedData { + pub fn max_fee_per_gas( + &self, + base_fee_multiplier: FixedU64, + ) -> ::ChainAmount { + base_fee_multiplier + .saturating_mul_int(self.base_fee) + .saturating_add(self.priority_fee) + } +} + impl Default for EthereumTrackedData { #[track_caller] fn default() -> Self { diff --git a/state-chain/chains/src/eth/api.rs b/state-chain/chains/src/eth/api.rs index a31f07a3b10..d0caf01409b 100644 --- a/state-chain/chains/src/eth/api.rs +++ b/state-chain/chains/src/eth/api.rs @@ -256,6 +256,7 @@ where transfer_param: TransferAssetParams, source_chain: ForeignChain, source_address: Option, + gas_budget: ::ChainAmount, message: Vec, ) -> Result { let transfer_param = EncodableTransferAssetParams { @@ -271,6 +272,7 @@ where transfer_param, source_chain, source_address, + gas_budget, message, ), ))) @@ -370,6 +372,12 @@ impl ApiCall for EthereumApi { } } +impl EthereumApi { + pub fn gas_budget(&self) -> Option<::ChainAmount> { + map_over_api_variants!(self, call, call.gas_budget()) + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, Encode, Decode, TypeInfo, MaxEncodedLen)] pub enum EthereumContract { StateChainGateway, diff --git a/state-chain/chains/src/evm.rs b/state-chain/chains/src/evm.rs index db2b92b131e..744213b4063 100644 --- a/state-chain/chains/src/evm.rs +++ b/state-chain/chains/src/evm.rs @@ -418,7 +418,7 @@ impl Transaction { Ok(()) } - /// Returns an error if any of the recovered transactoin parameters do not match those specified + /// Returns an error if any of the recovered transaction parameters do not match those specified /// in the original [Transaction]. /// /// See [CheckedTransactionParameter]. diff --git a/state-chain/chains/src/evm/api.rs b/state-chain/chains/src/evm/api.rs index 11966c20e90..a9f82f9d26c 100644 --- a/state-chain/chains/src/evm/api.rs +++ b/state-chain/chains/src/evm/api.rs @@ -139,6 +139,9 @@ pub trait EvmCall { .collect::>(), )) } + fn gas_budget(&self) -> Option<::ChainAmount> { + None + } } #[derive(Encode, Decode, TypeInfo, MaxEncodedLen, Clone, RuntimeDebug, PartialEq, Eq)] @@ -160,6 +163,10 @@ impl EvmTransactionBuilder { pub fn chain_id(&self) -> EvmChainId { self.replay_protection.chain_id } + + pub fn gas_budget(&self) -> Option<::ChainAmount> { + self.call.gas_budget() + } } pub type EvmChainId = u64; diff --git a/state-chain/chains/src/evm/api/execute_x_swap_and_call.rs b/state-chain/chains/src/evm/api/execute_x_swap_and_call.rs index 4dc8ac5e25f..e023e2884b1 100644 --- a/state-chain/chains/src/evm/api/execute_x_swap_and_call.rs +++ b/state-chain/chains/src/evm/api/execute_x_swap_and_call.rs @@ -19,6 +19,8 @@ pub struct ExecutexSwapAndCall { source_chain: u32, /// The source address of the transfer. source_address: Vec, + /// Amount of funds that can be used as gas used to execute this call on the target chain. + gas_budget: ::ChainAmount, /// Message that needs to be passed through. message: Vec, } @@ -30,11 +32,12 @@ impl ExecutexSwapAndCall { transfer_param: EncodableTransferAssetParams, source_chain: ForeignChain, source_address: Option, + gas_budget: ::ChainAmount, message: Vec, ) -> Self { let (source_chain, source_address) = Self::destructure_address(source_chain, source_address); - Self { egress_id, transfer_param, source_chain, source_address, message } + Self { egress_id, transfer_param, source_chain, source_address, gas_budget, message } } fn destructure_address( @@ -77,6 +80,10 @@ impl EvmCall for ExecutexSwapAndCall { self.message.clone().tokenize(), ] } + + fn gas_budget(&self) -> Option<::ChainAmount> { + Some(self.gas_budget) + } } #[cfg(test)] @@ -100,6 +107,7 @@ mod test_execute_x_swap_and_execute { const FAKE_VAULT_ADDR: [u8; 20] = asymmetrise([0xdf; 20]); const CHAIN_ID: u64 = 1; const NONCE: u64 = 9; + const GAS_BUDGET: ::ChainAmount = 100_000u128; let dummy_transfer_asset_param = EncodableTransferAssetParams { asset: Address::from_slice(&[5; 20]), @@ -135,6 +143,7 @@ mod test_execute_x_swap_and_execute { dummy_transfer_asset_param.clone(), dummy_src_chain, Some(dummy_src_address), + GAS_BUDGET, dummy_message.clone(), ), ); diff --git a/state-chain/chains/src/lib.rs b/state-chain/chains/src/lib.rs index a79da767903..5f325a99f2f 100644 --- a/state-chain/chains/src/lib.rs +++ b/state-chain/chains/src/lib.rs @@ -18,6 +18,7 @@ use frame_support::{ }; use scale_info::TypeInfo; use serde::{Deserialize, Serialize}; +use sp_core::U256; use sp_std::{ cmp::Ord, convert::{Into, TryFrom}, @@ -246,6 +247,11 @@ where current_key: &<::ChainCrypto as ChainCrypto>::AggKey, signature: &<::ChainCrypto as ChainCrypto>::ThresholdSignature, ) -> bool; + + /// Calculate the Units of gas that is allowed to make this call. + fn calculate_gas_limit(_call: &Call) -> Option { + Default::default() + } } /// Contains all the parameters required to fetch incoming transactions on an external chain. @@ -343,6 +349,7 @@ pub trait ExecutexSwapAndCall: ApiCall { transfer_param: TransferAssetParams, source_chain: ForeignChain, source_address: Option, + gas_budget: C::ChainAmount, message: Vec, ) -> Result; } diff --git a/state-chain/pallets/cf-ingress-egress/src/lib.rs b/state-chain/pallets/cf-ingress-egress/src/lib.rs index c8ffa3066d1..d269b7ebaad 100644 --- a/state-chain/pallets/cf-ingress-egress/src/lib.rs +++ b/state-chain/pallets/cf-ingress-egress/src/lib.rs @@ -73,6 +73,7 @@ pub(crate) struct CrossChainMessage { pub source_address: Option, // Where funds might be returned to if the message fails. pub cf_parameters: Vec, + pub gas_budget: C::ChainAmount, } impl CrossChainMessage { @@ -666,6 +667,7 @@ impl, I: 'static> Pallet { }, ccm.source_chain, ccm.source_address, + ccm.gas_budget, ccm.message, ) { Ok(api_call) => { @@ -836,25 +838,28 @@ impl, I: 'static> EgressApi for Pallet { asset: TargetChainAsset, amount: TargetChainAmount, destination_address: TargetChainAccount, - maybe_message: Option, + maybe_ccm_with_gas_budget: Option<(CcmDepositMetadata, TargetChainAmount)>, ) -> EgressId { let egress_counter = EgressIdCounter::::mutate(|id| { *id = id.saturating_add(1); *id }); let egress_id = (>::TargetChain::get(), egress_counter); - match maybe_message { - Some(CcmDepositMetadata { source_chain, source_address, channel_metadata }) => - ScheduledEgressCcm::::append(CrossChainMessage { - egress_id, - asset, - amount, - destination_address: destination_address.clone(), - message: channel_metadata.message, - cf_parameters: channel_metadata.cf_parameters, - source_chain, - source_address, - }), + match maybe_ccm_with_gas_budget { + Some(( + CcmDepositMetadata { source_chain, source_address, channel_metadata }, + gas_budget, + )) => ScheduledEgressCcm::::append(CrossChainMessage { + egress_id, + asset, + amount, + destination_address: destination_address.clone(), + message: channel_metadata.message, + cf_parameters: channel_metadata.cf_parameters, + source_chain, + source_address, + gas_budget, + }), None => ScheduledEgressFetchOrTransfer::::append(FetchOrTransfer::< T::TargetChain, >::Transfer { diff --git a/state-chain/pallets/cf-ingress-egress/src/tests.rs b/state-chain/pallets/cf-ingress-egress/src/tests.rs index a321ff2b220..f9cfbcac564 100644 --- a/state-chain/pallets/cf-ingress-egress/src/tests.rs +++ b/state-chain/pallets/cf-ingress-egress/src/tests.rs @@ -87,6 +87,7 @@ fn blacklisted_asset_will_not_egress_via_batch_all() { fn blacklisted_asset_will_not_egress_via_ccm() { new_test_ext().execute_with(|| { let asset = ETH_ETH; + let gas_budget = 1000u128; let ccm = CcmDepositMetadata { source_chain: ForeignChain::Ethereum, source_address: Some(ForeignChainAddress::Eth([0xcf; 20].into())), @@ -101,8 +102,18 @@ fn blacklisted_asset_will_not_egress_via_ccm() { assert_ok!(IngressEgress::enable_or_disable_egress(RuntimeOrigin::root(), asset, true)); // Eth should be blocked while Flip can be sent - IngressEgress::schedule_egress(asset, 1_000, ALICE_ETH_ADDRESS, Some(ccm.clone())); - IngressEgress::schedule_egress(ETH_FLIP, 1_000, ALICE_ETH_ADDRESS, Some(ccm.clone())); + IngressEgress::schedule_egress( + asset, + 1_000, + ALICE_ETH_ADDRESS, + Some((ccm.clone(), gas_budget)), + ); + IngressEgress::schedule_egress( + ETH_FLIP, + 1_000, + ALICE_ETH_ADDRESS, + Some((ccm.clone(), gas_budget)), + ); IngressEgress::on_finalize(1); @@ -118,6 +129,7 @@ fn blacklisted_asset_will_not_egress_via_ccm() { source_chain: ForeignChain::Ethereum, source_address: ccm.source_address.clone(), cf_parameters: ccm.channel_metadata.cf_parameters, + gas_budget, }] ); @@ -509,12 +521,13 @@ fn can_egress_ccm() { new_test_ext().execute_with(|| { let destination_address: H160 = [0x01; 20].into(); let destination_asset = eth::Asset::Eth; + let gas_budget = 1_000u128; let ccm = CcmDepositMetadata { source_chain: ForeignChain::Ethereum, source_address: Some(ForeignChainAddress::Eth([0xcf; 20].into())), channel_metadata: CcmChannelMetadata { message: vec![0x00, 0x01, 0x02], - gas_budget: 1_000, + gas_budget, cf_parameters: vec![], } }; @@ -523,7 +536,7 @@ fn can_egress_ccm() { destination_asset, amount, destination_address, - Some(ccm.clone()) + Some((ccm.clone(), gas_budget)) ); assert!(ScheduledEgressFetchOrTransfer::::get().is_empty()); @@ -537,6 +550,7 @@ fn can_egress_ccm() { cf_parameters: vec![], source_chain: ForeignChain::Ethereum, source_address: Some(ForeignChainAddress::Eth([0xcf; 20].into())), + gas_budget, } ]); System::assert_last_event(RuntimeEvent::IngressEgress( @@ -561,6 +575,7 @@ fn can_egress_ccm() { }, ccm.source_chain, ccm.source_address, + gas_budget, ccm.channel_metadata.message, ).unwrap()]); diff --git a/state-chain/pallets/cf-swapping/Cargo.toml b/state-chain/pallets/cf-swapping/Cargo.toml index 01e5cf4d482..f018f19a524 100644 --- a/state-chain/pallets/cf-swapping/Cargo.toml +++ b/state-chain/pallets/cf-swapping/Cargo.toml @@ -34,6 +34,7 @@ frame-system = { git = "https://github.com/chainflip-io/substrate.git", tag = "c sp-arithmetic = { git = "https://github.com/chainflip-io/substrate.git", tag = "chainflip-monthly-2023-08+2", default-features = false } sp-std = { git = "https://github.com/chainflip-io/substrate.git", tag = "chainflip-monthly-2023-08+2", default-features = false } +sp-runtime = { git = "https://github.com/chainflip-io/substrate.git", tag = "chainflip-monthly-2023-08+2", default-features = false } [dev-dependencies] cf-test-utilities = { path = '../../test-utilities' } @@ -56,6 +57,7 @@ std = [ 'scale-info/std', 'sp-arithmetic/std', 'sp-std/std', + 'sp-runtime/std', ] runtime-benchmarks = [ 'cf-chains/runtime-benchmarks', diff --git a/state-chain/pallets/cf-swapping/src/lib.rs b/state-chain/pallets/cf-swapping/src/lib.rs index 44a71ad3c83..d60a5706d80 100644 --- a/state-chain/pallets/cf-swapping/src/lib.rs +++ b/state-chain/pallets/cf-swapping/src/lib.rs @@ -7,17 +7,14 @@ use cf_primitives::{ Asset, AssetAmount, ChannelId, ForeignChain, SwapLeg, TransactionHash, STABLE_ASSET, }; use cf_traits::{impl_pallet_safe_mode, liquidity::SwappingApi, CcmHandler, DepositApi}; -use frame_support::{ - pallet_prelude::*, - sp_runtime::{ - traits::{BlockNumberProvider, Get, Saturating}, - DispatchError, Permill, - }, - storage::with_storage_layer, -}; +use frame_support::{pallet_prelude::*, storage::with_storage_layer}; use frame_system::pallet_prelude::*; pub use pallet::*; use sp_arithmetic::{helpers_128bit::multiply_by_rational_with_rounding, traits::Zero, Rounding}; +use sp_runtime::{ + traits::{BlockNumberProvider, Get, Saturating}, + DispatchError, Permill, +}; use sp_std::{collections::btree_map::BTreeMap, vec, vec::Vec}; #[cfg(test)] @@ -900,7 +897,7 @@ pub mod pallet { ccm_swap.destination_asset, ccm_output_principal, ccm_swap.destination_address.clone(), - Some(ccm_swap.deposit_metadata), + Some((ccm_swap.deposit_metadata, ccm_output_gas)), ); if let Some(swap_id) = ccm_swap.principal_swap_id { Self::deposit_event(Event::::SwapEgressScheduled { diff --git a/state-chain/pallets/cf-swapping/src/tests.rs b/state-chain/pallets/cf-swapping/src/tests.rs index 0a68321426c..2e9943a0191 100644 --- a/state-chain/pallets/cf-swapping/src/tests.rs +++ b/state-chain/pallets/cf-swapping/src/tests.rs @@ -600,6 +600,7 @@ fn can_process_ccms_via_swap_deposit_address() { destination_address: ForeignChainAddress::Eth(Default::default()), message: vec![0x01], cf_parameters: vec![], + gas_budget, },] ); @@ -683,6 +684,7 @@ fn can_process_ccms_via_extrinsic() { destination_address: ForeignChainAddress::Eth(Default::default()), message: vec![0x02], cf_parameters: vec![], + gas_budget, },] ); @@ -772,6 +774,7 @@ fn can_handle_ccms_with_non_native_gas_asset() { destination_address: ForeignChainAddress::Eth(Default::default()), message: vec![0x00], cf_parameters: vec![], + gas_budget, },] ); @@ -856,6 +859,7 @@ fn can_handle_ccms_with_native_gas_asset() { destination_address: ForeignChainAddress::Eth(Default::default()), message: vec![0x00], cf_parameters: vec![], + gas_budget, },] ); @@ -925,6 +929,7 @@ fn can_handle_ccms_with_no_swaps_needed() { destination_address: ForeignChainAddress::Eth(Default::default()), message: vec![0x00], cf_parameters: vec![], + gas_budget, },] ); diff --git a/state-chain/runtime/src/chainflip.rs b/state-chain/runtime/src/chainflip.rs index fd4ed6ca416..29ede494021 100644 --- a/state-chain/runtime/src/chainflip.rs +++ b/state-chain/runtime/src/chainflip.rs @@ -39,8 +39,8 @@ use cf_chains::{ EvmCrypto, Transaction, }, AnyChain, ApiCall, CcmChannelMetadata, CcmDepositMetadata, Chain, ChainCrypto, - ChainEnvironment, DepositChannel, ForeignChain, ReplayProtectionProvider, SetCommKeyWithAggKey, - SetGovKeyWithAggKey, TransactionBuilder, + ChainEnvironment, ChainState, DepositChannel, ForeignChain, ReplayProtectionProvider, + SetCommKeyWithAggKey, SetGovKeyWithAggKey, TransactionBuilder, }; use cf_primitives::{chains::assets, AccountRole, Asset, BasisPoints, ChannelId, EgressId}; use cf_traits::{ @@ -52,7 +52,10 @@ use cf_traits::{ use codec::{Decode, Encode}; use frame_support::{ dispatch::{DispatchError, DispatchErrorWithPostInfo, PostDispatchInfo}, - sp_runtime::traits::{BlockNumberProvider, UniqueSaturatedFrom, UniqueSaturatedInto}, + sp_runtime::{ + traits::{BlockNumberProvider, UniqueSaturatedFrom, UniqueSaturatedInto}, + FixedU64, + }, traits::Get, }; pub use missed_authorship_slots::MissedAuraSlots; @@ -146,39 +149,33 @@ impl cf_traits::WaivedFees for WaivedFees { } } +/// We are willing to pay at most 2x the base fee. This is approximately the theoretical +/// limit of the rate of increase of the base fee over 6 blocks (12.5% per block). +const ETHEREUM_BASE_FEE_MULTIPLIER: FixedU64 = FixedU64::from_rational(2, 1); + pub struct EthTransactionBuilder; impl TransactionBuilder> for EthTransactionBuilder { fn build_transaction( signed_call: &EthereumApi, ) -> ::Transaction { - // TODO: This should take into account the ccm gas budget. (See PRO-161) - const CCM_GAS_LIMIT: u64 = 400_000; - const DEFAULT_GAS_LIMIT: u64 = 15_000_000; - let gas_limit = match signed_call { - EthereumApi::ExecutexSwapAndCall(_) => Some(CCM_GAS_LIMIT.into()), - // None means there is no gas limit. - _ => Some(DEFAULT_GAS_LIMIT.into()), - }; Transaction { chain_id: signed_call.replay_protection().chain_id, contract: signed_call.replay_protection().contract_address, data: signed_call.chain_encoded(), - gas_limit, + gas_limit: Self::calculate_gas_limit(signed_call), ..Default::default() } } fn refresh_unsigned_data(unsigned_tx: &mut ::Transaction) { - let tracked_data = EthereumChainTracking::chain_state().unwrap().tracked_data; - // double the last block's base fee. This way we know it'll be selectable for at least 6 - // blocks (12.5% increase on each block) - let max_fee_per_gas = tracked_data - .base_fee - .saturating_mul(2) - .saturating_add(tracked_data.priority_fee); - unsigned_tx.max_fee_per_gas = Some(U256::from(max_fee_per_gas)); - unsigned_tx.max_priority_fee_per_gas = Some(U256::from(tracked_data.priority_fee)); + if let Some(ChainState { tracked_data, .. }) = EthereumChainTracking::chain_state() { + let max_fee_per_gas = tracked_data.max_fee_per_gas(ETHEREUM_BASE_FEE_MULTIPLIER); + unsigned_tx.max_fee_per_gas = Some(U256::from(max_fee_per_gas)); + unsigned_tx.max_priority_fee_per_gas = Some(U256::from(tracked_data.priority_fee)); + } else { + log::warn!("No chain data for Ethereum. This should never happen. Please check Chain Tracking data."); + } } fn is_valid_for_rebroadcast( @@ -194,6 +191,31 @@ impl TransactionBuilder> for EthTransactio signature, ) } + + /// Calculate the gas limit for a Ethereum call, using the current gas price. + /// Currently for only CCM calls, the gas limit is calculated as: + /// Gas limit = gas_budget / (multiplier * base_gas_price + priority_fee) + /// All other calls uses a default gas limit. + fn calculate_gas_limit(call: &EthereumApi) -> Option { + if let Some(gas_budget) = call.gas_budget() { + let max_fee_per_gas = EthereumChainTracking::chain_state() + .or_else(||{ + log::warn!("No chain data for Ethereum. This should never happen. Please check Chain Tracking data."); + None + })? + .tracked_data + .max_fee_per_gas(ETHEREUM_BASE_FEE_MULTIPLIER); + Some(gas_budget + .checked_div(max_fee_per_gas) + .unwrap_or_else(||{ + log::warn!("Current gas price for Ethereum is 0. This should never happen. Please check Chain Tracking data."); + Default::default() + }) + .into()) + } else { + None + } + } } pub struct DotTransactionBuilder; @@ -514,7 +536,7 @@ macro_rules! impl_egress_api_for_anychain { asset: Asset, amount: ::ChainAmount, destination_address: ::ChainAccount, - maybe_message: Option, + maybe_ccm_with_gas_budget: Option<(CcmDepositMetadata, ::ChainAmount)>, ) -> EgressId { match asset.into() { $( @@ -524,7 +546,7 @@ macro_rules! impl_egress_api_for_anychain { destination_address .try_into() .expect("This address cast is ensured to succeed."), - maybe_message, + maybe_ccm_with_gas_budget.map(|(metadata, gas_budget)| (metadata, gas_budget.try_into().expect("Chain's Amount must be compatible with u128."))), ), )+ diff --git a/state-chain/traits/src/lib.rs b/state-chain/traits/src/lib.rs index 6c8bc7e87be..52f36cb51e3 100644 --- a/state-chain/traits/src/lib.rs +++ b/state-chain/traits/src/lib.rs @@ -704,7 +704,7 @@ pub trait EgressApi { asset: C::ChainAsset, amount: C::ChainAmount, destination_address: C::ChainAccount, - maybe_message: Option, + maybe_ccm_with_gas_budget: Option<(CcmDepositMetadata, C::ChainAmount)>, ) -> EgressId; } @@ -713,7 +713,7 @@ impl EgressApi for T { _asset: assets::eth::Asset, _amount: ::ChainAmount, _destination_address: ::ChainAccount, - _maybe_message: Option, + _maybe_ccm_with_gas_budget: Option<(CcmDepositMetadata, ::ChainAmount)>, ) -> EgressId { (ForeignChain::Ethereum, 0) } @@ -724,7 +724,7 @@ impl EgressApi for T { _asset: assets::dot::Asset, _amount: ::ChainAmount, _destination_address: ::ChainAccount, - _maybe_message: Option, + _maybe_ccm_with_gas_budget: Option<(CcmDepositMetadata, ::ChainAmount)>, ) -> EgressId { (ForeignChain::Polkadot, 0) } diff --git a/state-chain/traits/src/mocks/api_call.rs b/state-chain/traits/src/mocks/api_call.rs index 94d66349621..057d22d082a 100644 --- a/state-chain/traits/src/mocks/api_call.rs +++ b/state-chain/traits/src/mocks/api_call.rs @@ -99,6 +99,7 @@ pub struct MockExecutexSwapAndCall { transfer_param: TransferAssetParams, source_chain: ForeignChain, source_address: Option, + gas_budget: ::ChainAmount, message: Vec, _phantom: PhantomData, } @@ -109,6 +110,7 @@ impl ExecutexSwapAndCall for MockEthereumApiCall { transfer_param: TransferAssetParams, source_chain: ForeignChain, source_address: Option, + gas_budget: ::ChainAmount, message: Vec, ) -> Result { if MockEthEnvironment::lookup(transfer_param.asset).is_none() { @@ -120,6 +122,7 @@ impl ExecutexSwapAndCall for MockEthereumApiCall { transfer_param, source_chain, source_address, + gas_budget, message, _phantom: PhantomData, })) diff --git a/state-chain/traits/src/mocks/egress_handler.rs b/state-chain/traits/src/mocks/egress_handler.rs index 295cf37be91..e215a01c60d 100644 --- a/state-chain/traits/src/mocks/egress_handler.rs +++ b/state-chain/traits/src/mocks/egress_handler.rs @@ -25,6 +25,7 @@ pub enum MockEgressParameter { destination_address: C::ChainAccount, message: Vec, cf_parameters: Vec, + gas_budget: C::ChainAmount, }, } @@ -60,20 +61,21 @@ impl EgressApi for MockEgressHandler { asset: ::ChainAsset, amount: ::ChainAmount, destination_address: ::ChainAccount, - maybe_message: Option, + maybe_ccm_with_gas_budget: Option<(CcmDepositMetadata, ::ChainAmount)>, ) -> EgressId { ::mutate_value(b"SCHEDULED_EGRESSES", |storage| { if storage.is_none() { *storage = Some(vec![]); } storage.as_mut().map(|v| { - v.push(match maybe_message { - Some(message) => MockEgressParameter::::Ccm { + v.push(match maybe_ccm_with_gas_budget { + Some((message, gas_budget)) => MockEgressParameter::::Ccm { asset, amount, destination_address, message: message.channel_metadata.message, cf_parameters: message.channel_metadata.cf_parameters, + gas_budget, }, None => MockEgressParameter::::Swap { asset, amount, destination_address }, });