Skip to content
Merged
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
101 changes: 58 additions & 43 deletions chains/solana/contracts/programs/rmn-remote/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,69 +3,84 @@ use std::cell::Ref;
use anchor_lang::prelude::*;
use anchor_lang::Discriminator;

use crate::context::ANCHOR_DISCRIMINATOR;
use crate::state::{CodeVersion, Config};
use crate::RmnRemoteError;

#[derive(AnchorDeserialize, InitSpace, Debug)]
pub(super) struct ConfigV1 {
#[derive(AnchorSerialize, AnchorDeserialize, InitSpace, Debug)]
pub struct ConfigV2 {
// --- v1 fields ---
pub version: u8,
pub owner: Pubkey,

pub proposed_owner: Pubkey,
pub default_code_version: CodeVersion,
}

impl TryFrom<ConfigV1> for Config {
type Error = anchor_lang::error::Error;

fn try_from(v1: ConfigV1) -> std::result::Result<Config, Self::Error> {
require_eq!(
v1.version,
1, // this deserialization is only valid for v1
RmnRemoteError::InvalidInputsConfigAccount
);
// --- v2 fields ---
// Using max_len for INIT_SPACE calculation. At the time of this migration, there is a single entry here
#[max_len(1)]
pub event_authorities: Vec<Pubkey>,
}

Ok(Config {
version: 1,
owner: v1.owner,
proposed_owner: v1.proposed_owner,
default_code_version: v1.default_code_version,
event_authorities: vec![], // this is not part of the v1 data, it defaults to empty
})
impl From<ConfigV2> for Config {
fn from(v2: ConfigV2) -> Config {
Config {
version: 2,
owner: v2.owner,
proposed_owner: v2.proposed_owner,
default_code_version: v2.default_code_version,
event_authorities: v2.event_authorities,
curser: Pubkey::default(), // added in v3, defaults to empty (unset)
bump: 0, // added in v3, defaults to 0
}
}
}

pub(super) fn load_config(config: &AccountInfo<'_>) -> Result<Config> {
let borrowed_data = config.try_borrow_data()?;
let (discriminator, data) = borrowed_data.split_at(ANCHOR_DISCRIMINATOR);

require!(
Config::DISCRIMINATOR == discriminator,
RmnRemoteError::InvalidInputsConfigAccount
);
// version is the first byte of the data after the discriminator
let version_byte = borrowed_data[Config::DISCRIMINATOR.len()];

let version_byte = data[0]; // version is the first byte of the data (after the discriminator)
match version_byte {
2 => {
// this assumes a single entry in the event_authorities vec, which is true at the time of this migration,
// which will only be executed once.
const V2_SPACE: usize = Config::DISCRIMINATOR.len() + ConfigV2::INIT_SPACE;

const V1_SPACE: usize = ANCHOR_DISCRIMINATOR + ConfigV1::INIT_SPACE;
if config.data_len() == V1_SPACE && version_byte == 1 {
let config_v1 = load_config_v1_unchecked(borrowed_data)?;
return Config::try_from(config_v1);
}
require_eq!(
config.data_len(),
V2_SPACE,
RmnRemoteError::InvalidInputsConfigAccount
);
let old_config = load_old_config::<ConfigV2>(borrowed_data)?;
Ok(Config::from(old_config))
}
Config::LATEST_VERSION => {
const MIN_SPACE: usize = Config::DISCRIMINATOR.len() + Config::INIT_SPACE;

// Use >= for the size because the event_authorities vec makes the size variable
const V2_MIN_SPACE: usize = ANCHOR_DISCRIMINATOR + Config::INIT_SPACE;
if config.data_len() >= V2_MIN_SPACE && version_byte == 2 {
let mut data: &[u8] = &borrowed_data;
return Config::try_deserialize(&mut data);
require_gte!(
config.data_len(),
MIN_SPACE,
RmnRemoteError::InvalidInputsConfigAccount
);
let mut data: &[u8] = &borrowed_data;
Config::try_deserialize(&mut data)
}
_ => Err(RmnRemoteError::InvalidInputsConfigAccount.into()),
}

Err(RmnRemoteError::InvalidInputsConfigAccount.into())
}

pub(super) fn load_config_v1_unchecked(borrowed_data: Ref<&mut [u8]>) -> Result<ConfigV1> {
let (_discriminator, data) = borrowed_data.split_at(ANCHOR_DISCRIMINATOR);
let config_v1 = ConfigV1::deserialize(&mut &data[..])
.map_err(|_| RmnRemoteError::InvalidInputsConfigAccount)?;
Ok(config_v1)
pub(super) fn load_old_config<T>(borrowed_data: Ref<&mut [u8]>) -> Result<T>
where
T: AnchorDeserialize,
Config: From<T>,
{
let (discriminator, data) = borrowed_data.split_at(Config::DISCRIMINATOR.len());
require!(
Config::DISCRIMINATOR == discriminator,
RmnRemoteError::InvalidInputsConfigAccount
);
let old =
T::deserialize(&mut &data[..]).map_err(|_| RmnRemoteError::InvalidInputsConfigAccount)?;
Ok(old)
}
54 changes: 33 additions & 21 deletions chains/solana/contracts/programs/rmn-remote/src/context.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use anchor_lang::prelude::*;
use ccip_common::seed;

use crate::{program::RmnRemote, Config, CurseSubject, Curses, RmnRemoteError};
use crate::{
config::load_config, program::RmnRemote, Config, CurseSubject, Curses, RmnRemoteError,
};

/// Static space allocated to any account: must always be added to space calculations.
pub const ANCHOR_DISCRIMINATOR: usize = 8;
Expand All @@ -22,7 +24,7 @@ pub fn uninitialized(v: u8) -> bool {

/// Maximum acceptable config version accepted by this module: any accounts with higher
/// version numbers than this will be rejected.
pub const MAX_CONFIG_V: u8 = 2;
pub const MAX_CONFIG_V: u8 = 3;
pub const MAX_CURSES_V: u8 = 1;

#[derive(Accounts)]
Expand Down Expand Up @@ -69,8 +71,8 @@ pub struct UpdateConfig<'info> {
#[account(
mut,
seeds = [seed::CONFIG],
bump,
constraint = valid_version(config.version, MAX_CONFIG_V) @ RmnRemoteError::InvalidVersion,
bump = config.bump,
constraint = config.version == MAX_CONFIG_V @ RmnRemoteError::InvalidVersion,
)]
pub config: Account<'info, Config>,

Expand All @@ -92,8 +94,8 @@ pub struct UpdateEventAuthorities<'info> {
#[account(
mut,
seeds = [seed::CONFIG],
bump,
constraint = version_in_range(config.version, 2, MAX_CONFIG_V) @ RmnRemoteError::InvalidVersion,
bump = config.bump,
constraint = config.version == MAX_CONFIG_V @ RmnRemoteError::InvalidVersion,
realloc = ANCHOR_DISCRIMINATOR + Config::dynamic_len(new_event_authorities.len()),
realloc::payer = authority,
realloc::zero = false,
Expand All @@ -107,7 +109,7 @@ pub struct UpdateEventAuthorities<'info> {
}

#[derive(Accounts)]
pub struct MigrateConfigV1ToV2<'info> {
pub struct MigrateConfigV2ToV3<'info> {
#[account(
mut,
seeds = [seed::CONFIG],
Expand All @@ -129,8 +131,8 @@ pub struct AcceptOwnership<'info> {
#[account(
mut,
seeds = [seed::CONFIG],
bump,
constraint = valid_version(config.version, MAX_CONFIG_V) @ RmnRemoteError::InvalidVersion,
bump = config.bump,
constraint = config.version == MAX_CONFIG_V @ RmnRemoteError::InvalidVersion,
)]
pub config: Account<'info, Config>,

Expand All @@ -141,15 +143,22 @@ pub struct AcceptOwnership<'info> {

#[derive(Accounts)]
pub struct Curse<'info> {
/// CHECK: Unchecked by Anchor as we will be migrating the Config account in-place. Thus, we will be loading the
/// account manually to handle the different structs before and after migration., allowing for no-downtime migration.
#[account(
seeds = [seed::CONFIG],
bump,
constraint = valid_version(config.version, MAX_CONFIG_V) @ RmnRemoteError::InvalidVersion,
owner = crate::ID, // check it is initialized
)]
pub config: Account<'info, Config>,
pub config: UncheckedAccount<'info>,

// validate signer is registered admin
#[account(mut, address = config.owner @ RmnRemoteError::Unauthorized)]
#[account(
mut,
constraint = {
let config_data = load_config(&config).unwrap();
authority.key() == config_data.curser || authority.key() == config_data.owner
} @ RmnRemoteError::Unauthorized)
]
pub authority: Signer<'info>,

#[account(
Expand All @@ -168,15 +177,17 @@ pub struct Curse<'info> {

#[derive(Accounts)]
pub struct Uncurse<'info> {
/// CHECK: Unchecked by Anchor as we will be migrating the Config account in-place. Thus, we will be loading the
/// account manually to handle the different structs before and after migration., allowing for no-downtime migration.
#[account(
seeds = [seed::CONFIG],
bump,
constraint = valid_version(config.version, MAX_CONFIG_V) @ RmnRemoteError::InvalidVersion,
owner = crate::ID, // check it is initialized
)]
pub config: Account<'info, Config>,
pub config: UncheckedAccount<'info>,

// validate signer is registered admin
#[account(mut, address = config.owner @ RmnRemoteError::Unauthorized)]
#[account(mut, address = load_config(&config).unwrap().owner @ RmnRemoteError::Unauthorized)]
pub authority: Signer<'info>,

#[account(
Expand Down Expand Up @@ -207,8 +218,7 @@ pub struct InspectCurses<'info> {
bump,
owner = crate::ID, // check it is initialized, can be removed when using Account<'info, Config>
)]
/// CHECK: using UncheckedAccount to allow no-downtime during config upgrade, so load using load_config method.
/// After the upgrade is made, this can be changed to Account<'info, Config>.
/// CHECK: using UncheckedAccount to allow no-downtime during config upgrade.
pub config: UncheckedAccount<'info>,
}

Expand All @@ -217,12 +227,14 @@ pub struct CpiEvent<'info> {
#[account(
seeds = [seed::CONFIG],
bump,
constraint = version_in_range(config.version, 2, MAX_CONFIG_V) @ RmnRemoteError::InvalidVersion,
owner = crate::ID, // check it is initialized
)]
pub config: Account<'info, Config>,
/// CHECK: using UncheckedAccount to allow no-downtime during config upgrade, so load using load_config method.
/// After the upgrade is made, this can be changed to Account<'info, Config>.
pub config: UncheckedAccount<'info>,

#[account(
constraint = config.event_authorities.contains(authority.key) @ RmnRemoteError::Unauthorized,
constraint = load_config(&config).unwrap().event_authorities.contains(authority.key) @ RmnRemoteError::Unauthorized,
)]
pub authority: Signer<'info>,
}
5 changes: 5 additions & 0 deletions chains/solana/contracts/programs/rmn-remote/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,8 @@ pub struct SubjectUncursed {
pub struct EventAuthoritiesSet {
pub event_authorities: Vec<Pubkey>,
}

#[event]
pub struct CurserSet {
pub curser: Pubkey,
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
use anchor_lang::prelude::*;

use crate::context::{MigrateConfigV1ToV2, UpdateEventAuthorities};
use crate::context::{MigrateConfigV2ToV3, UpdateEventAuthorities};
use crate::state::CodeVersion;
use crate::{AcceptOwnership, Curse, CurseSubject, InspectCurses, Uncurse, UpdateConfig};

pub trait Public {
fn verify_not_cursed(&self, ctx: Context<InspectCurses>, subject: CurseSubject) -> Result<()>;

fn migrate_config_v1_to_v2(&self, ctx: Context<MigrateConfigV1ToV2>) -> Result<()>;
fn migrate_config_v2_to_v3(&self, ctx: Context<MigrateConfigV2ToV3>) -> Result<()>;
}

pub trait Admin {
Expand All @@ -29,4 +29,6 @@ pub trait Admin {
ctx: Context<UpdateEventAuthorities>,
new_event_authorities: Vec<Pubkey>,
) -> Result<()>;

fn set_curser(&self, ctx: Context<UpdateConfig>, curser: Pubkey) -> Result<()>;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use anchor_lang::prelude::*;

use crate::context::UpdateEventAuthorities;
use crate::event::EventAuthoritiesSet;
use crate::event::{CurserSet, EventAuthoritiesSet};
use crate::instructions::interfaces::Admin;
use crate::{
AcceptOwnership, CodeVersion, ConfigSet, Curse, CurseSubject, OwnershipTransferRequested,
Expand Down Expand Up @@ -98,4 +98,13 @@ impl Admin for Impl {

Ok(())
}

fn set_curser(&self, ctx: Context<UpdateConfig>, curser: Pubkey) -> Result<()> {
ctx.accounts.config.curser = curser;
emit!(CurserSet {
curser: ctx.accounts.config.curser,
});

Ok(())
}
}
Loading
Loading