@agoric/fast-usdc 0.1.1-dev-02967de.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +58 -0
- package/package.json +81 -0
- package/src/cli/bin.js +6 -0
- package/src/cli/cli.js +211 -0
- package/src/cli/config.js +101 -0
- package/src/cli/transfer.js +91 -0
- package/src/constants.js +27 -0
- package/src/exos/README.md +26 -0
- package/src/exos/advancer.js +240 -0
- package/src/exos/liquidity-pool.js +365 -0
- package/src/exos/operator-kit.js +120 -0
- package/src/exos/settler.js +97 -0
- package/src/exos/status-manager.js +176 -0
- package/src/exos/transaction-feed.js +180 -0
- package/src/fast-usdc.contract.js +216 -0
- package/src/fast-usdc.start.js +264 -0
- package/src/pool-share-math.js +189 -0
- package/src/type-guards.js +95 -0
- package/src/types-index.d.ts +1 -0
- package/src/types-index.js +1 -0
- package/src/types.ts +58 -0
- package/src/util/agoric.js +12 -0
- package/src/util/cctp.js +71 -0
- package/src/util/file.js +30 -0
- package/src/util/noble.js +110 -0
- package/src/utils/address.js +71 -0
- package/src/utils/config-marshal.js +130 -0
- package/src/utils/fees.js +58 -0
- package/src/utils/zoe.js +28 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { deeplyFulfilledObject, makeTracer, objectMap } from '@agoric/internal';
|
|
2
|
+
import { Fail } from '@endo/errors';
|
|
3
|
+
import { E } from '@endo/far';
|
|
4
|
+
import { makeMarshal } from '@endo/marshal';
|
|
5
|
+
import { M } from '@endo/patterns';
|
|
6
|
+
import { FastUSDCTermsShape, FeeConfigShape } from './type-guards.js';
|
|
7
|
+
import { fromExternalConfig } from './utils/config-marshal.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @import {DepositFacet} from '@agoric/ertp/src/types.js'
|
|
11
|
+
* @import {TypedPattern} from '@agoric/internal'
|
|
12
|
+
* @import {Instance, StartParams} from '@agoric/zoe/src/zoeService/utils'
|
|
13
|
+
* @import {Board} from '@agoric/vats'
|
|
14
|
+
* @import {ManifestBundleRef} from '@agoric/deploy-script-support/src/externalTypes.js'
|
|
15
|
+
* @import {BootstrapManifest} from '@agoric/vats/src/core/lib-boot.js'
|
|
16
|
+
* @import {LegibleCapData} from './utils/config-marshal.js'
|
|
17
|
+
* @import {FastUsdcSF, FastUsdcTerms} from './fast-usdc.contract.js'
|
|
18
|
+
* @import {FeeConfig} from './types.js'
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const trace = makeTracer('FUSD-Start', true);
|
|
22
|
+
|
|
23
|
+
const contractName = 'fastUsdc';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {{
|
|
27
|
+
* terms: FastUsdcTerms;
|
|
28
|
+
* oracles: Record<string, string>;
|
|
29
|
+
* feeConfig: FeeConfig;
|
|
30
|
+
* }} FastUSDCConfig
|
|
31
|
+
*/
|
|
32
|
+
/** @type {TypedPattern<FastUSDCConfig>} */
|
|
33
|
+
export const FastUSDCConfigShape = M.splitRecord({
|
|
34
|
+
terms: FastUSDCTermsShape,
|
|
35
|
+
oracles: M.recordOf(M.string(), M.string()),
|
|
36
|
+
feeConfig: FeeConfigShape,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* XXX Shouldn't the bridge or board vat handle this?
|
|
41
|
+
*
|
|
42
|
+
* @param {string} path
|
|
43
|
+
* @param {{
|
|
44
|
+
* chainStorage: ERef<StorageNode>;
|
|
45
|
+
* board: ERef<Board>;
|
|
46
|
+
* }} io
|
|
47
|
+
*/
|
|
48
|
+
const makePublishingStorageKit = async (path, { chainStorage, board }) => {
|
|
49
|
+
const storageNode = await E(chainStorage).makeChildNode(path);
|
|
50
|
+
|
|
51
|
+
const marshaller = await E(board).getPublishingMarshaller();
|
|
52
|
+
return { storageNode, marshaller };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const BOARD_AUX = 'boardAux';
|
|
56
|
+
const marshalData = makeMarshal(_val => Fail`data only`);
|
|
57
|
+
/**
|
|
58
|
+
* @param {Brand} brand
|
|
59
|
+
* @param {Pick<BootstrapPowers['consume'], 'board' | 'chainStorage'>} powers
|
|
60
|
+
*/
|
|
61
|
+
const publishDisplayInfo = async (brand, { board, chainStorage }) => {
|
|
62
|
+
// chainStorage type includes undefined, which doesn't apply here.
|
|
63
|
+
// @ts-expect-error UNTIL https://github.com/Agoric/agoric-sdk/issues/8247
|
|
64
|
+
const boardAux = E(chainStorage).makeChildNode(BOARD_AUX);
|
|
65
|
+
const [id, displayInfo, allegedName] = await Promise.all([
|
|
66
|
+
E(board).getId(brand),
|
|
67
|
+
E(brand).getDisplayInfo(),
|
|
68
|
+
E(brand).getAllegedName(),
|
|
69
|
+
]);
|
|
70
|
+
const node = E(boardAux).makeChildNode(id);
|
|
71
|
+
const aux = marshalData.toCapData(harden({ allegedName, displayInfo }));
|
|
72
|
+
await E(node).setValue(JSON.stringify(aux));
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @typedef { PromiseSpaceOf<{
|
|
77
|
+
* fastUsdcKit: FastUSDCKit
|
|
78
|
+
* }> & {
|
|
79
|
+
* installation: PromiseSpaceOf<{ fastUsdc: Installation<FastUsdcSF> }>;
|
|
80
|
+
* instance: PromiseSpaceOf<{ fastUsdc: Instance<FastUsdcSF> }>;
|
|
81
|
+
* issuer: PromiseSpaceOf<{ FastLP: Issuer }>;
|
|
82
|
+
* brand: PromiseSpaceOf<{ FastLP: Brand }>;
|
|
83
|
+
* }} FastUSDCCorePowers
|
|
84
|
+
*
|
|
85
|
+
* @typedef {StartedInstanceKitWithLabel & {
|
|
86
|
+
* privateArgs: StartParams<FastUsdcSF>['privateArgs'];
|
|
87
|
+
* }} FastUSDCKit
|
|
88
|
+
*/
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @throws if oracle smart wallets are not yet provisioned
|
|
92
|
+
*
|
|
93
|
+
* @param {BootstrapPowers & FastUSDCCorePowers } powers
|
|
94
|
+
* @param {{ options: LegibleCapData<FastUSDCConfig> }} config
|
|
95
|
+
*/
|
|
96
|
+
export const startFastUSDC = async (
|
|
97
|
+
{
|
|
98
|
+
produce: { fastUsdcKit },
|
|
99
|
+
consume: {
|
|
100
|
+
agoricNames,
|
|
101
|
+
namesByAddress,
|
|
102
|
+
board,
|
|
103
|
+
chainStorage,
|
|
104
|
+
chainTimerService: timerService,
|
|
105
|
+
localchain,
|
|
106
|
+
cosmosInterchainService,
|
|
107
|
+
startUpgradable,
|
|
108
|
+
zoe,
|
|
109
|
+
},
|
|
110
|
+
issuer: {
|
|
111
|
+
produce: { FastLP: produceShareIssuer },
|
|
112
|
+
},
|
|
113
|
+
brand: {
|
|
114
|
+
produce: { FastLP: produceShareBrand },
|
|
115
|
+
},
|
|
116
|
+
installation: {
|
|
117
|
+
consume: { fastUsdc },
|
|
118
|
+
},
|
|
119
|
+
instance: {
|
|
120
|
+
produce: { fastUsdc: produceInstance },
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
config,
|
|
124
|
+
) => {
|
|
125
|
+
trace('startFastUSDC');
|
|
126
|
+
|
|
127
|
+
await null;
|
|
128
|
+
/** @type {Issuer<'nat'>} */
|
|
129
|
+
const USDCissuer = await E(agoricNames).lookup('issuer', 'USDC');
|
|
130
|
+
const brands = harden({
|
|
131
|
+
USDC: await E(USDCissuer).getBrand(),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const { terms, oracles, feeConfig } = fromExternalConfig(
|
|
135
|
+
config?.options, // just in case config is missing somehow
|
|
136
|
+
brands,
|
|
137
|
+
FastUSDCConfigShape,
|
|
138
|
+
);
|
|
139
|
+
trace('using terms', terms);
|
|
140
|
+
trace('using fee config', feeConfig);
|
|
141
|
+
|
|
142
|
+
trace('look up oracle deposit facets');
|
|
143
|
+
const oracleDepositFacets = await deeplyFulfilledObject(
|
|
144
|
+
objectMap(oracles, async address => {
|
|
145
|
+
/** @type {DepositFacet} */
|
|
146
|
+
const depositFacet = await E(namesByAddress).lookup(
|
|
147
|
+
address,
|
|
148
|
+
'depositFacet',
|
|
149
|
+
);
|
|
150
|
+
return depositFacet;
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const { storageNode, marshaller } = await makePublishingStorageKit(
|
|
155
|
+
contractName,
|
|
156
|
+
{
|
|
157
|
+
board,
|
|
158
|
+
// @ts-expect-error Promise<null> case is vestigial
|
|
159
|
+
chainStorage,
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const privateArgs = await deeplyFulfilledObject(
|
|
164
|
+
harden({
|
|
165
|
+
agoricNames,
|
|
166
|
+
feeConfig,
|
|
167
|
+
localchain,
|
|
168
|
+
orchestrationService: cosmosInterchainService,
|
|
169
|
+
storageNode,
|
|
170
|
+
timerService,
|
|
171
|
+
marshaller,
|
|
172
|
+
}),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const kit = await E(startUpgradable)({
|
|
176
|
+
label: contractName,
|
|
177
|
+
installation: fastUsdc,
|
|
178
|
+
issuerKeywordRecord: harden({ USDC: USDCissuer }),
|
|
179
|
+
terms,
|
|
180
|
+
privateArgs,
|
|
181
|
+
});
|
|
182
|
+
fastUsdcKit.resolve(harden({ ...kit, privateArgs }));
|
|
183
|
+
const { instance, creatorFacet } = kit;
|
|
184
|
+
|
|
185
|
+
const {
|
|
186
|
+
issuers: { PoolShares: shareIssuer },
|
|
187
|
+
brands: { PoolShares: shareBrand },
|
|
188
|
+
} = await E(zoe).getTerms(instance);
|
|
189
|
+
produceShareIssuer.resolve(shareIssuer);
|
|
190
|
+
produceShareBrand.resolve(shareBrand);
|
|
191
|
+
await publishDisplayInfo(shareBrand, { board, chainStorage });
|
|
192
|
+
|
|
193
|
+
await Promise.all(
|
|
194
|
+
Object.entries(oracleDepositFacets).map(async ([name, depositFacet]) => {
|
|
195
|
+
const address = oracles[name];
|
|
196
|
+
trace('making invitation for', name, address);
|
|
197
|
+
const toWatch = await E(creatorFacet).makeOperatorInvitation(address);
|
|
198
|
+
|
|
199
|
+
const amt = await E(depositFacet).receive(toWatch);
|
|
200
|
+
trace('sent', amt, 'to', name);
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
produceInstance.reset();
|
|
205
|
+
produceInstance.resolve(instance);
|
|
206
|
+
trace('startFastUSDC done', instance);
|
|
207
|
+
};
|
|
208
|
+
harden(startFastUSDC);
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* @param {{
|
|
212
|
+
* restoreRef: (b: ERef<ManifestBundleRef>) => Promise<Installation>;
|
|
213
|
+
* }} utils
|
|
214
|
+
* @param {{
|
|
215
|
+
* installKeys: { fastUsdc: ERef<ManifestBundleRef> };
|
|
216
|
+
* options: LegibleCapData<FastUSDCConfig>;
|
|
217
|
+
* }} param1
|
|
218
|
+
*/
|
|
219
|
+
export const getManifestForFastUSDC = (
|
|
220
|
+
{ restoreRef },
|
|
221
|
+
{ installKeys, options },
|
|
222
|
+
) => {
|
|
223
|
+
return {
|
|
224
|
+
/** @type {BootstrapManifest} */
|
|
225
|
+
manifest: {
|
|
226
|
+
[startFastUSDC.name]: {
|
|
227
|
+
produce: {
|
|
228
|
+
fastUsdcKit: true,
|
|
229
|
+
},
|
|
230
|
+
consume: {
|
|
231
|
+
chainStorage: true,
|
|
232
|
+
chainTimerService: true,
|
|
233
|
+
localchain: true,
|
|
234
|
+
cosmosInterchainService: true,
|
|
235
|
+
|
|
236
|
+
// limited distribution durin MN2: contract installation
|
|
237
|
+
startUpgradable: true,
|
|
238
|
+
zoe: true, // only getTerms() is needed. XXX should be split?
|
|
239
|
+
|
|
240
|
+
// widely shared: name services
|
|
241
|
+
agoricNames: true,
|
|
242
|
+
namesByAddress: true,
|
|
243
|
+
board: true,
|
|
244
|
+
},
|
|
245
|
+
issuer: {
|
|
246
|
+
produce: { FastLP: true }, // UNTIL #10432
|
|
247
|
+
},
|
|
248
|
+
brand: {
|
|
249
|
+
produce: { FastLP: true }, // UNTIL #10432
|
|
250
|
+
},
|
|
251
|
+
instance: {
|
|
252
|
+
produce: { fastUsdc: true },
|
|
253
|
+
},
|
|
254
|
+
installation: {
|
|
255
|
+
consume: { fastUsdc: true },
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
installations: {
|
|
260
|
+
fastUsdc: restoreRef(installKeys.fastUsdc),
|
|
261
|
+
},
|
|
262
|
+
options,
|
|
263
|
+
};
|
|
264
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { AmountMath } from '@agoric/ertp/src/amountMath.js';
|
|
2
|
+
import {
|
|
3
|
+
divideBy,
|
|
4
|
+
makeRatio,
|
|
5
|
+
makeRatioFromAmounts,
|
|
6
|
+
multiplyBy,
|
|
7
|
+
} from '@agoric/zoe/src/contractSupport/ratio.js';
|
|
8
|
+
import { Fail, q } from '@endo/errors';
|
|
9
|
+
|
|
10
|
+
const { getValue, add, isEmpty, isEqual, isGTE, subtract } = AmountMath;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @import {PoolStats} from './types';
|
|
14
|
+
* @import {RepayAmountKWR} from './exos/liquidity-pool';
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Invariant: shareWorth is the pool balance divided by shares outstanding.
|
|
19
|
+
*
|
|
20
|
+
* Use `makeParity(make(USDC, epsilon), PoolShares)` for an initial
|
|
21
|
+
* value, for some negligible `epsilon` such as 1n.
|
|
22
|
+
*
|
|
23
|
+
* @typedef {Ratio} ShareWorth
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Make a 1-to-1 ratio between amounts of 2 brands.
|
|
28
|
+
*
|
|
29
|
+
* @param {Amount<'nat'>} numerator
|
|
30
|
+
* @param {Brand<'nat'>} denominatorBrand
|
|
31
|
+
*/
|
|
32
|
+
export const makeParity = (numerator, denominatorBrand) => {
|
|
33
|
+
const value = getValue(numerator.brand, numerator);
|
|
34
|
+
return makeRatio(value, numerator.brand, value, denominatorBrand);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {{
|
|
39
|
+
* deposit: {
|
|
40
|
+
* give: { USDC: Amount<'nat'> },
|
|
41
|
+
* want?: { PoolShare: Amount<'nat'> }
|
|
42
|
+
* },
|
|
43
|
+
* withdraw: {
|
|
44
|
+
* give: { PoolShare: Amount<'nat'> }
|
|
45
|
+
* want: { USDC: Amount<'nat'> },
|
|
46
|
+
* }
|
|
47
|
+
* }} USDCProposalShapes
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Compute Shares payout from a deposit proposal, as well as updated shareWorth.
|
|
52
|
+
*
|
|
53
|
+
* Clearly:
|
|
54
|
+
*
|
|
55
|
+
* sharesOutstanding' = sharesOutstanding + Shares
|
|
56
|
+
* poolBalance' = poolBalance + ToPool
|
|
57
|
+
* shareWorth' = poolBalance' / sharesOutstanding'
|
|
58
|
+
*
|
|
59
|
+
* In order to maintain the ShareWorth invariant, we need:
|
|
60
|
+
*
|
|
61
|
+
* Shares = ToPool / shareWorth'
|
|
62
|
+
*
|
|
63
|
+
* Solving for Shares gives:
|
|
64
|
+
*
|
|
65
|
+
* Shares = ToPool * sharesOutstanding / poolBalance
|
|
66
|
+
*
|
|
67
|
+
* that is:
|
|
68
|
+
*
|
|
69
|
+
* Shares = ToPool / shareWorth
|
|
70
|
+
*
|
|
71
|
+
* @param {ShareWorth} shareWorth previous to the deposit
|
|
72
|
+
* @param {USDCProposalShapes['deposit']} proposal
|
|
73
|
+
* @returns {{ payouts: { PoolShare: Amount<'nat'> }; shareWorth: ShareWorth }}
|
|
74
|
+
*/
|
|
75
|
+
export const depositCalc = (shareWorth, { give, want }) => {
|
|
76
|
+
assert(!isEmpty(give.USDC)); // nice diagnostic provided by proposalShape
|
|
77
|
+
|
|
78
|
+
const { denominator: sharesOutstanding, numerator: poolBalance } = shareWorth;
|
|
79
|
+
|
|
80
|
+
const fairPoolShare = divideBy(give.USDC, shareWorth);
|
|
81
|
+
if (want?.PoolShare) {
|
|
82
|
+
isGTE(fairPoolShare, want.PoolShare) ||
|
|
83
|
+
Fail`deposit cannot pay out ${q(want.PoolShare)}; ${q(give.USDC)} only gets ${q(fairPoolShare)}`;
|
|
84
|
+
}
|
|
85
|
+
const outstandingPost = add(sharesOutstanding, fairPoolShare);
|
|
86
|
+
const balancePost = add(poolBalance, give.USDC);
|
|
87
|
+
const worthPost = makeRatioFromAmounts(balancePost, outstandingPost);
|
|
88
|
+
return harden({
|
|
89
|
+
payouts: { PoolShare: fairPoolShare },
|
|
90
|
+
shareWorth: worthPost,
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Compute payout from a withdraw proposal, along with updated shareWorth
|
|
96
|
+
*
|
|
97
|
+
* @param {ShareWorth} shareWorth
|
|
98
|
+
* @param {USDCProposalShapes['withdraw']} proposal
|
|
99
|
+
* @returns {{ shareWorth: ShareWorth, payouts: { USDC: Amount<'nat'> }}}
|
|
100
|
+
*/
|
|
101
|
+
export const withdrawCalc = (shareWorth, { give, want }) => {
|
|
102
|
+
assert(!isEmpty(give.PoolShare));
|
|
103
|
+
assert(!isEmpty(want.USDC));
|
|
104
|
+
|
|
105
|
+
const payout = multiplyBy(give.PoolShare, shareWorth);
|
|
106
|
+
isGTE(payout, want.USDC) ||
|
|
107
|
+
Fail`cannot withdraw ${q(want.USDC)}; ${q(give.PoolShare)} only worth ${q(payout)}`;
|
|
108
|
+
const { denominator: sharesOutstanding, numerator: poolBalance } = shareWorth;
|
|
109
|
+
!isGTE(want.USDC, poolBalance) ||
|
|
110
|
+
Fail`cannot withdraw ${q(want.USDC)}; only ${q(poolBalance)} in pool`;
|
|
111
|
+
const balancePost = subtract(poolBalance, payout);
|
|
112
|
+
// giving more shares than are outstanding is impossible,
|
|
113
|
+
// so it's not worth a custom diagnostic. subtract will fail
|
|
114
|
+
const outstandingPost = subtract(sharesOutstanding, give.PoolShare);
|
|
115
|
+
|
|
116
|
+
const worthPost = makeRatioFromAmounts(balancePost, outstandingPost);
|
|
117
|
+
return harden({ shareWorth: worthPost, payouts: { USDC: payout } });
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {ShareWorth} shareWorth
|
|
122
|
+
* @param {Amount<'nat'>} fees
|
|
123
|
+
*/
|
|
124
|
+
export const withFees = (shareWorth, fees) => {
|
|
125
|
+
const balancePost = add(shareWorth.numerator, fees);
|
|
126
|
+
return makeRatioFromAmounts(balancePost, shareWorth.denominator);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
*
|
|
131
|
+
* @param {Amount<'nat'>} requested
|
|
132
|
+
* @param {Amount<'nat'>} poolSeatAllocation
|
|
133
|
+
* @param {Amount<'nat'>} encumberedBalance
|
|
134
|
+
* @param {PoolStats} poolStats
|
|
135
|
+
* @throws {Error} if requested is not less than poolSeatAllocation
|
|
136
|
+
*/
|
|
137
|
+
export const borrowCalc = (
|
|
138
|
+
requested,
|
|
139
|
+
poolSeatAllocation,
|
|
140
|
+
encumberedBalance,
|
|
141
|
+
poolStats,
|
|
142
|
+
) => {
|
|
143
|
+
// pool must never go empty
|
|
144
|
+
!isGTE(requested, poolSeatAllocation) ||
|
|
145
|
+
Fail`Cannot borrow. Requested ${q(requested)} must be less than pool balance ${q(poolSeatAllocation)}.`;
|
|
146
|
+
|
|
147
|
+
return harden({
|
|
148
|
+
encumberedBalance: add(encumberedBalance, requested),
|
|
149
|
+
poolStats: {
|
|
150
|
+
...poolStats,
|
|
151
|
+
totalBorrows: add(poolStats.totalBorrows, requested),
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {ShareWorth} shareWorth
|
|
158
|
+
* @param {Allocation} fromSeatAllocation
|
|
159
|
+
* @param {RepayAmountKWR} amounts
|
|
160
|
+
* @param {Amount<'nat'>} encumberedBalance aka 'outstanding borrows'
|
|
161
|
+
* @param {PoolStats} poolStats
|
|
162
|
+
* @throws {Error} if allocations do not match amounts or Principal exceeds encumberedBalance
|
|
163
|
+
*/
|
|
164
|
+
export const repayCalc = (
|
|
165
|
+
shareWorth,
|
|
166
|
+
fromSeatAllocation,
|
|
167
|
+
amounts,
|
|
168
|
+
encumberedBalance,
|
|
169
|
+
poolStats,
|
|
170
|
+
) => {
|
|
171
|
+
(isEqual(fromSeatAllocation.Principal, amounts.Principal) &&
|
|
172
|
+
isEqual(fromSeatAllocation.PoolFee, amounts.PoolFee) &&
|
|
173
|
+
isEqual(fromSeatAllocation.ContractFee, amounts.ContractFee)) ||
|
|
174
|
+
Fail`Cannot repay. From seat allocation ${q(fromSeatAllocation)} does not equal amounts ${q(amounts)}.`;
|
|
175
|
+
|
|
176
|
+
isGTE(encumberedBalance, amounts.Principal) ||
|
|
177
|
+
Fail`Cannot repay. Principal ${q(amounts.Principal)} exceeds encumberedBalance ${q(encumberedBalance)}.`;
|
|
178
|
+
|
|
179
|
+
return harden({
|
|
180
|
+
shareWorth: withFees(shareWorth, amounts.PoolFee),
|
|
181
|
+
encumberedBalance: subtract(encumberedBalance, amounts.Principal),
|
|
182
|
+
poolStats: {
|
|
183
|
+
...poolStats,
|
|
184
|
+
totalRepays: add(poolStats.totalRepays, amounts.Principal),
|
|
185
|
+
totalPoolFees: add(poolStats.totalPoolFees, amounts.PoolFee),
|
|
186
|
+
totalContractFees: add(poolStats.totalContractFees, amounts.ContractFee),
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { AmountShape, BrandShape, RatioShape } from '@agoric/ertp';
|
|
2
|
+
import { M } from '@endo/patterns';
|
|
3
|
+
import { PendingTxStatus } from './constants.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @import {TypedPattern} from '@agoric/internal';
|
|
7
|
+
* @import {FastUsdcTerms} from './fast-usdc.contract.js';
|
|
8
|
+
* @import {USDCProposalShapes} from './pool-share-math.js';
|
|
9
|
+
* @import {CctpTxEvidence, FeeConfig, PendingTx, PoolMetrics} from './types.js';
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {Brand} brand must be a 'nat' brand, not checked
|
|
14
|
+
* @param {NatValue} [min]
|
|
15
|
+
*/
|
|
16
|
+
export const makeNatAmountShape = (brand, min) =>
|
|
17
|
+
harden({ brand, value: min ? M.gte(min) : M.nat() });
|
|
18
|
+
|
|
19
|
+
/** @param {Record<'PoolShares' | 'USDC', Brand<'nat'>>} brands */
|
|
20
|
+
export const makeProposalShapes = ({ PoolShares, USDC }) => {
|
|
21
|
+
/** @type {TypedPattern<USDCProposalShapes['deposit']>} */
|
|
22
|
+
const deposit = M.splitRecord(
|
|
23
|
+
{ give: { USDC: makeNatAmountShape(USDC, 1n) } },
|
|
24
|
+
{ want: { PoolShare: makeNatAmountShape(PoolShares) } },
|
|
25
|
+
);
|
|
26
|
+
/** @type {TypedPattern<USDCProposalShapes['withdraw']>} */
|
|
27
|
+
const withdraw = M.splitRecord({
|
|
28
|
+
give: { PoolShare: makeNatAmountShape(PoolShares, 1n) },
|
|
29
|
+
want: { USDC: makeNatAmountShape(USDC, 1n) },
|
|
30
|
+
});
|
|
31
|
+
return harden({ deposit, withdraw });
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** @type {TypedPattern<FastUsdcTerms>} */
|
|
35
|
+
export const FastUSDCTermsShape = harden({
|
|
36
|
+
usdcDenom: M.string(),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/** @type {TypedPattern<string>} */
|
|
40
|
+
export const EvmHashShape = M.string({
|
|
41
|
+
stringLengthLimit: 66,
|
|
42
|
+
});
|
|
43
|
+
harden(EvmHashShape);
|
|
44
|
+
|
|
45
|
+
/** @type {TypedPattern<CctpTxEvidence>} */
|
|
46
|
+
export const CctpTxEvidenceShape = {
|
|
47
|
+
aux: {
|
|
48
|
+
forwardingChannel: M.string(),
|
|
49
|
+
recipientAddress: M.string(),
|
|
50
|
+
},
|
|
51
|
+
blockHash: EvmHashShape,
|
|
52
|
+
blockNumber: M.bigint(),
|
|
53
|
+
blockTimestamp: M.bigint(),
|
|
54
|
+
chainId: M.number(),
|
|
55
|
+
tx: {
|
|
56
|
+
amount: M.bigint(),
|
|
57
|
+
forwardingAddress: M.string(),
|
|
58
|
+
},
|
|
59
|
+
txHash: EvmHashShape,
|
|
60
|
+
};
|
|
61
|
+
harden(CctpTxEvidenceShape);
|
|
62
|
+
|
|
63
|
+
/** @type {TypedPattern<PendingTx>} */
|
|
64
|
+
// @ts-expect-error TypedPattern not recognized as record
|
|
65
|
+
export const PendingTxShape = {
|
|
66
|
+
...CctpTxEvidenceShape,
|
|
67
|
+
status: M.or(...Object.values(PendingTxStatus)),
|
|
68
|
+
};
|
|
69
|
+
harden(PendingTxShape);
|
|
70
|
+
|
|
71
|
+
export const EudParamShape = {
|
|
72
|
+
EUD: M.string(),
|
|
73
|
+
};
|
|
74
|
+
harden(EudParamShape);
|
|
75
|
+
|
|
76
|
+
const NatAmountShape = { brand: BrandShape, value: M.nat() };
|
|
77
|
+
/** @type {TypedPattern<FeeConfig>} */
|
|
78
|
+
export const FeeConfigShape = {
|
|
79
|
+
flat: NatAmountShape,
|
|
80
|
+
variableRate: RatioShape,
|
|
81
|
+
maxVariable: NatAmountShape,
|
|
82
|
+
contractRate: RatioShape,
|
|
83
|
+
};
|
|
84
|
+
harden(FeeConfigShape);
|
|
85
|
+
|
|
86
|
+
/** @type {TypedPattern<PoolMetrics>} */
|
|
87
|
+
export const PoolMetricsShape = {
|
|
88
|
+
encumberedBalance: AmountShape,
|
|
89
|
+
shareWorth: RatioShape,
|
|
90
|
+
totalContractFees: AmountShape,
|
|
91
|
+
totalPoolFees: AmountShape,
|
|
92
|
+
totalBorrows: AmountShape,
|
|
93
|
+
totalRepays: AmountShape,
|
|
94
|
+
};
|
|
95
|
+
harden(PoolMetricsShape);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type * from './types.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ChainAddress } from '@agoric/orchestration';
|
|
2
|
+
import type { IBCChannelID } from '@agoric/vats';
|
|
3
|
+
import type { Amount } from '@agoric/ertp';
|
|
4
|
+
import type { PendingTxStatus } from './constants.js';
|
|
5
|
+
|
|
6
|
+
export type EvmHash = `0x${string}`;
|
|
7
|
+
export type NobleAddress = `noble1${string}`;
|
|
8
|
+
|
|
9
|
+
export interface CctpTxEvidence {
|
|
10
|
+
/** from Noble RPC */
|
|
11
|
+
aux: {
|
|
12
|
+
forwardingChannel: IBCChannelID;
|
|
13
|
+
recipientAddress: ChainAddress['value'];
|
|
14
|
+
};
|
|
15
|
+
blockHash: EvmHash;
|
|
16
|
+
blockNumber: bigint;
|
|
17
|
+
blockTimestamp: bigint;
|
|
18
|
+
chainId: number;
|
|
19
|
+
/** data covered by signature (aka txHash) */
|
|
20
|
+
tx: {
|
|
21
|
+
amount: bigint;
|
|
22
|
+
forwardingAddress: NobleAddress;
|
|
23
|
+
};
|
|
24
|
+
txHash: EvmHash;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type LogFn = (...args: unknown[]) => void;
|
|
28
|
+
|
|
29
|
+
export interface PendingTx extends CctpTxEvidence {
|
|
30
|
+
status: PendingTxStatus;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** internal key for `StatusManager` exo */
|
|
34
|
+
export type PendingTxKey = `pendingTx:${string}`;
|
|
35
|
+
|
|
36
|
+
/** internal key for `StatusManager` exo */
|
|
37
|
+
export type SeenTxKey = `seenTx:${string}`;
|
|
38
|
+
|
|
39
|
+
export type FeeConfig = {
|
|
40
|
+
flat: Amount<'nat'>;
|
|
41
|
+
variableRate: Ratio;
|
|
42
|
+
maxVariable: Amount<'nat'>;
|
|
43
|
+
contractRate: Ratio;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export interface PoolStats {
|
|
47
|
+
totalBorrows: Amount<'nat'>;
|
|
48
|
+
totalContractFees: Amount<'nat'>;
|
|
49
|
+
totalPoolFees: Amount<'nat'>;
|
|
50
|
+
totalRepays: Amount<'nat'>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface PoolMetrics extends PoolStats {
|
|
54
|
+
encumberedBalance: Amount<'nat'>;
|
|
55
|
+
shareWorth: Ratio;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type * from './constants.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** @import { VStorage } from '@agoric/client-utils' */
|
|
2
|
+
|
|
3
|
+
export const queryFastUSDCLocalChainAccount = async (
|
|
4
|
+
/** @type {VStorage} */ vstorage,
|
|
5
|
+
out = console,
|
|
6
|
+
) => {
|
|
7
|
+
const agoricAddr = await vstorage.readLatest(
|
|
8
|
+
'published.fastUSDC.settlementAccount',
|
|
9
|
+
);
|
|
10
|
+
out.log(`Got Fast USDC Local Chain Account ${agoricAddr}`);
|
|
11
|
+
return agoricAddr;
|
|
12
|
+
};
|
package/src/util/cctp.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { bech32 } from 'bech32';
|
|
3
|
+
import { ethers } from 'ethers';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Adapted from https://docs.noble.xyz/cctp/mint#encoding
|
|
7
|
+
*
|
|
8
|
+
* @param {string} address
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
export const encodeBech32Address = address => {
|
|
12
|
+
const decoded = bech32.decode(address);
|
|
13
|
+
const rawBytes = Buffer.from(bech32.fromWords(decoded.words));
|
|
14
|
+
|
|
15
|
+
const padded = Buffer.alloc(32);
|
|
16
|
+
rawBytes.copy(padded, 32 - rawBytes.length);
|
|
17
|
+
|
|
18
|
+
return `0x${padded.toString('hex')}`;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const tokenAbi = ['function approve(address spender, uint256 value) external'];
|
|
22
|
+
|
|
23
|
+
const contractAbi = [
|
|
24
|
+
'function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken) external',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export const makeProvider = (/** @type {string} */ rpc) =>
|
|
28
|
+
new ethers.JsonRpcProvider(rpc);
|
|
29
|
+
|
|
30
|
+
const USDC_DECIMALS = 6;
|
|
31
|
+
// For CCTP, noble's domain is universally "4"
|
|
32
|
+
const NOBLE_DOMAIN = 4;
|
|
33
|
+
|
|
34
|
+
export const depositForBurn = async (
|
|
35
|
+
/** @type {ethers.JsonRpcProvider} */ provider,
|
|
36
|
+
/** @type {string} */ ethSeed,
|
|
37
|
+
/** @type {string} */ tokenMessengerAddress,
|
|
38
|
+
/** @type {string} */ tokenAddress,
|
|
39
|
+
/** @type {string} */ destination,
|
|
40
|
+
/** @type {string} */ amount,
|
|
41
|
+
out = console,
|
|
42
|
+
) => {
|
|
43
|
+
const privateKey = ethSeed;
|
|
44
|
+
const wallet = new ethers.Wallet(privateKey, provider);
|
|
45
|
+
const contractAddress = tokenMessengerAddress;
|
|
46
|
+
const token = new ethers.Contract(tokenAddress, tokenAbi, wallet);
|
|
47
|
+
const contract = new ethers.Contract(contractAddress, contractAbi, wallet);
|
|
48
|
+
const parsedAmount = ethers.parseUnits(amount, USDC_DECIMALS);
|
|
49
|
+
out.log('approving');
|
|
50
|
+
const approveTx = await token.approve(contractAddress, parsedAmount);
|
|
51
|
+
out.log('Transaction sent, waiting for confirmation...');
|
|
52
|
+
const approveReceipt = await approveTx.wait();
|
|
53
|
+
out.log('Transaction confirmed in block', approveReceipt.blockNumber);
|
|
54
|
+
out.log('Transaction hash:', approveReceipt.hash);
|
|
55
|
+
|
|
56
|
+
const mintRecipient = encodeBech32Address(destination);
|
|
57
|
+
out.log('depositing for burn', parsedAmount, 4, mintRecipient, tokenAddress);
|
|
58
|
+
const tx = await contract.depositForBurn(
|
|
59
|
+
parsedAmount,
|
|
60
|
+
NOBLE_DOMAIN,
|
|
61
|
+
mintRecipient,
|
|
62
|
+
tokenAddress,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
out.log('Transaction sent, waiting for confirmation...');
|
|
66
|
+
const receipt = await tx.wait();
|
|
67
|
+
|
|
68
|
+
out.log('Transaction confirmed in block', receipt.blockNumber);
|
|
69
|
+
out.log('Transaction hash:', receipt.hash);
|
|
70
|
+
out.log('USDC transfer initiated successfully, our work here is done.');
|
|
71
|
+
};
|