Understand the use of account owner checks when processing incoming instructions.
Owner
trait which allows the
Account<'info, T>
wrapper to automatically verify program ownershipAccountInfo
struct contains the following fields. An owner
check refers to checking that the owner
field in the AccountInfo
matches an
expected program ID.
admin_instruction
intended to be accessible only by
an admin
account stored on an admin_config
account.
Although the instruction checks the admin
account signed the transaction and
matches the admin
field stored on the admin_config
account, there is no
owner check to verify the admin_config
account passed into the instruction is
owned by the executing program.
Since the admin_config
is unchecked as indicated by the AccountInfo
type, a
fake admin_config
account owned by a different program could be used in the
admin_instruction
. This means that an attacker could create a program with an
admin_config
whose data structure matches the admin_config
of your program,
set their public key as the admin
and pass their admin_config
account into
your program. This would let them spoof your program into thinking that they are
the authorized admin for your program.
This simplified example only prints the admin
to the program logs. However,
you can imagine how a missing owner check could allow fake accounts to exploit
an instruction.
owner
field on
the account to the program ID. If they do not match, you would return an
IncorrectProgramId
error.
admin_config
account. If a fake admin_config
account was
used in the admin_instruction
, then the transaction would fail.
Account<'info, T>
Account
type.
Account<'info, T>
is a wrapper around AccountInfo
that verifies program
ownership and deserializes underlying data into the specified account type T
.
This in turn allows you to use Account<'info, T>
to easily validate ownership.
For context, the #[account]
attribute implements various traits for a data
structure representing an account. One of these is the Owner
trait which
defines an address expected to own an account. The owner is set as the program
ID specified in the declare_id!
macro.
In the example below, Account<'info, AdminConfig>
is used to validate the
admin_config
. This will automatically perform the owner check and deserialize
the account data. Additionally, the has_one
constraint is used to check that
the admin
account matches the admin
field stored on the admin_config
account.
This way, you don’t need to clutter your instruction logic with owner checks.
#[account(owner = <expr>)]
constraintAccount
type, you can use an owner
constraint. The
owner
constraint allows you to define the program that should own an account
if it’s different from the currently executing one. This comes in handy if, for
example, you are writing an instruction that expects an account to be a PDA
derived from a different program. You can use the seeds
and bump
constraints
and define the owner
to properly derive and verify the address of the account
passed in.
To use the owner
constraint, you’ll have to have access to the public key of
the program you expect to own an account. You can either pass the program in as
an additional account or hard-code the public key somewhere in your program.
starter
branch of
this repository.
The starter code includes two programs clone
and owner_check
and the
boilerplate setup for the test file.
The owner_check
program includes two instructions:
initialize_vault
initializes a simplified vault account that stores the
addresses of a token account and an authority accountinsecure_withdraw
withdraws tokens from the token account, but is missing an
owner check for the vault accountclone
program includes a single instruction:
initialize_vault
initializes a “vault” account that mimics the vault account
of the owner_check
program. It stores the address of the real vault’s token
account, but allows the malicious user to put their own authority account.insecure_withdraw
instructioninitialize_vault
instruction on
the owner_check
program using the provider wallet as the authority
and then
mints 100 tokens to the token account.
The test file also includes a test to invoke the initialize_vault
instruction
on the clone
program to initialize a fake vault
account storing the same
tokenPDA
account, but a different authority
. Note that no new tokens are
minted here.
Let’s add a test to invoke the insecure_withdraw
instruction. This test should
pass in the cloned vault and the fake authority. Since there is no owner check
to verify the vaultClone
account is owned by the owner_check
program, the
instruction’s data validation check will pass and show walletFake
as a valid
authority. The tokens from the tokenPDA
account will then be withdrawn to the
withdrawDestinationFake
account.
anchor test
to see that the insecure_withdraw
completes successfully.
vaultClone
deserializes successfully even though Anchor
automatically initializes new accounts with a unique 8 byte discriminator and
checks the discriminator when deserializing an account. This is because the
discriminator is a hash of the account type name.
Vault
, the accounts have the same discriminator even though they are owned by
different programs.
secure_withdraw
instructionlib.rs
file of the owner_check
program add a secure_withdraw
instruction and a SecureWithdraw
accounts struct.
In the SecureWithdraw
struct, let’s use Account<'info, Vault>
to ensure that
an owner check is performed on the vault
account. We’ll also use the has_one
constraint to check that the token_account
and authority
passed into the
instruction match the values stored on the vault
account.
secure_withdraw
instructionsecure_withdraw
instruction, we’ll invoke the instruction twice.
First, we’ll invoke the instruction using the vaultClone
account, which we
expect to fail. Then, we’ll invoke the instruction using the correct vault
account to check that the instruction works as intended.
anchor test
to see that the transaction using the vaultClone
account
will now return an Anchor Error while the transaction using the vault
account
completes successfully.
Account<'info, T>
type can simplify the account
validation process to automate the ownership check. Additionally, note that
Anchor Errors can specify the account that causes the error (e.g. the third line
of the logs above say AnchorError caused by account: vault
). This can be very
helpful when debugging.
solution
branch of
the repository.