From 423ddde30af8ba7de29e3de09a77e8a1254d9766 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 8 Dec 2021 22:50:08 +0100 Subject: [PATCH] lang: Always execute constraints for init_if_needed (#1096) --- CHANGELOG.md | 20 +- client/example/Cargo.toml | 2 +- client/example/src/main.rs | 2 +- lang/src/error.rs | 23 +- lang/src/lib.rs | 6 +- lang/syn/src/codegen/accounts/constraints.rs | 68 ++- lang/syn/src/idl/file.rs | 2 +- lang/syn/src/parser/accounts/constraints.rs | 2 +- .../tests/bpf-upgradable-state.ts | 8 +- tests/errors/tests/errors.js | 14 +- tests/lockup/tests/lockup.js | 8 +- tests/misc/programs/misc/src/context.rs | 74 +++- tests/misc/programs/misc/src/lib.rs | 25 ++ tests/misc/tests/misc.js | 404 +++++++++++++++++- .../system-accounts/tests/system-accounts.js | 2 +- ts/src/error.ts | 86 ++-- 16 files changed, 668 insertions(+), 78 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16b43a02a5..c424a5dafc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,17 +14,22 @@ incremented for features. ### Fixes * lang: Add `deprecated` attribute to `ProgramAccount` ([#1014](https://github.com/project-serum/anchor/pull/1014)). -* cli: Add version number from programs `Cargo.toml` into extracted IDL -* lang: Add `deprecated` attribute to `Loader`([#1078](https://github.com/project-serum/anchor/pull/1078)) +* cli: Add version number from programs `Cargo.toml` into extracted IDL ([#1061](https://github.com/project-serum/anchor/pull/1061)). +* lang: Add `deprecated` attribute to `Loader`([#1078](https://github.com/project-serum/anchor/pull/1078)). +* lang: the `init_if_needed` attribute now checks that given attributes (e.g. space, owner, token::authority etc.) are validated even when init is not needed ([#1096](https://github.com/project-serum/anchor/pull/1096)). ### Features -* lang: Add `ErrorCode::AccountNotInitialized` error to separate the situation when the account has the wrong owner from when it does not exist (#[1024](https://github.com/project-serum/anchor/pull/1024)) -* lang: Called instructions now log their name by default. This can be turned off with the `no-log-ix-name` flag ([#1057](https://github.com/project-serum/anchor/pull/1057)) -* ts: Add `getAccountInfo` helper method to account namespace/client ([#1084](https://github.com/project-serum/anchor/pull/1084)) -* lang: `ProgramData` and `UpgradableLoaderState` can now be passed into `Account` as generics. see [UpgradeableLoaderState](https://docs.rs/solana-program/latest/solana_program/bpf_loader_upgradeable/enum.UpgradeableLoaderState.html). `UpgradableLoaderState` can also be matched on to get `ProgramData`, but when `ProgramData` is used instead, anchor does the serialization and checking that it is actually program data for you ([#1095](https://github.com/project-serum/anchor/pull/1095)) -* ts: Add better error msgs in the ts client if something wrong (i.e. not a pubkey or a string) is passed in as an account in an instruction accounts object ([#1098](https://github.com/project-serum/anchor/pull/1098)) +* lang: Add `ErrorCode::AccountNotInitialized` error to separate the situation when the account has the wrong owner from when it does not exist (#[1024](https://github.com/project-serum/anchor/pull/1024)). +* lang: Called instructions now log their name by default. This can be turned off with the `no-log-ix-name` flag ([#1057](https://github.com/project-serum/anchor/pull/1057)). +* lang: `ProgramData` and `UpgradableLoaderState` can now be passed into `Account` as generics. see [UpgradeableLoaderState](https://docs.rs/solana-program/latest/solana_program/bpf_loader_upgradeable/enum.UpgradeableLoaderState.html). `UpgradableLoaderState` can also be matched on to get `ProgramData`, but when `ProgramData` is used instead, anchor does the serialization and checking that it is actually program data for you ([#1095](https://github.com/project-serum/anchor/pull/1095)). +* ts: Add better error msgs in the ts client if something wrong (i.e. not a pubkey or a string) is passed in as an account in an instruction accounts object ([#1098](https://github.com/project-serum/anchor/pull/1098)). +* ts: Add inputs `postInstructions` and `preInstructions` as a replacement for (the now deprecated) `instructions` ([#1007](https://github.com/project-serum/anchor/pull/1007)). +* ts: Add `getAccountInfo` helper method to account namespace/client ([#1084](https://github.com/project-serum/anchor/pull/1084)). +### Breaking + +* lang, ts: Error codes have been mapped to new numbers to allow for more errors per namespace ([#1096](https://github.com/project-serum/anchor/pull/1096)). ## [0.18.2] - 2021-11-14 @@ -33,7 +38,6 @@ incremented for features. ### 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)) -* ts: Add inputs `postInstructions` and `preInstructions` as a replacement for (the now deprecated) `instructions` ### Fixes diff --git a/client/example/Cargo.toml b/client/example/Cargo.toml index 8058969bd6..d34fa48729 100644 --- a/client/example/Cargo.toml +++ b/client/example/Cargo.toml @@ -15,5 +15,5 @@ events = { path = "../../tests/events/programs/events", features = ["no-entrypoi shellexpand = "2.1.0" anyhow = "1.0.32" rand = "0.7.3" -clap = "3.0.0-beta.5" +clap = { version = "3.0.0-rc.0", features = ["derive"] } solana-sdk = "1.7.11" \ No newline at end of file diff --git a/client/example/src/main.rs b/client/example/src/main.rs index 29aa16ba5a..6ce0b536df 100644 --- a/client/example/src/main.rs +++ b/client/example/src/main.rs @@ -16,8 +16,8 @@ use events::MyEvent; use basic_4::accounts as basic_4_accounts; use basic_4::basic_4::Counter as CounterState; use basic_4::instruction as basic_4_instruction; -// The `accounts` and `instructions` modules are generated by the framework. use clap::Parser; +// The `accounts` and `instructions` modules are generated by the framework. use composite::accounts::{Bar, CompositeUpdate, Foo, Initialize}; use composite::instruction as composite_instruction; use composite::{DummyA, DummyB}; diff --git a/lang/src/error.rs b/lang/src/error.rs index 70a7ca977e..3f895736fb 100644 --- a/lang/src/error.rs +++ b/lang/src/error.rs @@ -15,13 +15,13 @@ pub enum ErrorCode { // IDL instructions. #[msg("The program was compiled without idl instructions")] - IdlInstructionStub = 120, + IdlInstructionStub = 1000, #[msg("Invalid program given to the IDL instruction")] IdlInstructionInvalidProgram, // Constraints. #[msg("A mut constraint was violated")] - ConstraintMut = 140, + ConstraintMut = 2000, #[msg("A has one constraint was violated")] ConstraintHasOne, #[msg("A signer constraint as violated")] @@ -48,10 +48,23 @@ pub enum ErrorCode { ConstraintAddress, #[msg("Expected zero account discriminant")] ConstraintZero, + #[msg("A token mint constraint was violated")] + ConstraintTokenMint, + #[msg("A token owner constraint was violated")] + ConstraintTokenOwner, + // The mint mint is intentional -> a mint authority for the mint. + #[msg("A mint mint authority constraint was violated")] + ConstraintMintMintAuthority, + #[msg("A mint freeze authority constraint was violated")] + ConstraintMintFreezeAuthority, + #[msg("A mint decimals constraint was violated")] + ConstraintMintDecimals, + #[msg("A space constraint was violated")] + ConstraintSpace, // Accounts. #[msg("The account discriminator was already set on this account")] - AccountDiscriminatorAlreadySet = 160, + AccountDiscriminatorAlreadySet = 3000, #[msg("No 8 byte discriminator was found on the account")] AccountDiscriminatorNotFound, #[msg("8 byte discriminator did not match what was expected")] @@ -81,9 +94,9 @@ pub enum ErrorCode { // State. #[msg("The given state account does not have the correct address")] - StateInvalidAddress = 180, + StateInvalidAddress = 4000, // Used for APIs that shouldn't be used anymore. #[msg("The API being used is deprecated and should no longer be used")] - Deprecated = 299, + Deprecated = 5000, } diff --git a/lang/src/lib.rs b/lang/src/lib.rs index b570c22b70..cbfc246446 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -183,8 +183,8 @@ pub trait AccountDeserialize: Sized { /// uninitialized accounts, where the bytes are zeroed. Implementations /// should be unique to a particular account type so that one can never /// successfully deserialize the data of one account type into another. - /// For example, if the SPL token program where to implement this trait, - /// it should impossible to deserialize a `Mint` account into a token + /// For example, if the SPL token program were to implement this trait, + /// it should be impossible to deserialize a `Mint` account into a token /// `Account`. fn try_deserialize(buf: &mut &[u8]) -> Result; @@ -303,7 +303,7 @@ pub mod __private { } // The starting point for user defined error codes. - pub const ERROR_CODE_OFFSET: u32 = 300; + pub const ERROR_CODE_OFFSET: u32 = 6000; // Calculates the size of an account, which may be larger than the deserialized // data in it. This trait is currently only used for `#[state]` accounts. diff --git a/lang/syn/src/codegen/accounts/constraints.rs b/lang/syn/src/codegen/accounts/constraints.rs index a02ef0c7e4..86087851fc 100644 --- a/lang/syn/src/codegen/accounts/constraints.rs +++ b/lang/syn/src/codegen/accounts/constraints.rs @@ -449,6 +449,14 @@ pub fn generate_init( } let pa: #ty_decl = #from_account_info; + if !(!#if_needed || #field.to_account_info().owner == &anchor_lang::solana_program::system_program::ID) { + if pa.mint != #mint.key() { + return Err(anchor_lang::__private::ErrorCode::ConstraintTokenMint.into()); + } + if pa.owner != #owner.key() { + return Err(anchor_lang::__private::ErrorCode::ConstraintTokenOwner.into()); + } + } pa }; } @@ -473,6 +481,14 @@ pub fn generate_init( anchor_spl::associated_token::create(cpi_ctx)?; } let pa: #ty_decl = #from_account_info; + if !(!#if_needed || #field.to_account_info().owner == &anchor_lang::solana_program::system_program::ID) { + if pa.mint != #mint.key() { + return Err(anchor_lang::__private::ErrorCode::ConstraintTokenMint.into()); + } + if pa.owner != #owner.key() { + return Err(anchor_lang::__private::ErrorCode::ConstraintTokenOwner.into()); + } + } pa }; } @@ -489,8 +505,8 @@ pub fn generate_init( seeds_with_nonce, ); let freeze_authority = match freeze_authority { - Some(fa) => quote! { Some(&#fa.key()) }, - None => quote! { None }, + Some(fa) => quote! { Option::<&anchor_lang::prelude::Pubkey>::Some(&#fa.key()) }, + None => quote! { Option::<&anchor_lang::prelude::Pubkey>::None }, }; quote! { let #field: #ty_decl = { @@ -508,9 +524,23 @@ pub fn generate_init( rent: rent.to_account_info(), }; let cpi_ctx = CpiContext::new(cpi_program, accounts); - anchor_spl::token::initialize_mint(cpi_ctx, #decimals, &#owner.to_account_info().key, #freeze_authority)?; + anchor_spl::token::initialize_mint(cpi_ctx, #decimals, &#owner.key(), #freeze_authority)?; } let pa: #ty_decl = #from_account_info; + if !(!#if_needed || #field.to_account_info().owner == &anchor_lang::solana_program::system_program::ID) { + if pa.mint_authority != anchor_lang::solana_program::program_option::COption::Some(#owner.key()) { + return Err(anchor_lang::__private::ErrorCode::ConstraintMintMintAuthority.into()); + } + if pa.freeze_authority + .as_ref() + .map(|fa| #freeze_authority.as_ref().map(|expected_fa| fa != *expected_fa).unwrap_or(true)) + .unwrap_or(#freeze_authority.is_some()) { + return Err(anchor_lang::__private::ErrorCode::ConstraintMintFreezeAuthority.into()); + } + if pa.decimals != #decimals { + return Err(anchor_lang::__private::ErrorCode::ConstraintMintDecimals.into()); + } + } pa }; } @@ -550,16 +580,42 @@ pub fn generate_init( &#o }, }; + let pda_check = if !seeds_with_nonce.is_empty() { + quote! { + let expected_key = anchor_lang::prelude::Pubkey::create_program_address( + #seeds_with_nonce, + #owner + ).map_err(|_| anchor_lang::__private::ErrorCode::ConstraintSeeds)?; + if expected_key != #field.key() { + return Err(anchor_lang::__private::ErrorCode::ConstraintSeeds.into()); + } + } + } else { + quote! {} + }; let create_account = - generate_create_account(field, quote! {space}, owner, seeds_with_nonce); + generate_create_account(field, quote! {space}, owner.clone(), seeds_with_nonce); quote! { let #field = { - if !#if_needed || #field.to_account_info().owner == &anchor_lang::solana_program::system_program::ID { - #space + let actual_field = #field.to_account_info(); + let actual_owner = actual_field.owner; + #space + if !#if_needed || actual_owner == &anchor_lang::solana_program::system_program::ID { #payer #create_account } let pa: #ty_decl = #from_account_info; + if !(!#if_needed || actual_owner == &anchor_lang::solana_program::system_program::ID) { + if space != actual_field.data_len() { + return Err(anchor_lang::__private::ErrorCode::ConstraintSpace.into()); + } + + if actual_owner != #owner { + return Err(anchor_lang::__private::ErrorCode::ConstraintOwner.into()); + } + + #pda_check + } pa }; } diff --git a/lang/syn/src/idl/file.rs b/lang/syn/src/idl/file.rs index 689d133889..ca90525a80 100644 --- a/lang/syn/src/idl/file.rs +++ b/lang/syn/src/idl/file.rs @@ -11,7 +11,7 @@ use std::path::Path; const DERIVE_NAME: &str = "Accounts"; // TODO: sharee this with `anchor_lang` crate. -const ERROR_CODE_OFFSET: u32 = 300; +const ERROR_CODE_OFFSET: u32 = 6000; // Parse an entire interface file. pub fn parse(filename: impl AsRef, version: String) -> Result> { diff --git a/lang/syn/src/parser/accounts/constraints.rs b/lang/syn/src/parser/accounts/constraints.rs index 119814d5bf..3915f003dc 100644 --- a/lang/syn/src/parser/accounts/constraints.rs +++ b/lang/syn/src/parser/accounts/constraints.rs @@ -432,7 +432,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> { if self.token_mint.is_none() { return Err(ParseError::new( token_authority.span(), - "token authority must be provided if token mint is", + "token mint must be provided if token authority is", )); } } diff --git a/tests/bpf-upgradeable-state/tests/bpf-upgradable-state.ts b/tests/bpf-upgradeable-state/tests/bpf-upgradable-state.ts index eef41fe03f..c2a1efb483 100644 --- a/tests/bpf-upgradeable-state/tests/bpf-upgradable-state.ts +++ b/tests/bpf-upgradeable-state/tests/bpf-upgradable-state.ts @@ -53,7 +53,7 @@ describe('bpf_upgradeable_state', () => { }); assert.ok(false); } catch (err) { - assert.equal(err.code, 143); + assert.equal(err.code, 2003); assert.equal(err.msg, "A raw constraint was violated"); } }); @@ -73,7 +73,7 @@ describe('bpf_upgradeable_state', () => { }); assert.ok(false); } catch (err) { - assert.equal(err.code, 173); + assert.equal(err.code, 3013); assert.equal(err.msg, "The given account is not a program data account"); } }); @@ -93,7 +93,7 @@ describe('bpf_upgradeable_state', () => { }); assert.ok(false); } catch (err) { - assert.equal(err.code, 167); + assert.equal(err.code, 3007); assert.equal(err.msg, "The given account is not owned by the executing program"); } }); @@ -119,7 +119,7 @@ describe('bpf_upgradeable_state', () => { }); assert.ok(false); } catch (err) { - assert.equal(err.code, 300); + assert.equal(err.code, 6000); } }); }); diff --git a/tests/errors/tests/errors.js b/tests/errors/tests/errors.js index 8880578d91..c79bb1c643 100644 --- a/tests/errors/tests/errors.js +++ b/tests/errors/tests/errors.js @@ -17,7 +17,7 @@ describe("errors", () => { "This is an error message clients will automatically display"; assert.equal(err.toString(), errMsg); assert.equal(err.msg, errMsg); - assert.equal(err.code, 300); + assert.equal(err.code, 6000); } }); @@ -29,7 +29,7 @@ describe("errors", () => { const errMsg = "HelloNoMsg"; assert.equal(err.toString(), errMsg); assert.equal(err.msg, errMsg); - assert.equal(err.code, 300 + 123); + assert.equal(err.code, 6000 + 123); } }); @@ -41,7 +41,7 @@ describe("errors", () => { const errMsg = "HelloNext"; assert.equal(err.toString(), errMsg); assert.equal(err.msg, errMsg); - assert.equal(err.code, 300 + 124); + assert.equal(err.code, 6000 + 124); } }); @@ -57,7 +57,7 @@ describe("errors", () => { const errMsg = "A mut constraint was violated"; assert.equal(err.toString(), errMsg); assert.equal(err.msg, errMsg); - assert.equal(err.code, 140); + assert.equal(err.code, 2000); } }); @@ -80,7 +80,7 @@ describe("errors", () => { const errMsg = "A has_one constraint was violated"; assert.equal(err.toString(), errMsg); assert.equal(err.msg, errMsg); - assert.equal(err.code, 141); + assert.equal(err.code, 2001); } }); @@ -108,7 +108,7 @@ describe("errors", () => { assert.ok(false); } catch (err) { const errMsg = - "Error: failed to send transaction: Transaction simulation failed: Error processing Instruction 0: custom program error: 0x8e"; + "Error: failed to send transaction: Transaction simulation failed: Error processing Instruction 0: custom program error: 0x7d2"; assert.equal(err.toString(), errMsg); } }); @@ -125,7 +125,7 @@ describe("errors", () => { const errMsg = "HelloCustom"; assert.equal(err.toString(), errMsg); assert.equal(err.msg, errMsg); - assert.equal(err.code, 300 + 125); + assert.equal(err.code, 6000 + 125); } }); diff --git a/tests/lockup/tests/lockup.js b/tests/lockup/tests/lockup.js index 5415820b92..b825af3c98 100644 --- a/tests/lockup/tests/lockup.js +++ b/tests/lockup/tests/lockup.js @@ -118,7 +118,7 @@ describe("Lockup and Registry", () => { await lockup.state.rpc.whitelistAdd(e, { accounts }); }, (err) => { - assert.equal(err.code, 308); + assert.equal(err.code, 6008); assert.equal(err.msg, "Whitelist is full"); return true; } @@ -218,7 +218,7 @@ describe("Lockup and Registry", () => { }); }, (err) => { - assert.equal(err.code, 307); + assert.equal(err.code, 6007); assert.equal(err.msg, "Insufficient withdrawal balance."); return true; } @@ -783,7 +783,7 @@ describe("Lockup and Registry", () => { (err) => { // Solana doesn't propagate errors across CPI. So we receive the registry's error code, // not the lockup's. - const errorCode = "custom program error: 0x140"; + const errorCode = "custom program error: 0x1784"; assert.ok(err.toString().split(errorCode).length === 2); return true; } @@ -865,7 +865,7 @@ describe("Lockup and Registry", () => { await tryEndUnstake(); }, (err) => { - assert.equal(err.code, 309); + assert.equal(err.code, 6009); assert.equal(err.msg, "The unstake timelock has not yet expired."); return true; } diff --git a/tests/misc/programs/misc/src/context.rs b/tests/misc/programs/misc/src/context.rs index 86c8421bcf..6caa1e9ab9 100644 --- a/tests/misc/programs/misc/src/context.rs +++ b/tests/misc/programs/misc/src/context.rs @@ -245,14 +245,86 @@ pub struct TestEmptySeedsConstraint<'info> { pub pda: AccountInfo<'info>, } +#[derive(Accounts)] +pub struct InitWithSpace<'info> { + #[account(init, payer = payer)] + pub data: Account<'info, DataU16>, + pub payer: Signer<'info>, + pub system_program: Program<'info, System>, +} + #[derive(Accounts)] pub struct TestInitIfNeeded<'info> { - #[account(init_if_needed, payer = payer)] + #[account(init_if_needed, payer = payer, space = 500)] pub data: Account<'info, DataU16>, pub payer: Signer<'info>, pub system_program: Program<'info, System>, } +#[derive(Accounts)] +pub struct TestInitIfNeededChecksOwner<'info> { + #[account(init_if_needed, payer = payer, space = 100, owner = *owner.key, seeds = [b"hello"], bump)] + pub data: UncheckedAccount<'info>, + pub payer: Signer<'info>, + pub system_program: Program<'info, System>, + pub owner: AccountInfo<'info> +} + +#[derive(Accounts)] +#[instruction(seed_data: String)] +pub struct TestInitIfNeededChecksSeeds<'info> { + #[account(init_if_needed, payer = payer, space = 100, seeds = [seed_data.as_bytes()], bump)] + pub data: UncheckedAccount<'info>, + #[account(mut)] + pub payer: Signer<'info>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +#[instruction(decimals: u8)] +pub struct TestInitMintIfNeeded<'info> { + #[account(init_if_needed, mint::decimals = decimals, mint::authority = mint_authority, mint::freeze_authority = freeze_authority, payer = payer)] + pub mint: Account<'info, Mint>, + #[account(signer)] + pub payer: AccountInfo<'info>, + pub rent: Sysvar<'info, Rent>, + pub system_program: AccountInfo<'info>, + pub token_program: AccountInfo<'info>, + pub mint_authority: AccountInfo<'info>, + pub freeze_authority: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct TestInitTokenIfNeeded<'info> { + #[account(init_if_needed, token::mint = mint, token::authority = authority, payer = payer)] + pub token: Account<'info, TokenAccount>, + pub mint: Account<'info, Mint>, + #[account(signer)] + pub payer: AccountInfo<'info>, + pub rent: Sysvar<'info, Rent>, + pub system_program: AccountInfo<'info>, + pub token_program: AccountInfo<'info>, + pub authority: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct TestInitAssociatedTokenIfNeeded<'info> { + #[account( + init_if_needed, + payer = payer, + associated_token::mint = mint, + associated_token::authority = authority, + )] + pub token: Account<'info, TokenAccount>, + pub mint: Account<'info, Mint>, + pub payer: Signer<'info>, + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, + pub token_program: Program<'info, Token>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub authority: AccountInfo<'info> +} + #[derive(Accounts)] pub struct TestMultidimensionalArray<'info> { #[account(zero)] diff --git a/tests/misc/programs/misc/src/lib.rs b/tests/misc/programs/misc/src/lib.rs index 90d9d198dc..6a49ae1f6f 100644 --- a/tests/misc/programs/misc/src/lib.rs +++ b/tests/misc/programs/misc/src/lib.rs @@ -196,6 +196,31 @@ pub mod misc { Ok(()) } + pub fn test_init_if_needed_checks_owner(ctx: Context) -> ProgramResult { + Ok(()) + } + + pub fn test_init_if_needed_checks_seeds(ctx: Context, seed_data: String) -> ProgramResult { + Ok(()) + } + + pub fn test_init_mint_if_needed(ctx: Context, decimals: u8) -> ProgramResult { + Ok(()) + } + + pub fn test_init_token_if_needed(ctx: Context) -> ProgramResult { + Ok(()) + } + + pub fn test_init_associated_token_if_needed(ctx: Context) -> ProgramResult { + Ok(()) + } + + pub fn init_with_space(ctx: Context, data: u16) -> ProgramResult { + Ok(()) + } + + pub fn test_multidimensional_array( ctx: Context, data: [[u8; 10]; 10], diff --git a/tests/misc/tests/misc.js b/tests/misc/tests/misc.js index 81509d62a8..992a517cfc 100644 --- a/tests/misc/tests/misc.js +++ b/tests/misc/tests/misc.js @@ -7,6 +7,7 @@ const { Token, } = require("@solana/spl-token"); const miscIdl = require("../target/idl/misc.json"); +const utf8 = anchor.utils.bytes.utf8; describe("misc", () => { // Configure the client to use the local cluster. @@ -214,7 +215,7 @@ describe("misc", () => { const errMsg = "A close constraint was violated"; assert.equal(err.toString(), errMsg); assert.equal(err.msg, errMsg); - assert.equal(err.code, 151); + assert.equal(err.code, 2011); } }); @@ -667,7 +668,7 @@ describe("misc", () => { }); }, (err) => { - assert.equal(err.code, 149); + assert.equal(err.code, 2009); return true; } ); @@ -802,7 +803,7 @@ describe("misc", () => { }, }), (err) => { - assert.equal(err.code, 146); + assert.equal(err.code, 2006); return true; } ); @@ -859,6 +860,403 @@ describe("misc", () => { ); }); + it("init_if_needed throws if account exists but is not owned by the expected program", async () => { + const newAcc = await anchor.web3.PublicKey.findProgramAddress([utf8.encode("hello")], program.programId); + await program.rpc.testInitIfNeededChecksOwner({ + accounts: { + data: newAcc[0], + systemProgram: anchor.web3.SystemProgram.programId, + payer: program.provider.wallet.publicKey, + owner: program.programId + } + }); + + try { + await program.rpc.testInitIfNeededChecksOwner({ + accounts: { + data: newAcc[0], + systemProgram: anchor.web3.SystemProgram.programId, + payer: program.provider.wallet.publicKey, + owner: anchor.web3.Keypair.generate().publicKey + }, + }); + assert.ok(false); + } catch (err) { + assert.equal(err.code, 2004); + } + }); + + it("init_if_needed throws if pda account exists but does not have the expected seeds", async () => { + const newAcc = await anchor.web3.PublicKey.findProgramAddress([utf8.encode("nothello")], program.programId); + await program.rpc.testInitIfNeededChecksSeeds("nothello", { + accounts: { + data: newAcc[0], + systemProgram: anchor.web3.SystemProgram.programId, + payer: program.provider.wallet.publicKey, + } + }); + + // this will throw if it is not a proper PDA + // we need this so we know that the following tx failed + // not because it couldn't create this pda + // but because the two pdas were different + anchor.web3.PublicKey.createProgramAddress([utf8.encode("hello")], program.programId); + + try { + await program.rpc.testInitIfNeededChecksSeeds("hello", { + accounts: { + data: newAcc[0], + systemProgram: anchor.web3.SystemProgram.programId, + payer: program.provider.wallet.publicKey, + owner: anchor.web3.Keypair.generate().publicKey + }, + }); + assert.ok(false); + } catch (err) { + assert.equal(err.code, 2006); + } + }); + + + + it("init_if_needed throws if account exists but is not the expected space", async () => { + const newAcc = anchor.web3.Keypair.generate(); + await program.rpc.initWithSpace(3, { + accounts: { + data: newAcc.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + payer: program.provider.wallet.publicKey, + }, + signers: [newAcc], + }); + + try { + await program.rpc.testInitIfNeeded(3, { + accounts: { + data: newAcc.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + payer: program.provider.wallet.publicKey, + }, + signers: [newAcc], + }); + assert.ok(false); + } catch (err) { + assert.equal(err.code, 2019); + } + }); + + it("init_if_needed throws if mint exists but has the wrong mint authority", async () => { + const mint = anchor.web3.Keypair.generate(); + await program.rpc.testInitMint({ + accounts: { + mint: mint.publicKey, + payer: program.provider.wallet.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }, + signers: [mint], + }); + + try { + await program.rpc.testInitMintIfNeeded(6,{ + accounts: { + mint: mint.publicKey, + payer: program.provider.wallet.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + mintAuthority: anchor.web3.Keypair.generate().publicKey, + freezeAuthority: program.provider.wallet.publicKey + }, + signers: [mint], + }); + assert.ok(false); + } catch (err) { + assert.equal(err.code, 2016); + } + }); + + it("init_if_needed throws if mint exists but has the wrong freeze authority", async () => { + const mint = anchor.web3.Keypair.generate(); + await program.rpc.testInitMint({ + accounts: { + mint: mint.publicKey, + payer: program.provider.wallet.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }, + signers: [mint], + }); + + try { + await program.rpc.testInitMintIfNeeded(6,{ + accounts: { + mint: mint.publicKey, + payer: program.provider.wallet.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + mintAuthority: program.provider.wallet.publicKey, + freezeAuthority: anchor.web3.Keypair.generate().publicKey + }, + signers: [mint], + }); + assert.ok(false); + } catch (err) { + assert.equal(err.code, 2017); + } + }); + + it("init_if_needed throws if mint exists but has the wrong decimals", async () => { + const mint = anchor.web3.Keypair.generate(); + await program.rpc.testInitMint({ + accounts: { + mint: mint.publicKey, + payer: program.provider.wallet.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }, + signers: [mint], + }); + + try { + await program.rpc.testInitMintIfNeeded(9,{ + accounts: { + mint: mint.publicKey, + payer: program.provider.wallet.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + mintAuthority: program.provider.wallet.publicKey, + freezeAuthority: program.provider.wallet.publicKey + }, + signers: [mint], + }); + assert.ok(false); + } catch (err) { + assert.equal(err.code, 2018); + } + }); + + it("init_if_needed throws if token exists but has the wrong owner", async () => { + const mint = anchor.web3.Keypair.generate(); + await program.rpc.testInitMint({ + accounts: { + mint: mint.publicKey, + payer: program.provider.wallet.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }, + signers: [mint], + }); + + const token = anchor.web3.Keypair.generate(); + await program.rpc.testInitToken({ + accounts: { + token: token.publicKey, + mint: mint.publicKey, + payer: program.provider.wallet.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }, + signers: [token], + }); + + try { + await program.rpc.testInitTokenIfNeeded({ + accounts: { + token: token.publicKey, + mint: mint.publicKey, + payer: program.provider.wallet.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + authority: anchor.web3.Keypair.generate().publicKey, + }, + signers: [token], + }); + assert.ok(false); + } catch (err) { + assert.equal(err.code, 2015); + } + }); + + it("init_if_needed throws if token exists but has the wrong mint", async () => { + const mint = anchor.web3.Keypair.generate(); + await program.rpc.testInitMint({ + accounts: { + mint: mint.publicKey, + payer: program.provider.wallet.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }, + signers: [mint], + }); + + const mint2 = anchor.web3.Keypair.generate(); + await program.rpc.testInitMint({ + accounts: { + mint: mint2.publicKey, + payer: program.provider.wallet.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }, + signers: [mint2], + }); + + const token = anchor.web3.Keypair.generate(); + await program.rpc.testInitToken({ + accounts: { + token: token.publicKey, + mint: mint.publicKey, + payer: program.provider.wallet.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }, + signers: [token], + }); + + try { + await program.rpc.testInitTokenIfNeeded({ + accounts: { + token: token.publicKey, + mint: mint2.publicKey, + payer: program.provider.wallet.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + authority: program.provider.wallet.publicKey, + }, + signers: [token], + }); + assert.ok(false); + } catch (err) { + assert.equal(err.code, 2014); + } + }); + + it("init_if_needed throws if associated token exists but has the wrong owner", async () => { + const mint = anchor.web3.Keypair.generate(); + await program.rpc.testInitMint({ + accounts: { + mint: mint.publicKey, + payer: program.provider.wallet.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }, + signers: [mint], + }); + + const associatedToken = await Token.getAssociatedTokenAddress( + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, + mint.publicKey, + program.provider.wallet.publicKey + ); + + await program.rpc.testInitAssociatedToken({ + accounts: { + token: associatedToken, + mint: mint.publicKey, + payer: program.provider.wallet.publicKey, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }, + }); + + try { + await program.rpc.testInitAssociatedTokenIfNeeded({ + accounts: { + token: associatedToken, + mint: mint.publicKey, + payer: program.provider.wallet.publicKey, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + authority: anchor.web3.Keypair.generate().publicKey + }, + }); + assert.ok(false); + } catch (err) { + assert.equal(err.code, 2015); + } + }) + + it("init_if_needed throws if associated token exists but has the wrong mint", async () => { + const mint = anchor.web3.Keypair.generate(); + await program.rpc.testInitMint({ + accounts: { + mint: mint.publicKey, + payer: program.provider.wallet.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }, + signers: [mint], + }); + + const mint2 = anchor.web3.Keypair.generate(); + await program.rpc.testInitMint({ + accounts: { + mint: mint2.publicKey, + payer: program.provider.wallet.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }, + signers: [mint2], + }); + + const associatedToken = await Token.getAssociatedTokenAddress( + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, + mint.publicKey, + program.provider.wallet.publicKey + ); + + await program.rpc.testInitAssociatedToken({ + accounts: { + token: associatedToken, + mint: mint.publicKey, + payer: program.provider.wallet.publicKey, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }, + }); + + try { + await program.rpc.testInitAssociatedTokenIfNeeded({ + accounts: { + token: associatedToken, + mint: mint2.publicKey, + payer: program.provider.wallet.publicKey, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + authority: program.provider.wallet.publicKey + }, + }); + assert.ok(false); + } catch (err) { + assert.equal(err.code, 2014); + } + }) + it("Can use multidimensional array", async () => { const array2d = new Array(10).fill(new Array(10).fill(99)); const data = anchor.web3.Keypair.generate(); diff --git a/tests/system-accounts/tests/system-accounts.js b/tests/system-accounts/tests/system-accounts.js index 97a2cfa6c9..9930ab70c2 100644 --- a/tests/system-accounts/tests/system-accounts.js +++ b/tests/system-accounts/tests/system-accounts.js @@ -54,7 +54,7 @@ describe('system_accounts', () => { 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); + assert.equal(err.code, 3011); } }); }); diff --git a/ts/src/error.ts b/ts/src/error.ts index d31fa5adca..1ed4eca426 100644 --- a/ts/src/error.ts +++ b/ts/src/error.ts @@ -58,45 +58,51 @@ const LangErrorCode = { InstructionDidNotSerialize: 103, // IDL instructions. - IdlInstructionStub: 120, - IdlInstructionInvalidProgram: 121, + IdlInstructionStub: 1000, + IdlInstructionInvalidProgram: 1001, // Constraints. - ConstraintMut: 140, - ConstraintHasOne: 141, - ConstraintSigner: 142, - ConstraintRaw: 143, - ConstraintOwner: 144, - ConstraintRentExempt: 145, - ConstraintSeeds: 146, - ConstraintExecutable: 147, - ConstraintState: 148, - ConstraintAssociated: 149, - ConstraintAssociatedInit: 150, - ConstraintClose: 151, - ConstraintAddress: 152, + ConstraintMut: 2000, + ConstraintHasOne: 2001, + ConstraintSigner: 2002, + ConstraintRaw: 2003, + ConstraintOwner: 2004, + ConstraintRentExempt: 2005, + ConstraintSeeds: 2006, + ConstraintExecutable: 2007, + ConstraintState: 2008, + ConstraintAssociated: 2009, + ConstraintAssociatedInit: 2010, + ConstraintClose: 2011, + ConstraintAddress: 2012, + ConstraintZero: 2013, + ConstraintTokenMint: 2014, + ConstraintTokenOwner: 2015, + ConstraintMintMintAuthority: 2016, + ConstraintMintFreezeAuthority: 2017, + ConstraintMintDecimals: 2018, + ConstraintSpace: 2019, // Accounts. - AccountDiscriminatorAlreadySet: 160, - AccountDiscriminatorNotFound: 161, - AccountDiscriminatorMismatch: 162, - AccountDidNotDeserialize: 163, - AccountDidNotSerialize: 164, - AccountNotEnoughKeys: 165, - AccountNotMutable: 166, - AccountNotProgramOwned: 167, - InvalidProgramId: 168, - InvalidProgramExecutable: 169, - AccountNotSigner: 170, - AccountNotSystemOwned: 171, - AccountNotInitialized: 172, - AccountNotProgramData: 173, - + AccountDiscriminatorAlreadySet: 3000, + AccountDiscriminatorNotFound: 3001, + AccountDiscriminatorMismatch: 3002, + AccountDidNotDeserialize: 3003, + AccountDidNotSerialize: 3004, + AccountNotEnoughKeys: 3005, + AccountNotMutable: 3006, + AccountNotProgramOwned: 3007, + InvalidProgramId: 3008, + InvalidProgramExecutable: 3009, + AccountNotSigner: 3010, + AccountNotSystemOwned: 3011, + AccountNotInitialized: 3012, + AccountNotProgramData: 3013, // State. - StateInvalidAddress: 180, + StateInvalidAddress: 4000, // Used for APIs that shouldn't be used anymore. - Deprecated: 299, + Deprecated: 5000, }; const LangErrorMessage = new Map([ @@ -145,6 +151,22 @@ const LangErrorMessage = new Map([ ], [LangErrorCode.ConstraintClose, "A close constraint was violated"], [LangErrorCode.ConstraintAddress, "An address constraint was violated"], + [LangErrorCode.ConstraintZero, "Expected zero account discriminant"], + [LangErrorCode.ConstraintTokenMint, "A token mint constraint was violated"], + [LangErrorCode.ConstraintTokenOwner, "A token owner constraint was violated"], + [ + LangErrorCode.ConstraintMintMintAuthority, + "A mint mint authority constraint was violated", + ], + [ + LangErrorCode.ConstraintMintFreezeAuthority, + "A mint freeze authority constraint was violated", + ], + [ + LangErrorCode.ConstraintMintDecimals, + "A mint decimals constraint was violated", + ], + [LangErrorCode.ConstraintSpace, "A space constraint was violated"], // Accounts. [