Phylax Credible Layer assertions implementation. Implements phylax/credible layer assertion contracts using cheatcodes, triggers, and event/state inspection.
Turn a written invariant spec into a correct, gas-safe V2 assertion contract.
designing-assertions.testing-assertions.Before writing code, read references/v2-precompiles-and-triggers.md and copy the exact signatures from the target repo's vendored credible-std when present.
For new assertions:
Assertion from credible-std.triggers(): registerFnCallTrigger, registerTxEndTrigger, registerErc20ChangeTrigger, watchCumulativeInflow, or watchCumulativeOutflow.ph.context() only inside a registerFnCallTrigger assertion.ph.staticcallAt, ph.loadStateAt, _preTx(), _postTx(), _preCall(id), _postCall(id)) instead of legacy fork switching.{ContractOrFeature}Assertion.a.sol (e.g., VaultOwnerAssertion.a.sol){ContractOrFeature}Assertion.t.sol (e.g., VaultOwnerAssertion.t.sol)assertPoolCustodyCoversBalances, assertPostOperationSolvency, or assertSharePriceEnvelope.assertions/src/ for assertion contracts, assertions/test/ for testscontract MyAssertion is Assertion {
address immutable target;
constructor(address target_) {
target = target_;
}
function triggers() external view override {
registerTxEndTrigger(this.assertAccountingEnvelope.selector);
registerFnCallTrigger(this.assertPerCallRisk.selector, ITarget.borrow.selector);
}
function assertAccountingEnvelope() external {
PhEvm.ForkId memory post = _postTx();
uint256 liabilities = _readUint(target, abi.encodeCall(ITarget.totalLiabilities, ()), post);
uint256 assets = _readUint(target, abi.encodeCall(ITarget.totalAssets, ()), post);
require(assets >= liabilities, "Target: assets below liabilities");
}
function assertPerCallRisk() external {
PhEvm.TriggerContext memory ctx = ph.context();
PhEvm.ForkId memory postCall = _postCall(ctx.callEnd);
bytes memory input = ph.callinputAt(ctx.callStart); // raw calldata: selector + args
// Decode operation context and check the post-call protocol property.
}
function _readUint(address account, bytes memory data, PhEvm.ForkId memory fork)
internal
view
returns (uint256 value)
{
PhEvm.StaticCallResult memory result = ph.staticcallAt(account, data, 50_000, fork);
require(result.ok, "Target: read failed");
value = abi.decode(result.data, (uint256));
}
}
registerFnCallTrigger(fn, selector) for per-operation properties and registerTxEndTrigger(fn) for whole-transaction envelopes.watchCumulativeOutflow or watchCumulativeInflow for rolling-window flow limits instead of custom assertion storage.ph.staticcallAt and ph.loadStateAt with _preTx(), _postTx(), _preCall(ctx.callStart), and _postCall(ctx.callEnd).ph.context() only for registerFnCallTrigger assertions; use ph.callinputAt(ctx.callStart) and ph.callOutputAt(ctx.callStart) for input/output-dependent checks.ph.callinputAt returns raw calldata including the selector. Strip the first 4 bytes before abi.decode unless your helper expects full calldata.emitter and topics[0]; decode indexed vs data fields correctly.ph.loadStateAt for EIP-1967 slots, packed fields, and mappings; derive slots from source or forge inspect <Contract> storage-layout.getStateChanges* includes the initial value at index 0; length 0 means no changes.ph.matchingCalls or legacy call-input helpers deliberately and cap result sizes.this. calls when you need call context.staticcall probing and skip when unsupported.max/sentinel values (e.g., full repay/withdraw) using pre-state.CreateContractSizeLimit.Do not route unrelated trigger families through one assertion function that dispatches to helpers:
function triggers() external view override {
registerFnCallTrigger(this.assertAdminMutation.selector, IVault.setFee.selector);
registerFnCallTrigger(this.assertAdminMutation.selector, IVault.setGuardian.selector);
registerFnCallTrigger(this.assertAdminMutation.selector, IVault.submitCap.selector);
}
function assertAdminMutation() external {
// Dispatches internally based on what was called.
}
Register separate assertion functions when failure meanings differ. Share helpers for common reads:
function triggers() external view override {
registerFnCallTrigger(this.assertSetFeeBounds.selector, IVault.setFee.selector);
registerFnCallTrigger(this.assertGuardianChangeSafe.selector, IVault.setGuardian.selector);
}
function assertSetFeeBounds() external {
_checkFeeBounds();
}
function assertGuardianChangeSafe() external {
_checkGuardianState();
}
Do not mix selectors from parent and child interfaces:
registerFnCallTrigger(this.assertDeposit.selector, IERC4626.deposit.selector);
registerFnCallTrigger(this.assertSubmitCap.selector, IVault.submitCap.selector);
Use the adopter's interface consistently:
registerFnCallTrigger(this.assertDeposit.selector, IVault.deposit.selector);
registerFnCallTrigger(this.assertSubmitCap.selector, IVault.submitCap.selector);