Skip to content

Smart Contract Testing with Foundry

RunTimeAdmin edited this page Oct 30, 2026 · 19 revisions

Smart Contract Testing with Foundry

**Referenced Files in This Document** - [foundry.toml](https://github.com/RunTimeAdmin/Sigvara/blob/main/foundry.toml) - [RegistrationHelper.sol](https://github.com/RunTimeAdmin/Sigvara/blob/main/test/helpers/RegistrationHelper.sol) - [E2E.t.sol](https://github.com/RunTimeAdmin/Sigvara/blob/main/test/E2E.t.sol) - [StakingInvariant.t.sol](https://github.com/RunTimeAdmin/Sigvara/blob/main/test/StakingInvariant.t.sol) - [SigvaraStaking.t.sol](https://github.com/RunTimeAdmin/Sigvara/blob/main/test/SigvaraStaking.t.sol) - [SigvaraIdentity.t.sol](https://github.com/RunTimeAdmin/Sigvara/blob/main/test/SigvaraIdentity.t.sol) - [FeeOnTransfer.t.sol](https://github.com/RunTimeAdmin/Sigvara/blob/main/test/FeeOnTransfer.t.sol) - [DisputeFreeze.t.sol](https://github.com/RunTimeAdmin/Sigvara/blob/main/test/DisputeFreeze.t.sol) - [SecurityFixes.t.sol](https://github.com/RunTimeAdmin/Sigvara/blob/main/test/SecurityFixes.t.sol)

Table of Contents

  1. Introduction
  2. Project Structure
  3. Core Components
  4. Architecture Overview
  5. Detailed Component Analysis
  6. Dependency Analysis
  7. Performance Considerations
  8. Troubleshooting Guide
  9. Conclusion
  10. Appendices

Introduction

This document explains Sigvara’s smart contract testing framework built on Foundry. It covers how tests are organized, how to write unit, integration, and end-to-end tests, and how the suite validates state transitions, events, error conditions, fuzzing, invariants, and gas-sensitive behavior. It also documents the RegistrationHelper utility and common test helpers for agent registration, stake management, time control, role setup, and mock token usage.

The repository uses:

  • Forge-std for EVM utilities, mocking, and assertions.
  • OpenZeppelin contracts for proxies, access control, and ERC-20 tokens.
  • A consistent pattern of deploying proxy implementations and wiring cross-contract roles in setUp.
  • Fuzzing and invariant testing for protocol stability.
  • Regression suites that pin security fixes and edge cases.

Project Structure

The Solidity test suite lives under test/, with shared helpers under test/helpers/. The main configuration is in foundry.toml, which defines source/output directories, remappings, optimizer settings, the EVM version (cancun), fuzz runs, RPC endpoints, and filesystem permissions.

As of 29 October 2026 the suite has 699 tests across 30 files, and each of the 19 invariants counts once. Without a fork URL, 692 of them run and 7 skip. Beside the files in the diagram below, other suites cover the consumer reads (SigvaraReader.t.sol, GatedMarketplace.t.sol), the guardian pause (InflowGuard.t.sol), timelocked role grants (TimelockedGrants.t.sol, UpgraderRoleAdmin.t.sol), operator custody (OperatorCustody.t.sol), the move of admin and upgrade rights to a timelock and Safe (SplitAuthority.t.sol, with SplitAuthorityFork.t.sol rehearsing it on a fork of the testnet), the deploy scripts (DeployScripts.t.sol), reentrancy guards (Reentrancy.t.sol), smart-wallet agents (SmartWalletAgent.t.sol), a slash rehearsal against the live testnet deployment (SlashDrillFork.t.sol), and the operator bond registry's invariants (OracleBondInvariant.t.sol). SigvaraReader.t.sol, GatedMarketplace.t.sol, InflowGuard.t.sol, OperatorCustody.t.sol and SmartWalletAgent.t.sol share test/helpers/StackFixture.sol, which deploys the three registries behind proxies, a funded operator and a SigvaraReader. SlashDrillFork.t.sol forks Arc testnet on every run, so it needs the arc_testnet endpoint in foundry.toml to answer, and SplitAuthorityFork.t.sol skips its seven tests unless you pass --fork-url https://rpc.testnet.arc.io.

graph TB
Root["Repository Root"] --> Foundry["foundry.toml"]
Root --> Src["src/*.sol"]
Root --> Test["test/*.t.sol"]
Test --> Helpers["test/helpers/RegistrationHelper.sol"]
Test --> UnitTests["Unit Tests<br/>SigvaraStaking.t.sol<br/>SigvaraIdentity.t.sol"]
Test --> IntegrationTests["Integration / E2E<br/>E2E.t.sol"]
Test --> InvariantTests["Invariant Tests<br/>StakingInvariant.t.sol"]
Test --> EdgeCases["Edge Cases<br/>FeeOnTransfer.t.sol<br/>DisputeFreeze.t.sol<br/>SecurityFixes.t.sol<br/>ReporterAuthority.t.sol<br/>UnbondingSnapshot.t.sol<br/>RejectedEvidence.t.sol"]
Loading

Diagram sources

Section sources

Core Components

  • RegistrationHelper: Shared helper for signing and registering agents across tests.
  • Mock SVR Token: Minimal ERC-20 used by most tests to represent the bond/stake token.
  • Proxy Setup Pattern: Tests deploy upgradeable implementations behind ERC1967 proxies and wire roles between Identity, Reputation, Staking, and other contracts.
  • Time Control: Tests use vm.warp to advance time through challenge windows, unbonding periods, and dispute resolution windows.
  • Role Management: Admin grants roles such as STAKING_CORE_ROLE, ORACLE_ROLE, and SLASHING_COMMITTEE_ROLE during setup. That address can do it because initialize also gives it UPGRADER_ROLE: on main the staking-core, committee and slasher roles are administered by UPGRADER_ROLE, which the timelock holds on a real deployment (TimelockedGrants.t.sol and UpgraderRoleAdmin.t.sol pin this).

Key responsibilities:

  • RegistrationHelper abstracts signature creation and registration calls so tests focus on business logic.
  • E2E.t.sol exercises the full lifecycle: register → stake → propose/finalize reputation → slash → zero-reputation, plus disputes, unbonding, and multi-agent scenarios.
  • StakingInvariant.t.sol drives randomized call sequences via a handler and asserts global invariants like custody conservation and collateralization.
  • FeeOnTransfer.t.sol ensures accounting correctness when the token charges a transfer fee.
  • DisputeFreeze.t.sol and SecurityFixes.t.sol pin regression paths for critical fixes.
  • ReporterAuthority.t.sol, UnbondingSnapshot.t.sol and RejectedEvidence.t.sol cover three staking changes: a slash is executed only while its reporter is on the committee, a withdrawal's claim time is fixed when it is queued, and evidence the admin rejected is refused for that agent. Each pins its behaviour with tests that fail when the change is removed.

Section sources

Architecture Overview

The test architecture mirrors the production system:

  • Identity manages agent identities and status.
  • Reputation proposes and finalizes scores with a challenge window.
  • Staking holds bonds, enforces slashing, and manages withdrawals.
  • Optional contracts (OracleBond, EpochFees) appear in specific test suites.
graph TB
TestSuite["Test Suite"] --> Helper["RegistrationHelper"]
TestSuite --> MockToken["Mock SVR Token"]
TestSuite --> Identity["SigvaraIdentity"]
TestSuite --> Reputation["SigvaraReputation"]
TestSuite --> Staking["SigvaraStaking"]
Identity --> Staking
Reputation --> Staking
Staking --> MockToken
Loading

Diagram sources

Detailed Component Analysis

RegistrationHelper Utility

RegistrationHelper centralizes agent registration and signature generation. It:

  • Derives the agent address from a private key.
  • Computes the registration digest using the identity contract.
  • Signs the digest with the agent’s private key using forge-std’s vm.sign.
  • Wraps the signature into the format expected by the identity contract.
  • Registers the agent under an operator via registerAgent.

This abstraction removes repetitive signing ceremony code from individual tests and makes tampering with signatures or call parameters straightforward for negative tests.

flowchart TD
Start(["registerSigned"]) --> Addr["Derive agent address from private key"]
Addr --> Digest["Compute registrationDigest(...)"]
Digest --> Sign["Sign digest with vm.sign(...)"]
Sign --> Prank["Prank operator"]
Prank --> Call["Call identity.registerAgent(agentAddress, ed25519PubKey, sig)"]
Call --> Return["Return didHash"]
Loading

Diagram sources

Section sources

End-to-End Workflow Tests

E2E.t.sol demonstrates the complete agent lifecycle and several advanced flows:

  • Registration leaves the agent in PendingBond; depositing stake activates it.
  • Oracle proposes reputation; committee can reject within the challenge window.
  • Slash initiation suspends the agent; after the challenge period, execution burns half and credits victim/reporter.
  • Disputes freeze the bond until resolved or expired.
  • Unbonding prevents early withdrawal; queued funds remain slashable.
  • Multiple epochs evolve reputation scores over time.
  • Multiple agents operate independently.
sequenceDiagram
participant T as "Test"
participant H as "RegistrationHelper"
participant I as "SigvaraIdentity"
participant S as "SigvaraStaking"
participant R as "SigvaraReputation"
participant M as "MockSVR"
T->>H : registerSigned(identity, operator, agentPk, pubKey)
H->>I : registerAgent(agentAddress, pubKey, sig)
T->>S : depositStake(didHash, amount)
T->>R : proposeReputation(didHash, scoreData)
T->>R : finalizeReputation(didHash) after challenge window
T->>S : initiateSlash(didHash, victim, evidence)
T->>S : executeSlash(didHash) after challenge window
T->>S : claimSlashProceeds() for victim and committee
T->>I : verify status Slashed and reputation zeroed
Loading

Diagram sources

Section sources

Invariant Testing Strategy

StakingInvariant.t.sol defines a handler that randomly drives staking operations and tracks totals for deposits, claims, slashed-out amounts, and proceeds claimed. It then asserts invariants:

  • Custody matches recorded obligations: stake + queued withdrawals + unclaimed proceeds equals contract balance.
  • Token conservation: total deposited equals held + claimed + burned + proceeds claimed.
  • Slashed agents are fully zeroed: no stake or queued withdrawal remains.
  • Burned-only grows: burn address never decreases.
  • Disputed agents stay bonded: disputed proposals cannot release stake.
  • Active agents are collateralized: active agents must hold at least minimum stake including queued withdrawals.

The handler rules on disputes as the admin, and it also drives expireSlash, so a proposal nobody executes or cancels can lapse in the fuzzed state space. OracleBondInvariant.t.sol is a second campaign, over the operator bond registry, with five more invariants: signer mappings stay symmetric, a signer is never also an operator, an address has standing only through a bonded Active operator, nobody is left signing for nobody, and the active count and custody still add up. Together the two campaigns hold the 11 invariants.

classDiagram
class StakingHandler {
+deposit(seed, amount)
+initiateWithdrawal(seed, amount)
+claimWithdrawal(seed)
+setStatus(seed, active)
+initiateSlash(seed)
+disputeSlash(seed)
+executeSlash(seed)
+resolveDispute(seed, uphold)
+expireDispute(seed)
+expireSlash(seed)
+cancelSlash(seed)
+claimProceeds(seed)
+warp(secondsForward)
+seedBonds(amount)
}
class StakingInvariantTest {
+invariant_custodyMatchesRecordedObligations()
+invariant_tokenConservation()
+invariant_slashedAgentsFullyZeroed()
+invariant_burnedOnlyGrows()
+invariant_disputedAgentsStayBonded()
+invariant_activeAgentsAreCollateralised()
}
StakingInvariantTest --> StakingHandler : "drives random actions"
Loading

Diagram sources

Section sources

Unit Tests for Individual Functions

SigvaraStaking.t.sol and SigvaraIdentity.t.sol provide granular coverage:

  • Deposit accumulation, activation thresholds, and reactivation guards.
  • Operator checks and unregistered agent handling.
  • Status updates preventing unsafe reactivation without collateral.
  • Operator transfer mechanics and index maintenance.
  • Event emission verification and error selector checks.

These tests isolate single functions while still wiring up proxies and roles to reflect realistic interactions.

Section sources

Fee-on-Transfer Accounting Tests

FeeOnTransfer.t.sol introduces a token that charges a transfer fee and verifies:

  • Deposits credit what actually arrives, not what was requested.
  • Booked stake never exceeds contract balance.
  • Agents do not activate on tokens they never received.
  • Events report credited amounts.
  • Exact-transfer tokens remain unaffected.
  • Mixed deposits with changing fees net correctly.

It also includes sibling tests for OracleBond and EpochFees to ensure consistent accounting across contracts.

Section sources

Dispute Freeze and Security Fixes

DisputeFreeze.t.sol and SecurityFixes.t.sol pin regression paths:

  • Disputes keep bonds frozen until resolved or expired.
  • Re-initiating slashes over open disputes is blocked.
  • Resolving disputes either slashes or reinstates the agent.
  • Expired disputes release the bond.
  • Cancelled slashes clear abandoned proposals.
  • Security fixes include slash guard validations, period floors, deposit validation for unregistered DIDs, zero-bond clearing, and faucet cooldown behavior.
  • Three later files each pin one staking change. ReporterAuthority.t.sol: a revoked reporter's proposal cannot be executed, yet a current member can cancel it and anyone can lapse it. UnbondingSnapshot.t.sol: lowering or raising unbondingPeriod leaves a queued exit where it was, with a fuzz test over any new period and any wait. RejectedEvidence.t.sol: a veto or a ruling for the agent blocks the same evidence, while a committee cancel, a lapse and an expired dispute do not.

Section sources

Dependency Analysis

The test suite depends on:

  • Forge-std for VM controls (prank, warp, sign, addr) and assertions.
  • OpenZeppelin for ERC1967 proxies, ERC-20 tokens, and access control interfaces.
  • Production contracts under src/ for Identity, Reputation, Staking, OracleBond, EpochFees, and SVRToken.
graph LR
ForgeStd["forge-std/Test.sol"] --> Tests["All *.t.sol"]
OZProxies["@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"] --> Tests
OZERC20["@openzeppelin/contracts/token/ERC20/ERC20.sol"] --> Tests
OZAccess["@openzeppelin/contracts/access/IAccessControl.sol"] --> Tests
SrcContracts["src/*.sol"] --> Tests
Helpers["test/helpers/RegistrationHelper.sol"] --> Tests
Loading

Diagram sources

Section sources

Performance Considerations

  • Optimizer enabled with 200 runs in default profile.
  • Fuzz runs are 1,000 in the default profile and are increased to 5000 iterations in the CI profile for stronger coverage.
  • Gas-sensitive behavior is validated indirectly through event emissions and accounting constraints (e.g., fee-on-transfer tests).
  • Invariant tests stress complex state transitions to expose hidden inefficiencies or vulnerabilities.

Recommendations:

  • Use forge test --match-contract <Contract> to target slow suites.
  • Keep fuzz runs reasonable locally and rely on CI for heavier runs.
  • Prefer delta-based accounting and bounded inputs in fuzz handlers.

Section sources

Troubleshooting Guide

Common issues and debugging techniques:

  • Signature mismatches: Ensure the digest prefix matches wallet expectations; RegistrationHelper applies EIP-191 prefix before signing.
  • Role errors: Verify admin pranks and role grants in setUp; many tests require STAKING_CORE_ROLE, ORACLE_ROLE, or SLASHING_COMMITTEE_ROLE.
  • Time-related failures: Use vm.warp to advance past challenge windows, unbonding periods, and dispute resolution windows.
  • Event assertion failures: Use vm.expectEmit with correct selectors and data.
  • Error selector checks: Assert specific revert selectors rather than generic reverts for precise failure paths.
  • Fork and deployment scripts: Use provided RPC endpoints and profiles; avoid adding etherscan entries for unsupported chain IDs.

Foundry-specific tips:

  • Use -vvv for verbose output in E2E tests.
  • Leverage makeAddr and makeAddrAndKey for deterministic actors.
  • Inspect storage slots with vm.load for index integrity checks.
  • Use targetContract in invariant tests to constrain fuzzer input space.

Section sources

Conclusion

Sigvara’s Foundry test suite provides comprehensive coverage across unit, integration, end-to-end, fuzzing, and invariant testing. The RegistrationHelper utility simplifies agent registration and signature handling, while consistent proxy setup and role wiring mirror production behavior. The suite rigorously validates state transitions, event emissions, error conditions, fee-on-transfer accounting, dispute freezes, and security fixes. Invariant testing ensures protocol stability by asserting global properties under randomized action sequences. Together, these practices form a robust foundation for maintaining correctness and security in Sigvara’s smart contracts.

Appendices

Running Tests

  • Fetch the Solidity dependencies first: sh scripts/fetch-deps.sh (OpenZeppelin and forge-std, each fetched by commit into lib/, which is gitignored; there are no submodules)
  • All contract tests: forge test
  • CI fuzz runs: FOUNDRY_PROFILE=ci forge test
  • Invariant tests: forge test --match-contract StakingInvariantTest
  • Operator bond invariants: forge test --match-contract OracleBondInvariantTest
  • E2E integration tests: forge test --match-contract E2EIntegrationTest -vvv
  • Fork suites: forge test --match-contract SlashDrillFork -vv forks the testnet, and forge test --match-path test/SplitAuthorityFork.t.sol --fork-url https://rpc.testnet.arc.io -vv runs the seven tests that are skipped without a fork URL

Section sources

Overview

Clone this wiki locally