diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 8145b5b5..860e786e 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -360,6 +360,8 @@ jobs: path: tests/escrow - cmd: cd tests/pyth && anchor test --skip-lint && npx tsc --noEmit path: tests/pyth + - cmd: cd tests/realloc && anchor test --skip-lint && npx tsc --noEmit + path: tests/realloc - cmd: cd tests/system-accounts && anchor test --skip-lint path: tests/system-accounts - cmd: cd tests/misc && anchor test --skip-lint && npx tsc --noEmit diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b7dc62f..07a6d5b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,9 @@ The minor version will be incremented upon a breaking change and the patch versi ### Features +* lang: Add `realloc`, `realloc::payer`, and `realloc::zero` as a new constraint group for program accounts ([#1986](https://github.com/coral-xyz/anchor/pull/1986)). * lang: Add `PartialEq` and `Eq` for `anchor_lang::Error` ([#1544](https://github.com/coral-xyz/anchor/pull/1544)). -* cli: Add `--skip-build` to `anchor publish` ([#1786](https://github. -com/project-serum/anchor/pull/1841)). +* cli: Add `--skip-build` to `anchor publish` ([#1786](https://github.com/coral-xyz/anchor/pull/1841)). * cli: Add `--program-keypair` to `anchor deploy` ([#1786](https://github.com/coral-xyz/anchor/pull/1786)). * cli: Add compilation optimizations to cli template ([#1807](https://github.com/coral-xyz/anchor/pull/1807)). * cli: `build` now adds docs to idl. This can be turned off with `--no-docs` ([#1561](https://github.com/coral-xyz/anchor/pull/1561)). @@ -41,6 +41,7 @@ com/project-serum/anchor/pull/1841)). * ts: Change `BROWSER` env variable to `ANCHOR_BROWSER` ([#1233](https://github.com/coral-xyz/anchor/pull/1233)). * ts: Add transaction signature to `EventCallback` parameters ([#1851](https://github.com/coral-xyz/anchor/pull/1851)). * ts: Change `EventParser#parseLogs` implementation to be a generator instead of callback function ([#2018](https://github.com/coral-xyz/anchor/pull/2018)). +* lang: Adds a new `&mut reallocs: BTreeSet` argument to `Accounts::try_accounts` ([#1986](https://github.com/coral-xyz/anchor/pull/1986)). ## [0.24.2] - 2022-04-13 diff --git a/lang/derive/accounts/src/lib.rs b/lang/derive/accounts/src/lib.rs index 1c94ced5..28ca1fb5 100644 --- a/lang/derive/accounts/src/lib.rs +++ b/lang/derive/accounts/src/lib.rs @@ -44,6 +44,7 @@ use syn::parse_macro_input; /// /// - [Normal Constraints](#normal-constraints) /// - [SPL Constraints](#spl-constraints) +/// /// # Normal Constraints /// /// @@ -418,6 +419,48 @@ use syn::parse_macro_input; /// /// /// +/// +/// +/// +/// /// ///
+/// #[account(realloc = <space>, realloc::payer = <target>, realloc::zero = <bool>)] +/// +/// Used to realloc +/// program account space at the beginning of an instruction. +///

+/// The account must be marked as mut and applied to either Account or AccountLoader types. +///

+/// If the change in account data length is additive, lamports will be transferred from the realloc::payer into the +/// program account in order to maintain rent exemption. Likewise, if the change is subtractive, lamports will be transferred from +/// the program account back into the realloc::payer. +///

+/// The realloc::zero constraint is required in order to determine whether the new memory should be zero initialized after +/// reallocation. Please read the documentation on the AccountInfo::realloc function linked above to understand the +/// caveats regarding compute units when providing truefalse to this flag. +///

+/// The manual use of `AccountInfo::realloc` is discouraged in favor of the `realloc` constraint group due to the lack of native runtime checks +/// to prevent reallocation over the `MAX_PERMITTED_DATA_INCREASE` limit (which can unintentionally cause account data overwrite other accounts). +/// The constraint group also ensure account reallocation idempotency but checking and restricting duplicate account reallocation within a single ix. +///

+/// Example: +///
+/// #[derive(Accounts)]
+/// pub struct Example {
+///     #[account(mut)]
+///     pub payer: Signer<'info>,
+///     #[account(
+///         mut,
+///         seeds = [b"example"],
+///         bump,
+///         realloc = 8 + std::mem::size_of::() + 100,
+///         realloc::payer = payer,
+///         realloc::zero = false,
+///     )]
+///     pub acc: Account<'info, MyType>,
+///     pub system_program: Program<'info, System>,
+/// }
+///                 
+///
/// diff --git a/lang/src/accounts/account.rs b/lang/src/accounts/account.rs index 1900f1e6..9e53452c 100644 --- a/lang/src/accounts/account.rs +++ b/lang/src/accounts/account.rs @@ -10,7 +10,7 @@ use solana_program::account_info::AccountInfo; use solana_program::instruction::AccountMeta; use solana_program::pubkey::Pubkey; use solana_program::system_program; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::fmt; use std::ops::{Deref, DerefMut}; @@ -321,6 +321,7 @@ where accounts: &mut &[AccountInfo<'info>], _ix_data: &[u8], _bumps: &mut BTreeMap, + _reallocs: &mut BTreeSet, ) -> Result { if accounts.is_empty() { return Err(ErrorCode::AccountNotEnoughKeys.into()); diff --git a/lang/src/accounts/account_info.rs b/lang/src/accounts/account_info.rs index de23283a..54d6cc80 100644 --- a/lang/src/accounts/account_info.rs +++ b/lang/src/accounts/account_info.rs @@ -7,7 +7,7 @@ use crate::{Accounts, AccountsExit, Key, Result, ToAccountInfos, ToAccountMetas} use solana_program::account_info::AccountInfo; use solana_program::instruction::AccountMeta; use solana_program::pubkey::Pubkey; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; impl<'info> Accounts<'info> for AccountInfo<'info> { fn try_accounts( @@ -15,6 +15,7 @@ impl<'info> Accounts<'info> for AccountInfo<'info> { accounts: &mut &[AccountInfo<'info>], _ix_data: &[u8], _bumps: &mut BTreeMap, + _reallocs: &mut BTreeSet, ) -> Result { if accounts.is_empty() { return Err(ErrorCode::AccountNotEnoughKeys.into()); diff --git a/lang/src/accounts/account_loader.rs b/lang/src/accounts/account_loader.rs index 0432eb4c..73d8c22a 100644 --- a/lang/src/accounts/account_loader.rs +++ b/lang/src/accounts/account_loader.rs @@ -11,7 +11,7 @@ use solana_program::account_info::AccountInfo; use solana_program::instruction::AccountMeta; use solana_program::pubkey::Pubkey; use std::cell::{Ref, RefMut}; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::fmt; use std::io::Write; use std::marker::PhantomData; @@ -221,6 +221,7 @@ impl<'info, T: ZeroCopy + Owner> Accounts<'info> for AccountLoader<'info, T> { accounts: &mut &[AccountInfo<'info>], _ix_data: &[u8], _bumps: &mut BTreeMap, + _reallocs: &mut BTreeSet, ) -> Result { if accounts.is_empty() { return Err(ErrorCode::AccountNotEnoughKeys.into()); diff --git a/lang/src/accounts/boxed.rs b/lang/src/accounts/boxed.rs index 83ef8357..b143024e 100644 --- a/lang/src/accounts/boxed.rs +++ b/lang/src/accounts/boxed.rs @@ -17,7 +17,7 @@ use crate::{Accounts, AccountsClose, AccountsExit, Result, ToAccountInfos, ToAcc use solana_program::account_info::AccountInfo; use solana_program::instruction::AccountMeta; use solana_program::pubkey::Pubkey; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::ops::Deref; impl<'info, T: Accounts<'info>> Accounts<'info> for Box { @@ -26,8 +26,9 @@ impl<'info, T: Accounts<'info>> Accounts<'info> for Box { accounts: &mut &[AccountInfo<'info>], ix_data: &[u8], bumps: &mut BTreeMap, + reallocs: &mut BTreeSet, ) -> Result { - T::try_accounts(program_id, accounts, ix_data, bumps).map(Box::new) + T::try_accounts(program_id, accounts, ix_data, bumps, reallocs).map(Box::new) } } diff --git a/lang/src/accounts/cpi_account.rs b/lang/src/accounts/cpi_account.rs index 5f956789..ee7c9076 100644 --- a/lang/src/accounts/cpi_account.rs +++ b/lang/src/accounts/cpi_account.rs @@ -53,6 +53,7 @@ where accounts: &mut &[AccountInfo<'info>], _ix_data: &[u8], _bumps: &mut BTreeMap, + _reallocs: &mut BTreeSet, ) -> Result { if accounts.is_empty() { return Err(ErrorCode::AccountNotEnoughKeys.into()); diff --git a/lang/src/accounts/cpi_state.rs b/lang/src/accounts/cpi_state.rs index 401dabfa..42150d2c 100644 --- a/lang/src/accounts/cpi_state.rs +++ b/lang/src/accounts/cpi_state.rs @@ -8,7 +8,7 @@ use crate::{ use solana_program::account_info::AccountInfo; use solana_program::instruction::AccountMeta; use solana_program::pubkey::Pubkey; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::ops::{Deref, DerefMut}; /// Boxed container for the program state singleton, used when the state @@ -72,6 +72,7 @@ where accounts: &mut &[AccountInfo<'info>], _ix_data: &[u8], _bumps: &mut BTreeMap, + _reallocs: &mut BTreeSet, ) -> Result { if accounts.is_empty() { return Err(ErrorCode::AccountNotEnoughKeys.into()); diff --git a/lang/src/accounts/loader.rs b/lang/src/accounts/loader.rs index 516e5a98..1d52207d 100644 --- a/lang/src/accounts/loader.rs +++ b/lang/src/accounts/loader.rs @@ -9,7 +9,7 @@ use solana_program::account_info::AccountInfo; use solana_program::instruction::AccountMeta; use solana_program::pubkey::Pubkey; use std::cell::{Ref, RefMut}; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::fmt; use std::io::Write; use std::marker::PhantomData; @@ -163,6 +163,7 @@ impl<'info, T: ZeroCopy> Accounts<'info> for Loader<'info, T> { accounts: &mut &[AccountInfo<'info>], _ix_data: &[u8], _bumps: &mut BTreeMap, + _reallocs: &mut BTreeSet, ) -> Result { if accounts.is_empty() { return Err(ErrorCode::AccountNotEnoughKeys.into()); diff --git a/lang/src/accounts/program.rs b/lang/src/accounts/program.rs index fcdc0be5..d7e9975f 100644 --- a/lang/src/accounts/program.rs +++ b/lang/src/accounts/program.rs @@ -8,7 +8,7 @@ use solana_program::account_info::AccountInfo; use solana_program::bpf_loader_upgradeable::{self, UpgradeableLoaderState}; use solana_program::instruction::AccountMeta; use solana_program::pubkey::Pubkey; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::fmt; use std::marker::PhantomData; use std::ops::Deref; @@ -147,6 +147,7 @@ where accounts: &mut &[AccountInfo<'info>], _ix_data: &[u8], _bumps: &mut BTreeMap, + _reallocs: &mut BTreeSet, ) -> Result { if accounts.is_empty() { return Err(ErrorCode::AccountNotEnoughKeys.into()); diff --git a/lang/src/accounts/program_account.rs b/lang/src/accounts/program_account.rs index 312ad199..db6baffc 100644 --- a/lang/src/accounts/program_account.rs +++ b/lang/src/accounts/program_account.rs @@ -9,7 +9,7 @@ use crate::{ use solana_program::account_info::AccountInfo; use solana_program::instruction::AccountMeta; use solana_program::pubkey::Pubkey; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::ops::{Deref, DerefMut}; /// Boxed container for a deserialized `account`. Use this to reference any @@ -83,6 +83,7 @@ where accounts: &mut &[AccountInfo<'info>], _ix_data: &[u8], _bumps: &mut BTreeMap, + _reallocs: &mut BTreeSet, ) -> Result { if accounts.is_empty() { return Err(ErrorCode::AccountNotEnoughKeys.into()); diff --git a/lang/src/accounts/signer.rs b/lang/src/accounts/signer.rs index 2cec796c..7d757024 100644 --- a/lang/src/accounts/signer.rs +++ b/lang/src/accounts/signer.rs @@ -4,7 +4,7 @@ use crate::{Accounts, AccountsExit, Key, Result, ToAccountInfos, ToAccountMetas} use solana_program::account_info::AccountInfo; use solana_program::instruction::AccountMeta; use solana_program::pubkey::Pubkey; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::ops::Deref; /// Type validating that the account signed the transaction. No other ownership @@ -61,6 +61,7 @@ impl<'info> Accounts<'info> for Signer<'info> { accounts: &mut &[AccountInfo<'info>], _ix_data: &[u8], _bumps: &mut BTreeMap, + _reallocs: &mut BTreeSet, ) -> Result { if accounts.is_empty() { return Err(ErrorCode::AccountNotEnoughKeys.into()); diff --git a/lang/src/accounts/state.rs b/lang/src/accounts/state.rs index 45e506ff..563409fd 100644 --- a/lang/src/accounts/state.rs +++ b/lang/src/accounts/state.rs @@ -9,7 +9,7 @@ use crate::{ use solana_program::account_info::AccountInfo; use solana_program::instruction::AccountMeta; use solana_program::pubkey::Pubkey; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::ops::{Deref, DerefMut}; pub const PROGRAM_STATE_SEED: &str = "unversioned"; @@ -74,6 +74,7 @@ where accounts: &mut &[AccountInfo<'info>], _ix_data: &[u8], _bumps: &mut BTreeMap, + _reallocs: &mut BTreeSet, ) -> Result { if accounts.is_empty() { return Err(ErrorCode::AccountNotEnoughKeys.into()); diff --git a/lang/src/accounts/system_account.rs b/lang/src/accounts/system_account.rs index a47ceafc..8c90fd53 100644 --- a/lang/src/accounts/system_account.rs +++ b/lang/src/accounts/system_account.rs @@ -6,7 +6,7 @@ use solana_program::account_info::AccountInfo; use solana_program::instruction::AccountMeta; use solana_program::pubkey::Pubkey; use solana_program::system_program; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::ops::Deref; /// Type validating that the account is owned by the system program @@ -40,6 +40,7 @@ impl<'info> Accounts<'info> for SystemAccount<'info> { accounts: &mut &[AccountInfo<'info>], _ix_data: &[u8], _bumps: &mut BTreeMap, + _reallocs: &mut BTreeSet, ) -> Result { if accounts.is_empty() { return Err(ErrorCode::AccountNotEnoughKeys.into()); diff --git a/lang/src/accounts/sysvar.rs b/lang/src/accounts/sysvar.rs index e2e8336f..c955ff9c 100644 --- a/lang/src/accounts/sysvar.rs +++ b/lang/src/accounts/sysvar.rs @@ -5,7 +5,7 @@ use crate::{Accounts, AccountsExit, Key, Result, ToAccountInfos, ToAccountMetas} use solana_program::account_info::AccountInfo; use solana_program::instruction::AccountMeta; use solana_program::pubkey::Pubkey; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::fmt; use std::ops::{Deref, DerefMut}; @@ -71,6 +71,7 @@ impl<'info, T: solana_program::sysvar::Sysvar> Accounts<'info> for Sysvar<'info, accounts: &mut &[AccountInfo<'info>], _ix_data: &[u8], _bumps: &mut BTreeMap, + _reallocs: &mut BTreeSet, ) -> Result { if accounts.is_empty() { return Err(ErrorCode::AccountNotEnoughKeys.into()); diff --git a/lang/src/accounts/unchecked_account.rs b/lang/src/accounts/unchecked_account.rs index 73576196..5a001275 100644 --- a/lang/src/accounts/unchecked_account.rs +++ b/lang/src/accounts/unchecked_account.rs @@ -6,7 +6,7 @@ use crate::{Accounts, AccountsExit, Key, Result, ToAccountInfos, ToAccountMetas} use solana_program::account_info::AccountInfo; use solana_program::instruction::AccountMeta; use solana_program::pubkey::Pubkey; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::ops::Deref; /// Explicit wrapper for AccountInfo types to emphasize @@ -26,6 +26,7 @@ impl<'info> Accounts<'info> for UncheckedAccount<'info> { accounts: &mut &[AccountInfo<'info>], _ix_data: &[u8], _bumps: &mut BTreeMap, + _reallocs: &mut BTreeSet, ) -> Result { if accounts.is_empty() { return Err(ErrorCode::AccountNotEnoughKeys.into()); diff --git a/lang/src/error.rs b/lang/src/error.rs index 7de49c51..30d5a2d2 100644 --- a/lang/src/error.rs +++ b/lang/src/error.rs @@ -178,6 +178,12 @@ pub enum ErrorCode { /// 3015 - The given public key does not match the required sysvar #[msg("The given public key does not match the required sysvar")] AccountSysvarMismatch, + /// 3016 - The account reallocation exceeds the MAX_PERMITTED_DATA_INCREASE limit + #[msg("The account reallocation exceeds the MAX_PERMITTED_DATA_INCREASE limit")] + AccountReallocExceedsLimit, + /// 3017 - The account was duplicated for more than one reallocation + #[msg("The account was duplicated for more than one reallocation")] + AccountDuplicateReallocs, // State. /// 4000 - The given state account does not have the correct address diff --git a/lang/src/lib.rs b/lang/src/lib.rs index b269fe22..65147d87 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -27,7 +27,7 @@ use bytemuck::{Pod, Zeroable}; use solana_program::account_info::AccountInfo; use solana_program::instruction::AccountMeta; use solana_program::pubkey::Pubkey; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::io::Write; mod account_meta; @@ -82,6 +82,7 @@ pub trait Accounts<'info>: ToAccountMetas + ToAccountInfos<'info> + Sized { accounts: &mut &[AccountInfo<'info>], ix_data: &[u8], bumps: &mut BTreeMap, + reallocs: &mut BTreeSet, ) -> Result; } diff --git a/lang/src/vec.rs b/lang/src/vec.rs index 1036f674..44c132be 100644 --- a/lang/src/vec.rs +++ b/lang/src/vec.rs @@ -2,7 +2,7 @@ use crate::{Accounts, Result, ToAccountInfos, ToAccountMetas}; use solana_program::account_info::AccountInfo; use solana_program::instruction::AccountMeta; use solana_program::pubkey::Pubkey; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; impl<'info, T: ToAccountInfos<'info>> ToAccountInfos<'info> for Vec { fn to_account_infos(&self) -> Vec> { @@ -26,9 +26,11 @@ impl<'info, T: Accounts<'info>> Accounts<'info> for Vec { accounts: &mut &[AccountInfo<'info>], ix_data: &[u8], bumps: &mut BTreeMap, + reallocs: &mut BTreeSet, ) -> Result { let mut vec: Vec = Vec::new(); - T::try_accounts(program_id, accounts, ix_data, bumps).map(|item| vec.push(item))?; + T::try_accounts(program_id, accounts, ix_data, bumps, reallocs) + .map(|item| vec.push(item))?; Ok(vec) } } @@ -78,9 +80,11 @@ mod tests { Epoch::default(), ); let mut bumps = std::collections::BTreeMap::new(); + let mut reallocs = std::collections::BTreeSet::new(); let mut accounts = &[account1, account2][..]; let parsed_accounts = - Vec::::try_accounts(&program_id, &mut accounts, &[], &mut bumps).unwrap(); + Vec::::try_accounts(&program_id, &mut accounts, &[], &mut bumps, &mut reallocs) + .unwrap(); assert_eq!(accounts.len(), parsed_accounts.len()); } @@ -90,7 +94,9 @@ mod tests { fn test_accounts_trait_for_vec_empty() { let program_id = Pubkey::default(); let mut bumps = std::collections::BTreeMap::new(); + let mut reallocs = std::collections::BTreeSet::new(); let mut accounts = &[][..]; - Vec::::try_accounts(&program_id, &mut accounts, &[], &mut bumps).unwrap(); + Vec::::try_accounts(&program_id, &mut accounts, &[], &mut bumps, &mut reallocs) + .unwrap(); } } diff --git a/lang/syn/src/codegen/accounts/constraints.rs b/lang/syn/src/codegen/accounts/constraints.rs index 75f4a00f..d8286eb7 100644 --- a/lang/syn/src/codegen/accounts/constraints.rs +++ b/lang/syn/src/codegen/accounts/constraints.rs @@ -59,6 +59,7 @@ pub fn linearize(c_group: &ConstraintGroup) -> Vec { associated_token, token_account, mint, + realloc, } = c_group.clone(); let mut constraints = Vec::new(); @@ -69,6 +70,9 @@ pub fn linearize(c_group: &ConstraintGroup) -> Vec { if let Some(c) = init { constraints.push(Constraint::Init(c)); } + if let Some(c) = realloc { + constraints.push(Constraint::Realloc(c)); + } if let Some(c) = seeds { constraints.push(Constraint::Seeds(c)); } @@ -130,6 +134,7 @@ fn generate_constraint(f: &Field, c: &Constraint) -> proc_macro2::TokenStream { Constraint::AssociatedToken(c) => generate_constraint_associated_token(f, c), Constraint::TokenAccount(c) => generate_constraint_token_account(f, c), Constraint::Mint(c) => generate_constraint_mint(f, c), + Constraint::Realloc(c) => generate_constraint_realloc(f, c), } } @@ -320,6 +325,59 @@ pub fn generate_constraint_rent_exempt( } } +fn generate_constraint_realloc(f: &Field, c: &ConstraintReallocGroup) -> proc_macro2::TokenStream { + let field = &f.ident; + let account_name = field.to_string(); + let new_space = &c.space; + let payer = &c.payer; + let zero = &c.zero; + + quote! { + // Blocks duplicate account reallocs in a single instruction to prevent accidental account overwrites + // and to ensure the calculation of the change in bytes is based on account size at program entry + // which inheritantly guarantee idempotency. + if __reallocs.contains(&#field.key()) { + return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::AccountDuplicateReallocs).with_account_name(#account_name)); + } + + let __anchor_rent = anchor_lang::prelude::Rent::get()?; + let __field_info = #field.to_account_info(); + let __new_rent_minimum = __anchor_rent.minimum_balance(#new_space); + + let __delta_space = (::std::convert::TryInto::::try_into(#new_space).unwrap()) + .checked_sub(::std::convert::TryInto::try_into(__field_info.data_len()).unwrap()) + .unwrap(); + + if __delta_space != 0 { + if __delta_space > 0 { + if ::std::convert::TryInto::::try_into(__delta_space).unwrap() > anchor_lang::solana_program::entrypoint::MAX_PERMITTED_DATA_INCREASE { + return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::AccountReallocExceedsLimit).with_account_name(#account_name)); + } + + if __new_rent_minimum > __field_info.lamports() { + anchor_lang::system_program::transfer( + anchor_lang::context::CpiContext::new( + system_program.to_account_info(), + anchor_lang::system_program::Transfer { + from: #payer.to_account_info(), + to: __field_info.clone(), + }, + ), + __new_rent_minimum.checked_sub(__field_info.lamports()).unwrap(), + )?; + } + } else { + let __lamport_amt = __field_info.lamports().checked_sub(__new_rent_minimum).unwrap(); + **#payer.to_account_info().lamports.borrow_mut() = #payer.to_account_info().lamports().checked_add(__lamport_amt).unwrap(); + **__field_info.lamports.borrow_mut() = __field_info.lamports().checked_sub(__lamport_amt).unwrap(); + } + + #field.to_account_info().realloc(#new_space, #zero)?; + __reallocs.insert(#field.key()); + } + } +} + fn generate_constraint_init_group(f: &Field, c: &ConstraintInitGroup) -> proc_macro2::TokenStream { let field = &f.ident; let name_str = f.ident.to_string(); diff --git a/lang/syn/src/codegen/accounts/try_accounts.rs b/lang/syn/src/codegen/accounts/try_accounts.rs index b193dcba..0cacf9c8 100644 --- a/lang/syn/src/codegen/accounts/try_accounts.rs +++ b/lang/syn/src/codegen/accounts/try_accounts.rs @@ -25,7 +25,7 @@ pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream { quote! { #[cfg(feature = "anchor-debug")] ::solana_program::log::sol_log(stringify!(#name)); - let #name: #ty = anchor_lang::Accounts::try_accounts(program_id, accounts, ix_data, __bumps)?; + let #name: #ty = anchor_lang::Accounts::try_accounts(program_id, accounts, ix_data, __bumps, __reallocs)?; } } AccountField::Field(f) => { @@ -47,7 +47,7 @@ pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream { quote! { #[cfg(feature = "anchor-debug")] ::solana_program::log::sol_log(stringify!(#typed_name)); - let #typed_name = anchor_lang::Accounts::try_accounts(program_id, accounts, ix_data, __bumps) + let #typed_name = anchor_lang::Accounts::try_accounts(program_id, accounts, ix_data, __bumps, __reallocs) .map_err(|e| e.with_account_name(#name))?; } } @@ -98,6 +98,7 @@ pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream { accounts: &mut &[anchor_lang::solana_program::account_info::AccountInfo<'info>], ix_data: &[u8], __bumps: &mut std::collections::BTreeMap, + __reallocs: &mut std::collections::BTreeSet, ) -> anchor_lang::Result { // Deserialize instruction, if declared. #ix_de diff --git a/lang/syn/src/codegen/program/handlers.rs b/lang/syn/src/codegen/program/handlers.rs index 543e14af..f965bd3f 100644 --- a/lang/syn/src/codegen/program/handlers.rs +++ b/lang/syn/src/codegen/program/handlers.rs @@ -26,36 +26,41 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { match ix { anchor_lang::idl::IdlInstruction::Create { data_len } => { let mut bumps = std::collections::BTreeMap::new(); + let mut reallocs = std::collections::BTreeSet::new(); let mut accounts = - anchor_lang::idl::IdlCreateAccounts::try_accounts(program_id, &mut accounts, &[], &mut bumps)?; + anchor_lang::idl::IdlCreateAccounts::try_accounts(program_id, &mut accounts, &[], &mut bumps, &mut reallocs)?; __idl_create_account(program_id, &mut accounts, data_len)?; accounts.exit(program_id)?; }, anchor_lang::idl::IdlInstruction::CreateBuffer => { let mut bumps = std::collections::BTreeMap::new(); + let mut reallocs = std::collections::BTreeSet::new(); let mut accounts = - anchor_lang::idl::IdlCreateBuffer::try_accounts(program_id, &mut accounts, &[], &mut bumps)?; + anchor_lang::idl::IdlCreateBuffer::try_accounts(program_id, &mut accounts, &[], &mut bumps, &mut reallocs)?; __idl_create_buffer(program_id, &mut accounts)?; accounts.exit(program_id)?; }, anchor_lang::idl::IdlInstruction::Write { data } => { let mut bumps = std::collections::BTreeMap::new(); + let mut reallocs = std::collections::BTreeSet::new(); let mut accounts = - anchor_lang::idl::IdlAccounts::try_accounts(program_id, &mut accounts, &[], &mut bumps)?; + anchor_lang::idl::IdlAccounts::try_accounts(program_id, &mut accounts, &[], &mut bumps, &mut reallocs)?; __idl_write(program_id, &mut accounts, data)?; accounts.exit(program_id)?; }, anchor_lang::idl::IdlInstruction::SetAuthority { new_authority } => { let mut bumps = std::collections::BTreeMap::new(); + let mut reallocs = std::collections::BTreeSet::new(); let mut accounts = - anchor_lang::idl::IdlAccounts::try_accounts(program_id, &mut accounts, &[], &mut bumps)?; + anchor_lang::idl::IdlAccounts::try_accounts(program_id, &mut accounts, &[], &mut bumps, &mut reallocs)?; __idl_set_authority(program_id, &mut accounts, new_authority)?; accounts.exit(program_id)?; }, anchor_lang::idl::IdlInstruction::SetBuffer => { let mut bumps = std::collections::BTreeMap::new(); + let mut reallocs = std::collections::BTreeSet::new(); let mut accounts = - anchor_lang::idl::IdlSetBuffer::try_accounts(program_id, &mut accounts, &[], &mut bumps)?; + anchor_lang::idl::IdlSetBuffer::try_accounts(program_id, &mut accounts, &[], &mut bumps, &mut reallocs)?; __idl_set_buffer(program_id, &mut accounts)?; accounts.exit(program_id)?; }, @@ -216,13 +221,14 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { let instruction::state::#variant_arm = ix; let mut __bumps = std::collections::BTreeMap::new(); + let mut __reallocs = std::collections::BTreeSet::new(); // Deserialize accounts. let mut remaining_accounts: &[AccountInfo] = accounts; let ctor_accounts = - anchor_lang::__private::Ctor::try_accounts(program_id, &mut remaining_accounts, &[], &mut __bumps)?; + anchor_lang::__private::Ctor::try_accounts(program_id, &mut remaining_accounts, &[], &mut __bumps, &mut __reallocs)?; let mut ctor_user_def_accounts = - #anchor_ident::try_accounts(program_id, &mut remaining_accounts, ix_data, &mut __bumps)?; + #anchor_ident::try_accounts(program_id, &mut remaining_accounts, ix_data, &mut __bumps, &mut __reallocs)?; // Create the solana account for the ctor data. let from = ctor_accounts.from.key; @@ -295,13 +301,14 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { let instruction::state::#variant_arm = ix; let mut __bumps = std::collections::BTreeMap::new(); + let mut __reallocs = std::collections::BTreeSet::new(); // Deserialize accounts. let mut remaining_accounts: &[AccountInfo] = accounts; let ctor_accounts = - anchor_lang::__private::Ctor::try_accounts(program_id, &mut remaining_accounts, &[], &mut __bumps)?; + anchor_lang::__private::Ctor::try_accounts(program_id, &mut remaining_accounts, &[], &mut __bumps, &mut __reallocs)?; let mut ctor_user_def_accounts = - #anchor_ident::try_accounts(program_id, &mut remaining_accounts, ix_data, &mut __bumps)?; + #anchor_ident::try_accounts(program_id, &mut remaining_accounts, ix_data, &mut __bumps, &mut __reallocs)?; // Invoke the ctor. let instance = #mod_name::#name::new( @@ -405,12 +412,15 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { // Bump collector. let mut __bumps = std::collections::BTreeMap::new(); + // Realloc tracker + let mut __reallocs= std::collections::BTreeSet::new(); + // Load state. let mut remaining_accounts: &[AccountInfo] = accounts; if remaining_accounts.is_empty() { return Err(anchor_lang::error::ErrorCode::AccountNotEnoughKeys.into()); } - let loader: anchor_lang::accounts::loader::Loader<#mod_name::#name> = anchor_lang::accounts::loader::Loader::try_accounts(program_id, &mut remaining_accounts, &[], &mut __bumps)?; + let loader: anchor_lang::accounts::loader::Loader<#mod_name::#name> = anchor_lang::accounts::loader::Loader::try_accounts(program_id, &mut remaining_accounts, &[], &mut __bumps, &mut __reallocs)?; // Deserialize accounts. let mut accounts = #anchor_ident::try_accounts( @@ -418,6 +428,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { &mut remaining_accounts, ix_data, &mut __bumps, + &mut __reallocs, )?; let ctx = anchor_lang::context::Context::new( @@ -461,6 +472,9 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { // Bump collector. let mut __bumps = std::collections::BTreeMap::new(); + // Realloc tracker. + let mut __reallocs = std::collections::BTreeSet::new(); + // Load state. let mut remaining_accounts: &[AccountInfo] = accounts; if remaining_accounts.is_empty() { @@ -471,6 +485,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { &mut remaining_accounts, &[], &mut __bumps, + &mut __reallocs, )?; // Deserialize accounts. @@ -479,6 +494,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { &mut remaining_accounts, ix_data, &mut __bumps, + &mut __reallocs, )?; let ctx = anchor_lang::context::Context::new( @@ -589,6 +605,9 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { // Bump collector. let mut __bumps = std::collections::BTreeMap::new(); + // Realloc tracker. + let mut __reallocs= std::collections::BTreeSet::new(); + // Deserialize the program state account. let mut remaining_accounts: &[AccountInfo] = accounts; if remaining_accounts.is_empty() { @@ -599,6 +618,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { &mut remaining_accounts, &[], &mut __bumps, + &mut __reallocs, )?; // Deserialize accounts. @@ -607,6 +627,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { &mut remaining_accounts, ix_data, &mut __bumps, + &mut __reallocs, )?; let ctx = anchor_lang::context::Context::new( @@ -651,6 +672,8 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { // Bump collector. let mut __bumps = std::collections::BTreeMap::new(); + let mut __reallocs = std::collections::BTreeSet::new(); + // Deserialize accounts. let mut remaining_accounts: &[AccountInfo] = accounts; let mut accounts = #anchor_ident::try_accounts( @@ -658,6 +681,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { &mut remaining_accounts, ix_data, &mut __bumps, + &mut __reallocs, )?; // Execute user defined function. @@ -719,6 +743,8 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { // Bump collector. let mut __bumps = std::collections::BTreeMap::new(); + let mut __reallocs = std::collections::BTreeSet::new(); + // Deserialize accounts. let mut remaining_accounts: &[AccountInfo] = accounts; let mut accounts = #anchor::try_accounts( @@ -726,6 +752,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { &mut remaining_accounts, ix_data, &mut __bumps, + &mut __reallocs, )?; // Invoke user defined handler. diff --git a/lang/syn/src/lib.rs b/lang/syn/src/lib.rs index c589d87b..5aeac91b 100644 --- a/lang/syn/src/lib.rs +++ b/lang/syn/src/lib.rs @@ -635,6 +635,7 @@ pub struct ConstraintGroup { associated_token: Option, token_account: Option, mint: Option, + realloc: Option, } impl ConstraintGroup { @@ -678,6 +679,7 @@ pub enum Constraint { Address(ConstraintAddress), TokenAccount(ConstraintTokenAccountGroup), Mint(ConstraintTokenMintGroup), + Realloc(ConstraintReallocGroup), } // Constraint token is a single keyword in a `#[account()]` attribute. @@ -709,6 +711,9 @@ pub enum ConstraintToken { MintDecimals(Context), Bump(Context), ProgramSeed(Context), + Realloc(Context), + ReallocPayer(Context), + ReallocZero(Context), } impl Parse for ConstraintToken { @@ -733,6 +738,28 @@ pub struct ConstraintMut { pub error: Option, } +#[derive(Debug, Clone)] +pub struct ConstraintReallocGroup { + pub payer: Expr, + pub space: Expr, + pub zero: Expr, +} + +#[derive(Debug, Clone)] +pub struct ConstraintRealloc { + pub space: Expr, +} + +#[derive(Debug, Clone)] +pub struct ConstraintReallocPayer { + pub target: Expr, +} + +#[derive(Debug, Clone)] +pub struct ConstraintReallocZero { + pub zero: Expr, +} + #[derive(Debug, Clone)] pub struct ConstraintSigner { pub error: Option, diff --git a/lang/syn/src/parser/accounts/constraints.rs b/lang/syn/src/parser/accounts/constraints.rs index fcd38c1e..ac71a680 100644 --- a/lang/syn/src/parser/accounts/constraints.rs +++ b/lang/syn/src/parser/accounts/constraints.rs @@ -196,6 +196,47 @@ pub fn parse_token(stream: ParseStream) -> ParseResult { )) } } + "realloc" => { + if stream.peek(Token![=]) { + stream.parse::()?; + let span = ident + .span() + .join(stream.span()) + .unwrap_or_else(|| ident.span()); + ConstraintToken::Realloc(Context::new( + span, + ConstraintRealloc { + space: stream.parse()?, + }, + )) + } else { + stream.parse::()?; + stream.parse::()?; + let kw = stream.call(Ident::parse_any)?.to_string(); + stream.parse::()?; + + let span = ident + .span() + .join(stream.span()) + .unwrap_or_else(|| ident.span()); + + match kw.as_str() { + "payer" => ConstraintToken::ReallocPayer(Context::new( + span, + ConstraintReallocPayer { + target: stream.parse()?, + }, + )), + "zero" => ConstraintToken::ReallocZero(Context::new( + span, + ConstraintReallocZero { + zero: stream.parse()?, + }, + )), + _ => return Err(ParseError::new(ident.span(), "Invalid attribute. realloc::payer and realloc::zero are the only valid attributes")), + } + } + } _ => { stream.parse::()?; let span = ident @@ -313,6 +354,9 @@ pub struct ConstraintGroupBuilder<'ty> { pub mint_decimals: Option>, pub bump: Option>, pub program_seed: Option>, + pub realloc: Option>, + pub realloc_payer: Option>, + pub realloc_zero: Option>, } impl<'ty> ConstraintGroupBuilder<'ty> { @@ -344,6 +388,9 @@ impl<'ty> ConstraintGroupBuilder<'ty> { mint_decimals: None, bump: None, program_seed: None, + realloc: None, + realloc_payer: None, + realloc_zero: None, } } @@ -439,6 +486,22 @@ impl<'ty> ConstraintGroupBuilder<'ty> { } } + // Realloc. + if let Some(r) = &self.realloc { + if self.realloc_payer.is_none() { + return Err(ParseError::new( + r.span(), + "realloc::payer must be provided when using realloc", + )); + } + if self.realloc_zero.is_none() { + return Err(ParseError::new( + r.span(), + "realloc::zero must be provided when using realloc", + )); + } + } + // Zero. if let Some(z) = &self.zeroed { match self.mutable { @@ -526,6 +589,9 @@ impl<'ty> ConstraintGroupBuilder<'ty> { mint_decimals, bump, program_seed, + realloc, + realloc_payer, + realloc_zero, } = self; // Converts Option> -> Option. @@ -644,6 +710,11 @@ impl<'ty> ConstraintGroupBuilder<'ty> { } }, })).transpose()?, + realloc: realloc.as_ref().map(|r| ConstraintReallocGroup { + payer: into_inner!(realloc_payer).unwrap().target, + space: r.space.clone(), + zero: into_inner!(realloc_zero).unwrap().zero, + }), zeroed: into_inner!(zeroed), mutable: into_inner!(mutable), signer: into_inner!(signer), @@ -690,6 +761,9 @@ impl<'ty> ConstraintGroupBuilder<'ty> { ConstraintToken::MintDecimals(c) => self.add_mint_decimals(c), ConstraintToken::Bump(c) => self.add_bump(c), ConstraintToken::ProgramSeed(c) => self.add_program_seed(c), + ConstraintToken::Realloc(c) => self.add_realloc(c), + ConstraintToken::ReallocPayer(c) => self.add_realloc_payer(c), + ConstraintToken::ReallocZero(c) => self.add_realloc_zero(c), } } @@ -757,6 +831,56 @@ impl<'ty> ConstraintGroupBuilder<'ty> { Ok(()) } + fn add_realloc(&mut self, c: Context) -> ParseResult<()> { + if !matches!(self.f_ty, Some(Ty::Account(_))) + && !matches!(self.f_ty, Some(Ty::AccountLoader(_))) + { + return Err(ParseError::new( + c.span(), + "realloc must be on an Account or AccountLoader", + )); + } + if self.mutable.is_none() { + return Err(ParseError::new( + c.span(), + "mut must be provided before realloc", + )); + } + if self.realloc.is_some() { + return Err(ParseError::new(c.span(), "realloc already provided")); + } + self.realloc.replace(c); + Ok(()) + } + + fn add_realloc_payer(&mut self, c: Context) -> ParseResult<()> { + if self.realloc.is_none() { + return Err(ParseError::new( + c.span(), + "realloc must be provided before realloc::payer", + )); + } + if self.realloc_payer.is_some() { + return Err(ParseError::new(c.span(), "realloc::payer already provided")); + } + self.realloc_payer.replace(c); + Ok(()) + } + + fn add_realloc_zero(&mut self, c: Context) -> ParseResult<()> { + if self.realloc.is_none() { + return Err(ParseError::new( + c.span(), + "realloc must be provided before realloc::zero", + )); + } + if self.realloc_zero.is_some() { + return Err(ParseError::new(c.span(), "realloc::zero already provided")); + } + self.realloc_zero.replace(c); + Ok(()) + } + fn add_close(&mut self, c: Context) -> ParseResult<()> { if !matches!(self.f_ty, Some(Ty::ProgramAccount(_))) && !matches!(self.f_ty, Some(Ty::Account(_))) diff --git a/lang/syn/src/parser/accounts/mod.rs b/lang/syn/src/parser/accounts/mod.rs index a31f2184..5d9d19ba 100644 --- a/lang/syn/src/parser/accounts/mod.rs +++ b/lang/syn/src/parser/accounts/mod.rs @@ -55,7 +55,7 @@ fn constraints_cross_checks(fields: &[AccountField]) -> ParseResult<()> { init_fields[0].ident.span(), "the init constraint requires \ the system_program field to exist in the account \ - validation struct. Use the program type to add \ + validation struct. Use the Program type to add \ the system_program field to your validation struct.", )); } @@ -70,7 +70,7 @@ fn constraints_cross_checks(fields: &[AccountField]) -> ParseResult<()> { init_fields[0].ident.span(), "the init constraint requires \ the token_program field to exist in the account \ - validation struct. Use the program type to add \ + validation struct. Use the Program type to add \ the token_program field to your validation struct.", )); } @@ -86,7 +86,7 @@ fn constraints_cross_checks(fields: &[AccountField]) -> ParseResult<()> { init_fields[0].ident.span(), "the init constraint requires \ the associated_token_program field to exist in the account \ - validation struct. Use the program type to add \ + validation struct. Use the Program type to add \ the associated_token_program field to your validation struct.", )); } @@ -141,6 +141,61 @@ fn constraints_cross_checks(fields: &[AccountField]) -> ParseResult<()> { } } } + + // REALLOC + let realloc_fields: Vec<&Field> = fields + .iter() + .filter_map(|f| match f { + AccountField::Field(field) if field.constraints.realloc.is_some() => Some(field), + _ => None, + }) + .collect(); + + if !realloc_fields.is_empty() { + // realloc needs system program. + if fields.iter().all(|f| f.ident() != "system_program") { + return Err(ParseError::new( + realloc_fields[0].ident.span(), + "the realloc constraint requires \ + the system_program field to exist in the account \ + validation struct. Use the Program type to add \ + the system_program field to your validation struct.", + )); + } + + for field in realloc_fields { + // Get allocator for realloc-ed account + let associated_payer_name = match field.constraints.realloc.clone().unwrap().payer { + // composite allocator, check not supported + Expr::Field(_) => continue, + field_name => field_name.to_token_stream().to_string(), + }; + + // Check allocator is mutable + let associated_payer_field = fields.iter().find_map(|f| match f { + AccountField::Field(field) if *f.ident() == associated_payer_name => Some(field), + _ => None, + }); + + match associated_payer_field { + Some(associated_payer_field) => { + if !associated_payer_field.constraints.is_mutable() { + return Err(ParseError::new( + field.ident.span(), + "the realloc::payer specified for an realloc constraint must be mutable.", + )); + } + } + _ => { + return Err(ParseError::new( + field.ident.span(), + "the realloc::payer specified does not exist.", + )); + } + } + } + } + Ok(()) } diff --git a/tests/cfo/deps/stake b/tests/cfo/deps/stake index aa0d8adc..fd78344a 160000 --- a/tests/cfo/deps/stake +++ b/tests/cfo/deps/stake @@ -1 +1 @@ -Subproject commit aa0d8adc94192607b35661d7ae48a26b0491fd16 +Subproject commit fd78344ab5f34c36a91bdaf8b9edf2fbd8a93510 diff --git a/tests/package.json b/tests/package.json index 35112548..f280dcc6 100644 --- a/tests/package.json +++ b/tests/package.json @@ -24,6 +24,7 @@ "permissioned-markets", "pda-derivation", "pyth", + "realloc", "spl/token-proxy", "swap", "system-accounts", @@ -48,8 +49,8 @@ "chai": "^4.3.4", "@types/chai": "^4.3.0", "@types/mocha": "^9.1.0", - "mocha": "^9.1.3", - "ts-mocha": "^8.0.0", + "mocha": "^10.0.0", + "ts-mocha": "^10.0.0", "typescript": "^4.4.4", "prettier": "^2.5.1", "tsc": "^2.0.4" diff --git a/tests/realloc/Anchor.toml b/tests/realloc/Anchor.toml new file mode 100644 index 00000000..a9ac74c5 --- /dev/null +++ b/tests/realloc/Anchor.toml @@ -0,0 +1,15 @@ +[features] +seeds = false + +[programs.localnet] +realloc = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS" + +[registry] +url = "https://anchor.projectserum.com" + +[provider] +cluster = "localnet" +wallet = "~/.config/solana/id.json" + +[scripts] +test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" \ No newline at end of file diff --git a/tests/realloc/Cargo.toml b/tests/realloc/Cargo.toml new file mode 100644 index 00000000..a60de986 --- /dev/null +++ b/tests/realloc/Cargo.toml @@ -0,0 +1,4 @@ +[workspace] +members = [ + "programs/*" +] diff --git a/tests/realloc/package.json b/tests/realloc/package.json new file mode 100644 index 00000000..8ed4255a --- /dev/null +++ b/tests/realloc/package.json @@ -0,0 +1,19 @@ +{ + "name": "realloc", + "version": "0.24.2", + "license": "(MIT OR Apache-2.0)", + "homepage": "https://github.com/project-serum/anchor#readme", + "bugs": { + "url": "https://github.com/project-serum/anchor/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/project-serum/anchor.git" + }, + "engines": { + "node": ">=11" + }, + "scripts": { + "test": "anchor test" + } +} diff --git a/tests/realloc/programs/realloc/Cargo.toml b/tests/realloc/programs/realloc/Cargo.toml new file mode 100644 index 00000000..4f014303 --- /dev/null +++ b/tests/realloc/programs/realloc/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "realloc" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "realloc" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] + +[profile.release] +overflow-checks = true + +[dependencies] +anchor-lang = { path = "../../../../lang" } diff --git a/tests/realloc/programs/realloc/Xargo.toml b/tests/realloc/programs/realloc/Xargo.toml new file mode 100644 index 00000000..475fb71e --- /dev/null +++ b/tests/realloc/programs/realloc/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/tests/realloc/programs/realloc/src/lib.rs b/tests/realloc/programs/realloc/src/lib.rs new file mode 100644 index 00000000..a854e6a0 --- /dev/null +++ b/tests/realloc/programs/realloc/src/lib.rs @@ -0,0 +1,112 @@ +use anchor_lang::prelude::*; + +declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); + +#[program] +pub mod realloc { + use super::*; + + pub fn initialize(ctx: Context) -> Result<()> { + ctx.accounts.sample.data = vec![0]; + ctx.accounts.sample.bump = *ctx.bumps.get("sample").unwrap(); + Ok(()) + } + + pub fn realloc(ctx: Context, len: u16) -> Result<()> { + ctx.accounts + .sample + .data + .resize_with(len as usize, Default::default); + Ok(()) + } + + pub fn realloc2(ctx: Context, len: u16) -> Result<()> { + ctx.accounts + .sample1 + .data + .resize_with(len as usize, Default::default); + + ctx.accounts + .sample2 + .data + .resize_with(len as usize, Default::default); + Ok(()) + } +} + +#[derive(Accounts)] +pub struct Initialize<'info> { + #[account(mut)] + pub authority: Signer<'info>, + + #[account( + init, + payer = authority, + seeds = [b"sample"], + bump, + space = Sample::space(1), + )] + pub sample: Account<'info, Sample>, + + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +#[instruction(len: u16)] +pub struct Realloc<'info> { + #[account(mut)] + pub authority: Signer<'info>, + + #[account( + mut, + seeds = [b"sample"], + bump = sample.bump, + realloc = Sample::space(len as usize), + realloc::payer = authority, + realloc::zero = false, + )] + pub sample: Account<'info, Sample>, + + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +#[instruction(len: u16)] +pub struct Realloc2<'info> { + #[account(mut)] + pub authority: Signer<'info>, + + #[account( + mut, + seeds = [b"sample"], + bump = sample1.bump, + realloc = Sample::space(len as usize), + realloc::payer = authority, + realloc::zero = false, + )] + pub sample1: Account<'info, Sample>, + + #[account( + mut, + seeds = [b"sample"], + bump = sample2.bump, + realloc = Sample::space((len + 10) as usize), + realloc::payer = authority, + realloc::zero = false, + )] + pub sample2: Account<'info, Sample>, + + pub system_program: Program<'info, System>, +} + +#[account] +pub struct Sample { + pub data: Vec, + pub bump: u8, +} + +impl Sample { + pub fn space(len: usize) -> usize { + 8 + (4 + len) + 1 + } +} diff --git a/tests/realloc/tests/realloc.ts b/tests/realloc/tests/realloc.ts new file mode 100644 index 00000000..7ae700bc --- /dev/null +++ b/tests/realloc/tests/realloc.ts @@ -0,0 +1,90 @@ +import * as anchor from "@project-serum/anchor"; +import { AnchorError, Program } from "@project-serum/anchor"; +import { assert } from "chai"; +import { Realloc } from "../target/types/realloc"; + +describe("realloc", () => { + // Configure the client to use the local cluster. + anchor.setProvider(anchor.AnchorProvider.env()); + + const program = anchor.workspace.Realloc as Program; + const authority = (program.provider as any).wallet + .payer as anchor.web3.Keypair; + + let sample: anchor.web3.PublicKey; + + before(async () => { + [sample] = await anchor.web3.PublicKey.findProgramAddress( + [Buffer.from("sample")], + program.programId + ); + }); + + it("initialized", async () => { + await program.methods + .initialize() + .accounts({ authority: authority.publicKey, sample }) + .rpc(); + + const samples = await program.account.sample.all(); + assert.lengthOf(samples, 1); + assert.lengthOf(samples[0].account.data, 1); + }); + + it("fails if delta bytes exceeds permitted limit", async () => { + try { + await program.methods + .realloc(10250) + .accounts({ authority: authority.publicKey, sample }) + .rpc(); + assert.ok(false); + } catch (e) { + assert.isTrue(e instanceof AnchorError); + const err: AnchorError = e; + const errMsg = + "The account reallocation exceeds the MAX_PERMITTED_DATA_INCREASE limit"; + assert.strictEqual(err.error.errorMessage, errMsg); + assert.strictEqual(err.error.errorCode.number, 3016); + } + }); + + it("realloc additive", async () => { + await program.methods + .realloc(5) + .accounts({ authority: authority.publicKey, sample }) + .rpc(); + + const s = await program.account.sample.fetch(sample); + assert.lengthOf(s.data, 5); + }); + + it("realloc substractive", async () => { + await program.methods + .realloc(1) + .accounts({ authority: authority.publicKey, sample }) + .rpc(); + + const s = await program.account.sample.fetch(sample); + assert.lengthOf(s.data, 1); + }); + + it("fails with duplicate account reallocations", async () => { + try { + await program.methods + .realloc2(1000) + .accounts({ + authority: authority.publicKey, + sample1: sample, + sample2: sample, + }) + .rpc(); + } catch (e) { + assert.isTrue(e instanceof AnchorError); + const err: AnchorError = e; + const errMsg = + "The account was duplicated for more than one reallocation"; + assert.strictEqual(err.error.errorMessage, errMsg); + assert.strictEqual(err.error.errorCode.number, 3017); + } + }); +}); diff --git a/tests/realloc/tsconfig.json b/tests/realloc/tsconfig.json new file mode 100644 index 00000000..cd5d2e3d --- /dev/null +++ b/tests/realloc/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "types": ["mocha", "chai"], + "typeRoots": ["./node_modules/@types"], + "lib": ["es2015"], + "module": "commonjs", + "target": "es6", + "esModuleInterop": true + } +} diff --git a/tests/yarn.lock b/tests/yarn.lock index a5da0018..1853d720 100644 --- a/tests/yarn.lock +++ b/tests/yarn.lock @@ -66,6 +66,7 @@ js-sha256 "^0.9.0" pako "^2.0.3" snake-case "^3.0.4" + superstruct "^0.15.4" toml "^3.0.0" "@project-serum/borsh@^0.2.2": @@ -341,6 +342,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -448,6 +456,21 @@ chokidar@3.5.2: optionalDependencies: fsevents "~2.3.2" +chokidar@3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + circular-json@^0.5.9: version "0.5.9" resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.5.9.tgz#932763ae88f4f7dead7a0d09c8a51a4743a53b1d" @@ -510,6 +533,13 @@ debug@4.3.2: dependencies: ms "2.1.2" +debug@4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + decamelize@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" @@ -666,6 +696,18 @@ glob@7.1.7: once "^1.3.0" path-is-absolute "^1.0.0" +glob@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + growl@1.10.5: version "1.10.5" resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" @@ -874,6 +916,13 @@ minimatch@3.0.4, minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" +minimatch@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" + integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.0, minimist@^1.2.5: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" @@ -886,6 +935,34 @@ mkdirp@^0.5.1: dependencies: minimist "^1.2.5" +mocha@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.0.0.tgz#205447d8993ec755335c4b13deba3d3a13c4def9" + integrity sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA== + dependencies: + "@ungap/promise-all-settled" "1.1.2" + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.5.3" + debug "4.3.4" + diff "5.0.0" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.2.0" + he "1.2.0" + js-yaml "4.1.0" + log-symbols "4.1.0" + minimatch "5.0.1" + ms "2.1.3" + nanoid "3.3.3" + serialize-javascript "6.0.0" + strip-json-comments "3.1.1" + supports-color "8.1.1" + workerpool "6.2.1" + yargs "16.2.0" + yargs-parser "20.2.4" + yargs-unparser "2.0.0" + mocha@^9.1.3: version "9.1.3" resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.1.3.tgz#8a623be6b323810493d8c8f6f7667440fa469fdb" @@ -931,6 +1008,11 @@ nanoid@3.1.25: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.25.tgz#09ca32747c0e543f0e1814b7d3793477f9c8e152" integrity sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q== +nanoid@3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" + integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== + no-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" @@ -1136,6 +1218,11 @@ superstruct@^0.14.2: resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.14.2.tgz#0dbcdf3d83676588828f1cf5ed35cda02f59025b" integrity sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ== +superstruct@^0.15.4: + version "0.15.5" + resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.15.5.tgz#0f0a8d3ce31313f0d84c6096cd4fa1bfdedc9dab" + integrity sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ== + supports-color@8.1.1: version "8.1.1" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" @@ -1187,10 +1274,10 @@ traverse-chain@~0.1.0: resolved "https://registry.yarnpkg.com/traverse-chain/-/traverse-chain-0.1.0.tgz#61dbc2d53b69ff6091a12a168fd7d433107e40f1" integrity sha1-YdvC1Ttp/2CRoSoWj9fUMxB+QPE= -ts-mocha@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/ts-mocha/-/ts-mocha-8.0.0.tgz#962d0fa12eeb6468aa1a6b594bb3bbc818da3ef0" - integrity sha512-Kou1yxTlubLnD5C3unlCVO7nh0HERTezjoVhVw/M5S1SqoUec0WgllQvPk3vzPMc6by8m6xD1uR1yRf8lnVUbA== +ts-mocha@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/ts-mocha/-/ts-mocha-10.0.0.tgz#41a8d099ac90dbbc64b06976c5025ffaebc53cb9" + integrity sha512-VRfgDO+iiuJFlNB18tzOfypJ21xn2xbuZyDvJvqpTbWgkAgD17ONGr8t+Tl8rcBtOBdjXp5e/Rk+d39f7XBHRw== dependencies: ts-node "7.0.1" optionalDependencies: @@ -1287,6 +1374,11 @@ workerpool@6.1.5: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.1.5.tgz#0f7cf076b6215fd7e1da903ff6f22ddd1886b581" integrity sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw== +workerpool@6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" + integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== + wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" diff --git a/ts/src/error.ts b/ts/src/error.ts index f36f4212..bb7fffa1 100644 --- a/ts/src/error.ts +++ b/ts/src/error.ts @@ -364,6 +364,9 @@ export const LangErrorCode = { AccountNotProgramData: 3013, AccountNotAssociatedTokenAccount: 3014, AccountSysvarMismatch: 3015, + AccountReallocExceedsLimit: 3016, + AccountDuplicateReallocs: 3017, + // State. StateInvalidAddress: 4000, @@ -502,6 +505,14 @@ export const LangErrorMessage = new Map([ LangErrorCode.AccountSysvarMismatch, "The given public key does not match the required sysvar", ], + [ + LangErrorCode.AccountReallocExceedsLimit, + "The account reallocation exceeds the MAX_PERMITTED_DATA_INCREASE limit", + ], + [ + LangErrorCode.AccountDuplicateReallocs, + "The account was duplicated for more than one reallocation", + ], // State. [