Browse Source

#[account(rent_exempt)] attribute (#17)

Armani Ferrante 4 years ago
parent
commit
830c187279

+ 3 - 1
README.md

@@ -55,6 +55,7 @@ mod basic_1 {
 pub struct Initialize<'info> {
     #[account(init)]
     pub my_account: ProgramAccount<'info, MyAccount>,
+    pub rent: Rent,
 }
 
 #[derive(Accounts)]
@@ -93,9 +94,10 @@ purposes of the Accounts macro) that can be specified on a struct deriving `Acco
 | `#[account(signer)]` | On raw `AccountInfo` structs. | Checks the given account signed the transaction. |
 | `#[account(mut)]` | On `ProgramAccount` structs. | Marks the account as mutable and persists the state transition. |
 | `#[account(init)]` | On `ProgramAccount` structs. | Marks the account as being initialized, skipping the account discriminator check. |
-| `#[account(belongs_to = <target>)]` | On `ProgramAccount` structs | Checks the `target` field on the account matches the `target` field in the accounts array. |
+| `#[account(belongs_to = <target>)]` | On `ProgramAccount` structs | Checks the `target` field on the account matches the `target` field in the struct deriving `Accounts`. |
 | `#[account(owner = program \| skip)]` | On `ProgramAccount` and `AccountInfo` structs | Checks the owner of the account is the current program or skips the check. |
 | `#[account("<literal>")]` | On `ProgramAccount` structs | Executes the given code literal as a constraint. The literal should evaluate to a boolean. |
+| `#[account(rent_exempt = <skip>)]` | On `AccountInfo` or `ProgramAccount` structs | Optional attribute to skip the rent exemption check. By default, all accounts marked with `#[account(init)]` will be rent exempt. Similarly, omitting `= skip` will mark the account rent exempt. |
 
 ## License
 

+ 6 - 1
docs/src/tutorials/tutorial-1.md

@@ -39,7 +39,7 @@ pick it up.
 
 Additionally,
 notice how we take a mutable reference to `my_account` and assign the `data` to it. This leads us
-the `Initialize` struct, deriving `Accounts`. There are two things to notice about `Initialize`.
+the `Initialize` struct, deriving `Accounts`. There are three things to notice about `Initialize`.
 
 1. The `my_account` field is of type `ProgramAccount<'info, MyAccount>`, telling the program it *must*
 be **owned** by the currently executing program, and the deserialized data structure is `MyAccount`.
@@ -47,6 +47,11 @@ be **owned** by the currently executing program, and the deserialized data struc
 in one situation: when a given `ProgramAccount` is newly created and is being used by the program
 for the first time (and thus it's data field is all zero). If `#[account(init)]` is not used
 when account data is zero initialized, the transaction will be rejected.
+3. The `Rent` **sysvar** is required for the rent exemption check, which the framework enforces
+by default for any account marked with `#[account(init)]`. To be more explicit about the check,
+one can specify `#[account(init, rent_exempt)]`. To skip this check, (and thus
+allowing one to omit the `Rent` acccount) one can specify
+`#[account(init, rent_exempt = skip)]` on the account being initialized (here, `my_account`).
 
 ::: details
 All accounts created with Anchor are laid out as follows: `8-byte-discriminator || borsh

+ 1 - 0
examples/tutorial/basic-1/programs/basic-1/src/lib.rs

@@ -23,6 +23,7 @@ mod basic_1 {
 pub struct Initialize<'info> {
     #[account(init)]
     pub my_account: ProgramAccount<'info, MyAccount>,
+    pub rent: Rent,
 }
 
 #[derive(Accounts)]

+ 2 - 0
examples/tutorial/basic-1/tests/basic-1.js

@@ -36,6 +36,7 @@ describe('basic-1', () => {
     await program.rpc.initialize(new anchor.BN(1234), {
       accounts: {
         myAccount: myAccount.publicKey,
+        rent: anchor.web3.SYSVAR_RENT_PUBKEY,
       },
     });
     // #endregion code-separated
@@ -62,6 +63,7 @@ describe('basic-1', () => {
     await program.rpc.initialize(new anchor.BN(1234), {
       accounts: {
         myAccount: myAccount.publicKey,
+        rent: anchor.web3.SYSVAR_RENT_PUBKEY,
       },
       signers: [myAccount],
       instructions: [

+ 2 - 0
examples/tutorial/basic-2/programs/basic-2/src/lib.rs

@@ -52,6 +52,7 @@ mod basic_2 {
 pub struct CreateRoot<'info> {
     #[account(init)]
     pub root: ProgramAccount<'info, Root>,
+    pub rent: Rent,
 }
 
 #[derive(Accounts)]
@@ -67,6 +68,7 @@ pub struct CreateLeaf<'info> {
     pub root: ProgramAccount<'info, Root>,
     #[account(init)]
     pub leaf: ProgramAccount<'info, Leaf>,
+    pub rent: Rent,
 }
 
 #[derive(Accounts)]

+ 65 - 27
syn/src/codegen/accounts.rs

@@ -1,6 +1,6 @@
 use crate::{
     AccountsStruct, Constraint, ConstraintBelongsTo, ConstraintLiteral, ConstraintOwner,
-    ConstraintSigner, Field, SysvarTy, Ty,
+    ConstraintRentExempt, ConstraintSigner, Field, SysvarTy, Ty,
 };
 use quote::quote;
 
@@ -16,26 +16,42 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
         })
         .collect();
 
-    let (access_checks, return_tys): (
-        Vec<proc_macro2::TokenStream>,
-        Vec<proc_macro2::TokenStream>,
-    ) = accs
-        .fields
-        .iter()
-        .map(|f: &Field| {
-            let name = &f.ident;
-
-            // Account validation.
-            let access_control = generate_field(f);
-
-            // Single field in the final deserialized accounts struct.
-            let return_ty = quote! {
-                #name
-            };
+    let (deser_fields, access_checks, return_tys) = {
+        // Deserialization for each field.
+        let deser_fields: Vec<proc_macro2::TokenStream> = accs
+            .fields
+            .iter()
+            .map(generate_field_deserialization)
+            .collect();
+        // Constraint checks for each account fields.
+        let access_checks: Vec<proc_macro2::TokenStream> = accs
+            .fields
+            .iter()
+            .map(|f: &Field| {
+                let checks: Vec<proc_macro2::TokenStream> = f
+                    .constraints
+                    .iter()
+                    .map(|c| generate_constraint(&f, c))
+                    .collect();
+                quote! {
+                    #(#checks)*
+                }
+            })
+            .collect();
+        // Each field in the final deserialized accounts struct.
+        let return_tys: Vec<proc_macro2::TokenStream> = accs
+            .fields
+            .iter()
+            .map(|f: &Field| {
+                let name = &f.ident;
+                quote! {
+                    #name
+                }
+            })
+            .collect();
 
-            (access_control, return_ty)
-        })
-        .unzip();
+        (deser_fields, access_checks, return_tys)
+    };
 
     let on_save: Vec<proc_macro2::TokenStream> = accs
         .fields
@@ -73,10 +89,16 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
             fn try_accounts(program_id: &Pubkey, accounts: &[AccountInfo<'info>]) -> Result<Self, ProgramError> {
                 let acc_infos = &mut accounts.iter();
 
+                // Pull out each account info from the `accounts` slice.
                 #(#acc_infos)*
 
+                // Deserialize each account.
+                #(#deser_fields)*
+
+                // Perform constraint checks on each account.
                 #(#access_checks)*
 
+                // Success. Return the validated accounts.
                 Ok(#name {
                     #(#return_tys),*
                 })
@@ -92,7 +114,7 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
     }
 }
 
-pub fn generate_field(f: &Field) -> proc_macro2::TokenStream {
+pub fn generate_field_deserialization(f: &Field) -> proc_macro2::TokenStream {
     let ident = &f.ident;
     let assign_ty = match &f.ty {
         Ty::AccountInfo => quote! {
@@ -142,14 +164,9 @@ pub fn generate_field(f: &Field) -> proc_macro2::TokenStream {
             },
         },
     };
-    let checks: Vec<proc_macro2::TokenStream> = f
-        .constraints
-        .iter()
-        .map(|c| generate_constraint(&f, c))
-        .collect();
+
     quote! {
         #assign_ty
-        #(#checks)*
     }
 }
 
@@ -159,6 +176,7 @@ pub fn generate_constraint(f: &Field, c: &Constraint) -> proc_macro2::TokenStrea
         Constraint::Signer(c) => generate_constraint_signer(f, c),
         Constraint::Literal(c) => generate_constraint_literal(f, c),
         Constraint::Owner(c) => generate_constraint_owner(f, c),
+        Constraint::RentExempt(c) => generate_constraint_rent_exempt(f, c),
     }
 }
 
@@ -217,3 +235,23 @@ pub fn generate_constraint_owner(f: &Field, c: &ConstraintOwner) -> proc_macro2:
         },
     }
 }
+
+pub fn generate_constraint_rent_exempt(
+    f: &Field,
+    c: &ConstraintRentExempt,
+) -> proc_macro2::TokenStream {
+    let ident = &f.ident;
+    let info = match f.ty {
+        Ty::AccountInfo => quote! { #ident },
+        Ty::ProgramAccount(_) => quote! { #ident.info },
+        _ => panic!("Invalid syntax: rent exemption cannot be specified."),
+    };
+    match c {
+        ConstraintRentExempt::Skip => quote! {},
+        ConstraintRentExempt::Enforce => quote! {
+            if !rent.is_exempt(#info.lamports(), #info.try_data_len()?) {
+                return Err(ProgramError::Custom(2)); // todo: error codes
+            }
+        },
+    }
+}

+ 6 - 0
syn/src/lib.rs

@@ -101,6 +101,7 @@ pub enum Constraint {
     BelongsTo(ConstraintBelongsTo),
     Literal(ConstraintLiteral),
     Owner(ConstraintOwner),
+    RentExempt(ConstraintRentExempt),
 }
 
 pub struct ConstraintBelongsTo {
@@ -117,3 +118,8 @@ pub enum ConstraintOwner {
     Program,
     Skip,
 }
+
+pub enum ConstraintRentExempt {
+    Enforce,
+    Skip,
+}

+ 41 - 3
syn/src/parser/accounts.rs

@@ -1,6 +1,6 @@
 use crate::{
     AccountsStruct, Constraint, ConstraintBelongsTo, ConstraintLiteral, ConstraintOwner,
-    ConstraintSigner, Field, ProgramAccountTy, SysvarTy, Ty,
+    ConstraintRentExempt, ConstraintSigner, Field, ProgramAccountTy, SysvarTy, Ty,
 };
 
 pub fn parse(strct: &syn::ItemStruct) -> AccountsStruct {
@@ -61,7 +61,7 @@ fn parse_field(f: &syn::Field, anchor: Option<&syn::Attribute>) -> Field {
 fn parse_ty(f: &syn::Field) -> Ty {
     let path = match &f.ty {
         syn::Type::Path(ty_path) => ty_path.path.clone(),
-        _ => panic!("invalid type"),
+        _ => panic!("invalid account syntax"),
     };
     // TODO: allow segmented paths.
     assert!(path.segments.len() == 1);
@@ -79,7 +79,7 @@ fn parse_ty(f: &syn::Field) -> Ty {
         "StakeHistory" => Ty::Sysvar(SysvarTy::StakeHistory),
         "Instructions" => Ty::Sysvar(SysvarTy::Instructions),
         "Rewards" => Ty::Sysvar(SysvarTy::Rewards),
-        _ => panic!("invalid type"),
+        _ => panic!("invalid account type"),
     }
 }
 
@@ -119,6 +119,7 @@ fn parse_constraints(anchor: &syn::Attribute, ty: &Ty) -> (Vec<Constraint>, bool
     let mut is_signer = false;
     let mut constraints = vec![];
     let mut has_owner_constraint = false;
+    let mut is_rent_exempt = None;
 
     let mut inner_tts = g_stream.into_iter();
     while let Some(token) = inner_tts.next() {
@@ -127,6 +128,11 @@ fn parse_constraints(anchor: &syn::Attribute, ty: &Ty) -> (Vec<Constraint>, bool
                 "init" => {
                     is_init = true;
                     is_mut = true;
+                    // If it's not specified, all program owned accounts default
+                    // to being rent exempt.
+                    if is_rent_exempt.is_none() {
+                        is_rent_exempt = Some(true);
+                    }
                 }
                 "mut" => {
                     is_mut = true;
@@ -169,6 +175,30 @@ fn parse_constraints(anchor: &syn::Attribute, ty: &Ty) -> (Vec<Constraint>, bool
                     constraints.push(Constraint::Owner(constraint));
                     has_owner_constraint = true;
                 }
+                "rent_exempt" => {
+                    match inner_tts.next() {
+                        None => is_rent_exempt = Some(true),
+                        Some(tkn) => {
+                            match tkn {
+                                proc_macro2::TokenTree::Punct(punct) => {
+                                    assert!(punct.as_char() == '=');
+                                    punct
+                                }
+                                _ => panic!("invalid syntax"),
+                            };
+                            let should_skip = match inner_tts.next().unwrap() {
+                                proc_macro2::TokenTree::Ident(ident) => ident,
+                                _ => panic!("invalid syntax"),
+                            };
+                            match should_skip.to_string().as_str() {
+                                "skip" => {
+                                    is_rent_exempt = Some(false);
+                                },
+                                _ => panic!("invalid syntax: omit the rent_exempt attribute to enforce rent exemption"),
+                            };
+                        }
+                    };
+                }
                 _ => {
                     panic!("invalid syntax");
                 }
@@ -197,5 +227,13 @@ fn parse_constraints(anchor: &syn::Attribute, ty: &Ty) -> (Vec<Constraint>, bool
         }
     }
 
+    match is_rent_exempt {
+        None => constraints.push(Constraint::RentExempt(ConstraintRentExempt::Skip)),
+        Some(is_re) => match is_re {
+            false => constraints.push(Constraint::RentExempt(ConstraintRentExempt::Skip)),
+            true => constraints.push(Constraint::RentExempt(ConstraintRentExempt::Enforce)),
+        },
+    }
+
     (constraints, is_mut, is_signer, is_init)
 }