Ensure instructions are only ran by authorized accounts by implmementing Signer checks.
is_signer
property is true
Signer
account type in your account
validation struct to have Anchor automatically perform a signer check on a
given accountauthority
field stored on a program account.
Notice that the authority
field on the UpdateAuthority
account validation
struct is of type AccountInfo
. In Anchor, the AccountInfo
account type
indicates that no checks are performed on the account prior to instruction
execution.
Although the has_one
constraint is used to validate the authority
account
passed into the instruction matches the authority
field stored on the vault
account, there is no check to verify the authority
account authorized the
transaction.
This means an attacker can simply pass in the public key of the authority
account and their own public key as the new_authority
account to reassign
themselves as the new authority of the vault
account. At that point, they can
interact with the program as the new authority.
authority
account signed is to add a
signer check within the instruction. That simply means checking that
authority.is_signer
is true
, and returning a MissingRequiredSignature
error if false
.
authority
account also signed the transaction. If the
transaction was not signed by the account passed in as the authority
account,
then the transaction would fail.
Signer
account typeSigner
account type. Simply change the authority
account’s type in the
account validation struct to be of type Signer
, and Anchor will check at
runtime that the specified account is a signer on the transaction. This is the
approach we generally recommend since it allows you to separate the signer check
from instruction logic.
In the example below, if the authority
account does not sign the transaction,
then the transaction will fail before even reaching the instruction logic.
Signer
type, no other ownership or type checks are
performed.
#[account(signer)]
constraintSigner
account type will suffice to ensure an account
has signed a transaction, the fact that no other ownership or type checks are
performed means that this account can’t really be used for anything else in the
instruction.
This is where the signer
constraint comes in handy. The #[account(signer)]
constraint allows you to verify the account signed the transaction, while also
getting the benefits of using the Account
type if you wanted access to it’s
underlying data as well.
As an example of when this would be useful, imagine writing an instruction that
you expect to be invoked via CPI that expects one of the passed in accounts to
be both a **signer****** on the transaciton and a *******data
source*******. Using the Signer
account type here removes the
automatic deserialization and type checking you would get with the Account
type. This is both inconvenient, as you need to manually deserialize the account
data in the instruction logic, and may make your program vulnerable by not
getting the ownership and type checking performed by the Account
type.
In the example below, you can safely write logic to interact with the data
stored in the authority
account while also verifying that it signed the
transaction.
starter
branch of
this repository. The
starter code includes a program with two instructions and the boilerplate setup
for the test file.
The initialize_vault
instruction initializes two new accounts: Vault
and
TokenAccount
. The Vault
account will be initialized using a Program Derived
Address (PDA) and store the address of a token account and the authority of the
vault. The authority of the token account will be the vault
PDA which enables
the program to sign for the transfer of tokens.
The insecure_withdraw
instruction will transfer tokens in the vault
account’s token account to a withdraw_destination
token account. However, the
authority
account in the InsecureWithdraw
struct has a type of
UncheckedAccount
. This is a wrapper around AccountInfo
to explicitly
indicate the account is unchecked.
Without a signer check, anyone can simply provide the public key of the
authority
account that matches authority
stored on the vault
account and
the insecure_withdraw
instruction would continue to process.
While this is somewhat contrived in that any DeFi program with a vault would be
more sophisticated than this, it will show how the lack of a signer check can
result in tokens being withdrawn by the wrong party.
insecure_withdraw
instructioninitialize_vault
instruction
using wallet
as the authority
on the vault. The code then mints 100 tokens
to the vault
token account. Theoretically, the wallet
key should be the only
one that can withdraw the 100 tokens from the vault.
Now, let’s add a test to invoke insecure_withdraw
on the program to show that
the current version of the program allows a third party to in fact withdraw
those 100 tokens.
In the test, we’ll still use the public key of wallet
as the authority
account, but we’ll use a different keypair to sign and send the transaction.
anchor test
to see that both transactions will complete successfully.
authority
account, the
insecure_withdraw
instruction will transfer tokens from the vault
token
account to the withdrawDestinationFake
token account as long as the public key
of theauthority
account matches the public key stored on the authority field
of the vault
account. Clearly, the insecure_withdraw
instruction is as
insecure as the name suggests.
secure_withdraw
instructionsecure_withdraw
. This
instruction will be identical to the insecure_withdraw
instruction, except
we’ll use the Signer
type in the Accounts struct to validate the authority
account in the SecureWithdraw
struct. If the authority
account is not a
signer on the transaction, then we expect the transaction to fail and return an
error.
secure_withdraw
instructionsecure_withdraw
instruction. Invoke the secure_withdraw
instruction, again
using the public key of wallet
as the authority
account and the
withdrawDestinationFake
keypair as the signer and withdraw destination. Since
the authority
account is validated using the Signer
type, we expect the
transaction to fail the signer check and return an error.
anchor test
to see that the transaction will now return a signature
verification error.
solution
branch of
the repository.