瀏覽代碼

lang: adjust `realloc` implementation to safeguard max increase and idempotency (#1986)

Matthew Callens 3 年之前
父節點
當前提交
c47fb2877e
共有 38 個文件被更改,包括 786 次插入43 次删除
  1. 2 0
      .github/workflows/tests.yaml
  2. 3 2
      CHANGELOG.md
  3. 43 0
      lang/derive/accounts/src/lib.rs
  4. 2 1
      lang/src/accounts/account.rs
  5. 2 1
      lang/src/accounts/account_info.rs
  6. 2 1
      lang/src/accounts/account_loader.rs
  7. 3 2
      lang/src/accounts/boxed.rs
  8. 1 0
      lang/src/accounts/cpi_account.rs
  9. 2 1
      lang/src/accounts/cpi_state.rs
  10. 2 1
      lang/src/accounts/loader.rs
  11. 2 1
      lang/src/accounts/program.rs
  12. 2 1
      lang/src/accounts/program_account.rs
  13. 2 1
      lang/src/accounts/signer.rs
  14. 2 1
      lang/src/accounts/state.rs
  15. 2 1
      lang/src/accounts/system_account.rs
  16. 2 1
      lang/src/accounts/sysvar.rs
  17. 2 1
      lang/src/accounts/unchecked_account.rs
  18. 6 0
      lang/src/error.rs
  19. 2 1
      lang/src/lib.rs
  20. 10 4
      lang/src/vec.rs
  21. 58 0
      lang/syn/src/codegen/accounts/constraints.rs
  22. 3 2
      lang/syn/src/codegen/accounts/try_accounts.rs
  23. 37 10
      lang/syn/src/codegen/program/handlers.rs
  24. 27 0
      lang/syn/src/lib.rs
  25. 124 0
      lang/syn/src/parser/accounts/constraints.rs
  26. 58 3
      lang/syn/src/parser/accounts/mod.rs
  27. 1 1
      tests/cfo/deps/stake
  28. 3 2
      tests/package.json
  29. 15 0
      tests/realloc/Anchor.toml
  30. 4 0
      tests/realloc/Cargo.toml
  31. 19 0
      tests/realloc/package.json
  32. 22 0
      tests/realloc/programs/realloc/Cargo.toml
  33. 2 0
      tests/realloc/programs/realloc/Xargo.toml
  34. 112 0
      tests/realloc/programs/realloc/src/lib.rs
  35. 90 0
      tests/realloc/tests/realloc.ts
  36. 10 0
      tests/realloc/tsconfig.json
  37. 96 4
      tests/yarn.lock
  38. 11 0
      ts/src/error.ts

+ 2 - 0
.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

+ 3 - 2
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<Pubkey>` argument to `Accounts::try_accounts` ([#1986](https://github.com/coral-xyz/anchor/pull/1986)).
 
 ## [0.24.2] - 2022-04-13
 

+ 43 - 0
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
 /// <table>
 ///     <thead>
@@ -418,6 +419,48 @@ use syn::parse_macro_input;
 ///                 </code></pre>
 ///             </td>
 ///         </tr>
+///         <tr>
+///             <td>
+///                 <code>#[account(realloc = &lt;space&gt;, realloc::payer = &lt;target&gt;, realloc::zero = &lt;bool&gt;)]</code>
+///             </td>
+///             <td>
+///                 Used to <a href="https://docs.rs/solana-program/latest/solana_program/account_info/struct.AccountInfo.html#method.realloc" target = "_blank" rel = "noopener noreferrer">realloc</a>
+///                 program account space at the beginning of an instruction.
+///                 <br><br>
+///                 The account must be marked as <code>mut</code> and applied to either <code>Account</code> or <code>AccountLoader</code> types.
+///                 <br><br>
+///                 If the change in account data length is additive, lamports will be transferred from the <code>realloc::payer</code> 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 <code>realloc::payer</code>.
+///                 <br><br>
+///                 The <code>realloc::zero</code> constraint is required in order to determine whether the new memory should be zero initialized after
+///                 reallocation. Please read the documentation on the <code>AccountInfo::realloc</code> function linked above to understand the
+///                 caveats regarding compute units when providing <code>true</code or <code>false</code> to this flag.
+///                 <br><br>
+///                 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.
+///                 <br><br>
+///                 Example:
+///                 <pre>
+/// #[derive(Accounts)]
+/// pub struct Example {
+///     #[account(mut)]
+///     pub payer: Signer<'info>,
+///     #[account(
+///         mut,
+///         seeds = [b"example"],
+///         bump,
+///         realloc = 8 + std::mem::size_of::<MyType>() + 100,
+///         realloc::payer = payer,
+///         realloc::zero = false,
+///     )]
+///     pub acc: Account<'info, MyType>,
+///     pub system_program: Program<'info, System>,
+/// }
+///                 </pre>
+///             </td>
+///         </tr>
 ///     </tbody>
 /// </table>
 ///

+ 2 - 1
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<String, u8>,
+        _reallocs: &mut BTreeSet<Pubkey>,
     ) -> Result<Self> {
         if accounts.is_empty() {
             return Err(ErrorCode::AccountNotEnoughKeys.into());

+ 2 - 1
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<String, u8>,
+        _reallocs: &mut BTreeSet<Pubkey>,
     ) -> Result<Self> {
         if accounts.is_empty() {
             return Err(ErrorCode::AccountNotEnoughKeys.into());

+ 2 - 1
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<String, u8>,
+        _reallocs: &mut BTreeSet<Pubkey>,
     ) -> Result<Self> {
         if accounts.is_empty() {
             return Err(ErrorCode::AccountNotEnoughKeys.into());

+ 3 - 2
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<T> {
@@ -26,8 +26,9 @@ impl<'info, T: Accounts<'info>> Accounts<'info> for Box<T> {
         accounts: &mut &[AccountInfo<'info>],
         ix_data: &[u8],
         bumps: &mut BTreeMap<String, u8>,
+        reallocs: &mut BTreeSet<Pubkey>,
     ) -> Result<Self> {
-        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)
     }
 }
 

+ 1 - 0
lang/src/accounts/cpi_account.rs

@@ -53,6 +53,7 @@ where
         accounts: &mut &[AccountInfo<'info>],
         _ix_data: &[u8],
         _bumps: &mut BTreeMap<String, u8>,
+        _reallocs: &mut BTreeSet<Pubkey>,
     ) -> Result<Self> {
         if accounts.is_empty() {
             return Err(ErrorCode::AccountNotEnoughKeys.into());

+ 2 - 1
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<String, u8>,
+        _reallocs: &mut BTreeSet<Pubkey>,
     ) -> Result<Self> {
         if accounts.is_empty() {
             return Err(ErrorCode::AccountNotEnoughKeys.into());

+ 2 - 1
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<String, u8>,
+        _reallocs: &mut BTreeSet<Pubkey>,
     ) -> Result<Self> {
         if accounts.is_empty() {
             return Err(ErrorCode::AccountNotEnoughKeys.into());

+ 2 - 1
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<String, u8>,
+        _reallocs: &mut BTreeSet<Pubkey>,
     ) -> Result<Self> {
         if accounts.is_empty() {
             return Err(ErrorCode::AccountNotEnoughKeys.into());

+ 2 - 1
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<String, u8>,
+        _reallocs: &mut BTreeSet<Pubkey>,
     ) -> Result<Self> {
         if accounts.is_empty() {
             return Err(ErrorCode::AccountNotEnoughKeys.into());

+ 2 - 1
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<String, u8>,
+        _reallocs: &mut BTreeSet<Pubkey>,
     ) -> Result<Self> {
         if accounts.is_empty() {
             return Err(ErrorCode::AccountNotEnoughKeys.into());

+ 2 - 1
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<String, u8>,
+        _reallocs: &mut BTreeSet<Pubkey>,
     ) -> Result<Self> {
         if accounts.is_empty() {
             return Err(ErrorCode::AccountNotEnoughKeys.into());

+ 2 - 1
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<String, u8>,
+        _reallocs: &mut BTreeSet<Pubkey>,
     ) -> Result<Self> {
         if accounts.is_empty() {
             return Err(ErrorCode::AccountNotEnoughKeys.into());

+ 2 - 1
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<String, u8>,
+        _reallocs: &mut BTreeSet<Pubkey>,
     ) -> Result<Self> {
         if accounts.is_empty() {
             return Err(ErrorCode::AccountNotEnoughKeys.into());

+ 2 - 1
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<String, u8>,
+        _reallocs: &mut BTreeSet<Pubkey>,
     ) -> Result<Self> {
         if accounts.is_empty() {
             return Err(ErrorCode::AccountNotEnoughKeys.into());

+ 6 - 0
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

+ 2 - 1
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<String, u8>,
+        reallocs: &mut BTreeSet<Pubkey>,
     ) -> Result<Self>;
 }
 

+ 10 - 4
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<T> {
     fn to_account_infos(&self) -> Vec<AccountInfo<'info>> {
@@ -26,9 +26,11 @@ impl<'info, T: Accounts<'info>> Accounts<'info> for Vec<T> {
         accounts: &mut &[AccountInfo<'info>],
         ix_data: &[u8],
         bumps: &mut BTreeMap<String, u8>,
+        reallocs: &mut BTreeSet<Pubkey>,
     ) -> Result<Self> {
         let mut vec: Vec<T> = 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::<Test>::try_accounts(&program_id, &mut accounts, &[], &mut bumps).unwrap();
+            Vec::<Test>::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::<Test>::try_accounts(&program_id, &mut accounts, &[], &mut bumps).unwrap();
+        Vec::<Test>::try_accounts(&program_id, &mut accounts, &[], &mut bumps, &mut reallocs)
+            .unwrap();
     }
 }

+ 58 - 0
lang/syn/src/codegen/accounts/constraints.rs

@@ -59,6 +59,7 @@ pub fn linearize(c_group: &ConstraintGroup) -> Vec<Constraint> {
         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<Constraint> {
     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::<isize>::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::<usize>::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();

+ 3 - 2
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<String, u8>,
+                __reallocs: &mut std::collections::BTreeSet<anchor_lang::solana_program::pubkey::Pubkey>,
             ) -> anchor_lang::Result<Self> {
                 // Deserialize instruction, if declared.
                 #ix_de

+ 37 - 10
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.

+ 27 - 0
lang/syn/src/lib.rs

@@ -635,6 +635,7 @@ pub struct ConstraintGroup {
     associated_token: Option<ConstraintAssociatedToken>,
     token_account: Option<ConstraintTokenAccountGroup>,
     mint: Option<ConstraintTokenMintGroup>,
+    realloc: Option<ConstraintReallocGroup>,
 }
 
 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(<TOKEN>)]` attribute.
@@ -709,6 +711,9 @@ pub enum ConstraintToken {
     MintDecimals(Context<ConstraintMintDecimals>),
     Bump(Context<ConstraintTokenBump>),
     ProgramSeed(Context<ConstraintProgramSeed>),
+    Realloc(Context<ConstraintRealloc>),
+    ReallocPayer(Context<ConstraintReallocPayer>),
+    ReallocZero(Context<ConstraintReallocZero>),
 }
 
 impl Parse for ConstraintToken {
@@ -733,6 +738,28 @@ pub struct ConstraintMut {
     pub error: Option<Expr>,
 }
 
+#[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<Expr>,

+ 124 - 0
lang/syn/src/parser/accounts/constraints.rs

@@ -196,6 +196,47 @@ pub fn parse_token(stream: ParseStream) -> ParseResult<ConstraintToken> {
                 ))
             }
         }
+        "realloc" => {
+            if stream.peek(Token![=]) {
+                stream.parse::<Token![=]>()?;
+                let span = ident
+                    .span()
+                    .join(stream.span())
+                    .unwrap_or_else(|| ident.span());
+                ConstraintToken::Realloc(Context::new(
+                    span,
+                    ConstraintRealloc {
+                        space: stream.parse()?,
+                    },
+                ))
+            } else {
+                stream.parse::<Token![:]>()?;
+                stream.parse::<Token![:]>()?;
+                let kw = stream.call(Ident::parse_any)?.to_string();
+                stream.parse::<Token![=]>()?;
+
+                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::<Token![=]>()?;
             let span = ident
@@ -313,6 +354,9 @@ pub struct ConstraintGroupBuilder<'ty> {
     pub mint_decimals: Option<Context<ConstraintMintDecimals>>,
     pub bump: Option<Context<ConstraintTokenBump>>,
     pub program_seed: Option<Context<ConstraintProgramSeed>>,
+    pub realloc: Option<Context<ConstraintRealloc>>,
+    pub realloc_payer: Option<Context<ConstraintReallocPayer>>,
+    pub realloc_zero: Option<Context<ConstraintReallocZero>>,
 }
 
 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<Context<T>> -> Option<T>.
@@ -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<ConstraintRealloc>) -> 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<ConstraintReallocPayer>) -> 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<ConstraintReallocZero>) -> 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<ConstraintClose>) -> ParseResult<()> {
         if !matches!(self.f_ty, Some(Ty::ProgramAccount(_)))
             && !matches!(self.f_ty, Some(Ty::Account(_)))

+ 58 - 3
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(())
 }
 

+ 1 - 1
tests/cfo/deps/stake

@@ -1 +1 @@
-Subproject commit aa0d8adc94192607b35661d7ae48a26b0491fd16
+Subproject commit fd78344ab5f34c36a91bdaf8b9edf2fbd8a93510

+ 3 - 2
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"

+ 15 - 0
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"

+ 4 - 0
tests/realloc/Cargo.toml

@@ -0,0 +1,4 @@
+[workspace]
+members = [
+    "programs/*"
+]

+ 19 - 0
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"
+  }
+}

+ 22 - 0
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" }

+ 2 - 0
tests/realloc/programs/realloc/Xargo.toml

@@ -0,0 +1,2 @@
+[target.bpfel-unknown-unknown.dependencies.std]
+features = []

+ 112 - 0
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<Initialize>) -> Result<()> {
+        ctx.accounts.sample.data = vec![0];
+        ctx.accounts.sample.bump = *ctx.bumps.get("sample").unwrap();
+        Ok(())
+    }
+
+    pub fn realloc(ctx: Context<Realloc>, len: u16) -> Result<()> {
+        ctx.accounts
+            .sample
+            .data
+            .resize_with(len as usize, Default::default);
+        Ok(())
+    }
+
+    pub fn realloc2(ctx: Context<Realloc2>, 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<u8>,
+    pub bump: u8,
+}
+
+impl Sample {
+    pub fn space(len: usize) -> usize {
+        8 + (4 + len) + 1
+    }
+}

+ 90 - 0
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<Realloc>;
+  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);
+    }
+  });
+});

+ 10 - 0
tests/realloc/tsconfig.json

@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "types": ["mocha", "chai"],
+    "typeRoots": ["./node_modules/@types"],
+    "lib": ["es2015"],
+    "module": "commonjs",
+    "target": "es6",
+    "esModuleInterop": true
+  }
+}

+ 96 - 4
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"

+ 11 - 0
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.
   [