DeFi Protocolsintermediatetutorial20 min read

DeFi Testing Guides: Complete Tutorial for Smart Contract Developers | VCC

DeFi testing combines unit testing, fork testing, invariant testing, and fuzzing to verify smart contract behavior under adversarial conditions. Essential tools: Foundry (cheatcodes, invariant runner), Hardhat (TypeScript, fork mainnet), and specialized frameworks like Echidna for property-based tes

Vietcardano teamTeam

Đội ngũ biên tập VietCardanoCommunity — các chuyên gia blockchain và Cardano, cung cấp nội dung giáo dục và nghiên cứu chuyên sâu cho cộng đồng.

Published September 14, 2026

DeFi testing combines unit testing, fork testing, invariant testing, and fuzzing to verify smart contract behavior under adversarial conditions. Essential tools: Foundry (cheatcodes, invariant runner), Hardhat (TypeScript, fork mainnet), and specialized frameworks like Echidna for property-based testing. Test oracle manipulation, flash loans, reentrancy, and economic invariants before mainnet deployment.

DeFi protocols secure billions in TVL, yet testing remains the most overlooked phase of development. Unlike traditional software, DeFi bugs are irreversible and instantly exploitable. This guide covers the complete testing stack: from unit tests for individual functions to invariant testing that mathematically proves protocol properties hold under all conditions. We'll use Foundry as the primary tool (fastest, best cheatcodes) with Hardhat alternatives, and show real attack vectors from major DeFi hacks (Euler, Mango, Cream) as test cases.

Diagram - DeFi Testing Stack Architecture

Diagram - DeFi Testing Stack Architecture

1. DeFi Testing Fundamentals

Why DeFi testing differs from Web2 testing comes down to three properties: immutability (no hotfixes), composability (your contract interacts with unknown protocols), and economic attacks (profit-driven adversaries). A single oversight — a missing reentrancy guard, an unchecked oracle price, a rounding error in share accounting — can drain the entire protocol.

The testing pyramid for DeFi has five layers, each catching what the previous misses:

  1. Unit tests — individual functions, pure logic, math operations
  2. Integration tests — multi-contract flows, protocol interactions
  3. Fork tests — mainnet state replay, adversarial scenarios, historical exploits
  4. Invariant tests — mathematical properties that must ALWAYS hold
  5. Fuzzing — randomized input exploration for edge cases
Flowchart - Unit Test Coverage for DeFi Primitives

Flowchart - Unit Test Coverage for DeFi Primitives

ToolBest ForCheatcodesInvariant RunnerFuzzingLearning Curve
FoundrySpeed, DeFi-native, fork/invariant/fuzz all-in-one✅ Excellent✅ Built-in✅ Built-inLow (Solidity)
HardhatTypeScript teams, plugin ecosystem❌ Manual❌ External❌ ExternalMedium (JS/TS)
EchidnaProperty-based, stateful fuzzingN/AN/A✅ SpecializedHigh (Haskell-like)
MedusaHigh-throughput fuzzing, CI/CDN/AN/A✅ FastestHigh
SlitherStatic analysis, vulnerability detectionN/AN/AN/ALow (CLI)

Featured Snippet target: "What is the best tool for DeFi testing?" → Foundry for speed/cheatcodes, Echidna for property-based testing.

2. Setting Up Your Testing Environment

Foundry Installation and Project Structure

`bash

curl -L https://foundry.paradigm.xyz | bash foundryup

forge init defi-testing-demo cd defi-testing-demo

`

Hardhat Alternative Setup

`bash npm init -y npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox npx hardhat init `

Configuring Fork Testing

Fork testing requires an RPC endpoint with archive node access. Recommended providers:

  • Alchemy — generous free tier, Ethereum mainnet + L2s
  • QuickNode — multi-chain, good for cross-chain testing
  • Chainstack — reliable archive access

Configure in foundry.toml:

`toml [profile.ci] fork_url = "https://eth-mainnet.alchemyapi.io/v2/YOUR_KEY" fork_block_number = 19_500_000 # Pin to specific block for reproducibility

[fuzz] runs = 10000 max_test_rejects = 10000

[invariant] runs = 1000 depth = 50 fail_on_revert = false `

CI/CD Pipeline

GitHub Actions workflow (.github/workflows/test.yml):

`yaml name: DeFi Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps:

  • uses: actions/checkout@v4
  • uses: foundry-rs/foundry-toolchain@v1

with: version: nightly

  • run: forge build
  • run: forge test -vvv
  • run: forge test --invariant --invariant-runs 1000
  • run: forge coverage --report lcov
  • uses: codecov/codecov-action@v3

`

Diagram - DeFi Testing Stack Architecture

Diagram - DeFi Testing Stack Architecture

3. Unit Testing DeFi Primitives

Testing ERC20/ERC721 Compliance

Use OpenZeppelin test helpers for standard compliance:

`solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20;

import "forge-std/Test.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; import "@openzeppelin/contracts/test/helpers/ERC20TestHelpers.sol";

contract MyTokenTest is Test, ERC20TestHelpers { MyToken token; address alice = makeAddr("alice"); address bob = makeAddr("bob");

function setUp() public { token = new MyToken("Test", "TST"); }

function testTransfer() public { token.mint(alice, 1000 ether); testTransferFrom(alice, bob, 100 ether); }

function testPermit() public { token.mint(alice, 1000 ether); testPermit(alice, address(this), 100 ether, 0, 0, 0); } } `

AMM Math: Constant Product Formula

The core invariant: x * y = k (reserves product constant, minus fees).

`solidity contract UniswapV2MathTest is Test { function testConstantProduct() public { uint256 reserveA = 1000 ether; uint256 reserveB = 2000 ether; uint256 k = reserveA * reserveB;

// Swap 100 A for B uint256 amountIn = 100 ether; uint256 amountOut = (amountIn * reserveB) / (reserveA + amountIn);

// Verify invariant holds (with 0.3% fee) uint256 newReserveA = reserveA + amountIn; uint256 newReserveB = reserveB - amountOut; uint256 fee = (amountIn 3) / 1000; uint256 expectedK = (reserveA reserveB) + (fee * reserveB);

assertApproxEqAbs(newReserveA * newReserveB, expectedK, 1); }

function testSwapRounding() public { // Edge case: tiny swaps should not drain reserves uint256 reserveA = 1 ether; uint256 reserveB = 1 ether; uint256 amountOut = (1 wei * reserveB) / (reserveA + 1 wei); assertEq(amountOut, 0); // Too small, correctly returns 0 } } `

Lending Protocol: Collateral Factor & Liquidation

`solidity contract LendingMathTest is Test { // Collateral factor: 75% = borrow up to 75% of collateral value // Liquidation threshold: 80% = liquidate when debt > 80% of collateral

function testMaxBorrow() public { uint256 collateralValue = 1000 ether; // $1000 worth of ETH uint256 collateralFactor = 7500; // 75% in basis points uint256 maxBorrow = (collateralValue * collateralFactor) / 10000; assertEq(maxBorrow, 750 ether); }

function testLiquidationTrigger() public { uint256 collateralValue = 1000 ether; uint256 debt = 850 ether; // 85% — above 80% threshold uint256 liquidationThreshold = 8000; // 80%

uint256 healthFactor = (collateralValue liquidationThreshold) / (debt 10000); assertLt(healthFactor, 10000); // Health factor < 1 = liquidatable } } `

Vault/Share Accounting: Deposit/Withdraw Rounding

`solidity contract VaultAccountingTest is Test { Vault vault; ERC20Mock asset;

function setUp() public { asset = new ERC20Mock("Asset", "AST"); vault = new Vault(asset); asset.mint(address(this), 10000 ether); }

function testDepositWithdrawRoundTrip() public { uint256 depositAmount = 1000 ether; asset.approve(address(vault), depositAmount);

uint256 sharesBefore = vault.totalSupply(); uint256 assetsBefore = asset.balanceOf(address(vault));

vault.deposit(depositAmount, address(this));

uint256 sharesAfter = vault.totalSupply(); uint256 assetsAfter = asset.balanceOf(address(vault));

// Shares received should match assets deposited (first depositor gets 1:1) assertEq(sharesAfter - sharesBefore, depositAmount); assertEq(assetsAfter - assetsBefore, depositAmount); }

function testSharePriceManipulation() public { // Attack: donate assets directly to vault to inflate share price asset.mint(address(vault), 1000 ether); // Donation

// Victim deposits — gets fewer shares due to inflated price asset.mint(alice, 100 ether); asset.approve(address(vault), 100 ether, alice); vm.prank(alice); vault.deposit(100 ether, alice);

// Alice gets fewer shares than she should uint256 aliceShares = vault.balanceOf(alice); assertLt(aliceShares, 100 ether); // Rounding loss! } } `

Run with: forge test --match-contract VaultAccountingTest -vv

4. Fork Testing: Mainnet Replay & Adversarial Scenarios

Sequence - Fork Test Execution Flow

Sequence - Fork Test Execution Flow

Fork testing lets you replay mainnet state at any block and simulate transactions against it. Foundry's cheatcodes make this powerful:

`solidity contract ForkTest is Test { function setUp() public { // Create fork at block 19_500_000 (pre-exploit) vm.createFork("https://eth-mainnet.alchemyapi.io/v2/KEY", 19_500_000); }

function testEulerFinanceExploit() public { // Euler exploit: donate → borrow → manipulate → liquidate // 1. Attacker donates eToken to protocol (inflates totalAssets) // 2. Borrows against inflated collateral // 3. Manipulates price oracle // 4. Liquidates own position at profit

address attacker = makeAddr("attacker"); address euler = 0x...; // Euler lending contract

vm.prank(attacker); // ... exploit steps replayed from tx trace

// Verify attacker profits assertGt(eth.balanceOf(attacker), initialBalance); }

function testFlashLoanAttack() public { // Single-transaction: borrow → manipulate → repay address pool = 0x...; // Aave/Uniswap pool address attacker = makeAddr("attacker");

vm.startPrank(attacker); // Execute flash loan // Manipulate oracle/price // Repay loan + profit vm.stopPrank(); }

function testOracleManipulation() public { // Chainlink stale price: don't update for hours // Pyth TWAP: manipulate over multiple slots // Custom oracle: direct price setter

// Simulate Chainlink stale price vm.warp(block.timestamp + 2 hours); // Past heartbeat // Price now stale — protocol uses wrong value }

function testMEVSandwich() public { // Frontrun: detect pending swap, buy before, sell after // Backrun: detect liquidation, repay debt, claim collateral

vm.expectRevert(); // Victim tx reverts due to slippage // ... sandwich execution } } `

Oracle Manipulation Testing Deep Dive

Chainlink Stale Price: `solidity function testChainlinkStalePrice() public { AggregatorV3Interface feed = AggregatorV3Interface(0x...);

// Normal price (, int256 price,,,) = feed.latestRoundData(); assertGt(price, 0);

// Fast-forward past heartbeat (typically 1-24 hours) vm.warp(block.timestamp + 25 hours);

// Price now stale — roundId hasn't changed (uint80 roundId,,,,) = feed.latestRoundData(); // Protocol should reject or use fallback } `

Pyth TWAP Deviation: `solidity function testPythTWAPManipulation() public { // Pyth publishes price every slot // TWAP = average over N slots // Attacker manipulates N consecutive slots

for (uint i = 0; i < 10; i++) { vm.roll(block.number + 1); // Submit manipulated price via Pyth wormhole pythMock.updatePrice(manipulatedPrice); }

// TWAP now reflects manipulated prices uint256 twap = pythMock.getTWAP(10); assertApproxEqAbs(twap, manipulatedPrice, 1%); } `

5. Invariant Testing: Mathematical Guarantees

Diagram - Invariant Testing Architecture

Diagram - Invariant Testing Architecture

What are invariants? Properties that must ALWAYS hold, regardless of user actions, market conditions, or attack vectors. Unlike unit tests (specific inputs → expected outputs), invariant tests say: "For ANY sequence of valid operations, these properties remain true."

DeFi Invariant Catalog

Protocol TypeCritical Invariants
AMM (Uniswap V2)reserve0 * reserve1 ≥ k_last (with fees), reserve0 > 0 && reserve1 > 0, no overflow in getAmountOut
AMM (Uniswap V3)totalLiquidity = Σ position.liquidity, sqrtPriceX96 within tick bounds, fee growth monotonic
Lending (Aave/Compound)totalAssets ≥ totalBorrows, collateralRatio ≥ liquidationThreshold for all positions, supplyIndex monotonic
Vaults (ERC4626)totalAssets() ≥ convertToAssets(totalSupply()), previewDeposit(shares) ≥ assets, no share price manipulation via donation
StakingtotalShares * pricePerShare = totalAssets, rewardsDistributed ≤ rewardsAvailable, no double-claim
GovernancequorumReached → executionDelay → timelock → execute, proposalThreshold ≤ votingPower, no double-vote

Writing Invariant Tests in Foundry

`solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20;

import "forge-std/Test.sol"; import "../src/UniswapV2Pair.sol";

contract UniswapV2InvariantTest is Test { UniswapV2Pair pair; ERC20Mock token0; ERC20Mock token1; address alice = makeAddr("alice"); address bob = makeAddr("bob");

function setUp() public { token0 = new ERC20Mock("Token0", "TK0"); token1 = new ERC20Mock("Token1", "TK1"); pair = new UniswapV2Pair();

// Initialize with liquidity token0.mint(alice, 10000 ether); token1.mint(alice, 10000 ether); vm.prank(alice); token0.approve(address(pair), type(uint256).max); token1.approve(address(pair), type(uint256).max); pair.initialize(alice); // Sets up reserves }

// Invariant: reserve product never decreases (accounting for fees) function invariant_reserveProductNonDecreasing() public view { (uint112 reserve0, uint112 reserve1,) = pair.getReserves(); uint256 k = uint256(reserve0) * uint256(reserve1); // kLast tracks reserve product at last sync // After swaps with fees, k should increase assertGe(k, pair.kLast()); }

// Invariant: reserves never zero (prevents division by zero) function invariant_reservesNonZero() public view { (uint112 reserve0, uint112 reserve1,) = pair.getReserves(); assertGt(reserve0, 0); assertGt(reserve1, 0); }

// Invariant: swap output matches formula function invariant_swapFormulaCorrect() public view { (uint112 reserve0, uint112 reserve1,) = pair.getReserves();

// Test various input amounts uint256[] memory testAmounts = [1 ether, 10 ether, 100 ether, 1000 ether]; for (uint i = 0; i < testAmounts.length; i++) { uint256 amountIn = testAmounts[i]; if (amountIn >= reserve0) continue; // Skip invalid

uint256 expectedOut = (amountIn 997 reserve1) / (reserve0 1000 + amountIn 997); uint256 actualOut = pair.getAmountOut(amountIn, reserve0, reserve1); assertEq(actualOut, expectedOut); } }

// Invariant: total supply equals sum of user balances function invariant_totalSupplyEqualsSum() public view { // This requires tracking all holders — use for simpler contracts // For pair, totalSupply = liquidity tokens minted assertEq(pair.totalSupply(), pair.balanceOf(alice) + pair.balanceOf(bob) + ...); } } `

Running Invariant Tests

`bash

forge test --invariant --invariant-depth 50 --invariant-runs 1000

forge test --invariant --match-contract UniswapV2InvariantTest

forge test --invariant -vvvv

forge test --invariant --invariant-depth 10 --match-test invariant_reserveProductNonDecreasing -vvvv `

Interpreting Results

When an invariant fails, Foundry provides a counterexample — a sequence of calls that violates the property. The output shows:

  1. Call sequence that led to violation
  2. State before/after each call
  3. Shrinking — Foundry tries to minimize the counterexample
  4. Replay — Add the sequence as a unit test to debug

`bash

[FAIL. Reason: invariant_reserveProductNonDecreasing() failed] Counterexample:

  1. swap(token0, 100 ether) → reserve0=1100, reserve1=909
  2. swap(token1, 500 ether) → reserve0=1000, reserve1=1000 // k decreased!
  3. sync() → kLast updated incorrectly

`

6. Fuzzing & Property-Based Testing

Diagram - Fuzzing vs Invariant Testing Comparison

Diagram - Fuzzing vs Invariant Testing Comparison

Stateless Fuzzing (Foundry)

For pure functions, stateless fuzzing generates random inputs:

`solidity contract MathFuzzTest is Test { // Foundry automatically fuzzes any test function with parameters function testGetAmountOutFuzz(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) public { // Filters: skip invalid inputs vm.assume(reserveIn > 0); vm.assume(reserveOut > 0); vm.assume(amountIn > 0); vm.assume(amountIn < reserveIn); // Can't swap more than reserve

uint256 expected = (amountIn 997 reserveOut) / (reserveIn 1000 + amountIn 997); uint256 actual = UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut); assertEq(actual, expected); }

// Run: forge test --match-test testGetAmountOutFuzz --fuzz-runs 100000 } `

Stateful Fuzzing (Echidna/Medusa)

For protocol-level properties requiring state:

`yaml

config: testMode: "assertion" maxTx: 50 maxBlocks: 100 corpusDir: "./corpus"

targets:

  • "LendingProtocol"

dictionary:

  • "deposit"
  • "withdraw"
  • "borrow"
  • "repay"
  • "liquidate"

`

`solidity // Echidna test contract contract LendingProtocolFuzz is Test { LendingProtocol protocol; ERC20Mock collateral; ERC20Mock debt;

function setUp() public { collateral = new ERC20Mock("COL", "COL"); debt = new ERC20Mock("DEBT", "DEBT"); protocol = new LendingProtocol(collateral, debt); }

// Property: health factor never < 1 for non-liquidated positions function echidna_healthFactor() public view returns (bool) { // Check all positions for (uint i = 0; i < protocol.positionCount(); i++) { Position memory pos = protocol.positions(i); if (pos.collateral > 0) { uint256 hf = protocol.healthFactor(pos.user); if (hf < 1e18 && !pos.liquidated) { return false; // Violated! } } } return true; }

// Property: total assets always ≥ total borrows function echidna_solvency() public view returns (bool) { return protocol.totalAssets() >= protocol.totalBorrows(); } } `

Run: echidna-test LendingProtocolFuzz.sol --config echidna.yaml

When to Use Fuzzing vs Invariant Testing

AspectFuzzingInvariant Testing
GoalFind edge cases, crashesProve properties hold
InputRandom (guided by corpus)Random sequences of calls
StateStateless (mostly)Stateful
OutputCrashing inputsCounterexample sequences
Best ForPure math, encoding/decodingEconomic invariants, complex flows
ToolsFoundry (builtin), EchidnaFoundry (builtin), Medusa

Use both: Fuzzing finds bugs in isolated functions; invariant testing proves protocol-level properties.

7. Cross-Chain & Upgradeable Protocol Testing

Diagram - Cross-Chain Testing Architecture

Diagram - Cross-Chain Testing Architecture

Testing Bridge Messages

`solidity contract BridgeMessageTest is Test { Bridge bridge; MockEndpoint endpoint;

function setUp() public { endpoint = new MockEndpoint(); bridge = new Bridge(endpoint); }

function testMessagePassing() public { address user = makeAddr("user"); bytes memory payload = abi.encodeWithSelector( ILendingProtocol.deposit.selector, 100 ether );

// Send message from source chain vm.prank(user); bridge.sendMessage(chainIdB, address(protocolB), payload);

// Verify message queued assertEq(endpoint.queuedMessages.length, 1);

// Simulate relay on destination chain endpoint.relayMessage(0);

// Verify execution on destination assertEq(protocolB.totalAssets(), 100 ether); }

function testReplayProtection() public { // Each message has unique nonce // Replay attempt should fail bytes memory payload = abi.encodeWithSelector(...);

bridge.sendMessage(chainIdB, address(protocolB), payload); endpoint.relayMessage(0);

vm.expectRevert("AlreadyExecuted"); endpoint.relayMessage(0); // Duplicate nonce } } `

Omnichain Contracts (LayerZero, Wormhole, Axelar)

`solidity contract OmnichainTest is Test { // Mock LayerZero endpoint MockLZEndpoint lzEndpoint; OmnichainVault vaultA; OmnichainVault vaultB;

function testCrossChainDeposit() public { // User deposits on Chain A vm.prank(alice); vaultA.depositCrossChain(100 ether, chainIdB, address(vaultB));

// Message sent via LayerZero // Simulate receipt on Chain B lzEndpoint.simulateReceipt(chainIdA, address(vaultA), payload);

// Vault B should credit user assertEq(vaultB.balanceOf(alice), 100 ether); } } `

Proxy/Upgradeable Testing

`solidity contract UpgradeableTest is Test { ProxyAdmin admin; TransparentUpgradeableProxy proxy; VaultV1 vaultV1; VaultV2 vaultV2;

function setUp() public { admin = new ProxyAdmin(); vaultV1 = new VaultV1(); proxy = new TransparentUpgradeableProxy( address(vaultV1), admin, "" // No initializer ); }

function testStorageLayoutCollision() public { // Deploy V2 with new state variable vaultV2 = new VaultV2();

// Upgrade vm.prank(address(admin)); proxy.upgradeTo(address(vaultV2));

// Verify V1 state preserved IVault(proxy).deposit(100 ether, alice); assertEq(IVault(proxy).balanceOf(alice), 100 ether);

// New V2 functionality works IVaultV2(proxy).newFeature(); }

function testInitializerGuard() public { // V2 initializer should not be callable twice vm.expectRevert("Initializable: already initialized"); IVaultV2(proxy).initialize(); } } `

Governance Timelock Testing

`solidity contract GovernanceTimelockTest is Test { TimelockController timelock; Governor governor; Vault vault;

function testProposalLifecycle() public { // 1. Propose bytes memory calldata = abi.encodeWithSelector( IVault.setFee.selector, 500 // 5% new fee ); uint256 proposalId = governor.propose( [address(vault)], [0], [calldata], "Update fee to 5%" );

// 2. Vote vm.warp(votingDelay + 1); vm.prank(whale); governor.castVote(proposalId, 1); // For

// 3. Queue (after voting period) vm.warp(votingPeriod + votingDelay + 1); governor.queue(proposalId);

// 4. Execute (after timelock delay) vm.warp(timelockDelay + 1); governor.execute(proposalId);

// Verify assertEq(vault.fee(), 500); } } `

8. Gas Profiling & Performance Testing

`bash

forge test --gas-report

forge snapshot

forge snapshot --diff # Shows gas increases/decreases

forge loadtest --rpc-url $RPC_URL --tx-count 1000 --gas-limit 30000000 `

Gas Optimization Checklist

PatternOptimizationGas Saved
for (uint i=0; iCache length: uint len = arr.length~3,000/loop
storage in loopsCache to memory~20,000/access
require(msg.sender == owner)Custom error: error NotOwner()~200/revert
string error messagesCustom errors with params~100/revert
Multiple SSTORE to same slotBatch in memory, single write~20,000/write
unchecked { x + y }Skip overflow checks (when safe)~50/op

9. Common Mistakes & Anti-Patterns

  1. Testing only happy paths — Never test failure modes (reverts, edge cases). Every require needs a test that triggers it.
  2. Mocking oracles incorrectly — Use real price feeds in fork tests; mocks miss manipulation vectors.
  3. Ignoring composability — Test with actual protocol integrations, not mocks. Your contract lives in an ecosystem.
  4. Skipping invariant testing — Unit tests cannot catch economic bugs. Invariants are your safety net.
  5. No CI/CD — Tests that don't run on every PR are useless. GitHub Actions + Foundry = 5 min setup.
  6. Hardcoding addresses — Use vm.label, config files, and environment variables for multi-chain.
  7. Forgetting upgradeability — Test proxy admin, initializer guards, storage gaps, UUPS vs Transparent.
  8. Ignoring gas costs — A function that works but costs 5M gas is broken. Profile every release.

10. FAQ

What is the difference between fork testing and invariant testing? Fork testing replays mainnet state to simulate specific scenarios (exploits, oracle states). Invariant testing mathematically proves properties hold for ALL possible state transitions. Use fork tests for known attacks; invariants for unknown ones.

How many fuzz runs are sufficient for DeFi contracts? Minimum 10,000 runs for stateless fuzzing; 1,000+ invariant runs with depth 50+. For high-value protocols, run nightly with 100,000+ fuzz runs and 10,000+ invariant runs.

Can I test DeFi protocols without mainnet RPC access? Yes — use public RPCs (Alchemy free tier), local anvil forks (anvil --fork-url), or testnet forks (Sepolia, Arbitrum Sepolia). For CI, use ephemeral forks via GitHub Actions.

What invariants should every AMM satisfy?

  1. reserve0 * reserve1 ≥ kLast (with fees)
  2. reserve0 > 0 && reserve1 > 0 (no zero reserves)
  3. getAmountOut matches constant product formula
  4. totalSupply equals sum of LP positions
  5. Fee growth monotonically increases per LP position

How do I test upgradeable DeFi contracts safely?

  1. Verify storage layout compatibility (no collisions)
  2. Test initializer guards (cannot re-initialize)
  3. Test proxy admin permissions (only governance)
  4. Simulate upgrade → verify state preserved → test new features
  5. Test emergency pause/upgrade paths

Ready to audit your DeFi protocol? Our security team specializes in invariant testing, fork replay, and formal verification. [Book a consultation](/security-audit)

Tags:

defi testing guidesdefi testing guides: complete tutorial for smart contract de