浏览代码

Feature: CPI Events API (#2438)

Co-authored-by: acheron <acheroncrypto@gmail.com>
Noah Gundotra 2 年之前
父节点
当前提交
23b90bffc0

+ 1 - 0
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
 

+ 1 - 1
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" }
 anchor-client = { path = "../client", version = "0.27.0" }
-anchor-syn = { path = "../lang/syn", features = ["idl", "init-if-needed"], version = "0.27.0" }
+anchor-syn = { path = "../lang/syn", features = ["event-cpi", "idl", "init-if-needed"], version = "0.27.0" }
 serde_json = "1.0"
 shellexpand = "2.1.0"
 toml = "0.5.8"

+ 1 - 0
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",

+ 1 - 0
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"

+ 133 - 6
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<MyInstruction>) -> 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<u8> = 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})
 }

+ 5 - 0
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")]

+ 3 - 0
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();

+ 6 - 0
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::*;

+ 1 - 0
lang/syn/Cargo.toml

@@ -16,6 +16,7 @@ hash = []
 default = []
 anchor-debug = []
 seeds = []
+event-cpi = []
 
 [dependencies]
 proc-macro2 = { version = "1.0", features=["span-locations"]}

+ 28 - 3
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<proc_macro2::TokenStream> {
         }
     })
 }
+
+/// 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())
+    }
+}

+ 50 - 2
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<proc_macro2::TokenStream> = 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! {}
+}

+ 70 - 0
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::<syn::Expr>(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<syn::ItemStruct> {
+    let syn::ItemStruct {
+        attrs,
+        vis,
+        struct_token,
+        ident,
+        generics,
+        fields,
+        ..
+    } = accounts_struct;
+
+    let fields = fields.into_iter().collect::<Vec<_>>();
+
+    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)
+}

+ 30 - 7
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<AccountsStruct> {
-    let instruction_api: Option<Punctuated<Expr, Comma>> = strct
+pub fn parse(accounts_struct: &syn::ItemStruct) -> ParseResult<AccountsStruct> {
+    let instruction_api: Option<Punctuated<Expr, Comma>> = accounts_struct
         .attrs
         .iter()
         .find(|a| {
@@ -20,7 +22,24 @@ pub fn parse(strct: &syn::ItemStruct) -> ParseResult<AccountsStruct> {
         })
         .map(|ix_attr| ix_attr.parse_args_with(Punctuated::<Expr, Comma>::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<AccountsStruct> {
             .collect::<ParseResult<Vec<AccountField>>>()?,
         _ => {
             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<AccountsStruct> {
 
     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<()> {

+ 1 - 1
tests/events/programs/events/Cargo.toml

@@ -16,4 +16,4 @@ cpi = ["no-entrypoint"]
 default = []
 
 [dependencies]
-anchor-lang = { path = "../../../../lang" }
+anchor-lang = { path = "../../../../lang", features = ["event-cpi"] }

+ 12 - 0
tests/events/programs/events/src/lib.rs

@@ -23,6 +23,14 @@ pub mod events {
         });
         Ok(())
     }
+
+    pub fn test_event_cpi(ctx: Context<TestEventCpi>) -> 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,

+ 95 - 39
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));
-}

+ 5 - 26
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"

+ 51 - 0
ts/packages/anchor/src/program/accounts-resolver.ts

@@ -88,6 +88,7 @@ export class AccountsResolver<IDL extends Idl> {
   //       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<IDL extends Idl> {
     }
   }
 
+  /**
+   * 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[] = []