Co-authored-by: Michael George <michael@certora.com> Co-authored-by: Nick Armstrong <nick@certora.com> Co-authored-by: Michael George <mdgeorge@cs.cornell.edu> Co-authored-by: Aleksander Kryukov <firealexkryukov@gmail.com> Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
216 lines
8.1 KiB
Ruby
216 lines
8.1 KiB
Ruby
//// ## Verification of `Initializable`
|
|
////
|
|
//// `Initializable` is a contract used to make constructors for upgradable
|
|
//// contracts. This is accomplished by applying the `initializer` modifier to any
|
|
//// function that serves as a constructor, which makes this function only
|
|
//// callable once. The secondary modifier `reinitializer` allows for upgrades
|
|
//// that should run at most once after the contract is upgraded.
|
|
////
|
|
////
|
|
//// ### Assumptions and Simplifications
|
|
//// We assume `initializer()` and `reinitializer(1)` are equivalent if they
|
|
//// both guarantee `_initialized` to be set to 1 after a successful call. This
|
|
//// allows us to use `reinitializer(n)` as a general version that also handles
|
|
//// the regular `initialzer` case.
|
|
////
|
|
//// #### Harnessing
|
|
//// Two harness versions were implemented, a simple flat contract, and a
|
|
//// Multi-inheriting contract. The two versions together help us ensure there are
|
|
//// No unexpected results because of different implementations. `Initializable` can
|
|
//// Be used in many different ways but we believe these 2 cases provide good
|
|
//// Coverage for all cases. In both harnesses we use getter functions for
|
|
//// `_initialized` and `_initializing` and implement `initializer` and
|
|
//// `reinitializer` functions that use their respective modifiers. We also
|
|
//// Implement some versioned functions that are only callable in specific
|
|
//// Versions of the contract to mimic upgrading contracts.
|
|
////
|
|
//// #### Munging
|
|
//// Variables `_initialized` and `_initializing` were changed to have internal
|
|
//// visibility to be harnessable.
|
|
|
|
methods {
|
|
initialize(uint256, uint256, uint256) envfree
|
|
reinitialize(uint256, uint256, uint256, uint8) envfree
|
|
initialized() returns uint8 envfree
|
|
initializing() returns bool envfree
|
|
thisIsContract() returns bool envfree
|
|
|
|
returnsV1() returns uint256 envfree
|
|
returnsVN(uint8) returns uint256 envfree
|
|
returnsAV1() returns uint256 envfree
|
|
returnsAVN(uint8) returns uint256 envfree
|
|
returnsBV1() returns uint256 envfree
|
|
returnsBVN(uint8) returns uint256 envfree
|
|
a() returns uint256 envfree
|
|
b() returns uint256 envfree
|
|
val() returns uint256 envfree
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
//// #### Definitions ////
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
//// ***`isUninitialized:`*** A contract's `_initialized` variable is equal to 0.
|
|
definition isUninitialized() returns bool = initialized() == 0;
|
|
|
|
//// ***`isInitialized:`*** A contract's `_initialized` variable is greater than 0.
|
|
definition isInitialized() returns bool = initialized() > 0;
|
|
|
|
//// ***`isInitializedOnce:`*** A contract's `_initialized` variable is equal to 1.
|
|
definition isInitializedOnce() returns bool = initialized() == 1;
|
|
|
|
//// ***`isReinitialized:`*** A contract's `_initialized` variable is greater than 1.
|
|
definition isReinitialized() returns bool = initialized() > 1;
|
|
|
|
//// ***`isDisabled:`*** A contract's `_initialized` variable is equal to 255.
|
|
definition isDisabled() returns bool = initialized() == 255;
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
//// ### Properties ///////////////////////////
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
// Invariants /////////////////////////////////////
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
|
|
/// A contract must only ever be in an initializing state while in the middle
|
|
/// of a transaction execution.
|
|
invariant notInitializing()
|
|
!initializing()
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
// Rules /////////////////////////////////////
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
|
|
/// @title Only initialized once
|
|
/// @notice An initializable contract with a function that inherits the
|
|
/// initializer modifier must be initializable only once
|
|
rule initOnce() {
|
|
uint256 val; uint256 a; uint256 b;
|
|
|
|
require isInitialized();
|
|
initialize@withrevert(val, a, b);
|
|
assert lastReverted, "contract must only be initialized once";
|
|
}
|
|
|
|
/// Successfully calling reinitialize() with a version value of 1 must result
|
|
/// in `_initialized` being set to 1.
|
|
rule reinitializeEffects {
|
|
uint256 val; uint256 a; uint256 b;
|
|
|
|
reinitialize(val, a, b, 1);
|
|
|
|
assert isInitializedOnce(), "reinitialize(1) must set _initialized to 1";
|
|
}
|
|
|
|
/// Successfully calling `initialize()` must result in `_initialized` being set to 1.
|
|
/// @dev We assume `initialize()` and `reinitialize(1)` are equivalent if this rule
|
|
/// and the [above rule][#reinitializeEffects] both pass.
|
|
rule initializeEffects {
|
|
uint256 val; uint256 a; uint256 b;
|
|
|
|
initialize(val, a, b);
|
|
|
|
assert isInitializedOnce(), "initialize() must set _initialized to 1";
|
|
}
|
|
|
|
/// A disabled initializable contract must always stay disabled.
|
|
rule disabledStaysDisabled(method f) {
|
|
env e; calldataarg args;
|
|
|
|
bool disabledBefore = isDisabled();
|
|
f(e, args);
|
|
bool disabledAfter = isDisabled();
|
|
|
|
assert disabledBefore => disabledAfter, "a disabled initializer must stay disabled";
|
|
}
|
|
|
|
/// The variable `_initialized` must not decrease.
|
|
rule increasingInitialized(method f) {
|
|
env e; calldataarg args;
|
|
|
|
uint8 initBefore = initialized();
|
|
f(e, args);
|
|
uint8 initAfter = initialized();
|
|
assert initBefore <= initAfter, "_initialized must only increase";
|
|
}
|
|
|
|
/// If `reinitialize(...)` was called successfully, then the variable
|
|
/// `_initialized` must increase.
|
|
/// @title Reinitialize increases `init`
|
|
rule reinitializeIncreasesInit {
|
|
uint256 val; uint8 n; uint256 a; uint256 b;
|
|
|
|
uint8 initBefore = initialized();
|
|
reinitialize(val, a, b, n);
|
|
uint8 initAfter = initialized();
|
|
|
|
assert initAfter > initBefore, "calling reinitialize must increase _initialized";
|
|
}
|
|
|
|
/// `reinitialize(n)` must be callable if the contract is not in an
|
|
/// `_initializing` state and `n` is greater than `_initialized` and less than
|
|
/// 255
|
|
rule reinitializeLiveness {
|
|
uint256 val; uint8 n; uint256 a; uint256 b;
|
|
|
|
requireInvariant notInitializing();
|
|
uint8 initVal = initialized();
|
|
reinitialize@withrevert(val, a, b, n);
|
|
|
|
assert n > initVal => !lastReverted, "reinitialize(n) call must succeed if n was greater than _initialized";
|
|
}
|
|
|
|
/// If `reinitialize(n)` was called successfully then `n` was greater than
|
|
/// `_initialized`.
|
|
rule reinitializeRule {
|
|
uint256 val; uint8 n; uint256 a; uint256 b;
|
|
|
|
uint8 initBefore = initialized();
|
|
reinitialize(val, a, b, n);
|
|
|
|
assert n > initBefore;
|
|
}
|
|
|
|
/// Functions implemented in the parent contract that require `_initialized` to
|
|
/// be a certain value are only callable when it is that value.
|
|
/// @title Reinitialize version check parent
|
|
rule reinitVersionCheckParent {
|
|
uint8 n;
|
|
|
|
returnsVN(n);
|
|
assert initialized() == n, "parent contract's version n functions must only be callable in version n";
|
|
}
|
|
|
|
/// Functions implemented in the child contract that require `_initialized` to
|
|
/// be a certain value are only callable when it is that value.
|
|
/// @title Reinitialize version check child
|
|
rule reinitVersionCheckChild {
|
|
uint8 n;
|
|
|
|
returnsAVN(n);
|
|
assert initialized() == n, "child contract's version n functions must only be callable in version n";
|
|
}
|
|
|
|
/// Functions implemented in the grandchild contract that require `_initialized`
|
|
/// to be a certain value are only callable when it is that value.
|
|
/// @title Reinitialize version check grandchild
|
|
rule reinitVersionCheckGrandchild {
|
|
uint8 n;
|
|
|
|
returnsBVN(n);
|
|
assert initialized() == n, "gransdchild contract's version n functions must only be callable in version n";
|
|
}
|
|
|
|
/// Calling parent initializer function must initialize all child contracts.
|
|
rule inheritanceCheck {
|
|
uint256 val; uint8 n; uint256 a; uint256 b;
|
|
|
|
reinitialize(val, a, b, n);
|
|
assert val() == val && a() == a && b() == b, "all child contract values must be initialized";
|
|
}
|
|
|