5. Assessing your specs through mutation testing

How do you know if your rules are good enough to catch potential bugs? One technique is called “mutation testing” where small faults are injected in to the program and it is checked against the same rule. Verification should fail if the rule is good at catching the fault. If verification passes, that means your rule has gaps that must be addressed.

We have provided 3 hand-written mutants in src/certora/mutants. Copy them one at a time to src/ and rename them to lib.rs to replace the original src/lib.rs. Then run the rules you wrote above for transfer on these mutants. Are they caught?

Can you detect what the mutation was, for each mutant? You can see the solution in solutions/bugs-in-mutants.md.

Note that there are other ways to assess the quality of your rule. You can mutate the rule to see if it is vacuous, you can check if the rule is a tautology, and you can use UNSAT cores to understand what parts of the code were covered by the rule.

Mutant 1. Solution

The bug is that the value read when the addr key is not present in storage is 1 but it should be 0.

pub fn read_balance(e: &Env, addr: &Address) -> i64 {
    e.storage().persistent().get(&addr).unwrap_or(1)
}
Mutant 2. Solution

The bug is that transfer calls spend_balance twice instead of calling it once followed by a receive_balance.

pub fn transfer(e: &Env, from: Address, to: Address, amount: i64) {
    from.require_auth();
    check_nonnegative_amount(amount);
    spend_balance(&e, from.clone(), amount);
    spend_balance(&e, to.clone(), amount);
}
Mutant 3. Solution

The bug is that transfer calls spend_balance(&e, from.clone(), amount + 1); instead of spend_balance(&e, from.clone(), amount);.

pub fn transfer(e: &Env, from: Address, to: Address, amount: i64) {
    from.require_auth();
    check_nonnegative_amount(amount);
    spend_balance(&e, from.clone(), amount + 1);
    receive_balance(&e, to.clone(), amount);
}