Claude-skill-registry implementing-assertions
Phylax Credible Layer assertions implementation. Implements phylax/credible layer assertion contracts using cheatcodes, triggers, and event/state inspection.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/implementing-assertions" ~/.claude/skills/majiayu000-claude-skill-registry-implementing-assertions && rm -rf "$T"
skills/data/implementing-assertions/SKILL.mdImplementing 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:
(https://github.com/phylaxsystems/credible-std).forge install phylaxsystems/credible-std - Update
with:remappings.txt
(https://github.com/phylaxsystems/credible-std)credible-std/=lib/credible-std/src/
(https://github.com/foundry-rs/forge-std)forge-std/=lib/forge-std/src/
- Credible cheatcodes (
) are documented at https://docs.phylax.systems/credible/cheatcodes-overview and https://docs.phylax.systems/credible/cheatcodes-reference; useph.*
in https://github.com/phylaxsystems/credible-std for the exact interface.credible-std/src/PhEvm.sol - Credible Layer overview: https://docs.phylax.systems/credible/credible-introduction.
File and Naming Conventions
- Assertion files:
(e.g.,{ContractOrFeature}Assertion.a.sol
)VaultOwnerAssertion.a.sol - Test files:
(e.g.,{ContractOrFeature}Assertion.t.sol
)VaultOwnerAssertion.t.sol - Assertion functions: must start with
followed by the property name (e.g.,assertion
,assertionOwnershipChange
,assertionHealthFactor
)assertionSupplyCap - Directory structure:
for assertion contracts,assertions/src/
for testsassertions/test/
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 (
orregisterCallTrigger(fn)
without a selector/slot).registerStorageChangeTrigger(fn) - 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
only when needed; default is post-state.forkPreTx() - Call-Scoped Checks: use
+getCallInputs
/forkPreCall
for per-call invariants.forkPostCall - Call Input Shape:
contains args only (selector is stripped). Decode args directly; if you needCallInputs.input
(e.g., timelock keys), rebuild withmsg.data
.abi.encodePacked(selector, input) - Preconditions: use
hook triggers orbefore*
to assert pre-state requirements.forkPreCall - Baselines: for intra-tx stability, read
once and compare per-call post snapshots.forkPreTx() - Event Parsing: filter by
andemitter
; decode indexed vs data fields correctly.topics[0] - Storage Slots: use
(notph.load
) for EIP-1967 slots, packed fields, and mappings; derive slots viavm.load
.forge inspect <Contract> storage-layout - State Changes:
includes the initial value at index 0; length 0 means no changes.getStateChanges* - Constructors: cheatcodes are unavailable; prefer
inside assertion functions and pass only constants via constructor args if needed.ph.getAssertionAdopter() - Nested Calls: avoid double counting; prefer
to avoid proxy duplicates.getCallInputs - Internal Calls: internal Solidity calls are not traced; register on external entrypoints (or
calls) when you need call inputs.this. - Batch Dedupe: deduplicate targets/accounts when a batch can repeat entries.
- Tolerances: use minimal, documented tolerances for price/decimals rounding.
- Optional Interfaces: use
probing and skip when unsupported.staticcall - 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
, skip or fail early to avoid false positives.address(0) - Sentinel Amounts: normalize
/sentinel values (e.g., full repay/withdraw) using pre-state.max - 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.