From 20726d06bdd6fc17526731a576161ee1b70747e2 Mon Sep 17 00:00:00 2001 From: Matthew Callens Date: Sat, 30 Oct 2021 12:45:06 -0400 Subject: [PATCH] lang: Add `SystemAccount<'info>` Account Type (#954) --- .travis.yml | 1 + CHANGELOG.md | 4 + lang/src/error.rs | 2 + lang/src/lib.rs | 6 +- lang/src/system_account.rs | 94 +++++++++++++++++++ lang/syn/src/lib.rs | 8 ++ lang/syn/src/parser/accounts/mod.rs | 2 + tests/system-accounts/Anchor.toml | 9 ++ tests/system-accounts/Cargo.toml | 4 + .../programs/system-accounts/Cargo.toml | 16 ++++ .../programs/system-accounts/Xargo.toml | 2 + .../programs/system-accounts/src/lib.rs | 18 ++++ .../system-accounts/tests/system-accounts.js | 60 ++++++++++++ ts/src/error.ts | 10 +- 14 files changed, 231 insertions(+), 5 deletions(-) create mode 100644 lang/src/system_account.rs create mode 100644 tests/system-accounts/Anchor.toml create mode 100644 tests/system-accounts/Cargo.toml create mode 100644 tests/system-accounts/programs/system-accounts/Cargo.toml create mode 100644 tests/system-accounts/programs/system-accounts/Xargo.toml create mode 100644 tests/system-accounts/programs/system-accounts/src/lib.rs create mode 100644 tests/system-accounts/tests/system-accounts.js diff --git a/.travis.yml b/.travis.yml index 2d3d84457..532a6d1e3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -78,6 +78,7 @@ jobs: script: - pushd tests/escrow && yarn && anchor test && popd - pushd tests/pyth && yarn && anchor test && popd + - pushd tests/system-accounts && yarn && anchor test && popd - pushd examples/tutorial/basic-0 && anchor test && popd - pushd examples/tutorial/basic-1 && anchor test && popd - pushd examples/tutorial/basic-2 && anchor test && popd diff --git a/CHANGELOG.md b/CHANGELOG.md index c0f8712a8..1bc2d3345 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ incremented for features. ## [Unreleased] +### Features + +* lang: Add `SystemAccount<'info>` account type for generic wallet addresses or accounts owned by the system program ([#954](https://github.com/project-serum/anchor/pull/954)) + ### Fixes * cli: fix dns in NODE_OPTIONS ([#928](https://github.com/project-serum/anchor/pull/928)). diff --git a/lang/src/error.rs b/lang/src/error.rs index b3f678687..539ec272d 100644 --- a/lang/src/error.rs +++ b/lang/src/error.rs @@ -72,6 +72,8 @@ pub enum ErrorCode { InvalidProgramExecutable, #[msg("The given account did not sign")] AccountNotSigner, + #[msg("The given account is not owned by the system program")] + AccountNotSystemOwned, // State. #[msg("The given state account does not have the correct address")] diff --git a/lang/src/lib.rs b/lang/src/lib.rs index a3c063b4d..f1851cf35 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -49,6 +49,7 @@ mod program; mod program_account; mod signer; pub mod state; +mod system_account; mod system_program; mod sysvar; mod unchecked_account; @@ -75,6 +76,7 @@ pub use crate::signer::Signer; #[doc(hidden)] #[allow(deprecated)] pub use crate::state::ProgramState; +pub use crate::system_account::SystemAccount; pub use crate::system_program::System; pub use crate::sysvar::Sysvar; pub use crate::unchecked_account::UncheckedAccount; @@ -250,8 +252,8 @@ pub mod prelude { access_control, account, declare_id, emit, error, event, interface, program, require, state, zero_copy, Account, AccountDeserialize, AccountLoader, AccountSerialize, Accounts, AccountsExit, AnchorDeserialize, AnchorSerialize, Context, CpiContext, Id, Key, Loader, - Owner, Program, ProgramAccount, Signer, System, Sysvar, ToAccountInfo, ToAccountInfos, - ToAccountMetas, UncheckedAccount, + Owner, Program, ProgramAccount, Signer, System, SystemAccount, Sysvar, ToAccountInfo, + ToAccountInfos, ToAccountMetas, UncheckedAccount, }; #[allow(deprecated)] diff --git a/lang/src/system_account.rs b/lang/src/system_account.rs new file mode 100644 index 000000000..bb0c08a53 --- /dev/null +++ b/lang/src/system_account.rs @@ -0,0 +1,94 @@ +use crate::error::ErrorCode; +use crate::*; +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; +use solana_program::system_program; +use std::ops::Deref; + +#[derive(Clone)] +pub struct SystemAccount<'info> { + info: AccountInfo<'info>, +} + +impl<'info> SystemAccount<'info> { + fn new(info: AccountInfo<'info>) -> SystemAccount<'info> { + Self { info } + } + + #[inline(never)] + pub fn try_from(info: &AccountInfo<'info>) -> Result, ProgramError> { + if *info.owner != system_program::ID { + return Err(ErrorCode::AccountNotSystemOwned.into()); + } + Ok(SystemAccount::new(info.clone())) + } +} + +impl<'info> Accounts<'info> for SystemAccount<'info> { + #[inline(never)] + fn try_accounts( + _program_id: &Pubkey, + accounts: &mut &[AccountInfo<'info>], + _ix_data: &[u8], + ) -> Result { + if accounts.is_empty() { + return Err(ErrorCode::AccountNotEnoughKeys.into()); + } + let account = &accounts[0]; + *accounts = &accounts[1..]; + SystemAccount::try_from(account) + } +} + +impl<'info> AccountsExit<'info> for SystemAccount<'info> { + fn exit(&self, _program_id: &Pubkey) -> ProgramResult { + // No-op. + Ok(()) + } +} + +impl<'info> ToAccountMetas for SystemAccount<'info> { + fn to_account_metas(&self, is_signer: Option) -> Vec { + let is_signer = is_signer.unwrap_or(self.info.is_signer); + let meta = match self.info.is_writable { + false => AccountMeta::new_readonly(*self.info.key, is_signer), + true => AccountMeta::new(*self.info.key, is_signer), + }; + vec![meta] + } +} + +impl<'info> ToAccountInfos<'info> for SystemAccount<'info> { + fn to_account_infos(&self) -> Vec> { + vec![self.info.clone()] + } +} + +impl<'info> ToAccountInfo<'info> for SystemAccount<'info> { + fn to_account_info(&self) -> AccountInfo<'info> { + self.info.clone() + } +} + +impl<'info> AsRef> for SystemAccount<'info> { + fn as_ref(&self) -> &AccountInfo<'info> { + &self.info + } +} + +impl<'info> Deref for SystemAccount<'info> { + type Target = AccountInfo<'info>; + + fn deref(&self) -> &Self::Target { + &self.info + } +} + +impl<'info> Key for SystemAccount<'info> { + fn key(&self) -> Pubkey { + *self.info.key + } +} diff --git a/lang/syn/src/lib.rs b/lang/syn/src/lib.rs index b69e5d82d..ba17f4ef5 100644 --- a/lang/syn/src/lib.rs +++ b/lang/syn/src/lib.rs @@ -184,6 +184,9 @@ impl Field { Ty::Signer => quote! { Signer }, + Ty::SystemAccount => quote! { + SystemAccount + }, Ty::Account(AccountTy { boxed, .. }) => { if *boxed { quote! { @@ -294,6 +297,7 @@ impl Field { Ty::AccountInfo => quote! {}, Ty::UncheckedAccount => quote! {}, Ty::Signer => quote! {}, + Ty::SystemAccount => quote! {}, } } @@ -309,6 +313,9 @@ impl Field { Ty::Signer => quote! { Signer }, + Ty::SystemAccount => quote! { + SystemAccount + }, Ty::ProgramAccount(ty) => { let ident = &ty.account_type_path; quote! { @@ -397,6 +404,7 @@ pub enum Ty { Account(AccountTy), Program(ProgramTy), Signer, + SystemAccount, } #[derive(Debug, PartialEq)] diff --git a/lang/syn/src/parser/accounts/mod.rs b/lang/syn/src/parser/accounts/mod.rs index ceed8b999..c50db1ab0 100644 --- a/lang/syn/src/parser/accounts/mod.rs +++ b/lang/syn/src/parser/accounts/mod.rs @@ -78,6 +78,7 @@ fn is_field_primitive(f: &syn::Field) -> ParseResult { | "Account" | "Program" | "Signer" + | "SystemAccount" ); Ok(r) } @@ -100,6 +101,7 @@ fn parse_ty(f: &syn::Field) -> ParseResult { "Account" => Ty::Account(parse_account_ty(&path)?), "Program" => Ty::Program(parse_program_ty(&path)?), "Signer" => Ty::Signer, + "SystemAccount" => Ty::SystemAccount, _ => return Err(ParseError::new(f.ty.span(), "invalid account type given")), }; diff --git a/tests/system-accounts/Anchor.toml b/tests/system-accounts/Anchor.toml new file mode 100644 index 000000000..7e29585e2 --- /dev/null +++ b/tests/system-accounts/Anchor.toml @@ -0,0 +1,9 @@ +[provider] +cluster = "localnet" +wallet = "~/.config/solana/id.json" + +[programs.localnet] +system_accounts = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS" + +[scripts] +test = "mocha -t 1000000 tests/" diff --git a/tests/system-accounts/Cargo.toml b/tests/system-accounts/Cargo.toml new file mode 100644 index 000000000..a60de986d --- /dev/null +++ b/tests/system-accounts/Cargo.toml @@ -0,0 +1,4 @@ +[workspace] +members = [ + "programs/*" +] diff --git a/tests/system-accounts/programs/system-accounts/Cargo.toml b/tests/system-accounts/programs/system-accounts/Cargo.toml new file mode 100644 index 000000000..8f6360802 --- /dev/null +++ b/tests/system-accounts/programs/system-accounts/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "system-accounts" +version = "0.1.0" +description = "Created with Anchor" +edition = "2018" + +[lib] +crate-type = ["cdylib", "lib"] +name = "system_accounts" + +[features] +no-entrypoint = [] +cpi = ["no-entrypoint"] + +[dependencies] +anchor-lang = { path = "../../../../lang" } diff --git a/tests/system-accounts/programs/system-accounts/Xargo.toml b/tests/system-accounts/programs/system-accounts/Xargo.toml new file mode 100644 index 000000000..475fb71ed --- /dev/null +++ b/tests/system-accounts/programs/system-accounts/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/tests/system-accounts/programs/system-accounts/src/lib.rs b/tests/system-accounts/programs/system-accounts/src/lib.rs new file mode 100644 index 000000000..79127c926 --- /dev/null +++ b/tests/system-accounts/programs/system-accounts/src/lib.rs @@ -0,0 +1,18 @@ +use anchor_lang::prelude::*; + +declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); + +#[program] +mod system_accounts { + use super::*; + + pub fn initialize(_ctx: Context) -> ProgramResult { + Ok(()) + } +} + +#[derive(Accounts)] +pub struct Initialize<'info> { + pub authority: Signer<'info>, + pub wallet: SystemAccount<'info>, +} diff --git a/tests/system-accounts/tests/system-accounts.js b/tests/system-accounts/tests/system-accounts.js new file mode 100644 index 000000000..97a2cfa6c --- /dev/null +++ b/tests/system-accounts/tests/system-accounts.js @@ -0,0 +1,60 @@ +const anchor = require('@project-serum/anchor'); +const splToken = require('@solana/spl-token'); +const assert = require('assert'); + +describe('system_accounts', () => { + anchor.setProvider(anchor.Provider.local()); + const program = anchor.workspace.SystemAccounts; + const authority = program.provider.wallet.payer; + const wallet = anchor.web3.Keypair.generate(); + + it('Is initialized!', async () => { + const tx = await program.rpc.initialize({ + accounts: { + authority: authority.publicKey, + wallet: wallet.publicKey + }, + signers: [authority] + }); + + console.log("Your transaction signature", tx); + }); + + it('Emits an AccountNotSystemOwned error', async () => { + const mint = await splToken.Token.createMint( + program.provider.connection, + authority, + authority.publicKey, + null, + 9, + splToken.TOKEN_PROGRAM_ID, + ); + + const tokenAccount = await mint.createAssociatedTokenAccount( + wallet.publicKey + ); + + await mint.mintTo( + tokenAccount, + authority.publicKey, + [], + 1 * anchor.web3.LAMPORTS_PER_SOL, + ); + + try { + await program.rpc.initialize({ + accounts: { + authority: authority.publicKey, + wallet: tokenAccount + }, + signers: [authority] + }) + assert.ok(false); + } catch (err) { + const errMsg = 'The given account is not owned by the system program'; + assert.equal(err.toString(), errMsg); + assert.equal(err.msg, errMsg); + assert.equal(err.code, 171); + } + }); +}); diff --git a/ts/src/error.ts b/ts/src/error.ts index bf1aa7693..932ce3415 100644 --- a/ts/src/error.ts +++ b/ts/src/error.ts @@ -86,7 +86,9 @@ const LangErrorCode = { AccountNotMutable: 166, AccountNotProgramOwned: 167, InvalidProgramId: 168, - InvalidProgramIdExecutable: 169, + InvalidProgramExecutable: 169, + AccountNotSigner: 170, + AccountNotSystemOwned: 171, // State. StateInvalidAddress: 180, @@ -167,9 +169,11 @@ const LangErrorMessage = new Map([ "The given account is not owned by the executing program", ], [LangErrorCode.InvalidProgramId, "Program ID was not as expected"], + [LangErrorCode.InvalidProgramExecutable, "Program account is not executable"], + [LangErrorCode.AccountNotSigner, "The given account did not sign"], [ - LangErrorCode.InvalidProgramIdExecutable, - "Program account is not executable", + LangErrorCode.AccountNotSystemOwned, + "The given account is not owned by the system program", ], // State.