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.
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.
Unlike WETH and other wrapped tokens, pETH enforces a mathematical invariant on every transaction:
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();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.
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.
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.
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
}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.
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.
- No oracle
- No cross-chain bridges
- No governance token
- No upgrades (immutable by design)
| 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 |
// 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)
- 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 —
CannotIncreaseFeeerror on any increase attempt - Only token holder can burn their own tokens
- Allowances enforced on
transferFrom - Reentrancy guard on all state-changing functions
- Reject zero amounts
- Reject zero addresses
- Fee bounds checked (max 10%)
- Overflow protection on T+F
// 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// Use pETH as collateral backing
// Know with certainty: T + F == R
// Can always verify solvency on-chain
(bool solvencyHolds, , ,) = pETH.checkInvariant();
require(solvencyHolds);// Lock ETH in pETH
// Only treasury multi-sig can sweep fees or change fee rates
// No risk of fractional reserves
// Math is immutable proof// Accept ETH, issue pETH receipts — free
// Forced sends (selfdestruct) → absorbed as surplus
// Burn fees auto-accumulate and are sweepablecp .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// 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);R = reserve (wei)
T = outstanding supply
F = fees (inside R)
T + F = R (always, or revert)
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 ✓
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 ✓
# 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.
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 receivertransfer(address to, uint256 amount)— ERC-20 transferapprove(address spender, uint256 amount)— ERC-20 approvetransferFrom(address from, address to, uint256 amount)— ERC-20 delegated transfer
sweepFees(uint256 amount)— Withdraw accumulated fees (F)absorbSurplusAsFees(uint256 amount)— Absorb forced ETH into FsetMintFeeBps(uint256 newFeeBps)— Lower mint fee (downward only)setBurnFeeBps(uint256 newFeeBps)— Lower burn fee (downward only)initiateTreasuryTransfer(address newTreasury)— Begin 2-step treasury rotationcancelTreasuryTransfer()— Cancel a pending rotation
acceptTreasuryTransfer()— Complete the rotation (called by new treasury)
stateTuple()— Returns(R, T, F, balance)checkInvariant()— Returns(holds, accountingDiff, shortfall, balance)surplus()— Forced ETH above RcalculateMintFee(uint256 amount)— Fee for a given mint amount (0 at mintFeeBps=0)calculateBurnFee(uint256 amount)— Fee for a given burn amountpreviewMint(uint256 grossDeposit)— Expected tokens from a depositpreviewBurn(uint256 burnAmount)— Expected ETH from a burnpreviewCumulativeFeeDrag(uint256 amount, uint256 periods)— Fee drag over N roundtrips (cap: 1000)treasury()/pendingTreasury()— Current and pending treasury addressesmintFeeBps()/burnFeeBps()— Current fee ratesreserveToSupplyRatioBps()/collateralizationRatioBps()— R/T ratio in bps
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%)
)BPS_DENOMINATOR = 100_00 // 100.00%
MAX_FEE_BPS = 10_00 // 10.00%Once set, fees may only decrease. Setting mintFeeBps to 0 permanently locks free entry — setMintFeeBps will revert on every future call.
Current wrapped token risks:
- Proof-of-Reserves reports lag (stale)
- Bookkeeping errors accumulate (human error)
- Solvency is off-chain (unverifiable)
- Fees leak or double-count (accounting bug)
- Forced ETH creates untracked balances
- On-chain solvency proof — T+F==R is enforced per-transaction
- No accounting lag — State is current at every block
- Math-first design — Invariant is enforced by the state machine
- Fee completeness — Fees are part of the state, fully tracked
- Surplus handler — Forced or accidental ETH appears as surplus above R; treasury can explicitly absorb it into F
- Governance-friendly ratchet — Fee rates can only move in users' favor
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
| 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 |
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.8checked arithmetic) - Complete input validation (zero amounts, zero addresses, fee bounds)
- Downward-only fee ratchet — governance cannot raise fees
- 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
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:
- It is verified every transaction (not periodically)
- It is on-chain (not external)
- It is a code-enforced invariant, not a reporting claim
- 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