Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/reusable-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,8 @@ jobs:
path: tests/bench
- cmd: cd tests/idl && ./test.sh
path: tests/idl
- cmd: cd tests/interface-account && anchor test
path: tests/interface-account
- cmd: cd tests/lazy-account && anchor test --skip-lint
path: tests/lazy-account
- cmd: cd tests/test-instruction-validation && ./test.sh
Expand Down
27 changes: 9 additions & 18 deletions lang/src/accounts/interface_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,28 +234,19 @@ impl<'a, T: AccountSerialize + AccountDeserialize + Clone> InterfaceAccount<'a,
}

impl<'a, T: AccountSerialize + AccountDeserialize + CheckOwner + Clone> InterfaceAccount<'a, T> {
/// Deserializes the given `info` into a `InterfaceAccount`.
///
/// This **does not** check an Anchor discriminator. It first validates
/// program ownership via `T::check_owner`, then deserializes using
/// `AccountDeserialize::try_deserialize_unchecked`.
/// Deserializes the given `info` into an `InterfaceAccount`.
#[inline(never)]
pub fn try_from(info: &'a AccountInfo<'a>) -> Result<Self> {
// `InterfaceAccount` targets foreign program accounts (e.g., SPL Token
// accounts) that do not have Anchor discriminators. Because of that, we
// intentionally skip the Anchor discriminator check here and instead:
//
// 1) Validate program ownership via `T::check_owner(info.owner)?`
// 2) Deserialize without a discriminator by delegating to
// `T::try_deserialize_unchecked`
Self::try_from_unchecked(info)
if info.owner == &system_program::ID && info.lamports() == 0 {
return Err(ErrorCode::AccountNotInitialized.into());
}
T::check_owner(info.owner)?;
let mut data: &[u8] = &info.try_borrow_data()?;
Ok(Self::new(info, T::try_deserialize(&mut data)?))
}

/// Deserializes the given `info` into a `InterfaceAccount` **without** checking
/// the account discriminator. This is intended for foreign program accounts.
/// Prefer `Self::try_from` when you also want the ownership check, but note
/// that both skip Anchor discriminator checks, and `try_from` additionally
/// enforces ownership.
/// Deserializes the given `info` into an `InterfaceAccount` without checking the account
/// discriminator. Be careful when using this and avoid it if possible.
#[inline(never)]
pub fn try_from_unchecked(info: &'a AccountInfo<'a>) -> Result<Self> {
if info.owner == &system_program::ID && info.lamports() == 0 {
Expand Down
11 changes: 11 additions & 0 deletions tests/interface-account/Anchor.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[programs.localnet]
interface_account = "interfaceAccount111111111111111111111111111"
new = "New1111111111111111111111111111111111111111"
old = "oLd1111111111111111111111111111111111111111"

[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"

[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
14 changes: 14 additions & 0 deletions tests/interface-account/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[workspace]
members = [
"programs/*"
]
resolver = "2"

[profile.release]
overflow-checks = true
lto = "fat"
codegen-units = 1
[profile.release.build-override]
opt-level = 3
incremental = false
codegen-units = 1
16 changes: 16 additions & 0 deletions tests/interface-account/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "interface-account",
"version": "0.32.1",
"license": "(MIT OR Apache-2.0)",
"homepage": "https://github.com/coral-xyz/anchor#readme",
"bugs": {
"url": "https://github.com/coral-xyz/anchor/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/coral-xyz/anchor.git"
},
"engines": {
"node": ">=17"
}
}
21 changes: 21 additions & 0 deletions tests/interface-account/programs/interface-account/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "interface-account"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"

[lib]
crate-type = ["cdylib", "lib"]
name = "interface_account"

[features]
default = []
cpi = ["no-entrypoint"]
no-entrypoint = []
no-idl = []
idl-build = ["anchor-lang/idl-build"]

[dependencies]
anchor-lang = { path = "../../../../lang" }
new = { path = "../new", features = ["cpi"] }
old = { path = "../old", features = ["cpi"] }
52 changes: 52 additions & 0 deletions tests/interface-account/programs/interface-account/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#![allow(warnings)]

use anchor_lang::prelude::*;

declare_id!("interfaceAccount111111111111111111111111111");

#[program]
pub mod interface_account {
use super::*;

pub fn test(ctx: Context<Test>) -> Result<()> {
Ok(())
}
}

#[derive(Accounts)]
pub struct Test<'info> {
pub expected_account: InterfaceAccount<'info, interface::ExpectedAccount>,
}

mod interface {
#[derive(Clone)]
pub struct ExpectedAccount(new::ExpectedAccount);

impl anchor_lang::AccountDeserialize for ExpectedAccount {
fn try_deserialize(buf: &mut &[u8]) -> anchor_lang::Result<Self> {
new::ExpectedAccount::try_deserialize(buf).map(Self)
}

fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result<Self> {
new::ExpectedAccount::try_deserialize_unchecked(buf).map(Self)
}
}

impl anchor_lang::AccountSerialize for ExpectedAccount {}

impl anchor_lang::Owners for ExpectedAccount {
fn owners() -> &'static [anchor_lang::prelude::Pubkey] {
&[old::ID_CONST, new::ID_CONST]
}
}

#[cfg(feature = "idl-build")]
mod idl_impls {
use super::ExpectedAccount;

impl anchor_lang::IdlBuild for ExpectedAccount {}
impl anchor_lang::Discriminator for ExpectedAccount {
const DISCRIMINATOR: &'static [u8] = &[];
}
}
}
18 changes: 18 additions & 0 deletions tests/interface-account/programs/new/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "new"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"

[lib]
crate-type = ["cdylib", "lib"]

[features]
default = []
cpi = ["no-entrypoint"]
no-entrypoint = []
no-idl = []
idl-build = ["anchor-lang/idl-build"]

[dependencies]
anchor-lang = { path = "../../../../lang" }
47 changes: 47 additions & 0 deletions tests/interface-account/programs/new/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#![allow(warnings)]

use anchor_lang::prelude::*;

declare_id!("New1111111111111111111111111111111111111111");

#[program]
pub mod new {
use super::*;

pub fn init(ctx: Context<Init>) -> Result<()> {
Ok(())
}

pub fn init_another(ctx: Context<InitAnother>) -> Result<()> {
Ok(())
}
}

#[derive(Accounts)]
pub struct Init<'info> {
#[account(mut)]
pub authority: Signer<'info>,
#[account(init, payer = authority, space = 40)]
pub expected_account: Account<'info, ExpectedAccount>,
pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct InitAnother<'info> {
#[account(mut)]
pub authority: Signer<'info>,
#[account(init, payer = authority, space = 72)]
pub another_account: Account<'info, AnotherAccount>,
pub system_program: Program<'info, System>,
}

#[account]
pub struct ExpectedAccount {
pub data: Pubkey,
}

#[account]
pub struct AnotherAccount {
pub a: Pubkey,
pub b: Pubkey,
}
18 changes: 18 additions & 0 deletions tests/interface-account/programs/old/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "old"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"

[lib]
crate-type = ["cdylib", "lib"]

[features]
default = []
cpi = ["no-entrypoint"]
no-entrypoint = []
no-idl = []
idl-build = ["anchor-lang/idl-build"]

[dependencies]
anchor-lang = { path = "../../../../lang" }
28 changes: 28 additions & 0 deletions tests/interface-account/programs/old/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#![allow(warnings)]

use anchor_lang::prelude::*;

declare_id!("oLd1111111111111111111111111111111111111111");

#[program]
pub mod old {
use super::*;

pub fn init(ctx: Context<Init>) -> Result<()> {
Ok(())
}
}

#[derive(Accounts)]
pub struct Init<'info> {
#[account(mut)]
pub authority: Signer<'info>,
#[account(init, payer = authority, space = 40)]
pub expected_account: Account<'info, ExpectedAccount>,
pub system_program: Program<'info, System>,
}

#[account]
pub struct ExpectedAccount {
pub data: Pubkey,
}
90 changes: 90 additions & 0 deletions tests/interface-account/tests/interface-account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import * as anchor from "@coral-xyz/anchor";
import assert from "assert";

import type { InterfaceAccount } from "../target/types/interface_account";
import type { New } from "../target/types/new";
import type { Old } from "../target/types/old";

describe("interface-account", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program: anchor.Program<InterfaceAccount> =
anchor.workspace.interfaceAccount;
const oldProgram: anchor.Program<Old> = anchor.workspace.old;
const newProgram: anchor.Program<New> = anchor.workspace.new;

const oldExpectedAccount = anchor.web3.Keypair.generate();
const newExpectedAccount = anchor.web3.Keypair.generate();
const anotherAccount = anchor.web3.Keypair.generate();

// Initialize accounts
before(async () => {
// Initialize the expected account of the old program
await oldProgram.methods
.init()
.accounts({ expectedAccount: oldExpectedAccount.publicKey })
.signers([oldExpectedAccount])
.rpc();

// Initialize the expected account of the new program
await newProgram.methods
.init()
.accounts({ expectedAccount: newExpectedAccount.publicKey })
.signers([newExpectedAccount])
.rpc();

// Initialize another account of the new program
await newProgram.methods
.initAnother()
.accounts({ anotherAccount: anotherAccount.publicKey })
.signers([anotherAccount])
.rpc();
});

it("Allows old exptected accounts", async () => {
await program.methods
.test()
.accounts({ expectedAccount: oldExpectedAccount.publicKey })
.rpc();
});

it("Allows new exptected accounts", async () => {
await program.methods
.test()
.accounts({ expectedAccount: newExpectedAccount.publicKey })
.rpc();
});

it("Doesn't allow accounts owned by other programs", async () => {
try {
await program.methods
.test()
.accounts({ expectedAccount: program.provider.wallet!.publicKey })
.rpc();

assert.fail("Allowed unexpected account substitution!");
} catch (e) {
assert(e instanceof anchor.AnchorError);
assert.strictEqual(
e.error.errorCode.number,
anchor.LangErrorCode.AccountOwnedByWrongProgram
);
}
});

it("Doesn't allow unexpected accounts owned by the expected programs", async () => {
try {
await program.methods
.test()
.accounts({ expectedAccount: anotherAccount.publicKey })
.rpc();

assert.fail("Allowed unexpected account substitution!");
} catch (e) {
assert(e instanceof anchor.AnchorError);
assert.strictEqual(
e.error.errorCode.number,
anchor.LangErrorCode.AccountDiscriminatorMismatch
);
}
});
});
10 changes: 10 additions & 0 deletions tests/interface-account/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"lib": ["es2020"],
"module": "commonjs",
"target": "es6",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true
}
}
2 changes: 1 addition & 1 deletion tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"floats",
"idl",
"ido-pool",
"interface",
"interface-account",
"lazy-account",
"lockup",
"misc",
Expand Down
Loading