Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

authenticated calls using arbitrary cpi [poc and draft] #38

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.anchor/
/target
**/.DS_Store
node_modules/
test-ledger/
2 changes: 2 additions & 0 deletions Anchor.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ resolution = true
skip-lint = false

[programs.localnet]
callable-test = "HyJgPvyuHtXbVkttTbaBUU87sT7iPyHcD6UgUx82gEBq"
callable-test-2 = "B7apRShjWeCk2j64MFurBzjpnh5YYuNieMVkMZA7joVv"
protocol_contracts_solana = "ZETAjseVjuFsxdRxo6MmTCvqFwb3ZHUx56Co3vCmGis"

[registry]
Expand Down
14 changes: 14 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions programs/callable-test-2/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 2 additions & 0 deletions programs/callable-test-2/Xargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []
20 changes: 20 additions & 0 deletions programs/callable-test-2/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<OnCall>, sender: Pubkey, data: Vec<u8>) -> Result<()> {

Check warning on line 10 in programs/callable-test-2/src/lib.rs

View workflow job for this annotation

GitHub Actions / clippy

[clippy] programs/callable-test-2/src/lib.rs#L10

warning: unused variable: `ctx` --> programs/callable-test-2/src/lib.rs:10:20 | 10 | pub fn on_call(ctx: Context<OnCall>, sender: Pubkey, data: Vec<u8>) -> Result<()> { | ^^^ help: if this is intentional, prefix it with an underscore: `_ctx` | = note: `#[warn(unused_variables)]` on by default
Raw output
programs/callable-test-2/src/lib.rs:10:20:w:warning: unused variable: `ctx`
  --> programs/callable-test-2/src/lib.rs:10:20
   |
10 |     pub fn on_call(ctx: Context<OnCall>, sender: Pubkey, data: Vec<u8>) -> Result<()> {
   |                    ^^^ help: if this is intentional, prefix it with an underscore: `_ctx`
   |
   = note: `#[warn(unused_variables)]` on by default


__END__

Check warning on line 10 in programs/callable-test-2/src/lib.rs

View workflow job for this annotation

GitHub Actions / clippy

[clippy] programs/callable-test-2/src/lib.rs#L10

warning: unused variable: `sender` --> programs/callable-test-2/src/lib.rs:10:42 | 10 | pub fn on_call(ctx: Context<OnCall>, sender: Pubkey, data: Vec<u8>) -> Result<()> { | ^^^^^^ help: if this is intentional, prefix it with an underscore: `_sender`
Raw output
programs/callable-test-2/src/lib.rs:10:42:w:warning: unused variable: `sender`
  --> programs/callable-test-2/src/lib.rs:10:42
   |
10 |     pub fn on_call(ctx: Context<OnCall>, sender: Pubkey, data: Vec<u8>) -> Result<()> {
   |                                          ^^^^^^ help: if this is intentional, prefix it with an underscore: `_sender`


__END__

Check warning on line 10 in programs/callable-test-2/src/lib.rs

View workflow job for this annotation

GitHub Actions / clippy

[clippy] programs/callable-test-2/src/lib.rs#L10

warning: unused variable: `data` --> programs/callable-test-2/src/lib.rs:10:58 | 10 | pub fn on_call(ctx: Context<OnCall>, sender: Pubkey, data: Vec<u8>) -> Result<()> { | ^^^^ help: if this is intentional, prefix it with an underscore: `_data`
Raw output
programs/callable-test-2/src/lib.rs:10:58:w:warning: unused variable: `data`
  --> programs/callable-test-2/src/lib.rs:10:58
   |
10 |     pub fn on_call(ctx: Context<OnCall>, sender: Pubkey, data: Vec<u8>) -> Result<()> {
   |                                                          ^^^^ help: if this is intentional, prefix it with an underscore: `_data`


__END__

Ok(())
}
}

#[derive(Accounts)]
pub struct Initialize {}

#[derive(Accounts)]
pub struct OnCall {}
20 changes: 20 additions & 0 deletions programs/callable-test/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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" }
2 changes: 2 additions & 0 deletions programs/callable-test/Xargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []
24 changes: 24 additions & 0 deletions programs/callable-test/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use anchor_lang::prelude::*;

declare_id!("HhLWiKkriQSSZmu1Pfa2tkQD87HosDSFUqeuZKeEc88m");

#[program]
pub mod callable_test {
use super::*;

pub fn on_call(ctx: Context<OnCall>, sender: Pubkey, data: Vec<u8>) -> Result<()> {

Check warning on line 9 in programs/callable-test/src/lib.rs

View workflow job for this annotation

GitHub Actions / clippy

[clippy] programs/callable-test/src/lib.rs#L9

warning: unused variable: `ctx` --> programs/callable-test/src/lib.rs:9:20 | 9 | pub fn on_call(ctx: Context<OnCall>, sender: Pubkey, data: Vec<u8>) -> Result<()> { | ^^^ help: if this is intentional, prefix it with an underscore: `_ctx` | = note: `#[warn(unused_variables)]` on by default
Raw output
programs/callable-test/src/lib.rs:9:20:w:warning: unused variable: `ctx`
 --> programs/callable-test/src/lib.rs:9:20
  |
9 |     pub fn on_call(ctx: Context<OnCall>, sender: Pubkey, data: Vec<u8>) -> Result<()> {
  |                    ^^^ help: if this is intentional, prefix it with an underscore: `_ctx`
  |
  = note: `#[warn(unused_variables)]` on by default


__END__

Check warning on line 9 in programs/callable-test/src/lib.rs

View workflow job for this annotation

GitHub Actions / clippy

[clippy] programs/callable-test/src/lib.rs#L9

warning: unused variable: `sender` --> programs/callable-test/src/lib.rs:9:42 | 9 | pub fn on_call(ctx: Context<OnCall>, sender: Pubkey, data: Vec<u8>) -> Result<()> { | ^^^^^^ help: if this is intentional, prefix it with an underscore: `_sender`
Raw output
programs/callable-test/src/lib.rs:9:42:w:warning: unused variable: `sender`
 --> programs/callable-test/src/lib.rs:9:42
  |
9 |     pub fn on_call(ctx: Context<OnCall>, sender: Pubkey, data: Vec<u8>) -> Result<()> {
  |                                          ^^^^^^ help: if this is intentional, prefix it with an underscore: `_sender`


__END__

Check warning on line 9 in programs/callable-test/src/lib.rs

View workflow job for this annotation

GitHub Actions / clippy

[clippy] programs/callable-test/src/lib.rs#L9

warning: unused variable: `data` --> programs/callable-test/src/lib.rs:9:58 | 9 | pub fn on_call(ctx: Context<OnCall>, sender: Pubkey, data: Vec<u8>) -> Result<()> { | ^^^^ help: if this is intentional, prefix it with an underscore: `_data`
Raw output
programs/callable-test/src/lib.rs:9:58:w:warning: unused variable: `data`
 --> programs/callable-test/src/lib.rs:9:58
  |
9 |     pub fn on_call(ctx: Context<OnCall>, sender: Pubkey, data: Vec<u8>) -> Result<()> {
  |                                                          ^^^^ help: if this is intentional, prefix it with an underscore: `_data`


__END__
// 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<u8>, // Store the last used data
}
83 changes: 83 additions & 0 deletions programs/protocol-contracts-solana/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
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 {
Expand All @@ -25,10 +27,43 @@
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<u8>,
},
}

impl CallableInstruction {
pub fn pack(&self) -> Vec<u8> {
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::*;
Expand Down Expand Up @@ -287,6 +322,45 @@

Ok(())
}

pub fn execute(
ctx: Context<Execute>,
sender: Pubkey,
data: Vec<u8>,
) -> 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

Check warning on line 342 in programs/protocol-contracts-solana/src/lib.rs

View workflow job for this annotation

GitHub Actions / stable / typos

"limitting" should be "limiting".
// 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it is a set of data that could be potentially represented during the call on ZetaChain using a standard?

A bit like the idea of the Bitcoin memo standard.

User provide a generate bytes input, following a standard, this input is decoded to represent the full call on Solana.
This could be a generic field that we would need to other chain, it seems there will be lot of considerations to have as well for TON smart contract calls

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was not refering to data, but accounts. Accounts that solana programs interact with have to be determined before the call, that is how state is changed with stateless solana programs - state is maintained in accounts that program interacts with, not the program itself (difference with solidity where smart contracts contain state as well). Also important, program can not access those accounts when it executes transaction, the only way is that account need to be passed with instruction, basically preloaded before execution happens.

For example, if someone wants to have on_call(data) in their solana program, i would expect they want to somehow store that data, or modify existing data etc, but in order for program to access any state, account containing the state has to be sent inside instruction - in case of making generic calls to on_call not sure how we can pass any account there, maybe something generic, but not sure.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also important, program can not access those accounts when it executes transaction, the only way is that account need to be passed with instruction, basically preloaded before execution happens.

Yes, but this is a piece of information that could be passed in the gateway on ZetaChain?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it would have to be in call to gateway execute function already and then passed to on_call. But still not sure what we should send there.
@brewmaster012 what do you think about this, could you please check when you get a chance?

// 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?

Check warning on line 346 in programs/protocol-contracts-solana/src/lib.rs

View workflow job for this annotation

GitHub Actions / stable / typos

"limitting" should be "limiting".
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(
Expand Down Expand Up @@ -343,6 +417,15 @@
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)]
Expand Down
25 changes: 23 additions & 2 deletions tests/protocol-contracts-solana.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -22,6 +21,8 @@ describe("some tests", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const conn = anchor.getProvider().connection;
const gatewayProgram = anchor.workspace.Gateway as Program<Gateway>;
const callableProgram = anchor.workspace.CallableTest as Program<CallableTest>;

const wallet = anchor.workspace.Gateway.provider.wallet.payer;
const mint = anchor.web3.Keypair.generate();
let tokenAccount: spl.Account;
Expand Down Expand Up @@ -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
Expand Down
Loading