Summary
- Use data validation checks to verify that account data matches an expected value. Without appropriate data validation checks, unexpected accounts may be used in an instruction.
- To implement data validation checks in Rust, simply compare the data stored on
an account to an expected value.
- In Anchor, you can use
constraintto checks whether the given expression evaluates to true. Alternatively, you can usehas_oneto check that a target account field stored on the account matches the key of an account in theAccountsstruct.
Lesson
Account data matching refers to data validation checks used to verify the data stored on an account matches an expected value. Data validation checks provide a way to include additional constraints to ensure the appropriate accounts are passed into an instruction. This can be useful when accounts required by an instruction have dependencies on values stored in other accounts or if an instruction is dependent on the data stored in an account.Missing data validation check
The example below includes anupdate_admin instruction that updates the
admin field stored on an admin_config account.
The instruction is missing a data validation check to verify the admin account
signing the transaction matches the admin stored on the admin_config
account. This means any account signing the transaction and passed into the
instruction as the admin account can update the admin_config account.
Add data validation check
The basic Rust approach to solve this problem is to simply compare the passed inadmin key to the admin key stored in the admin_config account, throwing an
error if they don’t match.
update_admin instruction would only
process if the admin signer of the transaction matched the admin stored on
the admin_config account.
Use Anchor constraints
Anchor simplifies this with thehas_one constraint. You can use the has_one
constraint to move the data validation check from the instruction logic to the
UpdateAdmin struct.
In the example below, has_one = admin specifies that the admin account
signing the transaction must match the admin field stored on the
admin_config account. To use the has_one constraint, the naming convention
of the data field on the account must be consistent with the naming on the
account validation struct.
constraint to manually add an expression that must
evaluate to true in order for execution to continue. This is useful when for
some reason naming can’t be consistent or when you need a more complex
expression to fully validate the incoming data.
Lab
For this lab we’ll create a simple “vault” program similar to the program we used in the Signer Authorization lesson and the Owner Check lesson. Similar to those labs, we’ll show in this lab how a missing data validation 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 a new Vault account and a new
TokenAccount. The Vault account will store the address of a token account,
the authority of the vault, and a withdraw destination token account.
The authority of the new token account will be set as the vault, a PDA of the
program. This allows the vault account to sign for the transfer of tokens from
the token account.
The insecure_withdraw instruction transfers all the tokens in the vault
account’s token account to a withdraw_destination token account.
Notice that this instruction **does** have a signer check for
authority and an owner check for vault. However, nowhere in the account
validation or instruction logic is there code that checks that the authority
account passed into the instruction matches the authority account on the
vault.
2. Test insecure_withdraw instruction
To prove that this is a problem, let’s write a test where an account other than
the vault’s authority tries to withdraw from the vault.
The test file includes the code to invoke the initialize_vault instruction
using the provider wallet as the authority and then mints 100 tokens to the
vault token account.
Add a test to invoke the insecure_withdraw instruction. Use
withdrawDestinationFake as the withdrawDestination account and walletFake
as the authority. Then send the transaction using walletFake.
Since there are no checks the verify the authority account passed into the
instruction matches the values stored on the vault account initialized in the
first test, the instruction will process successfully and the tokens will be
transferred to the withdrawDestinationFake account.
anchor test to see that both transactions will complete successfully.
3. Add secure_withdraw instruction
Let’s go implement a secure version of this instruction called
secure_withdraw.
This instruction will be identical to the insecure_withdraw instruction,
except we’ll use the has_one constraint in the account validation struct
(SecureWithdraw) to check that the authority account passed into the
instruction matches the authority account on the vault account. That way
only the correct authority account can withdraw the vault’s tokens.
4. Test secure_withdraw instruction
Now let’s test the secure_withdraw instruction with two tests: one that uses
walletFake as the authority and one that uses wallet as the authority. We
expect the first invocation to return an error and the second to succeed.
anchor test to see that the transaction using an incorrect authority
account will now return an Anchor Error while the transaction using correct
accounts completes successfully.
AnchorError caused by account: vault).
solution branch of
the repository.

