Writing a valid smart contract requires both ability and diligence. Developers have to take into account all possible uses and scenarios. If requirements change, the assumptions and behaviour may need revalidation. That is why automated random testing on real-world systems is the best way to ensure smart contracts’ correctness.
The developer may implement tests sooner than a full proof of correctness, and random testing makes fewer assumptions for this proof. This is especially important in the light of smart contracts that have been proven correct according to specification, only for the specification to reveal its imperfections afterwards. Finally, well written random tests are a reasonable basis for proof of correctness.
The Plutus platform has built-in support for automatic testing, based on a popular property testing framework QuickCheck.
In this article, we show how to make an automatic test for the Plutus smart contract using our smart contract for “proof of burn” as a reference example.
The burning of cryptocurrencies and crypto tokens is sending them to a black hole address: the address with no access key to retrieve funds. After the transaction happens, the public can verify that a burn took place. To do so, the sender needs to share a “secret”: the commitment value. Proof-of-burn(Karantias, Kiayias, and Zindros 2020) is a protocol proposed for assuring the burning of funds in a way that is not censorable by the middlemen. The burning of blockchain funds may serve to buy back the tokens and boost valuations of the remaining tickets. Or it may be used as a proof of commitment in blockchain protocols(Ismail and Materwala 2019). Burning in large amounts may cause deflationary pressure since it decreases the total amount of the token in circulation.
*Burning of ADA tokens is not something that we promote or think will happen on Cardano. This is only used to explore smart contracts and explain methods of testing.
Plutus platform supports both standard unit tests with a fixed scenario and random tests that are called “property tests” because they aim at testing properties (also called “dynamic logic tests”) or laws applying to the program, not just exact outputs. Such tests combine user-specified and randomly generated scenarios. This allows covering many more scenarios as compared with fixed tests, and detect bugs earlier.
Firstly, the Plutus platform supports smart contract unit testing using the EmulatorTrace monad. This monad allows calling smart contracts in the test environment. This monad also simulates wallets and traces balance changes. For example, if a smart contract sends money from one wallet to another, EmulatorTrace simulates both wallets, catches money movement, and allows developers to check final balances on these wallets.
Let’s look at a code example:
In this test scenario, we lock 50 ADA from simulated wallet w1 in favour of another wallet w2 and check that other (for example, w3) wallets are unchanged.
Here we have the validation of state after the test in lines 3-5 and we have the test scenario itself in lines 8-13. All that is executed by the function checkPredicate. It takes a name of the test that is displayed to the user, predicate to post-checking and test scenario itself).
In the test scenario we first make an instance hndl1 of our proof-of-burn smart contract bound to simulated wallet w1 (line 8), then call the lock endpoint in line 10. We call this endpoint with two arguments: the address where to send toAddr value adaValueOf 50. Then it is needed to execute changes in the test blockchain environment, so we wait for one tick in line 11. Then we make another smart contract instance bound to simulated wallet w2 (line 12) and call endpoint redeem. Test scenario is complete.
Then the predicate in lines 3-5 is checked. Here we make sure that the balance of simulated wallet w1 decreased by 50 ADA, the balance of w2 increased by 50 ADA and the balance of w3 remains the same.
We provided unit tests for our proof-of-burn smart contract in UnitTests.hs.
Unit-test scenarios are hardwired (never change their behaviour). But sometimes, it is required to check smart contract properties on a wide range of behaviours. The Plutus platform has property-based testing (or dynamic logic testing). This feature allows testing multiple complex test scenarios by randomly generating them.
For this purpose, QuickCheck(quickcheck?) supports random generation of arbitrary input to a function. The developer can use the random generator to create an arbitrary test scenario and then check that this scenario runs as expected.
First, developers have to specify basic actions for interaction with the smart contract:
Proof-of-burn smart contract have four endpoints, so we introduce four actions, one action for each endpoint. We specify arguments for each endpoint in the corresponding action, for example, for lock endpoint, we specify the wallet to withdraw, the wallet to which funds will be sent after redeem and value.
Now Plutus platform can generate random sequence (with specified properties) like [Lock w1 w2 10, Lock w3 w1 20, Redeem w2, Burn w3 "h@ck_me" 50]. It is specified in arbitraryAction function in ContractModel instance using common QuickTest functions:
Note that the current model state is passed to this function, so we can generate actions depending on this current state. This can be used in more complex test scenarios. For example, if in some smart contract it is necessary to do initialisation first, we can check passed modelState and generate initialisation action. Then, when initialisation occurs, we can generate all other actions.
Each generated sequence run in two processes: first is simulation, second is running smart contract (under emulator in EmulatorTrace monad). For example, simulated run of Lock action is:
Here we mark that value v withdrawn from simulated wallet wFrom and mark that is locked in favour of the wallet wTo.
Simultaneously, the tester also runs the smart contract for this Lock action:
After each run, the platform checks that the simulation results match those on the actual run. Notably, it checks that simulated and real balances match. In our example, it checks that simulated balance after withdrawal matches with balance after callEndpoint @"lock" run in EmulatorTrace monad.
All previously described Plutus platform testing abilities combined into one: dynamic logic test scenarios which lets freely mix specific and random action sequences. For example:
Here we execute several random actions and then check that after some locks several Adas will be redeemed.
An important feature of DL test script running is the output of erroneous action sequences. For example, in early stage of testing our proof-of-burn contract, we discovered a faulty sequence (which we then fixed, but left on our test list):
Here DLScript [...] is (slightly corrected) output of the Plutus platform runner.
Validation criteria for the Proof-of-burn smart contract
We develop some criteria presented by respective property test listed below:
- prop_LockAndRedeem – here we check that redeem gets all the means locked with a lock and that burn does not interfere with this. To do this, we first call lock, then some arbitrary actions, then redeem, and then check that redeemed expected value.
- prop_BurnAndVerify – similar to the previous one, but we check that lock/redeem not interfere with burn/validateBurn.
- prop_RandomActionsIsConsistent – pure random actions. Our testing model has internal assertions and must confirm all these assertions for any sequence of actions.
As stated earlier, dynamic logic test runner can output DLTest list of actions, where some failures occur. When we develop our proof-of-burn, we have faced with such issues and save this output to tests. So we have two of them:
- prop_BurnValidatingInEmulatorIsWorks – checks that ValidateBurn correctly work in testing models.
- prop_GetObservableStateDon'tBreakEmulator – we seem to have en countered a bug in the Plutus emulator, and here we check that our test model bypasses it.
As a quick design check, a developer can use common unit tests. But property-based tests allow for much more extensive testing by randomly generating test scenarios. It is required to write an instance of ControlMonad (and some auxiliary code), but then developers can validate smart contracts against many times more test scenarios, and find most errors in the smart contract.
Using the Plutus platform’s extensive testing capabilities, we checked the performance of our application and corrected many bugs.
Ismail, Leila, and Huned Materwala. 2019. “A Review of Blockchain Architecture and Consensus Protocols: Use Cases, Challenges, and Solutions.” Symmetry 11 (10). https://doi.org/10.3390/sym11101198.
Karantias, Kostis, Aggelos Kiayias, and Dionysis Zindros. 2020. “Proof-of-Burn.” In, 523–40. https://doi.org/10.1007/978-3-030-51280-4_28.
Dmitry Krylov Michał J. Gajda