cow-settlement-interface 0.5.0

Account layouts, PDA derivations, and instruction builders for the CoW Protocol Solana settlement program.
Documentation
//! `RemoveSolver` instruction builder and parser.
//!
//! It removes a solver from the sorted solver list stored in the state PDA (see
//! [`crate::data::state`]). Only the solver authority may authorize it. The
//! state PDA shrinks by one solver and the freed rent is paid to
//! `rent_recipient`.

use core::mem::size_of;

use solana_instruction::{AccountMeta, Instruction};
use solana_program_error::ProgramError;
use solana_pubkey::Pubkey;

use crate::instruction::InstructionInputParsing;
use crate::SettlementInstruction;

/// Builder for a `RemoveSolver` instruction.
///
/// `authority` authorizes the change and must be the state PDA's current
/// solver authority; it signs but doesn't receive anything. `rent_recipient`
/// receives the freed rent. `solver` is removed from the sorted solver list;
/// removing one that isn't present fails.
///
/// Wire format: `[discriminator=9, solver (32 bytes)]`.
/// Required accounts: `[authority (S), rent_recipient (W), state_pda (W)]`.
pub struct RemoveSolver {
    pub program_id: Pubkey,
    pub authority: Pubkey,
    pub rent_recipient: Pubkey,
    pub state_pda: Pubkey,
    pub solver: Pubkey,
}

impl From<RemoveSolver> for Instruction {
    fn from(builder: RemoveSolver) -> Self {
        let mut data = vec![SettlementInstruction::RemoveSolver.discriminator()];
        data.extend_from_slice(&builder.solver.to_bytes());
        Instruction {
            program_id: builder.program_id,
            accounts: vec![
                AccountMeta::new_readonly(builder.authority, true),
                AccountMeta::new(builder.rent_recipient, false),
                AccountMeta::new(builder.state_pda, false),
            ],
            data,
        }
    }
}

/// Parsed inputs of a `RemoveSolver` instruction.
pub struct RemoveSolverInput<'a, A> {
    pub authority: &'a A,
    pub rent_recipient: &'a A,
    pub state_pda: &'a A,
    pub solver: Pubkey,
}

impl<'a, A> InstructionInputParsing<'a, A> for RemoveSolverInput<'a, A> {
    const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::RemoveSolver;

    fn parse_body(instruction_data: &[u8], accounts: &'a [A]) -> Result<Self, ProgramError> {
        let solver: &[u8; size_of::<Pubkey>()] = instruction_data
            .try_into()
            .map_err(|_| ProgramError::InvalidInstructionData)?;
        let solver = Pubkey::new_from_array(*solver);

        // Accounts: [authority (S), rent_recipient (W), state_pda (W)].
        let [authority, rent_recipient, state_pda, ..] = accounts else {
            return Err(ProgramError::NotEnoughAccountKeys);
        };

        Ok(Self {
            authority,
            rent_recipient,
            state_pda,
            solver,
        })
    }
}

/// Test scaffolding for `RemoveSolver` parsing and handling, shared by this
/// crate's tests and the settlement program's via the `test-fixtures` feature.
#[cfg(any(test, feature = "test-fixtures"))]
pub mod fixtures {
    use solana_address::Address;

    use super::{Instruction, RemoveSolver};

    /// Number of accounts `RemoveSolver` expects: solver authority, rent
    /// recipient, and state PDA.
    pub const NUM_ACCOUNTS: usize = 3;

    /// `RemoveSolver` instruction data with placeholder addresses, for failure
    /// cases where the actual addresses don't matter.
    pub fn remove_solver_data() -> Vec<u8> {
        let zero = Address::new_from_array([0; 32]);
        Instruction::from(RemoveSolver {
            program_id: zero,
            authority: zero,
            rent_recipient: zero,
            state_pda: zero,
            solver: zero,
        })
        .data
    }
}

#[cfg(test)]
mod tests {
    use super::fixtures::{remove_solver_data, NUM_ACCOUNTS};
    use super::*;
    use crate::fixtures::pubkey_from_seed;
    use crate::instruction::fixtures::{fake_account, fake_sequential_accounts};
    use crate::instruction::tests::{assert_readonly_signer, assert_writable_nonsigner};
    use solana_account_view::AccountView;

    #[test]
    fn remove_solver_input_parses_valid_input() {
        let program_id = pubkey_from_seed("program id");
        let authority = fake_account(pubkey_from_seed("solver authority"));
        let rent_recipient = fake_account(pubkey_from_seed("rent recipient"));
        let state_pda = fake_account(pubkey_from_seed("state pda"));
        let solver = pubkey_from_seed("solver");

        let data = Instruction::from(RemoveSolver {
            program_id,
            authority: *authority.address(),
            rent_recipient: *rent_recipient.address(),
            state_pda: *state_pda.address(),
            solver,
        })
        .data;
        let accounts = [authority, rent_recipient, state_pda];

        let RemoveSolverInput {
            authority: parsed_authority,
            rent_recipient: parsed_rent_recipient,
            state_pda: parsed_state_pda,
            solver: parsed_solver,
        } = RemoveSolverInput::parse(&data, &accounts).expect("parse should succeed");

        assert_eq!(parsed_authority.address(), accounts[0].address());
        assert_eq!(parsed_rent_recipient.address(), accounts[1].address());
        assert_eq!(parsed_state_pda.address(), accounts[2].address());
        assert_eq!(parsed_solver, solver);
    }

    #[test]
    fn remove_solver_input_rejects_long_data() {
        let mut data = remove_solver_data();
        data.push(0); // trailing byte
        let accounts = fake_sequential_accounts::<NUM_ACCOUNTS>();
        assert_eq!(
            RemoveSolverInput::parse(&data, &accounts).err(),
            Some(ProgramError::InvalidInstructionData),
        );
    }

    #[test]
    fn remove_solver_input_rejects_short_data() {
        let mut data = remove_solver_data();
        data.pop(); // one byte short
        let accounts = fake_sequential_accounts::<NUM_ACCOUNTS>();
        assert_eq!(
            RemoveSolverInput::parse(&data, &accounts).err(),
            Some(ProgramError::InvalidInstructionData),
        );
    }

    #[test]
    fn remove_solver_input_rejects_missing_accounts() {
        let data = remove_solver_data();
        let mut accounts: Vec<AccountView> = fake_sequential_accounts::<NUM_ACCOUNTS>().into();
        accounts.pop();
        assert_eq!(
            RemoveSolverInput::parse(&data, &accounts).err(),
            Some(ProgramError::NotEnoughAccountKeys),
        );
    }

    #[test]
    fn instruction_data_has_expected_layout() {
        let solver = pubkey_from_seed("solver");
        let Instruction { data, .. } = RemoveSolver {
            program_id: pubkey_from_seed("program id"),
            authority: pubkey_from_seed("solver authority"),
            rent_recipient: pubkey_from_seed("rent recipient"),
            state_pda: pubkey_from_seed("state pda"),
            solver,
        }
        .into();

        assert_eq!(data.len(), 1 + size_of::<Pubkey>());
        assert_eq!(data[0], SettlementInstruction::RemoveSolver.discriminator());
        assert_eq!(&data[1..], &solver.to_bytes());
    }

    #[test]
    fn instruction_data_regression() {
        let solver = Pubkey::new_from_array([0x11; 32]);
        let Instruction { data, .. } = RemoveSolver {
            program_id: pubkey_from_seed("program id"),
            authority: pubkey_from_seed("solver authority"),
            rent_recipient: pubkey_from_seed("rent recipient"),
            state_pda: pubkey_from_seed("state pda"),
            solver,
        }
        .into();

        #[rustfmt::skip]
        let expected: [u8; 1 + size_of::<Pubkey>()] = [
            // discriminator (RemoveSolver = 9)
            0x09,
            // solver
            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
        ];
        assert_eq!(data, expected);
    }

    #[test]
    fn instruction_has_expected_accounts() {
        let authority = pubkey_from_seed("solver authority");
        let rent_recipient = pubkey_from_seed("rent recipient");
        let state_pda = pubkey_from_seed("state pda");
        let Instruction { accounts, .. } = RemoveSolver {
            program_id: pubkey_from_seed("program id"),
            authority,
            rent_recipient,
            state_pda,
            solver: pubkey_from_seed("solver"),
        }
        .into();

        assert_eq!(accounts.len(), 3);
        // The solver authority authorizes the change; the freed rent is paid to
        // the recipient; the state PDA is shrunk and written.
        assert_readonly_signer(&accounts[0], authority);
        assert_writable_nonsigner(&accounts[1], rent_recipient);
        assert_writable_nonsigner(&accounts[2], state_pda);
    }
}