1use {
2 crate::{
3 cpi::request::CpiRequest,
4 errors::{RuntimeError, RuntimeResult},
5 },
6 solana_account::Account,
7 solana_address::Address,
8 solana_instruction::AccountMeta,
9};
10
11pub fn check_privileges(
13 request: &CpiRequest,
14 caller_account_metas: &[AccountMeta],
15) -> RuntimeResult<()> {
16 for cpi_meta in &request.accounts {
17 let caller_meta = caller_account_metas
18 .iter()
19 .find(|m| m.pubkey == cpi_meta.pubkey);
20
21 match caller_meta {
22 Some(cm) => {
23 if cpi_meta.is_signer
25 && !cm.is_signer
26 && !request.signers.contains(&cpi_meta.pubkey)
27 {
28 return Err(RuntimeError::PrivilegeEscalation(
29 "signer".to_string(),
30 cpi_meta.pubkey.to_string(),
31 ));
32 }
33
34 if cpi_meta.is_writable && !cm.is_writable {
36 return Err(RuntimeError::PrivilegeEscalation(
37 "writable".to_string(),
38 cpi_meta.pubkey.to_string(),
39 ));
40 }
41 }
42 None => {
43 return Err(RuntimeError::MissingAccount(cpi_meta.pubkey.to_string()));
44 }
45 }
46 }
47 Ok(())
48}
49
50pub fn check_account_change(
52 callee_program_id: &Address,
53 pubkey: &Address,
54 account: &Account,
55 new_owner: &Address,
56 new_lamports: u64,
57 new_data: &[u8],
58) -> RuntimeResult<()> {
59 let is_owner = account.owner == *callee_program_id;
60
61 if *new_owner != account.owner && !is_owner {
63 return Err(RuntimeError::PrivilegeEscalation(
64 "non-owner changed owner".to_string(),
65 pubkey.to_string(),
66 ));
67 }
68
69 if new_lamports < account.lamports && !is_owner {
71 return Err(RuntimeError::ExternalAccountLamportSpend(
72 pubkey.to_string(),
73 ));
74 }
75
76 if new_data != account.data.as_slice() && !is_owner {
78 return Err(RuntimeError::PrivilegeEscalation(
79 "non-owner modified data".to_string(),
80 pubkey.to_string(),
81 ));
82 }
83
84 if account.executable && new_data != account.data.as_slice() {
86 return Err(RuntimeError::PrivilegeEscalation(
87 "executable account modified".to_string(),
88 pubkey.to_string(),
89 ));
90 }
91
92 Ok(())
93}
94
95#[cfg(test)]
96mod tests {
97 use {
98 super::*,
99 crate::cpi::request::{CpiAccountMeta, CpiRequest},
100 };
101
102 const PROGRAM_A: Address = Address::new_from_array([1u8; 32]);
103 const PROGRAM_B: Address = Address::new_from_array([2u8; 32]);
104 const ACCT_1: Address = Address::new_from_array([10u8; 32]);
105 const ACCT_2: Address = Address::new_from_array([11u8; 32]);
106 const PDA: Address = Address::new_from_array([20u8; 32]);
107
108 fn make_request(accounts: Vec<CpiAccountMeta>, signers: Vec<Address>) -> CpiRequest {
109 CpiRequest {
110 program_id: PROGRAM_B,
111 accounts,
112 data: vec![],
113 caller_accounts: vec![],
114 signers,
115 }
116 }
117
118 fn make_account(owner: Address, lamports: u64, data: &[u8]) -> Account {
119 Account {
120 lamports,
121 data: data.to_vec(),
122 owner,
123 executable: false,
124 rent_epoch: 0,
125 }
126 }
127
128 #[test]
131 fn check_privileges_ok() {
132 let request = make_request(
133 vec![CpiAccountMeta {
134 pubkey: ACCT_1,
135 is_signer: true,
136 is_writable: true,
137 }],
138 vec![],
139 );
140 let caller_metas = vec![AccountMeta {
141 pubkey: ACCT_1,
142 is_signer: true,
143 is_writable: true,
144 }];
145 assert!(check_privileges(&request, &caller_metas).is_ok());
146 }
147
148 #[test]
149 fn check_privileges_signer_escalation() {
150 let request = make_request(
151 vec![CpiAccountMeta {
152 pubkey: ACCT_1,
153 is_signer: true,
154 is_writable: false,
155 }],
156 vec![],
157 );
158 let caller_metas = vec![AccountMeta {
159 pubkey: ACCT_1,
160 is_signer: false,
161 is_writable: false,
162 }];
163 assert!(check_privileges(&request, &caller_metas).is_err());
164 }
165
166 #[test]
167 fn check_privileges_signer_via_pda() {
168 let request = make_request(
169 vec![CpiAccountMeta {
170 pubkey: PDA,
171 is_signer: true,
172 is_writable: false,
173 }],
174 vec![PDA],
175 );
176 let caller_metas = vec![AccountMeta {
177 pubkey: PDA,
178 is_signer: false,
179 is_writable: false,
180 }];
181 assert!(check_privileges(&request, &caller_metas).is_ok());
182 }
183
184 #[test]
185 fn check_privileges_writable_escalation() {
186 let request = make_request(
187 vec![CpiAccountMeta {
188 pubkey: ACCT_1,
189 is_signer: false,
190 is_writable: true,
191 }],
192 vec![],
193 );
194 let caller_metas = vec![AccountMeta {
195 pubkey: ACCT_1,
196 is_signer: false,
197 is_writable: false,
198 }];
199 assert!(check_privileges(&request, &caller_metas).is_err());
200 }
201
202 #[test]
203 fn check_privileges_missing_account() {
204 let request = make_request(
205 vec![CpiAccountMeta {
206 pubkey: ACCT_2,
207 is_signer: false,
208 is_writable: false,
209 }],
210 vec![],
211 );
212 let caller_metas = vec![AccountMeta {
213 pubkey: ACCT_1,
214 is_signer: false,
215 is_writable: false,
216 }];
217 assert!(check_privileges(&request, &caller_metas).is_err());
218 }
219
220 #[test]
223 fn check_account_change_owner_modifies_data() {
224 let acct = make_account(PROGRAM_A, 100, b"hello");
225 assert!(
226 check_account_change(&PROGRAM_A, &ACCT_1, &acct, &PROGRAM_A, 100, b"world").is_ok()
227 );
228 }
229
230 #[test]
231 fn check_account_change_non_owner_modifies_data() {
232 let acct = make_account(PROGRAM_A, 100, b"hello");
233 assert!(
234 check_account_change(&PROGRAM_B, &ACCT_1, &acct, &PROGRAM_A, 100, b"world").is_err()
235 );
236 }
237
238 #[test]
239 fn check_account_change_owner_changes_owner() {
240 let acct = make_account(PROGRAM_A, 100, b"");
241 assert!(check_account_change(&PROGRAM_A, &ACCT_1, &acct, &PROGRAM_B, 100, b"").is_ok());
242 }
243
244 #[test]
245 fn check_account_change_non_owner_changes_owner() {
246 let acct = make_account(PROGRAM_A, 100, b"");
247 assert!(check_account_change(&PROGRAM_B, &ACCT_1, &acct, &PROGRAM_B, 100, b"").is_err());
248 }
249
250 #[test]
251 fn check_account_change_owner_debits_lamports() {
252 let acct = make_account(PROGRAM_A, 100, b"");
253 assert!(check_account_change(&PROGRAM_A, &ACCT_1, &acct, &PROGRAM_A, 50, b"").is_ok());
254 }
255
256 #[test]
257 fn check_account_change_non_owner_debits_lamports() {
258 let acct = make_account(PROGRAM_A, 100, b"");
259 assert!(check_account_change(&PROGRAM_B, &ACCT_1, &acct, &PROGRAM_A, 50, b"").is_err());
260 }
261
262 #[test]
263 fn check_account_change_non_owner_credits_lamports() {
264 let acct = make_account(PROGRAM_A, 100, b"");
265 assert!(check_account_change(&PROGRAM_B, &ACCT_1, &acct, &PROGRAM_A, 200, b"").is_ok());
266 }
267
268 #[test]
269 fn check_account_change_executable_data_rejected() {
270 let mut acct = make_account(PROGRAM_A, 100, b"code");
271 acct.executable = true;
272 assert!(
273 check_account_change(&PROGRAM_A, &ACCT_1, &acct, &PROGRAM_A, 100, b"hack").is_err()
274 );
275 }
276
277 #[test]
278 fn check_account_change_no_modifications() {
279 let acct = make_account(PROGRAM_A, 100, b"data");
280 assert!(check_account_change(&PROGRAM_B, &ACCT_1, &acct, &PROGRAM_A, 100, b"data").is_ok());
281 }
282}