Skip to content

Commit 88ad533

Browse files
authored
Open portfolio from an EVM Wallet signed message (#12344)
closes: https://github.com/Agoric/agoric-private/issues/666 closes: #12320 Refs: agoric-labs/agoric-to-axelar-local#52 ## Description - Updates GMP flows to support `createAndDeposit` message to the new `despositFactory` (see agoric-labs/agoric-to-axelar-local#52) - Implememt `openPortfolioFromEVM`, with a Permit2-backed create+deposit, plus supporting orchestration utilities and tests. - Add handling logic for EVM wallet messages, invoking related EVM facet and flows in the portfolio contract Key areas to review: `packages/portfolio-contract/src/portfolio.flows.ts`, `packages/portfolio-contract/src/pos-gmp.flows.ts`, and the flow/contract tests. ### Security Considerations The flow relies on Permit2 signatures and Axelar GMP payloads; it introduces a new entrypoint that consumes signed permit data and triggers EVM account creation and deposits. Review signature validation boundaries and payload decoding in the flow path. ### Scaling Considerations Unremarkable. ### Documentation Considerations See #12294 No end-user docs expected for this contributor-facing change; the contract test and flow tests serve as executable documentation for the API and design. ### Testing Considerations The several **test.todo**s represent a proposal to postpone those tests indefinitely on the basis that these are not cost-effective at this point. Reviewers will please advise. Primitive bootstrap test of the EVM Wallet message handler. ### Upgrade Considerations - The new factory and deposit factory EVM contract used by the change emit a different `SmartWalletCreated` event. The resolver change included in this PR needs to be deployed in conjunction with the deployment of the corresponding portfolio contract on chain.
2 parents dcd35b8 + aecbcfe commit 88ad533

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+3283
-656
lines changed

multichain-testing/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@
4545
"osmojs": "^16.15.0",
4646
"starshipjs": "^3.3.0",
4747
"ts-blank-space": "^0.4.4",
48-
"typescript": "~5.8.2"
48+
"typescript": "~5.8.2",
49+
"viem": "^2.43.4"
4950
},
5051
"resolutions": {
5152
"node-fetch@npm:^2.7.0": "patch:node-fetch@npm%3A2.7.0#~/.yarn/patches/node-fetch-npm-2.7.0-587d57004e.patch",
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
#!/usr/bin/env -S node --import ts-blank-space/register
2+
/**
3+
* @file Approve USDC for Permit2 contract on any supported EVM chain
4+
*
5+
* Usage:
6+
* node scripts/approve-usdc.ts <chain> <amount>
7+
*
8+
* The script reads PRIVATE_KEY and AGORIC_NET from:
9+
* 1. scripts/.env file (if exists)
10+
* 2. Environment variables (if .env not found or values not set)
11+
*
12+
* Examples:
13+
* node scripts/approve-usdc.ts Arbitrum 100000000
14+
* PRIVATE_KEY=0x... AGORIC_NET=main node scripts/approve-usdc.ts Ethereum 1000000000
15+
*
16+
* Amount is in the smallest unit (6 decimals for USDC):
17+
* 1 USDC = 1_000_000
18+
* 100 USDC = 100_000_000
19+
*/
20+
21+
import { readFileSync } from 'fs';
22+
import { resolve, dirname } from 'path';
23+
import { fileURLToPath } from 'url';
24+
import { ethers } from 'ethers';
25+
26+
// USDC contract addresses
27+
const USDC_ADDRESSES = {
28+
mainnet: {
29+
Arbitrum: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
30+
Avalanche: '0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E',
31+
Base: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
32+
Ethereum: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
33+
Optimism: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85',
34+
},
35+
testnet: {
36+
Arbitrum: '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d',
37+
Avalanche: '0x5425890298aed601595a70AB815c96711a31Bc65',
38+
Base: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
39+
Ethereum: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238',
40+
Optimism: '0x5fd84259d66Cd46123540766Be93DFE6D43130D7',
41+
},
42+
} as const;
43+
44+
/**
45+
* Load environment variables from .env file
46+
* Looks for .env in scripts/ directory
47+
*/
48+
const loadEnv = () => {
49+
try {
50+
const __filename = fileURLToPath(import.meta.url);
51+
const __dirname = dirname(__filename);
52+
const envPath = resolve(__dirname, '.env');
53+
const envContent = readFileSync(envPath, 'utf-8');
54+
55+
for (const line of envContent.split('\n')) {
56+
const trimmed = line.trim();
57+
if (!trimmed || trimmed.startsWith('#')) continue;
58+
59+
const match = trimmed.match(/^([^=]+)=(.*)$/);
60+
if (match) {
61+
const [, key, value] = match;
62+
const trimmedKey = key.trim();
63+
let trimmedValue = value.trim();
64+
65+
// Remove quotes if present
66+
if (
67+
(trimmedValue.startsWith('"') && trimmedValue.endsWith('"')) ||
68+
(trimmedValue.startsWith("'") && trimmedValue.endsWith("'"))
69+
) {
70+
trimmedValue = trimmedValue.slice(1, -1);
71+
}
72+
73+
// Only set if not already in environment
74+
if (!process.env[trimmedKey]) {
75+
process.env[trimmedKey] = trimmedValue;
76+
}
77+
}
78+
}
79+
} catch {
80+
// .env file not found or not readable - that's okay, will use env vars
81+
}
82+
};
83+
84+
// Permit2 contract address (same on all chains)
85+
const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3';
86+
87+
// ERC20 ABI for approve and allowance
88+
const ERC20_ABI = [
89+
'function approve(address spender, uint256 amount) returns (bool)',
90+
'function allowance(address owner, address spender) view returns (uint256)',
91+
'function balanceOf(address owner) view returns (uint256)',
92+
'function decimals() view returns (uint8)',
93+
'function symbol() view returns (string)',
94+
];
95+
96+
/**
97+
* RPC endpoints for each chain
98+
*/
99+
const RPC_URLS = {
100+
mainnet: {
101+
Arbitrum: 'https://arb1.arbitrum.io/rpc',
102+
Avalanche: 'https://api.avax.network/ext/bc/C/rpc',
103+
Base: 'https://mainnet.base.org',
104+
Ethereum: 'https://eth.llamarpc.com',
105+
Optimism: 'https://mainnet.optimism.io',
106+
},
107+
testnet: {
108+
Arbitrum: 'https://arbitrum-sepolia-rpc.publicnode.com',
109+
Avalanche: 'https://api.avax-test.network/ext/bc/C/rpc',
110+
Base: 'https://sepolia.base.org',
111+
Ethereum: 'https://ethereum-sepolia-rpc.publicnode.com',
112+
Optimism: 'https://sepolia.optimism.io',
113+
},
114+
} as const;
115+
116+
type ChainName = 'Arbitrum' | 'Avalanche' | 'Base' | 'Ethereum' | 'Optimism';
117+
118+
/**
119+
* Get network configuration based on environment
120+
*/
121+
const getNetworkConfig = (network: 'main' | 'devnet') => {
122+
switch (network) {
123+
case 'main':
124+
return {
125+
usdcAddresses: USDC_ADDRESSES.mainnet,
126+
rpcUrls: RPC_URLS.mainnet,
127+
label: 'mainnet',
128+
};
129+
case 'devnet':
130+
return {
131+
usdcAddresses: USDC_ADDRESSES.testnet,
132+
rpcUrls: RPC_URLS.testnet,
133+
label: 'testnet',
134+
};
135+
default:
136+
throw new Error(
137+
`Unsupported network: ${network}. Use 'main' or 'devnet'`,
138+
);
139+
}
140+
};
141+
142+
/**
143+
* Format amount with USDC decimals for display
144+
*/
145+
const formatUSDC = (amount: bigint): string => {
146+
const decimals = 6n;
147+
const divisor = 10n ** decimals;
148+
const whole = amount / divisor;
149+
const fraction = amount % divisor;
150+
return `${whole}.${fraction.toString().padStart(6, '0')} USDC`;
151+
};
152+
153+
/**
154+
* Approve USDC for Permit2 contract
155+
*/
156+
const approveUSDC = async ({
157+
chain,
158+
amount,
159+
privateKey,
160+
network,
161+
}: {
162+
chain: ChainName;
163+
amount: bigint;
164+
privateKey: string;
165+
network: 'main' | 'devnet';
166+
}) => {
167+
const config = getNetworkConfig(network);
168+
169+
// Validate chain
170+
if (!(chain in config.usdcAddresses)) {
171+
const supportedChains = Object.keys(config.usdcAddresses).join(', ');
172+
throw new Error(
173+
`Unsupported chain: ${chain}. Supported chains: ${supportedChains}`,
174+
);
175+
}
176+
177+
const rpcUrl = config.rpcUrls[chain];
178+
const usdcAddress = config.usdcAddresses[chain];
179+
180+
console.log(`\n🔗 Chain: ${chain} (${config.label})`);
181+
console.log(`📍 RPC: ${rpcUrl}`);
182+
console.log(`💵 USDC: ${usdcAddress}`);
183+
console.log(`🔐 Permit2: ${PERMIT2_ADDRESS}`);
184+
185+
// Connect to the chain
186+
const provider = new ethers.JsonRpcProvider(rpcUrl);
187+
const signer = new ethers.Wallet(privateKey, provider);
188+
const signerAddress = await signer.getAddress();
189+
190+
console.log(`👛 Wallet: ${signerAddress}`);
191+
192+
// Get USDC contract
193+
const usdc = new ethers.Contract(usdcAddress, ERC20_ABI, signer);
194+
195+
// Check current balance
196+
const balance = await usdc.balanceOf(signerAddress);
197+
console.log(`💰 USDC Balance: ${formatUSDC(balance)}`);
198+
199+
if (balance < amount) {
200+
console.warn(
201+
`⚠️ Warning: Requested approval (${formatUSDC(amount)}) exceeds balance (${formatUSDC(balance)})`,
202+
);
203+
}
204+
205+
// Check current allowance
206+
const currentAllowance = await usdc.allowance(signerAddress, PERMIT2_ADDRESS);
207+
console.log(`📊 Current Allowance: ${formatUSDC(currentAllowance)}`);
208+
209+
if (currentAllowance >= amount) {
210+
console.log(
211+
`✅ Allowance already sufficient (${formatUSDC(currentAllowance)} >= ${formatUSDC(amount)})`,
212+
);
213+
console.log('No approval needed.');
214+
return;
215+
}
216+
217+
// Approve USDC for Permit2
218+
console.log(`\n📝 Approving ${formatUSDC(amount)} for Permit2...`);
219+
const tx = await usdc.approve(PERMIT2_ADDRESS, amount);
220+
console.log(`📤 Transaction hash: ${tx.hash}`);
221+
console.log('⏳ Waiting for confirmation...');
222+
223+
const receipt = await tx.wait();
224+
console.log(`✅ Transaction confirmed in block ${receipt?.blockNumber}`);
225+
226+
// Verify new allowance
227+
const newAllowance = await usdc.allowance(signerAddress, PERMIT2_ADDRESS);
228+
console.log(`\n✨ New Allowance: ${formatUSDC(newAllowance)}`);
229+
console.log('🎉 Approval successful!');
230+
};
231+
232+
/**
233+
* Main entry point
234+
*/
235+
const main = async ({ argv = process.argv, env = process.env } = {}) => {
236+
// Load .env file first
237+
loadEnv();
238+
239+
// Parse arguments
240+
const [chain, amountStr] = argv.slice(2);
241+
242+
if (!chain || !amountStr) {
243+
console.error(`Usage: ${argv[1]} <chain> <amount>`);
244+
console.error(
245+
'\nSupported chains: Arbitrum, Avalanche, Base, Ethereum, Optimism',
246+
);
247+
console.error('Amount is in smallest unit (6 decimals for USDC)');
248+
console.error(' 1 USDC = 1000000');
249+
console.error(' 100 USDC = 100000000');
250+
console.error('\nConfiguration:');
251+
console.error(
252+
' Reads from scripts/.env file (if exists) or environment variables',
253+
);
254+
console.error(' PRIVATE_KEY - Your wallet private key (required)');
255+
console.error(
256+
' AGORIC_NET - Network to use: "main" or "devnet" (default: devnet)',
257+
);
258+
console.error('\nExample:');
259+
console.error(' node scripts/approve-usdc.ts Arbitrum 100000000');
260+
console.error(
261+
' PRIVATE_KEY=0x... AGORIC_NET=main node scripts/approve-usdc.ts Ethereum 1000000000',
262+
);
263+
process.exit(1);
264+
}
265+
266+
// Get environment variables
267+
const privateKey = env.PRIVATE_KEY;
268+
if (!privateKey) {
269+
throw new Error('PRIVATE_KEY environment variable is required');
270+
}
271+
272+
const network = (env.AGORIC_NET as 'main' | 'devnet') || 'devnet';
273+
if (network !== 'main' && network !== 'devnet') {
274+
throw new Error('AGORIC_NET must be either "main" or "devnet"');
275+
}
276+
277+
// Parse amount
278+
const amount = BigInt(amountStr);
279+
if (amount <= 0n) {
280+
throw new Error('Amount must be positive');
281+
}
282+
283+
// Approve USDC
284+
await approveUSDC({
285+
chain: chain as ChainName,
286+
amount,
287+
privateKey,
288+
network,
289+
});
290+
};
291+
292+
main().catch(err => {
293+
console.error('\n❌ Error:', err.message);
294+
process.exit(1);
295+
});

0 commit comments

Comments
 (0)