diff --git a/00-LEGACY_EXAMPLES/magic-actions/programs/magic-actions/src/lib.rs b/00-LEGACY_EXAMPLES/magic-actions/programs/magic-actions/src/lib.rs index 2613d67a..1d117d53 100644 --- a/00-LEGACY_EXAMPLES/magic-actions/programs/magic-actions/src/lib.rs +++ b/00-LEGACY_EXAMPLES/magic-actions/programs/magic-actions/src/lib.rs @@ -9,6 +9,8 @@ declare_id!("CrWQv121NBNzXjxVe5pNL7MsT2yW13dMheE4nemoudQ1"); pub const COUNTER_SEED: &[u8] = b"counter"; pub const LEADERBOARD_SEED: &[u8] = b"leaderboard"; +/// Escrow index used by `ActionArgs::new` when scheduling post-commit actions. +pub const ACTION_ESCROW_INDEX: u8 = 255; #[ephemeral] #[program] @@ -132,8 +134,24 @@ pub struct Increment<'info> { pub struct UpdateLeaderboard<'info> { #[account(mut, seeds = [LEADERBOARD_SEED], bump)] pub leaderboard: Account<'info, Leaderboard>, - /// CHECK: PDA owner depends on: 1) Delegated: Delegation Program; 2) Undelegated: Your program ID + /// CHECK: Owner depends on delegation state; the PDA constraint binds this + /// handler to the canonical counter account in either state. + #[account(seeds = [COUNTER_SEED], bump)] pub counter: UncheckedAccount<'info>, + /// CHECK: User-selected escrow authority. Leaderboard updates are + /// permissionless; the derived escrow signer below authenticates the + /// Magic Action call path. + pub escrow_auth: UncheckedAccount<'info>, + /// CHECK: Magic escrow PDA. Only the delegation program can sign for it, + /// which restricts this instruction to the post-commit action path. + #[account( + signer, + address = ephemeral_rollups_sdk::pda::ephemeral_balance_pda_from_payer( + &escrow_auth.key(), + ACTION_ESCROW_INDEX, + ), + )] + pub escrow: UncheckedAccount<'info>, } #[delegate] @@ -167,7 +185,8 @@ pub struct CommitAndUpdateLeaderboard<'info> { #[account(seeds = [LEADERBOARD_SEED], bump)] pub leaderboard: UncheckedAccount<'info>, - /// CHECK: Your program ID + /// CHECK: Destination program for the scheduled action. + #[account(address = crate::ID)] pub program_id: AccountInfo<'info>, } diff --git a/00-LEGACY_EXAMPLES/magic-actions/tests/magic-actions.ts b/00-LEGACY_EXAMPLES/magic-actions/tests/magic-actions.ts index 03be2e4c..b8d7824f 100644 --- a/00-LEGACY_EXAMPLES/magic-actions/tests/magic-actions.ts +++ b/00-LEGACY_EXAMPLES/magic-actions/tests/magic-actions.ts @@ -1,5 +1,6 @@ import * as anchor from "@coral-xyz/anchor"; import { Program, web3 } from "@coral-xyz/anchor"; +import { strict as assert } from "assert"; import { MagicActions } from "../target/types/magic_actions"; import { ConnectionMagicRouter, @@ -74,24 +75,29 @@ describe("magic-actions", () => { console.log("✅ Incremented Counter PDA! Signature:", signature); }); - it("Update Leaderboard!", async () => { - const tx = await program.methods - .updateLeaderboard() - .accounts({ - counter: pda, - escrowAuth: anchor.Wallet.local().publicKey, - escrow: escrowPdaFromEscrowAuthority(anchor.Wallet.local().publicKey), - }) - .transaction(); - - const signature = await sendAndConfirmTransaction( - routerConnection, - tx, - [anchor.Wallet.local().payer], - { skipPreflight: true } + it("Reject direct leaderboard updates!", async () => { + await assert.rejects( + async () => { + const tx = await program.methods + .updateLeaderboard() + .accounts({ + counter: pda, + escrowAuth: anchor.Wallet.local().publicKey, + escrow: escrowPdaFromEscrowAuthority( + anchor.Wallet.local().publicKey + ), + }) + .transaction(); + + await sendAndConfirmTransaction( + routerConnection, + tx, + [anchor.Wallet.local().payer], + { skipPreflight: true } + ); + }, + /signature verification failed|unknown signer/i ); - - await printCounter(program, pda, leaderboard_pda, routerConnection, signature, "✅ Updated Leaderboard!"); }); it("Delegate Counter to ER and create Escrow for Magic Action!", async () => { @@ -266,4 +272,4 @@ async function sleepWithAnimation(seconds: number): Promise { // Clear the line process.stdout.write('\r\x1b[K'); -} \ No newline at end of file +} diff --git a/magic-actions/anchor/programs/magic-actions/src/lib.rs b/magic-actions/anchor/programs/magic-actions/src/lib.rs index 2944a823..909809dc 100644 --- a/magic-actions/anchor/programs/magic-actions/src/lib.rs +++ b/magic-actions/anchor/programs/magic-actions/src/lib.rs @@ -9,6 +9,8 @@ declare_id!("9K7ybJnAYtVY7RQU8ELwLqCFeXi6e8FD33Yq8ZnjDsi9"); pub const COUNTER_SEED: &[u8] = b"counter"; pub const LEADERBOARD_SEED: &[u8] = b"leaderboard"; +/// Escrow index used by `ActionArgs::new` when scheduling post-commit actions. +pub const ACTION_ESCROW_INDEX: u8 = 255; #[ephemeral] #[program] @@ -132,8 +134,24 @@ pub struct Increment<'info> { pub struct UpdateLeaderboard<'info> { #[account(mut, seeds = [LEADERBOARD_SEED], bump)] pub leaderboard: Account<'info, Leaderboard>, - /// CHECK: PDA owner depends on: 1) Delegated: Delegation Program; 2) Undelegated: Your program ID + /// CHECK: Owner depends on delegation state; the PDA constraint binds this + /// handler to the canonical counter account in either state. + #[account(seeds = [COUNTER_SEED], bump)] pub counter: UncheckedAccount<'info>, + /// CHECK: User-selected escrow authority. Leaderboard updates are + /// permissionless; the derived escrow signer below authenticates the + /// Magic Action call path. + pub escrow_auth: UncheckedAccount<'info>, + /// CHECK: Magic escrow PDA. Only the delegation program can sign for it, + /// which restricts this instruction to the post-commit action path. + #[account( + signer, + address = ephemeral_rollups_sdk::pda::ephemeral_balance_pda_from_payer( + &escrow_auth.key(), + ACTION_ESCROW_INDEX, + ), + )] + pub escrow: UncheckedAccount<'info>, } #[delegate] @@ -167,7 +185,8 @@ pub struct CommitAndUpdateLeaderboard<'info> { #[account(seeds = [LEADERBOARD_SEED], bump)] pub leaderboard: UncheckedAccount<'info>, - /// CHECK: Your program ID + /// CHECK: Destination program for the scheduled action. + #[account(address = crate::ID)] pub program_id: UncheckedAccount<'info>, } diff --git a/magic-actions/anchor/tests/magic-actions-local.ts b/magic-actions/anchor/tests/magic-actions-local.ts index 70b3eb16..d89975c4 100644 --- a/magic-actions/anchor/tests/magic-actions-local.ts +++ b/magic-actions/anchor/tests/magic-actions-local.ts @@ -1,5 +1,6 @@ import * as anchor from "@coral-xyz/anchor"; import { Program, web3 } from "@coral-xyz/anchor"; +import { strict as assert } from "assert"; import { MagicActions } from "../target/types/magic_actions"; import { DELEGATION_PROGRAM_ID, @@ -117,21 +118,55 @@ describe("magic-actions-local", () => { console.log("✅ Incremented (base). Sig:", sig); }); - it("Update Leaderboard on base layer", async () => { + it("Reject direct leaderboard updates", async () => { const info = await provider.connection.getAccountInfo(pda); if (info?.owner.toBase58() === DELEGATION_PROGRAM_ID.toBase58()) { console.log("Skipping — counter is delegated"); return; } - const sig = await program.methods - .updateLeaderboard() - .accounts({ - counter: pda, - escrowAuth: provider.wallet.publicKey, - escrow: escrowPdaFromEscrowAuthority(provider.wallet.publicKey), - }) - .rpc({ skipPreflight: true }); - await printCounter(`✅ Updated leaderboard. Sig: ${sig}`); + await assert.rejects( + program.methods + .updateLeaderboard() + .accounts({ + counter: pda, + escrowAuth: provider.wallet.publicKey, + escrow: escrowPdaFromEscrowAuthority(provider.wallet.publicKey), + }) + .rpc({ skipPreflight: true }), + /signature verification failed|unknown signer/i, + ); + }); + + it("Reject an incorrect counter PDA", async () => { + const invalidEscrow = web3.Keypair.generate(); + await assert.rejects( + program.methods + .updateLeaderboard() + .accounts({ + counter: leaderboardPda, + escrowAuth: provider.wallet.publicKey, + escrow: invalidEscrow.publicKey, + }) + .signers([invalidEscrow]) + .rpc(), + /Error Code: ConstraintSeeds/, + ); + }); + + it("Reject an incorrect escrow PDA", async () => { + const invalidEscrow = web3.Keypair.generate(); + await assert.rejects( + program.methods + .updateLeaderboard() + .accounts({ + counter: pda, + escrowAuth: provider.wallet.publicKey, + escrow: invalidEscrow.publicKey, + }) + .signers([invalidEscrow]) + .rpc(), + /Error Code: ConstraintAddress/, + ); }); it("Delegate Counter and create Escrow", async () => { diff --git a/magic-actions/anchor/tests/magic-actions.ts b/magic-actions/anchor/tests/magic-actions.ts index cb85d242..8de630bb 100644 --- a/magic-actions/anchor/tests/magic-actions.ts +++ b/magic-actions/anchor/tests/magic-actions.ts @@ -1,5 +1,6 @@ import * as anchor from "@coral-xyz/anchor"; import { Program, web3 } from "@coral-xyz/anchor"; +import { strict as assert } from "assert"; import { MagicActions } from "../target/types/magic_actions"; import { ConnectionMagicRouter, @@ -82,31 +83,24 @@ describe("magic-actions", () => { console.log("✅ Incremented Counter PDA! Signature:", signature); }); - it("Update Leaderboard!", async () => { - const tx = await program.methods - .updateLeaderboard() - .accounts({ - counter: pda, - escrowAuth: anchor.Wallet.local().publicKey, - escrow: escrowPdaFromEscrowAuthority(anchor.Wallet.local().publicKey), - }) - .transaction(); - - const signature = await sendAndConfirmTransaction( - routerConnection, - tx, - [anchor.Wallet.local().payer], - { skipPreflight: true }, - ); - - await printCounter( - program, - pda, - leaderboard_pda, - routerConnection, - signature, - "✅ Updated Leaderboard!", - ); + it("Reject direct leaderboard updates!", async () => { + await assert.rejects(async () => { + const tx = await program.methods + .updateLeaderboard() + .accounts({ + counter: pda, + escrowAuth: anchor.Wallet.local().publicKey, + escrow: escrowPdaFromEscrowAuthority(anchor.Wallet.local().publicKey), + }) + .transaction(); + + await sendAndConfirmTransaction( + routerConnection, + tx, + [anchor.Wallet.local().payer], + { skipPreflight: true }, + ); + }, /signature verification failed|unknown signer/i); }); it("Delegate Counter to ER and create Escrow for Magic Action!", async () => {