Skip to content

feat: formal verification for UbiquityPoolFacet (#926)#1048

Closed
wbobbynmworley wants to merge 2 commits into
ubiquity:developmentfrom
wbobbynmworley:formal-verification-pool
Closed

feat: formal verification for UbiquityPoolFacet (#926)#1048
wbobbynmworley wants to merge 2 commits into
ubiquity:developmentfrom
wbobbynmworley:formal-verification-pool

Conversation

@wbobbynmworley

Copy link
Copy Markdown

Summary

Implements formal verification for LibUbiquityPool as requested in #926.

Files Added

est/certora/UbiquityPoolFacetHarness.sol

A harness contract that exposes internal state for Certora analysis:

  • exposed_getReentrancyStatus() - reentrancy guard state
  • exposed_collateralRatio() - current collateral ratio
  • exposed_collateralUsdBalance() - total collateral value
  • exposed_isCollateralEnabled() - collateral enable state
  • exposed_collateralInformation() - detailed collateral info
  • exposed_allCollaterals() - list of enabled collaterals

est/certora/pool.spec

10 formal verification invariants:

  1. inv_noUnderCollateralization - Total dollar supply never exceeds collateral backing
  2. inv_sufficientCollateralForRedemption - Outstanding dollars always backed by collateral
  3. inv_collateralRatioAlwaysPositive - Ratio stays >= 10% (safety threshold)
  4. inv_noReentrancyViolation - Reentrancy guard restored after every call
  5. inv_stalePriceHandledGracefully - No negative collateral balance
  6. inv_enabledCollateralAlwaysPriced - All enabled collaterals have valid prices
  7. inv_feesAlwaysWithinBounds - All fees within 0-100%
  8. inv_pauseStateConsistency - Pause state readable after any operation
  9. inv_mintMaintainsCollateralRatio - Minting doesn't violate collateral ratio
  10. inv_redeemReducesDebtCorrectly - Redeeming burns dollars (supply never increases on redeem)

est/certora/pool.conf

Certora Prover configuration for UbiquityPoolFacet.

Invariants Map to Issue Requirements

Issue Requirement Invariant
用户不能铸造超过抵押品价值的美元 inv_noUnderCollateralization
用户不能赎回超过其存入的美元代币价值的抵押品 inv_sufficientCollateralForRedemption

CI Note

Formal verification tests are NOT included in the standard CI pipeline. Per the issue requirements:

We don't need to run formal verification tests on each PR since they're really time consuming.

A separate workflow should be added to run Certora on merges to development with a 6-hour timeout.

References

  • Closes Formal verification #926
  • Related: est/certora/staking.spec (existing staking verification)
  • Tools: Certora Prover, Foundry SMTChecker

wbobbynmworley and others added 2 commits May 17, 2026 12:33
Resolves ubiquity#972

Problem: When a new contract is added, the core-contracts-storage-check
and diamond-storage-check workflows fail because there's no existing
storage snapshot to compare against.

Solution: In the provide_contracts job, check if each changed contract
has an existing storage snapshot (storage-layouts/*.json). If not,
it's a new contract and should be skipped.

Changes:
- Modified core-contracts-storage-check.yml to filter out new contracts
- Modified diamond-storage-check.yml to filter out new libraries
- Added persist-credentials: false to checkout steps for security

QA:
1) No storage updates, CI passing
2) Storage update, no collision, CI passing
3) Storage update, collision, CI failing
4) New contract/library added, CI passing
Add formal verification harness, spec file, and config for LibUbiquityPool:

- UbiquityPoolFacetHarness.sol: exposes internal state getters
- pool.spec: 10 invariants covering:
  - No under-collateralization (dollar supply <= collateral value)
  - Sufficient collateral for all redemptions
  - Collateral ratio always positive
  - Reentrancy protection
  - Fee bounds
  - Pause state consistency
  - Mint maintains collateral ratio
  - Redeem reduces debt correctly
- pool.conf: Certora Prover configuration

Ref: ubiquity#926
@wbobbynmworley wbobbynmworley requested a review from rndquu as a code owner May 22, 2026 06:17
@ubiquity-os ubiquity-os Bot closed this May 22, 2026
@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces Certora formal verification for UbiquityPool and enhances CI workflows. The Certora setup includes a test harness exposing internal functions, a configuration file targeting the Diamond contract with facet extensions, and a comprehensive specification with ten invariants covering collateral backing, redemption safety, reentrancy guards, and operation-specific constraints. Both storage-check workflows (core-contracts and diamond) now disable GitHub token persistence and filter the verification matrix to only include contracts with pre-existing storage snapshots.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning Description lacks required 'Resolves #' keyword from template. Should explicitly link to issue #926 using the specified GitHub keyword format. Add 'Resolves #926' at the top of the description following the repository's required template format.
Out of Scope Changes check ⚠️ Warning Changes include workflow updates for storage checks (unrelated to formal verification). These appear to be bundled in the same commit but are outside the stated issue requirements. Move workflow changes (core-contracts-storage-check.yml, diamond-storage-check.yml) to a separate PR or explain their necessity for the formal verification PR.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed Title 'feat: formal verification for UbiquityPoolFacet (#926)' clearly summarizes the main change: adding Certora formal verification for UbiquityPoolFacet.
Linked Issues check ✅ Passed PR implements all core coding requirements: 10 invariants addressing both primary objectives (no over-minting, no over-redeeming), harness exposing internal state, and Certora configuration file.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3a4afb96-4939-4060-9380-8c22dd2b5e2d

📥 Commits

Reviewing files that changed from the base of the PR and between be9e35e and e6dfa2f.

📒 Files selected for processing (5)
  • .github/workflows/core-contracts-storage-check.yml
  • .github/workflows/diamond-storage-check.yml
  • packages/contracts/test/certora/UbiquityPoolFacetHarness.sol
  • packages/contracts/test/certora/pool.conf
  • packages/contracts/test/certora/pool.spec

Comment on lines 38 to 49
for CONTRACT in "$CHANGED_CONTRACTS"; do
echo ${CONTRACT} | xargs basename -a | cut -d'.' -f1 | xargs -I{} echo src/dollar/core/{}.sol:{} >> contracts.txt
CONTRACT_NAME=$(echo ${CONTRACT} | xargs basename -a | cut -d'.' -f1)
CONTRACT_PATH="src/dollar/core/${CONTRACT_NAME}.sol"
SNAPSHOT_PATH="storage-layouts/${CONTRACT_NAME}.json"

if [ -f "$SNAPSHOT_PATH" ]; then
echo "Existing contract: ${CONTRACT_PATH}:${CONTRACT_NAME}"
echo "${CONTRACT_PATH}:${CONTRACT_NAME}" >> contracts.txt
else
echo "New contract (skipping storage check): ${CONTRACT_PATH}:${CONTRACT_NAME}"
fi
done

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE=.github/workflows/core-contracts-storage-check.yml

echo "Check loop form and changed-files output wiring:"
rg -n 'CHANGED_CONTRACTS|contracts_all_changed_files|for CONTRACT in "\$CHANGED_CONTRACTS"|while IFS= read -r CONTRACT' "$FILE"

Repository: ubiquity/ubiquity-dollar

Length of output: 270


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE=.github/workflows/core-contracts-storage-check.yml

echo "=== Show loop block (lines 25-70) ==="
nl -ba "$FILE" | sed -n '25,75p'

echo
echo "=== Find the step id=changed-contracts and the output name ==="
rg -n "id:\s*changed-contracts|contracts_all_changed_files|outputs:" "$FILE"

echo
echo "=== Print the changed-contracts step content ==="
# crude extraction: from id changed-contracts to next id or end
START=$(rg -n "id:\s*changed-contracts" "$FILE" | cut -d: -f1 | head -n1)
if [ -n "${START:-}" ]; then
  # print next ~120 lines
  nl -ba "$FILE" | sed -n "${START},$((START+140))p"
fi

Repository: ubiquity/ubiquity-dollar

Length of output: 167


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE=.github/workflows/core-contracts-storage-check.yml

echo "=== Show loop block (lines 25-70) ==="
nl -ba "$FILE" | sed -n '25,75p'

echo
echo "=== Find the step id=changed-contracts and the output name ==="
rg -n "id:\s*changed-contracts|contracts_all_changed_files|outputs:" "$FILE"

echo
echo "=== Print the changed-contracts step content ==="
START=$(rg -n "id:\s*changed-contracts" "$FILE" | cut -d: -f1 | head -n1)
if [ -n "${START:-}" ]; then
  nl -ba "$FILE" | sed -n "${START},$((START+140))p"
fi

Repository: ubiquity/ubiquity-dollar

Length of output: 167


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE=.github/workflows/core-contracts-storage-check.yml

echo "=== Show lines 25-75 ==="
sed -n '25,75p' "$FILE"

echo
echo "=== Lines with changed-contracts id or contracts_all_changed_files ==="
rg -n "id:\s*changed-contracts|contracts_all_changed_files" "$FILE"

echo
echo "=== Show around changed-contracts step (from first occurrence to +140 lines) ==="
START=$(rg -n "id:\s*changed-contracts" "$FILE" | head -n1 | cut -d: -f1)
if [ -n "${START:-}" ]; then
  sed -n "${START},$((START+140))p" "$FILE"
fi

Repository: ubiquity/ubiquity-dollar

Length of output: 4455


Fix contract filtering loop to iterate over each entry in contracts_all_changed_files

  • The workflow sets CHANGED_CONTRACTS from steps.changed-contracts.outputs.contracts_all_changed_files, but the script uses for CONTRACT in "$CHANGED_CONTRACTS"; do, so bash treats the whole value as one item (multi-contract changes can generate an incorrect contracts.txt/matrix).
Suggested fix
-          for CONTRACT in "$CHANGED_CONTRACTS"; do
-            CONTRACT_NAME=$(echo ${CONTRACT} | xargs basename -a | cut -d'.' -f1)
+          while IFS= read -r CONTRACT; do
+            [ -z "$CONTRACT" ] && continue
+            CONTRACT_NAME="$(basename "$CONTRACT" .sol)"
             CONTRACT_PATH="src/dollar/core/${CONTRACT_NAME}.sol"
             SNAPSHOT_PATH="storage-layouts/${CONTRACT_NAME}.json"
             
             if [ -f "$SNAPSHOT_PATH" ]; then
               echo "Existing contract: ${CONTRACT_PATH}:${CONTRACT_NAME}"
               echo "${CONTRACT_PATH}:${CONTRACT_NAME}" >> contracts.txt
             else
               echo "New contract (skipping storage check): ${CONTRACT_PATH}:${CONTRACT_NAME}"
             fi
-          done
+          done <<< "$CHANGED_CONTRACTS"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for CONTRACT in "$CHANGED_CONTRACTS"; do
echo ${CONTRACT} | xargs basename -a | cut -d'.' -f1 | xargs -I{} echo src/dollar/core/{}.sol:{} >> contracts.txt
CONTRACT_NAME=$(echo ${CONTRACT} | xargs basename -a | cut -d'.' -f1)
CONTRACT_PATH="src/dollar/core/${CONTRACT_NAME}.sol"
SNAPSHOT_PATH="storage-layouts/${CONTRACT_NAME}.json"
if [ -f "$SNAPSHOT_PATH" ]; then
echo "Existing contract: ${CONTRACT_PATH}:${CONTRACT_NAME}"
echo "${CONTRACT_PATH}:${CONTRACT_NAME}" >> contracts.txt
else
echo "New contract (skipping storage check): ${CONTRACT_PATH}:${CONTRACT_NAME}"
fi
done
while IFS= read -r CONTRACT; do
[ -z "$CONTRACT" ] && continue
CONTRACT_NAME="$(basename "$CONTRACT" .sol)"
CONTRACT_PATH="src/dollar/core/${CONTRACT_NAME}.sol"
SNAPSHOT_PATH="storage-layouts/${CONTRACT_NAME}.json"
if [ -f "$SNAPSHOT_PATH" ]; then
echo "Existing contract: ${CONTRACT_PATH}:${CONTRACT_NAME}"
echo "${CONTRACT_PATH}:${CONTRACT_NAME}" >> contracts.txt
else
echo "New contract (skipping storage check): ${CONTRACT_PATH}:${CONTRACT_NAME}"
fi
done <<< "$CHANGED_CONTRACTS"

Comment on lines 42 to 53
for DIAMOND_LIB in "$CHANGED_LIBS"; do
echo ${DIAMOND_LIB} | xargs basename -a | cut -d'.' -f1 | xargs -I{} echo src/dollar/libraries/{}.sol:{} >> contracts.txt
LIB_NAME=$(echo ${DIAMOND_LIB} | xargs basename -a | cut -d'.' -f1)
LIB_PATH="src/dollar/libraries/${LIB_NAME}.sol"
SNAPSHOT_PATH="storage-layouts/${LIB_NAME}.json"

if [ -f "$SNAPSHOT_PATH" ]; then
echo "Existing library: ${LIB_PATH}:${LIB_NAME}"
echo "${LIB_PATH}:${LIB_NAME}" >> contracts.txt
else
echo "New library (skipping storage check): ${LIB_PATH}:${LIB_NAME}"
fi
done

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE=.github/workflows/diamond-storage-check.yml

echo "Check loop form and changed-files output wiring:"
rg -n 'CHANGED_LIBS|libraries_all_changed_files|for DIAMOND_LIB in "\$CHANGED_LIBS"|while IFS= read -r DIAMOND_LIB' "$FILE"

Repository: ubiquity/ubiquity-dollar

Length of output: 263


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=.github/workflows/diamond-storage-check.yml
echo "== Lines 1-120 =="
nl -ba "$FILE" | sed -n '1,120p'

echo
echo "== Locate changed-libraries step =="
rg -n 'id:\s*changed-libraries|changed-libraries|libraries_all_changed_files' "$FILE"

Repository: ubiquity/ubiquity-dollar

Length of output: 147


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE=.github/workflows/diamond-storage-check.yml

echo "== cat -n 1-160 =="
cat -n "$FILE" | sed -n '1,160p'

echo
echo "== Find step/outputs for changed-libraries =="
rg -n 'id:\s*changed-libraries|steps\.changed-libraries|libraries_all_changed_files|changed-libraries' "$FILE"

Repository: ubiquity/ubiquity-dollar

Length of output: 3673


🌐 Web query:

tj-actions/changed-files libraries_all_changed_files output delimiter format newline space

💡 Result:

In the tj-actions/changed-files GitHub Action, the output format for lists of changed files (such as all_changed_files) is controlled by the separator input [1][2]. The default delimiter for these outputs is a single space character (' ') [3][1]. You can customize this by providing a different string to the separator input in your workflow configuration [1][2]. For example, to output file paths separated by a newline, you would configure your step as follows [3][4]: - uses: tj-actions/changed-files@v45 with: separator: ' ' Note that when using shell commands to process these outputs, using a space-based delimiter can sometimes lead to issues if filenames contain spaces. In such cases, configuring the separator to a newline or another character that does not appear in your filenames is recommended [3][4]. Finally, please be aware that historical versions of this action were subject to a supply chain compromise involving secret exfiltration [5][6][7]. It is recommended to ensure you are using a secure, verified version of the action [6].

Citations:


Fix CHANGED_LIBS iteration in diamond-storage-check.yml (space-delimited list)

CHANGED_LIBS comes from tj-actions/changed-fileslibraries_all_changed_files output, which defaults to a single space separator. With for DIAMOND_LIB in "$CHANGED_LIBS"; do, the whole space-delimited list becomes one loop token, leading to incorrect/merged library handling and a broken/incomplete matrix.

Use either: (1) don’t quote the loop expansion (for DIAMOND_LIB in $CHANGED_LIBS; do ...) or (2) set separator on the changed-libraries step to newline and then iterate line-by-line.

Comment on lines +16 to +33
"Diamond": [
{
"extension": "AccessControlFacet",
"exclude": []
},
{
"extension": "ManagerFacet",
"exclude": []
},
{
"extension": "UbiquityPoolFacet",
"exclude": []
},
{
"extension": "StakingFacetHarness",
"exclude": []
}
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify exposed_* functions are only in the harness and that pool.conf extension list includes it.
rg -n -C2 'function\s+exposed_' packages/contracts/test/certora/UbiquityPoolFacetHarness.sol
rg -n -C3 '"contract_extensions"|"extension"' packages/contracts/test/certora/pool.conf

Repository: ubiquity/ubiquity-dollar

Length of output: 2137


Mount UbiquityPoolFacetHarness in contract_extensions.Diamond (not UbiquityPoolFacet).

pool.spec calls exposed_* selectors, which are implemented in test/certora/UbiquityPoolFacetHarness.sol; however packages/contracts/test/certora/pool.conf currently lists "extension": "UbiquityPoolFacet" under contract_extensions.Diamond, so those exposed_* methods won’t be present on the configured Diamond.

Suggested config change
             {
-                "extension": "UbiquityPoolFacet",
+                "extension": "UbiquityPoolFacetHarness",
                 "exclude": []
             },

Comment on lines +69 to +98
// Get current state
totalDollarSupply = dollarToken.totalSupply();
collateralBalanceUsd = exposed_collateralUsdBalance();
dollarPrice = getDollarPriceUsd();
collateralRatio = exposed_collateralRatio();

// If there are dollars minted and collateral exists, verify ratio is maintained
require totalDollarSupply > 0;
require collateralBalanceUsd > 0;
require isCollateralRatioSane(collateralRatio);

calldataarg args;
env e;

// Execute the method
f(e, args);

// After execution, re-read state
uint256 newTotalDollarSupply = dollarToken.totalSupply();
uint256 newCollateralBalanceUsd = exposed_collateralUsdBalance();

// The value of dollars in circulation should not exceed collateral value * ratio
// dollar_value = total_supply * price / 1e6
// collateral_value = collateral_balance * ratio / RATIO_PRECISION
// invariant: dollar_value <= collateral_value
mathint dollarValue = newTotalDollarSupply * dollarPrice / 1000000;
mathint maxAllowedDebt = newCollateralBalanceUsd * collateralRatio / RATIO_PRECISION;

assert dollarValue <= maxAllowedDebt,
"CRITICAL: Dollars minted exceed collateral backing";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the exact file and print the relevant section with line numbers
ls -la packages/contracts/test/certora/pool.spec

sed -n '1,140p' packages/contracts/test/certora/pool.spec | cat -n

# Also extract any nearby definitions that may affect the meaning of the snippet
# (expand search a bit around the section)
rg -n "exposed_collateralUsdBalance|getDollarPriceUsd|exposed_collateralRatio|isCollateralRatioSane|calldataarg args|env e|\\bf\\(e, args\\)" packages/contracts/test/certora/pool.spec

# Print further context around the solvency check to ensure no other gating/invariant exists
sed -n '140,260p' packages/contracts/test/certora/pool.spec | cat -n || true

Repository: ubiquity/ubiquity-dollar

Length of output: 12157


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show where these are defined/used in the spec directory
rg -n "function\s+getDollarPriceUsd|definition\s+getDollarPriceUsd|getDollarPriceUsd\s*\(" packages/contracts/test/certora -S

rg -n "exposed_collateralRatio\s*\(" packages/contracts/test/certora -S

rg -n "exposed_collateralUsdBalance\s*\(" packages/contracts/test/certora -S

# Also search the broader repo in case these helpers are defined elsewhere
rg -n "getDollarPriceUsd\s*\(" -S . --hidden --glob '!**/node_modules/**'
rg -n "exposed_collateralRatio\s*\(" -S . --hidden --glob '!**/node_modules/**'

Repository: ubiquity/ubiquity-dollar

Length of output: 4607


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the harness getters used by the certora spec
sed -n '1,120p' packages/contracts/test/certora/UbiquityPoolFacetHarness.sol | cat -n

# Show getDollarPriceUsd implementation and collateral ratio/base functions it relies on
sed -n '340,460p' packages/contracts/src/dollar/libraries/LibUbiquityPool.sol | cat -n
sed -n '520,740p' packages/contracts/src/dollar/libraries/LibUbiquityPool.sol | cat -n

# Also check the facet wrapper for getDollarPriceUsd
sed -n '1,130p' packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol | cat -n

Repository: ubiquity/ubiquity-dollar

Length of output: 23413


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Pull the definitions of collateralRatio() and collateralUsdBalance() from LibUbiquityPool.sol
rg -n "function\s+collateralRatio\s*\(|function\s+collateralUsdBalance\s*\(" packages/contracts/src/dollar/libraries/LibUbiquityPool.sol

# Print the surrounding implementations (small slices around the matches)
python3 - <<'PY'
import re, pathlib
path = pathlib.Path("packages/contracts/src/dollar/libraries/LibUbiquityPool.sol")
text = path.read_text().splitlines()
matches = []
for i,line in enumerate(text,1):
    if re.search(r"function\s+collateralRatio\s*\(", line):
        matches.append(i)
    if re.search(r"function\s+collateralUsdBalance\s*\(", line):
        matches.append(i)
print("matches:", matches)
PY

# Use sed windows around likely line numbers: print 200 lines around each match
# (Compute approximate line numbers by re-running rg with --context would be nicer, but keep output small)
rg -n "function\s+collateralRatio\s*\(" -n packages/contracts/src/dollar/libraries/LibUbiquityPool.sol
rg -n "function\s+collateralUsdBalance\s*\(" -n packages/contracts/src/dollar/libraries/LibUbiquityPool.sol

# We'll print broad but bounded ranges to capture both functions.
sed -n '300,430p' packages/contracts/src/dollar/libraries/LibUbiquityPool.sol | cat -n
sed -n '430,580p' packages/contracts/src/dollar/libraries/LibUbiquityPool.sol | cat -n

Repository: ubiquity/ubiquity-dollar

Length of output: 12595


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find any writes/assignments to collateralRatio in the contracts source
rg -n "collateralRatio\s*=" packages/contracts/src -S
rg -n "\.collateralRatio\s*[\+\-\*\/%]?=" packages/contracts/src -S
rg -n "collateralRatio\s*\(" packages/contracts/src/dollar -S

# Also check for any function named like "setCollateralRatio" or "updateCollateralRatio"
rg -n "setCollateralRatio|updateCollateralRatio|CollateralRatio" packages/contracts/src -S

# Find any writes to collateralPrices[] or unclaimedPoolCollateral that affect collateralUsdBalance
rg -n "collateralPrices\s*\[" packages/contracts/src -S
rg -n "unclaimedPoolCollateral\s*\[" packages/contracts/src -S
rg -n "missingDecimals\s*\[" packages/contracts/src -S

Repository: ubiquity/ubiquity-dollar

Length of output: 465


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Re-run the searches with simpler patterns
rg -n "collateralRatio\s*=" packages/contracts/src/dollar -S
rg -n "newCollateralRatio" packages/contracts/src/dollar -S

# Show the exact function around the assignment
sed -n '1020,1120p' packages/contracts/src/dollar/libraries/LibUbiquityPool.sol | cat -n

# Also find any access-control or update function names near there
rg -n "function\s+.*collateralRatio|update.*collateralRatio|set.*collateralRatio" packages/contracts/src/dollar/libraries/LibUbiquityPool.sol -S

# And scan for any other collateralRatio writes outside that file
rg -n "collateralRatio\s*=" packages/contracts/src/dollar -S

Repository: ubiquity/ubiquity-dollar

Length of output: 6338


Fix inv_noUnderCollateralization to use post-state price/ratio and avoid vacuous preconditions.

  • inv_noUnderCollateralization reads dollarPrice = getDollarPriceUsd() and collateralRatio = exposed_collateralRatio() before f(e, args) (lines 71-73) but uses them in the post-state solvency assertion with newTotalDollarSupply/newCollateralBalanceUsd (lines 94-95); recompute dollarPrice/collateralRatio after f, since the price feed inputs are modeled as nondeterministic and collateralRatio is mutable via setCollateralRatio.
  • The pre-gating require totalDollarSupply > 0; and require collateralBalanceUsd > 0; (lines 76-78) can skip the invariant exactly in the “first mint / from-empty collateral” transition where an under-collateralization would be most dangerous; remove/relax these preconditions so the solvency check always runs on the post-state.

Comment on lines +193 to +205
rule inv_stalePriceHandledGracefully(method f) {
env e;

// If price feeds return stale data, operations should still be safe
calldataarg args;
f(e, args);

// After any operation, collateral balance should be non-negative
uint256 collateralUsd = exposed_collateralUsdBalance();

assert collateralUsd >= 0,
"Collateral balance went negative";
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="packages/contracts/test/certora/pool.spec"
echo "== file: $file =="
# show around the reported lines
nl -ba "$file" | sed -n '160,240p'

Repository: ubiquity/ubiquity-dollar

Length of output: 183


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="packages/contracts/test/certora/pool.spec"
echo "== file: $file =="
# Prefer cat -n for line numbers
if command -v cat >/dev/null 2>&1; then
  cat -n "$file" | sed -n '160,240p'
else
  echo "cat not available"
fi

Repository: ubiquity/ubiquity-dollar

Length of output: 3089


inv_stalePriceHandledGracefully is tautological and doesn’t test stale-price safety.
In packages/contracts/test/certora/pool.spec (193–205), collateralUsd is a uint256, so assert collateralUsd >= 0 can never fail. As written, the rule provides no validation that stale price feeds are handled safely.

Comment on lines +213 to +258
rule inv_enabledCollateralAlwaysPriced(method f) {
env e;
calldataarg args;
f(e, args);

// Check each enabled collateral has a valid price
address[] collaterals = exposed_allCollaterals();

// For each enabled collateral, price should always be > 0
// (This requires checking each one individually which Certora handles via loop)
}

//========
// Invariant 7: Fee bounds
//
// Minting and redemption fees must always be within acceptable bounds (0-100%)
//========

rule inv_feesAlwaysWithinBounds(method f) {
address[] collaterals = exposed_allCollaterals();

env e;
calldataarg args;
f(e, args);

// After any operation, check that all collateral fees are within bounds
// 0 <= fee <= 1_000_000 (where 1_000_000 = 100%)
// This is verified by checking collateralInformation for each collateral
}

//========
// Invariant 8: Pause state consistency
//
// Pausing the contract should properly suspend all minting and redemption
//========

rule inv_pauseStateConsistency(method f) {
env e;

// Check pause-related invariants
calldataarg args;
f(e, args);

// After any operation, we should be able to read pause state
// without revert (pause getter doesn't revert)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="packages/contracts/test/certora/pool.spec"
echo "== File exists? =="
ls -l "$FILE"
echo "== Lines 180-290 =="
nl -ba "$FILE" | sed -n '180,290p'

Repository: ubiquity/ubiquity-dollar

Length of output: 260


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="packages/contracts/test/certora/pool.spec"
echo "== File exists? =="
ls -l "$FILE"
echo "== Lines 180-290 =="
nl -ba "$FILE" | sed -n '180,290p'

Repository: ubiquity/ubiquity-dollar

Length of output: 260


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="packages/contracts/test/certora/pool.spec"

echo "== Snippet $FILE lines 205-275 =="
awk 'NR>=205 && NR<=275 {printf "%d:%s\n", NR, $0}' "$FILE"

echo
echo "== Find rule declarations =="
rg -n "rule inv_(enabledCollateralAlwaysPriced|feesAlwaysWithinBounds|pauseStateConsistency)\b" "$FILE" || true

echo
echo "== Print full rule bodies (best-effort) =="
# Print from each rule line until the next "rule " line.
for name in inv_enabledCollateralAlwaysPriced inv_feesAlwaysWithinBounds inv_pauseStateConsistency; do
  echo "--- $name ---"
  start=$(rg -n "rule $name\\b" "$FILE" | head -n1 | cut -d: -f1)
  if [[ -z "${start:-}" ]]; then
    echo "not found"
    continue
  fi
  awk -v s="$start" '
    NR>=s {
      if (NR>s && $0 ~ /^rule /) { exit }
      print
    }
  ' "$FILE"
done

Repository: ubiquity/ubiquity-dollar

Length of output: 4125


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="packages/contracts/test/certora/pool.spec"

# show any assert-like tokens within each rule region (lightweight checks)
for start in 213 231 249; do
  awk -v s="$start" '
    NR>=s {
      if (NR>s && $0 ~ /^rule /) exit
      print
    }
  ' "$FILE" | rg -n "assert|check|invariant|require\b|guarantee|ensures|forall|exists" || true
done

Repository: ubiquity/ubiquity-dollar

Length of output: 356


Fix Certora invariants: these rules currently don’t assert anything

  • In packages/contracts/test/certora/pool.spec (lines 213-258), inv_enabledCollateralAlwaysPriced, inv_feesAlwaysWithinBounds, and inv_pauseStateConsistency only call f(e, args) and read helper state—there are no Certora assert/check/guarantee-style properties—so they provide no formal guarantees.

Comment on lines +285 to +289
// If total supply increased (minting occurred), verify collateral still covers it
require totalSupplyAfter >= totalSupplyBefore;
require collateralAfter >= collateralBefore;
require ratioAfter >= ratioBefore; // ratio should not decrease

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/contracts/test/certora/pool.spec"

echo "== Lines around 260-320 =="
nl -ba "$FILE" | sed -n '250,330p'

echo
echo "== Search for minting / mint occurrences in this spec =="
rg -n "mint|minting|totalSupplyAfter|collateralAfter|ratioAfter|collateralBefore|ratioBefore|totalSupplyBefore" "$FILE"

Repository: ubiquity/ubiquity-dollar

Length of output: 156


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/contracts/test/certora/pool.spec"

echo "== Lines 250-330 (with line numbers) =="
python3 - <<'PY'
import itertools
path = "packages/contracts/test/certora/pool.spec"
start, end = 250, 330
with open(path, "r", encoding="utf-8") as f:
    for i, line in enumerate(f, start=1):
        if start <= i <= end:
            print(f"{i:4d}\t{line.rstrip()}")
        elif i > end:
            break
PY

echo
echo "== Search for minting / mint occurrences in this spec =="
rg -n "mint|minting" "$FILE" || true

echo
echo "== Search for key variables around the snippet =="
rg -n "totalSupplyAfter|totalSupplyBefore|collateralAfter|collateralBefore|ratioAfter|ratioBefore" "$FILE" || true

Repository: ubiquity/ubiquity-dollar

Length of output: 4671


🌐 Web query:

Certora Prover rule semantics require vs assert failing require skips execution treated as precondition

💡 Result:

In the Certora Prover, require and assert statements serve distinct roles in defining and verifying rules [1]. Require statements act as preconditions [2][1]. If a require statement evaluates to false for a specific execution trace, that entire trace is ignored and excluded from consideration by the Prover [3][1]. Consequently, execution does not "fail" or trigger a violation in the presence of a failing require; rather, the Prover simply filters out those cases and proceeds to evaluate remaining paths [3][1]. Assert statements define the expected behavior of the contract [2][1]. If an assert statement evaluates to false on any execution path that satisfies all preceding require statements, the Prover reports a violation and provides a counterexample [3][1]. The behavior where failing requirements are treated as preconditions that prune the search space is intentional [3][1]. This allows users to restrict verification to relevant, feasible, or intended scenarios [4][1]. However, this mechanism introduces the risk of vacuity, where a rule may pass simply because all execution paths were filtered out by overly aggressive require statements [5][6]. To mitigate this, the Certora Prover includes tools to detect such cases, such as vacuity checks (e.g., automatically appending assert false to rules to ensure they are reachable) [5].

Citations:


Fix vacuity in inv_mintMaintainsCollateralRatio by removing mint-time monotonicity requires
require failures are treated as preconditions that prune traces; in packages/contracts/test/certora/pool.spec lines 286-288, if a mint operation decreases collateralAfter or ratioAfter, the rule is excluded before reaching assert debtAfter <= backingAfter, hiding the violation. Gate on actual mint occurrence (e.g., totalSupplyAfter > totalSupplyBefore) and then assert backing safety unconditionally for those traces.

Comment on lines +303 to +316
rule inv_redeemReducesDebtCorrectly(method f) {
env e;

uint256 totalSupplyBefore = dollarToken.totalSupply();

calldataarg args;
f(e, args);

uint256 totalSupplyAfter = dollarToken.totalSupply();

// After redeem operations, total supply should not increase
// (redeeming burns dollars, doesn't mint)
assert totalSupplyAfter <= totalSupplyBefore,
"Dollar supply increased during what should be a burn operation";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the certora spec and show the relevant rule plus surrounding context
FILE="packages/contracts/test/certora/pool.spec"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE" >&2
  exit 1
fi

# Show around the asserted lines
python3 - <<'PY'
import itertools
path="packages/contracts/test/certora/pool.spec"
start=270
end=360
with open(path,'r',encoding='utf-8') as f:
    for i,line in enumerate(f,1):
        if start<=i<=end:
            print(f"{i:4d}: {line.rstrip()}")
PY

# Also search for the rule name and any other inv_redeem... rules
rg -n "inv_redeemReducesDebtCorrectly|redeemReducesDebtCorrectly" "$FILE" || true

# Search within certora specs for any filter/helper macros like "redeem" method predicates
rg -n "redeem|mint|burn|Dollar|dollarToken" "$FILE" | head -n 80

# Search for other rules using "method f" / calldataarg patterns to see how they filter methods
rg -n "rule .*method f" "$FILE" || true

Repository: ubiquity/ubiquity-dollar

Length of output: 5017


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/contracts/test/certora/pool.spec"

# Header/context (includes DISPATCHER and any method set definitions)
python3 - <<'PY'
path="packages/contracts/test/certora/pool.spec"
with open(path,'r',encoding='utf-8') as f:
    for i,line in enumerate(f,1):
        if i<=120:
            print(f"{i:4d}: {line.rstrip()}")
        else:
            break
PY

echo "---- filtered blocks ----"
rg -n "filtered\s*\{[^\}]*\}" "$FILE" || true

echo "---- any redeem-specific predicates ----"
rg -n "redeem|burn|mint" "$FILE" | head -n 200

Repository: ubiquity/ubiquity-dollar

Length of output: 6742


Restrict inv_redeemReducesDebtCorrectly to redeem/burn methods.

inv_redeemReducesDebtCorrectly(method f) runs f(e, args) over the whole methods { ... } set (which includes mint), yet it asserts dollarToken.totalSupply() never increases—this can fail on mint paths. Add a filtered { ... }/method-set restriction so the rule only applies to the redeem/burn entrypoints.

Comment on lines +15 to +16
* @return status 1 = entered, 2 = not entered
*/

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix reentrancy status value mapping in docs.

Line 15 documents the status values opposite to what pool.spec assumes (not entered = 1). Please align the mapping to prevent inverted invariant assumptions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Formal verification

1 participant