Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
878e5ed
Add Whitelist program
GuidoDipietro Dec 17, 2025
57af08c
Settler: Intent lifecycle
GuidoDipietro Dec 17, 2025
cc10fad
Settler: Proposal Lifecycle
GuidoDipietro Dec 17, 2025
cf0e619
Settler: Signatures
GuidoDipietro Dec 17, 2025
f5e81b9
Fix tests
GuidoDipietro Dec 17, 2025
2f91061
Merge branch 'solana/1-whitelist' into solana/2-settler-intent-lifecycle
GuidoDipietro Dec 17, 2025
f1545be
Trim tests to this PRs features
GuidoDipietro Dec 17, 2025
9bd0e0e
Rm signatures utils as not relevant in this PR
GuidoDipietro Dec 17, 2025
ea31118
Trim SDK to match this PRs features
GuidoDipietro Dec 17, 2025
0707a85
Trim tests to this PRs features
GuidoDipietro Dec 17, 2025
1b87e10
Merge branch 'solana/2-settler-intent-lifecycle' into solana/3-settle…
GuidoDipietro Dec 17, 2025
890d5de
Merge branch 'solana/3-settler-proposal-lifecycle' into solana/4-sett…
GuidoDipietro Dec 17, 2025
d05f128
Add remaining files from original PR
GuidoDipietro Dec 17, 2025
3d93df3
EVM: Deploy contracts to Ethereum (#51)
alavarello Dec 17, 2025
a20c769
chore: update readme
facuspagnuolo Dec 18, 2025
ea6b0fb
Check deadlines on macros instead of ix fn body consistently
GuidoDipietro Dec 22, 2025
fc51d1c
Refactor if-return-err to require
GuidoDipietro Dec 22, 2025
804e2a4
Code review: simplify set admin process
GuidoDipietro Dec 30, 2025
a8b019b
Code review: Remove updated_by and last_update from EntityRegistry
GuidoDipietro Dec 30, 2025
eff0a23
Code review: rename Whitelist to Controller/Allowlist
GuidoDipietro Dec 30, 2025
90c6e3c
Code review: rm EntityRegistry status and close when not in allowlist
GuidoDipietro Dec 30, 2025
5cfce2b
Several fixes in Controller code
GuidoDipietro Dec 30, 2025
4282a1f
Code review: address other comments
GuidoDipietro Dec 30, 2025
b918391
Code review: add license
GuidoDipietro Dec 30, 2025
44a93c6
Merge branch 'solana/1-whitelist' into solana/2-settler-intent-lifecycle
GuidoDipietro Dec 30, 2025
e916fa1
Code review: adapt Settler to Controller changes
GuidoDipietro Dec 30, 2025
a1eb61d
Code review: rename controller::GlobalSettings to ControllerSettings,…
GuidoDipietro Dec 30, 2025
5f8eb31
Code review: rm is_paused from SettlerSettings
GuidoDipietro Dec 30, 2025
f42465b
Merge branch 'solana/2-settler-intent-lifecycle' into solana/3-settle…
GuidoDipietro Dec 30, 2025
1039442
Code review: adapt Settler to match Controller changes
GuidoDipietro Dec 30, 2025
435b7dc
Rm unused is_paused flag
GuidoDipietro Dec 30, 2025
5e4eb54
Merge branch 'solana/2-settler-intent-lifecycle' into solana/3-settle…
GuidoDipietro Dec 30, 2025
b638369
Fix lint
GuidoDipietro Dec 30, 2025
3130209
Code review: rename proposal_creator to creator
GuidoDipietro Dec 30, 2025
efbfea9
Fix lint
GuidoDipietro Dec 30, 2025
876abc6
Merge branch 'solana/3-settler-proposal-lifecycle' into solana/4-sett…
GuidoDipietro Dec 30, 2025
1c1c0c9
Code review: adapt Settler for changes in Controller
GuidoDipietro Dec 30, 2025
e1ef9ed
Code review: several comments
GuidoDipietro Jan 5, 2026
c715467
Code review: context() pattern in tests
GuidoDipietro Jan 5, 2026
0d815fa
Merge branch 'solana/1-whitelist' into solana/2-settler-intent-lifecycle
GuidoDipietro Jan 5, 2026
40d2ad6
Fix lint and merge errors
GuidoDipietro Jan 5, 2026
89ddecc
Code review: several comments
GuidoDipietro Jan 5, 2026
38d0f18
Merge branch 'solana/2-settler-intent-lifecycle' into solana/3-settle…
GuidoDipietro Jan 5, 2026
4d99fd4
Merge branch 'solana/3-settler-proposal-lifecycle' into solana/4-sett…
GuidoDipietro Jan 5, 2026
ba1d144
Code review: change crate::controller to controller
GuidoDipietro Jan 5, 2026
571ea10
Merge branch 'solana/3-settler-proposal-lifecycle' into solana/4-sett…
GuidoDipietro Jan 5, 2026
0ff6529
(1) Add Controller program (#45)
GuidoDipietro Jan 6, 2026
e690d3c
Merge branch 'solana/settler' into solana/3-settler-proposal-lifecycle
GuidoDipietro Jan 6, 2026
1ac6503
Revert "Merge branch 'solana/settler' into solana/3-settler-proposal-…
GuidoDipietro Jan 6, 2026
9fd1cc8
Merge branch 'solana/settler' into solana/3-settler-proposal-lifecycle
GuidoDipietro Jan 6, 2026
391eb43
Correct controller.test.ts file
GuidoDipietro Jan 6, 2026
f7d2a12
Merge branch 'solana/3-settler-proposal-lifecycle' into solana/4-sett…
GuidoDipietro Jan 6, 2026
ce6dba5
Code review: remove batch capabilities from claim_stale_proposal
GuidoDipietro Jan 7, 2026
286e8de
Merge branch 'solana/3-settler-proposal-lifecycle' into solana/4-sett…
GuidoDipietro Jan 7, 2026
b869e79
Merge branch 'solana/settler' into solana/4-settler-signatures
GuidoDipietro Jan 9, 2026
80fed04
Code review: remove unused instruction
GuidoDipietro Jan 20, 2026
b79f20c
Code review: add_validator_sig EntityRegistry seed checks
GuidoDipietro Jan 21, 2026
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
77 changes: 77 additions & 0 deletions packages/svm/programs/settler/src/instructions/add_axia_sig.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
use anchor_lang::{
prelude::{instruction::Instruction, sysvar::instructions::get_instruction_relative, *},
solana_program::sysvar::instructions::ID as IX_ID,
};

use crate::{
controller::{self, accounts::EntityRegistry, types::EntityType},
errors::SettlerError,
state::Proposal,
utils::{check_ed25519_ix, get_args_from_ed25519_ix_data, Ed25519Args},
};

#[derive(Accounts)]
pub struct AddAxiaSig<'info> {
#[account(mut)]
pub solver: Signer<'info>,

#[account(
seeds = [b"entity-registry", &[EntityType::Solver as u8 + 1], solver.key().as_ref()],
bump = solver_registry.bump,
seeds::program = controller::ID,
)]
pub solver_registry: Box<Account<'info, EntityRegistry>>,

#[account(
seeds = [b"entity-registry", &[EntityType::Axia as u8 + 1], axia_registry.entity_pubkey.as_ref()],
bump = axia_registry.bump,
seeds::program = controller::ID,
)]
pub axia_registry: Box<Account<'info, EntityRegistry>>,

/// CHECK: Any proposal
#[account(
mut,
constraint = proposal.deadline > Clock::get()?.unix_timestamp as u64 @ SettlerError::ProposalIsExpired,
constraint = proposal.is_final @ SettlerError::ProposalIsNotFinal,
)]
pub proposal: Box<Account<'info, Proposal>>,

/// CHECK: The address check is needed because otherwise
/// the supplied Sysvar could be anything else.
#[account(address = IX_ID)]
pub ix_sysvar: AccountInfo<'info>,
}

pub fn add_axia_sig(ctx: Context<AddAxiaSig>) -> Result<()> {
let proposal = &mut ctx.accounts.proposal;

// NOP if already signed
if proposal.is_signed {
return Ok(());
}

// Get Ed25519 instruction
let ed25519_ix: Instruction = get_instruction_relative(-1, &ctx.accounts.ix_sysvar)?;
let ed25519_ix_args: Ed25519Args = get_args_from_ed25519_ix_data(&ed25519_ix.data)?;

// Verify correct program and accounts
check_ed25519_ix(&ed25519_ix)?;

// Verify correct message was signed
require!(
ed25519_ix_args.msg == proposal.key().as_array(),
SettlerError::SigVerificationFailed
);

// Verify pubkey is whitelisted Axia
require!(
ed25519_ix_args.pubkey == &ctx.accounts.axia_registry.entity_pubkey.to_bytes(),
SettlerError::AxiaNotAllowlisted
);

// Updates proposal as signed
proposal.is_signed = true;

Ok(())
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ pub struct AddInstructionsToProposal<'info> {
realloc::zero = true,
has_one = creator @ SettlerError::IncorrectProposalCreator
)]
// Any proposal

/// Any proposal
#[account(
constraint = proposal.deadline > Clock::get()?.unix_timestamp as u64 @ SettlerError::ProposalIsExpired,
constraint = !proposal.is_final @ SettlerError::ProposalIsFinal
)]
pub proposal: Box<Account<'info, Proposal>>,

pub system_program: Program<'info, System>,
Expand All @@ -29,12 +34,8 @@ pub fn add_instructions_to_proposal(
more_instructions: Vec<ProposalInstruction>,
finalize: bool,
) -> Result<()> {
let now = Clock::get()?.unix_timestamp as u64;
let proposal = &mut ctx.accounts.proposal;

require!(proposal.deadline > now, SettlerError::ProposalIsExpired);
require!(!proposal.is_final, SettlerError::ProposalIsFinal);

proposal.instructions.extend_from_slice(&more_instructions);

if finalize {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use anchor_lang::{
prelude::{instruction::Instruction, sysvar::instructions::get_instruction_relative, *},
solana_program::sysvar::instructions::ID as IX_ID,
};

use crate::{
controller::{self, accounts::EntityRegistry, types::EntityType},
errors::SettlerError,
state::Intent,
utils::{check_ed25519_ix, get_args_from_ed25519_ix_data, Ed25519Args},
};

#[derive(Accounts)]
pub struct AddValidatorSig<'info> {
#[account(mut)]
pub solver: Signer<'info>,

#[account(
seeds = [b"entity-registry", &[EntityType::Solver as u8 + 1], solver.key().as_ref()],
bump = solver_registry.bump,
seeds::program = controller::ID,
)]
pub solver_registry: Box<Account<'info, EntityRegistry>>,

// Any Intent
#[account(
mut,
constraint = intent.deadline > Clock::get()?.unix_timestamp as u64 @ SettlerError::IntentIsExpired,
constraint = intent.is_final @ SettlerError::IntentIsNotFinal
)]
pub intent: Box<Account<'info, Intent>>,

#[account(
seeds = [b"fulfilled-intent", intent.hash.as_ref()],
bump
)]
/// This PDA must be uninitialized
pub fulfilled_intent: SystemAccount<'info>,

#[account(
seeds = [b"entity-registry", &[EntityType::Validator as u8 + 1], validator_registry.entity_pubkey.as_ref()],
bump = validator_registry.bump,
seeds::program = controller::ID,
)]
pub validator_registry: Box<Account<'info, EntityRegistry>>,

/// CHECK: The address check is needed because otherwise
/// the supplied Sysvar could be anything else.
#[account(address = IX_ID)]
pub ix_sysvar: AccountInfo<'info>,
}

pub fn add_validator_sig(ctx: Context<AddValidatorSig>) -> Result<()> {
let intent = &mut ctx.accounts.intent;

// Get Ed25519 instruction
let ed25519_ix: Instruction = get_instruction_relative(-1, &ctx.accounts.ix_sysvar)?;
let ed25519_ix_args: Ed25519Args = get_args_from_ed25519_ix_data(&ed25519_ix.data)?;

// Verify correct program and accounts
check_ed25519_ix(&ed25519_ix)?;

// Verify correct message was signed
require!(
ed25519_ix_args.msg == intent.hash,
SettlerError::SigVerificationFailed
);

// Verify pubkey is a whitelisted Validator
require_keys_eq!(
ctx.accounts.validator_registry.entity_pubkey,
Pubkey::new_from_array(*ed25519_ix_args.pubkey),
SettlerError::ValidatorNotAllowlisted,
);

// Updates intent PDA if signature not present and min_validations not met

if intent.validators.len() == intent.min_validations as usize {
return Ok(());
}

let ed25519_pubkey = Pubkey::try_from_slice(ed25519_ix_args.pubkey)?;

if intent.validators.contains(&ed25519_pubkey) {
return Ok(());
}

intent.validators.push(ed25519_pubkey);

Ok(())
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub struct ClaimStaleIntent<'info> {
mut,
close = creator,
has_one = creator @ SettlerError::IncorrectIntentCreator,
constraint = Clock::get()?.unix_timestamp as u64 > intent.deadline @ SettlerError::IntentNotYetExpired
constraint = intent.deadline < Clock::get()?.unix_timestamp as u64 @ SettlerError::IntentNotYetExpired
)]
pub intent: Box<Account<'info, Intent>>,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub struct ClaimStaleProposal<'info> {
mut,
close = creator,
has_one = creator @ SettlerError::IncorrectProposalCreator,
constraint = Clock::get()?.unix_timestamp as u64 > proposal.deadline @ SettlerError::ProposalNotYetExpired
constraint = proposal.deadline < Clock::get()?.unix_timestamp as u64 @ SettlerError::ProposalNotYetExpired
)]
pub proposal: Box<Account<'info, Proposal>>,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub struct ExecuteProposal<'info> {
has_one = intent @ SettlerError::IncorrectIntentForProposal,
constraint = proposal.creator == proposal_creator.key() @ SettlerError::IncorrectProposalCreator,
constraint = proposal.is_signed @ SettlerError::ProposalIsNotSigned,
constraint = proposal.deadline > Clock::get()?.unix_timestamp as u64 @ SettlerError::ProposalIsExpired,
close = proposal_creator
)]
pub proposal: Box<Account<'info, Proposal>>,
Expand Down Expand Up @@ -56,12 +57,8 @@ pub struct ExecuteProposal<'info> {
}

pub fn execute_proposal(ctx: Context<ExecuteProposal>) -> Result<()> {
let now = Clock::get()?.unix_timestamp as u64;
let proposal = &ctx.accounts.proposal;
let intent = &ctx.accounts.intent;

require!(proposal.deadline > now, SettlerError::ProposalIsExpired);

// TODO: Execute proposal

// TODO: Validate execution
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub struct ExtendIntent<'info> {
mut,
has_one = creator @ SettlerError::IncorrectIntentCreator,
constraint = !intent.is_final @ SettlerError::IntentIsFinal,
constraint = intent.deadline > Clock::get()?.unix_timestamp as u64 @ SettlerError::IntentIsExpired,
realloc =
Intent::extended_size(intent.to_account_info().data_len(), &more_data, &more_max_fees, &more_events)?,
realloc::payer = creator,
Expand Down
4 changes: 4 additions & 0 deletions packages/svm/programs/settler/src/instructions/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
pub mod add_axia_sig;
pub mod add_instructions_to_proposal;
pub mod add_validator_sig;
pub mod claim_stale_intent;
pub mod claim_stale_proposal;
pub mod create_intent;
Expand All @@ -7,7 +9,9 @@ pub mod execute_proposal;
pub mod extend_intent;
pub mod initialize;

pub use add_axia_sig::*;
pub use add_instructions_to_proposal::*;
pub use add_validator_sig::*;
pub use claim_stale_intent::*;
pub use claim_stale_proposal::*;
pub use create_intent::*;
Expand Down
8 changes: 8 additions & 0 deletions packages/svm/programs/settler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ use crate::{instructions::*, state::*, types::*};
pub mod settler {
use super::*;

pub fn add_axia_sig(ctx: Context<AddAxiaSig>) -> Result<()> {
instructions::add_axia_sig(ctx)
}

pub fn add_instructions_to_proposal(
ctx: Context<AddInstructionsToProposal>,
more_instructions: Vec<ProposalInstruction>,
Expand All @@ -24,6 +28,10 @@ pub mod settler {
instructions::add_instructions_to_proposal(ctx, more_instructions, finalize)
}

pub fn add_validator_sig(ctx: Context<AddValidatorSig>) -> Result<()> {
instructions::add_validator_sig(ctx)
}

pub fn claim_stale_intent(ctx: Context<ClaimStaleIntent>) -> Result<()> {
instructions::claim_stale_intent(ctx)
}
Expand Down
2 changes: 2 additions & 0 deletions packages/svm/programs/settler/src/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub mod math;
pub mod sigs;

pub use math::*;
pub use sigs::*;
75 changes: 75 additions & 0 deletions packages/svm/programs/settler/src/utils/sigs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
use anchor_lang::prelude::{instruction::Instruction, *};

use crate::errors::SettlerError;

pub fn check_ed25519_ix(ix: &Instruction) -> Result<()> {
if ix.program_id.to_string() != "Ed25519SigVerify111111111111111111111111111"
|| ix.accounts.len() != 0
{
return err!(SettlerError::SigVerificationFailed);
}

Ok(())
}

pub struct Ed25519Args<'a> {
pub pubkey: &'a [u8; 32],
pub sig: &'a [u8; 64],
pub msg: &'a [u8],
}

pub fn get_args_from_ed25519_ix_data(data: &[u8]) -> Result<Ed25519Args<'_>> {
if data.len() < 112 {
return err!(SettlerError::SigVerificationFailed);
}

// Header
let num_signatures = &[data[0]];
let padding = &[data[1]];
let signature_offset = &data[2..=3];
let signature_instruction_index = &data[4..=5];
let public_key_offset = &data[6..=7];
let public_key_instruction_index = &data[8..=9];
let message_data_offset = &data[10..=11];
let message_data_size = &data[12..=13];
let message_instruction_index = &data[14..=15];

// Data
let pubkey = &data[16..16 + 32];
let sig = &data[48..48 + 64];
let msg = &data[112..];

// Expected values
let exp_public_key_offset: u16 = 16; // 2*u8 + 7*u16
let exp_signature_offset: u16 = exp_public_key_offset + 32_u16;
let exp_message_data_offset: u16 = exp_signature_offset + 64_u16;
let exp_num_signatures: u8 = 1;
let exp_message_data_size: u16 = msg
.len()
.try_into()
.map_err(|_| SettlerError::SigVerificationFailed)?;

// Header
if num_signatures != &exp_num_signatures.to_le_bytes()
|| padding != &[0]
|| signature_offset != &exp_signature_offset.to_le_bytes()
|| signature_instruction_index != &u16::MAX.to_le_bytes()
|| public_key_offset != &exp_public_key_offset.to_le_bytes()
|| public_key_instruction_index != &u16::MAX.to_le_bytes()
|| message_data_offset != &exp_message_data_offset.to_le_bytes()
|| message_data_size != &exp_message_data_size.to_le_bytes()
|| message_instruction_index != &u16::MAX.to_le_bytes()
{
return err!(SettlerError::SigVerificationFailed);
}

Ok(Ed25519Args {
pubkey: pubkey
.try_into()
.map_err(|_| SettlerError::SigVerificationFailed)?,
sig: sig
.try_into()
.map_err(|_| SettlerError::SigVerificationFailed)?,
msg,
})
}
Loading