solana: couple more things

* remove burn_source_authority

* fix Cargo.toml

* clean up tests
This commit is contained in:
A5 Pickle 2024-01-23 08:26:27 -06:00
parent 6c6a41c03d
commit dc6b868268
No known key found for this signature in database
GPG Key ID: DD6C727938DE8E65
11 changed files with 203 additions and 242 deletions

View File

@ -30,6 +30,10 @@ startup_wait = 16000
[test.validator]
url = "https://api.devnet.solana.com"
### At 160 ticks/s, 64 ticks per slot implies that leader rotation and voting will happen
### every 400 ms. A fast voting cadence ensures faster finality and convergence
ticks_per_slot = 8
### Forked Wormhole Circle Integration Program
[[test.validator.clone]]
address = "wCCTPvsyeL9qYqbHTv3DUAyzEfYcyHoYw5c4mgcbBeW"

1
solana/Cargo.lock generated
View File

@ -2395,6 +2395,7 @@ dependencies = [
name = "wormhole-circle-integration-solana"
version = "0.0.1-alpha.7"
dependencies = [
"ahash 0.8.6",
"anchor-lang",
"anchor-spl",
"cfg-if",

View File

@ -39,6 +39,10 @@ ruint = "1.9.0"
cfg-if = "1.0"
hex-literal = "0.4.1"
### https://github.com/coral-xyz/anchor/issues/2755
### This dependency must be added for each program.
ahash = "=0.8.6"
[profile.release]
overflow-checks = true
lto = "fat"

View File

@ -34,5 +34,7 @@ ruint.workspace = true
cfg-if.workspace = true
ahash.workspace = true
[dev-dependencies]
hex-literal.workspace = true

View File

@ -22,10 +22,6 @@ pub struct TransferTokensWithPayload<'info> {
)]
custodian: Account<'info, Custodian>,
/// Signer who must have the authority (either as the owner or has been delegated authority)
/// over the `burn_source` token account.
burn_source_authority: Signer<'info>,
/// Circle-supported mint.
///
/// CHECK: Mutable. This token account's mint must be the same as the one found in the CCTP
@ -38,6 +34,9 @@ pub struct TransferTokensWithPayload<'info> {
/// Token account where assets are burned from. The CCTP Token Messenger Minter program will
/// burn the configured [amount](TransferTokensWithPayloadArgs::amount) from this account.
///
/// NOTE: Transfer authority must be delegated to the custodian because this instruction
/// transfers assets from this account to the custody token account.
#[account(
mut,
token::mint = mint
@ -160,22 +159,23 @@ pub fn transfer_tokens_with_payload(
payload,
} = args;
let custodian_seeds = &[Custodian::SEED_PREFIX, &[ctx.accounts.custodian.bump]];
// Because the transfer initiator in the Circle message is whoever signs to burn assets, we need
// to transfer assets from the source token account to one that belongs to this program.
token::transfer(
CpiContext::new(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
token::Transfer {
from: ctx.accounts.burn_source.to_account_info(),
to: ctx.accounts.custody_token.to_account_info(),
authority: ctx.accounts.burn_source_authority.to_account_info(),
authority: ctx.accounts.custodian.to_account_info(),
},
&[custodian_seeds],
),
amount,
)?;
let custodian_seeds = &[Custodian::SEED_PREFIX, &[ctx.accounts.custodian.bump]];
wormhole_cctp_solana::cpi::burn_and_publish(
CpiContext::new_with_signer(
ctx.accounts

View File

@ -9,7 +9,7 @@ import { NodeWallet, postVaaSolana } from "@certusone/wormhole-sdk/lib/cjs/solan
import { derivePostedVaaKey } from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole";
import { Connection, Keypair, Transaction, sendAndConfirmTransaction } from "@solana/web3.js";
import "dotenv/config";
import { WormholeCctpProgram } from "../src";
import { CircleIntegrationProgram } from "../src";
const PROGRAM_ID = "wCCTPvsyeL9qYqbHTv3DUAyzEfYcyHoYw5c4mgcbBeW";
@ -22,7 +22,7 @@ async function main() {
let govSequence = 6900n;
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const wormholeCctp = new WormholeCctpProgram(connection, PROGRAM_ID);
const circleIntegration = new CircleIntegrationProgram(connection, PROGRAM_ID);
if (process.env.SOLANA_PRIVATE_KEY === undefined) {
throw new Error("SOLANA_PRIVATE_KEY is undefined");
@ -39,7 +39,7 @@ async function main() {
const cctpDomain = 0;
await registerEmitterAndDomain(
wormholeCctp,
circleIntegration,
payer,
govSequence++,
foreignChain,
@ -53,7 +53,7 @@ async function main() {
const cctpDomain = 1;
await registerEmitterAndDomain(
wormholeCctp,
circleIntegration,
payer,
govSequence++,
foreignChain,
@ -67,7 +67,7 @@ async function main() {
const cctpDomain = 2;
await registerEmitterAndDomain(
wormholeCctp,
circleIntegration,
payer,
govSequence++,
foreignChain,
@ -81,7 +81,7 @@ async function main() {
const cctpDomain = 3;
await registerEmitterAndDomain(
wormholeCctp,
circleIntegration,
payer,
govSequence++,
foreignChain,
@ -91,32 +91,34 @@ async function main() {
}
}
async function intialize(wormholeCctp: WormholeCctpProgram, payer: Keypair) {
console.log("custodian", wormholeCctp.custodianAddress().toString());
async function intialize(circleIntegration: CircleIntegrationProgram, payer: Keypair) {
console.log("custodian", circleIntegration.custodianAddress().toString());
const ix = await wormholeCctp.initializeIx(payer.publicKey);
const ix = await circleIntegration.initializeIx(payer.publicKey);
const connection = wormholeCctp.program.provider.connection;
const connection = circleIntegration.program.provider.connection;
const txSig = await sendAndConfirmTransaction(connection, new Transaction().add(ix), [payer]);
console.log("intialize", txSig);
}
async function registerEmitterAndDomain(
wormholeCctp: WormholeCctpProgram,
circleIntegration: CircleIntegrationProgram,
payer: Keypair,
govSequence: bigint,
foreignChain: ChainName,
foreignEmitter: string,
cctpDomain: number,
) {
const connection = wormholeCctp.program.provider.connection;
const connection = circleIntegration.program.provider.connection;
const registeredEmitter = wormholeCctp.registeredEmitterAddress(coalesceChainId(foreignChain));
const registeredEmitter = circleIntegration.registeredEmitterAddress(
coalesceChainId(foreignChain),
);
const emitterAddress = Array.from(tryNativeToUint8Array(foreignEmitter, foreignChain));
const exists = await connection.getAccountInfo(registeredEmitter).then((acct) => acct != null);
if (exists) {
const registered = await wormholeCctp.fetchRegisteredEmitter(registeredEmitter);
const registered = await circleIntegration.fetchRegisteredEmitter(registeredEmitter);
if (Buffer.from(registered.address).equals(Buffer.from(emitterAddress))) {
console.log("already registered", foreignChain, foreignEmitter, cctpDomain);
return;
@ -157,14 +159,14 @@ async function registerEmitterAndDomain(
await postVaaSolana(
connection,
new NodeWallet(payer).signTransaction,
wormholeCctp.coreBridgeProgramId(),
circleIntegration.coreBridgeProgramId(),
payer.publicKey,
vaaBuf,
);
const vaa = derivePostedVaaKey(wormholeCctp.coreBridgeProgramId(), parseVaa(vaaBuf).hash);
const vaa = derivePostedVaaKey(circleIntegration.coreBridgeProgramId(), parseVaa(vaaBuf).hash);
const ix = await wormholeCctp.registerEmitterAndDomainIx({
const ix = await circleIntegration.registerEmitterAndDomainIx({
payer: payer.publicKey,
vaa,
});

View File

@ -4,7 +4,7 @@ import { NodeWallet, postVaaSolana } from "@certusone/wormhole-sdk/lib/cjs/solan
import { derivePostedVaaKey } from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole";
import { Connection, Keypair, Transaction, sendAndConfirmTransaction } from "@solana/web3.js";
import "dotenv/config";
import { WormholeCctpProgram } from "../src";
import { CircleIntegrationProgram } from "../src";
const PROGRAM_ID = "wCCTPvsyeL9qYqbHTv3DUAyzEfYcyHoYw5c4mgcbBeW";
@ -20,22 +20,22 @@ async function main() {
let govSequence = 6910n;
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const wormholeCctp = new WormholeCctpProgram(connection, PROGRAM_ID);
const circleIntegration = new CircleIntegrationProgram(connection, PROGRAM_ID);
if (process.env.SOLANA_PRIVATE_KEY === undefined) {
throw new Error("SOLANA_PRIVATE_KEY is undefined");
}
const payer = Keypair.fromSecretKey(Buffer.from(process.env.SOLANA_PRIVATE_KEY, "hex"));
await upgradeContract(wormholeCctp, payer, govSequence);
await upgradeContract(circleIntegration, payer, govSequence);
}
async function upgradeContract(
wormholeCctp: WormholeCctpProgram,
circleIntegration: CircleIntegrationProgram,
payer: Keypair,
govSequence: bigint,
) {
const connection = wormholeCctp.program.provider.connection;
const connection = circleIntegration.program.provider.connection;
const govEmitter = new MockEmitter(
"0000000000000000000000000000000000000000000000000000000000000004",
@ -69,14 +69,14 @@ async function upgradeContract(
await postVaaSolana(
connection,
new NodeWallet(payer).signTransaction,
wormholeCctp.coreBridgeProgramId(),
circleIntegration.coreBridgeProgramId(),
payer.publicKey,
vaaBuf,
);
const vaa = derivePostedVaaKey(wormholeCctp.coreBridgeProgramId(), parseVaa(vaaBuf).hash);
const vaa = derivePostedVaaKey(circleIntegration.coreBridgeProgramId(), parseVaa(vaaBuf).hash);
const ix = await wormholeCctp.upgradeContractIx({
const ix = await circleIntegration.upgradeContractIx({
payer: payer.publicKey,
vaa,
});

View File

@ -115,7 +115,7 @@ export type SolanaWormholeCctpTxData = {
encodedCctpMessage: Buffer;
};
export class WormholeCctpProgram {
export class CircleIntegrationProgram {
private _programId: ProgramId;
program: Program<WormholeCircleIntegrationSolana>;
@ -346,23 +346,10 @@ export class WormholeCctpProgram {
mint: PublicKey;
burnSource: PublicKey;
coreMessage: PublicKey;
burnSourceAuthority?: PublicKey;
},
args: TransferTokensWithPayloadArgs,
): Promise<TransactionInstruction> {
let {
payer,
burnSource,
mint,
coreMessage,
burnSourceAuthority: inputBurnSender,
} = accounts;
const burnSourceAuthority =
inputBurnSender ??
(await splToken
.getAccount(this.program.provider.connection, burnSource)
.then((token) => token.owner));
let { payer, burnSource, mint, coreMessage } = accounts;
const { amount, targetChain, mintRecipient, wormholeMessageNonce, payload } = args;
@ -395,7 +382,6 @@ export class WormholeCctpProgram {
.accounts({
payer,
custodian,
burnSourceAuthority,
mint,
burnSource,
custodyToken,

View File

@ -4,7 +4,14 @@ import { getPostedMessage } from "@certusone/wormhole-sdk/lib/cjs/solana/wormhol
import * as anchor from "@coral-xyz/anchor";
import * as splToken from "@solana/spl-token";
import { expect } from "chai";
import { CctpTokenBurnMessage, Deposit, DepositHeader, WormholeCctpProgram } from "../src";
import {
CctpTokenBurnMessage,
Deposit,
DepositHeader,
CircleIntegrationProgram,
VaaAccount,
Claim,
} from "../src";
import {
CircleAttester,
ETHEREUM_USDC_ADDRESS,
@ -25,7 +32,7 @@ describe("Circle Integration -- Localnet", () => {
const connection = new anchor.web3.Connection("http://localhost:8899", "processed");
const payer = anchor.web3.Keypair.fromSecretKey(PAYER_PRIVATE_KEY);
const wormholeCctp = new WormholeCctpProgram(
const circleIntegration = new CircleIntegrationProgram(
connection,
"Wormho1eCirc1e1ntegration111111111111111111",
);
@ -34,7 +41,7 @@ describe("Circle Integration -- Localnet", () => {
describe("Setup", () => {
it("Invoke `initialize`", async () => {
const ix = await wormholeCctp.initializeIx(payer.publicKey);
const ix = await circleIntegration.initializeIx(payer.publicKey);
await expectIxOk(connection, [ix], [payer]);
});
@ -49,7 +56,7 @@ describe("Circle Integration -- Localnet", () => {
);
await expectIxOk(connection, [createIx], [payer]);
const usdcCommonAccounts = wormholeCctp.commonAccounts(USDC_MINT_ADDRESS);
const usdcCommonAccounts = circleIntegration.commonAccounts(USDC_MINT_ADDRESS);
// Extend.
const extendIx = anchor.web3.AddressLookupTableProgram.extendLookupTable({
@ -85,12 +92,12 @@ describe("Circle Integration -- Localnet", () => {
},
});
const ix = await wormholeCctp.registerEmitterAndDomainIx({
const ix = await circleIntegration.registerEmitterAndDomainIx({
payer: payer.publicKey,
vaa,
});
await expectIxErr(connection, [ix], [payer], "GovernanceForAnotherChain");
await expectIxErr(connection, [ix], [payer], "Error Code: GovernanceForAnotherChain");
});
it("Cannot Invoke `register_emitter_and_domain` with Invalid Governance", async () => {
@ -101,7 +108,7 @@ describe("Circle Integration -- Localnet", () => {
},
});
const ix = await wormholeCctp.registerEmitterAndDomainIx({
const ix = await circleIntegration.registerEmitterAndDomainIx({
payer: payer.publicKey,
vaa,
remoteTokenMessenger: new anchor.web3.PublicKey(
@ -109,7 +116,7 @@ describe("Circle Integration -- Localnet", () => {
),
});
await expectIxErr(connection, [ix], [payer], "InvalidGovernanceAction");
await expectIxErr(connection, [ix], [payer], "Error Code: InvalidGovernanceAction");
});
it("Cannot Invoke `register_emitter_and_domain` with Invalid CCTP Domain", async () => {
@ -127,12 +134,12 @@ describe("Circle Integration -- Localnet", () => {
},
});
const ix = await wormholeCctp.registerEmitterAndDomainIx({
const ix = await circleIntegration.registerEmitterAndDomainIx({
payer: payer.publicKey,
vaa,
});
await expectIxErr(connection, [ix], [payer], "InvalidCctpDomain");
await expectIxErr(connection, [ix], [payer], "Error Code: InvalidCctpDomain");
});
it("Invoke `register_emitter_and_domain`", async () => {
@ -151,12 +158,12 @@ describe("Circle Integration -- Localnet", () => {
},
});
const ix = await wormholeCctp.registerEmitterAndDomainIx({
const ix = await circleIntegration.registerEmitterAndDomainIx({
payer: payer.publicKey,
vaa,
});
const registeredEmitter = wormholeCctp.registeredEmitterAddress(foreignChain);
const registeredEmitter = circleIntegration.registeredEmitterAddress(foreignChain);
// Verify that account does not exist before invoking ix.
{
@ -168,7 +175,7 @@ describe("Circle Integration -- Localnet", () => {
// Now check account contents.
const registeredEmitterData =
await wormholeCctp.fetchRegisteredEmitter(registeredEmitter);
await circleIntegration.fetchRegisteredEmitter(registeredEmitter);
expect(registeredEmitterData).to.eql({
bump: 255,
cctpDomain,
@ -177,18 +184,32 @@ describe("Circle Integration -- Localnet", () => {
});
localVariables.set("vaa", vaa);
localVariables.set("registeredEmitter", registeredEmitter);
});
it("Cannot Invoke `register_emitter_and_domain` with Same Governance Sequence", async () => {
const vaa = localVariables.get("vaa") as anchor.web3.PublicKey;
expect(localVariables.delete("vaa")).is.true;
const ix = await wormholeCctp.registerEmitterAndDomainIx({
const registeredEmitter = localVariables.get(
"registeredEmitter",
) as anchor.web3.PublicKey;
expect(localVariables.delete("registeredEmitter")).is.true;
const ix = await circleIntegration.registerEmitterAndDomainIx({
payer: payer.publicKey,
vaa,
});
await expectIxErr(connection, [ix], [payer], "already in use");
// NOTE: This error actually triggers because a registered emitter is already present.
// In case something changes with registration, we will keep this test around (it could
// fail if registration changes in the future).
await expectIxErr(
connection,
[ix],
[payer],
`Allocate: account Address { address: ${registeredEmitter.toString()}, base: None } already in use`,
);
});
it("Cannot Invoke `register_emitter_and_domain` with Updated Emitter on Same Chain", async () => {
@ -211,23 +232,28 @@ describe("Circle Integration -- Localnet", () => {
},
});
const ix = await wormholeCctp.registerEmitterAndDomainIx({
const ix = await circleIntegration.registerEmitterAndDomainIx({
payer: payer.publicKey,
vaa,
});
const registeredEmtiter = wormholeCctp.registeredEmitterAddress(foreignChain);
const registeredEmtiter = circleIntegration.registeredEmitterAddress(foreignChain);
// Show that the foreign emitter about to be registered is not already written to the
// account.
{
const currentForeignEmitter = await wormholeCctp
const currentForeignEmitter = await circleIntegration
.fetchRegisteredEmitter(registeredEmtiter)
.then((registered) => registered.address);
expect(currentForeignEmitter).not.eql(foreignEmitter);
}
await expectIxErr(connection, [ix], [payer], "already in use");
await expectIxErr(
connection,
[ix],
[payer],
`Allocate: account Address { address: ${registeredEmtiter.toString()}, base: None } already in use`,
);
});
});
@ -244,7 +270,7 @@ describe("Circle Integration -- Localnet", () => {
const inputPayload = Buffer.from("All your base are belong to us.");
const message = anchor.web3.Keypair.generate();
const ix = await wormholeCctp.transferTokensWithPayloadIx(
const ix = await circleIntegration.transferTokensWithPayloadIx(
{
payer: payer.publicKey,
mint: USDC_MINT_ADDRESS,
@ -260,14 +286,27 @@ describe("Circle Integration -- Localnet", () => {
},
);
const approveIx = splToken.createApproveInstruction(
payerToken,
circleIntegration.custodianAddress(),
payer.publicKey,
1,
);
const lookupTableAccount = await connection
.getAddressLookupTable(lookupTableAddress)
.then((resp) => resp.value);
/// NOTE: This is a CCTP Token Messenger Minter program error.
await expectIxErr(connection, [ix], [payer, message], "InvalidAmount", {
addressLookupTableAccounts: [lookupTableAccount],
});
await expectIxErr(
connection,
[approveIx, ix],
[payer, message],
"Error Code: InvalidAmount",
{
addressLookupTableAccounts: [lookupTableAccount],
},
);
});
it("Cannot Invoke `transfer_tokens_with_payload` with Invalid Mint Recipient", async () => {
@ -282,7 +321,7 @@ describe("Circle Integration -- Localnet", () => {
const inputPayload = Buffer.from("All your base are belong to us.");
const message = anchor.web3.Keypair.generate();
const ix = await wormholeCctp.transferTokensWithPayloadIx(
const ix = await circleIntegration.transferTokensWithPayloadIx(
{
payer: payer.publicKey,
mint: USDC_MINT_ADDRESS,
@ -298,25 +337,35 @@ describe("Circle Integration -- Localnet", () => {
},
);
const approveIx = splToken.createApproveInstruction(
payerToken,
circleIntegration.custodianAddress(),
payer.publicKey,
amount,
);
const lookupTableAccount = await connection
.getAddressLookupTable(lookupTableAddress)
.then((resp) => resp.value);
/// NOTE: This is a CCTP Token Messenger Minter program error.
await expectIxErr(connection, [ix], [payer, message], "InvalidMintRecipient", {
addressLookupTableAccounts: [lookupTableAccount],
});
await expectIxErr(
connection,
[approveIx, ix],
[payer, message],
"Error Code: InvalidMintRecipient",
{
addressLookupTableAccounts: [lookupTableAccount],
},
);
});
it("Cannot Invoke `transfer_tokens_with_payload` if Burn Sender Not Authority for Burn Source", async () => {
it("Cannot Invoke `transfer_tokens_with_payload` if Custodian Not Delegated Authority", async () => {
const payerToken = splToken.getAssociatedTokenAddressSync(
USDC_MINT_ADDRESS,
payer.publicKey,
);
// Create another sender authority.
const sender = anchor.web3.Keypair.generate();
const amount = 69n;
const targetChain = 2;
const mintRecipient = Array.from(Buffer.alloc(32, "deadbeef", "hex"));
@ -324,13 +373,12 @@ describe("Circle Integration -- Localnet", () => {
const inputPayload = Buffer.from("All your base are belong to us.");
const message = anchor.web3.Keypair.generate();
const ix = await wormholeCctp.transferTokensWithPayloadIx(
const ix = await circleIntegration.transferTokensWithPayloadIx(
{
payer: payer.publicKey,
mint: USDC_MINT_ADDRESS,
burnSource: payerToken,
coreMessage: message.publicKey,
burnSourceAuthority: sender.publicKey,
},
{
amount,
@ -346,7 +394,7 @@ describe("Circle Integration -- Localnet", () => {
.then((resp) => resp.value);
// NOTE: This is an SPL Token program error.
await expectIxErr(connection, [ix], [payer, sender, message], "owner does not match", {
await expectIxErr(connection, [ix], [payer, message], "Error: owner does not match", {
addressLookupTableAccounts: [lookupTableAccount],
});
});
@ -364,7 +412,7 @@ describe("Circle Integration -- Localnet", () => {
const inputPayload = Buffer.from("All your base are belong to us.");
const message = anchor.web3.Keypair.generate();
const ix = await wormholeCctp.transferTokensWithPayloadIx(
const ix = await circleIntegration.transferTokensWithPayloadIx(
{
payer: payer.publicKey,
mint: USDC_MINT_ADDRESS,
@ -380,6 +428,13 @@ describe("Circle Integration -- Localnet", () => {
},
);
const approveIx = splToken.createApproveInstruction(
payerToken,
circleIntegration.custodianAddress(),
payer.publicKey,
amount,
);
const balanceBefore = await splToken
.getAccount(connection, payerToken)
.then((token) => token.amount);
@ -387,9 +442,14 @@ describe("Circle Integration -- Localnet", () => {
const lookupTableAccount = await connection
.getAddressLookupTable(lookupTableAddress)
.then((resp) => resp.value);
const txReceipt = await expectIxOkDetails(connection, [ix], [payer, message], {
addressLookupTableAccounts: [lookupTableAccount],
});
const txReceipt = await expectIxOkDetails(
connection,
[approveIx, ix],
[payer, message],
{
addressLookupTableAccounts: [lookupTableAccount],
},
);
// Balance check.
const balanceAfter = await splToken
@ -402,7 +462,7 @@ describe("Circle Integration -- Localnet", () => {
const { deposit, payload } = Deposit.decode(posted.message.payload);
expect(payload).to.eql(inputPayload);
const parsedTxData = await wormholeCctp.parseTransactionReceipt(txReceipt, [
const parsedTxData = await circleIntegration.parseTransactionReceipt(txReceipt, [
lookupTableAccount,
]);
expect(parsedTxData).has.length(1);
@ -412,7 +472,7 @@ describe("Circle Integration -- Localnet", () => {
const burnMessage = CctpTokenBurnMessage.decode(txData.encodedCctpMessage);
expect(burnMessage.sender).to.eql(
Array.from(wormholeCctp.custodianAddress().toBuffer()),
Array.from(circleIntegration.custodianAddress().toBuffer()),
);
expect(burnMessage.mintRecipient).to.eql(mintRecipient);
@ -435,112 +495,11 @@ describe("Circle Integration -- Localnet", () => {
payloadLen: inputPayload.length,
} as DepositHeader);
const foreignEmitter = await wormholeCctp
.fetchRegisteredEmitter(wormholeCctp.registeredEmitterAddress(targetChain))
const foreignEmitter = await circleIntegration
.fetchRegisteredEmitter(circleIntegration.registeredEmitterAddress(targetChain))
.then((registered) => registered.address);
expect(targetCaller).to.eql(foreignEmitter);
});
it("Invoke `transfer_tokens_with_payload` Multiple Times in One Transaction", async () => {
const payerToken = splToken.getAssociatedTokenAddressSync(
USDC_MINT_ADDRESS,
payer.publicKey,
);
const amount = 69n;
const targetChain = 2;
const mintRecipient = Array.from(Buffer.alloc(32, "deadbeef", "hex"));
const wormholeMessageNonce = 420;
const inputPayload = Buffer.from("Boop.");
const messages = [
anchor.web3.Keypair.generate(),
anchor.web3.Keypair.generate(),
anchor.web3.Keypair.generate(),
];
const ixs = await Promise.all(
messages.map((message) =>
wormholeCctp.transferTokensWithPayloadIx(
{
payer: payer.publicKey,
mint: USDC_MINT_ADDRESS,
burnSource: payerToken,
coreMessage: message.publicKey,
},
{
amount,
targetChain,
mintRecipient,
wormholeMessageNonce,
payload: inputPayload,
},
),
),
);
const balanceBefore = await splToken
.getAccount(connection, payerToken)
.then((token) => token.amount);
const lookupTableAccount = await connection
.getAddressLookupTable(lookupTableAddress)
.then((resp) => resp.value);
const txReceipt = await expectIxOkDetails(connection, ixs, [payer].concat(messages), {
addressLookupTableAccounts: [lookupTableAccount],
});
// Balance check.
const balanceAfter = await splToken
.getAccount(connection, payerToken)
.then((token) => token.amount);
expect(balanceAfter + BigInt(messages.length) * amount).to.equal(balanceBefore);
const parsedTxData = await wormholeCctp.parseTransactionReceipt(txReceipt, [
lookupTableAccount,
]);
expect(parsedTxData).has.length(messages.length);
const foreignEmitter = await wormholeCctp
.fetchRegisteredEmitter(wormholeCctp.registeredEmitterAddress(targetChain))
.then((registered) => registered.address);
for (let i = 0; i < messages.length; ++i) {
const txData = parsedTxData[i];
const message = messages[i];
expect(txData.coreMessageAccount).is.eql(message.publicKey);
// Check messages.
const posted = await getPostedMessage(connection, message.publicKey);
const { deposit, payload } = Deposit.decode(posted.message.payload);
expect(payload).to.eql(inputPayload);
const burnMessage = CctpTokenBurnMessage.decode(txData.encodedCctpMessage);
expect(burnMessage.sender).to.eql(
Array.from(wormholeCctp.custodianAddress().toBuffer()),
);
expect(burnMessage.mintRecipient).to.eql(mintRecipient);
const {
cctp: {
sourceDomain: sourceCctpDomain,
destinationDomain: destinationCctpDomain,
nonce: cctpNonce,
targetCaller,
},
} = burnMessage;
expect(deposit).to.eql({
tokenAddress: Array.from(USDC_MINT_ADDRESS.toBuffer()),
amount,
sourceCctpDomain,
destinationCctpDomain,
cctpNonce,
burnSource: Array.from(payerToken.toBuffer()),
mintRecipient,
payloadLen: inputPayload.length,
} as DepositHeader);
expect(targetCaller).to.eql(foreignEmitter);
}
});
});
describe("Inbound Transfers", () => {
@ -571,7 +530,7 @@ describe("Circle Integration -- Localnet", () => {
const burnSource = Array.from(Buffer.alloc(32, "beefdead", "hex"));
const { burnMessage, destinationCctpDomain, encodedCctpMessage, cctpAttestation } =
await craftCctpTokenBurnMessage(
wormholeCctp,
circleIntegration,
sourceCctpDomain,
cctpNonce,
encodedMintRecipient,
@ -605,7 +564,7 @@ describe("Circle Integration -- Localnet", () => {
const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({
units: 250_000,
});
const ix = await wormholeCctp.redeemTokensWithPayloadIx(
const ix = await circleIntegration.redeemTokensWithPayloadIx(
{
payer: payer.publicKey,
vaa,
@ -649,7 +608,7 @@ describe("Circle Integration -- Localnet", () => {
const burnSource = Array.from(Buffer.alloc(32, "beefdead", "hex"));
const { destinationCctpDomain, burnMessage, encodedCctpMessage, cctpAttestation } =
await craftCctpTokenBurnMessage(
wormholeCctp,
circleIntegration,
sourceCctpDomain,
cctpNonce,
encodedMintRecipient,
@ -686,7 +645,7 @@ describe("Circle Integration -- Localnet", () => {
const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({
units: 250_000,
});
const ix = await wormholeCctp.redeemTokensWithPayloadIx(
const ix = await circleIntegration.redeemTokensWithPayloadIx(
{
payer: payer.publicKey,
vaa,
@ -730,7 +689,7 @@ describe("Circle Integration -- Localnet", () => {
const burnSource = Array.from(Buffer.alloc(32, "beefdead", "hex"));
const { destinationCctpDomain, burnMessage, encodedCctpMessage, cctpAttestation } =
await craftCctpTokenBurnMessage(
wormholeCctp,
circleIntegration,
sourceCctpDomain,
cctpNonce,
encodedMintRecipient,
@ -764,7 +723,7 @@ describe("Circle Integration -- Localnet", () => {
const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({
units: 250_000,
});
const ix = await wormholeCctp.redeemTokensWithPayloadIx(
const ix = await circleIntegration.redeemTokensWithPayloadIx(
{
payer: payer.publicKey,
vaa,
@ -779,7 +738,7 @@ describe("Circle Integration -- Localnet", () => {
connection,
[computeIx, ix],
[payer, mintRecipientAuthority],
"InvalidMintRecipient",
"Error Code: InvalidMintRecipient",
{
addressLookupTableAccounts: [lookupTableAccount],
},
@ -806,7 +765,7 @@ describe("Circle Integration -- Localnet", () => {
const burnSource = Array.from(Buffer.alloc(32, "beefdead", "hex"));
const { destinationCctpDomain, burnMessage, encodedCctpMessage, cctpAttestation } =
await craftCctpTokenBurnMessage(
wormholeCctp,
circleIntegration,
sourceCctpDomain,
cctpNonce,
encodedMintRecipient,
@ -840,7 +799,7 @@ describe("Circle Integration -- Localnet", () => {
const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({
units: 250_000,
});
const ix = await wormholeCctp.redeemTokensWithPayloadIx(
const ix = await circleIntegration.redeemTokensWithPayloadIx(
{
payer: payer.publicKey,
vaa,
@ -856,7 +815,7 @@ describe("Circle Integration -- Localnet", () => {
connection,
[computeIx, ix],
[payer, someoneElse],
"ConstraintTokenOwner",
"Error Code: ConstraintTokenOwner",
{
addressLookupTableAccounts: [lookupTableAccount],
},
@ -881,7 +840,7 @@ describe("Circle Integration -- Localnet", () => {
const burnSource = Array.from(Buffer.alloc(32, "beefdead", "hex"));
const { destinationCctpDomain, burnMessage, encodedCctpMessage, cctpAttestation } =
await craftCctpTokenBurnMessage(
wormholeCctp,
circleIntegration,
sourceCctpDomain,
cctpNonce,
encodedMintRecipient,
@ -915,7 +874,7 @@ describe("Circle Integration -- Localnet", () => {
const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({
units: 250_000,
});
const ix = await wormholeCctp.redeemTokensWithPayloadIx(
const ix = await circleIntegration.redeemTokensWithPayloadIx(
{
payer: payer.publicKey,
vaa,
@ -930,7 +889,7 @@ describe("Circle Integration -- Localnet", () => {
connection,
[computeIx, ix],
[payer, mintRecipientAuthority],
"SourceCctpDomainMismatch",
"Error Code: SourceCctpDomainMismatch",
{
addressLookupTableAccounts: [lookupTableAccount],
},
@ -955,7 +914,7 @@ describe("Circle Integration -- Localnet", () => {
const burnSource = Array.from(Buffer.alloc(32, "beefdead", "hex"));
const { destinationCctpDomain, burnMessage, encodedCctpMessage, cctpAttestation } =
await craftCctpTokenBurnMessage(
wormholeCctp,
circleIntegration,
sourceCctpDomain,
cctpNonce,
encodedMintRecipient,
@ -989,7 +948,7 @@ describe("Circle Integration -- Localnet", () => {
const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({
units: 250_000,
});
const ix = await wormholeCctp.redeemTokensWithPayloadIx(
const ix = await circleIntegration.redeemTokensWithPayloadIx(
{
payer: payer.publicKey,
vaa,
@ -1004,7 +963,7 @@ describe("Circle Integration -- Localnet", () => {
connection,
[computeIx, ix],
[payer, mintRecipientAuthority],
"DestinationCctpDomainMismatch",
"Error Code: DestinationCctpDomainMismatch",
{
addressLookupTableAccounts: [lookupTableAccount],
},
@ -1029,7 +988,7 @@ describe("Circle Integration -- Localnet", () => {
const burnSource = Array.from(Buffer.alloc(32, "beefdead", "hex"));
const { destinationCctpDomain, burnMessage, encodedCctpMessage, cctpAttestation } =
await craftCctpTokenBurnMessage(
wormholeCctp,
circleIntegration,
sourceCctpDomain,
cctpNonce,
encodedMintRecipient,
@ -1063,7 +1022,7 @@ describe("Circle Integration -- Localnet", () => {
const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({
units: 250_000,
});
const ix = await wormholeCctp.redeemTokensWithPayloadIx(
const ix = await circleIntegration.redeemTokensWithPayloadIx(
{
payer: payer.publicKey,
vaa,
@ -1078,7 +1037,7 @@ describe("Circle Integration -- Localnet", () => {
connection,
[computeIx, ix],
[payer, mintRecipientAuthority],
"CctpNonceMismatch",
"Error Code: CctpNonceMismatch",
{
addressLookupTableAccounts: [lookupTableAccount],
},
@ -1103,7 +1062,7 @@ describe("Circle Integration -- Localnet", () => {
const burnSource = Array.from(Buffer.alloc(32, "beefdead", "hex"));
const { destinationCctpDomain, burnMessage, encodedCctpMessage, cctpAttestation } =
await craftCctpTokenBurnMessage(
wormholeCctp,
circleIntegration,
sourceCctpDomain,
cctpNonce,
encodedMintRecipient,
@ -1137,7 +1096,7 @@ describe("Circle Integration -- Localnet", () => {
const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({
units: 250_000,
});
const ix = await wormholeCctp.redeemTokensWithPayloadIx(
const ix = await circleIntegration.redeemTokensWithPayloadIx(
{
payer: payer.publicKey,
vaa,
@ -1183,7 +1142,7 @@ describe("Circle Integration -- Localnet", () => {
) as anchor.web3.Keypair;
expect(localVariables.delete("mintRecipientAuthority")).is.true;
const ix = await wormholeCctp.redeemTokensWithPayloadIx(
const ix = await circleIntegration.redeemTokensWithPayloadIx(
{
payer: payer.publicKey,
vaa,
@ -1196,14 +1155,14 @@ describe("Circle Integration -- Localnet", () => {
connection,
[ix],
[payer, mintRecipientAuthority],
"NonceAlreadyUsed",
"Error Code: NonceAlreadyUsed",
);
});
});
});
async function craftCctpTokenBurnMessage(
wormholeCctp: WormholeCctpProgram,
circleIntegration: CircleIntegrationProgram,
sourceCctpDomain: number,
cctpNonce: bigint,
encodedMintRecipient: number[],
@ -1213,13 +1172,13 @@ async function craftCctpTokenBurnMessage(
) {
const { destinationCctpDomain: inputDestinationCctpDomain } = overrides;
const messageTransmitterProgram = wormholeCctp.messageTransmitterProgram();
const messageTransmitterProgram = circleIntegration.messageTransmitterProgram();
const { version, localDomain } = await messageTransmitterProgram.fetchMessageTransmitterConfig(
messageTransmitterProgram.messageTransmitterConfigAddress(),
);
const destinationCctpDomain = inputDestinationCctpDomain ?? localDomain;
const tokenMessengerMinterProgram = wormholeCctp.tokenMessengerMinterProgram();
const tokenMessengerMinterProgram = circleIntegration.tokenMessengerMinterProgram();
const sourceTokenMessenger = await tokenMessengerMinterProgram
.fetchRemoteTokenMessenger(
tokenMessengerMinterProgram.remoteTokenMessengerAddress(sourceCctpDomain),
@ -1234,7 +1193,7 @@ async function craftCctpTokenBurnMessage(
nonce: cctpNonce,
sender: sourceTokenMessenger,
recipient: Array.from(tokenMessengerMinterProgram.ID.toBuffer()), // targetTokenMessenger
targetCaller: Array.from(wormholeCctp.custodianAddress().toBuffer()), // targetCaller
targetCaller: Array.from(circleIntegration.custodianAddress().toBuffer()), // targetCaller
},
0,
Array.from(wormholeSdk.tryNativeToUint8Array(ETHEREUM_USDC_ADDRESS, "ethereum")), // sourceTokenAddress

View File

@ -1,7 +1,7 @@
import { MockEmitter, MockGuardians } from "@certusone/wormhole-sdk/lib/cjs/mock";
import * as anchor from "@coral-xyz/anchor";
import { expect } from "chai";
import { WormholeCctpProgram } from "../src";
import { CircleIntegrationProgram } from "../src";
import {
GUARDIAN_KEY,
PAYER_PRIVATE_KEY,
@ -22,7 +22,7 @@ describe("Circle Integration -- Testnet Fork", () => {
const connection = new anchor.web3.Connection("http://localhost:8899", "processed");
const payer = anchor.web3.Keypair.fromSecretKey(PAYER_PRIVATE_KEY);
const wormholeCctp = new WormholeCctpProgram(
const circleIntegration = new CircleIntegrationProgram(
connection,
"wCCTPvsyeL9qYqbHTv3DUAyzEfYcyHoYw5c4mgcbBeW",
);
@ -33,7 +33,7 @@ describe("Circle Integration -- Testnet Fork", () => {
it("Deploy Implementation", async () => {
const implementation = await loadProgramBpf(
ARTIFACTS_PATH,
wormholeCctp.upgradeAuthorityAddress(),
circleIntegration.upgradeAuthorityAddress(),
);
localVariables.set("implementation", implementation);
@ -59,7 +59,7 @@ describe("Circle Integration -- Testnet Fork", () => {
},
);
const ix = await wormholeCctp.upgradeContractIx({
const ix = await circleIntegration.upgradeContractIx({
payer: payer.publicKey,
vaa,
});
@ -70,7 +70,7 @@ describe("Circle Integration -- Testnet Fork", () => {
it("Deploy Same Implementation and Invoke `upgrade_contract` with Another VAA", async () => {
const implementation = await loadProgramBpf(
ARTIFACTS_PATH,
wormholeCctp.upgradeAuthorityAddress(),
circleIntegration.upgradeAuthorityAddress(),
);
const vaa = await postGovVaa(
@ -89,7 +89,7 @@ describe("Circle Integration -- Testnet Fork", () => {
},
);
const ix = await wormholeCctp.upgradeContractIx({
const ix = await circleIntegration.upgradeContractIx({
payer: payer.publicKey,
vaa,
});
@ -104,22 +104,25 @@ describe("Circle Integration -- Testnet Fork", () => {
const vaa = localVariables.get("vaa") as anchor.web3.PublicKey;
expect(localVariables.delete("vaa")).is.true;
const ix = await wormholeCctp.upgradeContractIx({
const ix = await circleIntegration.upgradeContractIx({
payer: payer.publicKey,
vaa,
});
// NOTE: The claim account created in the upgrade contract instruction doesn't trigger
// the protection for a replay attack. The account data in the program data does. But
// we will keep this test here just in case something changes in the future.
await expectIxErr(connection, [ix], [payer], "invalid account data for instruction");
});
it("Cannot Invoke `upgrade_contract` with Implementation Mismatch", async () => {
const implementation = await loadProgramBpf(
ARTIFACTS_PATH,
wormholeCctp.upgradeAuthorityAddress(),
circleIntegration.upgradeAuthorityAddress(),
);
const anotherImplementation = await loadProgramBpf(
ARTIFACTS_PATH,
wormholeCctp.upgradeAuthorityAddress(),
circleIntegration.upgradeAuthorityAddress(),
);
const vaa = await postGovVaa(
@ -139,24 +142,24 @@ describe("Circle Integration -- Testnet Fork", () => {
);
// Create the upgrade instruction, but pass a different implementation.
const ix = await wormholeCctp.upgradeContractIx({
const ix = await circleIntegration.upgradeContractIx({
payer: payer.publicKey,
vaa,
buffer: implementation,
});
await expectIxErr(connection, [ix], [payer], "ImplementationMismatch");
await expectIxErr(connection, [ix], [payer], "Error Code: ImplementationMismatch");
});
it("Cannot Invoke `upgrade_contract` with Invalid Governance Emitter", async () => {
const implementation = await loadProgramBpf(
ARTIFACTS_PATH,
wormholeCctp.upgradeAuthorityAddress(),
circleIntegration.upgradeAuthorityAddress(),
);
// Create a bad governance emitter by using an invalid address.
const invalidEmitter = new MockEmitter(
wormholeCctp.ID.toBuffer().toString("hex"),
circleIntegration.ID.toBuffer().toString("hex"),
1,
12121212,
);
@ -179,19 +182,19 @@ describe("Circle Integration -- Testnet Fork", () => {
);
// Create the upgrade instruction, but pass a different implementation.
const ix = await wormholeCctp.upgradeContractIx({
const ix = await circleIntegration.upgradeContractIx({
payer: payer.publicKey,
vaa,
buffer: implementation,
});
await expectIxErr(connection, [ix], [payer], "InvalidGovernanceEmitter");
await expectIxErr(connection, [ix], [payer], "Error Code: InvalidGovernanceEmitter");
});
it("Cannot Invoke `upgrade_contract` with Governance For Another Chain", async () => {
const implementation = await loadProgramBpf(
ARTIFACTS_PATH,
wormholeCctp.upgradeAuthorityAddress(),
circleIntegration.upgradeAuthorityAddress(),
);
const vaa = await postGovVaa(
@ -210,18 +213,18 @@ describe("Circle Integration -- Testnet Fork", () => {
},
);
const ix = await wormholeCctp.upgradeContractIx({
const ix = await circleIntegration.upgradeContractIx({
payer: payer.publicKey,
vaa,
});
await expectIxErr(connection, [ix], [payer], "GovernanceForAnotherChain");
await expectIxErr(connection, [ix], [payer], "Error Code: GovernanceForAnotherChain");
});
it("Cannot Invoke `upgrade_contract` with Invalid Governance Action", async () => {
const implementation = await loadProgramBpf(
ARTIFACTS_PATH,
wormholeCctp.upgradeAuthorityAddress(),
circleIntegration.upgradeAuthorityAddress(),
);
const vaa = await postGovVaa(
@ -247,13 +250,13 @@ describe("Circle Integration -- Testnet Fork", () => {
},
);
const ix = await wormholeCctp.upgradeContractIx({
const ix = await circleIntegration.upgradeContractIx({
payer: payer.publicKey,
vaa,
buffer: implementation,
});
await expectIxErr(connection, [ix], [payer], "InvalidGovernanceAction");
await expectIxErr(connection, [ix], [payer], "Error Code: InvalidGovernanceAction");
});
});
});

View File

@ -181,8 +181,8 @@ export async function loadProgramBpf(
);
// Sometimes the validator fails to fetch a blockhash after this buffer gets loaded, so we wait
// a bit to ensure that doesn't happen.
await new Promise((resolve) => setTimeout(resolve, 5000));
// a bit to ensure that doesn't happen. Uncomment this in if this is an issue.
//await new Promise((resolve) => setTimeout(resolve, 5000));
// Return the pubkey for the buffer (our new program implementation).
return buffer;