Summary
-
Discriminators are 8-byte identifiers written to accounts that distinguish between different account types, ensuring programs interact with the correct data.
-
Implement a discriminator in Rust by including a field in the account struct to represent the account type.
-
Check the discriminator in Rust to verify that the deserialized account data matches the expected value.
-
In Anchor, program account types automatically implement the
Discriminator
trait, which creates an 8-byte unique identifier for a type. -
Use Anchor's
Account<'info, T>
type to automatically check the discriminator when deserializing the account data.
Lesson
"Type cosplay" refers to using an unexpected account type in place of an expected one. Under the hood, account data is stored as an array of bytes that a program deserializes into a custom account type. Without a method to distinguish between account types explicitly, data from an unexpected account could result in instructions being used in unintended ways.
Unchecked Account
In the example below, both the AdminConfig
and UserConfig
account types
store a single public key. The admin_instruction
deserializes the
admin_config
account as an AdminConfig
type and then performs an owner check
and data validation check.
However, since the AdminConfig
and UserConfig
account types have the same
data structure, a UserConfig
account type could be passed as the
admin_config
account. As long as the public key stored on the account matches
the admin
signing the transaction, the admin_instruction
would process, even
if the signer isn't actually an admin.
Note that the names of the fields stored on the account types (admin
and
user
) make no difference when deserializing account data. The data is
serialized and deserialized based on the order of fields rather than their
names.
Add Account Discriminator
To resolve this, add a discriminant field for each account type and set the discriminant when initializing an account.
While they sound similar, a Rust discriminant isn't the same thing as an Anchor discriminator!
-
Rust discriminant: This is an internal value that Rust uses to keep track of which variant an enum currently represents. It's like a behind-the-scenes label for enum variants.
-
Anchor discriminator: This is a unique 8-byte identifier that Anchor adds to the beginning of each account's data. It helps Solana programs quickly recognize what type of account they're dealing with.
In simple terms:
- Discriminants are Rust's way of organizing enum variants.
- Discriminators are Anchor's way of labeling different account types in Solana.
The example below updates the AdminConfig
and UserConfig
account types with
a discriminant
field. The admin_instruction
now includes an additional data
validation check for the discriminant
field.
If the discriminant
field of the account passed into the instruction as the
admin_config
account does not match the expected AccountDiscriminant
, the
transaction will fail. Ensure that the appropriate value for discriminant
is
set when initializing each account, and then include these checks in every
subsequent instruction.
Use Anchor's Account Wrapper
Implementing these checks for every account in every instruction can be tedious.
Fortunately, Anchor provides a #[account]
attribute macro for automatically
implementing traits that every account should have.
Structs marked with #[account]
can then be used with Account
to validate
that the passed-in account is indeed the type you expect. When initializing an
account whose struct representation has the #[account]
attribute, the first 8
bytes are automatically reserved for a discriminator unique to the account type.
When deserializing the account data, Anchor will automatically check if the
discriminator matches the expected account type and throw an error if it does
not.
In the example below, Account<'info, AdminConfig>
specifies that the
admin_config
account should be of type AdminConfig
. Anchor then
automatically checks that the first 8 bytes of account data match the
discriminator of the AdminConfig
type.
The data validation check for the admin
field is also moved from the
instruction logic to the account validation struct using the has_one
constraint. #[account(has_one = admin)]
specifies that the admin_config
account's admin
field must match the admin
account passed into the
instruction. Note that for the has_one
constraint to work, the naming of the
account in the struct must match the naming of the field on the account you are
validating.
This vulnerability is something you generally don't have to worry about when using Anchor—that's the whole point! However, after exploring how this issue can arise in native Rust programs, you should now have a better understanding of the importance of the account discriminator in an Anchor account. Anchor's automatic discriminator checks mean that developers can focus more on their product, but it's still crucial to understand what Anchor is doing behind the scenes to build robust Solana programs.
Lab
In this lab, you'll create two programs to demonstrate a type cosplay vulnerability:
- The first program initializes accounts without a discriminator.
- The second program initializes accounts using Anchor's
init
constraint, which automatically sets an account discriminator.
1. Starter
To get started, download the starter code from the starter branch of this repository. The starter code includes a program with three instructions and some tests.
The three instructions are:
initialize_admin
- Initializes an admin account and sets the admin authority of the program.initialize_user
- Initializes a standard user account.update_admin
- Allows the existing admin to update the admin authority of the program.
Review the instructions in the lib.rs
file. The last instruction should only
be callable by the account matching the admin
field on the admin account
initialized using the initialize_admin
instruction.
2. Test Insecure update_admin Instruction
Both the AdminConfig
and User
account types have the same fields and field
types:
Because of this, it's possible to pass a User
account in place of the admin
account in the update_admin
instruction, bypassing the requirement that only
an admin can call this instruction.
Take a look at the solana-type-cosplay.ts
file in the tests
directory. It
contains a basic setup and two tests: one initializes a user account, and the
other invokes update_admin
with the user account instead of an admin account.
Run anchor test
to see that invoking update_admin
completes successfully:
3. Create type-checked Program
Next, create a new program called type-checked
by running
anchor new type-checked
from the root of the existing anchor program.
Now, in your programs
folder, you will have two programs. Run
anchor keys list
to see the program ID for the new program. Add it to the
lib.rs
file of the type-checked
program and to the Anchor.toml
file.
Update the test file's setup to include the new program and two new keypairs for the accounts to be initialized:
4. Implement the type-checked Program
In the type_checked
program, add two instructions using the init
constraint
to initialize an AdminConfig
account and a User
account. Anchor will
automatically set the first 8 bytes of account data as a unique discriminator
for the account type.
Add an update_admin
instruction that validates the admin_config
account as
an AdminConfig
account type using Anchor's Account
wrapper. Anchor will
automatically check that the account discriminator matches the expected account
type:
5. Test Secure update_admin Instruction
In the test file, initialize an AdminConfig
account and a User
account from
the type_checked
program. Then, invoke the updateAdmin
instruction twice,
passing in the newly created accounts:
Run anchor test
. For the transaction where we pass in the User account type,
we expect the instruction to return an Anchor Error due to the account not being
of type AdminConfig:
Following Anchor's best practices ensures that your programs avoid this
vulnerability. Always use the #[account]
attribute when creating account
structs, use the init
constraint when initializing accounts, and use the
Account
type in your account validation structs.
For the final solution code, you can find it on the solution
branch of
the repository.
Challenge
As with other lessons in this unit, practice avoiding this security exploit by auditing your own or other programs.
Review at least one program and ensure that account types have a discriminator and that these are checked for each account and instruction. Since standard Anchor types handle this check automatically, you're more likely to find a vulnerability in a native program.
Remember, if you find a bug or exploit in somebody else's program, please alert them. If you find one in your own program, patch it immediately.
Push your code to GitHub and tell us what you thought of this lesson!