How to avoid reentrancy attacks on your Concordium smart contracts — Part 2
This is the second article in the series about reentrance attacks on Concordium. In the first part we built a contract which could be exploited. We then built secure contracts where one of them used the Concordium `read_only` feature, which ensures no side effects on the state of the caller contract when it calls other contracts.
Following good software principles we should test our code — and Concordium’s testing framework comes to the rescue. In this second part we will test contracts from part one.
Testing reentrant contract
If you are familiar with the Hardhat testing framework on Solidity you will quickly get up to speed with the framework on Concordium. With it, you can write unit and integration tests in Rust that test the exact smart contract Wasm module you can deploy to the chain.
Again, there is great documentation, so I will not show all code blocks and give explanations to all code parts. (To see all the code of the examples go to github.)
First, we do some initialization — this is where the contracts are initialized.
Then we run the actual test. The test is fairly long so let me take you through it. First, three different addresses deposit 42 CCDs. We check the balance of the victim contract after the deposits which then has a total amount of 126 CCDs. The attacking contract has only deposited 42 CCDs and hence should only be able to withdraw this amount.
We instantiate the attack by calling the entrypoint `attack` on the attacking contract.
The last assertion part checks that the victim contract has indeed been exploited. The victim contract has a balance of zero and the attacker contract has the full amount.
Testing mitigations
We start by validating that the withdrawal still works after we have applied our mitigation. We do this by making a non-evil contract, let’s call it Saint, which doesn’t exploit the victim contract.
The test below follows almost the same structure as the one we made for the attack. We deposit CCDs from two different accounts and then, from the saint contract, invoke the entrypoint `withdraw`.
In the assert section we validate the victim contract has only transferred the CCD amount the saint deposited — hence the victim contract hasn’t been exploited and it works as expected.
Let’s now make a test from our attacking contract calling victim contracts with mitigation.
We start — as always — to deposit from different addresses. When invoking the attack we are happy — the transaction fails and the attack is avoided. 🚀
Energy usage
The Concordium integration framework also provides you with details of energy used. We have given three different mitigation methods and which to choose depends on your use case. It is also nice to know if it is energy efficient since it reflects the cost of interaction with your contract.
Below I have made a table which gives the energy used (when the saint contract calls `withdraw`) on contracts with and without mitigations — in this case there isn’t much difference.
One may notice that there isn’t a difference between the `Reentrance mutex` and the `Reentrance checks-effects-interactions` even though the state has an additional bool field, hence an additional (u8). The bool is mutated in-memory two times during the call and finally persisted once the call has finished. There really is very little extra work and no extra host calls.
Conclusion
Security is a major concern when developing smart contracts, and luckily, with Concordium, you can easily develop and test your contract before deployments. The testing framework makes it easy to explore edge cases and validate the robustness of the contract using the exact smart contract modules and execution environment as is used on the chain.