
implementing-assertions
Phylax Credible Layer assertions implementation. Implements phylax/credible layer assertion contracts using cheatcodes, triggers, and event/state inspection.
"Phylax Credible Layer assertions implementation. Implements phylax/credible layer assertion contracts using cheatcodes, triggers, and event/state inspection."
Implementing Assertions
Turn a written invariant spec into a correct, gas-safe assertion contract.
Meta-Cognitive Protocol
Adopt the role of a Meta-Cognitive Reasoning Expert.
For every complex problem:
1.DECOMPOSE: Break into sub-problems
2.SOLVE: Address each with explicit confidence (0.0-1.0)
3.VERIFY: Check logic, facts, completeness, bias
4.SYNTHESIZE: Combine using weighted confidence
5.REFLECT: If confidence <0.8, identify weakness and retry
For simple questions, skip to direct answer.
Always output:
∙Clear answer
∙Confidence level
∙Key caveats
When to Use
- Writing a new assertion contract from a defined invariant.
- Refactoring or optimizing existing assertions.
- Adding trigger logic, call input parsing, or event/state checks.
When NOT to Use
- You only need invariant ideation or trigger selection. Use
designing-assertions. - You only need testing patterns. Use
testing-assertions.
Quick Start
Setup:
- Install the standard library:
forge install phylaxsystems/credible-std(https://github.com/phylaxsystems/credible-std). - Update
remappings.txtwith:credible-std/=lib/credible-std/src/(https://github.com/phylaxsystems/credible-std)forge-std/=lib/forge-std/src/(https://github.com/foundry-rs/forge-std)
- Credible cheatcodes (
ph.*) are documented at https://docs.phylax.systems/credible/cheatcodes-overview and https://docs.phylax.systems/credible/cheatcodes-reference; usecredible-std/src/PhEvm.solin https://github.com/phylaxsystems/credible-std for the exact interface. - Credible Layer overview: https://docs.phylax.systems/credible/credible-introduction.
File and Naming Conventions
- Assertion files:
{ContractOrFeature}Assertion.a.sol(e.g.,VaultOwnerAssertion.a.sol) - Test files:
{ContractOrFeature}Assertion.t.sol(e.g.,VaultOwnerAssertion.t.sol) - Assertion functions: must start with
assertionfollowed by the property name (e.g.,assertionOwnershipChange,assertionHealthFactor,assertionSupplyCap) - Directory structure:
assertions/src/for assertion contracts,assertions/test/for tests
contract MyAssertion is Assertion {
function triggers() external view override {
registerCallTrigger(this.assertionMonotonic.selector, ITarget.doThing.selector);
}
function assertionMonotonic() external {
ITarget target = ITarget(ph.getAssertionAdopter());
ph.forkPreTx();
uint256 pre = target.totalAssets();
ph.forkPostTx();
uint256 post = target.totalAssets();
require(post >= pre, "Invariant violated");
}
}
Implementation Checklist
- Triggers: use the narrowest possible trigger; avoid global triggers (
registerCallTrigger(fn)orregisterStorageChangeTrigger(fn)without a selector/slot). - One Trigger, One Assertion: avoid multi-selector dispatchers; register each selector to its own assertion function and reuse shared helpers.
- Interface Clarity: use selectors from the interface that declares the function; if the adopter inherits it (e.g., ERC20/4626), call that out so the trigger source is obvious.
- Pre/Post: call
forkPreTx()only when needed; default is post-state. - Call-Scoped Checks: use
getCallInputs+forkPreCall/forkPostCallfor per-call invariants. - Call Input Shape:
CallInputs.inputcontains args only (selector is stripped). Decode args directly; if you needmsg.data(e.g., timelock keys), rebuild withabi.encodePacked(selector, input). - Preconditions: use
before*hook triggers orforkPreCallto assert pre-state requirements. - Baselines: for intra-tx stability, read
forkPreTx()once and compare per-call post snapshots. - Event Parsing: filter by
emitterandtopics[0]; decode indexed vs data fields correctly. - Storage Slots: use
ph.load(notvm.load) for EIP-1967 slots, packed fields, and mappings; derive slots viaforge inspect <Contract> storage-layout. - State Changes:
getStateChanges*includes the initial value at index 0; length 0 means no changes. - Constructors: cheatcodes are unavailable; prefer
ph.getAssertionAdopter()inside assertion functions and pass only constants via constructor args if needed. - Nested Calls: avoid double counting; prefer
getCallInputsto avoid proxy duplicates. - Internal Calls: internal Solidity calls are not traced; register on external entrypoints (or
this.calls) when you need call inputs. - Batch Dedupe: deduplicate targets/accounts when a batch can repeat entries.
- Tolerances: use minimal, documented tolerances for price/decimals rounding.
- Optional Interfaces: use
staticcallprobing and skip when unsupported. - Token Quirks: validate using balance deltas; handle fee-on-transfer and rebasing tokens.
- Packed Calldata: decode using protocol packing logic (assetId, amount, mode) and map ids via helpers.
- Delta-Based Supply Checks: compare totalSupply delta to sum of per-call amounts instead of enumerating users.
- Id Mapping Guards: if a packed id maps to
address(0), skip or fail early to avoid false positives. - Sentinel Amounts: normalize
max/sentinel values (e.g., full repay/withdraw) using pre-state. - Gas: assertion gas cap is 300k; happy path is often most expensive; early return, cache reads, and limit loops.
- Size Limit: organize assertions by domain (e.g., access control, timelock, accounting) and split if you hit
CreateContractSizeLimit.
Anti-Patterns
❌ Dispatcher Pattern (Avoid)
Do not route multiple triggers through one assertion function that dispatches to helpers:
// ❌ WRONG: Many triggers → one dispatcher → helpers
function triggers() external view override {
registerCallTrigger(this.assertionOwnership.selector, IVault.setFee.selector);
registerCallTrigger(this.assertionOwnership.selector, IVault.setGuardian.selector);
registerCallTrigger(this.assertionOwnership.selector, IVault.submitCap.selector);
}
function assertionOwnership() external {
// Dispatches internally based on what was called - hard to debug, wastes gas
}
✅ One Trigger, One Assertion (Preferred)
Register each trigger to its own assertion function. Share logic via internal helpers:
// ✅ CORRECT: Each trigger has its own assertion function
function triggers() external view override {
registerCallTrigger(this.assertionSetFee.selector, IVault.setFee.selector);
registerCallTrigger(this.assertionSetGuardian.selector, IVault.setGuardian.selector);
registerCallTrigger(this.assertionSubmitCap.selector, IVault.submitCap.selector);
}
function assertionSetFee() external {
_checkOnlyOwner(); // Reuse helper
}
function assertionSetGuardian() external {
_checkOnlyOwner(); // Reuse helper
}
❌ Mixed Interfaces (Avoid)
Do not mix selectors from parent and child interfaces:
// ❌ CONFUSING: Mixing IERC4626 and IVault when IVault extends IERC4626
registerCallTrigger(this.assertionDeposit.selector, IERC4626.deposit.selector);
registerCallTrigger(this.assertionSubmitCap.selector, IVault.submitCap.selector);
✅ Consistent Interface (Preferred)
Use the adopter's interface consistently:
// ✅ CLEAR: Use IVault for everything since it extends IERC4626
registerCallTrigger(this.assertionDeposit.selector, IVault.deposit.selector);
registerCallTrigger(this.assertionSubmitCap.selector, IVault.submitCap.selector);
Rationalizations to Reject
- "Use getAllCallInputs everywhere." It can double-count proxy calls.
- "Many selectors can share one assertion dispatcher." It hurts gas and makes debugging harder.
- "I can ignore nested calls." Batched flows are common and must be handled.
- "Events are enough." If events can be skipped, back them with state checks.
- "We can rely on storage layout guesses." Always derive slots from layout.
References
You Might Also Like
Related Skills

coding-agent
Run Codex CLI, Claude Code, OpenCode, or Pi Coding Agent via background process for programmatic control.
openclaw
add-uint-support
Add unsigned integer (uint) type support to PyTorch operators by updating AT_DISPATCH macros. Use when adding support for uint16, uint32, uint64 types to operators, kernels, or when user mentions enabling unsigned types, barebones unsigned types, or uint support.
pytorch
at-dispatch-v2
Convert PyTorch AT_DISPATCH macros to AT_DISPATCH_V2 format in ATen C++ code. Use when porting AT_DISPATCH_ALL_TYPES_AND*, AT_DISPATCH_FLOATING_TYPES*, or other dispatch macros to the new v2 API. For ATen kernel files, CUDA kernels, and native operator implementations.
pytorch
skill-writer
Guide users through creating Agent Skills for Claude Code. Use when the user wants to create, write, author, or design a new Skill, or needs help with SKILL.md files, frontmatter, or skill structure.
pytorch
implementing-jsc-classes-cpp
Implements JavaScript classes in C++ using JavaScriptCore. Use when creating new JS classes with C++ bindings, prototypes, or constructors.
oven-sh
implementing-jsc-classes-zig
Creates JavaScript classes using Bun's Zig bindings generator (.classes.ts). Use when implementing new JS APIs in Zig with JSC integration.
oven-sh