dimensional-analysis
Annotates codebases with dimensional analysis comments documenting units, dimensions, and decimal scaling. Use when someone asks to annotate units in a codebase, perform a dimensional analysis, or find vulnerabilities in a DeFi protocol, offchain code, or other blockchain-related
Install
npx skills add https://github.com/trailofbits/skills/tree/main/plugins/dimensional-analysis/skills/dimensional-analysis
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install trailofbits-skills@llmmart
git clone https://github.com/trailofbits/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole trailofbits/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Dimensional Analysis Skill
This skill orchestrates a dimensional-analysis pipeline for codebases that perform numeric computations with mixed units, precisions, or scaling factors. The main skill context is a workflow controller only: it delegates scanning, vocabulary discovery, annotation, propagation, and validation to specialized subagents, then manages batching, persistence, retries, coverage gates, and final reporting.
When to Use
- Annotating a codebase with unit/dimension comments (e.g.,
D18{tok},D27{UoA/tok}) - Performing dimensional analysis on DeFi protocols, financial code, or scientific computations
- Hunting for arithmetic bugs caused by unit mismatches, missing scaling, or precision loss
- Auditing codebases with mixed decimal precisions or fixed-point arithmetic
When NOT to Use
- Codebases with no numeric arithmetic or unit conversions — there is nothing to annotate
- Pure integer counting logic (loop indices, array lengths) with no physical or financial dimensions
- When you only need a quick spot-check of a single formula — read the code directly instead of running the full pipeline
Execution Mode
This skill runs in one mode only: full-auto.
This is a workflow-based skill that delegates step-specific work to specialized agents via the Task tool. You orchestrate the overall process, manage coverage and state persistence, and ensure that every in-scope file is processed through each step of the pipeline.
- Always run the full pipeline in this order: Step 1 -> Step 2 -> Step 3 -> Step 4.
- The main skill context must not perform repository-wide dimensional analysis, annotation, propagation, or bug validation itself when a dedicated subagent exists for that step.
- The main skill context may inspect artifacts, manifests, and subagent outputs only as needed to route work, build prompts, persist state, and determine completion.
- Any mode argument provided by the caller is ignored.
- Report all results at the end in a single summary.
When you start a step, report it:
Starting Step: Step {n}
Scope and Coverage Guarantees
This skill must audit all in-scope arithmetic files, including large repositories.
- In-scope files are defined by Step 1 scanner output (
filesarray), across all priority tiers (CRITICAL, HIGH, MEDIUM, LOW). - If Step 1 narrows inputs for vocabulary discovery (for example, CRITICAL/HIGH only), that narrowing applies to discovery only. It never reduces annotation or validation scope.
arithmetic-scannerpersists the in-scope file manifest toDIMENSIONAL_SCOPE.jsonin the project root, and that manifest is the source of truth for Steps 2-4.- A file is considered fully covered only when all three statuses are present:
step2: anchor annotation completed (or explicit no-anchor result)step3: propagation completed (or explicit no-propagation result)step4: validation completed
dimension-discovererpersists the discovered dimensional vocabulary toDIMENSIONAL_UNITS.mdin the project root for reuse by later steps and future runs.- When a file ends in a terminal
BLOCKEDstate, persist the blocking reason and retry count inDIMENSIONAL_SCOPE.jsonand reflect the same file incoverage.unprocessed_files. - Do not finish while any in-scope file remains unprocessed in any step.
Delegation Contract
arithmetic-scannerowns repository scanning, arithmetic-file prioritization, and writingDIMENSIONAL_SCOPE.json.dimension-discovererowns dimensional vocabulary discovery, unit inference, and writingDIMENSIONAL_UNITS.md.dimension-annotatorowns annotation format decisions, anchor-point edits, and comment-writing behavior.dimension-propagatorowns propagation logic, inferred annotations, and mismatch reporting during tracing.dimension-validatorowns bug detection, red-flag evaluation, rationalization rejection, and confirmation or refutation of propagated mismatches.- The main skill context must not substitute its own dimensional reasoning for skipped or unlaunched subagents. If a step requires specialized reasoning, launch the corresponding subagent.
- Use reference files as subagent support material. Pass them to the relevant step in prompts instead of treating them as instructions for the main skill context.
Workflow
Follow these sections in order. Do not advance until the current step satisfies its completion gate.
Shared Orchestration Rules
DIMENSIONAL_SCOPE.jsonandDIMENSIONAL_UNITS.mdlive in the project root.- The main skill context verifies Step 1 artifacts but does not write either Step 1 artifact itself.
DIMENSIONAL_SCOPE.json.in_scope_filesis the source of truth for Steps 2-4. Never derive later scope from discovery-only inputs.- When a later step reaches terminal
BLOCKED, persist the matchingstep*_reasonandstep*_retry_countfields on the file entry inDIMENSIONAL_SCOPE.json. coverage.unprocessed_filesmust be derived from terminalBLOCKEDentries inDIMENSIONAL_SCOPE.jsonusing{ "path": "...", "blocked_step": "step2|step3|step4", "reason": "...", "retry_count": 1 }.- A step may retry a
BLOCKEDfile once with a focused prompt. If it is stillBLOCKED, keep the documented reason and continue. Do not finalize while any file remainsPENDING.
Step 1: Vocabulary and Scope Discovery
If cached artifacts cannot be reused, delegate repository scanning to arithmetic-scanner and vocabulary discovery to dimension-discoverer. Do not do that step-specific analysis directly in the main skill context.
- Check whether
DIMENSIONAL_UNITS.mdandDIMENSIONAL_SCOPE.jsonalready exist in the project root. - If both exist, read them and confirm:
DIMENSIONAL_SCOPE.json.project_rootmatches the current repo rootDIMENSIONAL_SCOPE.jsoncontainsin_scope_files,discoverer_focus_files,recommended_discovery_order, and per-filestep2,step3,step4fieldsDIMENSIONAL_UNITS.mdis a usable dimensional vocabulary for this repo
- If either artifact is stale, malformed, missing required structure, or clearly for another repo, discard reuse and rerun the rest of Step 1.
- If both artifacts are valid, reuse them directly. If
in_scope_filesis empty, skip Steps 2-4 and produce final output with zero findings. - Otherwise use the
Tasktool to spawn thearithmetic-scanneragent. Its prompt must include:- project root path
- absolute output path for
DIMENSIONAL_SCOPE.json - instruction to write the Step 1 scope manifest to disk and return the same scope data in its report
- The scanner owns Step 1 scope persistence. It must:
- identify dimensional-arithmetic files and prioritize them as usual
- write
DIMENSIONAL_SCOPE.jsonwithproject_root,in_scope_files,discoverer_focus_files, andrecommended_discovery_order - initialize every in-scope file with
step2: "PENDING",step3: "PENDING", andstep4: "PENDING" - still write an empty manifest when no arithmetic files are found
- still narrow
discoverer_focus_filesto CRITICAL/HIGH when more than 50 arithmetic files are found, while keeping all priorities inin_scope_files
- After the scanner completes, read
DIMENSIONAL_SCOPE.jsonfrom disk and confirm it exists and contains the required Step 1 fields before continuing. - Use the
Tasktool to spawn thedimension-discovereragent. Its prompt must include:- project root path
- absolute path to
DIMENSIONAL_SCOPE.json - absolute output path for
DIMENSIONAL_UNITS.md - prioritized
discoverer_focus_fileswith each file's path, priority, score, and category recommended_discovery_order
- The discoverer owns Step 1 vocabulary persistence. It must read
DIMENSIONAL_SCOPE.jsonas the Step 1 source of truth and writeDIMENSIONAL_UNITS.mdwithBase Units,Derived Units, andPrecision Prefixessections. Ifin_scope_filesis empty, it must still write the same headings with empty sections. - Step 1 is complete only when both artifacts exist on disk, pass the reuse checks above, and correctly represent the zero-file case. If
in_scope_filesis empty after the discoverer writesDIMENSIONAL_UNITS.md, skip Steps 2-4 and produce final output with zero findings.
Step 2: Anchor Annotation
The main skill context must not add annotations itself. Use the Task tool to spawn dimension-annotator agents for all anchor-point annotation work. For full examples and annotation format details, see [{baseDir}/references/annotate.md]({baseDir}/references/annotate.md).
- Read
DIMENSIONAL_SCOPE.jsonand build batches fromin_scope_files. Every in-scope file, including MEDIUM and LOW priority files, must receive a Step 2 outcome. - Batch files instead of spawning one agent per file:
<= 10files: one batch11-30files: one batch per category> 30files: one batch per category, splitting categories larger than 10 files into sub-batches of about 8 files
- Launch categories in Step 1 recommended discovery order: math libraries, then oracles, then core logic, then peripheral. Batches inside the same category may run in parallel.
- Before launching annotators, set
step2 = "PENDING"for every in-scope file and persist the updatedDIMENSIONAL_SCOPE.json. - Each annotator prompt must include:
- absolute path to
DIMENSIONAL_UNITS.md - absolute path to
DIMENSIONAL_SCOPE.json - assigned file paths in order
- each file's category and matched patterns from scanner output
- summary of previously annotated interfaces or types from earlier batches, when applicable
- required per-file status output:
ANNOTATED,REVIEWED_NO_ANCHOR_CHANGES, orBLOCKEDplus a one-line justification
- absolute path to
- After each batch, immediately persist each assigned file to exactly one Step 2 status:
ANNOTATEDREVIEWED_NO_ANCHOR_CHANGESBLOCKED
- If a file is
BLOCKED, also persiststep2_reasonandstep2_retry_count. Retry eachBLOCKEDfile once with a focused prompt. - Do not continue to Step 3 while any file remains
PENDINGin on-disk manifest state.
Step 3: Dimension Propagation
The main skill context must not perform propagation reasoning itself. Use the Task tool to spawn dimension-propagator agents to extend annotations through arithmetic, function calls, and assignments. For algebra details, see [{baseDir}/references/dimension-algebra.md]({baseDir}/references/dimension-algebra.md).
- Read
DIMENSIONAL_SCOPE.jsonand build propagation batches fromin_scope_files. Every in-scope file must receive a Step 3 outcome. - Use the same batching rules and category ordering as Step 2.
- Before launching propagators, confirm every file already has a non-pending Step 2 status.
- Then set
step3 = "PENDING"for every in-scope file and persist the updated manifest. - Each propagator prompt must include:
- absolute path to
DIMENSIONAL_UNITS.md - absolute path to
DIMENSIONAL_SCOPE.json - assigned file paths in order
- each file's category and matched patterns
- summary of Step 2 anchor annotations for the assigned files and any upstream interfaces they depend on
- required per-file status output:
PROPAGATED,REVIEWED_NO_PROPAGATION_CHANGES, orBLOCKEDplus a one-line justification
- absolute path to
- After each batch, immediately persist each assigned file to exactly one Step 3 status:
PROPAGATEDREVIEWED_NO_PROPAGATION_CHANGESBLOCKED
- If a file is
BLOCKED, also persiststep3_reasonandstep3_retry_count. Retry eachBLOCKEDfile once with a focused prompt. - After all propagators complete, aggregate:
- annotations added by confidence level (
CERTAIN,INFERRED,UNCERTAIN) - mismatches found, with severities for validator deduplication
- coverage gaps that could not be inferred
- annotations added by confidence level (
- Do not continue to Step 4 while any file remains
PENDINGin on-disk manifest state.
Step 4: Bug Detection
The main skill context must not perform bug detection itself. Use the Task tool to spawn dimension-validator agents to detect dimensional bugs in annotated code. For examples, red flags, rationalization checks, and standard vocabulary, see [{baseDir}/references/bug-patterns.md]({baseDir}/references/bug-patterns.md), [{baseDir}/references/common-dimensions.md]({baseDir}/references/common-dimensions.md), and [{baseDir}/references/dimension-algebra.md]({baseDir}/references/dimension-algebra.md). DO NOT DETECT BUGS IN ANY OTHER STEP.
- Validate every file in
DIMENSIONAL_SCOPE.json.in_scope_files. - Use this priority order without skipping lower tiers:
- files with CRITICAL or HIGH Step 3 mismatches
- remaining CRITICAL and HIGH scanner-priority files
- remaining MEDIUM and LOW files
- Before launching validators, confirm every file already has a non-pending Step 3 status.
- Then set
step4 = "PENDING"for every in-scope file and persist the updated manifest. - Spawn one
dimension-validatoragent per file. For large repos, run them in waves of roughly 10-30 files to keep orchestration stable. - Each validator prompt must include:
- absolute path to
DIMENSIONAL_UNITS.md - absolute path to
DIMENSIONAL_SCOPE.json - the single file path to validate
- a summary of anchor and propagated annotations in the file
- Step 3 mismatch summaries for the file, including mismatch IDs
- cross-file function signatures or return dimensions needed for call-boundary checks
- required per-file status output:
VALIDATEDorBLOCKED
- absolute path to
- After each wave, immediately persist each file to exactly one Step 4 status:
VALIDATEDBLOCKED
- If a file is
BLOCKED, also persiststep4_reasonandstep4_retry_count. Retry eachBLOCKEDfile once with a focused prompt. - Deduplicate findings:
- confirmed Step 3 mismatches keep their original IDs and severities
- refuted Step 3 mismatches are noted as false positives and excluded from final counts
- genuinely new findings receive new
DIM-XXXIDs
- Aggregate confirmed findings, new findings, refuted findings, coverage summary, and final
coverage.unprocessed_files. - Step 4 is complete only when
DIMENSIONAL_SCOPE.json.in_scope_filescontains nostep4: "PENDING"entries.
Reference Documentation
Pass these references to the relevant subagent when a step needs them:
[{baseDir}/references/dimension-algebra.md]({baseDir}/references/dimension-algebra.md)- Propagator and validator algebra rules[{baseDir}/references/common-dimensions.md]({baseDir}/references/common-dimensions.md)- Validator vocabulary reference[{baseDir}/references/bug-patterns.md]({baseDir}/references/bug-patterns.md)- Validator bug-pattern and red-flag reference[{baseDir}/references/annotate.md]({baseDir}/references/annotate.md)- Annotator format and example reference
Final Output
At the end of the analysis, provide a structured summary unless some other output format has been specified:
{
"mode": "full-auto",
"project_root": "<path>",
"vocabulary": {
"base_units": ["..."],
"derived_units": ["..."],
"precision_prefixes": ["..."]
},
"annotations": {
"total_added": 0,
"by_file": {}
},
"findings": {
"critical": 0,
"high": 0,
"medium": 0,
"details": []
},
"uncertainties_resolved": 0,
"coverage": {
"in_scope_files": 0,
"anchor_reviewed_files": "0/0",
"propagation_reviewed_files": "0/0",
"validation_reviewed_files": "0/0",
"annotated_functions": "0/0",
"annotated_variables": "0/0",
"unprocessed_files": [
{
"path": "/path/to/repo/contracts/LegacyMath.sol",
"blocked_step": "step3",
"reason": "Parser could not process generated source",
"retry_count": 1
}
]
}
}
Completion Checklist
You are NOT done until all of these are true:
File Coverage Gates
-
DIMENSIONAL_UNITS.mdexists in the project root -
DIMENSIONAL_SCOPE.jsonexists in the project root and is the source of truth for downstream coverage - Every in-scope arithmetic file discovered in Step 1 appears in
DIMENSIONAL_SCOPE.json.in_scope_files - Every in-scope file has a non-
PENDINGStep 2 status (ANNOTATED,REVIEWED_NO_ANCHOR_CHANGES, orBLOCKED) - Every in-scope file has a non-
PENDINGStep 3 status (PROPAGATED,REVIEWED_NO_PROPAGATION_CHANGES, orBLOCKED) - Every in-scope file has a non-
PENDINGStep 4 status (VALIDATEDorBLOCKED) - No in-scope file remains
PENDINGin any step - Any
BLOCKEDfile has a documented reason in the final output -
coverage.unprocessed_filesexactly matches the final set of terminalBLOCKEDfiles after retries, usingpath,blocked_step,reason, andretry_count
Summary Report
- Final summary JSON/report provided
- Final coverage counters match
DIMENSIONAL_SCOPE.json - List of modified files provided when edits occurred
- Any dimensional mismatches or bugs found are summarized
- Any remaining blocked or unprocessed files are called out with reasons
If DIMENSIONAL_SCOPE.json and the final report disagree, reconcile the report or continue processing until they match.
Do not claim completion from agent intent alone; completion is determined by manifest coverage and final reported statuses.
Files (skills)
-
agents
-
openai.yaml 245 B
interface: display_name: "Dimensional Analysis" short_description: "Annotate units and scaling to find arithmetic mistakes" icon_small: "assets/trail-of-bits-mark.svg" icon_large: "assets/trail-of-bits-mark.svg" brand_color: "#D83A34"
-
-
assets
-
trail-of-bits-mark.svg 3 KB · in bundle
-
-
references
-
annotate.md 11.7 KB
# Step 2: Annotate the Codebase After defining dimensions (Step 1), add annotations to all numeric values in the code. > **Note:** Examples below use Solidity syntax. For other languages, adapt the comment syntax (e.g., `//` in Rust, `#` in Python, `//` or `/** */` in TypeScript) while keeping the same dimensional annotation format. --- ## Comment Placement Patterns ### Variable Declarations ```solidity // Inline (preferred for brevity) uint256 public totalAssets; // D18{UNDERLYING} // Above (for longer descriptions) /// D18{UNDERLYING} Total assets under management, excluding pending withdrawals uint256 public totalAssets; ``` ### Function Parameters (NatSpec) ```solidity // NatSpec (preferred) /// @param assets D18{UNDERLYING} Amount to deposit /// @param receiver Address receiving shares /// @return shares D18{SHARE} Shares minted function deposit(uint256 assets, address receiver) external returns (uint256 shares); // Inline (acceptable for simple cases) function deposit( uint256 assets, // D18{UNDERLYING} address receiver ) external returns (uint256 shares); // D18{SHARE} ``` ### Struct Fields ```solidity struct Position { uint256 collateral; // D18{COLLATERAL} Collateral deposited uint256 debt; // D18{DEBT} Amount borrowed uint256 lastUpdate; // {s} Timestamp of last interest accrual } ``` ### Formula Verification Comments ```solidity // Show dimensional algebra for non-trivial formulas // // shares = assets * totalSupply / totalAssets // {SHARE} = {UNDERLYING} * {SHARE} / {UNDERLYING} // = {SHARE} ✓ // shares = assets.mulDiv(totalSupply(), totalAssets()); ``` --- ## Annotating State Variables For each state variable holding a numeric value: 1. Determine its dimension from the glossary 2. Determine its decimal scaling (D6, D8, D18, D27, etc.) 3. Add inline comment in format: `// D{scale}{dimension} Description` ### Examples ```solidity // Storage variables uint256 public totalDeposits; // D18{UNDERLYING} Total tokens deposited uint256 public totalShares; // D18{VAULT} Total vault shares outstanding uint256 public lastPriceUpdate; // {s} Timestamp of last oracle update uint256 public feeRate; // D18{1} Fee as 18-decimal fraction (1e18 = 100%) uint256 public accumulatedFees; // D18{UNDERLYING} Protocol fees collected // Mappings mapping(address => uint256) public userShares; // D18{VAULT} per user mapping(address => uint256) public userDebt; // D18{DEBT} per user mapping(address => uint256) public lastActionTime; // {s} per user ``` --- ## Annotating Function Signatures For each public/external function: 1. Add NatSpec with dimensions for all numeric parameters 2. Add dimensions for return values 3. Format: `@param name D{scale}{dimension} Description` ### Examples ```solidity /// @notice Deposit assets and receive shares /// @param assets D18{UNDERLYING} Amount of tokens to deposit /// @param minShares D18{VAULT} Minimum shares to receive (slippage protection) /// @return shares D18{VAULT} Actual shares minted function deposit(uint256 assets, uint256 minShares) external returns (uint256 shares); /// @notice Get current price from oracle /// @return price D8{USD/UNDERLYING} Current asset price function getPrice() external view returns (uint256 price); /// @notice Calculate health factor for a position /// @param collateralValue D18{USD} Total collateral value in USD /// @param debtValue D18{USD} Total debt value in USD /// @return healthFactor D18{1} Health factor (>1e18 is healthy) function calculateHealthFactor( uint256 collateralValue, uint256 debtValue ) external pure returns (uint256 healthFactor); ``` --- ## Full Annotated Examples ### ERC-4626 Vault ```solidity /** * ═══════════════════════════════════════════════════════════ * DIMENSIONAL GLOSSARY * ═══════════════════════════════════════════════════════════ * * Base Dimensions: * - {UNDERLYING} The underlying asset token * - {SHARE} Vault share token * - {s} Time in seconds * - {1} Dimensionless * * Derived Dimensions: * - {SHARE/UNDERLYING} Exchange rate (shares per asset) * - {UNDERLYING/SHARE} Inverse exchange rate */ contract Vault is ERC4626 { uint256 public totalAssets; // D{X}{UNDERLYING} where X = token decimals uint256 public totalSupply; // D18{SHARE} uint256 public lastHarvestTime; // {s} Timestamp of last yield harvest uint256 public performanceFee; // D18{1} Fee rate (1e18 = 100%) /// @notice Deposit assets and receive shares /// @param assets D{X}{UNDERLYING} Amount to deposit /// @return shares D18{SHARE} Shares minted function deposit(uint256 assets) external returns (uint256 shares) { // {SHARE} = {UNDERLYING} * {SHARE} / {UNDERLYING} = {SHARE} ✓ shares = assets.mulDiv(totalSupply(), totalAssets()); } /// @notice Withdraw assets by burning shares /// @param shares D18{SHARE} Shares to burn /// @return assets D{X}{UNDERLYING} Assets returned function withdraw(uint256 shares) external returns (uint256 assets) { // {UNDERLYING} = {SHARE} * {UNDERLYING} / {SHARE} = {UNDERLYING} ✓ assets = shares.mulDiv(totalAssets(), totalSupply()); } /// @notice Get current exchange rate /// @return rate D18{UNDERLYING/SHARE} Assets per share function exchangeRate() external view returns (uint256 rate) { // {UNDERLYING/SHARE} = {UNDERLYING} * 1e18 / {SHARE} rate = totalAssets().mulDiv(1e18, totalSupply()); } } ``` ### AMM / DEX ```solidity /** * ═══════════════════════════════════════════════════════════ * DIMENSIONAL GLOSSARY * ═══════════════════════════════════════════════════════════ * * Base Dimensions: * - {TOKEN_A} First token in pair * - {TOKEN_B} Second token in pair * - {LP} Liquidity provider token * - {1} Dimensionless * * Derived Dimensions: * - {TOKEN_A * TOKEN_B} Constant product invariant * - {TOKEN_B / TOKEN_A} Price of A in terms of B */ contract AMM { uint256 public reserveA; // D{X}{TOKEN_A} uint256 public reserveB; // D{Y}{TOKEN_B} uint256 public totalSupply; // D18{LP} uint256 public kLast; // {TOKEN_A * TOKEN_B} Last invariant value uint256 public swapFee; // D18{1} Fee rate (e.g., 3e15 = 0.3%) /// @notice Swap token A for token B /// @param amountAIn D{X}{TOKEN_A} Amount of token A to swap /// @return amountBOut D{Y}{TOKEN_B} Amount of token B received function swapAForB(uint256 amountAIn) external returns (uint256 amountBOut) { // Calculate output using constant product formula // (reserveA + amountAIn) * (reserveB - amountBOut) = k // // amountBOut = reserveB - k / (reserveA + amountAIn) // {TOKEN_B} = {TOKEN_B} - {TOKEN_A * TOKEN_B} / {TOKEN_A} // {TOKEN_B} = {TOKEN_B} - {TOKEN_B} = {TOKEN_B} ✓ } /// @notice Get current price of A in terms of B /// @return price D18{TOKEN_B / TOKEN_A} Price function getPrice() external view returns (uint256 price) { // {TOKEN_B / TOKEN_A} = {TOKEN_B} * 1e18 / {TOKEN_A} price = reserveB.mulDiv(1e18, reserveA); } /// @notice Add liquidity /// @param amountA D{X}{TOKEN_A} Amount of token A /// @param amountB D{Y}{TOKEN_B} Amount of token B /// @return lpTokens D18{LP} LP tokens minted function addLiquidity(uint256 amountA, uint256 amountB) external returns (uint256 lpTokens) { // LP tokens proportional to liquidity added // {LP} = {LP} * {TOKEN_A} / {TOKEN_A} = {LP} ✓ lpTokens = totalSupply.mulDiv(amountA, reserveA); } } ``` ### Lending Protocol ```solidity /** * ═══════════════════════════════════════════════════════════ * DIMENSIONAL GLOSSARY * ═══════════════════════════════════════════════════════════ * * Base Dimensions: * - {COLLATERAL} Collateral token (e.g., WETH) * - {DEBT} Borrowed token (e.g., USDC) * - {USD} Oracle price denomination * - {aToken} Receipt token for deposits * - {s} Time in seconds * - {1} Dimensionless * * Derived Dimensions: * - {USD/COLLATERAL} Collateral price * - {USD/DEBT} Debt token price * - {1/s} Interest rate per second */ contract LendingPool { uint256 public totalBorrowed; // D{X}{DEBT} uint256 public totalCollateral; // D{Y}{COLLATERAL} uint256 public liquidationRatio; // D18{1} (e.g., 1.5e18 = 150%) uint256 public borrowRate; // D27{1/s} Interest rate per second uint256 public lastAccrualTime; // {s} Last interest accrual timestamp struct Position { uint256 collateral; // D{Y}{COLLATERAL} Collateral deposited uint256 debt; // D{X}{DEBT} Amount borrowed uint256 lastUpdate; // {s} Timestamp of last update } mapping(address => Position) public positions; /// @notice Calculate health factor for a position /// @param collateral D{Y}{COLLATERAL} Collateral amount /// @param collateralPrice D8{USD/COLLATERAL} Oracle price /// @param debt D{X}{DEBT} Debt amount /// @param debtPrice D8{USD/DEBT} Oracle price /// @return healthFactor D18{1} Health factor (>1e18 is healthy) function getHealthFactor( uint256 collateral, uint256 collateralPrice, uint256 debt, uint256 debtPrice ) external pure returns (uint256 healthFactor) { // collateralValue = collateral * collateralPrice / 1e8 // D18{USD} = D{Y}{COLLATERAL} * D8{USD/COLLATERAL} / 1e8 uint256 collateralValue = collateral.mulDiv(collateralPrice, 1e8); // debtValue = debt * debtPrice / 1e8 // D18{USD} = D{X}{DEBT} * D8{USD/DEBT} / 1e8 uint256 debtValue = debt.mulDiv(debtPrice, 1e8); // healthFactor = collateralValue * 1e18 / debtValue // D18{1} = D18{USD} * 1e18 / D18{USD} healthFactor = collateralValue.mulDiv(1e18, debtValue); } /// @notice Borrow tokens against collateral /// @param amount D{X}{DEBT} Amount to borrow function borrow(uint256 amount) external { Position storage pos = positions[msg.sender]; pos.debt += amount; // {DEBT} + {DEBT} = {DEBT} ✓ totalBorrowed += amount; // {DEBT} + {DEBT} = {DEBT} ✓ } /// @notice Deposit collateral /// @param amount D{Y}{COLLATERAL} Amount to deposit function depositCollateral(uint256 amount) external { Position storage pos = positions[msg.sender]; pos.collateral += amount; // {COLLATERAL} + {COLLATERAL} = {COLLATERAL} ✓ totalCollateral += amount; // {COLLATERAL} + {COLLATERAL} = {COLLATERAL} ✓ } } ``` --- ## Step 2 Checklist - [ ] Annotate all numeric state variables with `// D{scale}{dimension}` - [ ] Annotate all function parameters with NatSpec dimensions - [ ] Annotate all return values with dimensions - [ ] Annotate all struct fields with dimensions - [ ] Add dimensional algebra comments for non-trivial formulas - [ ] Ensure price dimensions clearly indicate numerator/denominator - [ ] Ensure different token types have distinct dimensions -
bug-patterns.md 11.7 KB
# Dimensional Bug Patterns This document catalogs common dimensional bugs with examples and detection strategies. Examples use Solidity syntax, but these bug patterns occur in any language performing arithmetic with mixed units and scaling factors (Rust, TypeScript, Python, etc.). ## Critical Bugs (P0) ### Pattern 1: Unit Mismatch in Price Feeds **Description:** Oracle returns price in different precision than expected. **Example:** ```solidity // Contract assumes D27 prices uint256 price; // D27{UoA/tok} // But Chainlink returns D8! (, int256 answer,,,) = priceFeed.latestRoundData(); price = uint256(answer); // BUG: D8 assigned to D27 variable // Correct: price = uint256(answer) * 1e19; // Scale D8 to D27 ``` **Impact:** Price values off by 10^19, causing catastrophic mispricing. **Detection:** Check oracle `decimals()` vs expected precision. --- ### Pattern 2: Cross-Contract Dimension Assumption Mismatch **Description:** Caller assumes different dimension than callee returns. **Example:** ```solidity // Protocol A's Vault function getSharePrice() external returns (uint256) { return totalAssets * 1e18 / totalShares; // Returns D18{tok/share} } // Protocol B consuming it uint256 sharePrice; // D27{tok/share} - WRONG ASSUMPTION sharePrice = vaultA.getSharePrice(); // BUG: D18 value in D27 variable // Correct: sharePrice = vaultA.getSharePrice() * 1e9; ``` **Impact:** 9 orders of magnitude error in calculations. **Detection:** Compare interface documentation and actual return dimensions. --- ### Pattern 3: Adding Incompatible Dimensions **Description:** Adding values with different semantic meanings. **Example:** ```solidity // User's position uint256 tokenBalance; // {tok} uint256 shareBalance; // {share} // BUG: Can't add tokens and shares! uint256 totalPosition = tokenBalance + shareBalance; // Correct: Convert to common dimension uint256 totalTokens = tokenBalance + convertToAssets(shareBalance); ``` **Impact:** Result is mathematically meaningless. **Detection:** Verify both operands have identical dimensions. --- ### Pattern 4: Wrong Precision Causing Overflow **Description:** Multiplication without scaling causes overflow or precision explosion. **Example:** ```solidity uint256 amount; // D18{tok} uint256 price; // D27{UoA/tok} // BUG: D18 * D27 = D45, overflows! uint256 value = amount * price; // Correct: uint256 value = Math.mulDiv(amount, price, 1e27); // Result: D18{UoA} ``` **Impact:** Overflow reverts or silent wraparound. **Detection:** Track precision through multiplication chains. --- ## High Severity Bugs (P1) ### Pattern 5: Missing Scaling Factor **Description:** Calculation omits necessary precision adjustment. **Example:** ```solidity // Calculate shares from deposit uint256 assets; // D18{tok} uint256 supply; // D18{share} uint256 totalAssets; // D18{tok} // BUG: Missing D18 scaling uint256 shares = assets * supply / totalAssets; // Actually: D18 * D18 / D18 = D18, but intermediate is D36! // Correct: uint256 shares = Math.mulDiv(assets, supply, totalAssets); ``` **Impact:** Precision loss or overflow in intermediate calculation. **Detection:** Verify all multiplications are properly scaled. --- ### Pattern 6: Wrong Scaling Direction **Description:** Multiply when should divide, or vice versa. **Example:** ```solidity uint256 priceD27; // D27{UoA/tok} // BUG: Multiplied instead of divided uint256 priceD18 = priceD27 * 1e9; // Now D36! // Correct: uint256 priceD18 = priceD27 / 1e9; ``` **Impact:** Value off by 10^18. **Detection:** Verify scaling direction matches precision conversion intent. --- ### Pattern 7: Inconsistent Return Path Dimensions **Description:** Different code paths return values with different dimensions. **Example:** ```solidity function getValue(bool useOracle) returns (uint256) { if (useOracle) { return oracle.getPrice(token); // D8{UoA/tok} } else { return cachedPrice; // D18{UoA/tok} - DIFFERENT DIMENSION! } } ``` **Impact:** Callers receive inconsistent values. **Detection:** Trace all return paths and verify dimensions match. --- ### Pattern 8: Implicit Precision Truncation **Description:** High precision value assigned to lower precision variable. **Example:** ```solidity uint256 preciseValue; // D27{UoA} uint256 result; // D18{UoA} // BUG: Truncates 9 decimal places result = preciseValue / 1e9; // If preciseValue = 1.5e27 (1.5 in D27) // result = 1.5e18 (1.5 in D18) - OK // But if preciseValue = 1e18 (tiny in D27) // result = 1e9 (still tiny in D18, lost precision) ``` **Impact:** Small values may become zero. **Detection:** Identify precision reductions and check for rounding issues. --- ## Medium Severity Bugs (P2) ### Pattern 9: Redundant Scaling **Description:** Unnecessary conversion that wastes gas or introduces rounding. **Example:** ```solidity uint256 priceD18; // D18{UoA/tok} // Redundant: scale up then down uint256 temp = priceD18 * 1e9; // D27 uint256 result = temp / 1e9; // Back to D18 // Could just use priceD18 directly ``` **Impact:** Gas waste, possible rounding errors. **Detection:** Identify inverse scaling operations. --- ### Pattern 10: Fee Applied to Wrong Dimension **Description:** Fee percentage applied to value instead of amount, or vice versa. **Example:** ```solidity uint256 depositAmount; // {tok} uint256 feePercent; // D18{1} uint256 pricePerToken; // D27{UoA/tok} // BUG: Fee on USD value, not token amount uint256 fee = depositAmount * pricePerToken * feePercent / 1e45; // Correct: Fee on token amount uint256 fee = depositAmount * feePercent / 1e18; // {tok} ``` **Impact:** Fee calculation incorrect, may over/under charge. **Detection:** Verify fee is applied to intended base. --- ### Pattern 11: Time Unit Confusion **Description:** Mixing seconds with other time units. **Example:** ```solidity uint256 ratePerYear; // D18{1} annual rate uint256 elapsed; // {s} seconds // BUG: Applying annual rate to seconds uint256 accrued = principal * ratePerYear * elapsed / 1e18; // Correct: Convert to per-second rate uint256 SECONDS_PER_YEAR = 365.25 days; uint256 ratePerSecond = ratePerYear / SECONDS_PER_YEAR; uint256 accrued = principal * ratePerSecond * elapsed / 1e18; ``` **Impact:** Interest/fees off by ~31.5 million. **Detection:** Verify time unit consistency in rate calculations. --- ### Pattern 12: Division Before Multiplication **Description:** Dividing first causes precision loss. **Example:** ```solidity uint256 a = 100; uint256 b = 3; uint256 c = 7; // BUG: Division truncates uint256 result = a / b * c; // = 33 * 7 = 231 // Correct: uint256 result = a * c / b; // = 700 / 3 = 233 ``` **Impact:** Silent precision loss. **Detection:** Reorder to multiply before divide, or use mulDiv. --- ## Common Traps These traps catch even experienced auditors. Always verify explicitly. ### Trap 1: Assumed Dimensionless Constant ```solidity // Is this correct? uint256 result = amount * MULTIPLIER / DIVISOR; // Question: Are MULTIPLIER and DIVISOR truly dimensionless? // If MULTIPLIER is actually a price {USD/TOKEN}, this formula is WRONG // If DIVISOR is actually an amount {TOKEN}, this formula is WRONG // Always verify what constants represent ``` ### Trap 2: Hidden Dimension in Helper Function ```solidity // Is this correct? uint256 normalized = normalize(amount); // Question: What dimension does normalize() return? // - Does it change {TOKEN_A} to {TOKEN_B}? // - Does it convert to {USD}? // - Does it change scale but not dimension? // MUST trace into the helper function ``` ### Trap 3: Chained Operations Hiding Dimension Changes ```solidity // Is this correct? uint256 final = step1(step2(step3(input))); // MUST trace dimensions through each step // Don't assume the chain is correct because each function "looks right" // Trace: // input : {A} // step3(input) : {?} - determine from step3's implementation // step2(step3...) : {?} - determine from step2's implementation // step1(step2...) : {?} - determine from step1's implementation // final : expected {?} ``` ### Trap 4: Scale Confused with Dimension ```solidity // Common mistake: uint256 price = oracle.getPrice(); // D8{USD/TOKEN} uint256 value = amount * price / 1e18; // WRONG! Should be 1e8 // Scale is PART of dimensional correctness // D18{TOKEN} * D8{USD/TOKEN} / 1e18 = D8{USD} ← wrong scale // D18{TOKEN} * D8{USD/TOKEN} / 1e8 = D18{USD} ← correct scale ``` ### Trap 5: Decimals vs Amounts (Common Real Bug) ```solidity // Extremely common bug pattern: uint256 decimals = IERC20(token).decimals(); // Returns 18, dimension is {1} uint256 amount = vault.convertToAssets(decimals); // WRONG! Expects {SHARE} // decimals is a COUNT, not a token amount // It has dimension {1}, not {TOKEN} or {SHARE} ``` **Real-world example:** ```solidity // BUGGY CODE (simplified from real audit) function price(address _asset) external view returns (uint256 latestAnswer) { address underlying = IERC4626(_asset).asset(); (latestAnswer, ) = IOracle(msg.sender).getPrice(underlying); uint256 tokenDecimals = IERC20Metadata(underlying).decimals(); uint256 pricePerFullShare = IERC4626(_asset).convertToAssets(tokenDecimals); latestAnswer = latestAnswer * pricePerFullShare / tokenDecimals; } ``` **Dimensional analysis catches this:** ``` Step 1: What does convertToAssets expect? According to ERC-4626: input is {SHARE}, output is {ASSET} Step 2: What is tokenDecimals? It's the NUMBER of decimals (e.g., 18), dimension is {1} NOT a token amount! Step 3: Trace the call convertToAssets(tokenDecimals) convertToAssets({1}) // WRONG! Expects {SHARE} This passes a dimensionless number where a share amount is expected! BUG: decimals (a count, {1}) was passed to a function expecting assets ({SHARE}) ``` ### Trap 6: Same Name, Different Tokens ```solidity // Is this correct? uint256 total = totalSupplyA + totalSupplyB; // If A and B are different tokens: // {TOKEN_A} + {TOKEN_B} = ??? (INVALID) // Same-named variables for different tokens can't be added ``` --- ## Detection Strategies ### Static Analysis 1. **Parse annotations** - Extract all dimensional comments 2. **Build dimension graph** - Map variable → dimension 3. **Trace arithmetic** - Apply algebra rules 4. **Check assignments** - LHS must match RHS dimension 5. **Check function boundaries** - Args match params, returns match declarations ### Code Patterns to Flag ```solidity // Flag: Multiplication without mulDiv a * b // Needs dimension check // Flag: Direct oracle assignment price = oracle.latestAnswer() // Check precision match // Flag: Addition of different variables total = valueA + valueB // Verify same dimension // Flag: Return in conditional if (x) return a; else return b; // Verify a and b same dimension // Flag: Scaling literals value * 1e9 // Verify direction correct value / 1e18 // Verify scaling appropriate ``` ### Human Review Triggers - Cross-contract calls (external assumptions) - Complex multi-step calculations - Non-standard token decimals (not 18) - Custom oracle implementations - Protocol-specific units ## False Positive Avoidance ### Acceptable Patterns ```solidity // Intentional dimensionless arithmetic uint256 doubled = amount * 2; // {tok} * {1} = {tok} // Loop bounds for (uint256 i = 0; i < length; i++) // {1} // Explicit documented conversion // Intentionally converting D27 to D18 with precision loss uint256 approxPrice = precisePrice / 1e9; // Test contracts contract MockOracle { ... } // Ignore test code ``` ### Context Clues - Check for comments explaining intent - Check for test file paths - Check for "mock" or "test" in names - Check for explicit precision documentation -
common-dimensions.md 7.3 KB
# Common Dimensions in DeFi This document catalogs standard dimensional units used across DeFi protocols. While examples use Solidity syntax, the dimensional vocabulary is protocol-agnostic and applies equally to Rust (Anchor, CosmWasm), TypeScript, Python, or any other language implementing DeFi logic. ## Universal Base Units ### Token Amounts: `{tok}` Represents a quantity of tokens. ```solidity uint256 public totalSupply; // {tok} uint256 public balanceOf; // {tok} uint256 amount; // {tok} ``` **Typical precision:** D6 (USDC, USDT), D8 (WBTC), D18 (most ERC20) ### Share Amounts: `{share}` Represents vault/pool shares. ```solidity uint256 public totalShares; // {share} uint256 userShares; // {share} ``` **Typical precision:** D18 ### Time: `{s}` Timestamps and durations in seconds. ```solidity uint256 public lastUpdate; // {s} uint256 elapsed; // {s} uint256 duration; // {s} block.timestamp // {s} ``` **Precision:** Integer (no decimals) ### Dimensionless: `{1}` Pure ratios, percentages, multipliers. ```solidity uint256 public feeRate; // D18{1} uint256 percent; // D18{1} or D4{1} for basis points uint256 multiplier; // D18{1} ``` **Typical precision:** D18 or D4 (basis points) ### Unit of Account: `{UoA}` Abstract value unit, typically USD-equivalent. ```solidity uint256 public totalValue; // {UoA} uint256 price; // {UoA} (absolute) ``` **Note:** Often implicit in price dimensions like `{UoA/tok}` ## Standard Derived Units ### Exchange Rate: `{tok/share}` or `{share/tok}` Conversion ratio between tokens and shares. ```solidity uint256 public exchangeRate; // D18{tok/share} uint256 sharePrice; // D18{tok/share} ``` ### Price: `{UoA/tok}` Value per token in unit of account. ```solidity uint256 public tokenPrice; // D8{UoA/tok} (Chainlink) uint256 oraclePrice; // D18{UoA/tok} or D27{UoA/tok} ``` ### Cross Price: `{tokA/tokB}` Exchange rate between two tokens. ```solidity uint256 public swapRate; // D18{tokA/tokB} ``` ### Rate Per Second: `{1/s}` Time-based rate (fees, interest). ```solidity uint256 public interestRate; // D18{1/s} uint256 feePerSecond; // D27{1/s} ``` ### Value Per Share: `{UoA/share}` Share value in unit of account. ```solidity uint256 public nav; // D18{UoA/share} uint256 shareValue; // D27{UoA/share} ``` ## Protocol-Specific Units ### Reserve Protocol | Unit | Description | Example | |------|-------------|---------| | `{BU}` | Basket Unit | Target basket composition | | `{tok/BU}` | Tokens per basket | Weight in basket | | `{UoA/BU}` | Basket value | Basket price | | `{BU/share}` | Baskets per share | RToken backing | | `{RToken}` | RToken amount | Alias for `{share}` | | `{RSR}` | RSR token amount | Staking token | ```solidity uint256 public basketsNeeded; // {BU} uint256 weight; // D27{tok/BU} uint256 price; // D27{UoA/tok} ``` ### Lending Protocols (Aave, Compound) | Unit | Description | Example | |------|-------------|---------| | `{debt}` | Debt token amount | Borrowed amount | | `{collateral}` | Collateral amount | Deposited collateral | | `{aToken}` | Aave interest-bearing token | Deposit receipt | | `{cToken}` | Compound interest-bearing token | Deposit receipt | | `{1}` | Health factor, LTV | Risk metrics | ```solidity uint256 public totalDebt; // {debt} uint256 healthFactor; // D18{1} uint256 ltv; // D4{1} (basis points) ``` ### AMM Protocols (Uniswap, Curve) | Unit | Description | Example | |------|-------------|---------| | `{liq}` | Liquidity units | Pool liquidity | | `{LP}` | LP token amount | Liquidity provider shares | | `{sqrtP}` | Square root price | Uniswap V3 | ```solidity uint256 public liquidity; // {liq} uint256 lpBalance; // {LP} uint160 sqrtPriceX96; // Q96{sqrtP} ``` ### Staking Protocols | Unit | Description | Example | |------|-------------|---------| | `{staked}` | Staked token amount | Deposited stake | | `{reward}` | Reward token amount | Earned rewards | | `{reward/staked}` | Reward rate | Per-token rewards | ```solidity uint256 public totalStaked; // {staked} uint256 rewardPerToken; // D18{reward/staked} ``` ## Standard Precision Levels | Prefix | Value | Common Usage | |--------|-------|--------------| | D4 | 1e4 | Basis points | | D6 | 1e6 | USDC, USDT decimals | | D8 | 1e8 | WBTC, Chainlink prices | | D18 | 1e18 | Standard ERC20, most calculations | | D27 | 1e27 | High-precision prices (Reserve) | | Q96 | 2^96 | Uniswap V3 fixed-point | ## Interface Dimensions ### ERC20 ```solidity function totalSupply() external view returns (uint256); // {tok} function balanceOf(address) external view returns (uint256); // {tok} function decimals() external view returns (uint8); // precision info function transfer(address, uint256 amount) external; // amount: {tok} function approve(address, uint256 amount) external; // amount: {tok} function transferFrom(address, address, uint256 amount); // amount: {tok} function allowance(address, address) returns (uint256); // {tok} ``` ### ERC4626 ```solidity function asset() external view returns (address); // underlying token function totalAssets() external view returns (uint256); // {tok} function convertToShares(uint256 assets) returns (uint256); // {tok} → {share} function convertToAssets(uint256 shares) returns (uint256); // {share} → {tok} function maxDeposit(address) external view returns (uint256); // {tok} function maxMint(address) external view returns (uint256); // {share} function maxWithdraw(address) external view returns (uint256); // {tok} function maxRedeem(address) external view returns (uint256); // {share} function previewDeposit(uint256 assets) returns (uint256); // {tok} → {share} function previewMint(uint256 shares) returns (uint256); // {share} → {tok} function previewWithdraw(uint256 assets) returns (uint256); // {tok} → {share} function previewRedeem(uint256 shares) returns (uint256); // {share} → {tok} function deposit(uint256 assets, address) returns (uint256); // {tok} → {share} function mint(uint256 shares, address) returns (uint256); // {share} → {tok} function withdraw(uint256 assets, ...) returns (uint256); // {tok} → {share} function redeem(uint256 shares, ...) returns (uint256); // {share} → {tok} ``` ### Chainlink ```solidity function decimals() external view returns (uint8); // usually 8 function latestRoundData() external view returns ( uint80 roundId, int256 answer, // D8{UoA/tok} typically uint256 startedAt, // {s} uint256 updatedAt, // {s} uint80 answeredInRound ); ``` ## Naming Convention Hints | Pattern | Likely Dimension | |---------|-----------------| | `*Balance`, `*Amount` | `{tok}` | | `*Shares`, `share*` | `{share}` | | `*Price`, `price*` | `{UoA/tok}` | | `*Rate`, `rate*` | `{1}` or `{1/s}` | | `*Time`, `*Timestamp` | `{s}` | | `*Duration`, `*Period` | `{s}` | | `*Fee`, `fee*` | `{1}` | | `*Ratio`, `ratio*` | `{1}` | | `*Value`, `value*` | `{UoA}` | | `*Per*` | derived unit | | `total*` | aggregate amount | | `max*`, `min*` | bounds (same as base) | -
dimension-algebra.md 6.3 KB
# Dimensional Algebra Rules This document defines the rules for dimensional arithmetic. While examples use Solidity syntax, these algebraic rules are universal and apply to any language performing fixed-point or scaled arithmetic. ## Notation - `{A}` - A semantic unit (e.g., `{tok}`, `{share}`, `{UoA}`) - `D18` - A precision prefix indicating 18 decimal places - `D18{A}` - A value with unit `{A}` and precision D18 - `{A/B}` - A derived unit (A per B) - `{A*B}` - A compound unit (A times B) - `{1}` - Dimensionless (pure ratio) ### Formal Grammar ``` annotation := scale? "{" dimension "}" scale := "D" number dimension := base_dim | derived_dim | "1" derived_dim := dimension "/" dimension | dimension "*" dimension base_dim := identifier ``` **Examples:** - `{tok}` - Token amount (no scale specified) - `D18{tok}` - Token amount, 18 decimals fixed-point - `D27{USD/tok}` - Price, 27 decimals fixed-point - `{1}` - Dimensionless (pure number or ratio) ## Basic Composition Rules ### Multiplication Dimensions multiply when values are multiplied: ``` {A} * {B} = {A*B} Examples: {tok} * {UoA/tok} = {UoA} # tokens × price = value {share} * {tok/share} = {tok} # shares × exchange rate = tokens {1} * {A} = {A} # dimensionless preserves dimension ``` ### Division Dimensions divide when values are divided: ``` {A} / {B} = {A/B} Examples: {tok} / {share} = {tok/share} # exchange rate {UoA} / {tok} = {UoA/tok} # price {A} / {A} = {1} # same dimensions cancel {A} / {1} = {A} # dividing by dimensionless preserves ``` ### Addition and Subtraction **CRITICAL: Addition and subtraction require identical dimensions.** ``` {A} + {A} = {A} # Valid {A} - {A} = {A} # Valid {A} + {B} = ERROR # Invalid! Dimension mismatch {A} - {B} = ERROR # Invalid! Dimension mismatch Examples: {tok} + {tok} = {tok} # Valid: adding token amounts {tok} + {share} = ERROR # Invalid: can't add tokens and shares {UoA/tok} + {UoA/tok} = {UoA/tok} # Valid: adding prices ``` ## Precision Arithmetic ### Multiplication Precision Precisions ADD when multiplying: ``` D18 * D18 = D36 D27 * D18 = D45 D18 * D27 = D45 Example: D18{tok} * D18{share/tok} = D36{share} # Need to scale down by D18 ``` ### Division Precision Precisions SUBTRACT when dividing: ``` D36 / D18 = D18 D27 / D18 = D9 D18 / D18 = D0 (integer) Example: D36{share} / D18 = D18{share} # Scaling down D27{UoA/tok} / D18{1} = D9{UoA/tok} # Precision reduced ``` ### Scaling Operations Scaling is multiplication/division by a pure precision constant: ``` D18{A} * D9 = D27{A} # Scale up precision D27{A} / D9 = D18{A} # Scale down precision D36{A} / D18 = D18{A} # Common pattern after multiplication ``` ## Common Patterns ### Price Calculation ```solidity // Calculate value from amount and price // {UoA} = {tok} * D27{UoA/tok} / D27 // D27{UoA} = D18{tok} * D27{UoA/tok} / D18 uint256 value = Math.mulDiv(amount, price, D18); ``` ### Share Conversion (ERC4626) ```solidity // Convert assets to shares // {share} = {tok} * {share} / {tok} // D18{share} = D18{tok} * D18{share} / D18{tok} uint256 shares = Math.mulDiv(assets, totalSupply, totalAssets); // Convert shares to assets // {tok} = {share} * {tok} / {share} uint256 assets = Math.mulDiv(shares, totalAssets, totalSupply); ``` ### Fee Application ```solidity // Apply percentage fee // {tok} = {tok} * D18{1} / D18 uint256 fee = Math.mulDiv(amount, feeRate, D18); uint256 netAmount = amount - fee; ``` ### Rate Per Second ```solidity // Calculate accrued amount // {tok} = {tok} * D18{1/s} * {s} / D18 uint256 accrued = Math.mulDiv(principal, rate * elapsed, D18); ``` ### Cross-Rate Calculation ```solidity // Calculate token A price in terms of token B // D27{B/A} = D27{UoA/A} * D27 / D27{UoA/B} uint256 crossRate = Math.mulDiv(priceA, D27, priceB); ``` ## Dimensional Simplification ### Cancellation When the same unit appears in numerator and denominator, it cancels: ``` {tok/share} * {share} = {tok} # share cancels {UoA/tok} * {tok/BU} = {UoA/BU} # tok cancels {A/B} * {B/C} = {A/C} # B cancels ``` ### Identity ``` {A} * {1} = {A} {A} / {1} = {A} {A} * {B/B} = {A} # Multiplying by 1 ``` ## Multi-Step Calculations For complex expressions, track dimensions step by step: ```solidity // Calculate share value in UoA // Step 1: {tok/share} = {tok} / {share} // Step 2: {UoA/share} = {tok/share} * {UoA/tok} // // In code: // D18{UoA/share} = D18{tok} * D27{UoA/tok} / D18{share} / D9 uint256 shareValue = Math.mulDiv( Math.mulDiv(totalAssets, price, totalShares), 1, 1e9 // Scale D27 to D18 ); ``` ## Error Patterns ### Division Before Multiplication **Risky for precision loss:** ```solidity // BAD: May lose precision uint256 result = a / b * c; // BETTER: Use mulDiv uint256 result = Math.mulDiv(a, c, b); ``` ### Missing Scaling **Common bug pattern:** ```solidity // BUG: D36 result stored in D18 variable uint256 result = amount * rate; // D18 * D18 = D36! // CORRECT: uint256 result = Math.mulDiv(amount, rate, D18); ``` ### Wrong Scaling Direction ```solidity // BUG: Multiplied when should divide uint256 price18 = price27 * 1e9; // Now D36! // CORRECT: uint256 price18 = price27 / 1e9; ``` ## Special Cases ### Dimensionless Constants Integer constants like `2`, `100`, `MAX_UINT` are dimensionless `{1}`: ```solidity uint256 doubled = amount * 2; // {tok} * {1} = {tok} uint256 half = amount / 2; // {tok} / {1} = {tok} ``` ### Timestamps Timestamps and durations have dimension `{s}` (seconds): ```solidity uint256 elapsed = block.timestamp - lastUpdate; // {s} - {s} = {s} uint256 rate = feePerSecond * elapsed; // {1/s} * {s} = {1} ``` ### Basis Points Basis points are `{1}` with implicit D4 precision: ```solidity uint256 constant BPS = 10000; // D4 uint256 fee = amount * feeBps / BPS; // {tok} * {1} / {1} = {tok} ``` ## Validation Checklist For any arithmetic operation: 1. ✓ Do operand dimensions combine correctly? 2. ✓ Is the result dimension what's expected? 3. ✓ Is precision handled correctly (scaling)? 4. ✓ Is rounding direction appropriate? 5. ✓ Could intermediate values overflow?
-
-
SKILL.md 17.4 KB
--- name: dimensional-analysis description: "Annotates codebases with dimensional analysis comments documenting units, dimensions, and decimal scaling. Use when someone asks to annotate units in a codebase, perform a dimensional analysis, or find vulnerabilities in a DeFi protocol, offchain code, or other blockchain-related codebase with arithmetic. Prevents dimensional mismatches and catches formula bugs early." allowed-tools: Read Write Grep List Glob Task TodoRead TodoWrite --- # Dimensional Analysis Skill This skill orchestrates a dimensional-analysis pipeline for codebases that perform numeric computations with mixed units, precisions, or scaling factors. The main skill context is a workflow controller only: it delegates scanning, vocabulary discovery, annotation, propagation, and validation to specialized subagents, then manages batching, persistence, retries, coverage gates, and final reporting. ## When to Use - Annotating a codebase with unit/dimension comments (e.g., `D18{tok}`, `D27{UoA/tok}`) - Performing dimensional analysis on DeFi protocols, financial code, or scientific computations - Hunting for arithmetic bugs caused by unit mismatches, missing scaling, or precision loss - Auditing codebases with mixed decimal precisions or fixed-point arithmetic ## When NOT to Use - Codebases with no numeric arithmetic or unit conversions — there is nothing to annotate - Pure integer counting logic (loop indices, array lengths) with no physical or financial dimensions - When you only need a quick spot-check of a single formula — read the code directly instead of running the full pipeline ## Execution Mode This skill runs in one mode only: `full-auto`. This is a workflow-based skill that delegates step-specific work to specialized agents via the `Task` tool. You orchestrate the overall process, manage coverage and state persistence, and ensure that every in-scope file is processed through each step of the pipeline. - Always run the full pipeline in this order: Step 1 -> Step 2 -> Step 3 -> Step 4. - The main skill context must not perform repository-wide dimensional analysis, annotation, propagation, or bug validation itself when a dedicated subagent exists for that step. - The main skill context may inspect artifacts, manifests, and subagent outputs only as needed to route work, build prompts, persist state, and determine completion. - Any mode argument provided by the caller is ignored. - Report all results at the end in a single summary. When you start a step, report it: ```text Starting Step: Step {n} ``` ## Scope and Coverage Guarantees This skill must audit **all in-scope arithmetic files**, including large repositories. - In-scope files are defined by Step 1 scanner output (`files` array), across **all** priority tiers (CRITICAL, HIGH, MEDIUM, LOW). - If Step 1 narrows inputs for vocabulary discovery (for example, CRITICAL/HIGH only), that narrowing applies to discovery only. It **never** reduces annotation or validation scope. - `arithmetic-scanner` persists the in-scope file manifest to `DIMENSIONAL_SCOPE.json` in the project root, and that manifest is the source of truth for Steps 2-4. - A file is considered fully covered only when all three statuses are present: - `step2`: anchor annotation completed (or explicit no-anchor result) - `step3`: propagation completed (or explicit no-propagation result) - `step4`: validation completed - `dimension-discoverer` persists the discovered dimensional vocabulary to `DIMENSIONAL_UNITS.md` in the project root for reuse by later steps and future runs. - When a file ends in a terminal `BLOCKED` state, persist the blocking reason and retry count in `DIMENSIONAL_SCOPE.json` and reflect the same file in `coverage.unprocessed_files`. - Do not finish while any in-scope file remains unprocessed in any step. ## Delegation Contract - `arithmetic-scanner` owns repository scanning, arithmetic-file prioritization, and writing `DIMENSIONAL_SCOPE.json`. - `dimension-discoverer` owns dimensional vocabulary discovery, unit inference, and writing `DIMENSIONAL_UNITS.md`. - `dimension-annotator` owns annotation format decisions, anchor-point edits, and comment-writing behavior. - `dimension-propagator` owns propagation logic, inferred annotations, and mismatch reporting during tracing. - `dimension-validator` owns bug detection, red-flag evaluation, rationalization rejection, and confirmation or refutation of propagated mismatches. - The main skill context must not substitute its own dimensional reasoning for skipped or unlaunched subagents. If a step requires specialized reasoning, launch the corresponding subagent. - Use reference files as subagent support material. Pass them to the relevant step in prompts instead of treating them as instructions for the main skill context. ## Workflow Follow these sections in order. Do not advance until the current step satisfies its completion gate. ### Shared Orchestration Rules - `DIMENSIONAL_SCOPE.json` and `DIMENSIONAL_UNITS.md` live in the project root. - The main skill context verifies Step 1 artifacts but does not write either Step 1 artifact itself. - `DIMENSIONAL_SCOPE.json.in_scope_files` is the source of truth for Steps 2-4. Never derive later scope from discovery-only inputs. - When a later step reaches terminal `BLOCKED`, persist the matching `step*_reason` and `step*_retry_count` fields on the file entry in `DIMENSIONAL_SCOPE.json`. - `coverage.unprocessed_files` must be derived from terminal `BLOCKED` entries in `DIMENSIONAL_SCOPE.json` using `{ "path": "...", "blocked_step": "step2|step3|step4", "reason": "...", "retry_count": 1 }`. - A step may retry a `BLOCKED` file once with a focused prompt. If it is still `BLOCKED`, keep the documented reason and continue. Do not finalize while any file remains `PENDING`. ### Step 1: Vocabulary and Scope Discovery If cached artifacts cannot be reused, delegate repository scanning to `arithmetic-scanner` and vocabulary discovery to `dimension-discoverer`. Do not do that step-specific analysis directly in the main skill context. 1. Check whether `DIMENSIONAL_UNITS.md` and `DIMENSIONAL_SCOPE.json` already exist in the project root. 2. If both exist, read them and confirm: - `DIMENSIONAL_SCOPE.json.project_root` matches the current repo root - `DIMENSIONAL_SCOPE.json` contains `in_scope_files`, `discoverer_focus_files`, `recommended_discovery_order`, and per-file `step2`, `step3`, `step4` fields - `DIMENSIONAL_UNITS.md` is a usable dimensional vocabulary for this repo 3. If either artifact is stale, malformed, missing required structure, or clearly for another repo, discard reuse and rerun the rest of Step 1. 4. If both artifacts are valid, reuse them directly. If `in_scope_files` is empty, skip Steps 2-4 and produce final output with zero findings. 5. Otherwise use the `Task` tool to spawn the `arithmetic-scanner` agent. Its prompt must include: - project root path - absolute output path for `DIMENSIONAL_SCOPE.json` - instruction to write the Step 1 scope manifest to disk and return the same scope data in its report 6. The scanner owns Step 1 scope persistence. It must: - identify dimensional-arithmetic files and prioritize them as usual - write `DIMENSIONAL_SCOPE.json` with `project_root`, `in_scope_files`, `discoverer_focus_files`, and `recommended_discovery_order` - initialize every in-scope file with `step2: "PENDING"`, `step3: "PENDING"`, and `step4: "PENDING"` - still write an empty manifest when no arithmetic files are found - still narrow `discoverer_focus_files` to CRITICAL/HIGH when more than 50 arithmetic files are found, while keeping all priorities in `in_scope_files` 7. After the scanner completes, read `DIMENSIONAL_SCOPE.json` from disk and confirm it exists and contains the required Step 1 fields before continuing. 8. Use the `Task` tool to spawn the `dimension-discoverer` agent. Its prompt must include: - project root path - absolute path to `DIMENSIONAL_SCOPE.json` - absolute output path for `DIMENSIONAL_UNITS.md` - prioritized `discoverer_focus_files` with each file's path, priority, score, and category - `recommended_discovery_order` 9. The discoverer owns Step 1 vocabulary persistence. It must read `DIMENSIONAL_SCOPE.json` as the Step 1 source of truth and write `DIMENSIONAL_UNITS.md` with `Base Units`, `Derived Units`, and `Precision Prefixes` sections. If `in_scope_files` is empty, it must still write the same headings with empty sections. 10. Step 1 is complete only when both artifacts exist on disk, pass the reuse checks above, and correctly represent the zero-file case. If `in_scope_files` is empty after the discoverer writes `DIMENSIONAL_UNITS.md`, skip Steps 2-4 and produce final output with zero findings. ### Step 2: Anchor Annotation The main skill context must not add annotations itself. Use the `Task` tool to spawn `dimension-annotator` agents for all anchor-point annotation work. For full examples and annotation format details, see `[{baseDir}/references/annotate.md]({baseDir}/references/annotate.md)`. - Read `DIMENSIONAL_SCOPE.json` and build batches from `in_scope_files`. Every in-scope file, including MEDIUM and LOW priority files, must receive a Step 2 outcome. - Batch files instead of spawning one agent per file: - `<= 10` files: one batch - `11-30` files: one batch per category - `> 30` files: one batch per category, splitting categories larger than 10 files into sub-batches of about 8 files - Launch categories in Step 1 recommended discovery order: math libraries, then oracles, then core logic, then peripheral. Batches inside the same category may run in parallel. - Before launching annotators, set `step2 = "PENDING"` for every in-scope file and persist the updated `DIMENSIONAL_SCOPE.json`. - Each annotator prompt must include: - absolute path to `DIMENSIONAL_UNITS.md` - absolute path to `DIMENSIONAL_SCOPE.json` - assigned file paths in order - each file's category and matched patterns from scanner output - summary of previously annotated interfaces or types from earlier batches, when applicable - required per-file status output: `ANNOTATED`, `REVIEWED_NO_ANCHOR_CHANGES`, or `BLOCKED` plus a one-line justification - After each batch, immediately persist each assigned file to exactly one Step 2 status: - `ANNOTATED` - `REVIEWED_NO_ANCHOR_CHANGES` - `BLOCKED` - If a file is `BLOCKED`, also persist `step2_reason` and `step2_retry_count`. Retry each `BLOCKED` file once with a focused prompt. - Do not continue to Step 3 while any file remains `PENDING` in on-disk manifest state. ### Step 3: Dimension Propagation The main skill context must not perform propagation reasoning itself. Use the `Task` tool to spawn `dimension-propagator` agents to extend annotations through arithmetic, function calls, and assignments. For algebra details, see `[{baseDir}/references/dimension-algebra.md]({baseDir}/references/dimension-algebra.md)`. - Read `DIMENSIONAL_SCOPE.json` and build propagation batches from `in_scope_files`. Every in-scope file must receive a Step 3 outcome. - Use the same batching rules and category ordering as Step 2. - Before launching propagators, confirm every file already has a non-pending Step 2 status. - Then set `step3 = "PENDING"` for every in-scope file and persist the updated manifest. - Each propagator prompt must include: - absolute path to `DIMENSIONAL_UNITS.md` - absolute path to `DIMENSIONAL_SCOPE.json` - assigned file paths in order - each file's category and matched patterns - summary of Step 2 anchor annotations for the assigned files and any upstream interfaces they depend on - required per-file status output: `PROPAGATED`, `REVIEWED_NO_PROPAGATION_CHANGES`, or `BLOCKED` plus a one-line justification - After each batch, immediately persist each assigned file to exactly one Step 3 status: - `PROPAGATED` - `REVIEWED_NO_PROPAGATION_CHANGES` - `BLOCKED` - If a file is `BLOCKED`, also persist `step3_reason` and `step3_retry_count`. Retry each `BLOCKED` file once with a focused prompt. - After all propagators complete, aggregate: - annotations added by confidence level (`CERTAIN`, `INFERRED`, `UNCERTAIN`) - mismatches found, with severities for validator deduplication - coverage gaps that could not be inferred - Do not continue to Step 4 while any file remains `PENDING` in on-disk manifest state. ### Step 4: Bug Detection The main skill context must not perform bug detection itself. Use the `Task` tool to spawn `dimension-validator` agents to detect dimensional bugs in annotated code. For examples, red flags, rationalization checks, and standard vocabulary, see `[{baseDir}/references/bug-patterns.md]({baseDir}/references/bug-patterns.md)`, `[{baseDir}/references/common-dimensions.md]({baseDir}/references/common-dimensions.md)`, and `[{baseDir}/references/dimension-algebra.md]({baseDir}/references/dimension-algebra.md)`. DO NOT DETECT BUGS IN ANY OTHER STEP. - Validate every file in `DIMENSIONAL_SCOPE.json.in_scope_files`. - Use this priority order without skipping lower tiers: 1. files with CRITICAL or HIGH Step 3 mismatches 2. remaining CRITICAL and HIGH scanner-priority files 3. remaining MEDIUM and LOW files - Before launching validators, confirm every file already has a non-pending Step 3 status. - Then set `step4 = "PENDING"` for every in-scope file and persist the updated manifest. - Spawn one `dimension-validator` agent per file. For large repos, run them in waves of roughly 10-30 files to keep orchestration stable. - Each validator prompt must include: - absolute path to `DIMENSIONAL_UNITS.md` - absolute path to `DIMENSIONAL_SCOPE.json` - the single file path to validate - a summary of anchor and propagated annotations in the file - Step 3 mismatch summaries for the file, including mismatch IDs - cross-file function signatures or return dimensions needed for call-boundary checks - required per-file status output: `VALIDATED` or `BLOCKED` - After each wave, immediately persist each file to exactly one Step 4 status: - `VALIDATED` - `BLOCKED` - If a file is `BLOCKED`, also persist `step4_reason` and `step4_retry_count`. Retry each `BLOCKED` file once with a focused prompt. - Deduplicate findings: - confirmed Step 3 mismatches keep their original IDs and severities - refuted Step 3 mismatches are noted as false positives and excluded from final counts - genuinely new findings receive new `DIM-XXX` IDs - Aggregate confirmed findings, new findings, refuted findings, coverage summary, and final `coverage.unprocessed_files`. - Step 4 is complete only when `DIMENSIONAL_SCOPE.json.in_scope_files` contains no `step4: "PENDING"` entries. ## Reference Documentation Pass these references to the relevant subagent when a step needs them: - `[{baseDir}/references/dimension-algebra.md]({baseDir}/references/dimension-algebra.md)` - Propagator and validator algebra rules - `[{baseDir}/references/common-dimensions.md]({baseDir}/references/common-dimensions.md)` - Validator vocabulary reference - `[{baseDir}/references/bug-patterns.md]({baseDir}/references/bug-patterns.md)` - Validator bug-pattern and red-flag reference - `[{baseDir}/references/annotate.md]({baseDir}/references/annotate.md)` - Annotator format and example reference ## Final Output At the end of the analysis, provide a structured summary unless some other output format has been specified: ```json { "mode": "full-auto", "project_root": "<path>", "vocabulary": { "base_units": ["..."], "derived_units": ["..."], "precision_prefixes": ["..."] }, "annotations": { "total_added": 0, "by_file": {} }, "findings": { "critical": 0, "high": 0, "medium": 0, "details": [] }, "uncertainties_resolved": 0, "coverage": { "in_scope_files": 0, "anchor_reviewed_files": "0/0", "propagation_reviewed_files": "0/0", "validation_reviewed_files": "0/0", "annotated_functions": "0/0", "annotated_variables": "0/0", "unprocessed_files": [ { "path": "/path/to/repo/contracts/LegacyMath.sol", "blocked_step": "step3", "reason": "Parser could not process generated source", "retry_count": 1 } ] } } ``` ## Completion Checklist You are NOT done until all of these are true: ### File Coverage Gates - [ ] `DIMENSIONAL_UNITS.md` exists in the project root - [ ] `DIMENSIONAL_SCOPE.json` exists in the project root and is the source of truth for downstream coverage - [ ] Every in-scope arithmetic file discovered in Step 1 appears in `DIMENSIONAL_SCOPE.json.in_scope_files` - [ ] Every in-scope file has a non-`PENDING` Step 2 status (`ANNOTATED`, `REVIEWED_NO_ANCHOR_CHANGES`, or `BLOCKED`) - [ ] Every in-scope file has a non-`PENDING` Step 3 status (`PROPAGATED`, `REVIEWED_NO_PROPAGATION_CHANGES`, or `BLOCKED`) - [ ] Every in-scope file has a non-`PENDING` Step 4 status (`VALIDATED` or `BLOCKED`) - [ ] No in-scope file remains `PENDING` in any step - [ ] Any `BLOCKED` file has a documented reason in the final output - [ ] `coverage.unprocessed_files` exactly matches the final set of terminal `BLOCKED` files after retries, using `path`, `blocked_step`, `reason`, and `retry_count` ### Summary Report - [ ] Final summary JSON/report provided - [ ] Final coverage counters match `DIMENSIONAL_SCOPE.json` - [ ] List of modified files provided when edits occurred - [ ] Any dimensional mismatches or bugs found are summarized - [ ] Any remaining blocked or unprocessed files are called out with reasons **If `DIMENSIONAL_SCOPE.json` and the final report disagree, reconcile the report or continue processing until they match.** **Do not claim completion from agent intent alone; completion is determined by manifest coverage and final reported statuses.**
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.