diff --git a/CHANGELOG.md b/CHANGELOG.md index 784473b24d..431168dfae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ The minor version will be incremented upon a breaking change and the patch versi - cli: Add support for Solidity programs. `anchor init` and `anchor new` take an option `--solidity` which creates solidity code rather than rust. `anchor build` and `anchor test` work accordingly ([#2421](https://github.com/coral-xyz/anchor/pull/2421)) - bench: Add benchmarking for compute units usage ([#2466](https://github.com/coral-xyz/anchor/pull/2466)) - cli: `idl set-buffer`, `idl set-authority` and `idl close` take an option `--print-only`. which prints transaction in a base64 Borsh compatible format but not sent to the cluster. It's helpful when managing authority under a multisig, e.g., a user can create a proposal for a `Custom Instruction` in SPL Governance ([#2486](https://github.com/coral-xyz/anchor/pull/2486)). +- lang: Add `emit_cpi!` and `#[event_cpi]` macros(behind `event-cpi` feature flag) to store event logs in transaction metadata ([#2438](https://github.com/coral-xyz/anchor/pull/2438)). ### Fixes diff --git a/cli/Cargo.toml b/cli/Cargo.toml index eedfabc31f..d3072e7b02 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -24,7 +24,7 @@ bincode = "1.3.3" syn = { version = "1.0.60", features = ["full", "extra-traits"] } anchor-lang = { path = "../lang", version = "0.27.0", package = "light-anchor-lang" } anchor-client = { path = "../client", version = "0.27.0", package = "light-anchor-client" } -anchor-syn = { path = "../lang/syn", features = ["idl", "init-if-needed"], version = "0.27.0", package = "light-anchor-syn" } +anchor-syn = { path = "../lang/syn", features = ["event-cpi", "idl", "init-if-needed"], version = "0.27.0", package = "light-anchor-syn" } serde_json = "1.0" shellexpand = "2.1.0" toml = "0.5.8" diff --git a/lang/Cargo.toml b/lang/Cargo.toml index ec14b27de4..f4431f1362 100644 --- a/lang/Cargo.toml +++ b/lang/Cargo.toml @@ -13,6 +13,7 @@ allow-missing-optionals = ["anchor-derive-accounts/allow-missing-optionals"] init-if-needed = ["anchor-derive-accounts/init-if-needed"] derive = [] default = [] +event-cpi = ["anchor-attribute-event/event-cpi"] anchor-debug = [ "anchor-attribute-access-control/anchor-debug", "anchor-attribute-account/anchor-debug", diff --git a/lang/attribute/event/Cargo.toml b/lang/attribute/event/Cargo.toml index b6bf82be1c..4c066c8deb 100644 --- a/lang/attribute/event/Cargo.toml +++ b/lang/attribute/event/Cargo.toml @@ -13,6 +13,7 @@ proc-macro = true [features] anchor-debug = ["anchor-syn/anchor-debug"] +event-cpi = ["anchor-syn/event-cpi"] [dependencies] proc-macro2 = "1.0" diff --git a/lang/attribute/event/src/lib.rs b/lang/attribute/event/src/lib.rs index 33933bdfda..7301162d3e 100644 --- a/lang/attribute/event/src/lib.rs +++ b/lang/attribute/event/src/lib.rs @@ -1,5 +1,7 @@ extern crate proc_macro; +#[cfg(feature = "event-cpi")] +use anchor_syn::parser::accounts::event_cpi::{add_event_cpi_accounts, EventAuthority}; use quote::quote; use syn::parse_macro_input; @@ -45,6 +47,14 @@ pub fn event( }) } +// EventIndex is a marker macro. It functionally does nothing other than +// allow one to mark fields with the `#[index]` inert attribute, which is +// used to add metadata to IDLs. +#[proc_macro_derive(EventIndex, attributes(index))] +pub fn derive_event(_item: proc_macro::TokenStream) -> proc_macro::TokenStream { + proc_macro::TokenStream::from(quote! {}) +} + /// Logs an event that can be subscribed to by clients. /// Uses the [`sol_log_data`](https://docs.rs/solana-program/latest/solana_program/log/fn.sol_log_data.html) /// syscall which results in the following log: @@ -81,10 +91,127 @@ pub fn emit(input: proc_macro::TokenStream) -> proc_macro::TokenStream { }) } -// EventIndex is a marker macro. It functionally does nothing other than -// allow one to mark fields with the `#[index]` inert attribute, which is -// used to add metadata to IDLs. -#[proc_macro_derive(EventIndex, attributes(index))] -pub fn derive_event(_item: proc_macro::TokenStream) -> proc_macro::TokenStream { - proc_macro::TokenStream::from(quote! {}) +/// Log an event by making a self-CPI that can be subscribed to by clients. +/// +/// This way of logging events is more reliable than [`emit!`](emit!) because RPCs are less likely +/// to truncate CPI information than program logs. +/// +/// Uses a [`invoke_signed`](https://docs.rs/solana-program/latest/solana_program/program/fn.invoke_signed.html) +/// syscall to store the event data in the ledger, which results in the data being stored in the +/// transaction metadata. +/// +/// This method requires the usage of an additional PDA to guarantee that the self-CPI is truly +/// being invoked by the same program. Requiring this PDA to be a signer during `invoke_signed` +/// syscall ensures that the program is the one doing the logging. +/// +/// The necessary accounts are added to the accounts struct via [`#[event_cpi]`](event_cpi) +/// attribute macro. +/// +/// # Example +/// +/// ```ignore +/// use anchor_lang::prelude::*; +/// +/// #[program] +/// pub mod my_program { +/// use super::*; +/// +/// pub fn my_instruction(ctx: Context) -> Result<()> { +/// emit_cpi!(MyEvent { data: 42 }); +/// Ok(()) +/// } +/// } +/// +/// #[event_cpi] +/// #[derive(Accounts)] +/// pub struct MyInstruction {} +/// +/// #[event] +/// pub struct MyEvent { +/// pub data: u64, +/// } +/// ``` +/// +/// **NOTE:** This macro requires `ctx` to be in scope. +/// +/// *Only available with `event-cpi` feature enabled.* +#[cfg(feature = "event-cpi")] +#[proc_macro] +pub fn emit_cpi(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let event_struct = parse_macro_input!(input as syn::Expr); + + let authority = EventAuthority::get(); + let authority_name = authority.name_token_stream(); + let authority_name_str = authority.name; + let authority_seeds = authority.seeds; + + proc_macro::TokenStream::from(quote! { + { + let authority_info = ctx.accounts.#authority_name.to_account_info(); + let authority_bump = *ctx.bumps.get(#authority_name_str).unwrap(); + + let disc = anchor_lang::event::EVENT_IX_TAG_LE; + let inner_data = anchor_lang::Event::data(&#event_struct); + let ix_data: Vec = disc.into_iter().chain(inner_data.into_iter()).collect(); + + let ix = anchor_lang::solana_program::instruction::Instruction::new_with_bytes( + crate::ID, + &ix_data, + vec![ + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + *authority_info.key, + true, + ), + ], + ); + anchor_lang::solana_program::program::invoke_signed( + &ix, + &[authority_info], + &[&[#authority_seeds, &[authority_bump]]], + ) + .map_err(anchor_lang::error::Error::from)?; + } + }) +} + +/// An attribute macro to add necessary event CPI accounts to the given accounts struct. +/// +/// Two accounts named `event_authority` and `program` will be appended to the list of accounts. +/// +/// # Example +/// +/// ```ignore +/// #[event_cpi] +/// #[derive(Accounts)] +/// pub struct MyInstruction<'info> { +/// pub signer: Signer<'info>, +/// } +/// ``` +/// +/// The code above will be expanded to: +/// +/// ```ignore +/// #[derive(Accounts)] +/// pub struct MyInstruction<'info> { +/// pub signer: Signer<'info>, +/// /// CHECK: Only the event authority can invoke self-CPI +/// #[account(seeds = [b"__event_authority"], bump)] +/// pub event_authority: AccountInfo<'info>, +/// /// CHECK: Self-CPI will fail if the program is not the current program +/// pub program: AccountInfo<'info>, +/// } +/// ``` +/// +/// See [`emit_cpi!`](emit_cpi!) for a full example. +/// +/// *Only available with `event-cpi` feature enabled.* +#[cfg(feature = "event-cpi")] +#[proc_macro_attribute] +pub fn event_cpi( + _attr: proc_macro::TokenStream, + input: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + let accounts_struct = parse_macro_input!(input as syn::ItemStruct); + let accounts_struct = add_event_cpi_accounts(&accounts_struct).unwrap(); + proc_macro::TokenStream::from(quote! {#accounts_struct}) } diff --git a/lang/src/error.rs b/lang/src/error.rs index b6a8c9cbc3..79e6a21890 100644 --- a/lang/src/error.rs +++ b/lang/src/error.rs @@ -44,6 +44,11 @@ pub enum ErrorCode { #[msg("IDL account must be empty in order to resize, try closing first")] IdlAccountNotEmpty, + // Event instructions + /// 1500 - The program was compiled without `event-cpi` feature + #[msg("The program was compiled without `event-cpi` feature")] + EventInstructionStub = 1500, + // Constraints /// 2000 - A mut constraint was violated #[msg("A mut constraint was violated")] diff --git a/lang/src/event.rs b/lang/src/event.rs new file mode 100644 index 0000000000..99f2abde12 --- /dev/null +++ b/lang/src/event.rs @@ -0,0 +1,3 @@ +// Sha256(anchor:event)[..8] +pub const EVENT_IX_TAG: u64 = 0x1d9acb512ea545e4; +pub const EVENT_IX_TAG_LE: [u8; 8] = EVENT_IX_TAG.to_le_bytes(); diff --git a/lang/src/lib.rs b/lang/src/lib.rs index 0317ea5cdc..1fb1a80918 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -38,6 +38,8 @@ mod common; pub mod context; pub mod error; #[doc(hidden)] +pub mod event; +#[doc(hidden)] pub mod idl; pub mod system_program; @@ -48,6 +50,8 @@ pub use anchor_attribute_account::{account, declare_id, zero_copy}; pub use anchor_attribute_constant::constant; pub use anchor_attribute_error::*; pub use anchor_attribute_event::{emit, event}; +#[cfg(feature = "event-cpi")] +pub use anchor_attribute_event::{emit_cpi, event_cpi}; pub use anchor_attribute_program::program; pub use anchor_derive_accounts::Accounts; pub use anchor_derive_space::InitSpace; @@ -299,6 +303,8 @@ pub mod prelude { AccountsClose, AccountsExit, AnchorDeserialize, AnchorSerialize, Id, InitSpace, Key, Owner, ProgramData, Result, Space, ToAccountInfo, ToAccountInfos, ToAccountMetas, }; + #[cfg(feature = "event-cpi")] + pub use super::{emit_cpi, event_cpi}; pub use anchor_attribute_error::*; pub use borsh; pub use error::*; diff --git a/lang/syn/Cargo.toml b/lang/syn/Cargo.toml index 9493f94548..838828b476 100644 --- a/lang/syn/Cargo.toml +++ b/lang/syn/Cargo.toml @@ -16,6 +16,7 @@ hash = [] default = [] anchor-debug = [] seeds = [] +event-cpi = [] [dependencies] proc-macro2 = { version = "1.0", features=["span-locations"]} diff --git a/lang/syn/src/codegen/program/dispatch.rs b/lang/syn/src/codegen/program/dispatch.rs index dac9015082..2c6090b887 100644 --- a/lang/syn/src/codegen/program/dispatch.rs +++ b/lang/syn/src/codegen/program/dispatch.rs @@ -27,9 +27,13 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { } }) .collect(); + let fallback_fn = gen_fallback(program).unwrap_or(quote! { Err(anchor_lang::error::ErrorCode::InstructionFallbackNotFound.into()) }); + + let event_cpi_handler = generate_event_cpi_handler(); + quote! { /// Performs method dispatch. /// @@ -67,17 +71,24 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { #(#global_dispatch_arms)* anchor_lang::idl::IDL_IX_TAG_LE => { // If the method identifier is the IDL tag, then execute an IDL - // instruction, injected into all Anchor programs. - if cfg!(not(feature = "no-idl")) { + // instruction, injected into all Anchor programs unless they have + // no-idl enabled + #[cfg(not(feature = "no-idl"))] + { __private::__idl::__idl_dispatch( program_id, accounts, &ix_data, ) - } else { + } + #[cfg(feature = "no-idl")] + { Err(anchor_lang::error::ErrorCode::IdlInstructionStub.into()) } } + anchor_lang::event::EVENT_IX_TAG_LE => { + #event_cpi_handler + } _ => { #fallback_fn } @@ -96,3 +107,17 @@ pub fn gen_fallback(program: &Program) -> Option { } }) } + +/// Generate the event-cpi instruction handler based on whether the `event-cpi` feature is enabled. +pub fn generate_event_cpi_handler() -> proc_macro2::TokenStream { + #[cfg(feature = "event-cpi")] + quote! { + // `event-cpi` feature is enabled, dispatch self-cpi instruction + __private::__events::__event_dispatch(program_id, accounts, &ix_data) + } + #[cfg(not(feature = "event-cpi"))] + quote! { + // `event-cpi` feature is not enabled + Err(anchor_lang::error::ErrorCode::EventInstructionStub.into()) + } +} diff --git a/lang/syn/src/codegen/program/handlers.rs b/lang/syn/src/codegen/program/handlers.rs index 321f93785d..07ee097b41 100644 --- a/lang/syn/src/codegen/program/handlers.rs +++ b/lang/syn/src/codegen/program/handlers.rs @@ -91,6 +91,8 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { } }; + let event_cpi_mod = generate_event_cpi_mod(); + let non_inlined_handlers: Vec = program .ixs .iter() @@ -173,14 +175,14 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { #idl_accounts_and_functions } - - /// __global mod defines wrapped handlers for global instructions. pub mod __global { use super::*; #(#non_inlined_handlers)* } + + #event_cpi_mod } } } @@ -189,3 +191,49 @@ fn generate_ix_variant_name(name: String) -> proc_macro2::TokenStream { let n = name.to_camel_case(); n.parse().unwrap() } + +/// Generate the event module based on whether the `event-cpi` feature is enabled. +fn generate_event_cpi_mod() -> proc_macro2::TokenStream { + #[cfg(feature = "event-cpi")] + { + let authority = crate::parser::accounts::event_cpi::EventAuthority::get(); + let authority_name = authority.name; + let authority_seeds = authority.seeds; + + quote! { + /// __events mod defines handler for self-cpi based event logging + pub mod __events { + use super::*; + + #[inline(never)] + pub fn __event_dispatch( + program_id: &Pubkey, + accounts: &[AccountInfo], + event_data: &[u8], + ) -> anchor_lang::Result<()> { + let given_event_authority = next_account_info(&mut accounts.iter())?; + if !given_event_authority.is_signer { + return Err(anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintSigner, + ) + .with_account_name(#authority_name)); + } + + let (expected_event_authority, _) = + Pubkey::find_program_address(&[#authority_seeds], &program_id); + if given_event_authority.key() != expected_event_authority { + return Err(anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintSeeds, + ) + .with_account_name(#authority_name) + .with_pubkeys((given_event_authority.key(), expected_event_authority))); + } + + Ok(()) + } + } + } + } + #[cfg(not(feature = "event-cpi"))] + quote! {} +} diff --git a/lang/syn/src/parser/accounts/event_cpi.rs b/lang/syn/src/parser/accounts/event_cpi.rs new file mode 100644 index 0000000000..fccbd2b3f4 --- /dev/null +++ b/lang/syn/src/parser/accounts/event_cpi.rs @@ -0,0 +1,70 @@ +use quote::quote; + +/// This struct is used to keep the authority account information in sync. +pub struct EventAuthority { + /// Account name of the event authority + pub name: &'static str, + /// Seeds expression of the event authority + pub seeds: proc_macro2::TokenStream, +} + +impl EventAuthority { + /// Returns the account name and the seeds expression of the event authority. + pub fn get() -> Self { + Self { + name: "event_authority", + seeds: quote! {b"__event_authority"}, + } + } + + /// Returns the name without surrounding quotes. + pub fn name_token_stream(&self) -> proc_macro2::TokenStream { + let name_token_stream = syn::parse_str::(self.name).unwrap(); + quote! {#name_token_stream} + } +} + +/// Add necessary event CPI accounts to the given accounts struct. +pub fn add_event_cpi_accounts( + accounts_struct: &syn::ItemStruct, +) -> syn::parse::Result { + let syn::ItemStruct { + attrs, + vis, + struct_token, + ident, + generics, + fields, + .. + } = accounts_struct; + + let fields = fields.into_iter().collect::>(); + + let info_lifetime = generics + .lifetimes() + .next() + .map(|lifetime| quote! {#lifetime}) + .unwrap_or(quote! {'info}); + let generics = generics + .lt_token + .map(|_| quote! {#generics}) + .unwrap_or(quote! {<'info>}); + + let authority = EventAuthority::get(); + let authority_name = authority.name_token_stream(); + let authority_seeds = authority.seeds; + + let accounts_struct = quote! { + #(#attrs)* + #vis #struct_token #ident #generics { + #(#fields,)* + + /// CHECK: Only the event authority can invoke self-CPI + #[account(seeds = [#authority_seeds], bump)] + pub #authority_name: AccountInfo<#info_lifetime>, + /// CHECK: Self-CPI will fail if the program is not the current program + pub program: AccountInfo<#info_lifetime>, + } + }; + syn::parse2(accounts_struct) +} diff --git a/lang/syn/src/parser/accounts/mod.rs b/lang/syn/src/parser/accounts/mod.rs index dedb6cdb92..1da0e52126 100644 --- a/lang/syn/src/parser/accounts/mod.rs +++ b/lang/syn/src/parser/accounts/mod.rs @@ -1,3 +1,7 @@ +pub mod constraints; +#[cfg(feature = "event-cpi")] +pub mod event_cpi; + use crate::parser::docs; use crate::*; use syn::parse::{Error as ParseError, Result as ParseResult}; @@ -7,10 +11,8 @@ use syn::token::Comma; use syn::Expr; use syn::Path; -pub mod constraints; - -pub fn parse(strct: &syn::ItemStruct) -> ParseResult { - let instruction_api: Option> = strct +pub fn parse(accounts_struct: &syn::ItemStruct) -> ParseResult { + let instruction_api: Option> = accounts_struct .attrs .iter() .find(|a| { @@ -20,7 +22,24 @@ pub fn parse(strct: &syn::ItemStruct) -> ParseResult { }) .map(|ix_attr| ix_attr.parse_args_with(Punctuated::::parse_terminated)) .transpose()?; - let fields = match &strct.fields { + + #[cfg(feature = "event-cpi")] + let accounts_struct = { + let is_event_cpi = accounts_struct + .attrs + .iter() + .filter_map(|attr| attr.path.get_ident()) + .any(|ident| *ident == "event_cpi"); + if is_event_cpi { + event_cpi::add_event_cpi_accounts(accounts_struct)? + } else { + accounts_struct.clone() + } + }; + #[cfg(not(feature = "event-cpi"))] + let accounts_struct = accounts_struct.clone(); + + let fields = match &accounts_struct.fields { syn::Fields::Named(fields) => fields .named .iter() @@ -28,7 +47,7 @@ pub fn parse(strct: &syn::ItemStruct) -> ParseResult { .collect::>>()?, _ => { return Err(ParseError::new_spanned( - &strct.fields, + &accounts_struct.fields, "fields must be named", )) } @@ -36,7 +55,11 @@ pub fn parse(strct: &syn::ItemStruct) -> ParseResult { constraints_cross_checks(&fields)?; - Ok(AccountsStruct::new(strct.clone(), fields, instruction_api)) + Ok(AccountsStruct::new( + accounts_struct, + fields, + instruction_api, + )) } fn constraints_cross_checks(fields: &[AccountField]) -> ParseResult<()> { diff --git a/tests/events/programs/events/Cargo.toml b/tests/events/programs/events/Cargo.toml index 972335d6e9..7d820d1247 100644 --- a/tests/events/programs/events/Cargo.toml +++ b/tests/events/programs/events/Cargo.toml @@ -16,4 +16,4 @@ cpi = ["no-entrypoint"] default = [] [dependencies] -anchor-lang = { path = "../../../../lang", package = "light-anchor-lang" } +anchor-lang = { path = "../../../../lang", features = ["event-cpi"], package = "light-anchor-lang" } diff --git a/tests/events/programs/events/src/lib.rs b/tests/events/programs/events/src/lib.rs index ef17b7b4e3..ff3f57c0e3 100644 --- a/tests/events/programs/events/src/lib.rs +++ b/tests/events/programs/events/src/lib.rs @@ -23,6 +23,14 @@ pub mod events { }); Ok(()) } + + pub fn test_event_cpi(ctx: Context) -> Result<()> { + emit_cpi!(MyOtherEvent { + data: 7, + label: "cpi".to_string(), + }); + Ok(()) + } } #[derive(Accounts)] @@ -31,6 +39,10 @@ pub struct Initialize {} #[derive(Accounts)] pub struct TestEvent {} +#[event_cpi] +#[derive(Accounts)] +pub struct TestEventCpi {} + #[event] pub struct MyEvent { pub data: u64, diff --git a/tests/events/tests/events.js b/tests/events/tests/events.js index b938e0555c..28a3599f90 100644 --- a/tests/events/tests/events.js +++ b/tests/events/tests/events.js @@ -1,61 +1,117 @@ const anchor = require("@coral-xyz/anchor"); const { assert } = require("chai"); -describe("events", () => { +describe("Events", () => { // Configure the client to use the local cluster. anchor.setProvider(anchor.AnchorProvider.env()); const program = anchor.workspace.Events; - it("Is initialized!", async () => { - let listener = null; + describe("Normal event", () => { + it("Single event works", async () => { + let listener = null; - let [event, slot] = await new Promise((resolve, _reject) => { - listener = program.addEventListener("MyEvent", (event, slot) => { - resolve([event, slot]); + let [event, slot] = await new Promise((resolve, _reject) => { + listener = program.addEventListener("MyEvent", (event, slot) => { + resolve([event, slot]); + }); + program.rpc.initialize(); }); - program.rpc.initialize(); - }); - await program.removeEventListener(listener); + await program.removeEventListener(listener); - assert.isAbove(slot, 0); - assert.strictEqual(event.data.toNumber(), 5); - assert.strictEqual(event.label, "hello"); - }); + assert.isAbove(slot, 0); + assert.strictEqual(event.data.toNumber(), 5); + assert.strictEqual(event.label, "hello"); + }); - it("Multiple events", async () => { - // Sleep so we don't get this transaction has already been processed. - await sleep(2000); + it("Multiple events work", async () => { + let listenerOne = null; + let listenerTwo = null; - let listenerOne = null; - let listenerTwo = null; + let [eventOne, slotOne] = await new Promise((resolve, _reject) => { + listenerOne = program.addEventListener("MyEvent", (event, slot) => { + resolve([event, slot]); + }); + program.rpc.initialize(); + }); - let [eventOne, slotOne] = await new Promise((resolve, _reject) => { - listenerOne = program.addEventListener("MyEvent", (event, slot) => { - resolve([event, slot]); + let [eventTwo, slotTwo] = await new Promise((resolve, _reject) => { + listenerTwo = program.addEventListener( + "MyOtherEvent", + (event, slot) => { + resolve([event, slot]); + } + ); + program.rpc.testEvent(); }); - program.rpc.initialize(); + + await program.removeEventListener(listenerOne); + await program.removeEventListener(listenerTwo); + + assert.isAbove(slotOne, 0); + assert.strictEqual(eventOne.data.toNumber(), 5); + assert.strictEqual(eventOne.label, "hello"); + + assert.isAbove(slotTwo, 0); + assert.strictEqual(eventTwo.data.toNumber(), 6); + assert.strictEqual(eventTwo.label, "bye"); }); + }); - let [eventTwo, slotTwo] = await new Promise((resolve, _reject) => { - listenerTwo = program.addEventListener("MyOtherEvent", (event, slot) => { - resolve([event, slot]); - }); - program.rpc.testEvent(); + describe("Self-CPI event", () => { + it("Works without accounts being specified", async () => { + const tx = await program.methods.testEventCpi().transaction(); + const config = { + commitment: "confirmed", + }; + const txHash = await program.provider.sendAndConfirm(tx, [], config); + const txResult = await program.provider.connection.getTransaction( + txHash, + config + ); + + const ixData = anchor.utils.bytes.bs58.decode( + txResult.meta.innerInstructions[0].instructions[0].data + ); + const eventData = anchor.utils.bytes.base64.encode(ixData.slice(8)); + const event = program.coder.events.decode(eventData); + + assert.strictEqual(event.name, "MyOtherEvent"); + assert.strictEqual(event.data.label, "cpi"); + assert.strictEqual(event.data.data.toNumber(), 7); }); - await program.removeEventListener(listenerOne); - await program.removeEventListener(listenerTwo); + it("Malicious invocation throws", async () => { + const tx = new anchor.web3.Transaction(); + tx.add( + new anchor.web3.TransactionInstruction({ + programId: program.programId, + keys: [ + { + pubkey: anchor.web3.PublicKey.findProgramAddressSync( + [Buffer.from("__event_authority")], + program.programId + )[0], + isSigner: false, + isWritable: false, + }, + { + pubkey: program.programId, + isSigner: false, + isWritable: false, + }, + ], + data: Buffer.from([0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d]), + }) + ); - assert.isAbove(slotOne, 0); - assert.strictEqual(eventOne.data.toNumber(), 5); - assert.strictEqual(eventOne.label, "hello"); + try { + await program.provider.sendAndConfirm(tx, []); + } catch (e) { + if (e.logs.some((log) => log.includes("ConstraintSigner"))) return; + console.log(e); + } - assert.isAbove(slotTwo, 0); - assert.strictEqual(eventTwo.data.toNumber(), 6); - assert.strictEqual(eventTwo.label, "bye"); + throw new Error("Was able to invoke the self-CPI instruction"); + }); }); }); - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} diff --git a/tests/yarn.lock b/tests/yarn.lock index f0a8cc1209..437dd856ee 100644 --- a/tests/yarn.lock +++ b/tests/yarn.lock @@ -16,28 +16,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@coral-xyz/anchor@=0.27.0": - version "0.26.0" - resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.26.0.tgz#c8e4f7177e93441afd030f22d777d54d0194d7d1" - integrity sha512-PxRl+wu5YyptWiR9F2MBHOLLibm87Z4IMUBPreX+DYBtPM+xggvcPi0KAN7+kIL4IrIhXI8ma5V0MCXxSN1pHg== - dependencies: - "@coral-xyz/borsh" "^0.26.0" - "@solana/web3.js" "^1.68.0" - base64-js "^1.5.1" - bn.js "^5.1.2" - bs58 "^4.0.1" - buffer-layout "^1.2.2" - camelcase "^6.3.0" - cross-fetch "^3.1.5" - crypto-hash "^1.3.0" - eventemitter3 "^4.0.7" - js-sha256 "^0.9.0" - pako "^2.0.3" - snake-case "^3.0.4" - superstruct "^0.15.4" - toml "^3.0.0" - -"@coral-xyz/anchor@file:../ts/packages/anchor": +"@coral-xyz/anchor@=0.27.0", "@coral-xyz/anchor@file:../ts/packages/anchor": version "0.27.0" dependencies: "@coral-xyz/borsh" "^0.27.0" @@ -56,10 +35,10 @@ superstruct "^0.15.4" toml "^3.0.0" -"@coral-xyz/borsh@^0.26.0", "@coral-xyz/borsh@^0.27.0": - version "0.26.0" - resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.26.0.tgz#d054f64536d824634969e74138f9f7c52bbbc0d5" - integrity sha512-uCZ0xus0CszQPHYfWAqKS5swS1UxvePu83oOF+TWpUkedsNlg6p2p4azxZNSSqwXb9uXMFgxhuMBX9r3Xoi0vQ== +"@coral-xyz/borsh@^0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.27.0.tgz#700c647ea5262b1488957ac7fb4e8acf72c72b63" + integrity sha512-tJKzhLukghTWPLy+n8K8iJKgBq1yLT/AxaNd10yJrX8mI56ao5+OFAKAqW/h0i79KCvb4BK0VGO5ECmmolFz9A== dependencies: bn.js "^5.1.2" buffer-layout "^1.2.0" diff --git a/ts/packages/anchor/src/program/accounts-resolver.ts b/ts/packages/anchor/src/program/accounts-resolver.ts index 3d144f7f1f..42b483e7d2 100644 --- a/ts/packages/anchor/src/program/accounts-resolver.ts +++ b/ts/packages/anchor/src/program/accounts-resolver.ts @@ -88,6 +88,7 @@ export class AccountsResolver { // addresses. That is, one PDA can be used as a seed in another. public async resolve() { await this.resolveConst(this._idlIx.accounts); + this._resolveEventCpi(this._idlIx.accounts); // Auto populate pdas and relations until we stop finding new accounts while ( @@ -225,6 +226,56 @@ export class AccountsResolver { } } + /** + * Resolve event CPI accounts `eventAuthority` and `program`. + * + * Accounts will only be resolved if they are declared next to each other to + * reduce the chance of name collision. + */ + private _resolveEventCpi( + accounts: IdlAccountItem[], + path: string[] = [] + ): void { + for (const i in accounts) { + const accountDescOrAccounts = accounts[i]; + const subAccounts = (accountDescOrAccounts as IdlAccounts).accounts; + if (subAccounts) { + this._resolveEventCpi(subAccounts, [ + ...path, + camelCase(accountDescOrAccounts.name), + ]); + } + + // Validate next index exists + const nextIndex = +i + 1; + if (nextIndex === accounts.length) return; + + const currentName = camelCase(accounts[i].name); + const nextName = camelCase(accounts[nextIndex].name); + + // Populate event CPI accounts if they exist + if (currentName === "eventAuthority" && nextName === "program") { + const currentPath = [...path, currentName]; + const nextPath = [...path, nextName]; + + if (!this.get(currentPath)) { + this.set( + currentPath, + PublicKey.findProgramAddressSync( + [Buffer.from("__event_authority")], + this._programId + )[0] + ); + } + if (!this.get(nextPath)) { + this.set(nextPath, this._programId); + } + + return; + } + } + } + private async resolvePdas( accounts: IdlAccountItem[], path: string[] = []