Summary
- Use Signer Checks to verify that specific accounts have signed a transaction. Without appropriate signer checks, accounts may be able to execute instructions they shouldn’t be authorized to perform.
- To implement a signer check in Rust, simply check that an account’s
is_signerproperty istrue - In Anchor, you can use the
Signeraccount type in your account validation struct to have Anchor automatically perform a signer check on a given account - Anchor also has an account constraint that will automatically verify that a given account has signed a transaction
Lesson
Signer checks are used to verify that a given account’s owner has authorized a transaction. Without a signer check, operations whose execution should be limited to only specific accounts can potentially be performed by any account. In the worst case scenario, this could result in wallets being completely drained by attackers passing in whatever account they want to an instruction.Missing Signer Check
The example below shows an oversimplified version of an instruction that updates theauthority 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.
Add signer authorization checks
All you need to do to validate that theauthority 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.
Use Anchor’s Signer account type
However, putting this check into the instruction function muddles the separation
between account validation and instruction logic.
Fortunately, Anchor makes it easy to perform signer checks by providing the
Signer 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.
Use Anchor’s #[account(signer)] constraint
While in most cases, the Signer 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.
Lab
Let’s practice by creating a simple program to demonstrate how a missing signer check can allow an attacker to withdraw tokens that don’t belong to them. This program initializes a simplified token “vault” account and demonstrates how a missing signer check could allow the vault to be drained.1. Starter
To get started, download the starter code from thestarter 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.
2. Test insecure_withdraw instruction
The test file includes the code to invoke the initialize_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.
3. Add secure_withdraw instruction
Let’s fix the problem in a new instruction called secure_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.
4. Test secure_withdraw instruction
With the instruction in place, return to the test file to test the
secure_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.

