-
Notifications
You must be signed in to change notification settings - Fork 53
Forwarder #1179
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
Forwarder #1179
Changes from all commits
4f3d2d8
14e7688
cacc3ab
1c6540d
c7af7dd
96669af
e92341f
5c05440
b75388f
708dd05
1e2950b
3334a12
45e3cee
5cc9075
d17ee7b
2a56634
579ed0e
1bb2e38
9842523
a058ce9
98ef3f4
ddf6fc0
b3766fe
9872702
a3c9347
a1dec90
6442d0e
92be3de
57dae86
252c1f0
53e625c
520c659
5be4032
5d333be
1a9ad5f
e6cb88b
8c32207
895411e
bd05537
df1f57c
571946a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
[package] | ||
name = "dummy-receiver" | ||
version = "0.1.0" | ||
description = "Created with Anchor" | ||
edition = "2021" | ||
|
||
[lib] | ||
crate-type = ["cdylib", "lib"] | ||
name = "dummy_receiver" | ||
|
||
[features] | ||
no-entrypoint = [] | ||
no-idl = [] | ||
no-log-ix-name = [] | ||
cpi = ["no-entrypoint"] | ||
default = [] | ||
|
||
[dependencies] | ||
anchor-lang = "0.29.0" | ||
keystone-forwarder = { version = "0.1.0", path = "../keystone-forwarder", default-features = false, features = ["cpi"] } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
[target.bpfel-unknown-unknown.dependencies.std] | ||
features = [] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
use anchor_lang::prelude::*; | ||
use keystone_forwarder::ForwarderState; | ||
use keystone_forwarder::ID as FORWARDER_ID; | ||
|
||
declare_id!("5z38tFCAmcPJb1DXUHSoKQhR8qQ8o9aNZ8rZFWe6gH4L"); | ||
|
||
// THIS IS UN-AUDITED CODE USED FOR TESTING PURPOSES ONLY | ||
// DO NOT USE THIS CODE IN PRODUCTION. | ||
|
||
#[program] | ||
pub mod dummy_receiver { | ||
use super::*; | ||
|
||
pub fn initialize(ctx: Context<Initialize>) -> Result<()> { | ||
ctx.accounts.report_state.forwarder_authority = ctx.accounts.forwarder_authority.key(); | ||
Ok(()) | ||
} | ||
|
||
pub fn on_report<'info>( | ||
ctx: Context<'_, '_, 'info, 'info, OnReport<'info>>, | ||
metadata: Vec<u8>, | ||
report: Vec<u8>, | ||
) -> Result<()> { | ||
// verify | ||
// 1. forwarder authority signer belongs to (is a PDA derived from) forwarder state (done in the anchor constraint!) | ||
// 2. forwarder authority signer is authorized by this program | ||
// 3. report metadata (not done in this dummy example) | ||
|
||
// 2 | ||
require!( | ||
ctx.accounts.forwarder_authority.key() == ctx.accounts.report_state.forwarder_authority, | ||
AuthError::Unauthorized | ||
); | ||
|
||
// in a production setting you'd also want to verify the metadata too... | ||
|
||
ctx.accounts.report_state.report = report; | ||
ctx.accounts.report_state.metadata = metadata; | ||
|
||
// note: alternative account implementation could pass as ctx.remaining_accounts | ||
// however that requires more work | ||
|
||
// let account_info = &ctx.remaining_accounts[0]; | ||
// let mut latest_report: Account<'info, LatestReport> = Account::try_from(account_info)?; | ||
// latest_report.metadata = metadata; | ||
// latest_report.report = report; | ||
|
||
// // includes anchor discriminator by default | ||
// latest_report.try_serialize(&mut &mut account_info.data.borrow_mut()[..])?; | ||
|
||
Ok(()) | ||
} | ||
} | ||
|
||
#[error_code] | ||
pub enum AuthError { | ||
#[msg("The signer is unauthorized")] | ||
Unauthorized, | ||
} | ||
|
||
#[account] | ||
#[derive(Default)] | ||
pub struct LatestReport { | ||
pub metadata: Vec<u8>, | ||
pub report: Vec<u8>, | ||
pub forwarder_authority: Pubkey, | ||
} | ||
|
||
#[derive(Accounts)] | ||
pub struct Initialize<'info> { | ||
#[account( | ||
init, | ||
payer = signer, | ||
space = 8 + 4 + 4 + 65 + 32 // [64 (metadata) + 1 (report)] = 65 | ||
)] | ||
pub report_state: Account<'info, LatestReport>, | ||
|
||
#[account(mut)] | ||
pub signer: Signer<'info>, | ||
|
||
/// CHECK: this is the expected signer of "on_report" | ||
#[account()] | ||
// #[account(address = report_state.key() @ AuthError::Unauthorized)] | ||
pub forwarder_authority: UncheckedAccount<'info>, | ||
|
||
pub system_program: Program<'info, System>, | ||
} | ||
|
||
#[derive(Accounts)] | ||
pub struct OnReport<'info> { | ||
#[account(owner = FORWARDER_ID)] | ||
pub state: Account<'info, ForwarderState>, | ||
Comment on lines
+91
to
+92
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since you can initialize multiple forwarders with the same program, this receiver would allow inputs from any forwarder (an attacker could create a forwarder) potential solution is to create an allow list of forwarder states There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Although an attacker could potentially create a forwarder, it's up to the receiver program to verify that the instance is allowed. I guess I forgot to do that here so I'll need to add that. But it'd be a hardcoded thing exactly like you mention. The data feeds cache on EVM for example has a list of allowed callers. Good call out There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note that a receiver might want to support multiple forwarders writing to it - so an array (with |
||
|
||
// note the forwarder authority is a PDA signer | ||
#[account(seeds = [b"forwarder", state.key().as_ref()], bump = state.authority_nonce, seeds::program = FORWARDER_ID)] | ||
pub forwarder_authority: Signer<'info>, | ||
|
||
#[account(mut)] | ||
pub report_state: Account<'info, LatestReport>, | ||
// remaining accounts may be passed in | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
[package] | ||
name = "keystone-forwarder" | ||
version = "0.1.0" | ||
description = "Created with Anchor" | ||
edition = "2021" | ||
|
||
[lib] | ||
crate-type = ["cdylib", "lib"] | ||
name = "keystone_forwarder" | ||
|
||
[features] | ||
no-entrypoint = [] | ||
no-idl = [] | ||
no-log-ix-name = [] | ||
cpi = ["no-entrypoint"] | ||
default = [] | ||
|
||
[dependencies] | ||
anchor-lang = { version = "0.29.0", features = ["init-if-needed"] } | ||
solana-program = { version = "1.17.25" } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
[target.bpfel-unknown-unknown.dependencies.std] | ||
features = [] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
pub const STATE_VERSION: u8 = 1; | ||
|
||
pub const ANCHOR_DISCRIMINATOR: usize = 8; | ||
|
||
pub const REPORT_CONTEXT_LEN: usize = 96; | ||
|
||
// our don size is directly limited by the transaction size during on_report's signature verification | ||
pub const MAX_ORACLES: usize = 17; | ||
|
||
pub const SIGNATURE_LEN: usize = 65; | ||
|
||
pub const FORWARDER_METADATA_LENGTH: usize = 45; | ||
pub const METADATA_LENGTH: usize = 109; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
use crate::common::ANCHOR_DISCRIMINATOR; | ||
use crate::error::AuthError; | ||
use crate::state::{ExecutionState, ForwarderState, OraclesConfig}; | ||
use crate::utils::{extract_config_id, extract_raw_report, extract_transmission_id, get_config_id}; | ||
use anchor_lang::prelude::*; | ||
|
||
#[derive(Accounts)] | ||
pub struct Initialize<'info> { | ||
// the account is not a PDA but it is initialized by the program | ||
#[account( | ||
init, | ||
payer = owner, | ||
space = ANCHOR_DISCRIMINATOR + ForwarderState::INIT_SPACE | ||
)] | ||
pub state: Account<'info, ForwarderState>, | ||
#[account(mut)] | ||
pub owner: Signer<'info>, | ||
|
||
pub system_program: Program<'info, System>, | ||
} | ||
|
||
#[derive(Accounts)] | ||
pub struct TransferOwnership<'info> { | ||
#[account(mut)] | ||
pub state: Account<'info, ForwarderState>, | ||
|
||
#[account(address = state.owner @ AuthError::Unauthorized)] | ||
pub current_owner: Signer<'info>, | ||
} | ||
|
||
#[derive(Accounts)] | ||
pub struct AcceptOwnership<'info> { | ||
#[account(mut)] | ||
pub state: Account<'info, ForwarderState>, | ||
|
||
#[account(address = state.proposed_owner @ AuthError::Unauthorized)] | ||
pub proposed_owner: Signer<'info>, | ||
} | ||
|
||
#[derive(Accounts)] | ||
#[instruction(don_id: u32, config_version: u32, f: u8, signer_addresses: Vec<[u8; 20]>)] | ||
pub struct InitOraclesConfig<'info> { | ||
pub state: Account<'info, ForwarderState>, | ||
|
||
#[account( | ||
init, | ||
payer = owner, | ||
seeds = [b"config", state.key().as_ref(), &get_config_id(don_id, config_version).to_be_bytes()], | ||
bump, | ||
space = ANCHOR_DISCRIMINATOR + OraclesConfig::space_with_signers(signer_addresses.len()) | ||
)] | ||
pub oracles_config: Account<'info, OraclesConfig>, | ||
|
||
#[account(mut, address = state.owner @ AuthError::Unauthorized)] | ||
pub owner: Signer<'info>, // must be the same owner as the one in the state account | ||
|
||
pub system_program: Program<'info, System>, | ||
} | ||
|
||
#[derive(Accounts)] | ||
#[instruction(don_id: u32, config_version: u32, f: u8, signer_addresses: Vec<[u8; 20]>)] | ||
pub struct UpdateOraclesConfig<'info> { | ||
pub state: Account<'info, ForwarderState>, | ||
|
||
#[account( | ||
mut, | ||
seeds = [b"config", state.key().as_ref(), &get_config_id(don_id, config_version).to_be_bytes()], | ||
bump, | ||
realloc = ANCHOR_DISCRIMINATOR + OraclesConfig::space_with_signers(signer_addresses.len()), | ||
realloc::payer = owner, | ||
realloc::zero = true | ||
)] | ||
pub oracles_config: Account<'info, OraclesConfig>, | ||
|
||
#[account(mut, address = state.owner @ AuthError::Unauthorized)] | ||
pub owner: Signer<'info>, | ||
|
||
pub system_program: Program<'info, System>, | ||
} | ||
|
||
#[derive(Accounts)] | ||
#[instruction(don_id: u32, config_version: u32)] | ||
pub struct CloseOraclesConfig<'info> { | ||
pub state: Account<'info, ForwarderState>, | ||
|
||
#[account( | ||
mut, | ||
seeds = [b"config", state.key().as_ref(), &get_config_id(don_id, config_version).to_be_bytes()], | ||
bump, | ||
close = owner | ||
)] | ||
pub oracles_config: Account<'info, OraclesConfig>, | ||
|
||
#[account(mut, address = state.owner @ AuthError::Unauthorized)] | ||
pub owner: Signer<'info>, // must be the same owner as the one in the state account | ||
} | ||
|
||
#[derive(Accounts)] | ||
#[instruction(data: Vec<u8>)] | ||
pub struct Report<'info> { | ||
pub state: Account<'info, ForwarderState>, | ||
|
||
#[account( | ||
mut, | ||
seeds = [b"config", state.key().as_ref(), &extract_config_id(extract_raw_report(&data))], | ||
bump | ||
)] | ||
pub oracles_config: Account<'info, OraclesConfig>, | ||
|
||
#[account(mut)] | ||
pub transmitter: Signer<'info>, | ||
|
||
/// CHECK: This is a PDA | ||
#[account(seeds = [b"forwarder", state.key().as_ref()], bump = state.authority_nonce)] | ||
pub forwarder_authority: UncheckedAccount<'info>, | ||
|
||
// it is dependent on the state.key(), a predetermined bump, workflow execution id, config_id, report_id | ||
#[account( | ||
init_if_needed, | ||
payer = transmitter, | ||
space = ANCHOR_DISCRIMINATOR + ExecutionState::INIT_SPACE, | ||
seeds = [ | ||
b"execution_state", | ||
state.key().as_ref(), | ||
&extract_transmission_id(extract_raw_report(&data), receiver_program.key) | ||
], | ||
bump | ||
)] | ||
pub execution_state: Account<'info, ExecutionState>, | ||
|
||
#[account(executable)] | ||
/// CHECK: We don't use Program<> here since it can be any program, "executable" is enough | ||
pub receiver_program: UncheckedAccount<'info>, | ||
|
||
pub system_program: Program<'info, System>, | ||
// remaining accounts passed to receiver | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
have we considered a
0.31.1
upgrade for the repo?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't... i'll look into it though. If we did 0.31.1 repo wouldn't we need another audit for the old contracts? and then bump those up on mainnet?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EDIT: oh I assumed that anchor versions were tied to solana versions, but it seems they are mix and match. however i think an anchor version change could still potentially change change deployed bytecode.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah it would potentially require a lot more work - i don't know if it's worth doing it now, but just pointing out that it might be good to have
especially since the team is responsible for solana DF contracts now