Skip to content

Commit eb78887

Browse files
authored
fix logging rate limits and add new pool scripts (#38)
1 parent 15eb049 commit eb78887

File tree

5 files changed

+726
-8
lines changed

5 files changed

+726
-8
lines changed

ccip-lib/svm/README.md

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,8 +390,80 @@ await pool.setRateLimit(
390390
new BN(100000000) // Rate: 0.1 tokens per second
391391
);
392392

393-
// Transfer admin role for a token pool
394-
await pool.transferAdminRole(tokenMint, newAdminPublicKey);
393+
// Transfer admin role for a token pool (two-step process)
394+
// Step 1: Current owner proposes new admin
395+
await pool.transferAdminRole(tokenMint, { newAdmin: newAdminPublicKey });
396+
397+
// Step 2: Proposed admin accepts the role (must be signed by newAdminPublicKey)
398+
await pool.acceptAdminRole(tokenMint);
399+
```
400+
401+
### Pool Ownership Management
402+
403+
Token pools use a two-step ownership transfer process for security. This ensures that ownership can only be transferred to a valid recipient who explicitly accepts the role.
404+
405+
#### Transfer Pool Ownership
406+
407+
```typescript
408+
import {
409+
TokenPoolManager,
410+
TokenPoolType,
411+
TransferAdminRoleOptions,
412+
AcceptAdminRoleOptions
413+
} from "../path/to/ccip-lib/svm";
414+
415+
// Create token pool manager
416+
const poolManager = TokenPoolManager.create(
417+
connection,
418+
currentOwnerKeypair,
419+
{ burnMint: burnMintPoolProgramId },
420+
config
421+
);
422+
423+
// Get the pool client
424+
const pool = poolManager.getTokenPoolClient(TokenPoolType.BURN_MINT);
425+
426+
// Step 1: Current owner proposes new administrator
427+
await pool.transferAdminRole(tokenMint, {
428+
newAdmin: newOwnerPublicKey,
429+
skipPreflight: false, // Optional transaction settings
430+
});
431+
```
432+
433+
#### Accept Pool Ownership
434+
435+
```typescript
436+
// The proposed new owner must sign this transaction
437+
const newOwnerPoolManager = TokenPoolManager.create(
438+
connection,
439+
newOwnerKeypair, // Must be the proposed owner
440+
{ burnMint: burnMintPoolProgramId },
441+
config
442+
);
443+
444+
const newOwnerPool = newOwnerPoolManager.getTokenPoolClient(TokenPoolType.BURN_MINT);
445+
446+
// Step 2: Proposed owner accepts the admin role
447+
await newOwnerPool.acceptAdminRole(tokenMint, {
448+
skipPreflight: false, // Optional transaction settings
449+
});
450+
```
451+
452+
#### Pool Ownership Best Practices
453+
454+
- **Two-Step Process**: Always use the two-step transfer process - never skip the acceptance step
455+
- **Verify Recipients**: Ensure the new owner address is correct before proposing transfer
456+
- **Test First**: Test the ownership transfer process on devnet before mainnet
457+
- **Secure Keys**: The new owner must have secure access to their keypair to accept ownership
458+
- **Monitor State**: Check pool configuration to verify ownership transfer completed successfully
459+
460+
#### Checking Current Pool Owner
461+
462+
```typescript
463+
// Get pool information to check current owner
464+
const poolInfo = await pool.getPoolInfo(tokenMint);
465+
console.log(`Current owner: ${poolInfo.config.config.owner.toString()}`);
466+
console.log(`Proposed owner: ${poolInfo.config.config.proposedOwner?.toString() || 'none'}`);
395467
```
396468

397469
### Token Pool Factory
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
/**
2+
* Pool Accept Ownership Script (CLI Framework Version)
3+
*
4+
* This script accepts the ownership of a token pool by the proposed owner.
5+
* This is step 2 of a two-step ownership transfer process.
6+
*/
7+
8+
import { PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";
9+
import { TokenPoolManager } from "../../../ccip-lib/svm/core/client/tokenpools";
10+
import { TokenPoolType, LogLevel, createLogger } from "../../../ccip-lib/svm";
11+
import { BurnMintTokenPoolInfo } from "../../../ccip-lib/svm/tokenpools/burnmint/accounts";
12+
import { resolveNetworkConfig, getExplorerUrl } from "../../config";
13+
import { getKeypairPath, loadKeypair } from "../utils";
14+
import { CCIPCommand, ArgumentDefinition, CommandMetadata, BaseCommandOptions } from "../utils/cli-framework";
15+
16+
/**
17+
* Configuration for accept ownership operations
18+
*/
19+
const ACCEPT_OWNERSHIP_CONFIG = {
20+
minSolRequired: 0.01,
21+
defaultLogLevel: LogLevel.INFO,
22+
};
23+
24+
/**
25+
* Options specific to the accept-ownership command
26+
*/
27+
interface AcceptOwnershipOptions extends BaseCommandOptions {
28+
tokenMint: string;
29+
burnMintPoolProgram: string;
30+
}
31+
32+
/**
33+
* Pool Accept Ownership Command
34+
*/
35+
class AcceptOwnershipCommand extends CCIPCommand<AcceptOwnershipOptions> {
36+
constructor() {
37+
const metadata: CommandMetadata = {
38+
name: "accept-ownership",
39+
description: "✅ Pool Ownership Acceptance\n\nAccepts the ownership of a token pool by the proposed owner. This is step 2 of a two-step ownership transfer process for security.",
40+
examples: [
41+
"# Accept pool ownership (you must be the proposed owner)",
42+
"yarn svm:pool:accept-ownership \\",
43+
" --token-mint 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU \\",
44+
" --burn-mint-pool-program 2YzPLhHBpRMwxCN7yLpHJGHg2AXBzQ5VPuKt51BDKxqh",
45+
"",
46+
"# With debug logging",
47+
"yarn svm:pool:accept-ownership \\",
48+
" --token-mint 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU \\",
49+
" --burn-mint-pool-program 2YzPLhHBpRMwxCN7yLpHJGHg2AXBzQ5VPuKt51BDKxqh \\",
50+
" --log-level DEBUG"
51+
],
52+
notes: [
53+
"✅ This completes the 2-step ownership transfer process",
54+
"Only callable by the proposed owner (set via transfer-ownership)",
55+
`Minimum ${ACCEPT_OWNERSHIP_CONFIG.minSolRequired} SOL required for transaction fees`,
56+
"You must be the proposed owner to execute this command",
57+
"Once accepted, you become the pool owner with full administrative rights",
58+
"Use 'yarn svm:pool:get-info' to verify ownership after acceptance",
59+
"Ensure you have secure access to this keypair before accepting"
60+
]
61+
};
62+
63+
super(metadata);
64+
}
65+
66+
protected defineArguments(): ArgumentDefinition[] {
67+
return [
68+
{
69+
name: "token-mint",
70+
required: true,
71+
type: "string",
72+
description: "Token mint address identifying the pool",
73+
example: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"
74+
},
75+
{
76+
name: "burn-mint-pool-program",
77+
required: true,
78+
type: "string",
79+
description: "Burn-mint token pool program ID",
80+
example: "2YzPLhHBpRMwxCN7yLpHJGHg2AXBzQ5VPuKt51BDKxqh"
81+
}
82+
];
83+
}
84+
85+
protected async execute(): Promise<void> {
86+
this.logger.info("✅ CCIP Token Pool Accept Ownership");
87+
this.logger.info("==========================================");
88+
this.logger.warn("⚠️ Step 2 of 2-step ownership transfer process");
89+
90+
// Resolve network configuration
91+
const config = resolveNetworkConfig(this.options);
92+
93+
// Load wallet (must be proposed owner)
94+
const keypairPath = getKeypairPath(this.options);
95+
const walletKeypair = loadKeypair(keypairPath);
96+
97+
this.logger.info(`Network: ${config.id}`);
98+
this.logger.info(`Proposed Owner (Wallet): ${walletKeypair.publicKey.toString()}`);
99+
100+
// Check SOL balance
101+
this.logger.info("");
102+
this.logger.info("💰 WALLET BALANCE");
103+
this.logger.info("==========================================");
104+
const balance = await config.connection.getBalance(walletKeypair.publicKey);
105+
const solBalance = balance / LAMPORTS_PER_SOL;
106+
this.logger.info(`SOL Balance: ${balance} lamports (${solBalance.toFixed(9)} SOL)`);
107+
108+
if (solBalance < ACCEPT_OWNERSHIP_CONFIG.minSolRequired) {
109+
throw new Error(
110+
`Insufficient balance. Need at least ${ACCEPT_OWNERSHIP_CONFIG.minSolRequired} SOL for transaction fees.\n` +
111+
`Current balance: ${solBalance.toFixed(9)} SOL\n\n` +
112+
`Request airdrop with:\n` +
113+
`solana airdrop 1 ${walletKeypair.publicKey.toString()} --url devnet`
114+
);
115+
}
116+
117+
// Parse and validate addresses
118+
let tokenMint: PublicKey;
119+
let burnMintPoolProgramId: PublicKey;
120+
121+
try {
122+
tokenMint = new PublicKey(this.options.tokenMint);
123+
} catch {
124+
throw new Error(`Invalid token mint address: ${this.options.tokenMint}`);
125+
}
126+
127+
try {
128+
burnMintPoolProgramId = new PublicKey(this.options.burnMintPoolProgram);
129+
} catch {
130+
throw new Error(`Invalid burn-mint pool program ID: ${this.options.burnMintPoolProgram}`);
131+
}
132+
133+
// Display configuration
134+
this.logger.info("");
135+
this.logger.info("📋 OWNERSHIP ACCEPTANCE CONFIGURATION");
136+
this.logger.info("==========================================");
137+
this.logger.info(`Token Mint: ${tokenMint.toString()}`);
138+
this.logger.info(`Pool Program: ${burnMintPoolProgramId.toString()}`);
139+
this.logger.info(`Proposed Owner (You): ${walletKeypair.publicKey.toString()}`);
140+
141+
this.logger.debug("Configuration details:");
142+
this.logger.debug(` Network: ${config.id}`);
143+
this.logger.debug(` Connection endpoint: ${config.connection.rpcEndpoint}`);
144+
this.logger.debug(` Commitment level: ${config.connection.commitment}`);
145+
this.logger.debug(` Skip preflight: ${this.options.skipPreflight}`);
146+
147+
try {
148+
// Create token pool manager using SDK
149+
const tokenPoolManager = TokenPoolManager.create(
150+
config.connection,
151+
walletKeypair,
152+
{
153+
burnMint: burnMintPoolProgramId,
154+
},
155+
{
156+
ccipRouterProgramId: config.routerProgramId.toString(),
157+
feeQuoterProgramId: config.feeQuoterProgramId.toString(),
158+
rmnRemoteProgramId: config.rmnRemoteProgramId.toString(),
159+
linkTokenMint: config.linkTokenMint.toString(),
160+
receiverProgramId: config.receiverProgramId.toString(),
161+
},
162+
{ logLevel: this.options.logLevel ?? LogLevel.INFO }
163+
);
164+
165+
const tokenPoolClient = tokenPoolManager.getTokenPoolClient(TokenPoolType.BURN_MINT);
166+
167+
// Check if pool exists and get current pool info for verification
168+
this.logger.info("");
169+
this.logger.info("🔍 VERIFYING POOL AND PENDING OWNERSHIP");
170+
this.logger.info("==========================================");
171+
this.logger.info("Checking pool exists and verifying pending ownership...");
172+
173+
let poolInfo: BurnMintTokenPoolInfo;
174+
try {
175+
poolInfo = await tokenPoolClient.getPoolInfo(tokenMint) as BurnMintTokenPoolInfo;
176+
this.logger.info("✅ Pool exists");
177+
this.logger.info(`Current Pool Owner: ${poolInfo.config.config.owner.toString()}`);
178+
this.logger.info(`Current Proposed Owner: ${poolInfo.config.config.proposedOwner?.toString() || 'none'}`);
179+
180+
this.logger.debug("Current pool details:", {
181+
poolType: poolInfo.poolType,
182+
owner: poolInfo.config.config.owner.toString(),
183+
proposedOwner: poolInfo.config.config.proposedOwner?.toString() || 'none',
184+
version: poolInfo.config.version,
185+
decimals: poolInfo.config.config.decimals,
186+
router: poolInfo.config.config.router.toString(),
187+
});
188+
} catch (error) {
189+
this.logger.error("");
190+
this.logger.error("❌ POOL NOT FOUND");
191+
this.logger.error("==========================================");
192+
this.logger.error("Pool does not exist for this token mint");
193+
this.logger.error("Initialize the pool first using 'yarn svm:pool:initialize'");
194+
this.logger.debug(
195+
`To initialize: yarn svm:pool:initialize --token-mint ${tokenMint.toString()} --burn-mint-pool-program ${burnMintPoolProgramId.toString()}`
196+
);
197+
throw new Error("Pool does not exist for this token mint");
198+
}
199+
200+
// Check if there's a pending ownership transfer to this wallet
201+
if (!poolInfo.config.config.proposedOwner || poolInfo.config.config.proposedOwner.equals(PublicKey.default)) {
202+
throw new Error(
203+
`No pending ownership transfer found.\n` +
204+
`Current Owner: ${poolInfo.config.config.owner.toString()}\n` +
205+
`Proposed Owner: none\n\n` +
206+
`The current owner must first propose you as the new owner using 'yarn svm:pool:transfer-ownership'.`
207+
);
208+
}
209+
210+
// Verify current wallet is the proposed owner
211+
if (!poolInfo.config.config.proposedOwner.equals(walletKeypair.publicKey)) {
212+
throw new Error(
213+
`Access denied: You are not the proposed owner of this pool.\n` +
214+
`Proposed Owner: ${poolInfo.config.config.proposedOwner.toString()}\n` +
215+
`Your Wallet: ${walletKeypair.publicKey.toString()}\n\n` +
216+
`Only the proposed owner can accept ownership.`
217+
);
218+
}
219+
220+
// Check if already the current owner (edge case)
221+
if (poolInfo.config.config.owner.equals(walletKeypair.publicKey)) {
222+
this.logger.info("");
223+
this.logger.info("ℹ️ ALREADY THE OWNER");
224+
this.logger.info("==========================================");
225+
this.logger.info("You are already the current owner of this pool.");
226+
this.logger.info("No action needed - ownership transfer not required.");
227+
return;
228+
}
229+
230+
this.logger.info("✅ Verified: You are the proposed owner");
231+
232+
// Accept ownership
233+
this.logger.info("");
234+
this.logger.info("🔧 ACCEPTING OWNERSHIP");
235+
this.logger.info("==========================================");
236+
this.logger.warn("⚠️ FINALIZING OWNERSHIP TRANSFER");
237+
this.logger.info("Executing ownership acceptance...");
238+
239+
const signature = await tokenPoolClient.acceptAdminRole(tokenMint, {
240+
skipPreflight: this.options.skipPreflight,
241+
});
242+
243+
// Display results
244+
this.logger.info("");
245+
this.logger.info("✅ OWNERSHIP ACCEPTED SUCCESSFULLY");
246+
this.logger.info("==========================================");
247+
this.logger.info(`Transaction Signature: ${signature}`);
248+
249+
// Display explorer URL
250+
this.logger.info("");
251+
this.logger.info("🔍 EXPLORER URLS");
252+
this.logger.info("==========================================");
253+
this.logger.info(`Transaction: ${getExplorerUrl(config.id, signature)}`);
254+
255+
// Display summary
256+
this.logger.info("");
257+
this.logger.info("👤 OWNERSHIP TRANSFER COMPLETE");
258+
this.logger.info("==========================================");
259+
this.logger.info(`Token Mint: ${tokenMint.toString()}`);
260+
this.logger.info(`Previous Owner: ${poolInfo.config.config.owner.toString()}`);
261+
this.logger.info(`New Owner (You): ${walletKeypair.publicKey.toString()}`);
262+
this.logger.info(`Pool Program: ${burnMintPoolProgramId.toString()}`);
263+
this.logger.info(`Transaction: ${signature}`);
264+
265+
this.logger.info("");
266+
this.logger.info("📋 NEXT STEPS");
267+
this.logger.info("==========================================");
268+
this.logger.info("1. Verify the ownership transfer completed:");
269+
this.logger.info(` yarn svm:pool:get-info --token-mint ${tokenMint.toString()}`);
270+
this.logger.info("");
271+
this.logger.info("2. You can now manage the pool as the owner:");
272+
this.logger.info(" • Configure remote chains for cross-chain transfers");
273+
this.logger.info(" • Set rate limits for security");
274+
this.logger.info(" • Transfer ownership to others if needed");
275+
this.logger.info("");
276+
this.logger.info("3. Keep your keypair secure - you are now the pool administrator");
277+
278+
this.logger.info("");
279+
this.logger.info("🎉 Ownership Transfer Complete!");
280+
this.logger.info("✅ You are now the owner of this token pool");
281+
this.logger.info("🔧 You have full administrative rights over the pool");
282+
283+
} catch (error) {
284+
this.logger.error(
285+
`❌ Failed to accept ownership: ${error instanceof Error ? error.message : String(error)}`
286+
);
287+
288+
if (error instanceof Error && error.stack) {
289+
this.logger.debug("\nError stack:");
290+
this.logger.debug(error.stack);
291+
}
292+
293+
throw error;
294+
}
295+
}
296+
}
297+
298+
// Create and run the command
299+
const command = new AcceptOwnershipCommand();
300+
command.run().catch((error) => {
301+
process.exit(1);
302+
});

0 commit comments

Comments
 (0)