diff --git a/.gitignore b/.gitignore index 427570f..d03ae4e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .anchor/ /target **/.DS_Store +node_modules/ +test-ledger/ \ No newline at end of file diff --git a/Anchor.toml b/Anchor.toml index 351af0a..3337400 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -5,6 +5,8 @@ resolution = true skip-lint = false [programs.localnet] +callable-test = "HyJgPvyuHtXbVkttTbaBUU87sT7iPyHcD6UgUx82gEBq" +callable-test-2 = "B7apRShjWeCk2j64MFurBzjpnh5YYuNieMVkMZA7joVv" protocol_contracts_solana = "ZETAjseVjuFsxdRxo6MmTCvqFwb3ZHUx56Co3vCmGis" [registry] diff --git a/Cargo.lock b/Cargo.lock index 44aed93..3d2586d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -671,6 +671,20 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "callable-test" +version = "0.1.0" +dependencies = [ + "anchor-lang", +] + +[[package]] +name = "callable-test-2" +version = "0.1.0" +dependencies = [ + "anchor-lang", +] + [[package]] name = "cargo_toml" version = "0.19.2" diff --git a/programs/callable-test-2/Cargo.toml b/programs/callable-test-2/Cargo.toml new file mode 100644 index 0000000..1a1b8a3 --- /dev/null +++ b/programs/callable-test-2/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "callable-test-2" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "callable_test_2" + +[features] +default = [] +cpi = ["no-entrypoint"] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +idl-build = ["anchor-lang/idl-build"] + +[dependencies] +anchor-lang = "0.30.0" diff --git a/programs/callable-test-2/Xargo.toml b/programs/callable-test-2/Xargo.toml new file mode 100644 index 0000000..475fb71 --- /dev/null +++ b/programs/callable-test-2/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/programs/callable-test-2/src/lib.rs b/programs/callable-test-2/src/lib.rs new file mode 100644 index 0000000..63ba61f --- /dev/null +++ b/programs/callable-test-2/src/lib.rs @@ -0,0 +1,20 @@ +use anchor_lang::prelude::*; + +declare_id!("B7apRShjWeCk2j64MFurBzjpnh5YYuNieMVkMZA7joVv"); + +// NOTE: will be removed, wanted to check if discriminator for on_call will be the same +#[program] +pub mod callable_test_2 { + use super::*; + + pub fn on_call(ctx: Context, sender: Pubkey, data: Vec) -> Result<()> { + + Ok(()) + } +} + +#[derive(Accounts)] +pub struct Initialize {} + +#[derive(Accounts)] +pub struct OnCall {} \ No newline at end of file diff --git a/programs/callable-test/Cargo.toml b/programs/callable-test/Cargo.toml new file mode 100644 index 0000000..5fa610d --- /dev/null +++ b/programs/callable-test/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "callable-test" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "callable_test" + +[features] +default = [] +cpi = ["no-entrypoint"] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +idl-build = ["anchor-lang/idl-build"] + +[dependencies] +anchor-lang = { version = "=0.30.0" } \ No newline at end of file diff --git a/programs/callable-test/Xargo.toml b/programs/callable-test/Xargo.toml new file mode 100644 index 0000000..475fb71 --- /dev/null +++ b/programs/callable-test/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/programs/callable-test/src/lib.rs b/programs/callable-test/src/lib.rs new file mode 100644 index 0000000..37bb604 --- /dev/null +++ b/programs/callable-test/src/lib.rs @@ -0,0 +1,24 @@ +use anchor_lang::prelude::*; + +declare_id!("HhLWiKkriQSSZmu1Pfa2tkQD87HosDSFUqeuZKeEc88m"); + +#[program] +pub mod callable_test { + use super::*; + + pub fn on_call(ctx: Context, sender: Pubkey, data: Vec) -> Result<()> { + // Perform custom logic here based on the received data + + Ok(()) + } +} + +#[derive(Accounts)] +pub struct OnCall {} + + +#[account] +pub struct StorageAccount { + pub last_sender: Pubkey, + pub last_data: Vec, // Store the last used data +} \ No newline at end of file diff --git a/programs/protocol-contracts-solana/src/lib.rs b/programs/protocol-contracts-solana/src/lib.rs index d49454e..ccc95f2 100644 --- a/programs/protocol-contracts-solana/src/lib.rs +++ b/programs/protocol-contracts-solana/src/lib.rs @@ -4,6 +4,8 @@ use anchor_spl::token::{transfer, Token, TokenAccount}; use solana_program::keccak::hash; use solana_program::secp256k1_recover::secp256k1_recover; use std::mem::size_of; +use solana_program::instruction::Instruction; +use solana_program::program::invoke; #[error_code] pub enum Errors { @@ -25,10 +27,43 @@ pub enum Errors { MemoLengthTooShort, #[msg("DepositPaused")] DepositPaused, + #[msg("InvalidInstructionData")] + InvalidInstructionData, } declare_id!("ZETAjseVjuFsxdRxo6MmTCvqFwb3ZHUx56Co3vCmGis"); +#[repr(C)] +#[derive(Clone, Debug, PartialEq)] +pub enum CallableInstruction { + OnCall { + sender: Pubkey, // this can be struct MessageContext { sender } but this is currently ok + data: Vec, + }, +} + +impl CallableInstruction { + pub fn pack(&self) -> Vec { + let mut buf; + match self { + CallableInstruction::OnCall { sender, data } => { + let data_len = data.len(); + buf = Vec::with_capacity(41 + data_len); // 41 = 8 (discriminator) + 32 (sender pubkey) + 1 (data length prefix) + + // NOTE: for program to know how to handle instruction after deserialization, discriminator is added + // anchor makes discriminator using hash("global:instruction_name") so every contract with on_call instruction should have same discriminator + // in case native development is used in target contract, that can be the problem, but probably they can define on_call instruction in this discriminator? + buf.extend_from_slice(&[16, 136, 66, 32, 254, 40, 181, 8]); + buf.extend_from_slice(&sender.to_bytes()); + buf.extend_from_slice(&data_len.to_le_bytes()); // have to put length of array so it can be deserialized properly + buf.extend_from_slice(data); + } + } + buf + } +} + + #[program] pub mod gateway { use super::*; @@ -287,6 +322,45 @@ pub mod gateway { Ok(()) } + + pub fn execute( + ctx: Context, + sender: Pubkey, + data: Vec, + ) -> Result<()> { + let pda = &mut ctx.accounts.pda; + require!(!pda.deposit_paused, Errors::DepositPaused); + + // NOTE: have to manually create Instruction, pack it and invoke since there is no crate for contract + // since any contract with on_call instruction can be called + let instruction_data = CallableInstruction::OnCall { + sender, + data, + } + .pack(); + + // NOTE: calling function in other program without passing accounts seems very limitting in what can be done + // every account that instruction interacts with has to be predetermined and set before the call, and various callable contracts might have different behavior and need different accounts + // also if there is account sent here, we might need to use invoke_signed instead of invoke which also seems not secure with these arbitrary CPIs + + // should we maybe predefine some accounts that can be used in every callable program, or just call without accounts which is really limitting? + let ix = Instruction { + program_id: ctx.accounts.destination_program.key(), + accounts: vec![], + data: instruction_data, + }; + + // NOTE: one more point is that we are doing arbitrary CPI here without checks about target program, which should be fine if we dont send any accounts, but if we decide to send, might be a problem + invoke( + &ix, + &[], + )?; + + + msg!("execute successfully"); + + Ok(()) + } } fn recover_eth_address( @@ -343,6 +417,15 @@ pub struct DepositSplToken<'info> { pub to: Account<'info, TokenAccount>, // this must be ATA of PDA } +#[derive(Accounts)] +pub struct Execute<'info> { + #[account(mut)] + pub signer: Signer<'info>, + #[account(mut)] + pub pda: Account<'info, Pda>, + pub destination_program: AccountInfo<'info>, +} + #[derive(Accounts)] pub struct Withdraw<'info> { #[account(mut)] diff --git a/tests/protocol-contracts-solana.ts b/tests/protocol-contracts-solana.ts index 33c209b..3a00caa 100644 --- a/tests/protocol-contracts-solana.ts +++ b/tests/protocol-contracts-solana.ts @@ -9,8 +9,7 @@ import { keccak256 } from 'ethereumjs-util'; import { bufferToHex } from 'ethereumjs-util'; import {expect} from 'chai'; import {ecdsaRecover} from 'secp256k1'; - - +import { CallableTest } from "../target/types/callable_test"; const ec = new EC('secp256k1'); // const keyPair = ec.genKeyPair(); @@ -22,6 +21,8 @@ describe("some tests", () => { anchor.setProvider(anchor.AnchorProvider.env()); const conn = anchor.getProvider().connection; const gatewayProgram = anchor.workspace.Gateway as Program; + const callableProgram = anchor.workspace.CallableTest as Program; + const wallet = anchor.workspace.Gateway.provider.wallet.payer; const mint = anchor.web3.Keypair.generate(); let tokenAccount: spl.Account; @@ -71,6 +72,26 @@ describe("some tests", () => { } }); + it("Calls execute and onCall", async () => { + await callableProgram.methods.initialize().rpc(); + + // Define the sender's public key and the arbitrary data to pass + const senderPubkey = wallet.publicKey; + const data = keccak256(Buffer.from("hello")); + + // Call the `execute` function in the gateway program + const tx = await gatewayProgram.methods + .execute(senderPubkey, data) + .accounts({ + pda: pdaAccount, + destinationProgram: callableProgram.programId, // Pass the callable program's ID + signer: wallet.publicKey, // The signer of the transaction + }) + .rpc(); + + console.log("Transaction signature:", tx); + }); + it("Mint a SPL USDC token", async () => { // now deploying a fake USDC SPL Token // 1. create a mint account