Paweł Rejkowicz
13
min read
Last Update:
December 6, 2022

If you’ve read our article about blockchain programming languages, you know that the smart contracts on Solana are developed either in C or Rust. Most projects in fact choose Rust, simply because it is safer and easier to use.

Since security is key in blockchain-based programs and the first step towards it on Solana is choosing Rust, what else can you do? 

There are several ways to make Solana programs more secure:

  • extensive testing
  • using existing frameworks (e.g. Anchor)
  • requesting security audits
  • using type theory

Now, let’s dive deeper into what is involved in each of those.

Extensive testing

Launching Solana programs without extensive testing can be reckless as they cannot be trusted just after you write them. Tests come in handy especially to validate if the program is working correctly. They will also be helpful in checking test cases that return errors.

Another thing to be aware of is the test coverage. You should keep the coverage at a high level as it helps you find the tests that are still missing.

But there’s one major problem with testing–you cannot check and test what you’re not aware of. You can find publications about common pitfalls and attack vectors that will guide you to some extent, but they look into the commons. By definition, such publication isn’t a complete guide to all potential errors.

Frameworks

Many teams have tried to develop their own development frameworks, but still the most popular is Anchor. It helps in creating frontend, securing the program, and faster, more in-depth testing. 

It partially solves the “I don’t know what I don’t know” problem. Frameworks solve them with magic checks and by documenting the tests well. But why partially? They still leave a chance that the program can be exploited.

The procedures that are the most vulnerable are the parts of code that require some preconditions. It is so common to omit one of the steps while adding some new features. Helper functions may be helpful, but when the program gets more and more complicated, it is hard to track if and where to execute the particular step. Callbacks and dependency injection makes it even worse. 

Another problem could be around performance. Frameworks usually do more than needed, because they have to be flexible. Since they need to suit many cases, they might not be the best for your particular project from the performance standpoint.

Security audits

A security audit company is like a personal trainer. They have the knowledge to solve your specific problem, so you can hire them to guide you. To do so, they’ll build a mathematical model of the program and try to prove it works as intended. Or find out that it’s not and provide you with a proposition of fixes.

A security auditing company can do one thing your developers cannot–look at the code from the outside. This gives a fresh view on the project, however audits also come with several drawbacks, too. They take time and can get quite costly. This is also because when some changes are required, the audit has to be started all over again to ensure that fixes haven’t brought any new errors.​

Read also: 10 Best Solana Wallets: A Review for Developers

Introducing type theory

For most developers, types are just a mechanism to organize the code and encapsulate access to primitives such as strings or integers. Surprisingly, types can also be used to prove some mathematical properties. Programming languages having first class types like Lean or Idris use upgraded version of types: dependent types. They are not supported in Rust.

The type theory is complex, but there are a few simple rules:

  • if the function has a parameter of type T, it can be called only if an instance of type T can be created
  • creating the instance of type T is a proof that one of constructors of type T was called
  • constructor is a function

Here’s an example:

Looking at the example, we can say a few things:

  • Uninitialized record can be created only using function from
  • To call from function, caller must provide the proof that record is deserialized
  • Function from creates the proof, that record is uninitialized and destroys deserialized record
  • Method initialize requires self, which is the proof that record is not initialized
  • Uninitialized record is destroyed at the end of function initialize, so it cannot be initialized twice

It might be surprising, that all the variables are passed by value, but:

  • protocol code will be efficiently optimized by Rust and helper private functions can still use references
  • side effects cannot be emitted twice
  • at the end of instruction handler, all the variables are consumed, there is nothing you can use from the stack

Record program rewritten

Record program is a simple smart contract from Solana Program Library. It can be used to store any data in the account. The account data and authority can be modified and the records can be erased or closed as well.

I have rewritten the same program with a few goals in mind:

  • program must have similar size
  • program must have similar execution time
  • all the types must be used and declared in separate modules
  • types should be impossible to instantiate from the outside
  • create an API with guarantee that if the program works, it means it is safe

To stick to the core of this example, I’ve only rewritten the instruction handling part, so the order of getting account info will not be checked during compilation.

Initialization

Initialization requires proof of uninitialized record, that is: `record_uninitialized::Type`.

It’s easy to see that introduced functions do too much. It's because of the rules:

  • Do not create extra type if it is not necessary
  • Store only required data in proof object

​You would be surprised how nicely this program evaluates.

Write

Write requires proof of authorized record: record_authorized::Type.

To authorize a record, it must be deserialized first. It is already done in steps a1-4, so it is a good idea to extract record_deserialized::Type out of them.

Another duplicated code is step A1, which can be saved as authority_info::Type.

Set authority

Set authority function requires proof of authorized record, the same as write instruction.

​Someone could say that there’s plenty of duplicated code, but actually, nothing really can go wrong here. All the lines are required by the compiler to call the set_authority method.

Close account

Close account function requires proof of authorized record, but also mutable reference to record lamports. The easiest way to achieve it is to store lamports in the authorized record, but it affects other instructions. Let's try to make it optimal.

In order to get mutable reference to record lamports, steps a1-2 must be extracted into record_info::Type. Before creating a deserialized record from record info, lamports must be extracted, and then both passed to ready to close constructor. 

Result

The full source code can be found here. Of course, it is not ready to deploy, but nicely describes required steps to achieve this particular effect. There is some duplicated code, but it does not introduce any risk, it may just take some more time to extract a new type if required in the future.

Summary

As shown, introducing type-driven development is quite easy in Solana programs written in Rust. What’s also encouraging is the fact that it can be used in any Solana project, with any framework. 

And if you’re wondering what else you can verify with types–there are plenty of those, but that’s a story for another post.

If you'd like to build a project on Solana or audit your code, don't hesitate to contact us!

Written by
Paweł Rejkowicz
Blockchain Security Researcher

Build the Top Web3 Dev Team

8 years of experience in one ebook. Check it out

Download