-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathundelegate.rs
336 lines (312 loc) · 12.3 KB
/
undelegate.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
use crate::consts::{EXTERNAL_UNDELEGATE_DISCRIMINATOR, RENT_FEES_PERCENTAGE};
use crate::error::DlpError;
use crate::processor::utils::loaders::{
load_initialized_delegation_metadata, load_initialized_delegation_record,
load_initialized_protocol_fees_vault, load_initialized_validator_fees_vault, load_owned_pda,
load_program, load_signer, load_uninitialized_pda,
};
use crate::processor::utils::pda::{close_pda, close_pda_with_fees, create_pda};
use crate::state::{DelegationMetadata, DelegationRecord};
use crate::{
commit_record_seeds_from_delegated_account, commit_state_seeds_from_delegated_account,
undelegate_buffer_seeds_from_delegated_account,
};
use borsh::to_vec;
use solana_program::instruction::{AccountMeta, Instruction};
use solana_program::msg;
use solana_program::program::{invoke, invoke_signed};
use solana_program::program_error::ProgramError;
use solana_program::rent::Rent;
use solana_program::system_instruction::transfer;
use solana_program::{
account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey, system_program,
};
/// Undelegate a delegated account
///
/// Accounts:
///
/// 0: `[signer]` the validator account
/// 1: `[writable]` the delegated account
/// 2: `[]` the owner program of the delegated account
/// 3: `[writable]` the undelegate buffer PDA we use to store the data temporarily
/// 4: `[]` the commit state PDA
/// 5: `[]` the commit record PDA
/// 6: `[writable]` the delegation record PDA
/// 7: `[writable]` the delegation metadata PDA
/// 8: `[]` the rent reimbursement account
/// 9: `[writable]` the protocol fees vault account
/// 10: `[writable]` the validator fees vault account
/// 11: `[]` the system program
///
/// Requirements:
///
/// - delegated account is owned by delegation program
/// - delegation record is initialized
/// - delegation metadata is initialized
/// - protocol fees vault is initialized
/// - validator fees vault is initialized
/// - commit state is uninitialized
/// - commit record is uninitialized
/// - delegated account is NOT undelegatable
/// - owner program account matches the owner in the delegation record
/// - rent reimbursement account matches the rent payer in the delegation metadata
///
/// Steps:
///
/// - Close the delegation metadata
/// - Close the delegation record
/// - If delegated account has no data, assign to prev owner (and stop here)
/// - If there's data, create an "undelegate_buffer" and store the data in it
/// - Close the original delegated account
/// - CPI to the original owner to re-open the PDA with the original owner and the new state
/// - CPI will be signed by the undelegation buffer PDA and will call the external program
/// using the discriminator EXTERNAL_UNDELEGATE_DISCRIMINATOR
/// - Verify that the new state is the same as the committed state
/// - Close the undelegation buffer PDA
pub fn process_undelegate(
_program_id: &Pubkey,
accounts: &[AccountInfo],
_data: &[u8],
) -> ProgramResult {
let [validator, delegated_account, owner_program, undelegate_buffer_account, commit_state_account, commit_record_account, delegation_record_account, delegation_metadata_account, rent_reimbursement, fees_vault, validator_fees_vault, system_program] =
accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
};
// Check accounts
load_signer(validator, "validator")?;
load_owned_pda(delegated_account, &crate::id(), "delegated account")?;
load_initialized_delegation_record(delegated_account, delegation_record_account, true)?;
load_initialized_delegation_metadata(delegated_account, delegation_metadata_account, true)?;
load_initialized_protocol_fees_vault(fees_vault, true)?;
load_initialized_validator_fees_vault(validator, validator_fees_vault, true)?;
load_program(system_program, system_program::id(), "system program")?;
// Make sure there is no pending commits to be finalized before this call
load_uninitialized_pda(
commit_state_account,
commit_state_seeds_from_delegated_account!(delegated_account.key),
&crate::id(),
false,
"commit state",
)?;
load_uninitialized_pda(
commit_record_account,
commit_record_seeds_from_delegated_account!(delegated_account.key),
&crate::id(),
false,
"commit record",
)?;
// Load delegation record
let delegation_record_data = delegation_record_account.try_borrow_data()?;
let delegation_record =
DelegationRecord::try_from_bytes_with_discriminator(&delegation_record_data)?;
// Check passed owner and owner stored in the delegation record match
if !delegation_record.owner.eq(owner_program.key) {
msg!(
"Expected delegation record owner to be {}, but got {}",
delegation_record.owner,
owner_program.key
);
return Err(ProgramError::InvalidAccountOwner);
}
// Load delegated account metadata
let delegation_metadata_data = delegation_metadata_account.try_borrow_data()?;
let delegation_metadata =
DelegationMetadata::try_from_bytes_with_discriminator(&delegation_metadata_data)?;
// Check if the delegated account is undelegatable
if !delegation_metadata.is_undelegatable {
msg!(
"delegation metadata ({}) indicates the account is not undelegatable",
delegation_metadata_account.key
);
return Err(DlpError::NotUndelegatable.into());
}
// Check if the rent payer is correct
if !delegation_metadata.rent_payer.eq(rent_reimbursement.key) {
msg!(
"Expected rent payer to be {}, but got {}",
delegation_metadata.rent_payer,
rent_reimbursement.key
);
return Err(DlpError::InvalidReimbursementAddressForDelegationRent.into());
}
// Dropping delegation references
drop(delegation_record_data);
drop(delegation_metadata_data);
// If there is no program to call CPI to, we can just assign the owner back and we're done
if delegated_account.data_is_empty() {
// TODO - we could also do this fast-path if the data was non-empty but zeroed-out
delegated_account.assign(owner_program.key);
process_delegation_cleanup(
delegation_record_account,
delegation_metadata_account,
rent_reimbursement,
fees_vault,
validator_fees_vault,
)?;
return Ok(());
}
// Initialize the undelegation buffer PDA
let undelegate_buffer_seeds: &[&[u8]] =
undelegate_buffer_seeds_from_delegated_account!(delegated_account.key);
let undelegate_buffer_bump: u8 = load_uninitialized_pda(
undelegate_buffer_account,
undelegate_buffer_seeds,
&crate::id(),
true,
"undelegate buffer",
)?;
create_pda(
undelegate_buffer_account,
&crate::id(),
delegated_account.data_len(),
undelegate_buffer_seeds,
undelegate_buffer_bump,
system_program,
validator,
)?;
// Copy data in the undelegation buffer PDA
(*undelegate_buffer_account.try_borrow_mut_data()?)
.copy_from_slice(&delegated_account.try_borrow_data()?);
// Generate the ephemeral balance PDA's signer seeds
let undelegate_buffer_bump_slice = &[undelegate_buffer_bump];
let undelegate_buffer_signer_seeds =
[undelegate_buffer_seeds, &[undelegate_buffer_bump_slice]].concat();
// Call a CPI to the owner program to give it back the new state
process_undelegation_with_cpi(
validator,
delegated_account,
owner_program,
undelegate_buffer_account,
&undelegate_buffer_signer_seeds,
delegation_metadata,
system_program,
)?;
// Done, close undelegation buffer
close_pda(undelegate_buffer_account, validator)?;
// Closing delegation accounts
process_delegation_cleanup(
delegation_record_account,
delegation_metadata_account,
rent_reimbursement,
fees_vault,
validator_fees_vault,
)?;
Ok(())
}
/// 1. Close the delegated account
/// 2. CPI to the owner program
/// 3. Check state
/// 4. Settle lamports balance
#[allow(clippy::too_many_arguments)]
fn process_undelegation_with_cpi<'a, 'info>(
validator: &'a AccountInfo<'info>,
delegated_account: &'a AccountInfo<'info>,
owner_program: &'a AccountInfo<'info>,
undelegate_buffer_account: &'a AccountInfo<'info>,
undelegate_buffer_signer_seeds: &[&[u8]],
delegation_metadata: DelegationMetadata,
system_program: &'a AccountInfo<'info>,
) -> ProgramResult {
let delegated_account_lamports_before_close = delegated_account.lamports();
close_pda(delegated_account, validator)?;
// Invoke the owner program's post-undelegation IX, to give the state back to the original program
let validator_lamports_before_cpi = validator.lamports();
cpi_external_undelegate(
validator,
delegated_account,
undelegate_buffer_account,
undelegate_buffer_signer_seeds,
system_program,
owner_program.key,
delegation_metadata,
)?;
let validator_lamports_after_cpi = validator.lamports();
// Check that the validator lamports are exactly as expected
let delegated_account_min_rent = Rent::default().minimum_balance(delegated_account.data_len());
if validator_lamports_before_cpi
!= validator_lamports_after_cpi
.checked_add(delegated_account_min_rent)
.ok_or(DlpError::Overflow)?
{
return Err(DlpError::InvalidValidatorBalanceAfterCPI.into());
}
// Check that the owner program properly moved the state back into the original account during CPI
if delegated_account.try_borrow_data()?.as_ref()
!= undelegate_buffer_account.try_borrow_data()?.as_ref()
{
return Err(DlpError::InvalidAccountDataAfterCPI.into());
}
// Return the extra lamports to the delegated account
let delegated_account_extra_lamports = delegated_account_lamports_before_close
.checked_sub(delegated_account_min_rent)
.ok_or(DlpError::Overflow)?;
invoke(
&transfer(
validator.key,
delegated_account.key,
delegated_account_extra_lamports,
),
&[
validator.clone(),
delegated_account.clone(),
system_program.clone(),
],
)?;
Ok(())
}
/// CPI to the original owner program to re-open the PDA with the new state
fn cpi_external_undelegate<'a, 'info>(
payer: &'a AccountInfo<'info>,
delegated_account: &'a AccountInfo<'info>,
undelegate_buffer_account: &'a AccountInfo<'info>,
undelegate_buffer_signer_seeds: &[&[u8]],
system_program: &'a AccountInfo<'info>,
owner_program_id: &Pubkey,
delegation_metadata: DelegationMetadata,
) -> ProgramResult {
let mut data = EXTERNAL_UNDELEGATE_DISCRIMINATOR.to_vec();
let serialized_seeds = to_vec(&delegation_metadata.seeds)?;
data.extend_from_slice(&serialized_seeds);
let external_undelegate_instruction = Instruction {
program_id: *owner_program_id,
accounts: vec![
AccountMeta::new(*delegated_account.key, false),
AccountMeta::new(*undelegate_buffer_account.key, true),
AccountMeta::new(*payer.key, true),
AccountMeta::new_readonly(*system_program.key, false),
],
data,
};
invoke_signed(
&external_undelegate_instruction,
&[
delegated_account.clone(),
undelegate_buffer_account.clone(),
payer.clone(),
system_program.clone(),
],
&[undelegate_buffer_signer_seeds],
)
}
fn process_delegation_cleanup<'a, 'info>(
delegation_record_account: &'a AccountInfo<'info>,
delegation_metadata_account: &'a AccountInfo<'info>,
rent_reimbursement: &'a AccountInfo<'info>,
fees_vault: &'a AccountInfo<'info>,
validator_fees_vault: &'a AccountInfo<'info>,
) -> ProgramResult {
close_pda_with_fees(
delegation_record_account,
rent_reimbursement,
&[validator_fees_vault, fees_vault],
RENT_FEES_PERCENTAGE,
)?;
close_pda_with_fees(
delegation_metadata_account,
rent_reimbursement,
&[validator_fees_vault, fees_vault],
RENT_FEES_PERCENTAGE,
)?;
Ok(())
}