Skip to content

Commit 55aa621

Browse files
authored
Merge pull request #7 from bcnmy/feat/examples
feat: examples
2 parents 03af373 + a85bd97 commit 55aa621

13 files changed

Lines changed: 1770 additions & 0 deletions

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ With composable transactions, you describe what should happen — not just what
1717
- [Pre-conditions and post-conditions](#pre-conditions-and-post-conditions)
1818
- [Runtime values](#runtime-values)
1919
- [On-chain constraints](#on-chain-constraints)
20+
- [Examples](#examples)
2021
- [Installation](#installation)
2122
- [Smart Batching Core](#smart-batching-core)
2223
- [createComposableBatch](#createcomposablebatch)
@@ -289,6 +290,27 @@ This pattern is a slippage guard: the batch only proceeds if the post-swap balan
289290

290291
---
291292

293+
## Examples
294+
295+
End-to-end examples showing real-world DeFi flows. Each file is self-contained and runs against Base mainnet addresses.
296+
297+
| Example | Description |
298+
|---------|-------------|
299+
| [01 — Safe ERC-20 transfer](./examples/01-safe-erc20-transfer.ts) | USDC transfer with sender pre-condition and recipient balance post-condition |
300+
| [02 — Aave deposit guarded](./examples/02-aave-deposit-guarded.ts) | Aave V3 deposit gated by balance and allowance pre-conditions |
301+
| [03 — Aave full deposit](./examples/03-aave-full-deposit.ts) | Atomic approve + Aave supply that sweeps the full runtime USDC balance |
302+
| [04 — Vault auto-compound](./examples/04-vault-auto-compound.ts) | ERC-4626 auto-compound with runtime balance and a deposit range guard |
303+
| [05 — Swap output guard](./examples/05-swap-output-guard.ts) | Uniswap V3 swap with a post-condition asserting minimum WETH received |
304+
| [06 — Compound debt repay](./examples/06-compound-debt-repay.ts) | Compound V3 full debt repayment using live `borrowBalanceOf` at execution time |
305+
| [07 — MEV-protected swap](./examples/07-mev-protected-swap.ts) | Uniswap swap gated by a live Chainlink oracle price band to block sandwich attacks |
306+
| [08 — Swap and vault deposit](./examples/08-swap-and-vault-deposit.ts) | Swap output captured at execution time and piped atomically into a yield vault |
307+
| [09 — Collateral top-up](./examples/09-collateral-topup.ts) | Keeper-triggered Aave collateral top-up using storage to lock in the amount |
308+
| [10 — Leverage loop](./examples/10-leverage-loop.ts) | Single-batch Aave leverage loop: deposit collateral → borrow → swap → re-deposit |
309+
| [11 — OR stop-loss / take-profit](./examples/11-or-stop-loss-take-profit.ts) | WETH exit triggered by stop-loss OR take-profit in a single OR predicate |
310+
| [12 — Cross-chain predicate-gated](./examples/12-cross-chain-predicate-gated.ts) | Across bridge + Aave deposit across Base → Arbitrum as one MEE supertransaction |
311+
312+
---
313+
292314
## Installation
293315

294316
```bash

examples/01-safe-erc20-transfer.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* Example: Safe ERC-20 transfer with pre/post guards
3+
*
4+
* Scenario: Send 100 USDC to a recipient. Simple enough — but in production
5+
* you want guarantees that plain `transfer` cannot give you:
6+
*
7+
* - The SCA actually holds enough before the transfer executes
8+
* - The recipient genuinely received the funds (not silently swallowed by
9+
* a fee-on-transfer token or a broken contract)
10+
* - If either condition fails, no state changes at all
11+
*
12+
* This is the most fundamental ERC-8211 pattern: pre-condition → action →
13+
* post-condition. Everything is atomic — either all three succeed or none do.
14+
*/
15+
16+
import { createComposableBatch } from '@biconomy/smart-batching';
17+
import { createPublicClient, http, parseUnits } from 'viem';
18+
import { base } from 'viem/chains';
19+
20+
// ─── Addresses (Base mainnet) ────────────────────────────────────────────────
21+
22+
const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
23+
const recipient = '0xRecipientAddress' as `0x${string}`;
24+
25+
// ─── Setup ───────────────────────────────────────────────────────────────────
26+
27+
const publicClient = createPublicClient({ chain: base, transport: http() });
28+
const scaAddress = '0xYourSmartAccountAddress' as `0x${string}`;
29+
30+
// ─── Transfer parameters ─────────────────────────────────────────────────────
31+
32+
const TRANSFER_AMOUNT = parseUnits('100', 6); // 100 USDC
33+
34+
// ─── Build the batch ─────────────────────────────────────────────────────────
35+
36+
const batch = createComposableBatch(publicClient, scaAddress);
37+
const usdc = batch.erc20Token(USDC);
38+
39+
// Read recipient balance now so the post-condition can verify the exact delta
40+
const recipientBalanceBefore = await usdc.read({
41+
functionName: 'balanceOf',
42+
args: [recipient],
43+
});
44+
45+
batch.add([
46+
// ── Pre-condition: sender has enough ──────────────────────────────────────
47+
// Reverts the entire batch if the SCA doesn't hold at least TRANSFER_AMOUNT.
48+
// Catches insufficient-balance situations before any gas is spent on a
49+
// transfer that would fail anyway.
50+
usdc.check({
51+
functionName: 'balanceOf',
52+
args: [scaAddress],
53+
constraint: { gte: TRANSFER_AMOUNT },
54+
}),
55+
56+
// ── Action: transfer ──────────────────────────────────────────────────────
57+
usdc.write({
58+
functionName: 'transfer',
59+
args: [recipient, TRANSFER_AMOUNT],
60+
}),
61+
62+
// ── Post-condition: recipient actually received the funds ─────────────────
63+
// Verifies the recipient's balance increased by at least TRANSFER_AMOUNT.
64+
// This catches fee-on-transfer tokens and any protocol that intercepts
65+
// the transfer without delivering the full amount to the recipient.
66+
usdc.check({
67+
functionName: 'balanceOf',
68+
args: [recipient],
69+
constraint: { gte: recipientBalanceBefore + TRANSFER_AMOUNT },
70+
}),
71+
]);
72+
73+
// ─── Generate calldata ────────────────────────────────────────────────────────
74+
75+
const calls = await batch.toCalls();
76+
// biome-ignore lint/suspicious/noConsole: intentional in examples
77+
console.log('Composable calls:', JSON.stringify(calls, null, 2));
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* Example: Pre-conditions — safe Aave deposit
3+
*
4+
* Scenario: Deposit USDC into Aave V3 to start earning yield.
5+
*
6+
* The problem without pre-conditions: if the SCA doesn't hold enough USDC, or
7+
* hasn't approved the Aave pool, the deposit call reverts on-chain — wasting
8+
* gas and leaving the batch in an inconsistent state.
9+
*
10+
* ERC-8211 pre-conditions let you place `check()` calls *before* the main
11+
* action. If any condition fails, the entire batch is rejected before any
12+
* state changes happen. Think of them as on-chain assertions that gate
13+
* execution.
14+
*/
15+
16+
import { createComposableBatch } from '@biconomy/smart-batching';
17+
import { createPublicClient, http, parseUnits } from 'viem';
18+
import { base } from 'viem/chains';
19+
20+
// ─── Addresses (Base mainnet) ────────────────────────────────────────────────
21+
22+
const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
23+
const AAVE_V3_POOL = '0xA238Dd80C259a72e81d7e4664a9801593F98d1c5';
24+
25+
// ─── Minimal Aave V3 Pool ABI ────────────────────────────────────────────────
26+
27+
const AAVE_V3_POOL_ABI = [
28+
{
29+
name: 'supply',
30+
type: 'function',
31+
stateMutability: 'nonpayable',
32+
inputs: [
33+
{ name: 'asset', type: 'address' },
34+
{ name: 'amount', type: 'uint256' },
35+
{ name: 'onBehalfOf', type: 'address' },
36+
{ name: 'referralCode', type: 'uint16' },
37+
],
38+
outputs: [],
39+
},
40+
] as const;
41+
42+
// ─── Setup ───────────────────────────────────────────────────────────────────
43+
44+
const publicClient = createPublicClient({ chain: base, transport: http() });
45+
46+
// Your ERC-4337 smart account address
47+
const scaAddress = '0xYourSmartAccountAddress' as `0x${string}`;
48+
49+
// ─── Deposit parameters ───────────────────────────────────────────────────────
50+
51+
const DEPOSIT_AMOUNT = parseUnits('500', 6); // Deposit 500 USDC into Aave
52+
53+
// ─── Build the batch ─────────────────────────────────────────────────────────
54+
55+
const batch = createComposableBatch(publicClient, scaAddress);
56+
57+
const usdc = batch.erc20Token(USDC);
58+
const aavePool = batch.contract(AAVE_V3_POOL, AAVE_V3_POOL_ABI);
59+
60+
batch.add([
61+
// ── Pre-condition 1: assert the SCA holds enough USDC ─────────────────────
62+
// If the balance is below DEPOSIT_AMOUNT, the batch reverts here — the
63+
// deposit call is never reached and no gas is wasted on a doomed transaction.
64+
usdc.check({
65+
functionName: 'balanceOf',
66+
args: [scaAddress],
67+
constraint: { gte: DEPOSIT_AMOUNT },
68+
}),
69+
70+
// ── Pre-condition 2: assert the Aave pool has sufficient allowance ─────────
71+
// Aave's `supply` will pull USDC from the SCA via transferFrom. If the
72+
// allowance hasn't been set (or has been partially consumed), this check
73+
// catches it before the deposit is even attempted.
74+
usdc.check({
75+
functionName: 'allowance',
76+
args: [scaAddress, AAVE_V3_POOL],
77+
constraint: { gte: DEPOSIT_AMOUNT },
78+
}),
79+
80+
// ── Main action: deposit into Aave ────────────────────────────────────────
81+
// Only reached if both pre-conditions above pass. The SCA supplies USDC to
82+
// Aave on its own behalf and starts accruing aUSDC yield immediately.
83+
aavePool.write({
84+
functionName: 'supply',
85+
args: [USDC, DEPOSIT_AMOUNT, scaAddress, 0],
86+
}),
87+
]);
88+
89+
// ─── Generate calldata ────────────────────────────────────────────────────────
90+
91+
const calls = await batch.toCalls();
92+
// biome-ignore lint/suspicious/noConsole: intentional in examples
93+
console.log('Composable calls:', JSON.stringify(calls, null, 2));

examples/03-aave-full-deposit.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/**
2+
* Example: ERC20 — atomic approve + Aave deposit in one batch
3+
*
4+
* Scenario: A user wants to move their entire USDC balance into Aave to earn
5+
* yield. Aave requires an ERC-20 approval before it can pull funds, which
6+
* normally means two separate transactions: approve, then supply.
7+
*
8+
* With ERC-8211, both happen atomically in a single batch. If either step
9+
* fails, neither executes — you can never end up in a state where the
10+
* approval was granted but the deposit didn't happen.
11+
*
12+
* The deposit amount is also unknown at construction time — it depends on
13+
* whatever balance the SCA holds at execution time. `runtimeBalance()` resolves
14+
* this: the same live balance value is used for both the approval amount and
15+
* the deposit amount, so they are guaranteed to match exactly.
16+
*
17+
* Flow:
18+
* 1. Pre-condition: assert USDC balance is worth depositing (not dust)
19+
* 2. Approve the Aave pool for exactly the runtime balance
20+
* 3. Supply that same runtime balance to Aave
21+
* 4. Post-condition: assert USDC balance is now (near) zero
22+
*/
23+
24+
import { createComposableBatch } from '@biconomy/smart-batching';
25+
import { createPublicClient, http, parseUnits } from 'viem';
26+
import { base } from 'viem/chains';
27+
28+
// ─── Addresses (Base mainnet) ────────────────────────────────────────────────
29+
30+
const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
31+
const AAVE_V3_POOL = '0xA238Dd80C259a72e81d7e4664a9801593F98d1c5';
32+
33+
// ─── Minimal Aave V3 Pool ABI ────────────────────────────────────────────────
34+
35+
const AAVE_V3_POOL_ABI = [
36+
{
37+
name: 'supply',
38+
type: 'function',
39+
stateMutability: 'nonpayable',
40+
inputs: [
41+
{ name: 'asset', type: 'address' },
42+
{ name: 'amount', type: 'uint256' },
43+
{ name: 'onBehalfOf', type: 'address' },
44+
{ name: 'referralCode', type: 'uint16' },
45+
],
46+
outputs: [],
47+
},
48+
] as const;
49+
50+
// ─── Setup ───────────────────────────────────────────────────────────────────
51+
52+
const publicClient = createPublicClient({ chain: base, transport: http() });
53+
54+
// Your ERC-4337 smart account address
55+
const scaAddress = '0xYourSmartAccountAddress' as `0x${string}`;
56+
57+
// ─── Deposit floor ────────────────────────────────────────────────────────────
58+
59+
const MIN_DEPOSIT = parseUnits('10', 6); // Don't bother depositing less than 10 USDC
60+
61+
// ─── Build the batch ─────────────────────────────────────────────────────────
62+
63+
const batch = createComposableBatch(publicClient, scaAddress);
64+
65+
const usdc = batch.erc20Token(USDC);
66+
const aavePool = batch.contract(AAVE_V3_POOL, AAVE_V3_POOL_ABI);
67+
68+
batch.add([
69+
// ── Step 1: pre-condition — assert balance is worth depositing ─────────────
70+
// `check()` acts as an inline guard. If the SCA holds less than MIN_DEPOSIT
71+
// the whole batch reverts before anything moves.
72+
usdc.check({
73+
functionName: 'balanceOf',
74+
args: [scaAddress],
75+
constraint: { gte: MIN_DEPOSIT },
76+
}),
77+
78+
// ── Step 2: approve Aave for exactly the runtime balance ──────────────────
79+
// The approval amount is resolved on-chain at execution time — not hardcoded.
80+
// This means the approval is tight (no excess allowance left over) and always
81+
// matches the deposit amount in step 3 regardless of when the batch executes.
82+
usdc.write({
83+
functionName: 'approve',
84+
args: [AAVE_V3_POOL, usdc.runtimeBalance()],
85+
}),
86+
87+
// ── Step 3: supply the runtime balance to Aave ────────────────────────────
88+
// Uses the same live balance as step 2. Both resolve via independent
89+
// staticcalls at execution time, but since no balance change happens between
90+
// them in this batch, they are guaranteed to return the same value.
91+
aavePool.write({
92+
functionName: 'supply',
93+
args: [USDC, usdc.runtimeBalance(), scaAddress, 0],
94+
}),
95+
96+
// ── Step 4: post-condition — assert the full balance was deposited ─────────
97+
// After a full-balance deposit the SCA should hold no USDC. If Aave only
98+
// partially accepted the deposit for any reason, this check catches it and
99+
// reverts the entire batch — including the approval and the supply.
100+
usdc.check({
101+
functionName: 'balanceOf',
102+
args: [scaAddress],
103+
constraint: { eq: 0n },
104+
}),
105+
]);
106+
107+
// ─── Generate calldata ────────────────────────────────────────────────────────
108+
109+
const calls = await batch.toCalls();
110+
// biome-ignore lint/suspicious/noConsole: intentional in examples
111+
console.log('Composable calls:', JSON.stringify(calls, null, 2));

0 commit comments

Comments
 (0)