Skip to content

Commit 2dc434e

Browse files
authored
chore: added gas estimation api to planner (#11953)
## Description Includes code to add gas estimation in planner. It estimates two different types of gas values. Outgoing as well as incoming. For the outgoing txs i.e Agoric -> EVM. We have a record of `gasLimitEstimates` that are representative of real world gas amounts used during makeAccount / supply / withdraw tx. At the moment there are only two types of gas limits we've added. one for the execution of the factory contract. the other for the execution of the wallet contract. In the future in place of the `Wallet` gas limit, we would have a list of gas Limits representing each item in the matrix {supply, withdraw} x {Aave, Compund, Beefy} since each is silghtly different and it would be more efficient to have the minimum possible value for each For incoming, the API call the estimation seems to be off according to our testing. The API gives an esitmate which is 20x higher than the value we use in testnet (0.0002 vs 0.005) ### Testing Considerations Tested in this Tx: https://testnet.axelarscan.io/gmp/0xa1b399894a97b70bf38302d1297fa1f6b2fd198b1fffc0529c0bb1e5c89725af-4 It supplied the build amount for the outgoing tx but also calculated the eth amount for the incoming tx
2 parents 45d40ea + 6e44475 commit 2dc434e

21 files changed

+444
-81
lines changed

packages/portfolio-contract/src/network/network-spec.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,29 @@ export type TransferProtocol =
1616
| 'cctpReturn'
1717
| 'cctpSlow'
1818
| 'local';
19-
export type FeeMode = 'toUSDN' | 'gmpCall' | 'gmpTransfer';
19+
/**
20+
* Link to Factory and Wallet contracts:
21+
* https://github.com/agoric-labs/agoric-to-axelar-local/blob/cd6087fa44de3b019b2cdac6962bb49b6a2bc1ca/packages/axelar-local-dev-cosmos/src/__tests__/contracts/Factory.sol
22+
*
23+
* Steps submitted to the contract are expected to include fee/gas payment
24+
* details which vary by the traversed link.
25+
* - toUSDN: transferring into USDN transfer reduces the *payload* (e.g., $10k
26+
* might get reduced to $9995)
27+
* - makeEvmAccount: the fee for executing the Factory contract to
28+
* create a new remote wallet
29+
* - evmToNoble: the fee for running the tx to send tokens from the remote wallet
30+
* to Noble
31+
* - evmToPool: the fee for sending and executing a tx on the Wallet contract
32+
* to supply tokens to a specified pool
33+
* - poolToEvm: the fee for sending and executing a tx on the Wallet contract
34+
* to withdraw tokens from a specified pool
35+
*/
36+
export type FeeMode =
37+
| 'toUSDN'
38+
| 'makeEvmAccount'
39+
| 'evmToNoble'
40+
| 'evmToPool'
41+
| 'poolToEvm';
2042

2143
// Chains (hubs)
2244
export interface ChainSpec {

packages/portfolio-contract/src/network/network.prod.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,23 +58,23 @@ export const PROD_NETWORK: NetworkSpec = {
5858
transfer: 'cctpReturn',
5959
variableFeeBps: 0,
6060
timeSec: 20,
61-
feeMode: 'gmpTransfer',
61+
feeMode: 'makeEvmAccount',
6262
},
6363
{
6464
src: '@noble',
6565
dest: '@Avalanche',
6666
transfer: 'cctpReturn',
6767
variableFeeBps: 0,
6868
timeSec: 20,
69-
feeMode: 'gmpTransfer',
69+
feeMode: 'makeEvmAccount',
7070
},
7171
{
7272
src: '@noble',
7373
dest: '@Ethereum',
7474
transfer: 'cctpReturn',
7575
variableFeeBps: 0,
7676
timeSec: 20,
77-
feeMode: 'gmpTransfer',
77+
feeMode: 'makeEvmAccount',
7878
},
7979
// Fast USDC (Axelar GMP)
8080
{
@@ -83,20 +83,23 @@ export const PROD_NETWORK: NetworkSpec = {
8383
transfer: 'fastusdc',
8484
variableFeeBps: 15,
8585
timeSec: 45,
86+
feeMode: 'evmToNoble',
8687
},
8788
{
8889
src: '@Avalanche',
8990
dest: '@noble',
9091
transfer: 'fastusdc',
9192
variableFeeBps: 15,
9293
timeSec: 45,
94+
feeMode: 'evmToNoble',
9395
},
9496
{
9597
src: '@Ethereum',
9698
dest: '@noble',
9799
transfer: 'fastusdc',
98100
variableFeeBps: 15,
99101
timeSec: 45,
102+
feeMode: 'evmToNoble',
100103
},
101104
// IBC between agoric and noble
102105
{ src: '@agoric', dest: '@noble', transfer: 'ibc', variableFeeBps: 0, timeSec: 10 },

packages/portfolio-contract/src/plan-solve.ts

Lines changed: 109 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,25 @@ export interface SolvedEdgeFlow {
8787
/** Model shape for javascript-lp-solver */
8888
export type LpModel = IModel<string, string>;
8989

90+
/**
91+
* Link to Factory and Wallet contracts:
92+
* https://github.com/agoric-labs/agoric-to-axelar-local/blob/cd6087fa44de3b019b2cdac6962bb49b6a2bc1ca/packages/axelar-local-dev-cosmos/src/__tests__/contracts/Factory.sol
93+
*
94+
* Gas estimation interface -
95+
* @see {@link ../../../services/ymax-planner/src/gas-estimation.ts}
96+
* - getFactoryContractEstimate: Estimate gas fees for executing the factory
97+
* contract to create a wallet on the specified chain
98+
* - getReturnFeeEstimate: Estimate return fees for sending a transaction back
99+
* from the factory contract to Agoric
100+
* - getWalletEstimate: Estimate gas fees for remote wallet operations on the
101+
* specified chain
102+
*/
103+
type GasEstimator = {
104+
getWalletEstimate: (chainName: AxelarChain) => Promise<bigint>;
105+
getFactoryContractEstimate: (chainName: AxelarChain) => Promise<bigint>;
106+
getReturnFeeEstimate: (chainName: AxelarChain) => Promise<bigint>;
107+
};
108+
90109
// --- keep existing type declarations above ---
91110

92111
/**
@@ -150,26 +169,35 @@ export const buildBaseGraph = (
150169
const hub = `@${chainName}` as AssetPlaceRef;
151170
if (node === hub) continue;
152171

153-
const feeMode = Object.keys(AxelarChain).includes(chainName)
154-
? { feeMode: 'gmpCall' as FeeMode }
155-
: {};
172+
const chainIsEvm = Object.keys(AxelarChain).includes(chainName);
156173
const base: Omit<FlowEdge, 'src' | 'dest' | 'id'> = {
157174
capacity: (Number.MAX_SAFE_INTEGER + 1) / 4,
158175
variableFee: vf,
159176
fixedFee: 0,
160177
timeFixed: tf,
161178
via: 'local',
162-
...feeMode,
163179
};
164180

165-
// eslint-disable-next-line no-plusplus
166-
edges.push({ id: `e${eid++}`, src: node, dest: hub, ...base });
181+
edges.push({
182+
// eslint-disable-next-line no-plusplus
183+
id: `e${eid++}`,
184+
src: node,
185+
dest: hub,
186+
...base,
187+
...(chainIsEvm ? { feeMode: 'poolToEvm' } : {}),
188+
});
167189

168190
// Skip @agoric → +agoric edge
169191
if (node === '+agoric') continue;
170192

171-
// eslint-disable-next-line no-plusplus
172-
edges.push({ id: `e${eid++}`, src: hub, dest: node, ...base });
193+
edges.push({
194+
// eslint-disable-next-line no-plusplus
195+
id: `e${eid++}`,
196+
src: hub,
197+
dest: node,
198+
...base,
199+
...(chainIsEvm ? { feeMode: 'evmToPool' } : {}),
200+
});
173201
}
174202

175203
// Return mutable graph (do NOT harden so we can add inter-chain links later)
@@ -373,10 +401,11 @@ export const solveRebalance = async (
373401
return flows;
374402
};
375403

376-
export const rebalanceMinCostFlowSteps = (
404+
export const rebalanceMinCostFlowSteps = async (
377405
flows: SolvedEdgeFlow[],
378406
graph: RebalanceGraph,
379-
): MovementDesc[] => {
407+
gasEstimator: GasEstimator,
408+
): Promise<MovementDesc[]> => {
380409
const supplies = new Map(
381410
typedEntries(graph.supplies).filter(([_place, amount]) => amount > 0),
382411
);
@@ -425,37 +454,67 @@ export const rebalanceMinCostFlowSteps = (
425454
pendingFlows.delete(chosen.edge.id);
426455
lastChain = chosen.srcChain;
427456
}
428-
429-
const steps: MovementDesc[] = prioritized.map(({ edge, flow }) => {
430-
Number.isSafeInteger(flow) ||
431-
Fail`flow ${flow} for edge ${edge} is not a safe integer`;
432-
const amount = AmountMath.make(graph.brand, BigInt(flow));
433-
434-
let details = {};
435-
switch (edge.feeMode) {
436-
case 'gmpTransfer':
437-
// TODO: Rather than hard-code, derive from Axelar `estimateGasFee`.
438-
// https://docs.axelar.dev/dev/axelarjs-sdk/axelar-query-api#estimategasfee
439-
details = { fee: AmountMath.make(graph.feeBrand, 30_000_000n) };
440-
break;
441-
case 'gmpCall':
442-
// TODO: Rather than hard-code, derive from Axelar `estimateGasFee`.
443-
// https://docs.axelar.dev/dev/axelarjs-sdk/axelar-query-api#estimategasfee
444-
details = { fee: AmountMath.make(graph.feeBrand, 30_000_000n) };
445-
break;
446-
case 'toUSDN': {
447-
// NOTE USDN transfer incurs a fee on output amount in basis points
448-
const usdnOut =
449-
(BigInt(flow) * (10000n - BigInt(edge.variableFee))) / 10000n;
450-
details = { detail: { usdnOut } };
451-
break;
457+
/**
458+
* Pad each fee estimate in case the landscape changes between estimation and
459+
* execution.
460+
*/
461+
const padFeeEstimate = (estimate: bigint): bigint => estimate * 3n;
462+
463+
const steps: MovementDesc[] = await Promise.all(
464+
prioritized.map(async ({ edge, flow }) => {
465+
Number.isSafeInteger(flow) ||
466+
Fail`flow ${flow} for edge ${edge} is not a safe integer`;
467+
const amount = AmountMath.make(graph.brand, BigInt(flow));
468+
469+
await null;
470+
let details = {};
471+
switch (edge.feeMode) {
472+
case 'makeEvmAccount': {
473+
const feeValue = await gasEstimator.getFactoryContractEstimate(
474+
chainOf(edge.dest) as AxelarChain,
475+
);
476+
details = {
477+
// XXX: not using getReturnFeeEstimate until we can verify axelar
478+
// API is accurate for this
479+
detail: { evmGas: 200_000_000_000_000n },
480+
fee: AmountMath.make(graph.feeBrand, padFeeEstimate(feeValue)),
481+
};
482+
break;
483+
}
484+
// XXX: revisit https://github.com/Agoric/agoric-sdk/pull/11953#discussion_r2383034184
485+
case 'poolToEvm':
486+
case 'evmToPool': {
487+
const feeValue = await gasEstimator.getWalletEstimate(
488+
chainOf(edge.dest) as AxelarChain,
489+
);
490+
details = {
491+
fee: AmountMath.make(graph.feeBrand, padFeeEstimate(feeValue)),
492+
};
493+
break;
494+
}
495+
case 'evmToNoble': {
496+
const feeValue = await gasEstimator.getWalletEstimate(
497+
chainOf(edge.src) as AxelarChain,
498+
);
499+
details = {
500+
fee: AmountMath.make(graph.feeBrand, padFeeEstimate(feeValue)),
501+
};
502+
break;
503+
}
504+
case 'toUSDN': {
505+
// NOTE USDN transfer incurs a fee on output amount in basis points
506+
const usdnOut =
507+
(BigInt(flow) * (10000n - BigInt(edge.variableFee))) / 10000n;
508+
details = { detail: { usdnOut } };
509+
break;
510+
}
511+
default:
512+
break;
452513
}
453-
default:
454-
break;
455-
}
456514

457-
return { src: edge.src, dest: edge.dest, amount, ...details };
458-
});
515+
return { src: edge.src, dest: edge.dest, amount, ...details };
516+
}),
517+
);
459518

460519
return harden(steps);
461520
};
@@ -476,8 +535,17 @@ export const planRebalanceFlow = async (opts: {
476535
brand: Amount['brand'];
477536
feeBrand: Amount['brand'];
478537
mode?: RebalanceMode;
538+
gasEstimator: GasEstimator;
479539
}) => {
480-
const { network, current, target, brand, feeBrand, mode = 'fastest' } = opts;
540+
const {
541+
network,
542+
current,
543+
target,
544+
brand,
545+
feeBrand,
546+
mode = 'fastest',
547+
gasEstimator,
548+
} = opts;
481549
// TODO remove "automatic" values that should be static
482550
const graph = makeGraphFromDefinition(
483551
network,
@@ -497,7 +565,7 @@ export const planRebalanceFlow = async (opts: {
497565
preflightValidateNetworkPlan(network as any, current as any, target as any);
498566
throw err;
499567
}
500-
const steps = rebalanceMinCostFlowSteps(flows, graph);
568+
const steps = await rebalanceMinCostFlowSteps(flows, graph, gasEstimator);
501569
return harden({ graph, model, flows, steps });
502570
};
503571

packages/portfolio-contract/src/plan-transfers.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,12 @@ export const makePortfolioSteps = async <
119119
'<Deposit>': deposit,
120120
};
121121

122+
const staticGasEstimator = {
123+
getWalletEstimate: async () => 30_000_000n,
124+
getFactoryContractEstimate: async () => 30_000_000n,
125+
getReturnFeeEstimate: async () => 200_000_000_000_000n,
126+
};
127+
122128
// Run the solver to compute movement steps
123129
const { steps: raw } = await planRebalanceFlow({
124130
network: PROD_NETWORK,
@@ -127,6 +133,7 @@ export const makePortfolioSteps = async <
127133
brand,
128134
feeBrand: brand, // Use same brand for fees in this context
129135
mode: 'cheapest',
136+
gasEstimator: staticGasEstimator,
130137
});
131138

132139
// Inject USDN detail and EVM fees to match existing behavior/tests

packages/portfolio-contract/test/mocks.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,3 +399,9 @@ export const gmpAddresses: GmpAddresses = harden({
399399
'axelar1dv4u5k73pzqrxlzujxg3qp8kvc3pje7jtdvu72npnt5zhq05ejcsn5qme5',
400400
AXELAR_GAS: 'axelar1aythygn6z5thymj6tmzfwekzh05ewg3l7d6y89',
401401
});
402+
403+
export const gasEstimator = {
404+
getWalletEstimate: async () => 10_000_000n,
405+
getFactoryContractEstimate: async () => 10_000_000n,
406+
getReturnFeeEstimate: async () => 200_000_000_000_000n,
407+
};

packages/portfolio-contract/test/network/buildGraph.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { AmountMath } from '@agoric/ertp';
44
import type { NetworkSpec } from '../../src/network/network-spec.js';
55
import { makeGraphFromDefinition } from '../../src/network/buildGraph.js';
66
import { planRebalanceFlow } from '../../src/plan-solve.js';
7+
import { gasEstimator } from '../mocks.js';
78

89
const brand = Far('TestBrand') as any;
910
const feeBrand = Far('TestFeeBrand') as any;
@@ -109,6 +110,7 @@ test('planRebalanceFlow uses NetworkSpec (legacy links param ignored at type lev
109110
brand,
110111
feeBrand,
111112
mode: 'cheapest',
113+
gasEstimator,
112114
});
113115
// Ensure only the two provided inter edges (plus intra) exist, not link-derived ones
114116
const hubEdges = res.graph.edges.filter(

packages/portfolio-contract/test/network/test-network.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ export const TEST_NETWORK: NetworkSpec = {
4848
{ src: '@Avalanche', dest: '@noble', transfer: 'cctpSlow', variableFeeBps: 0, timeSec: 1080 },
4949
{ src: '@Ethereum', dest: '@noble', transfer: 'cctpSlow', variableFeeBps: 0, timeSec: 1080 },
5050
// Return path
51-
{ src: '@noble', dest: '@Arbitrum', transfer: 'cctpReturn', variableFeeBps: 0, timeSec: 20, feeMode: 'gmpTransfer' },
52-
{ src: '@noble', dest: '@Polygon' as AssetPlaceRef, transfer: 'cctpReturn', variableFeeBps: 0, timeSec: 20, feeMode: 'gmpTransfer' },
53-
{ src: '@noble', dest: '@Avalanche', transfer: 'cctpReturn', variableFeeBps: 0, timeSec: 20, feeMode: 'gmpTransfer' },
54-
{ src: '@noble', dest: '@Ethereum', transfer: 'cctpReturn', variableFeeBps: 0, timeSec: 20, feeMode: 'gmpTransfer' },
51+
{ src: '@noble', dest: '@Arbitrum', transfer: 'cctpReturn', variableFeeBps: 0, timeSec: 20, feeMode: 'makeEvmAccount' },
52+
{ src: '@noble', dest: '@Polygon' as AssetPlaceRef, transfer: 'cctpReturn', variableFeeBps: 0, timeSec: 20, feeMode: 'makeEvmAccount' },
53+
{ src: '@noble', dest: '@Avalanche', transfer: 'cctpReturn', variableFeeBps: 0, timeSec: 20, feeMode: 'makeEvmAccount' },
54+
{ src: '@noble', dest: '@Ethereum', transfer: 'cctpReturn', variableFeeBps: 0, timeSec: 20, feeMode: 'makeEvmAccount' },
5555
// IBC agoric<->noble
5656
{ src: '@agoric', dest: '@noble', transfer: 'ibc', variableFeeBps: 0, timeSec: 10 },
5757
{ src: '@noble', dest: '@agoric', transfer: 'ibc', variableFeeBps: 0, timeSec: 10 },

0 commit comments

Comments
 (0)