Repository: Agoric/agoric-sdk
Pull request #11289: chore: update EVM dest fees; test against product spreadsheet
Author: login: dckc
Labels: name: automerge:rebase
description: Automatically rebase updates, then merge
Created: 2025-04-16T22:23:47Z | Merged: 2025-04-17T17:44:42Z

refs: #11149

## Description

Update EVM relayer fees to 0.50 USDC for ETH L1 (`eip155:1`) and 0.01 USDC for L2s.

### Security / Documentation Considerations

Fees must be sufficient to recover the cost of operating the contract.

Parties to the contract (user, liquidity providers, BLD stakers) must understand the fee structures and the specific fees.

### Scaling Considerations

none

### Testing Considerations

Test against a transcription of a spreadsheet negotiated with product.

### Upgrade Considerations

takes effect as part of #11149


Diff:
diff --git a/packages/fast-usdc-deploy/src/upgrade-evm-dests.core.js b/packages/fast-usdc-deploy/src/upgrade-evm-dests.core.js
index 9e0826a00d2..0b5d328ab44 100644
--- a/packages/fast-usdc-deploy/src/upgrade-evm-dests.core.js
+++ b/packages/fast-usdc-deploy/src/upgrade-evm-dests.core.js
@@ -26,7 +26,7 @@ const { make } = AmountMath;
 
 const { keys } = Object;
 
-const externalConfigContext = /** @type {const} */ ({
+export const externalConfigContext = /** @type {const} */ ({
   /** @type {Brand<'nat'>} */
   USDC: Far('USDC Brand'),
 });
@@ -37,7 +37,7 @@ const externalConfigContext = /** @type {const} */ ({
  */
 const USDC = value => make(externalConfigContext.USDC, value);
 
-const config = /** @type {const} */ ({
+export const config = /** @type {const} */ ({
   MAINNET: {
     agoricToNoble: {
       id: 'connection-72',
@@ -57,28 +57,17 @@ const config = /** @type {const} */ ({
         version: 'ics20-1',
       },
     },
-    /** FIXME #11149 use real values */
     legibleDestinationOverrides: toExternalConfig(
-      harden({
-        'eip155:1': {
-          relay: USDC(1_000_000n),
-        },
-        'eip155:43114': {
-          relay: USDC(500_000n),
-        },
-        'eip155:10': {
-          relay: USDC(500_000n),
-        },
-        'eip155:42161': {
-          relay: USDC(500_000n),
-        },
-        'eip155:8453': {
-          relay: USDC(500_000n),
-        },
-        'eip155:137': {
-          relay: USDC(500_000n),
-        },
-      }),
+      harden(
+        /** @type {FeeConfig['destinationOverrides']} */ ({
+          'eip155:1': { relay: USDC(500_000n) }, // ethereum L1
+          'eip155:43114': { relay: USDC(10_000n) }, // avalanche
+          'eip155:10': { relay: USDC(10_000n) }, // optimism
+          'eip155:42161': { relay: USDC(10_000n) }, // arbitrum
+          'eip155:8453': { relay: USDC(10_000n) }, // base
+          'eip155:137': { relay: USDC(10_000n) }, // polygon
+        }),
+      ),
       externalConfigContext,
       DestinationOverridesShape,
     ),
@@ -117,7 +106,6 @@ export const upgradeEvmDests = async (
   trace('options', options);
   const {
     agoricToNoble = config.MAINNET.agoricToNoble,
-    // @ts-expect-error Type instantiation is excessively deep and possibly infinite.
     legibleDestinationOverrides = config.MAINNET.legibleDestinationOverrides,
     fastUsdcCode = assert.fail('missing bundleID'),
   } = options;
diff --git a/packages/fast-usdc-deploy/test/snapshots/fast-usdc.test.ts.md b/packages/fast-usdc-deploy/test/snapshots/fast-usdc.test.ts.md
index e2db30ddac9..aded043a903 100644
--- a/packages/fast-usdc-deploy/test/snapshots/fast-usdc.test.ts.md
+++ b/packages/fast-usdc-deploy/test/snapshots/fast-usdc.test.ts.md
@@ -271,37 +271,37 @@ Generated by [AVA](https://avajs.dev).
             'eip155:1': {
               relay: {
                 brand: Object @Alleged: USDC brand {},
-                value: 1000000n,
+                value: 500000n,
               },
             },
             'eip155:10': {
               relay: {
                 brand: Object @Alleged: USDC brand {},
-                value: 500000n,
+                value: 10000n,
               },
             },
             'eip155:137': {
               relay: {
                 brand: Object @Alleged: USDC brand {},
-                value: 500000n,
+                value: 10000n,
               },
             },
             'eip155:42161': {
               relay: {
                 brand: Object @Alleged: USDC brand {},
-                value: 500000n,
+                value: 10000n,
               },
             },
             'eip155:43114': {
               relay: {
                 brand: Object @Alleged: USDC brand {},
-                value: 500000n,
+                value: 10000n,
               },
             },
             'eip155:8453': {
               relay: {
                 brand: Object @Alleged: USDC brand {},
-                value: 500000n,
+                value: 10000n,
               },
             },
           },
diff --git a/packages/fast-usdc-deploy/test/snapshots/fast-usdc.test.ts.snap b/packages/fast-usdc-deploy/test/snapshots/fast-usdc.test.ts.snap
index b5b877e3abd..f8929036f4d 100644
Binary files a/packages/fast-usdc-deploy/test/snapshots/fast-usdc.test.ts.snap and b/packages/fast-usdc-deploy/test/snapshots/fast-usdc.test.ts.snap differ
diff --git a/packages/fast-usdc-deploy/test/utils/fee-dest.test.ts b/packages/fast-usdc-deploy/test/utils/fee-dest.test.ts
new file mode 100644
index 00000000000..0e8d366dbd5
--- /dev/null
+++ b/packages/fast-usdc-deploy/test/utils/fee-dest.test.ts
@@ -0,0 +1,81 @@
+/** @file test EVM dest fees vs. examples from product spreadsheet */
+import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js';
+
+import { AmountMath } from '@agoric/ertp';
+import { makeRatio, multiplyBy, parseRatio } from '@agoric/ertp/src/ratio.js';
+import type { FeeConfig } from '@agoric/fast-usdc';
+import { DestinationOverridesShape } from '@agoric/fast-usdc/src/type-guards.js';
+import { makeFeeTools } from '@agoric/fast-usdc/src/utils/fees.js';
+import type { AccountId } from '@agoric/orchestration';
+import cctpChainInfo from '@agoric/orchestration/src/cctp-chain-info.js';
+import {
+  config as evmConfig,
+  externalConfigContext,
+} from '../../src/upgrade-evm-dests.core.js';
+import { fromExternalConfig } from '../../src/utils/config-marshal.js';
+
+type Dollars = `$${string}`;
+const numeral = (amt: Dollars) => amt.replace(/[$,]/g, '');
+
+/** Spreadsheet header becomes a tuple type with column names used as tags */
+type ExampleRow = [
+  destination: string,
+  amount: Dollars,
+  flatPlusRelay: Dollars,
+  variableFee: Dollars,
+  totalFee: Dollars,
+  netAdvance: Dollars,
+  contractCommission: Dollars,
+];
+
+const examples: ExampleRow[] = [
+  ['Default', '$1,500', '$0.01', '$1.50', '$1.5100', '$1,498.4900', '$0.3020'],
+  ['Ethereum', '$1,500', '$0.51', '$1.50', '$2.0100', '$1,497.9900', '$0.8020'],
+  ['Optimism', '$1,500', '$0.02', '$1.50', '$1.5200', '$1,498.4800', '$0.3120'],
+  ['Polygon', '$1,500', '$0.02', '$1.50', '$1.5200', '$1,498.4800', '$0.3120'],
+  ['Arbitrum', '$1,500', '$0.02', '$1.50', '$1.5200', '$1,498.4800', '$0.3120'],
+  ['Base', '$1,500', '$0.02', '$1.50', '$1.5200', '$1,498.4800', '$0.3120'],
+];
+
+const { USDC } = externalConfigContext;
+const { make, add } = AmountMath;
+
+const $ = (amt: Dollars) =>
+  multiplyBy(make(USDC, 1_000_000n), parseRatio(numeral(amt), USDC));
+
+const destinationOverrides = fromExternalConfig(
+  evmConfig.MAINNET.legibleDestinationOverrides,
+  externalConfigContext,
+  DestinationOverridesShape,
+);
+
+/** production feeConfig from vstorage */
+const mainFeeConfig: FeeConfig = {
+  contractRate: makeRatio(2n, USDC, 10n),
+  flat: make(USDC, 10000n),
+  variableRate: makeRatio(1n, USDC, 1000n),
+};
+harden(mainFeeConfig);
+const feeConfig = harden({ ...mainFeeConfig, destinationOverrides });
+const { calculateSplit, calculateBaseFee } = makeFeeTools(feeConfig);
+
+test('EVM fee config matches product spreadsheet', t => {
+  for (const row of examples) {
+    t.log(row.map(x => `${x}`).join(' '));
+    const [name, amt, flatPlusRelay, vbl, totFee, netAdvance, contractPart] =
+      row;
+
+    const { namespace, reference } = cctpChainInfo[name.toLowerCase()] || {};
+    const dest: AccountId = namespace
+      ? `${namespace}:${reference}:0x1234567890123456789012345678901234567890`
+      : 'cosmos:osmosis-1:osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men';
+    const base = calculateBaseFee($(amt), dest);
+    const { ContractFee, RelayFee, Principal } = calculateSplit($(amt), dest);
+
+    t.deepEqual(add(feeConfig.flat, RelayFee), $(flatPlusRelay), name);
+    t.deepEqual(base, add(feeConfig.flat, $(vbl)), name);
+    t.deepEqual(add(base, RelayFee), $(totFee), name);
+    t.deepEqual(Principal, $(netAdvance), name);
+    t.deepEqual(add(ContractFee, RelayFee), $(contractPart), name);
+  }
+});
diff --git a/packages/fast-usdc/src/type-guards.js b/packages/fast-usdc/src/type-guards.js
index 9975696a31d..9d7198f8e5f 100644
--- a/packages/fast-usdc/src/type-guards.js
+++ b/packages/fast-usdc/src/type-guards.js
@@ -110,6 +110,7 @@ harden(AddressHookShape);
 
 const NatAmountShape = { brand: BrandShape, value: M.nat() };
 
+/** @type {TypedPattern<FeeConfig['destinationOverrides']>} */
 export const DestinationOverridesShape = M.recordOf(
   M.string(),
   M.splitRecord(


Reviews:
user: login: LuqiPan
body: LGTM
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T22:51:11Z
user: login: dckc
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T22:53:17Z
user: login: dckc
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T22:59:33Z
user: login: 0xpatrickdev
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T00:06:16Z

Review comments:
diff_hunk: @@ -57,28 +57,17 @@ const config = /** @type {const} */ ({
         version: 'ics20-1',
       },
     },
-    /** FIXME #11149 use real values */
     legibleDestinationOverrides: toExternalConfig(
-      harden({
-        'eip155:1': {
-          relay: USDC(1_000_000n),
-        },
-        'eip155:43114': {
-          relay: USDC(500_000n),
-        },
-        'eip155:10': {
-          relay: USDC(500_000n),
-        },
-        'eip155:42161': {
-          relay: USDC(500_000n),
-        },
-        'eip155:8453': {
-          relay: USDC(500_000n),
-        },
-        'eip155:137': {
-          relay: USDC(500_000n),
-        },
-      }),
+      harden(
+        /** @type {FeeConfig['destinationOverrides']} */ ({
+          'eip155:1': { relay: USDC(500_000n) },
path: packages/fast-usdc-deploy/src/upgrade-evm-dests.core.js
user: login: LuqiPan
body: _low priority_

For readability, would it make sense to add comment next to each key what chain it corresponds to?
created_at: 2025-04-16T22:50:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,88 @@
+/** @file test EVM dest fees vs. examples from product spreadsheet */
+import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js';
+
+import { AmountMath } from '@agoric/ertp';
+import { makeRatio, multiplyBy, parseRatio } from '@agoric/ertp/src/ratio.js';
+import type { FeeConfig } from '@agoric/fast-usdc';
+import { DestinationOverridesShape } from '@agoric/fast-usdc/src/type-guards.js';
+import { makeFeeTools } from '@agoric/fast-usdc/src/utils/fees.js';
+import type { AccountId } from '@agoric/orchestration';
+import cctpChainInfo from '@agoric/orchestration/src/cctp-chain-info.js';
+import {
+  config as evmConfig,
+  externalConfigContext,
+} from '../../src/upgrade-evm-dests.core.js';
+import { fromExternalConfig } from '../../src/utils/config-marshal.js';
+
+type Dollars = `$${string}`;
+const numeral = (amt: Dollars) => amt.replace(/[$,]/g, '');
+
+/** Spreadsheet header becomes a tuple type with column names used as tags */
+type ExampleRow = [
+  destination: string,
+  amount: Dollars,
+  flatPlusRelay: Dollars,
+  variableFee: Dollars,
+  totalFee: Dollars,
+  netAdvance: Dollars,
+  contractCommission: Dollars,
+];
+
+const examples: ExampleRow[] = [
+  ['Default', '$1,500', '$0.01', '$1.50', '$1.5100', '$1,498.4900', '$0.3020'],
+  ['Ethereum', '$1,500', '$0.51', '$1.50', '$2.0100', '$1,497.9900', '$0.8020'],
+  ['Optimism', '$1,500', '$0.02', '$1.50', '$1.5200', '$1,498.4800', '$0.3120'],
+  ['Polygon', '$1,500', '$0.02', '$1.50', '$1.5200', '$1,498.4800', '$0.3120'],
+  ['Arbitrum', '$1,500', '$0.02', '$1.50', '$1.5200', '$1,498.4800', '$0.3120'],
+  ['Base', '$1,500', '$0.02', '$1.50', '$1.5200', '$1,498.4800', '$0.3120'],
+];
path: packages/fast-usdc-deploy/test/utils/fee-dest.test.ts
user: login: LuqiPan
body: Thank you, this table is super helpful in understanding what the fees are!
created_at: 2025-04-16T22:51:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -57,28 +57,17 @@ const config = /** @type {const} */ ({
         version: 'ics20-1',
       },
     },
-    /** FIXME #11149 use real values */
     legibleDestinationOverrides: toExternalConfig(
-      harden({
-        'eip155:1': {
-          relay: USDC(1_000_000n),
-        },
-        'eip155:43114': {
-          relay: USDC(500_000n),
-        },
-        'eip155:10': {
-          relay: USDC(500_000n),
-        },
-        'eip155:42161': {
-          relay: USDC(500_000n),
-        },
-        'eip155:8453': {
-          relay: USDC(500_000n),
-        },
-        'eip155:137': {
-          relay: USDC(500_000n),
-        },
-      }),
+      harden(
+        /** @type {FeeConfig['destinationOverrides']} */ ({
+          'eip155:1': { relay: USDC(500_000n) },
path: packages/fast-usdc-deploy/src/upgrade-evm-dests.core.js
user: login: dckc
body: I asked the same thing; @0xpatrickdev noted the names are in [cctp-chain-info.js](https://github.com/Agoric/agoric-sdk/blob/master/packages/orchestration/src/cctp-chain-info.js) ; I'll add a cross-reference.
created_at: 2025-04-16T22:53:17Z
author_association: MEMBER
diff_hunk: @@ -57,28 +57,17 @@ const config = /** @type {const} */ ({
         version: 'ics20-1',
       },
     },
-    /** FIXME #11149 use real values */
     legibleDestinationOverrides: toExternalConfig(
-      harden({
-        'eip155:1': {
-          relay: USDC(1_000_000n),
-        },
-        'eip155:43114': {
-          relay: USDC(500_000n),
-        },
-        'eip155:10': {
-          relay: USDC(500_000n),
-        },
-        'eip155:42161': {
-          relay: USDC(500_000n),
-        },
-        'eip155:8453': {
-          relay: USDC(500_000n),
-        },
-        'eip155:137': {
-          relay: USDC(500_000n),
-        },
-      }),
+      harden(
+        /** @type {FeeConfig['destinationOverrides']} */ ({
+          'eip155:1': { relay: USDC(500_000n) },
path: packages/fast-usdc-deploy/src/upgrade-evm-dests.core.js
user: login: dckc
body: on second thought, the indirection is inconvenient.
created_at: 2025-04-16T22:59:33Z
author_association: MEMBER

PR comments:
user: login: cloudflare-workers-and-pages[bot]
created_at: 2025-04-16T22:27:15Z
body: ## Deploying agoric-sdk with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>dd0c7ed</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://09de348f.agoric-sdk.pages.dev'>https://09de348f.agoric-sdk.pages.dev</a>
</td></tr>
<tr><td><strong>Branch Preview URL:</strong></td><td>
<a href='https://dc-dest-fees.agoric-sdk.pages.dev'>https://dc-dest-fees.agoric-sdk.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/0c4635effffcd7f36d1b9f0425a4367a/pages/view/agoric-sdk/09de348f-47a4-463c-af4c-34a3705a2b84)

author_association: NONE
user: login: LuqiPan
created_at: 2025-04-17T03:41:42Z
body: I tried to re-run the failing test but it didn't help, not sure if it's a flake
author_association: CONTRIBUTOR
user: login: dckc
created_at: 2025-04-17T12:00:19Z
body: Oops. Not a flake. A snapshot needs updating.
author_association: MEMBER