Jelajahi Sumber

lang: Always execute constraints for init_if_needed (#1096)

Paul 3 tahun lalu
induk
melakukan
423ddde30a

+ 12 - 8
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
 

+ 1 - 1
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"

+ 1 - 1
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};

+ 18 - 5
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,
 }

+ 3 - 3
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<Self, ProgramError>;
 
@@ -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.

+ 62 - 6
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
                 };
             }

+ 1 - 1
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<Path>, version: String) -> Result<Option<Idl>> {

+ 1 - 1
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",
                 ));
             }
         }

+ 4 - 4
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);
     }
   });
 });

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

+ 4 - 4
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;
       }

+ 73 - 1
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)]

+ 25 - 0
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<TestInitIfNeededChecksOwner>) -> ProgramResult {
+        Ok(())
+    }
+
+    pub fn test_init_if_needed_checks_seeds(ctx: Context<TestInitIfNeededChecksSeeds>, seed_data: String) -> ProgramResult {
+        Ok(())
+    }
+
+    pub fn test_init_mint_if_needed(ctx: Context<TestInitMintIfNeeded>, decimals: u8) -> ProgramResult {
+        Ok(())
+    }
+
+    pub fn test_init_token_if_needed(ctx: Context<TestInitTokenIfNeeded>) -> ProgramResult {
+        Ok(())
+    }
+
+    pub fn test_init_associated_token_if_needed(ctx: Context<TestInitAssociatedTokenIfNeeded>) -> ProgramResult {
+        Ok(())
+    }
+
+    pub fn init_with_space(ctx: Context<InitWithSpace>, data: u16) -> ProgramResult {
+        Ok(())
+    }
+
+
     pub fn test_multidimensional_array(
         ctx: Context<TestMultidimensionalArray>,
         data: [[u8; 10]; 10],

+ 401 - 3
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();

+ 1 - 1
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);
     }
   });
 });

+ 54 - 32
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.
   [