Contract Invariants
Properties that should always hold.
Contract invariants are properties that we expect to be maintained by the contract at 'all times'.
Invariant example: "The total of all balances doesn't change"
Contract invariants in Scribble are similar to class invariants in object oriented languages - internal functions to the contract are allowed to temporarily violate the invariants. However whenever we "exit" the contract (by either returning from a call into it, or by making an external call from it), the invariants must be true. We refer to these points in the execution as places where the state is externally observable.
Observable State
The state of a contract is observable whenever some external contract can read it.
before call (observable): The state of a contract before executing a transaction is observable! At this point anyone, and any contract could read and call into a contract. Those callers / readers expect the invariant to hold.
during call (not-observable): During a contract's execution no external cotnract looks at the state of the contract, since no other contract is executing. So it's fine if an invariant doesn't hold at this point.
before making external call out (observable): If we call to another contract, then that contract can depend our state or call back. This external contract expects the invariant to hold!
after (returning from) external call (observable): Once a contract is finished executing we are back to a scenario where anyone and any contract can depend on our state. So, the invariant should hold again at this point.
Example Observability
Let's explore this with an example. The invariant we'll use is "the variable x is 0"
Syntax
Invariants have the following structure, and are placed above contract declarations:
This is a property could be used in a token contract to assert that the token is not inflationary or deflationary.
External Calls
Don't use external calls in invariants!
There are two kinds of external calls: Ones that call to this
and ones that call into other contracts.
Scribble asserts that an invariant is valid at the end of contract construction, as this is the first point where contract state becomes observable. However, due to the mechanics of the EVM the contract account is not 100% set up. Calls into the contract don't work yet, because there is not actually any code at the contract address until the constructor returns. This is why you can't use re-entrant calls like this.getTotalBalance()
in a contract invariant.
The second type of external call, those to other contracts, is possible with Scribble. But we consider this an anti-pattern.
Invariants are meant to describe properties that must always hold for a single contract. We write properties for the contract in isolation, agnostic of any external components. This makes testing more efficient and reflects the composability of (DeFi) smart contracts.
Last updated