@lifi/composer-sdk 0.0.1-alpha → 0.2.0-staging.4
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/README.md +37 -1
- package/dist/authoring/signatureArgs.cjs +2 -2
- package/dist/authoring/signatureArgs.cjs.map +1 -1
- package/dist/authoring/signatureArgs.js +2 -2
- package/dist/authoring/signatureArgs.js.map +1 -1
- package/dist/client.cjs +2 -2
- package/dist/client.cjs.map +1 -1
- package/dist/client.js +2 -2
- package/dist/client.js.map +1 -1
- package/dist/generated/config.generated.cjs.map +1 -1
- package/dist/generated/config.generated.d.cts +307 -16
- package/dist/generated/config.generated.d.ts +307 -16
- package/dist/generated/guards.generated.cjs +6 -0
- package/dist/generated/guards.generated.cjs.map +1 -1
- package/dist/generated/guards.generated.d.cts +1 -1
- package/dist/generated/guards.generated.d.ts +1 -1
- package/dist/generated/guards.generated.js +5 -0
- package/dist/generated/guards.generated.js.map +1 -1
- package/dist/generated/materialisers.generated.cjs +8 -2
- package/dist/generated/materialisers.generated.cjs.map +1 -1
- package/dist/generated/materialisers.generated.d.cts +1 -1
- package/dist/generated/materialisers.generated.d.ts +1 -1
- package/dist/generated/materialisers.generated.js +6 -1
- package/dist/generated/materialisers.generated.js.map +1 -1
- package/dist/generated/operations.generated.cjs +84 -14
- package/dist/generated/operations.generated.cjs.map +1 -1
- package/dist/generated/operations.generated.d.cts +208 -13
- package/dist/generated/operations.generated.d.ts +208 -13
- package/dist/generated/operations.generated.js +84 -14
- package/dist/generated/operations.generated.js.map +1 -1
- package/dist/guards.generated-BPLFwe7y.d.ts +19 -0
- package/dist/guards.generated-M3VAIevI.d.cts +19 -0
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/materialisers.generated-BfWJ-g7t.d.ts +29 -0
- package/dist/materialisers.generated-DaJRwiRe.d.cts +29 -0
- package/dist/run/inputs.cjs +8 -5
- package/dist/run/inputs.cjs.map +1 -1
- package/dist/run/inputs.d.cts +8 -0
- package/dist/run/inputs.d.ts +8 -0
- package/dist/run/inputs.js +8 -5
- package/dist/run/inputs.js.map +1 -1
- package/dist/sdk.cjs +1 -0
- package/dist/sdk.cjs.map +1 -1
- package/dist/sdk.js +1 -0
- package/dist/sdk.js.map +1 -1
- package/package.json +14 -6
- package/src/authoring/signatureArgs.ts +2 -2
- package/src/client.ts +1 -1
- package/src/examples/aaveClaimRewards.ts +80 -0
- package/src/examples/aaveRepay.ts +69 -0
- package/src/examples/aaveRepayWithATokens.ts +81 -0
- package/src/examples/aaveSetEMode.ts +68 -0
- package/src/examples/run.ts +41 -4
- package/src/examples/staged/aaveBorrow.ts +104 -0
- package/src/examples/staged/aaveDebtRebalanceExactOut.ts +301 -0
- package/src/examples/staged/aaveHealthFactorExplode.ts +70 -0
- package/src/examples/staged/aaveToMorphoDebtMigration.ts +190 -0
- package/src/examples/staged/aaveToMorphoDebtMigrationWithSwap.ts +221 -0
- package/src/examples/staged/aaveToMorphoDebtMigrationWithSwapExactOut.ts +302 -0
- package/src/examples/staged/flashloanRepay.ts +138 -0
- package/src/examples/staged/morphoBlueBorrow.ts +89 -0
- package/src/examples/staged/morphoBlueRepay.ts +91 -0
- package/src/examples/staged/registry.ts +78 -0
- package/src/examples/swapToRecipient.ts +0 -19
- package/src/examples/swapWithFee.ts +59 -0
- package/src/generated/config.generated.ts +364 -18
- package/src/generated/guards.generated.ts +12 -1
- package/src/generated/materialisers.generated.ts +24 -12
- package/src/generated/operations.generated.ts +373 -13
- package/src/run/inputs.ts +16 -5
- package/src/sdk.ts +1 -0
- package/dist/guards.generated-DKKDKREA.d.ts +0 -14
- package/dist/guards.generated-hrQ1KPNv.d.cts +0 -14
- package/dist/materialisers.generated-BsA1uDW8.d.ts +0 -24
- package/dist/materialisers.generated-nMxTAaVi.d.cts +0 -24
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { ComposeCompileRequest, Flow } from '@lifi/compose-spec';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createComposeSdk,
|
|
5
|
+
materialisers,
|
|
6
|
+
preconditions,
|
|
7
|
+
resources,
|
|
8
|
+
} from '../index.js';
|
|
9
|
+
import type { Address } from '../types.js';
|
|
10
|
+
|
|
11
|
+
import { BASE_URL } from './config.js';
|
|
12
|
+
|
|
13
|
+
// Aave v3 contracts on Ethereum mainnet.
|
|
14
|
+
const AAVE_V3_POOL = '0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2';
|
|
15
|
+
const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
|
|
16
|
+
const A_ETH_USDC = '0x98C23E9d8f34FEFb1B7BD6a91B7FF122F4e16F5c';
|
|
17
|
+
|
|
18
|
+
export interface AaveRepayWithATokensInput {
|
|
19
|
+
readonly owner: Address;
|
|
20
|
+
readonly proxyAddress: Address;
|
|
21
|
+
readonly expectedATokenBalance: `${bigint}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Repay an Aave v3 variable-rate USDC debt by burning aToken collateral
|
|
26
|
+
* already held by the proxy (no fresh asset transfer required).
|
|
27
|
+
*
|
|
28
|
+
* The aTokens are sourced from the proxy's existing balance via the
|
|
29
|
+
* `balanceOf` materialiser. The `erc20Balance` precondition fails fast
|
|
30
|
+
* if the expected aTokens haven't landed on the proxy yet.
|
|
31
|
+
*
|
|
32
|
+
* Demonstrates:
|
|
33
|
+
* - `aave.repayWithATokens` with the aToken as the resource input.
|
|
34
|
+
* - `balanceOf` materialiser to consume proxy-held aTokens directly.
|
|
35
|
+
* - `erc20Balance` precondition to guard against silent zero-value repays.
|
|
36
|
+
* - The `residual` port returns any aTokens not burned (when debt < input).
|
|
37
|
+
*/
|
|
38
|
+
export const buildAaveRepayWithATokens = ({
|
|
39
|
+
owner,
|
|
40
|
+
proxyAddress,
|
|
41
|
+
expectedATokenBalance,
|
|
42
|
+
}: AaveRepayWithATokensInput): {
|
|
43
|
+
flow: Flow;
|
|
44
|
+
request: ComposeCompileRequest;
|
|
45
|
+
} => {
|
|
46
|
+
const sdk = createComposeSdk({ baseUrl: BASE_URL });
|
|
47
|
+
|
|
48
|
+
const builder = sdk.flow(1, {
|
|
49
|
+
name: 'aave-repay-with-atokens',
|
|
50
|
+
inputs: {
|
|
51
|
+
aTokenIn: resources.erc20(A_ETH_USDC, 1),
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
builder.aave.repayWithATokens('repay-atokens', {
|
|
56
|
+
bind: { aTokenIn: builder.inputs.aTokenIn },
|
|
57
|
+
config: {
|
|
58
|
+
pool: AAVE_V3_POOL,
|
|
59
|
+
asset: USDC,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const flow = builder.build();
|
|
64
|
+
|
|
65
|
+
const request = sdk.request(flow, {
|
|
66
|
+
signer: owner,
|
|
67
|
+
inputs: {
|
|
68
|
+
aTokenIn: materialisers.balanceOf({ owner: proxyAddress }),
|
|
69
|
+
},
|
|
70
|
+
preconditions: [
|
|
71
|
+
preconditions.erc20Balance({
|
|
72
|
+
wallet: proxyAddress,
|
|
73
|
+
token: A_ETH_USDC,
|
|
74
|
+
balance: expectedATokenBalance,
|
|
75
|
+
}),
|
|
76
|
+
],
|
|
77
|
+
sweepTo: builder.context.sender,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return { flow, request };
|
|
81
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ComposeCompileRequest, Flow } from '@lifi/compose-spec';
|
|
2
|
+
|
|
3
|
+
import { createComposeSdk } from '../index.js';
|
|
4
|
+
import type { Address } from '../types.js';
|
|
5
|
+
|
|
6
|
+
import { BASE_URL } from './config.js';
|
|
7
|
+
|
|
8
|
+
// Aave v3 contracts on Ethereum mainnet.
|
|
9
|
+
const AAVE_V3_POOL = '0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2';
|
|
10
|
+
|
|
11
|
+
// Mainnet ETH-correlated eMode category. Categories are per-pool; check
|
|
12
|
+
// `Pool.getEModeCategoryData(id)` on the target chain before reusing this id.
|
|
13
|
+
const ETH_CORRELATED_CATEGORY = 1;
|
|
14
|
+
|
|
15
|
+
export interface AaveSetEModeInput {
|
|
16
|
+
readonly owner: Address;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Switch the executionAddress's Aave v3 eMode category.
|
|
21
|
+
*
|
|
22
|
+
* eMode is a per-user setting on the pool that allows higher LTV when both
|
|
23
|
+
* collateral and debt are correlated assets (e.g. ETH-LSTs, stablecoins).
|
|
24
|
+
* The setting persists across transactions until changed again.
|
|
25
|
+
*
|
|
26
|
+
* Demonstrates:
|
|
27
|
+
* - `aave.setEMode` as a side-effecting op with no resource inputs or outputs.
|
|
28
|
+
* - The op's category is validated at request-validation time against the
|
|
29
|
+
* protocol's uint8 range. Per-pool category existence (Ethereum has 40+
|
|
30
|
+
* categories, Base has 15+, Arbitrum 10+; new ones are added by governance)
|
|
31
|
+
* is verified at simulation time, not in the schema.
|
|
32
|
+
*
|
|
33
|
+
* Real flows typically combine this op with `aave.supply` + `aave.borrow` of
|
|
34
|
+
* correlated assets. The reset call (`category: 0`) is intentionally omitted —
|
|
35
|
+
* leaving eMode on persists the leveraged position past the tx, and resetting
|
|
36
|
+
* to 0 after a high-LTV borrow would revert with a sub-1 health factor.
|
|
37
|
+
*/
|
|
38
|
+
export const buildAaveSetEMode = ({
|
|
39
|
+
owner,
|
|
40
|
+
}: AaveSetEModeInput): {
|
|
41
|
+
flow: Flow;
|
|
42
|
+
request: ComposeCompileRequest;
|
|
43
|
+
} => {
|
|
44
|
+
const sdk = createComposeSdk({ baseUrl: BASE_URL });
|
|
45
|
+
|
|
46
|
+
const builder = sdk.flow(1, {
|
|
47
|
+
name: 'aave-set-emode',
|
|
48
|
+
inputs: {},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
builder.aave.setEMode('enter-eth-emode', {
|
|
52
|
+
bind: {},
|
|
53
|
+
config: {
|
|
54
|
+
pool: AAVE_V3_POOL,
|
|
55
|
+
category: ETH_CORRELATED_CATEGORY,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const flow = builder.build();
|
|
60
|
+
|
|
61
|
+
const request = sdk.request(flow, {
|
|
62
|
+
signer: owner,
|
|
63
|
+
inputs: {},
|
|
64
|
+
sweepTo: builder.context.sender,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return { flow, request };
|
|
68
|
+
};
|
package/src/examples/run.ts
CHANGED
|
@@ -8,12 +8,16 @@
|
|
|
8
8
|
* LIFI_API_KEY=your-key COMPOSER_BASE_URL=https://composer.li.quest yarn example <name>
|
|
9
9
|
*
|
|
10
10
|
* Available examples:
|
|
11
|
+
* aave-repay — repay an Aave v3 WETH debt by supplying WETH
|
|
12
|
+
* aave-repay-atoken — repay an Aave v3 USDC debt by burning proxy aTokens
|
|
13
|
+
* aave-claim — claim AAVE rewards and forward to a recipient
|
|
11
14
|
* approve-deposit — approve vault allowance then deposit USDC
|
|
12
15
|
* consolidate — consolidate ERC-20 balances to USDC
|
|
13
16
|
* consolidate-eth — consolidate ERC-20 balances to native ETH
|
|
14
17
|
* deposit-proxy — deposit tokens already on the proxy into Aave
|
|
15
18
|
* dust-sweep — split USDC 80/20 and sweep leftover dust
|
|
16
19
|
* swap — swap WETH to USDC via LI.FI
|
|
20
|
+
* swap-fee — swap WETH to USDC with a 50 bps integrator fee
|
|
17
21
|
* split-zap — split USDC 60/40 and zap into Aave + Morpho vaults
|
|
18
22
|
* split-arithmetic — split USDC 70/30 with arithmetic assertions
|
|
19
23
|
* zap — zap USDC into Aave lending position
|
|
@@ -35,6 +39,9 @@ import type { ComposeCompileRequest } from '@lifi/compose-spec';
|
|
|
35
39
|
|
|
36
40
|
import { createComposeSdk } from '../sdk.js';
|
|
37
41
|
|
|
42
|
+
import { buildAaveClaimRewards } from './aaveClaimRewards.js';
|
|
43
|
+
import { buildAaveRepay } from './aaveRepay.js';
|
|
44
|
+
import { buildAaveRepayWithATokens } from './aaveRepayWithATokens.js';
|
|
38
45
|
import { buildApproveAndDeposit } from './approveAndDeposit.js';
|
|
39
46
|
import {
|
|
40
47
|
buildClaimRewards,
|
|
@@ -56,11 +63,22 @@ import { buildSwapAndZapExample } from './swapAndZap.js';
|
|
|
56
63
|
import { buildSwapToRecipient } from './swapToRecipient.js';
|
|
57
64
|
import { buildSwapWithAllowRevertExample } from './swapWithAllowRevert.js';
|
|
58
65
|
import { buildSwapWithBalanceCheck } from './swapWithBalanceCheck.js';
|
|
66
|
+
import { buildSwapWithFeeExample } from './swapWithFee.js';
|
|
59
67
|
import { buildSwapWithOutputValidation } from './swapWithOutputValidation.js';
|
|
60
68
|
import { buildPartialTransfer, buildTransferTokens } from './transferTokens.js';
|
|
61
69
|
import { buildUntypedOpWithTypedRef } from './untypedOpWithTypedRef.js';
|
|
62
70
|
|
|
63
71
|
const EXAMPLES: Record<string, () => ComposeCompileRequest> = {
|
|
72
|
+
'aave-repay': () =>
|
|
73
|
+
buildAaveRepay({ owner: OWNER, amount: '500000000000000000' }).request,
|
|
74
|
+
'aave-repay-atoken': () =>
|
|
75
|
+
buildAaveRepayWithATokens({
|
|
76
|
+
owner: OWNER,
|
|
77
|
+
proxyAddress: PROXY,
|
|
78
|
+
expectedATokenBalance: '1000000000',
|
|
79
|
+
}).request,
|
|
80
|
+
'aave-claim': () =>
|
|
81
|
+
buildAaveClaimRewards({ owner: OWNER, recipient: RECIPIENT }).request,
|
|
64
82
|
'approve-deposit': () =>
|
|
65
83
|
buildApproveAndDeposit({ owner: OWNER, amount: '1000000000' }).request,
|
|
66
84
|
consolidate: () =>
|
|
@@ -87,6 +105,7 @@ const EXAMPLES: Record<string, () => ComposeCompileRequest> = {
|
|
|
87
105
|
}).request,
|
|
88
106
|
'dust-sweep': () => buildDustSweepExample().request,
|
|
89
107
|
swap: () => buildLifiSwapExample().request,
|
|
108
|
+
'swap-fee': () => buildSwapWithFeeExample().request,
|
|
90
109
|
'split-zap': () => buildSplitAndZapExample().request,
|
|
91
110
|
'split-arithmetic': () =>
|
|
92
111
|
buildSplitWithArithmetic({ owner: OWNER, amount: '1000000000' }).request,
|
|
@@ -133,17 +152,35 @@ const EXAMPLES: Record<string, () => ComposeCompileRequest> = {
|
|
|
133
152
|
};
|
|
134
153
|
|
|
135
154
|
const run = async () => {
|
|
136
|
-
const
|
|
137
|
-
const
|
|
155
|
+
const args = process.argv.slice(2);
|
|
156
|
+
const useStaged = args.includes('--staged');
|
|
157
|
+
const name = args.find((arg) => arg !== '--staged');
|
|
158
|
+
|
|
159
|
+
// Staged examples live under examples/staged/ and reference not-yet-prod
|
|
160
|
+
// definitions, so they are deliberately excluded from this production file's
|
|
161
|
+
// type-check. With --staged, pull their registry in via a NON-LITERAL dynamic
|
|
162
|
+
// import (kept `any` to tsc, so the staged files are not dragged into the
|
|
163
|
+
// production program). Requires the SDK to have been regenerated with
|
|
164
|
+
// `yarn generate --staged` first, so the staged ops/materialisers exist at
|
|
165
|
+
// runtime; run `yarn generate` afterwards to restore the production surface.
|
|
166
|
+
let examples: Record<string, () => ComposeCompileRequest> = EXAMPLES;
|
|
167
|
+
if (useStaged) {
|
|
168
|
+
const stagedModule = (await import(
|
|
169
|
+
`${import.meta.dirname}/staged/registry.js`
|
|
170
|
+
)) as { STAGED_EXAMPLES: Record<string, () => ComposeCompileRequest> };
|
|
171
|
+
examples = { ...EXAMPLES, ...stagedModule.STAGED_EXAMPLES };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const builder = name ? examples[name] : undefined;
|
|
138
175
|
|
|
139
176
|
if (!builder) {
|
|
140
|
-
const valid = Object.keys(
|
|
177
|
+
const valid = Object.keys(examples).join(', ');
|
|
141
178
|
console.error(
|
|
142
179
|
`Error: ${
|
|
143
180
|
name ? `unknown example "${name}"` : 'no example specified'
|
|
144
181
|
}\n` +
|
|
145
182
|
`Valid examples: ${valid}\n` +
|
|
146
|
-
`Usage: COMPOSER_BASE_URL=https://composer.li.quest yarn example <name>`,
|
|
183
|
+
`Usage: COMPOSER_BASE_URL=https://composer.li.quest yarn example [--staged] <name>`,
|
|
147
184
|
);
|
|
148
185
|
process.exit(1);
|
|
149
186
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { ComposeCompileRequest, Flow } from '@lifi/compose-spec';
|
|
2
|
+
|
|
3
|
+
import { createComposeSdk, guards } from '../../index.js';
|
|
4
|
+
import type { Address } from '../../types.js';
|
|
5
|
+
import { BASE_URL } from '../config.js';
|
|
6
|
+
|
|
7
|
+
// Aave v3 contracts on Ethereum mainnet.
|
|
8
|
+
const AAVE_V3_POOL = '0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2';
|
|
9
|
+
const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
|
|
10
|
+
const VDEBT_USDC = '0x72E95b8931767C79bA4EeE721354d6E99a61D004';
|
|
11
|
+
|
|
12
|
+
// 1.05 WAD (1e18) — minimum acceptable Aave health factor after the borrow.
|
|
13
|
+
// The on-chain VM reverts the run if the post-borrow HF drops below this.
|
|
14
|
+
const MIN_HEALTH_FACTOR_WAD = '1050000000000000000';
|
|
15
|
+
|
|
16
|
+
export interface AaveBorrowInput {
|
|
17
|
+
readonly owner: Address;
|
|
18
|
+
readonly recipient: Address;
|
|
19
|
+
readonly amount: `${bigint}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Borrow USDC against existing Aave v3 collateral and transfer it to a recipient,
|
|
24
|
+
* with an on-chain health-factor guard that reverts if the post-borrow position
|
|
25
|
+
* is too thin.
|
|
26
|
+
*
|
|
27
|
+
* The proxy address must already hold supplied collateral. The borrow op
|
|
28
|
+
* exposes the borrowed amount as a tracked resource so downstream nodes can
|
|
29
|
+
* spend it.
|
|
30
|
+
*
|
|
31
|
+
* Demonstrates:
|
|
32
|
+
* - `aave.borrow` with no resource input — the borrowed asset materialises
|
|
33
|
+
* as the `borrowed` resource output.
|
|
34
|
+
* - Chaining the borrow output into a `core.transfer` to forward funds.
|
|
35
|
+
* - The `debtBalance` handle reports the post-borrow variable-debt balance
|
|
36
|
+
* for the proxy.
|
|
37
|
+
* - `aave.getHealthFactor` + `core.numericInvariant` guard asserts that
|
|
38
|
+
* `healthFactor >= MIN_HEALTH_FACTOR_WAD` after the borrow; the run reverts
|
|
39
|
+
* on-chain if the threshold is violated.
|
|
40
|
+
*/
|
|
41
|
+
export const buildAaveBorrow = ({
|
|
42
|
+
owner,
|
|
43
|
+
recipient,
|
|
44
|
+
amount,
|
|
45
|
+
}: AaveBorrowInput): {
|
|
46
|
+
flow: Flow;
|
|
47
|
+
request: ComposeCompileRequest;
|
|
48
|
+
} => {
|
|
49
|
+
const sdk = createComposeSdk({ baseUrl: BASE_URL });
|
|
50
|
+
|
|
51
|
+
// The flow takes no resource inputs — Aave hands us the borrowed asset.
|
|
52
|
+
const builder = sdk.flow(1, {
|
|
53
|
+
name: 'aave-borrow-usdc',
|
|
54
|
+
inputs: {
|
|
55
|
+
recipient: 'address',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const borrowOut = builder.aave.borrow('borrow', {
|
|
60
|
+
bind: {},
|
|
61
|
+
config: {
|
|
62
|
+
pool: AAVE_V3_POOL,
|
|
63
|
+
asset: USDC,
|
|
64
|
+
variableDebtToken: VDEBT_USDC,
|
|
65
|
+
amount,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Read the post-borrow Aave health factor and assert it stays above the
|
|
70
|
+
// minimum threshold. The numericInvariant guard lowers to an on-chain
|
|
71
|
+
// `AssertRawInvariant` IR1 instruction; the run reverts if HF < threshold.
|
|
72
|
+
builder.aave.getHealthFactor('hf', {
|
|
73
|
+
bind: {},
|
|
74
|
+
config: { pool: AAVE_V3_POOL },
|
|
75
|
+
guards: [
|
|
76
|
+
guards.coreNumericInvariant({
|
|
77
|
+
port: 'healthFactor',
|
|
78
|
+
op: 'gte',
|
|
79
|
+
threshold: MIN_HEALTH_FACTOR_WAD,
|
|
80
|
+
}),
|
|
81
|
+
],
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Forward the freshly borrowed USDC to the recipient.
|
|
85
|
+
builder.core.transfer('send-borrowed', {
|
|
86
|
+
bind: {
|
|
87
|
+
amount: borrowOut.borrowed,
|
|
88
|
+
recipient: builder.inputs.recipient,
|
|
89
|
+
},
|
|
90
|
+
config: {},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const flow = builder.build();
|
|
94
|
+
|
|
95
|
+
const request = sdk.request(flow, {
|
|
96
|
+
signer: owner,
|
|
97
|
+
inputs: {
|
|
98
|
+
recipient,
|
|
99
|
+
},
|
|
100
|
+
sweepTo: builder.context.sender,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return { flow, request };
|
|
104
|
+
};
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import type { ComposeCompileRequest, Flow } from '@lifi/compose-spec';
|
|
2
|
+
|
|
3
|
+
import { createComposeSdk, materialisers, resources } from '../../index.js';
|
|
4
|
+
import type { Address } from '../../types.js';
|
|
5
|
+
import { BASE_URL } from '../config.js';
|
|
6
|
+
|
|
7
|
+
// Base mainnet token addresses.
|
|
8
|
+
const BASE_WETH = '0x4200000000000000000000000000000000000006';
|
|
9
|
+
const BASE_USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
|
|
10
|
+
// Circle EURC on Base (6 decimals).
|
|
11
|
+
const BASE_EURC = '0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42';
|
|
12
|
+
|
|
13
|
+
// Token decimals for the Paraswap BUY (raw-address requests require them).
|
|
14
|
+
const EURC_DECIMALS = 6;
|
|
15
|
+
const USDC_DECIMALS = 6;
|
|
16
|
+
|
|
17
|
+
// Aave V3 on Base: pool + WETH aToken + variable-debt tokens for the old (USDC)
|
|
18
|
+
// and new (EURC) debt assets. The variable-debt token only feeds aave.borrow
|
|
19
|
+
// (to read the post-borrow debt balance); aave.repay infers the asset from its
|
|
20
|
+
// bound input resource. Verified live against Pool.getReserveData on Base.
|
|
21
|
+
const AAVE_V3_POOL_BASE = '0xA238Dd80C259a72e81d7e4664a9801593F98d1c5';
|
|
22
|
+
const A_BAS_WETH = '0xD4a0e0b9149BCee3C920d2E00b5dE09138fd8bb7';
|
|
23
|
+
const VDEBT_BAS_USDC = '0x59dca05b6c26dbd64b5381374aAaC5CD05644C28';
|
|
24
|
+
const VDEBT_BAS_EURC = '0x03D01595769333174036832e18fA2f17C74f8161';
|
|
25
|
+
|
|
26
|
+
// Aave V3 flashloan premium (FLASHLOAN_PREMIUM_TOTAL) = 5 bps of principal.
|
|
27
|
+
// Mirrors the builtins package's `computeDefaultFee('aave-v3', amount)`,
|
|
28
|
+
// inlined so the example stays free of compose-internals imports (the SDK must
|
|
29
|
+
// not cross the manifest boundary into the builtins package). Override if the
|
|
30
|
+
// pool's premium has changed on-chain.
|
|
31
|
+
const AAVE_V3_FEE_BPS = 5n;
|
|
32
|
+
const BPS_DENOMINATOR = 10_000n;
|
|
33
|
+
const computeAaveV3Fee = (principal: bigint): bigint =>
|
|
34
|
+
(principal * AAVE_V3_FEE_BPS) / BPS_DENOMINATOR;
|
|
35
|
+
|
|
36
|
+
// Source-cap slippage on the EURC → USDC BUY, in basis points. This bounds how
|
|
37
|
+
// much extra EURC Augustus may pull above the quoted source before refunding.
|
|
38
|
+
// `newDebtBorrowAmount` must exceed the resulting `maxAmountIn`.
|
|
39
|
+
const DEFAULT_MAX_SLIPPAGE_BPS = 100; // 1%
|
|
40
|
+
|
|
41
|
+
export interface AaveDebtRebalanceExactOutInput {
|
|
42
|
+
readonly owner: Address;
|
|
43
|
+
/**
|
|
44
|
+
* WETH collateral used to seed the position (in smallest units, 18 decimals).
|
|
45
|
+
* In production the position already exists and this seed step is omitted —
|
|
46
|
+
* the collateral is never touched by the rebalance itself.
|
|
47
|
+
*/
|
|
48
|
+
readonly collateralAmount: `${bigint}`;
|
|
49
|
+
/**
|
|
50
|
+
* Outstanding USDC debt to rebalance away from (6 decimals). The flashloan is
|
|
51
|
+
* sized to this; in production size it just above the live debt so the capped
|
|
52
|
+
* repay clears it after accrued interest.
|
|
53
|
+
*/
|
|
54
|
+
readonly debtAmount: `${bigint}`;
|
|
55
|
+
/**
|
|
56
|
+
* EURC to borrow as the *new* debt (6 decimals). Sized as a *generous upper
|
|
57
|
+
* bound* — large enough that the exact-output BUY can always produce
|
|
58
|
+
* `debtAmount + aaveV3Fee` USDC even after `maxSlippageBps` of source slippage
|
|
59
|
+
* (i.e. `newDebtBorrowAmount > maxAmountIn`). The BUY pulls only what it needs;
|
|
60
|
+
* the unspent EURC is repaid straight back into the freshly-minted EURC debt,
|
|
61
|
+
* so the *net* new debt is the original USDC debt (in EURC terms) plus only
|
|
62
|
+
* the slippage/fee actually consumed — NOT the full upper bound.
|
|
63
|
+
* Note: EUR/USD is not 1:1 — size off a live BUY quote, e.g.
|
|
64
|
+
* newDebtBorrowAmount ≈ ceil((debtAmount + aaveV3Fee) * eurcPerUsdc * (1 + slippage))
|
|
65
|
+
*/
|
|
66
|
+
readonly newDebtBorrowAmount: `${bigint}`;
|
|
67
|
+
/** Optional source-cap slippage on the BUY, in bps (default 100 = 1%). */
|
|
68
|
+
readonly maxSlippageBps?: number;
|
|
69
|
+
/** Optional integrator label for analytics / fee capture. */
|
|
70
|
+
readonly partner?: string;
|
|
71
|
+
/** Optional fee recipient; required to actually receive a partner fee. */
|
|
72
|
+
readonly partnerAddress?: Address;
|
|
73
|
+
/** Optional fixed partner fee in bps (e.g. '50' = 0.5%); needs partnerAddress. */
|
|
74
|
+
readonly partnerFeeBps?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Atomically **rebalance** (debt-swap) an Aave V3 position on Base from USDC
|
|
79
|
+
* debt into EURC debt — same protocol, same WETH collateral, only the debt
|
|
80
|
+
* *token* changes — using a native **exact-output** (Paraswap/Velora BUY) swap.
|
|
81
|
+
*
|
|
82
|
+
* This is the single-protocol "debt switch" pattern productised by Aave's
|
|
83
|
+
* `ParaSwapDebtSwapAdapter` and DeFi Saver's "Debt Switch": flashloan/borrow the
|
|
84
|
+
* new debt slightly over a cap, exact-out-swap to produce exactly the old debt
|
|
85
|
+
* owed, repay the old debt, then repay the leftover new asset back into the
|
|
86
|
+
* freshly-created new debt so lasting debt = only what was consumed. Contrast
|
|
87
|
+
* with `aaveToMorphoDebtMigrationWithSwapExactOut.ts`, which *migrates* the whole
|
|
88
|
+
* position across protocols (Aave → Morpho) and therefore needs a second
|
|
89
|
+
* flashloan to move the collateral; here the collateral never moves, so a single
|
|
90
|
+
* flashloan of the old debt token suffices.
|
|
91
|
+
*
|
|
92
|
+
* Why rebalance USDC → EURC: at the time of writing EURC's Base Aave V3 variable
|
|
93
|
+
* borrow APR (~3.87%) sits below USDC's (~4.28%), and a EUR-based borrower may
|
|
94
|
+
* also want the liability denominated in EUR to match income/assets (removing
|
|
95
|
+
* USD/EUR FX risk on the debt). The reward is the APR delta over the holding
|
|
96
|
+
* period; the cost is the Aave flashloan premium plus the realised swap
|
|
97
|
+
* slippage on the consumed notional only.
|
|
98
|
+
*
|
|
99
|
+
* Phase 1 — provision the starting position on Aave V3 (illustrative; omit in
|
|
100
|
+
* production where the position already exists):
|
|
101
|
+
* - User direct-deposits WETH.
|
|
102
|
+
* - `lifi.zap` supplies the WETH to Aave (WETH → aBasWETH routing edge).
|
|
103
|
+
* - `aave.borrow` mints the USDC debt being rebalanced; the borrowed USDC is
|
|
104
|
+
* swept back to the signer as the user's existing loan proceeds.
|
|
105
|
+
*
|
|
106
|
+
* Phase 2 — rebalance USDC debt → EURC debt with one flashloan:
|
|
107
|
+
* - Flashloan (Aave V3, USDC) bridges the old-debt payoff before the new debt
|
|
108
|
+
* exists.
|
|
109
|
+
* - `aave.repay(mode: 'max')` clears the USDC debt with the flashloan; Aave's
|
|
110
|
+
* Pool clamps to the outstanding debt exactly, any cap residual is swept.
|
|
111
|
+
* - `aave.borrow` mints a *capped* (generously over-sized) `newDebtBorrowAmount`
|
|
112
|
+
* of EURC against the same, still-supplied WETH collateral.
|
|
113
|
+
* - `paraswap.buy` buys *exactly* `debtAmount + aaveV3Fee` USDC with the borrowed
|
|
114
|
+
* EURC, pulling only the EURC needed. `amountOut` is the exact figure that
|
|
115
|
+
* settles the USDC flashloan leg; `unspentIn` is the refunded, unspent EURC.
|
|
116
|
+
* - `lifi.flashloanRepay` settles the USDC leg from `buy.amountOut` (no
|
|
117
|
+
* feeBuffer: `amountOut == principal + fee` by construction).
|
|
118
|
+
* - `aave.repay(mode: 'max')` repays the unspent EURC (`buy.unspentIn`) straight
|
|
119
|
+
* back into the freshly-minted EURC debt, unwinding the borrow buffer. Any
|
|
120
|
+
* genuine over-repay surfaces through the repay op's `residual`, swept to the
|
|
121
|
+
* signer.
|
|
122
|
+
*
|
|
123
|
+
* After execution the signer holds the same WETH collateral on Aave V3 with EURC
|
|
124
|
+
* debt equal to the original USDC debt (in EUR terms) plus only the consumed
|
|
125
|
+
* slippage/fee; the USDC debt is closed.
|
|
126
|
+
*/
|
|
127
|
+
export const buildAaveDebtRebalanceExactOut = ({
|
|
128
|
+
owner,
|
|
129
|
+
collateralAmount,
|
|
130
|
+
debtAmount,
|
|
131
|
+
newDebtBorrowAmount,
|
|
132
|
+
maxSlippageBps = DEFAULT_MAX_SLIPPAGE_BPS,
|
|
133
|
+
partner,
|
|
134
|
+
partnerAddress,
|
|
135
|
+
partnerFeeBps,
|
|
136
|
+
}: AaveDebtRebalanceExactOutInput): {
|
|
137
|
+
flow: Flow;
|
|
138
|
+
request: ComposeCompileRequest;
|
|
139
|
+
} => {
|
|
140
|
+
const sdk = createComposeSdk({ baseUrl: BASE_URL });
|
|
141
|
+
|
|
142
|
+
// The USDC the BUY must produce: the flashloan principal plus the Aave V3
|
|
143
|
+
// flashloan premium. This is exactly what settles the debt flashloan leg.
|
|
144
|
+
const exactUsdcOut = (
|
|
145
|
+
BigInt(debtAmount) + computeAaveV3Fee(BigInt(debtAmount))
|
|
146
|
+
).toString() as `${bigint}`;
|
|
147
|
+
|
|
148
|
+
const builder = sdk.flow(8453, {
|
|
149
|
+
name: 'aave-debt-rebalance-usdc-to-eurc-exact-out',
|
|
150
|
+
inputs: {
|
|
151
|
+
initialCollateral: resources.erc20(BASE_WETH, 8453),
|
|
152
|
+
// Single flashloan: the old (USDC) debt token. No collateral flashloan is
|
|
153
|
+
// needed because the WETH collateral never leaves Aave.
|
|
154
|
+
debtFlashloan: resources.erc20(BASE_USDC, 8453),
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// --- Phase 1: provision the starting Aave V3 USDC borrow -----------------
|
|
159
|
+
|
|
160
|
+
// WETH → aBasWETH via the aave routing edge family (Aave V3 supply). The
|
|
161
|
+
// supplied collateral is never withdrawn — it backs the new EURC debt — so
|
|
162
|
+
// the aBasWETH output is intentionally left unconsumed AND unswept: it is the
|
|
163
|
+
// user's resulting collateral position and must stay on the (persistent,
|
|
164
|
+
// per-user) proxy. This is why the flow does NOT set a blanket `sweepTo`
|
|
165
|
+
// (which would try to transfer the aToken out and revert, since collateral
|
|
166
|
+
// backing an open debt cannot be moved); instead we explicitly transfer only
|
|
167
|
+
// the genuinely-loose tokens below.
|
|
168
|
+
builder.lifi.zap('supply-collateral', {
|
|
169
|
+
bind: { amountIn: builder.inputs.initialCollateral },
|
|
170
|
+
config: { resourceOut: resources.erc20(A_BAS_WETH, 8453) },
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Borrow the USDC debt being rebalanced. The proceeds are the user's loan;
|
|
174
|
+
// they remain as the proxy's residual USDC and are forwarded to the signer at
|
|
175
|
+
// the end of the flow (see `payout-borrow-proceeds`).
|
|
176
|
+
builder.aave.borrow('borrow-usdc-debt', {
|
|
177
|
+
bind: {},
|
|
178
|
+
config: {
|
|
179
|
+
pool: AAVE_V3_POOL_BASE,
|
|
180
|
+
asset: BASE_USDC,
|
|
181
|
+
variableDebtToken: VDEBT_BAS_USDC,
|
|
182
|
+
amount: debtAmount,
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// --- Phase 2: rebalance USDC debt → EURC debt ----------------------------
|
|
187
|
+
|
|
188
|
+
// Settle the USDC debt with the flashloan. mode: 'max' makes Aave's Pool
|
|
189
|
+
// clamp paybackAmount to the outstanding debt exactly; unspent flashloan
|
|
190
|
+
// principal flows through the op's residual port (swept).
|
|
191
|
+
builder.aave.repay('repay-usdc-debt', {
|
|
192
|
+
bind: {
|
|
193
|
+
assetIn: builder.inputs.debtFlashloan,
|
|
194
|
+
onBehalfOf: builder.context.executionAddress,
|
|
195
|
+
},
|
|
196
|
+
config: { pool: AAVE_V3_POOL_BASE, mode: 'max' },
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Borrow a capped (generously over-sized) EURC amount against the same WETH
|
|
200
|
+
// collateral, now that the USDC debt is cleared. The over-borrow is
|
|
201
|
+
// intentional and is unwound below.
|
|
202
|
+
const borrowEurc = builder.aave.borrow('borrow-eurc-debt', {
|
|
203
|
+
bind: {},
|
|
204
|
+
config: {
|
|
205
|
+
pool: AAVE_V3_POOL_BASE,
|
|
206
|
+
asset: BASE_EURC,
|
|
207
|
+
variableDebtToken: VDEBT_BAS_EURC,
|
|
208
|
+
amount: newDebtBorrowAmount,
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Exact-output BUY: produce EXACTLY `exactUsdcOut` USDC from the borrowed
|
|
213
|
+
// EURC, pulling only the EURC needed and refunding the rest to the proxy
|
|
214
|
+
// (surfaced as `unspentIn`). `amountOut` is the exact figure that settles the
|
|
215
|
+
// USDC flashloan leg.
|
|
216
|
+
const buyUsdc = builder.paraswap.buy('buy-usdc-exact', {
|
|
217
|
+
bind: { amountIn: borrowEurc.borrowed },
|
|
218
|
+
config: {
|
|
219
|
+
resourceOut: resources.erc20(BASE_USDC, 8453),
|
|
220
|
+
exactAmountOut: exactUsdcOut,
|
|
221
|
+
srcDecimals: EURC_DECIMALS,
|
|
222
|
+
destDecimals: USDC_DECIMALS,
|
|
223
|
+
maxSlippageBps,
|
|
224
|
+
...(partner !== undefined ? { partner } : {}),
|
|
225
|
+
...(partnerAddress !== undefined ? { partnerAddress } : {}),
|
|
226
|
+
...(partnerFeeBps !== undefined ? { partnerFeeBps } : {}),
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Settle the USDC flashloan leg with the exact BUY output. No feeBuffer:
|
|
231
|
+
// `amountOut == principal + fee` by construction.
|
|
232
|
+
builder.lifi.flashloanRepay('repay-debt-flashloan', {
|
|
233
|
+
bind: { funds: buyUsdc.amountOut },
|
|
234
|
+
config: { leg: 'debtFlashloan' },
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Unwind the over-borrow: repay the proxy's FULL residual EURC back into the
|
|
238
|
+
// freshly-minted EURC debt, so the new debt equals exactly the EURC the swap
|
|
239
|
+
// consumed. We read the live EURC balance rather than binding `buy.unspentIn`:
|
|
240
|
+
// unspentIn is only Augustus's refund (cap − used), but the proxy ALSO still
|
|
241
|
+
// holds the headroom we over-borrowed above the swap cap (borrow − cap), which
|
|
242
|
+
// Augustus never pulled. The live balance captures both, leaving zero idle.
|
|
243
|
+
// mode: 'exact' pulls precisely this balance (Aave clamps to min(balance,
|
|
244
|
+
// debt)); mode: 'max' would pass type(uint256).max and try to pull the whole
|
|
245
|
+
// outstanding debt, far exceeding the residual on the proxy, and revert.
|
|
246
|
+
const eurcResidual = builder.core.balanceOf('read-eurc-residual', {
|
|
247
|
+
bind: {},
|
|
248
|
+
config: { token: BASE_EURC },
|
|
249
|
+
});
|
|
250
|
+
builder.aave.repay('repay-eurc-remainder', {
|
|
251
|
+
bind: {
|
|
252
|
+
assetIn: eurcResidual.balance,
|
|
253
|
+
onBehalfOf: builder.context.executionAddress,
|
|
254
|
+
},
|
|
255
|
+
config: { pool: AAVE_V3_POOL_BASE, mode: 'exact' },
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Forward the genuinely-loose token — the residual USDC loan proceeds — to the
|
|
259
|
+
// signer. We read the proxy's LIVE USDC balance rather than the fixed borrowed
|
|
260
|
+
// amount: Aave's `mode: 'max'` USDC-debt repay overshoots the borrow by up to
|
|
261
|
+
// 1 wei (ray-math round-trip), so the proxy ends a wei short of `borrowed` —
|
|
262
|
+
// transferring a fixed amount would revert "transfer amount exceeds balance".
|
|
263
|
+
// The live balance always moves exactly what is there.
|
|
264
|
+
//
|
|
265
|
+
// We deliberately do NOT use a blanket `sweepTo`: the only other proxy-held
|
|
266
|
+
// balance is the aBasWETH collateral position, which cannot be transferred
|
|
267
|
+
// while the EURC debt is open (Aave's finalizeTransfer reverts). The position
|
|
268
|
+
// therefore stays on the persistent per-user proxy; we move out only the USDC.
|
|
269
|
+
const usdcResidual = builder.core.balanceOf('read-usdc-residual', {
|
|
270
|
+
bind: {},
|
|
271
|
+
config: { token: BASE_USDC },
|
|
272
|
+
});
|
|
273
|
+
builder.core.transfer('payout-borrow-proceeds', {
|
|
274
|
+
bind: {
|
|
275
|
+
amount: usdcResidual.balance,
|
|
276
|
+
recipient: builder.context.sender,
|
|
277
|
+
},
|
|
278
|
+
config: {},
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const flow = builder.build();
|
|
282
|
+
|
|
283
|
+
const request = sdk.request(flow, {
|
|
284
|
+
signer: owner,
|
|
285
|
+
inputs: {
|
|
286
|
+
initialCollateral: materialisers.directDeposit({
|
|
287
|
+
amount: collateralAmount,
|
|
288
|
+
}),
|
|
289
|
+
debtFlashloan: materialisers.flashloan({
|
|
290
|
+
providerKind: 'aave-v3',
|
|
291
|
+
amount: debtAmount,
|
|
292
|
+
}),
|
|
293
|
+
},
|
|
294
|
+
// No blanket `sweepTo`: the resulting aBasWETH collateral position stays on
|
|
295
|
+
// the proxy (it backs the open EURC debt and cannot be moved). The loose
|
|
296
|
+
// USDC proceeds are forwarded via the explicit `payout-borrow-proceeds`
|
|
297
|
+
// transfer above.
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
return { flow, request };
|
|
301
|
+
};
|