Browse Source

lang, ts: Account close constraint (#371)

Armani Ferrante 4 years ago
parent
commit
df51a27a48

+ 1 - 0
CHANGELOG.md

@@ -15,6 +15,7 @@ incremented for features.
 
 * cli: Add `--program-name` option for build command to build a single program at a time ([#362](https://github.com/project-serum/anchor/pull/362)).
 * cli, client: Parse custom cluster urls from str ([#369](https://github.com/project-serum/anchor/pull/369)).
+* lang: Add `#[account(close = <destination>)]` constraint for closing accounts and sending the rent exemption lamports to a specified destination account ([#371](https://github.com/project-serum/anchor/pull/371)).
 
 ### Fixes
 

+ 1 - 0
Cargo.lock

@@ -163,6 +163,7 @@ dependencies = [
  "solana-client",
  "solana-sdk",
  "thiserror",
+ "url",
 ]
 
 [[package]]

+ 11 - 0
examples/misc/programs/misc/src/lib.rs

@@ -82,6 +82,10 @@ pub mod misc {
         ctx.accounts.data.data = data;
         Ok(())
     }
+
+    pub fn test_close(_ctx: Context<TestClose>) -> ProgramResult {
+        Ok(())
+    }
 }
 
 #[derive(Accounts)]
@@ -117,6 +121,13 @@ pub struct TestStateCpi<'info> {
     misc2_program: AccountInfo<'info>,
 }
 
+#[derive(Accounts)]
+pub struct TestClose<'info> {
+    #[account(mut, close = sol_dest)]
+    data: ProgramAccount<'info, Data>,
+    sol_dest: AccountInfo<'info>,
+}
+
 // `my_account` is the associated token account being created.
 // `authority` must be a `mut` and `signer` since it will pay for the creation
 // of the associated token account. `state` is used as an association, i.e., one

+ 54 - 1
examples/misc/tests/misc.js

@@ -257,7 +257,60 @@ describe("misc", () => {
   });
 
   it("Can use base58 strings to fetch an account", async () => {
-    const dataAccount = await program.account.dataI16.fetch(dataPubkey.toString());
+    const dataAccount = await program.account.dataI16.fetch(
+      dataPubkey.toString()
+    );
     assert.ok(dataAccount.data === -2048);
   });
+
+  it("Should fail to close an account when sending lamports to itself", async () => {
+    try {
+      await program.rpc.testClose({
+        accounts: {
+          data: data.publicKey,
+          solDest: data.publicKey,
+        },
+      });
+      assert.ok(false);
+    } catch (err) {
+      const errMsg = "A close constraint was violated";
+      assert.equal(err.toString(), errMsg);
+      assert.equal(err.msg, errMsg);
+      assert.equal(err.code, 151);
+    }
+  });
+
+  it("Can close an account", async () => {
+    const openAccount = await program.provider.connection.getAccountInfo(
+      data.publicKey
+    );
+    assert.ok(openAccount !== null);
+
+    let beforeBalance = (
+      await program.provider.connection.getAccountInfo(
+        program.provider.wallet.publicKey
+      )
+    ).lamports;
+
+    await program.rpc.testClose({
+      accounts: {
+        data: data.publicKey,
+        solDest: program.provider.wallet.publicKey,
+      },
+    });
+
+    let afterBalance = (
+      await program.provider.connection.getAccountInfo(
+        program.provider.wallet.publicKey
+      )
+    ).lamports;
+
+    // Retrieved rent exemption sol.
+    assert.ok(afterBalance > beforeBalance);
+
+    const closedAccount = await program.provider.connection.getAccountInfo(
+      data.publicKey
+    );
+    assert.ok(closedAccount === null);
+  });
 });

+ 1 - 0
lang/derive/accounts/src/lib.rs

@@ -40,6 +40,7 @@ use syn::parse_macro_input;
 /// | `#[account(signer)]` | On raw `AccountInfo` structs. | Checks the given account signed the transaction. |
 /// | `#[account(mut)]` | On `AccountInfo`, `ProgramAccount` or `CpiAccount` 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. When using `init`, a `rent` `Sysvar` must be present in the `Accounts` struct. |
+/// | `#[account(close = <target>)]` | On `ProgramAccount` and `Loader` structs. | Marks the account as being closed at the end of the instruction's execution, sending the rent exemption lamports to the specified <target>. |
 /// | `#[account(belongs_to = <target>)]` | On `ProgramAccount` or `CpiAccount` structs | Checks the `target` field on the account matches the `target` field in the struct deriving `Accounts`. |
 /// | `#[account(has_one = <target>)]` | On `ProgramAccount` or `CpiAccount` structs | Semantically different, but otherwise the same as `belongs_to`. |
 /// | `#[account(seeds = [<seeds>])]` | On `AccountInfo` structs | Seeds for the program derived address an `AccountInfo` struct represents. |

+ 24 - 0
lang/src/common.rs

@@ -0,0 +1,24 @@
+use crate::error::ErrorCode;
+use solana_program::account_info::AccountInfo;
+use solana_program::entrypoint::ProgramResult;
+use std::io::Write;
+
+pub fn close<'info>(
+    info: AccountInfo<'info>,
+    sol_destination: AccountInfo<'info>,
+) -> ProgramResult {
+    // Transfer tokens from the account to the sol_destination.
+    let dest_starting_lamports = sol_destination.lamports();
+    **sol_destination.lamports.borrow_mut() =
+        dest_starting_lamports.checked_add(info.lamports()).unwrap();
+    **info.lamports.borrow_mut() = 0;
+
+    // Mark the account discriminator as closed.
+    let mut data = info.try_borrow_mut_data()?;
+    let dst: &mut [u8] = &mut data;
+    let mut cursor = std::io::Cursor::new(dst);
+    cursor
+        .write_all(&crate::__private::CLOSED_ACCOUNT_DISCRIMINATOR)
+        .map_err(|_| ErrorCode::AccountDidNotSerialize)?;
+    Ok(())
+}

+ 2 - 0
lang/src/error.rs

@@ -42,6 +42,8 @@ pub enum ErrorCode {
     ConstraintAssociated,
     #[msg("An associated init constraint was violated")]
     ConstraintAssociatedInit,
+    #[msg("A close constraint was violated")]
+    ConstraintClose,
 
     // Accounts.
     #[msg("The account discriminator was already set on this account")]

+ 10 - 1
lang/src/lib.rs

@@ -25,6 +25,7 @@ extern crate self as anchor_lang;
 
 use bytemuck::{Pod, Zeroable};
 use solana_program::account_info::AccountInfo;
+use solana_program::entrypoint::ProgramResult;
 use solana_program::instruction::AccountMeta;
 use solana_program::program_error::ProgramError;
 use solana_program::pubkey::Pubkey;
@@ -32,6 +33,7 @@ use std::io::Write;
 
 mod account_info;
 mod boxed;
+mod common;
 mod context;
 mod cpi_account;
 mod cpi_state;
@@ -92,7 +94,13 @@ pub trait Accounts<'info>: ToAccountMetas + ToAccountInfos<'info> + Sized {
 /// should be done here.
 pub trait AccountsExit<'info>: ToAccountMetas + ToAccountInfos<'info> {
     /// `program_id` is the currently executing program.
-    fn exit(&self, program_id: &Pubkey) -> solana_program::entrypoint::ProgramResult;
+    fn exit(&self, program_id: &Pubkey) -> ProgramResult;
+}
+
+/// The close procedure to initiate garabage collection of an account, allowing
+/// one to retrieve the rent exemption.
+pub trait AccountsClose<'info>: ToAccountInfos<'info> {
+    fn close(&self, sol_destination: AccountInfo<'info>) -> ProgramResult;
 }
 
 /// A data structure of accounts providing a one time deserialization upon
@@ -275,4 +283,5 @@ pub mod __private {
     }
 
     pub use crate::state::PROGRAM_STATE_SEED;
+    pub const CLOSED_ACCOUNT_DISCRIMINATOR: [u8; 8] = [255, 255, 255, 255, 255, 255, 255, 255];
 }

+ 8 - 1
lang/src/loader.rs

@@ -1,6 +1,7 @@
 use crate::error::ErrorCode;
 use crate::{
-    Accounts, AccountsExit, AccountsInit, ToAccountInfo, ToAccountInfos, ToAccountMetas, ZeroCopy,
+    Accounts, AccountsClose, AccountsExit, AccountsInit, ToAccountInfo, ToAccountInfos,
+    ToAccountMetas, ZeroCopy,
 };
 use solana_program::account_info::AccountInfo;
 use solana_program::entrypoint::ProgramResult;
@@ -175,6 +176,12 @@ impl<'info, T: ZeroCopy> AccountsExit<'info> for Loader<'info, T> {
     }
 }
 
+impl<'info, T: ZeroCopy> AccountsClose<'info> for Loader<'info, T> {
+    fn close(&self, sol_destination: AccountInfo<'info>) -> ProgramResult {
+        crate::common::close(self.to_account_info(), sol_destination)
+    }
+}
+
 impl<'info, T: ZeroCopy> ToAccountMetas for Loader<'info, T> {
     fn to_account_metas(&self, is_signer: Option<bool>) -> Vec<AccountMeta> {
         let is_signer = is_signer.unwrap_or(self.acc_info.is_signer);

+ 10 - 2
lang/src/program_account.rs

@@ -1,7 +1,7 @@
 use crate::error::ErrorCode;
 use crate::{
-    AccountDeserialize, AccountSerialize, Accounts, AccountsExit, AccountsInit, CpiAccount,
-    ToAccountInfo, ToAccountInfos, ToAccountMetas,
+    AccountDeserialize, AccountSerialize, Accounts, AccountsClose, AccountsExit, AccountsInit,
+    CpiAccount, ToAccountInfo, ToAccountInfos, ToAccountMetas,
 };
 use solana_program::account_info::AccountInfo;
 use solana_program::entrypoint::ProgramResult;
@@ -120,6 +120,14 @@ impl<'info, T: AccountSerialize + AccountDeserialize + Clone> AccountsExit<'info
     }
 }
 
+impl<'info, T: AccountSerialize + AccountDeserialize + Clone> AccountsClose<'info>
+    for ProgramAccount<'info, T>
+{
+    fn close(&self, sol_destination: AccountInfo<'info>) -> ProgramResult {
+        crate::common::close(self.to_account_info(), sol_destination)
+    }
+}
+
 impl<'info, T: AccountSerialize + AccountDeserialize + Clone> ToAccountMetas
     for ProgramAccount<'info, T>
 {

+ 16 - 1
lang/syn/src/codegen/accounts/constraints.rs

@@ -1,5 +1,5 @@
 use crate::{
-    CompositeField, Constraint, ConstraintAssociatedGroup, ConstraintBelongsTo,
+    CompositeField, Constraint, ConstraintAssociatedGroup, ConstraintBelongsTo, ConstraintClose,
     ConstraintExecutable, ConstraintGroup, ConstraintInit, ConstraintLiteral, ConstraintMut,
     ConstraintOwner, ConstraintRaw, ConstraintRentExempt, ConstraintSeeds, ConstraintSigner,
     ConstraintState, Field, Ty,
@@ -50,6 +50,7 @@ pub fn linearize(c_group: &ConstraintGroup) -> Vec<Constraint> {
         executable,
         state,
         associated,
+        close,
     } = c_group.clone();
 
     let mut constraints = Vec::new();
@@ -94,6 +95,9 @@ pub fn linearize(c_group: &ConstraintGroup) -> Vec<Constraint> {
     if let Some(c) = state {
         constraints.push(Constraint::State(c));
     }
+    if let Some(c) = close {
+        constraints.push(Constraint::Close(c));
+    }
     constraints
 }
 
@@ -111,6 +115,7 @@ fn generate_constraint(f: &Field, c: &Constraint) -> proc_macro2::TokenStream {
         Constraint::Executable(c) => generate_constraint_executable(f, c),
         Constraint::State(c) => generate_constraint_state(f, c),
         Constraint::AssociatedGroup(c) => generate_constraint_associated(f, c),
+        Constraint::Close(c) => generate_constraint_close(f, c),
     }
 }
 
@@ -126,6 +131,16 @@ pub fn generate_constraint_init(_f: &Field, _c: &ConstraintInit) -> proc_macro2:
     quote! {}
 }
 
+pub fn generate_constraint_close(f: &Field, c: &ConstraintClose) -> proc_macro2::TokenStream {
+    let field = &f.ident;
+    let target = &c.sol_dest;
+    quote! {
+        if #field.to_account_info().key == #target.to_account_info().key {
+            return Err(anchor_lang::__private::ErrorCode::ConstraintClose.into());
+        }
+    }
+}
+
 pub fn generate_constraint_mut(f: &Field, _c: &ConstraintMut) -> proc_macro2::TokenStream {
     let ident = &f.ident;
     quote! {

+ 15 - 5
lang/syn/src/codegen/accounts/exit.rs

@@ -19,11 +19,21 @@ pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream {
             }
             AccountField::Field(f) => {
                 let ident = &f.ident;
-                match f.constraints.is_mutable() {
-                    false => quote! {},
-                    true => quote! {
-                        anchor_lang::AccountsExit::exit(&self.#ident, program_id)?;
-                    },
+                if f.constraints.is_close() {
+                    let close_target = &f.constraints.close.as_ref().unwrap().sol_dest;
+                    quote! {
+                        anchor_lang::AccountsClose::close(
+                            &self.#ident,
+                            self.#close_target.to_account_info(),
+                        )?;
+                    }
+                } else {
+                    match f.constraints.is_mutable() {
+                        false => quote! {},
+                        true => quote! {
+                            anchor_lang::AccountsExit::exit(&self.#ident, program_id)?;
+                        },
+                    }
                 }
             }
         })

+ 12 - 1
lang/syn/src/lib.rs

@@ -257,6 +257,7 @@ pub struct ConstraintGroup {
     belongs_to: Vec<ConstraintBelongsTo>,
     literal: Vec<ConstraintLiteral>,
     raw: Vec<ConstraintRaw>,
+    close: Option<ConstraintClose>,
 }
 
 impl ConstraintGroup {
@@ -271,6 +272,10 @@ impl ConstraintGroup {
     pub fn is_signer(&self) -> bool {
         self.signer.is_some()
     }
+
+    pub fn is_close(&self) -> bool {
+        self.close.is_some()
+    }
 }
 
 // A single account constraint *after* merging all tokens into a well formed
@@ -290,6 +295,7 @@ pub enum Constraint {
     Executable(ConstraintExecutable),
     State(ConstraintState),
     AssociatedGroup(ConstraintAssociatedGroup),
+    Close(ConstraintClose),
 }
 
 // Constraint token is a single keyword in a `#[account(<TOKEN>)]` attribute.
@@ -306,7 +312,7 @@ pub enum ConstraintToken {
     Seeds(Context<ConstraintSeeds>),
     Executable(Context<ConstraintExecutable>),
     State(Context<ConstraintState>),
-    AssociatedGroup(ConstraintAssociatedGroup),
+    Close(Context<ConstraintClose>),
     Associated(Context<ConstraintAssociated>),
     AssociatedPayer(Context<ConstraintAssociatedPayer>),
     AssociatedSpace(Context<ConstraintAssociatedSpace>),
@@ -396,6 +402,11 @@ pub struct ConstraintAssociatedSpace {
     pub space: LitInt,
 }
 
+#[derive(Debug, Clone)]
+pub struct ConstraintClose {
+    pub sol_dest: Ident,
+}
+
 // Syntaxt context object for preserving metadata about the inner item.
 #[derive(Debug, Clone)]
 pub struct Context<T> {

+ 63 - 9
lang/syn/src/parser/accounts/constraints.rs

@@ -1,9 +1,9 @@
 use crate::{
     ConstraintAssociated, ConstraintAssociatedGroup, ConstraintAssociatedPayer,
-    ConstraintAssociatedSpace, ConstraintAssociatedWith, ConstraintBelongsTo, ConstraintExecutable,
-    ConstraintGroup, ConstraintInit, ConstraintLiteral, ConstraintMut, ConstraintOwner,
-    ConstraintRaw, ConstraintRentExempt, ConstraintSeeds, ConstraintSigner, ConstraintState,
-    ConstraintToken, Context,
+    ConstraintAssociatedSpace, ConstraintAssociatedWith, ConstraintBelongsTo, ConstraintClose,
+    ConstraintExecutable, ConstraintGroup, ConstraintInit, ConstraintLiteral, ConstraintMut,
+    ConstraintOwner, ConstraintRaw, ConstraintRentExempt, ConstraintSeeds, ConstraintSigner,
+    ConstraintState, ConstraintToken, Context, Ty,
 };
 use syn::ext::IdentExt;
 use syn::parse::{Error as ParseError, Parse, ParseStream, Result as ParseResult};
@@ -12,8 +12,8 @@ use syn::spanned::Spanned;
 use syn::token::Comma;
 use syn::{bracketed, Expr, Ident, LitStr, Token};
 
-pub fn parse(f: &syn::Field) -> ParseResult<ConstraintGroup> {
-    let mut constraints = ConstraintGroupBuilder::default();
+pub fn parse(f: &syn::Field, f_ty: Option<&Ty>) -> ParseResult<ConstraintGroup> {
+    let mut constraints = ConstraintGroupBuilder::new(f_ty);
     for attr in f.attrs.iter().filter(is_account) {
         for c in attr.parse_args_with(Punctuated::<ConstraintToken, Comma>::parse_terminated)? {
             constraints.add(c)?;
@@ -122,6 +122,12 @@ pub fn parse_token(stream: ParseStream) -> ParseResult<ConstraintToken> {
                         raw: stream.parse()?,
                     },
                 )),
+                "close" => ConstraintToken::Close(Context::new(
+                    span,
+                    ConstraintClose {
+                        sol_dest: stream.parse()?,
+                    },
+                )),
                 _ => Err(ParseError::new(ident.span(), "Invalid attribute"))?,
             }
         }
@@ -131,7 +137,8 @@ pub fn parse_token(stream: ParseStream) -> ParseResult<ConstraintToken> {
 }
 
 #[derive(Default)]
-pub struct ConstraintGroupBuilder {
+pub struct ConstraintGroupBuilder<'ty> {
+    pub f_ty: Option<&'ty Ty>,
     pub init: Option<Context<ConstraintInit>>,
     pub mutable: Option<Context<ConstraintMut>>,
     pub signer: Option<Context<ConstraintSigner>>,
@@ -147,9 +154,31 @@ pub struct ConstraintGroupBuilder {
     pub associated_payer: Option<Context<ConstraintAssociatedPayer>>,
     pub associated_space: Option<Context<ConstraintAssociatedSpace>>,
     pub associated_with: Vec<Context<ConstraintAssociatedWith>>,
+    pub close: Option<Context<ConstraintClose>>,
 }
 
-impl ConstraintGroupBuilder {
+impl<'ty> ConstraintGroupBuilder<'ty> {
+    pub fn new(f_ty: Option<&'ty Ty>) -> Self {
+        Self {
+            f_ty,
+            init: None,
+            mutable: None,
+            signer: None,
+            belongs_to: Vec::new(),
+            literal: Vec::new(),
+            raw: Vec::new(),
+            owner: None,
+            rent_exempt: None,
+            seeds: None,
+            executable: None,
+            state: None,
+            associated: None,
+            associated_payer: None,
+            associated_space: None,
+            associated_with: Vec::new(),
+            close: None,
+        }
+    }
     pub fn build(mut self) -> ParseResult<ConstraintGroup> {
         // Init implies mutable and rent exempt.
         if let Some(i) = &self.init {
@@ -171,6 +200,7 @@ impl ConstraintGroupBuilder {
         }
 
         let ConstraintGroupBuilder {
+            f_ty: _,
             init,
             mutable,
             signer,
@@ -186,6 +216,7 @@ impl ConstraintGroupBuilder {
             associated_payer,
             associated_space,
             associated_with,
+            close,
         } = self;
 
         // Converts Option<Context<T>> -> Option<T>.
@@ -221,6 +252,7 @@ impl ConstraintGroupBuilder {
                 payer: associated_payer.map(|p| p.target.clone()),
                 space: associated_space.map(|s| s.space.clone()),
             }),
+            close: into_inner!(close),
         })
     }
 
@@ -241,7 +273,7 @@ impl ConstraintGroupBuilder {
             ConstraintToken::AssociatedPayer(c) => self.add_associated_payer(c),
             ConstraintToken::AssociatedSpace(c) => self.add_associated_space(c),
             ConstraintToken::AssociatedWith(c) => self.add_associated_with(c),
-            ConstraintToken::AssociatedGroup(_) => panic!("Invariant violation"),
+            ConstraintToken::Close(c) => self.add_close(c),
         }
     }
 
@@ -253,6 +285,28 @@ impl ConstraintGroupBuilder {
         Ok(())
     }
 
+    fn add_close(&mut self, c: Context<ConstraintClose>) -> ParseResult<()> {
+        if !matches!(self.f_ty, Some(Ty::ProgramAccount(_)))
+            && !matches!(self.f_ty, Some(Ty::Loader(_)))
+        {
+            return Err(ParseError::new(
+                c.span(),
+                "close must be on a ProgramAccount",
+            ));
+        }
+        if self.mutable.is_none() {
+            return Err(ParseError::new(
+                c.span(),
+                "mut must be provided before close",
+            ));
+        }
+        if self.close.is_some() {
+            return Err(ParseError::new(c.span(), "close already provided"));
+        }
+        self.close.replace(c);
+        Ok(())
+    }
+
     fn add_mut(&mut self, c: Context<ConstraintMut>) -> ParseResult<()> {
         if self.mutable.is_some() {
             return Err(ParseError::new(c.span(), "mut already provided"));

+ 10 - 8
lang/syn/src/parser/accounts/mod.rs

@@ -25,24 +25,26 @@ pub fn parse(strct: &syn::ItemStruct) -> ParseResult<AccountsStruct> {
 }
 
 pub fn parse_account_field(f: &syn::Field) -> ParseResult<AccountField> {
-    let constraints = constraints::parse(f)?;
-
     let ident = f.ident.clone().unwrap();
     let account_field = match is_field_primitive(f)? {
         true => {
             let ty = parse_ty(f)?;
+            let constraints = constraints::parse(f, Some(&ty))?;
             AccountField::Field(Field {
                 ident,
                 ty,
                 constraints,
             })
         }
-        false => AccountField::CompositeField(CompositeField {
-            ident,
-            constraints,
-            symbol: ident_string(f)?,
-            raw_field: f.clone(),
-        }),
+        false => {
+            let constraints = constraints::parse(f, None)?;
+            AccountField::CompositeField(CompositeField {
+                ident,
+                constraints,
+                symbol: ident_string(f)?,
+                raw_field: f.clone(),
+            })
+        }
     };
     Ok(account_field)
 }

+ 5 - 0
ts/src/error.ts

@@ -68,6 +68,7 @@ const LangErrorCode = {
   ConstraintState: 148,
   ConstraintAssociated: 149,
   ConstraintAssociatedInit: 150,
+  ConstraintClose: 151,
 
   // Accounts.
   AccountDiscriminatorAlreadySet: 160,
@@ -130,6 +131,10 @@ const LangErrorMessage = new Map([
     LangErrorCode.ConstraintAssociatedInit,
     "An associated init constraint was violated",
   ],
+  [
+    LangErrorCode.ConstraintClose,
+    "A close constraint was violated"
+  ],
 
   // Accounts.
   [