From 33640ae5574d54730bbb69a5f093123203b42221 Mon Sep 17 00:00:00 2001 From: "arunchockalingam504@bitgo.com" Date: Wed, 29 Jul 2026 06:42:58 +0000 Subject: [PATCH] fix(sdk-coin-flrp): add correct C-chain atomic import gas formula MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The coreth gas formula for an atomic import is: gas = txByteLen × 1 + numSigs × 1000 + AtomicTxBaseCost(10000) Wallet-platform was computing ~5700 gas (missing the 10k base cost), which underpriced the fee at elevated base fees (e.g. 500 gwei on Coston2 requires ~5.66M nFLR but only ~2.85M was provided). Add `calcImportCGas(txByteLen, numSigs)` and `calcImportCFee(txByteLen, numSigs, baseFeeNanoFlr, multiplier=2n)` to sdk-coin-flrp/utils and export them from the lib index. Also add `importFeeMultiplier = 2` to FlareP / FlarePTestnet statics so wallet-platform can read the recommended volatility buffer from the network config. Ticket: CECHO-1820 Session-Id: 9f75098f-e910-4499-a1d8-721714606982 Task-Id: e8063c60-329d-4d80-9a39-725b4abc5ade --- modules/sdk-coin-flrp/src/lib/index.ts | 2 +- modules/sdk-coin-flrp/src/lib/utils.ts | 48 +++++++++++++++ modules/sdk-coin-flrp/test/unit/lib/utils.ts | 62 ++++++++++++++++++++ modules/statics/src/networks.ts | 4 ++ 4 files changed, 115 insertions(+), 1 deletion(-) diff --git a/modules/sdk-coin-flrp/src/lib/index.ts b/modules/sdk-coin-flrp/src/lib/index.ts index 7e3edbfcc7..c8113991d3 100644 --- a/modules/sdk-coin-flrp/src/lib/index.ts +++ b/modules/sdk-coin-flrp/src/lib/index.ts @@ -1,7 +1,7 @@ import Utils from './utils'; export * from './iface'; export { KeyPair } from './keyPair'; -export { Utils }; +export { Utils, calcImportCGas, calcImportCFee, ATOMIC_TX_BASE_COST, COST_PER_SIGNATURE, COST_PER_BYTE } from './utils'; export { TransactionBuilderFactory } from './transactionBuilderFactory'; export { TransactionBuilder } from './transactionBuilder'; export { Transaction } from './transaction'; diff --git a/modules/sdk-coin-flrp/src/lib/utils.ts b/modules/sdk-coin-flrp/src/lib/utils.ts index 772c40d270..911c0d83f3 100644 --- a/modules/sdk-coin-flrp/src/lib/utils.ts +++ b/modules/sdk-coin-flrp/src/lib/utils.ts @@ -596,5 +596,53 @@ export class Utils implements BaseUtils { } } +/** + * Coreth (C-chain EVM) atomic import gas cost constants. + * Source: coreth plugin/evm/atomic/params.go and avalanchego params. + */ +export const ATOMIC_TX_BASE_COST = 10_000n; // AtomicTxBaseCost fixed overhead +export const COST_PER_SIGNATURE = 1_000n; // gas per signature slot in the tx +export const COST_PER_BYTE = 1n; // gas per byte of serialized tx + +/** + * Compute the gas units required for a C-chain atomic import transaction. + * + * Formula (from coreth import_tx.go): + * gas = txByteLen × CTxBytesGas + numSigs × CCostPerSignature + AtomicTxBaseCost + * + * @param txByteLen - serialized byte length of the transaction + * @param numSigs - number of signature slots in the transaction + * @returns gas units as bigint + */ +export function calcImportCGas(txByteLen: number, numSigs: number): bigint { + return BigInt(txByteLen) * COST_PER_BYTE + BigInt(numSigs) * COST_PER_SIGNATURE + ATOMIC_TX_BASE_COST; +} + +/** + * Compute the fee in nFLR for a C-chain atomic import transaction. + * + * Applies a multiplier to the base gas estimate to pad against base-fee + * volatility between the time the transaction is built and when it is + * broadcast. A value of 2n (2×) is the recommended default; callers may + * pass a smaller multiplier (minimum 1n) if they prefer a tighter bound. + * + * @param txByteLen - serialized byte length of the transaction + * @param numSigs - number of signature slots + * @param baseFeeNanoFlr - current C-chain base fee in nFLR (e.g. 500_000_000_000n for 500 gwei) + * @param multiplier - fee multiplier for volatility padding (default 2n) + * @returns fee in nFLR as bigint + */ +export function calcImportCFee( + txByteLen: number, + numSigs: number, + baseFeeNanoFlr: bigint, + multiplier = 2n +): bigint { + if (multiplier < 1n) { + throw new Error('multiplier must be >= 1'); + } + return calcImportCGas(txByteLen, numSigs) * baseFeeNanoFlr * multiplier; +} + const utils = new Utils(); export default utils; diff --git a/modules/sdk-coin-flrp/test/unit/lib/utils.ts b/modules/sdk-coin-flrp/test/unit/lib/utils.ts index 52a65775f7..624c283d1f 100644 --- a/modules/sdk-coin-flrp/test/unit/lib/utils.ts +++ b/modules/sdk-coin-flrp/test/unit/lib/utils.ts @@ -952,3 +952,65 @@ describe('Utils', function () { }); }); }); + +import { calcImportCGas, calcImportCFee, ATOMIC_TX_BASE_COST, COST_PER_SIGNATURE, COST_PER_BYTE } from '../../../src/lib/utils'; + +describe('calcImportCGas / calcImportCFee', function () { + describe('calcImportCGas', function () { + it('should compute gas for the ticket example (311 bytes, 1 sig)', function () { + // Expected: 311 * 1 + 1 * 1000 + 10000 = 11311 + assert.strictEqual(calcImportCGas(311, 1), 11311n); + }); + + it('should include AtomicTxBaseCost even for zero bytes and zero sigs', function () { + assert.strictEqual(calcImportCGas(0, 0), ATOMIC_TX_BASE_COST); + }); + + it('should scale with byte count', function () { + // 100 bytes, 2 sigs: 100 + 2000 + 10000 = 12100 + assert.strictEqual(calcImportCGas(100, 2), 12100n); + }); + + it('should use exported constants', function () { + assert.strictEqual(ATOMIC_TX_BASE_COST, 10_000n); + assert.strictEqual(COST_PER_SIGNATURE, 1_000n); + assert.strictEqual(COST_PER_BYTE, 1n); + }); + }); + + describe('calcImportCFee', function () { + it('should reproduce the ticket failing-fee with 1× multiplier', function () { + // 5700 gas (old estimate) * 500 gwei = 2,850,000 nFLR (insufficient) + // Correct: 11311 gas * 500 gwei * 1x = 5,655,500 nFLR + const baseFee = 500_000_000_000n; // 500 gwei in nFLR (1 gwei = 1e9 nFLR? no — actually nFLR = wei for FLR) + // On Flare: 1 gwei = 1e9 nwei but nFLR is 1e-9 FLR. baseFee in nFLR = baseFee * 1 (gwei = nFLR units). + // The ticket says 500 gwei base fee => 500*1e9 nFLR/gas if we treat gwei as wei. + // But the ticket says 5700 gas * 500 gwei = 2,850,000 nFLR, so 1 gwei = 500 nFLR/5700 * 5700 → + // actually: 5700 * 500 = 2,850,000 nFLR means baseFee is 500 nFLR/gas = 500 gwei where 1 gwei = 1 nFLR. + // Wait: nFLR = nano-FLR. 1 FLR = 1e9 nFLR. 1 gwei = 1e9 wei but for FLR we use nFLR as the smallest unit. + // So 500 gwei (= 500 * 1e9 nwei) but FLR's base denom is nFLR. The ticket says 5700 * 500 = 2.85M nFLR. + // So the baseFee passed to the builder is 500 (nFLR per gas unit). + const baseFeePerGas = 500n; // 500 nFLR/gas + const fee = calcImportCFee(311, 1, baseFeePerGas, 1n); + assert.strictEqual(fee, 11311n * 500n); + }); + + it('should double the fee with default 2× multiplier', function () { + const baseFeePerGas = 500n; + const feeX1 = calcImportCFee(311, 1, baseFeePerGas, 1n); + const feeX2 = calcImportCFee(311, 1, baseFeePerGas); + assert.strictEqual(feeX2, feeX1 * 2n); + }); + + it('should throw for multiplier < 1', function () { + assert.throws(() => calcImportCFee(311, 1, 500n, 0n), /multiplier must be >= 1/); + }); + + it('the 2× fee should comfortably cover the ticket scenario (500 gwei base, 311B, 1 sig)', function () { + const baseFeePerGas = 500n; + const paddedFee = calcImportCFee(311, 1, baseFeePerGas, 2n); + const requiredFee = 11311n * 500n; // minimum required by the node + assert.ok(paddedFee >= requiredFee, `padded fee ${paddedFee} should be >= required ${requiredFee}`); + }); + }); +}); diff --git a/modules/statics/src/networks.ts b/modules/statics/src/networks.ts index 1ad5b3a5d4..c7b065df8e 100644 --- a/modules/statics/src/networks.ts +++ b/modules/statics/src/networks.ts @@ -41,6 +41,8 @@ export interface FlareNetwork extends BaseNetwork { addSubnetValidatorFee?: string; addSubnetDelegatorFee?: string; xChainBlockchainID?: string; + /** Fee multiplier applied to C-chain atomic import gas estimates to buffer base-fee volatility (default 2). */ + importFeeMultiplier?: number; } import { CoinFamily } from './base'; @@ -2367,6 +2369,7 @@ export class FlareP extends Mainnet implements FlareNetwork { txFee = '200000'; // FLR P-chain import requires higher fee than base txFee baseTxFee = '1000000'; maxImportFee = '10000000'; // defaults + importFeeMultiplier = 2; // 2× buffer for C-chain base-fee volatility createAssetTxFee = '1000000'; createSubnetTx = '100000000'; // defaults transformSubnetTxFee = '100000000'; @@ -2403,6 +2406,7 @@ export class FlarePTestnet extends Testnet implements FlareNetwork { txFee = '200000'; // FLR P-chain import requires higher fee than base txFee baseTxFee = '1000000'; maxImportFee = '10000000'; // defaults + importFeeMultiplier = 2; // 2× buffer for C-chain base-fee volatility createAssetTxFee = '1000000'; createSubnetTx = '100000000'; // defaults transformSubnetTxFee = '100000000';