Skip to content

emilianosolazzi/pETH

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

55 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

pETH: Protocol ETH

Wrapped ETH with invariant-enforced solvency.
https://peth-eta.vercel.app/

pETH is a wrapped ETH token that treats solvency as a transaction-validity condition — not an external audit report. The contract introduces an invariant-first reserve model for ETH wrappers, where every economic transition must preserve the core invariant: T + F = R.

v1.2.0 fee model: free to wrap · 0.05% exit fee on redeem.


What "Solvency" Means (Plain English)

Solvency means you can redeem what you own.

For pETH, that means every pETH holder can redeem for ETH according to the protocol rules, and the contract enforces that accounting every time state changes. If a state transition would break solvency accounting, the transaction reverts instead of executing.

pETH = redeemable ETH under enforced reserve rules.

The contract enforces reserve accounting on every state-changing path. Redemption outcomes follow protocol rules (including fees), subject to chain liveness and contract correctness.

Quick mental model:

  • Use WETH when you optimize for execution (trading, routing, liquidity).
  • Use pETH when you optimize for assurance (verifiable reserve accounting).

Category framing:

  • pETH is not a WETH replacement.
  • WETH is the execution wrapper.
  • stETH is the yield wrapper.
  • pETH is the assurance wrapper.
  • Purpose: live, explicit, invariant-checked reserve accounting.

What Makes pETH Special

1. Invariant-First Design

Unlike WETH and other wrapped tokens, pETH enforces a mathematical invariant on every transaction:

$$T + F = R$$

Where:

  • T = Outstanding token supply (user-redeemable ETH)
  • F = Accumulated protocol fees (treasury-owned)
  • R = Total ETH reserve held by contract

Translation: The sum of user claims and protocol fees must equal the accounted reserve. The code enforces this invariant structurally rather than relying on external documentation or delayed audits.

// After ANY operation, this MUST hold:
if (T + F != R) revert InvariantViolation();

2. Oracle-Free & Self-Referential

pETH needs zero external data:

  • No Chainlink oracle
  • No L2 state root
  • No cross-chain relay
  • No price feed

Backing: 1 pETH = 1 ETH (native asset)

This is cryptographically simple:

mint(10 ETH)  → get 10 pETH  (free entry — mintFeeBps=0)
burn(10 pETH) → get 9.995 ETH (0.05% exit fee — burnFeeBps=5)

The ratio is self-proven on-chain: just check the balance.

3. Mathematically Complete Accounting

Every valid state transition is fully accounted:

Operation Transition
mint(x) at mintFeeBps=0 (R, T, F) → (R+x, T+x, F) — T+F still == R ✓
burn(x) at burnFeeBps=5 (R, T, F) → (R−(x−f), T−x, F+f) — T+F still == R ✓
sweep(y) (R, T, F) → (R−y, T, F−y) where y ≤ F — T+F still == R ✓
transfer No change to (R, T, F) — T+F unchanged ✓

No escape routes. The invariant is structurally enforced.

4. Built-in Fee Accounting

Fees are part of the reserve, not separate:

Mint 10 ETH (free entry, mintFeeBps=0):
  ├─ 0 ETH → F (no mint fee)
  ├─ 10 ETH → T (user supply)
  └─ 10 ETH total = R ✓

Burn 10 pETH (burnFeeBps=5, 0.05% exit fee):
  ├─ 0.005 ETH → F (exit fee)
  ├─ 9.995 ETH returned to user
  └─ F=0.005, T=0, R=0.005 — T+F still == R ✓

Treasury sweeps fee:
  ├─ Reduce F by 0.005 ETH
  ├─ Reduce R by 0.005 ETH
  └─ T + F still == R ✓

Benefit: Fees are tracked explicitly, do not get double-counted, and remain visible on-chain.

5. Solvency as a Transaction-Validity Condition

WETH model:

  • Minimal native ETH wrapper optimized for execution
  • Very small accounting surface

pETH model:

  • Adds explicit reserve accounting, protocol-fee attribution, and invariant telemetry
  • Rejects state transitions that violate the accounting boundary
  • Designed for systems that want stronger accounting observability
// Every mint, burn, sweep, and surplus absorption:
modifier invariantGuard() {
    _;
    _assertInvariant();  // Revert if violated
}

6. Downward-Only Fee Ratchet

Fees may only decrease, never increase:

// Treasury can lower burnFeeBps — but never raise it back
setBurnFeeBps(3);  // 0.05% → 0.03% ✓
setBurnFeeBps(5);  // Reverts: CannotIncreaseFee(3, 5) ✗

This means protocol governance can only become more user-favorable over time.

7. Forced ETH Handling

If ETH is force-sent (e.g., via selfdestruct), pETH has a mechanism:

surplus = address(this).balance - R

// Treasury can absorb surplus as fees:
absorbSurplusAsFees(surplus);
// Result: F += surplus, R += surplus, T unchanged
// Still: T + F == R ✓

Benefit: Forced deposits are accounted for instead of remaining untracked.

8. Zero External Dependencies

  • No oracle
  • No cross-chain bridges
  • No governance token
  • No upgrades (immutable by design)

Comparison: pETH vs. WETH

Feature pETH WETH
Solvency Model Explicit R/T/F invariant Minimal native ETH wrapper
Mint Fee 0% (free entry) None
Burn Fee 0.05% exit fee None
Fee Ratchet Downward-only (CannotIncreaseFee) N/A
Invariant Telemetry stateTuple() + checkInvariant() Not explicit
Use Case Assurance / reserve accounting Execution / liquidity
Oracle Needed None None
Forced ETH Handling absorbSurplusAsFees() Not a protocol feature
Treasury Rotation 2-step transfer pattern N/A

Security Properties

Invariant Enforcement

// Both ALWAYS hold, enforced by revert:
require(T + F == R);
require(address(this).balance >= R);

Tested with:

  • 77 unit tests (deterministic)
  • 46 gap & edge-case tests (including 4 reentrancy attack vectors)
  • 50 fuzz/deterministic fuzz-suite tests + 4 stateful invariant properties
  • 600,000+ total test scenarios
  • Manual audit: No Critical, No High (see SLITHER_ANALYSIS.md)

Access Control

  • Only treasury can sweep fees, absorb surplus, adjust fee rates, or initiate treasury rotation
  • Treasury rotation is 2-step: initiate (current treasury) → accept (pending treasury)
  • Fee rates can only decrease — CannotIncreaseFee error on any increase attempt
  • Only token holder can burn their own tokens
  • Allowances enforced on transferFrom
  • Reentrancy guard on all state-changing functions

Input Validation

  • Reject zero amounts
  • Reject zero addresses
  • Fee bounds checked (max 10%)
  • Overflow protection on T+F

Use Cases

1. Staking Protocol with Fees

// Deposit ETH for staking receipt — free entry
pETH.mint{value: 10 ether}(stakingPool);

// Protocol takes exit fee on redemption
// Fees accumulate in F on every burn
// Periodically sweep to treasury

2. Liquidity Protocol Collateral

// Use pETH as collateral backing
// Know with certainty: T + F == R

// Can always verify solvency on-chain
(bool solvencyHolds, , ,) = pETH.checkInvariant();
require(solvencyHolds);

3. Multi-Sig Treasury

// Lock ETH in pETH
// Only treasury multi-sig can sweep fees or change fee rates

// No risk of fractional reserves
// Math is immutable proof

4. Auction Protocol Reserve

// Accept ETH, issue pETH receipts — free
// Forced sends (selfdestruct) → absorbed as surplus
// Burn fees auto-accumulate and are sweepable

Getting Started

Deploy

cp .env.example .env
# Fill in: TREASURY_ADDRESS (use a Safe), MINT_FEE_BPS=0, BURN_FEE_BPS=5, RPC_URL, PRIVATE_KEY
# For mainnet only, set MAINNET_DEPLOYMENT_ACK=true after completing the release checklist.

forge script script/Deploy.s.sol --rpc-url $RPC_URL --broadcast --verify

# Post-deploy invariant/config check
PETH_ADDRESS=0xDeployedPETH forge script script/CheckDeployed.s.sol --rpc-url $RPC_URL

Use

// Wrap ETH — free entry
pETH.mint{value: 10 ether}(msg.sender);
// Get 10 pETH (mintFeeBps=0, no fee)

// Unwrap — 0.05% exit fee
pETH.burn(10 ether);
// Get 9.995 ETH

// Check invariant
(bool holds, , ,) = pETH.checkInvariant();
require(holds);  // Always true

// Rotate treasury (2-step)
pETH.initiateTreasuryTransfer(newSafe);   // current treasury
pETH.acceptTreasuryTransfer();            // new treasury

// Preview cumulative fee drag over N roundtrips
uint256 drag = pETH.previewCumulativeFeeDrag(1 ether, 100);

The Math

State Tuple

R = reserve (wei)
T = outstanding supply
F = fees (inside R)

Core Invariant

T + F = R  (always, or revert)

Mint Example (v1.2.0 — free entry)

Before:  R=0,   T=0,   F=0
Action:  Deposit 10 ETH, mintFeeBps=0 → fee=0
After:   R=10,  T=10,  F=0
Check:   10 + 0 = 10 ✓

Burn Example (v1.2.0 — 0.05% exit fee)

Before:  R=10,      T=10,    F=0
Action:  Burn 10 pETH, burnFeeBps=5
Exit fee: 10 × 0.0005 = 0.005 ETH
Release:  9.995 ETH
After:
  R = 10 - 9.995 = 0.005
  T = 10 - 10    = 0
  F = 0  + 0.005 = 0.005
Check:
  T + F = R
  0 + 0.005 = 0.005 ✓

Testing

# All pETH tests
forge test --match-path "test/pETH*"

# With gas report
forge test --match-path "test/pETH*" --gas-report

# Bounded stateful invariants
FOUNDRY_INVARIANT_RUNS=256 FOUNDRY_INVARIANT_DEPTH=128 forge test --match-test invariant_ -vv

# Coverage
forge coverage --match-path "test/pETH*"

# CI profile — 100,000 fuzz runs
FOUNDRY_PROFILE=ci forge test --match-path "test/pETH*"

All tests verify: T + F == R always holds

The accounting invariant has survived 600,000+ fuzz scenarios under adversarial state transitions. Stateful invariant properties verify T+F==R, balance>=R, checkInvariant() consistency, and supply conservation across all random action sequences.


Key Functions

User Functions

  • mint(address to) payable — Wrap ETH, receive pETH (free at mintFeeBps=0)
  • burn(uint256 amount) — Burn pETH, receive ETH (minus burnFee)
  • burnTo(address receiver, uint256 amount) — Burn pETH, send ETH to receiver
  • transfer(address to, uint256 amount) — ERC-20 transfer
  • approve(address spender, uint256 amount) — ERC-20 approve
  • transferFrom(address from, address to, uint256 amount) — ERC-20 delegated transfer

Treasury Functions (onlyTreasury)

  • sweepFees(uint256 amount) — Withdraw accumulated fees (F)
  • absorbSurplusAsFees(uint256 amount) — Absorb forced ETH into F
  • setMintFeeBps(uint256 newFeeBps) — Lower mint fee (downward only)
  • setBurnFeeBps(uint256 newFeeBps) — Lower burn fee (downward only)
  • initiateTreasuryTransfer(address newTreasury) — Begin 2-step treasury rotation
  • cancelTreasuryTransfer() — Cancel a pending rotation

Pending Treasury Functions

  • acceptTreasuryTransfer() — Complete the rotation (called by new treasury)

View Functions

  • stateTuple() — Returns (R, T, F, balance)
  • checkInvariant() — Returns (holds, accountingDiff, shortfall, balance)
  • surplus() — Forced ETH above R
  • calculateMintFee(uint256 amount) — Fee for a given mint amount (0 at mintFeeBps=0)
  • calculateBurnFee(uint256 amount) — Fee for a given burn amount
  • previewMint(uint256 grossDeposit) — Expected tokens from a deposit
  • previewBurn(uint256 burnAmount) — Expected ETH from a burn
  • previewCumulativeFeeDrag(uint256 amount, uint256 periods) — Fee drag over N roundtrips (cap: 1000)
  • treasury() / pendingTreasury() — Current and pending treasury addresses
  • mintFeeBps() / burnFeeBps() — Current fee rates
  • reserveToSupplyRatioBps() / collateralizationRatioBps() — R/T ratio in bps

Configuration

Constructor

InvariantFirstReserveToken(
    string  name_,         // "pETH"
    string  symbol_,       // "pETH"
    address treasury_,     // Must be a multi-sig for mainnet
    uint256 mintFeeBps_,   // 0 = 0.00%, free entry (v1.2.0 default)
    uint256 burnFeeBps_    // 5 = 0.05% exit fee (v1.2.0 default, max 10_00 = 10.00%)
)

Constants

BPS_DENOMINATOR = 100_00    // 100.00%
MAX_FEE_BPS = 10_00         // 10.00%

Fee Ratchet

Once set, fees may only decrease. Setting mintFeeBps to 0 permanently locks free entry — setMintFeeBps will revert on every future call.


Why This Matters

Problem It Solves

Current wrapped token risks:

  1. Proof-of-Reserves reports lag (stale)
  2. Bookkeeping errors accumulate (human error)
  3. Solvency is off-chain (unverifiable)
  4. Fees leak or double-count (accounting bug)
  5. Forced ETH creates untracked balances

pETH's Solution

  1. On-chain solvency proof — T+F==R is enforced per-transaction
  2. No accounting lag — State is current at every block
  3. Math-first design — Invariant is enforced by the state machine
  4. Fee completeness — Fees are part of the state, fully tracked
  5. Surplus handler — Forced or accidental ETH appears as surplus above R; treasury can explicitly absorb it into F
  6. Governance-friendly ratchet — Fee rates can only move in users' favor

Architecture Diagram

                    User
                     │
            ┌────────┼────────┐
            │        │        │
          mint     transfer   burn
         (free)              (0.05%)
            │        │        │
            ▼        ▼        ▼
    ┌─────────────────────────────┐
    │  pETH Contract              │
    │                             │
    │  State Tuple:               │
    │   R = reserve               │
    │   T = supply                │
    │   F = fees                  │
    │                             │
    │  Invariant:                 │
    │   T + F == R (verified!)    │
    │                             │
    └─────────────────────────────┘
            │        │        │
            ▼        ▼        ▼
         Token    ERC-20    ETH Out
       Supply    Balance

Specifications

Property Value
Contract InvariantFirstReserveToken v1.2.0
Token Standard ERC-20-compatible (custom implementation)
Collateral Native ETH only
Decimals 18
Mint Fee 0% (free entry — locked at 0 by ratchet)
Burn Fee 0.05% (5 bps — can only decrease)
Fee Range 0–10% (0–1,000 bps) per fee type
Fee Direction Downward only (CannotIncreaseFee error)
Reentrancy Guard Yes (nonReentrant on all mutations)
Upgradeable No (immutable by design)
Oracle Required No
Invariant Enforced Yes (every state-changing call)
Forced ETH Handler Yes (absorbSurplusAsFees)
Treasury Rotation 2-step (initiate → accept / cancel)
Compiler solc 0.8.30, evm_version = paris

Production Readiness

Auditable

  • Clean, documented state transitions
  • All invariants explicit in code
  • Every error and event path covered by tests
  • Manual audit complete: No Critical, No High (see SLITHER_ANALYSIS.md)

Testable

  • 77 unit tests + 46 gap tests + 50 fuzz-suite tests
  • 4 stateful invariant properties across random action sequences
  • 600,000+ total test scenarios
  • Static analysis via Slither 0.11.3 — 0 findings

Observable

  • State tuple on-chain (R, T, F)
  • Checkable invariant (checkInvariant())
  • Surplus calculation (surplus())
  • Treasury rotation state (treasury(), pendingTreasury())
  • Fee rates (mintFeeBps(), burnFeeBps())

Safe

  • Reentrancy guard on all state-changing functions
  • Treasury access control with 2-step rotation
  • Overflow/underflow checks (solc 0.8 checked arithmetic)
  • Complete input validation (zero amounts, zero addresses, fee bounds)
  • Downward-only fee ratchet — governance cannot raise fees

Documentation

  • QUICK_START.md — Overview and pre-deploy checklist
  • QUICK_COMMANDS.md — Copy-paste command reference
  • TEST_README.md — Per-test narrative, invariant walkthrough, debugging
  • TESTS_SUMMARY.md — Complete test catalogue with every test name and audit findings
  • TEST_INFRASTRUCTURE.md — Project layout, config, coverage tables
  • SLITHER_ANALYSIS.md — Static analysis + manual audit report, all findings dispositioned

The Innovation

pETH treats solvency as a transaction-validity condition, not an external audit report.

Every mint, burn, transfer, and sweep operation is validated:

If not (T + F == R):
    revert InvariantViolation()

This is more direct than Proof-of-Reserves because:

  1. It is verified every transaction (not periodically)
  2. It is on-chain (not external)
  3. It is a code-enforced invariant, not a reporting claim
  4. It can be deployed immutably, reducing governance risk

Status: Production-oriented design — manual audit complete, no Critical/High findings
Contract: pETH-IFE-1.2.0
Fee model: Free to wrap · 0.05% exit fee
Invariant: T + F == R (enforced on every state-changing call)
Last updated: 2026-06-24

About

pETH is invariant-first ETH: a reserve-backed holding asset for protocols and treasuries that need live, on-chain solvency assurance at every block.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages