
implementing-assertions
by phylaxsystems
Agent Skills for writing Assertions
SKILL.md
name: implementing-assertions description: "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
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon