Skip to main content

cow_settlement_interface/instruction/
transfer_authority.rs

1//! `TransferAuthority` instruction builder and parser.
2//!
3//! It transfers a [`Role`] stored in the state PDA to another account in a
4//! single step: the manager or the role's current holder names the new holder,
5//! and the role's holder is updated immediately.
6//!
7//! The instruction is role-agnostic: the [`Role`] is part of the instruction
8//! data, so the same instruction transfers any authority.
9
10use core::mem::size_of;
11
12use arrayref::array_refs;
13use solana_instruction::{AccountMeta, Instruction};
14use solana_program_error::ProgramError;
15use solana_pubkey::Pubkey;
16
17use crate::instruction::InstructionInputParsing;
18use crate::{Role, SettlementInstruction};
19
20/// Builder for a `TransferAuthority` instruction.
21///
22/// `signer` authorizes the transfer and must sign; it must be either the
23/// manager or the current holder of `role`. After execution, `new_authority`
24/// becomes the holder of `role`.
25///
26/// Wire format: `[discriminator=7, role (1 byte), new_authority (32 bytes)]`.
27/// Required accounts: `[signer (S), state_pda (W)]`.
28pub struct TransferAuthority {
29    pub program_id: Pubkey,
30    pub signer: Pubkey,
31    pub state_pda: Pubkey,
32    pub role: Role,
33    pub new_authority: Pubkey,
34}
35
36impl From<TransferAuthority> for Instruction {
37    fn from(builder: TransferAuthority) -> Self {
38        let mut data = vec![
39            SettlementInstruction::TransferAuthority.discriminator(),
40            builder.role.discriminator(),
41        ];
42        data.extend_from_slice(&builder.new_authority.to_bytes());
43        Instruction {
44            program_id: builder.program_id,
45            accounts: vec![
46                AccountMeta::new_readonly(builder.signer, true),
47                AccountMeta::new(builder.state_pda, false),
48            ],
49            data,
50        }
51    }
52}
53
54/// Parsed inputs of a `TransferAuthority` instruction.
55pub struct TransferAuthorityInput<'a, A> {
56    pub signer: &'a A,
57    pub state_pda: &'a A,
58    pub role: Role,
59    pub new_authority: Pubkey,
60}
61
62impl<'a, A> InstructionInputParsing<'a, A> for TransferAuthorityInput<'a, A> {
63    const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::TransferAuthority;
64
65    fn parse_body(instruction_data: &'a [u8], accounts: &'a [A]) -> Result<Self, ProgramError> {
66        let body: &[u8; 1 + size_of::<Pubkey>()] = instruction_data
67            .try_into()
68            .map_err(|_| ProgramError::InvalidInstructionData)?;
69        let (role, new_authority) = array_refs![body, 1, size_of::<Pubkey>()];
70        let role = Role::try_from(role[0])?;
71        let new_authority = Pubkey::new_from_array(*new_authority);
72
73        // Accounts: [signer (S), state_pda (W)].
74        let [signer, state_pda, ..] = accounts else {
75            return Err(ProgramError::NotEnoughAccountKeys);
76        };
77
78        Ok(Self {
79            signer,
80            state_pda,
81            role,
82            new_authority,
83        })
84    }
85}
86
87/// Test scaffolding for authority-transfer parsing and handling, shared by this
88/// crate's tests and the settlement program's via the `test-fixtures` feature.
89#[cfg(any(test, feature = "test-fixtures"))]
90pub mod fixtures {
91    use solana_address::Address;
92    use solana_instruction::Instruction;
93
94    use super::TransferAuthority;
95    use crate::Role;
96
97    /// Number of accounts the transfer instruction expects: the signing
98    /// authority and the state PDA.
99    pub const NUM_ACCOUNTS: usize = 2;
100
101    /// Byte offset of the role selector in the instruction data: it follows the
102    /// one-byte discriminator.
103    pub const ROLE_OFFSET: usize = 1;
104
105    /// `TransferAuthority` instruction data with placeholder addresses, for
106    /// failure cases where the actual addresses don't matter.
107    pub fn transfer_authority_data() -> Vec<u8> {
108        let zero = Address::default();
109        Instruction::from(TransferAuthority {
110            program_id: zero,
111            signer: zero,
112            state_pda: zero,
113            role: Role::Manager,
114            new_authority: zero,
115        })
116        .data
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::fixtures::{transfer_authority_data, NUM_ACCOUNTS, ROLE_OFFSET};
123    use super::*;
124    use crate::fixtures::pubkey_from_seed;
125    use crate::instruction::fixtures::{fake_account, fake_sequential_accounts};
126    use crate::instruction::tests::{assert_readonly_signer, assert_writable_nonsigner};
127    use solana_account_view::AccountView;
128
129    #[test]
130    fn transfer_authority_input_parses_valid_input() {
131        let program_id = pubkey_from_seed("program id");
132        let signer = fake_account(pubkey_from_seed("signer"));
133        let state_pda = fake_account(pubkey_from_seed("state pda"));
134        let role = Role::ReclaimAuthority;
135        let new_authority = pubkey_from_seed("new authority");
136
137        let data = Instruction::from(TransferAuthority {
138            program_id,
139            signer: *signer.address(),
140            state_pda: *state_pda.address(),
141            role,
142            new_authority,
143        })
144        .data;
145        let accounts = [signer, state_pda];
146
147        let TransferAuthorityInput {
148            signer: parsed_signer,
149            state_pda: parsed_state_pda,
150            role: parsed_role,
151            new_authority: parsed_new_authority,
152        } = TransferAuthorityInput::parse(&data, &accounts).expect("parse should succeed");
153
154        assert_eq!(parsed_signer.address(), signer.address());
155        assert_eq!(parsed_state_pda.address(), state_pda.address());
156        assert_eq!(parsed_role, role);
157        assert_eq!(parsed_new_authority, new_authority);
158    }
159
160    #[test]
161    fn transfer_authority_input_rejects_unknown_role() {
162        let mut data = transfer_authority_data();
163        data[ROLE_OFFSET] = 0xff; // role byte out of range
164        let accounts = fake_sequential_accounts::<NUM_ACCOUNTS>();
165        assert_eq!(
166            TransferAuthorityInput::parse(&data, &accounts).err(),
167            Some(ProgramError::InvalidInstructionData),
168        );
169    }
170
171    #[test]
172    fn transfer_authority_input_rejects_too_long_data() {
173        let mut data = transfer_authority_data();
174        data.push(0);
175        let accounts = fake_sequential_accounts::<NUM_ACCOUNTS>();
176        assert_eq!(
177            TransferAuthorityInput::parse(&data, &accounts).err(),
178            Some(ProgramError::InvalidInstructionData),
179        );
180    }
181
182    #[test]
183    fn transfer_authority_input_rejects_too_short_data() {
184        let mut data = transfer_authority_data();
185        data.pop();
186        let accounts = fake_sequential_accounts::<NUM_ACCOUNTS>();
187        assert_eq!(
188            TransferAuthorityInput::parse(&data, &accounts).err(),
189            Some(ProgramError::InvalidInstructionData),
190        );
191    }
192
193    #[test]
194    fn transfer_authority_input_rejects_missing_accounts() {
195        let data = transfer_authority_data();
196        let mut accounts: Vec<AccountView> = fake_sequential_accounts::<NUM_ACCOUNTS>().into();
197        accounts.pop();
198        assert_eq!(
199            TransferAuthorityInput::parse(&data, &accounts).err(),
200            Some(ProgramError::NotEnoughAccountKeys),
201        );
202    }
203
204    #[test]
205    fn instruction_data_has_expected_layout() {
206        let program_id = pubkey_from_seed("program id");
207        let signer = pubkey_from_seed("signer");
208        let state_pda = pubkey_from_seed("state pda");
209        let new_authority = pubkey_from_seed("new authority");
210
211        let Instruction { data, .. } = TransferAuthority {
212            program_id,
213            signer,
214            state_pda,
215            role: Role::Manager,
216            new_authority,
217        }
218        .into();
219
220        assert_eq!(data.len(), 1 + 1 + core::mem::size_of::<Pubkey>());
221        assert_eq!(
222            data[0],
223            SettlementInstruction::TransferAuthority.discriminator()
224        );
225        assert_eq!(data[ROLE_OFFSET], Role::Manager.discriminator());
226        assert_eq!(&data[2..], &new_authority.to_bytes());
227    }
228
229    #[test]
230    fn instruction_data_has_expected_accounts() {
231        let program_id = pubkey_from_seed("program id");
232        let signer = pubkey_from_seed("signer");
233        let state_pda = pubkey_from_seed("state pda");
234        let new_authority = pubkey_from_seed("new authority");
235
236        let Instruction { accounts, .. } = TransferAuthority {
237            program_id,
238            signer,
239            state_pda,
240            role: Role::Manager,
241            new_authority,
242        }
243        .into();
244
245        assert_eq!(accounts.len(), 2);
246        // The signer authorizes the transfer, the state PDA is written.
247        assert_readonly_signer(&accounts[0], signer);
248        assert_writable_nonsigner(&accounts[1], state_pda);
249    }
250}