Skip to content

Commit

Permalink
poc for authenticated calls using arbitrary cpi
Browse files Browse the repository at this point in the history
  • Loading branch information
skosito committed Oct 4, 2024
1 parent 98bd5c3 commit 187472a
Show file tree
Hide file tree
Showing 11 changed files with 211 additions and 2 deletions.
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
}
82 changes: 82 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 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 {
Expand All @@ -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<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,44 @@ pub mod gateway {

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
// 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,
};

invoke(
&ix,
&[],
)?;


msg!("execute successfully");

Ok(())
}
}

fn recover_eth_address(
Expand Down Expand Up @@ -343,6 +416,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)]
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

0 comments on commit 187472a

Please sign in to comment.