Skip to content

Commit

Permalink
feat(fast-usdc): withdraw and distribute contract fees (#10815)
Browse files Browse the repository at this point in the history
closes: #10700

## Description

 - creatorFacet method to withdraw fees
   - unit test
 - core eval script to distribute fees
   - bootstrap test
   - refactor: publishDisplayInfo is not specific to fast-usdc

### Security / Scaling Considerations / Upgrade Considerations

straightforward; not yet deployed

### Documentation Considerations

 - [x] builder script has usage docs: `Use: [--fixedFees <number> | --feePortion <percent>] --destinationAddress <address>`
 - ~~document how to use it with cosgov~~

### Testing Considerations

 - [x] contract test: 1 positive, 1 negative
 - [x] bootstrap test for core eval
    - [x] fixed amount
    - [x] portion
 - [x] multichain test
  • Loading branch information
mergify[bot] authored Jan 15, 2025
2 parents 9f46b37 + cfa9f78 commit 44f449b
Show file tree
Hide file tree
Showing 18 changed files with 508 additions and 129 deletions.
60 changes: 45 additions & 15 deletions multichain-testing/test/fast-usdc/fast-usdc.test.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import anyTest from '@endo/ses-ava/prepare-endo.js';

import type { TestFn } from 'ava';
import { encodeAddressHook } from '@agoric/cosmic-proto/address-hooks.js';
import type { QueryBalanceResponseSDKType } from '@agoric/cosmic-proto/cosmos/bank/v1beta1/query.js';
import { AmountMath } from '@agoric/ertp';
import type { Denom } from '@agoric/orchestration';
import { divideBy, multiplyBy } from '@agoric/zoe/src/contractSupport/ratio.js';
import type { IBCChannelID } from '@agoric/vats';
import { makeDoOffer, type WalletDriver } from '../../tools/e2e-tools.js';
import { makeDenomTools } from '../../tools/asset-info.js';
import { createWallet } from '../../tools/wallet.js';
import { makeQueryClient } from '../../tools/query.js';
import { commonSetup, type SetupContextWithWallets } from '../support.js';
import { makeFeedPolicyPartial, oracleMnemonics } from './config.js';
import { makeRandomDigits } from '../../tools/random.js';
import { makeTracer } from '@agoric/internal';
import type { USDCProposalShapes } from '@agoric/fast-usdc/src/pool-share-math.js';
import type {
CctpTxEvidence,
EvmAddress,
PoolMetrics,
} from '@agoric/fast-usdc/src/types.js';
import { makeTracer } from '@agoric/internal';
import type { Denom } from '@agoric/orchestration';
import type { CurrentWalletRecord } from '@agoric/smart-wallet/src/smartWallet.js';
import type { QueryBalanceResponseSDKType } from '@agoric/cosmic-proto/cosmos/bank/v1beta1/query.js';
import type { USDCProposalShapes } from '@agoric/fast-usdc/src/pool-share-math.js';
import type { IBCChannelID } from '@agoric/vats';
import { divideBy, multiplyBy } from '@agoric/zoe/src/contractSupport/ratio.js';
import type { TestFn } from 'ava';
import { makeDenomTools } from '../../tools/asset-info.js';
import { makeDoOffer, type WalletDriver } from '../../tools/e2e-tools.js';
import { makeQueryClient } from '../../tools/query.js';
import { makeRandomDigits } from '../../tools/random.js';
import { createWallet } from '../../tools/wallet.js';
import { commonSetup, type SetupContextWithWallets } from '../support.js';
import { makeFeedPolicyPartial, oracleMnemonics } from './config.js';

const log = makeTracer('MCFU');

Expand All @@ -33,6 +33,7 @@ const makeRandomNumber = () => Math.random();
const test = anyTest as TestFn<
SetupContextWithWallets & {
lpUser: WalletDriver;
feeUser: WalletDriver;
oracleWds: WalletDriver[];
nobleAgoricChannelId: IBCChannelID;
usdcOnOsmosis: Denom;
Expand All @@ -41,7 +42,7 @@ const test = anyTest as TestFn<
}
>;

const accounts = [...keys(oracleMnemonics), 'lp'];
const accounts = [...keys(oracleMnemonics), 'lp', 'feeDest'];
const contractName = 'fastUsdc';
const contractBuilder =
'../packages/builders/scripts/fast-usdc/start-fast-usdc.build.js';
Expand Down Expand Up @@ -95,10 +96,12 @@ test.before(async t => {
USDC: 8_000n,
BLD: 100n,
});
const feeUser = await provisionSmartWallet(wallets['feeDest'], { BLD: 100n });

t.context = {
...common,
lpUser,
feeUser,
oracleWds,
nobleAgoricChannelId,
usdcOnOsmosis,
Expand Down Expand Up @@ -401,6 +404,33 @@ const advanceAndSettleScenario = test.macro({
},
});

test('distribute FastUSDC contract fees', async t => {
const io = t.context;
const queryClient = makeQueryClient(
await io.useChain('agoric').getRestEndpoint(),
);
const builder =
'../packages/builders/scripts/fast-usdc/fast-usdc-fees.build.js';

const opts = {
destinationAddress: io.wallets['feeDest'],
feePortion: 0.25,
};
t.log('build, run proposal to distribute fees', opts);
await io.deployBuilder(builder, {
...opts,
feePortion: `${opts.feePortion}`,
});

const { balance } = await io.retryUntilCondition(
() => queryClient.queryBalance(opts.destinationAddress, io.usdcDenom),
({ balance }) => !!balance && BigInt(balance.amount) > 0n,
`fees received at ${opts.destinationAddress}`,
);
t.log('fees received', balance);
t.truthy(balance?.amount);
});

test.serial(advanceAndSettleScenario, LP_DEPOSIT_AMOUNT / 4n, 'osmosis');
test.serial(advanceAndSettleScenario, LP_DEPOSIT_AMOUNT / 8n, 'noble');
test.serial(advanceAndSettleScenario, LP_DEPOSIT_AMOUNT / 5n, 'agoric');
Expand Down
2 changes: 1 addition & 1 deletion multichain-testing/tools/e2e-tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@ export const makeE2ETools = async (
runCoreEval: buildAndRunCoreEval,
/**
* @param {string} address
* @param {Record<string, bigint>} amount
* @param {Record<string, bigint>} amount - should include BLD to pay for provisioning
*/
provisionSmartWallet: (address, amount) =>
provisionSmartWallet(address, amount, {
Expand Down
4 changes: 2 additions & 2 deletions multichain-testing/tools/wallet.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Bip39, Random } from '@cosmjs/crypto';
import { DirectSecp256k1HdWallet } from '@cosmjs/proto-signing';

export function generateMnemonic(): string {
return Bip39.encode(Random.getBytes(16)).toString();
export function generateMnemonic(getBytes = Random.getBytes): string {
return Bip39.encode(getBytes(16)).toString();
}

export const createWallet = async (
Expand Down
3 changes: 2 additions & 1 deletion packages/boot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
"build": "exit 0",
"clean": "rm -rf bundles/config.*",
"test": "ava",
"test:xs": "SWINGSET_WORKER_TYPE=xs-worker ava test/bootstrapTests test/upgrading test/fast-usdc",
"test:xs": "SWINGSET_WORKER_TYPE=xs-worker ava test/bootstrapTests test/upgrading",
"lint-fix": "yarn lint:eslint --fix",
"lint": "run-s --continue-on-error lint:*",
"lint:types": "tsc",
"lint:eslint": "eslint ."
},
"$scripts-note": "fast-usdc skipped in test:xs pending #10847",
"keywords": [],
"author": "Agoric",
"license": "Apache-2.0",
Expand Down
109 changes: 85 additions & 24 deletions packages/boot/test/fast-usdc/fast-usdc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,11 @@ import { encodeAddressHook } from '@agoric/cosmic-proto/address-hooks.js';
import { configurations } from '@agoric/fast-usdc/src/utils/deploy-config.js';
import { MockCctpTxEvidences } from '@agoric/fast-usdc/test/fixtures.js';
import { documentStorageSchema } from '@agoric/governance/tools/storageDoc.js';
import {
BridgeId,
deeplyFulfilledObject,
NonNullish,
objectMap,
} from '@agoric/internal';
import { BridgeId, NonNullish } from '@agoric/internal';
import { unmarshalFromVstorage } from '@agoric/internal/src/marshal.js';
import { defaultSerializer } from '@agoric/internal/src/storage-test-utils.js';
import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js';
import { buildVTransferEvent } from '@agoric/orchestration/tools/ibc-mocks.js';
import { Fail } from '@endo/errors';
import { makeMarshal } from '@endo/marshal';
import {
Expand Down Expand Up @@ -175,16 +171,6 @@ test.serial('writes fee config to vstorage', async t => {
await documentStorageSchema(t, storage, doc);
});

test.serial('writes pool metrics to vstorage', async t => {
const { storage } = t.context;
const doc = {
node: 'fastUsdc.poolMetrics',
owner: 'FastUSC LiquidityPool exo',
showValue: defaultSerializer.parse,
};
await documentStorageSchema(t, storage, doc);
});

test.serial('writes account addresses to vstorage', async t => {
const { storage } = t.context;
const doc = {
Expand Down Expand Up @@ -298,7 +284,9 @@ test.serial('makes usdc advance', async t => {

const EUD = 'dydx1anything';
const lastNodeValue = storage.getValues('published.fastUsdc').at(-1);
const { settlementAccount } = JSON.parse(NonNullish(lastNodeValue));
const { settlementAccount, poolAccount } = JSON.parse(
NonNullish(lastNodeValue),
);
const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(
// mock with the real settlementAccount address
encodeAddressHook(settlementAccount, { EUD }),
Expand Down Expand Up @@ -327,16 +315,53 @@ test.serial('makes usdc advance', async t => {
);
harness?.resetRunPolicy();

t.deepEqual(
const getTxStatus = txHash =>
storage
.getValues(`published.fastUsdc.txns.${evidence.txHash}`)
.map(defaultSerializer.parse),
[
{ evidence, status: 'OBSERVED' }, // observation includes evidence observed
{ status: 'ADVANCING' },
],
.getValues(`published.fastUsdc.txns.${txHash}`)
.map(defaultSerializer.parse);

t.deepEqual(getTxStatus(evidence.txHash), [
{ evidence, status: 'OBSERVED' }, // observation includes evidence observed
{ status: 'ADVANCING' },
]);

const { runInbound } = t.context.bridgeUtils;
await runInbound(
BridgeId.VTRANSFER,
buildVTransferEvent({
sender: poolAccount,
target: poolAccount,
sourceChannel: 'channel-62',
sequence: '1',
}),
);

// in due course, minted USDC arrives
await runInbound(
BridgeId.VTRANSFER,

buildVTransferEvent({
sequence: '1', // arbitrary; not used
amount: evidence.tx.amount,
denom: 'uusdc',
sender: evidence.tx.forwardingAddress,
target: settlementAccount,
receiver: encodeAddressHook(settlementAccount, { EUD }),
sourceChannel: evidence.aux.forwardingChannel,
destinationChannel: 'channel-62', // fetchedChainInfo
// destinationChannel: evidence.aux.forwardingChannel,
// sourceChannel: 'channel-62', // fetchedChainInfo
}),
);

await eventLoopIteration();
t.like(getTxStatus(evidence.txHash), [
{ status: 'OBSERVED' },
{ status: 'ADVANCING' },
{ status: 'ADVANCED' },
{ status: 'DISBURSED', split: { ContractFee: { value: 302000n } } },
]);

const doc = {
node: `fastUsdc.txns`,
owner: `the Ethereum transactions upon which Fast USDC is acting`,
Expand All @@ -345,6 +370,42 @@ test.serial('makes usdc advance', async t => {
await documentStorageSchema(t, storage, doc);
});

test.serial('writes pool metrics to vstorage', async t => {
const { storage } = t.context;
const doc = {
node: 'fastUsdc.poolMetrics',
owner: 'FastUSC LiquidityPool exo',
showValue: defaultSerializer.parse,
};
await documentStorageSchema(t, storage, doc);
});

test.serial('distributes fees per BLD staker decision', async t => {
const { walletFactoryDriver: wd, buildProposal, evalProposal } = t.context;

const ContractFee = 302000n; // see split above
t.is(((ContractFee - 250000n) * 5n) / 10n, 26000n);
const cases = [
{ dest: 'agoric1a', args: ['--fixedFees', '0.25'], rxd: '250000' },
{ dest: 'agoric1b', args: ['--feePortion', '0.5'], rxd: '26000' },
];
for (const { dest, args, rxd } of cases) {
await wd.provideSmartWallet(dest);
const materials = buildProposal(
'@agoric/builders/scripts/fast-usdc/fast-usdc-fees.build.js',
['--destinationAddress', dest, ...args],
);
await evalProposal(materials);

const { getOutboundMessages } = t.context.bridgeUtils;
const found = getOutboundMessages(BridgeId.BANK).find(
msg => msg.recipient === dest && msg.type === 'VBANK_GIVE',
);
t.log('dest vbank msg', found);
t.like(found, { amount: rxd });
}
});

test.serial('skips usdc advance when risks identified', async t => {
const { walletFactoryDriver: wfd, storage } = t.context;
const oracles = await Promise.all([
Expand Down
Loading

0 comments on commit 44f449b

Please sign in to comment.