Esperanto Stablecoin (ESP) Smart Contract Architecture
Copyright © Christian Derler
Document Version: 1.0 Last Updated: 2026-03-30 Status: Production-Ready Specification
Table of Contents
- Executive Summary
- Architecture Overview
- Core Contracts
- Multi-Chain Strategy
- Security Framework
- Deployment Plan
- Gas Optimization Notes
Executive Summary
The Esperanto Stablecoin (ESP) is a MiCAR-compliant Asset-Referenced Token designed to track a 5-pillar global index with institutional-grade risk management. The architecture leverages UUPS upgradeable proxies, time-locked governance, and multi-signature controls to balance innovation with regulatory requirements.
Key Characteristics: - Type: ERC-20 + ERC-2612 (permit functionality) - Compliance: MiCAR-ready with blocklist and pause controls - Primary Chain: Ethereum L1 (Mainnet) - Multi-Chain: Arbitrum, Base, Polygon (via canonical bridges) - Governance: 3-of-5 multisig with 48-hour timelock - Oracle: 2-of-3 medianization with commit-reveal scheme - Settlement: T+1 with daily limits and per-transaction caps
Architecture Overview
System-Level Architecture Diagram
┌─────────────────────────────────────────────────────────────────┐
│ ESP ECOSYSTEM ARCHITECTURE │
└─────────────────────────────────────────────────────────────────┘
┌──────────────────┐
│ ESPToken.sol │ (ERC-20 + ERC-2612)
│ UUPS Upgradeable│
└────────┬─────────┘
│
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ ESPMinter.sol│ │ESPOracle.sol │ │ESPCircuit │
│ │ │ │ │Breaker.sol │
│ Primary Mint │ │ Index Oracle │ │ │
│ Redemption │ │ Medianizer │ │ Emergency │
│ Queue │ │ Commit-Reveal│ │ Controls │
└────┬─────────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
└───────────────────┼─────────────────┘
│
┌───────▼────────┐
│ESPGovernance │
│ │
│Timelock │
│Parameter Mgmt │
│Role Control │
└────────────────┘
│
┌───────▼────────────────┐
│ 3-of-5 Gnosis Safe │
│ Admin Multisig │
└────────────────────────┘
MULTI-CHAIN LAYER:
┌──────────────────────────────────────────────────────────┐
│ Canonical Bridge (Lock-and-Mint Pattern) │
├──────────────────────────────────────────────────────────┤
│ Ethereum L1 ◄──► Arbitrum ◄──► Base ◄──► Polygon │
│ (Primary) (Layer 2) (Layer 2) (Sidechain) │
└──────────────────────────────────────────────────────────┘
Contract Dependency Graph
ESPToken (Core)
├── Used by: ESPMinter, ESPCircuitBreaker
├── Depends on: AccessControl, ERC-2612, Pausable, Burnable
└── Storage: Balances, Allowances, Blocklist
ESPMinter (Mint/Burn Logic)
├── Calls: ESPToken.mint(), ESPToken.burn()
├── Depends on: ESPOracle (for NAV pricing)
├── Manages: Redemption queue, Fee collection
└── Storage: Pending redeemptions, Fee vault
ESPOracle (Index Feed)
├── Inputs: Chainlink aggregators (x3), Committee signer
├── Implements: Commit-Reveal with 2-of-3 threshold
├── Manages: Historical prices, Staleness detection
└── Storage: Price history, Commit state
ESPCircuitBreaker (Emergency Controls)
├── Controlled by: 3-of-5 Gnosis Safe
├── Can pause: ESPToken, ESPMinter
├── Monitors: Oracle staleness, Price volatility
└── Triggers: Fee escalation, Emergency shutdown
ESPGovernance (Parameter Management)
├── Requires: 48-hour timelock minimum
├── Controls: All parameter updates
├── Managed by: 4-of-5 multisig (emergency override)
└── Storage: Pending updates, Execution timestamps
Core Contracts
1. ESPToken.sol — ERC-20 Token
The canonical token implementation with regulatory compliance features.
Interface Specification
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
interface IESPToken {
/// @notice Token metadata
function name() external pure returns (string memory);
function symbol() external pure returns (string memory);
function decimals() external pure returns (uint8);
/// @notice ERC-20 standard functions
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
/// @notice ERC-2612 Permit (gasless approvals)
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external;
function nonces(address owner) external view returns (uint256);
function DOMAIN_SEPARATOR() external view returns (bytes32);
/// @notice Burnable extension
function burn(uint256 amount) external;
function burnFrom(address account, uint256 amount) external;
/// @notice Pausable functions (PAUSER_ROLE only)
function pause() external;
function unpause() external;
function paused() external view returns (bool);
/// @notice Minting (MINTER_ROLE only)
function mint(address to, uint256 amount) external;
/// @notice Blocklist (regulatory compliance)
function addToBlocklist(address account) external;
function removeFromBlocklist(address account) external;
function isBlocklisted(address account) external view returns (bool);
/// @notice Role management
function grantRole(bytes32 role, address account) external;
function revokeRole(bytes32 role, address account) external;
function renounceRole(bytes32 role, address callerConfirmation) external;
function hasRole(bytes32 role, address account) external view returns (bool);
/// @notice UUPS proxy upgrade
function upgradeToAndCall(address newImplementation, bytes calldata data) external payable;
/// @notice Events
event Mint(address indexed to, uint256 amount);
event Burn(address indexed from, uint256 amount);
event BlocklistAdded(address indexed account);
event BlocklistRemoved(address indexed account);
event Paused(address indexed account);
event Unpaused(address indexed account);
// Role constants
bytes32 constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
bytes32 constant BLOCKLIST_MANAGER_ROLE = keccak256("BLOCKLIST_MANAGER_ROLE");
bytes32 constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");
}
Implementation Details
Key Features: - Pausable: Emergency pause of all token transfers (transfer, transferFrom, mint, burn) - Burnable: Token holders can burn their own tokens; MINTER_ROLE can burn on behalf - Blocklist: Regulatory-compliant blocklist preventing transfers to/from sanctioned addresses - ERC-2612 Permits: Gasless approvals enabling meta-transactions - UUPS Proxy: Upgradeable via proxy for bug fixes and feature enhancements - Access Control: Fine-grained role-based permission system
State Variables:
bool private _paused;
mapping(address => bool) private _blocklist;
Roles: - MINTER_ROLE: Can mint tokens (assigned to ESPMinter) - PAUSER_ROLE: Can pause/unpause (assigned to ESPCircuitBreaker) - BLOCKLIST_MANAGER_ROLE: Can manage blocklist (assigned to Admin multisig) - UPGRADER_ROLE: Can authorize upgrades (assigned to ESPGovernance) - DEFAULT_ADMIN_ROLE: Can manage all roles (held by ESPGovernance)
2. ESPMinter.sol — Mint/Burn Logic
Manages primary market minting with NAV-based pricing and redemption queue.
Interface Specification
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IESPMinter {
/// @notice Redemption request structure
struct RedemptionRequest {
address requester;
uint256 espAmount;
uint256 navPerToken;
uint256 timestamp;
bool settled;
}
/// @notice Mint configuration
struct MintConfig {
uint256 dailyMintCap;
uint256 perTxMintCap;
uint256 minMintAmount;
uint256 minterFeeBps; // 5-15 basis points
uint256 navBandBps; // ±10 basis points
uint256 settlementDelay; // T+1
}
/// @notice Requestor must be KYC'd counterparty
function requestMint(
address counterparty,
uint256 fiatAmount,
bytes calldata kycProof
) external returns (uint256 mintedAmount);
/// @notice Execute pending mint (after T+1 settlement)
function executeMint(uint256 requestId) external;
/// @notice Requestor must be KYC'd counterparty
function requestRedemption(uint256 espAmount) external returns (uint256 requestId);
/// @notice Execute pending redemption (after T+1 settlement)
function executeRedemption(uint256 requestId) external returns (uint256 fiatAmount);
/// @notice NAV-based pricing (index value)
function getNavPerToken() external view returns (uint256);
/// @notice Calculate mint output given NAV and fiat amount
function calculateMintOutput(
uint256 fiatAmount,
uint256 navPerToken
) external view returns (uint256 espAmount, uint256 feeBps);
/// @notice Calculate redemption output
function calculateRedemptionOutput(
uint256 espAmount,
uint256 navPerToken
) external view returns (uint256 fiatAmount, uint256 feeBps);
/// @notice View mint configuration
function getMintConfig() external view returns (MintConfig memory);
/// @notice Update configuration (GOVERNANCE only)
function updateMintConfig(
uint256 newDailyCap,
uint256 newPerTxCap,
uint256 newMinAmount,
uint256 newFeeBps,
uint256 newBandBps
) external;
/// @notice Get daily minting stats
function getDailyMintStats() external view returns (
uint256 todaysMintedAmount,
uint256 remainingCap,
uint256 resetTime
);
/// @notice View redemption request
function getRedemptionRequest(uint256 requestId)
external view returns (RedemptionRequest memory);
/// @notice List pending redemptions
function getPendingRedemptions() external view returns (uint256[] memory requestIds);
/// @notice Fee collection
function collectFees() external returns (uint256 feeAmount);
function getFeeVault() external view returns (uint256);
/// @notice Events
event MintRequested(
uint256 indexed requestId,
address indexed counterparty,
uint256 fiatAmount,
uint256 espAmount,
uint256 navPerToken,
uint256 feeBps
);
event MintExecuted(
uint256 indexed requestId,
address indexed counterparty,
uint256 espAmount
);
event RedemptionRequested(
uint256 indexed requestId,
address indexed requester,
uint256 espAmount,
uint256 navPerToken
);
event RedemptionExecuted(
uint256 indexed requestId,
address indexed requester,
uint256 fiatAmount
);
event ConfigUpdated(
uint256 dailyMintCap,
uint256 perTxMintCap,
uint256 minMintAmount,
uint256 minterFeeBps,
uint256 navBandBps
);
event FeeCollected(uint256 amount);
}
Implementation Details
Mint Flow: 1. KYC’d counterparty calls requestMint(fiatAmount, kycProof) — stores pending mint request 2. Contract validates: KYC status, daily cap, per-tx cap, NAV band (±10bps) 3. Returns mintedAmount = fiatAmount × navPerToken / (1 + feeBps / 10000) 4. After T+1 settlement window, counterparty calls executeMint(requestId) 5. ESPToken mints to counterparty; fees accrue to vault
Redemption Flow: 1. Token holder calls requestRedemption(espAmount) 2. ESP is held in escrow; request enters settlement queue 3. After T+1, holder calls executeRedemption(requestId) 4. Calculate: fiatAmount = espAmount × navPerToken / (1 + feeBps / 10000) 5. Fees deducted; fiat transferred to holder; ESP burned
Key Constraints: - Daily mint cap: Prevents excessive supply expansion - Per-transaction mint cap: Reduces slippage and sandwich attack surface - Minimum mint amount: Economically viable transactions - NAV band (±10bps): Prevents arbitrage against oracle feed - Fee range (5-15 bps): Configurable by governance, collected to vault
Storage:
mapping(uint256 => RedemptionRequest) public redemptionRequests;
uint256 public requestCounter;
uint256 public lastMintResetTime;
uint256 public todaysMintedAmount;
uint256 public feeVault;
MintConfig public config;
3. ESPOracle.sol — Index Oracle
Multi-source price oracle with commit-reveal scheme and 2-of-3 medianization.
Interface Specification
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IESPOracle {
/// @notice Price entry with timestamp
struct PriceUpdate {
uint256 timestamp;
uint256 navPerToken;
bytes32 dataHash;
}
/// @notice Commit-reveal state
struct CommitState {
bytes32 commitHash;
uint256 commitTime;
bool revealed;
}
/// @notice Get current NAV per token (2-of-3 median)
function getNavPerToken() external view returns (uint256);
/// @notice Get price with timestamp
function getLatestPrice() external view returns (uint256 navPerToken, uint256 timestamp);
/// @notice Chainlink feed price (source 1)
function getPriceFeed1() external view returns (uint256);
/// @notice Chainlink feed price (source 2)
function getPriceFeed2() external view returns (uint256);
/// @notice Chainlink feed price (source 3)
function getPriceFeed3() external view returns (uint256);
/// @notice Committee signer price (custom feed)
function getCommitteeSigner() external view returns (address);
/// @notice Commit phase: committees submit hash of price and salt
function commitPrice(bytes32 priceHash) external;
/// @notice Reveal phase: committees submit actual price and salt
/// @param price The NAV per token value
/// @param salt Random value used in hash generation
function revealPrice(uint256 price, bytes32 salt) external;
/// @notice Finalize pricing (2-of-3 threshold)
function finalizePrice() external;
/// @notice Check if price is stale (> 25 hours)
function isPriceStale() external view returns (bool);
/// @notice Get heartbeat interval (daily update)
function getHeartbeatInterval() external view returns (uint256);
/// @notice Get staleness threshold (25 hours in seconds)
function getStalenessThreshold() external view returns (uint256);
/// @notice Get historical prices
function getPriceHistory(uint256 lookbackHours)
external view returns (PriceUpdate[] memory);
/// @notice Get specific historical price
function getPriceAt(uint256 timestamp) external view returns (uint256);
/// @notice Update Chainlink feed addresses (GOVERNANCE only)
function updateChainlinkFeeds(
address feed1,
address feed2,
address feed3
) external;
/// @notice Update committee signer (GOVERNANCE only)
function updateCommitteeSigner(address newSigner) external;
/// @notice Emergency price update (CIRCUIT_BREAKER only)
function emergencyPriceUpdate(uint256 navPerToken) external;
/// @notice Events
event PriceCommitted(address indexed committer, bytes32 commitHash);
event PriceRevealed(address indexed revealer, uint256 price);
event PriceFinalized(uint256 navPerToken, uint256 timestamp);
event PriceEmergencyUpdated(uint256 navPerToken);
event FeedUpdated(uint8 indexed feedIndex, address newFeedAddress);
event CommitteeSignerUpdated(address newSigner);
}
Implementation Details
Medianization Strategy: 1. Source 1-3: Chainlink USDC/USD, EUR/USD, AUD/USD aggregators (3 independent feeds) 2. Weighting: Equal weight (1/3 each) across feeds 3. Aggregation: 2-of-3 median (requires at least 2 valid prices) 4. Committee: Off-chain signer submits custom 5-pillar index (redundancy/emergency path)
Commit-Reveal Scheme:
Week T:
Mon 08:00 UTC: Commitment phase begins
Mon 20:00 UTC: Reveal phase begins
Tue 04:00 UTC: Finalization deadline
Next cycle: Tue 08:00 UTC
Staleness Detection: - Max acceptable age: 25 hours - Action: ESPCircuitBreaker escalates fees, may pause minting - Emergency: Manual override via oracle signer with timelock
Historical Storage: - Stores last 52 weeks of daily prices (52 entries × ~100 bytes = 5.2 KB) - Circular buffer in-contract; offchain indexing via events - Enables dapp calculation of 7d, 30d, 365d moving averages
Storage:
address public chainlinkFeed1; // USDC/USD
address public chainlinkFeed2; // EUR/USD
address public chainlinkFeed3; // AUD/USD
address public committeeSigner;
uint256 public lastPriceUpdate;
uint256 public currentNavPerToken;
mapping(uint256 => PriceUpdate) public priceHistory;
mapping(address => CommitState) public commitStates;
4. ESPCircuitBreaker.sol — Emergency Controls
Pause controls, oracle stall detection, and dynamic fee escalation.
Interface Specification
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IESPCircuitBreaker {
/// @notice Circuit breaker states
enum BreakerState {
NORMAL,
MINT_PAUSED,
FULLY_PAUSED,
EMERGENCY_SHUTDOWN
}
/// @notice Stress configuration
struct StressConfig {
uint256 baseFeeBps; // 5 bps normal
uint256 stressFeeBps; // 15 bps under stress
uint256 volatilityThreshold; // 2% price swing triggers stress
uint256 stalePriceThreshold; // 25 hours
}
/// @notice Get current breaker state
function getState() external view returns (BreakerState);
/// @notice Get current fee multiplier (accounts for stress)
function getCurrentFeeBps() external view returns (uint256);
/// @notice Check if minting is paused
function isMintPaused() external view returns (bool);
/// @notice Check if all operations paused
function isFullyPaused() external view returns (bool);
/// @notice Pause only minting (allow burns/redemptions)
/// @dev Only callable by 3-of-5 Gnosis Safe
function pauseMinting() external;
/// @notice Pause all token operations
/// @dev Only callable by 3-of-5 Gnosis Safe
function pauseAllOperations() external;
/// @notice Resume minting (exits MINT_PAUSED state)
function resumeMinting() external;
/// @notice Resume all operations (exits FULLY_PAUSED state)
function resumeAllOperations() external;
/// @notice Emergency shutdown (irreversible until governance override)
function triggerEmergencyShutdown() external;
/// @notice Check oracle staleness
function isOracleStaledDetected() external view returns (bool);
/// @notice Auto-escalate fees if oracle is stale
function escalateFeesOnOracleStall() external;
/// @notice Check for price volatility (2% swing)
function isVolatilityThresholdExceeded() external view returns (bool);
/// @notice Stress configuration management (GOVERNANCE only)
function updateStressConfig(
uint256 newBaseFeeBps,
uint256 newStressFeeBps,
uint256 newVolatilityThreshold,
uint256 newStalePriceThreshold
) external;
/// @notice Get stress configuration
function getStressConfig() external view returns (StressConfig memory);
/// @notice Get multisig guard
function getMultisig() external view returns (address);
/// @notice Events
event MintPaused(address indexed caller, uint256 timestamp);
event AllOperationsPaused(address indexed caller, uint256 timestamp);
event MintResumed(address indexed caller, uint256 timestamp);
event AllOperationsResumed(address indexed caller, uint256 timestamp);
event EmergencyShutdownTriggered(address indexed caller, uint256 timestamp);
event FeesEscalated(uint256 oldFeeBps, uint256 newFeeBps, string reason);
event OracleStalenessDetected(uint256 lastUpdateTime);
event VolatilityThresholdExceeded(uint256 priceChange);
}
Implementation Details
State Machine:
NORMAL ──pause_mint──> MINT_PAUSED
│ │
│ resume_mint
│ │
│ ▼
└──pause_all──────────> FULLY_PAUSED
│
resume_all
│
▼
NORMAL
Emergency override from any state:
Any state ──trigger_shutdown──> EMERGENCY_SHUTDOWN
(only governance can recover)
Fee Escalation Triggers: 1. Oracle Stale (> 25 hours): Fees escalate from 5bps → 15bps 2. Price Volatility (> 2% swing): Fees escalate from 5bps → 15bps 3. Manual Admin Action: Explicit fee escalation (e.g., after market stress event)
Multisig Guard: - Pause operations require 3-of-5 Gnosis Safe approval - Resume operations require 3-of-5 Gnosis Safe approval - Emergency shutdown can be triggered by oracle signer (with timelock review)
Storage:
BreakerState public currentState = BreakerState.NORMAL;
address public multisigGuard; // 3-of-5 Gnosis Safe
uint256 public lastStateChange;
StressConfig public stressConfig;
5. ESPGovernance.sol — Parameter Management
Timelock-protected governance with role management and emergency override.
Interface Specification
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IESPGovernance {
/// @notice Pending parameter update
struct PendingUpdate {
bytes32 updateType; // keccak256("MINT_CONFIG"), etc.
bytes updateData;
uint256 proposalTime;
uint256 executionTime;
bool executed;
bool canceled;
}
/// @notice Propose parameter update (requires 48h timelock)
function proposeUpdate(
bytes32 updateType,
bytes calldata updateData
) external returns (uint256 updateId);
/// @notice Execute pending update after timelock
function executeUpdate(uint256 updateId) external;
/// @notice Cancel pending update (emergency)
function cancelUpdate(uint256 updateId) external;
/// @notice Get pending update details
function getPendingUpdate(uint256 updateId)
external view returns (PendingUpdate memory);
/// @notice Get timelock duration (48 hours)
function getTimelockDuration() external view returns (uint256);
/// @notice Can execute update?
function canExecute(uint256 updateId) external view returns (bool);
/// @notice Grant role to account (requires timelock)
function proposeGrantRole(bytes32 role, address account)
external returns (uint256 updateId);
/// @notice Revoke role from account (requires timelock)
function proposeRevokeRole(bytes32 role, address account)
external returns (uint256 updateId);
/// @notice Emergency override (4-of-5 multisig only, no timelock)
function emergencyOverride(
bytes32 updateType,
bytes calldata updateData
) external;
/// @notice Get admin multisig
function getAdminMultisig() external view returns (address);
/// @notice Get emergency multisig (4-of-5)
function getEmergencyMultisig() external view returns (address);
/// @notice Events
event UpdateProposed(
uint256 indexed updateId,
bytes32 indexed updateType,
uint256 proposalTime,
uint256 executionTime
);
event UpdateExecuted(uint256 indexed updateId, bytes32 indexed updateType);
event UpdateCanceled(uint256 indexed updateId, bytes32 indexed updateType);
event EmergencyOverrideExecuted(bytes32 indexed updateType);
}
Implementation Details
Timelock Protection: - All parameter changes require 48-hour minimum delay - Proposal includes: updateType, updateData, proposalTime, executionTime - Execution window: [executionTime, executionTime + 2 days] - After 2 days, update expires and must be re-proposed
Update Types: - MINT_CONFIG: Daily cap, per-tx cap, min amount, fees, NAV band - ORACLE_CONFIG: Chainlink feeds, committee signer, staleness threshold - STRESS_CONFIG: Base fees, stress fees, volatility threshold - GRANT_ROLE: Assign role to address - REVOKE_ROLE: Remove role from address - UPGRADE_TOKEN: Authorize new ESPToken implementation
Emergency Override: - 4-of-5 multisig can execute immediately (no timelock) - Use cases: Critical bug discovered, regulatory requirement, oracle failure recovery - All emergency overrides logged and reported to token holders
Storage:
uint256 public constant TIMELOCK_DURATION = 48 hours;
uint256 public updateCounter;
mapping(uint256 => PendingUpdate) public pendingUpdates;
address public adminMultisig; // 3-of-5
address public emergencyMultisig; // 4-of-5
Multi-Chain Strategy
Canonical Bridge Design: Lock-and-Mint Pattern
Architecture:
┌─────────────────────────────────────────────────────────────┐
│ ETHEREUM L1 (Primary) │
│ │
│ ESPToken ◄────────── ESPBridge (Lock) ──────────► │
│ (Balance: 100M) │
└────────────────────────┬──────────────────────────────────────┘
│
┌──────────────┴──────────────┐
│ │
┌─────▼──────────┐ ┌─────────▼──────────┐
│ ARBITRUM │ │ BASE │
│ (Layer 2) │ │ (Layer 2) │
│ │ │ │
│ ◄─ Mint ESP ◄┐ │ │ ◄─ Mint ESP ◄┐ │
│ token │ │ │ token │ │
│ ┌┘ │ │ ┌┘ │
│ (Balance: 50M) │ │ (Balance: 30M) │
└────────────────┘ └───────────────────┘
│ │
│ ┌──────▼─────────┐
│ │ POLYGON │
│ │ (Sidechain) │
│ │ │
└─ ◄─ Mint ESP ◄────────► token │
token relay (Balance: │
20M) │
└────────────────────┘
Deployment Per Chain
Ethereum L1 (Primary): - ESPToken (canonical implementation) - ESPMinter (primary minting authority) - ESPOracle (authoritative price feed) - ESPCircuitBreaker (emergency controls) - ESPGovernance (parameter management) - All governance/multisigs deployed here
Arbitrum One (Layer 2): - ESPToken (bridged copy, locked on L1) - ESPBridgeAdapter (minimal: receive mint messages, emit burn events) - No minting authority (depends on L1) - Oracle price feed relayed from L1 (updated hourly via Chainlink)
Base (Layer 2): - ESPToken (bridged copy, locked on L1) - ESPBridgeAdapter (minimal) - Staking/reward contracts (future expansion)
Polygon (Sidechain): - ESPToken (bridged copy, locked on L1) - ESPBridgeAdapter (minimal) - Native AMM liquidity pools (Uniswap v3)
Cross-Chain Message Verification
Whitelist Bridge Protocols: 1. Ethereum ↔︎ Arbitrum: Arbitrum native bridge (most secure) 2. Ethereum ↔︎ Base: Optimism native bridge 3. Ethereum ↔︎ Polygon: Polygon native bridge (PoS) 4. Fallback: LayerZero (for non-native chain pairs)
Message Verification:
interface IBridgeAdapter {
// Verify message came from authorized bridge
function verifyBridgeMessage(
bytes calldata message,
bytes calldata proof,
uint256 sourceChainId
) external view returns (bool);
// Relay mint authorization from L1
function relayMintAuthorization(
uint256 mintAmount,
address recipient,
bytes calldata l1Signature
) external;
// Emit burn event for L1 settlement
function burnAndNotifyL1(
uint256 amount,
address burner
) external;
}
Chain-Specific Considerations
Ethereum L1: - Gas optimization: Batch settlement on off-peak hours - Storage: On-chain history of all transactions - Multisig: 3-of-5 Gnosis Safe (mainnet)
Arbitrum: - Compressed calldata via L1 batching (lower fees) - Update oracle feed hourly instead of daily (no additional cost) - Multisig: Same 3-of-5 Safe contract (cross-chain execution)
Base: - Optimized for Coinbase integration (KYC easier) - Lower fees → lower mint minimums acceptable - Liquidity AMM incentives from Coinbase
Polygon: - Lower security model (PoS) → lower caps initially (Phase 1: $50M) - Staking/reward contracts for community engagement - Separate staking multisig (2-of-3 higher risk tolerance)
Security Framework
Audit Plan
Phase 1: Pre-Mainnet Audits (Timeline: Months 1-3)
- OpenZeppelin Formal Security Review
- Scope: ESPToken, ESPMinter, ESPOracle, ESPCircuitBreaker
- Focus: Access control, reentrancy, overflow/underflow, upgrade path
- Estimated cost: $75K, Duration: 2 weeks
- Expected completion: Month 1
- Trail of Bits Advanced Security Audit
- Scope: Full contract suite + multi-chain integration
- Focus: Game theory, oracle manipulation, cross-chain message passing
- Estimated cost: $100K, Duration: 3 weeks
- Expected completion: Month 2
- Certora Formal Verification (ESPOracle, ESPMinter)
- Scope: Mathematical correctness of pricing logic and settlement
- Properties to verify:
- Mint output always ≥ (fiatAmount - fiatAmount × 0.15%) / navPerToken
- Redemption output always ≥ (espAmount × navPerToken - espAmount × navPerToken × 0.15%)
- Oracle staleness always detected within 25 hours + 1 block
- Multisig can never be bypassed
- Estimated cost: $50K, Duration: 2 weeks
- Expected completion: Month 2
Phase 2: Testnet Security (Timeline: Months 2-3)
- Deploy to Sepolia, Arbitrum Sepolia, Base Sepolia
- Run 4-week testnet period with token limits ($1M daily cap)
- Security monitoring: Real-time alert system for anomalies
- White-hat bounty: Up to $10K for critical findings
Bug Bounty Program (Immunefi)
Tier Structure:
| Severity | Bounty Range | Examples |
|---|---|---|
| Critical | $50K - $100K | Mint without authorization, oracle manipulation allowing $1M+ fraud, break multisig |
| High | $10K - $50K | Pause bypass, fee calculation error >100bps, reentrancy allowing token theft |
| Medium | $1K - $10K | Oracle feed manipulation (< 1%), parameter escaping timelock |
| Low | $100 - $1K | Precision loss in calculations, event log inconsistencies |
Rules: - Bounties paid in stablecoin (USDC) - Submission via Immunefi (no direct emails) - 90-day responsible disclosure window - KYC required for claims > $5K
Incident Response Plan
Triggers: - Oracle feed down > 2 hours - Unauthorized mint > $100K detected - Multisig key compromise suspected - Smart contract exploit active
Escalation:
L1: Monitoring System (automated alerts)
↓
L2: Incident Lead (responds within 15 min)
↓
L3: Technical Team (investigates, deploys patch if needed)
↓
L4: 3-of-5 Multisig (votes on emergency pause/upgrade within 30 min)
↓
L5: Legal/Regulatory (notifies relevant authorities within 1 hour)
↓
L6: Public Communication (transparency report within 3 hours)
Response Actions (in order): 1. Pause affected operations (30 minutes max) - If oracle: ESPCircuitBreaker escalates fees - If mint leak: ESPCircuitBreaker pauses minting - If contract bug: Full pause via multisig 2. Investigate root cause (24-48 hours) - Blockchain forensics, transaction replay analysis - Third-party auditor on-call (2-hour SLA) 3. Prepare fix (48-72 hours) - Code patch + re-audit (abbreviated) - Test on Sepolia with same parameters 4. Emergency upgrade (if critical) - 4-of-5 multisig emergency override (no timelock) - Implement new implementation, proxy points to it - Maintain state integrity (no fund loss) 5. Reassessment (post-incident) - Full timeline published on governance forum - Lessons learned documented - Preventive measures (upgraded monitoring, additional circuit breakers)
Upgrade Governance
Standard Upgrade Path (48-hour timelock): 1. Propose new implementation contract (fully audited) 2. 3-of-5 multisig approves proposal 3. 48-hour public review period (community challenge) 4. Execute upgrade: UUPS proxy calls upgradeToAndCall(newImpl, data)
Emergency Upgrade Path (no timelock): 1. Critical vulnerability discovered and confirmed by 2 auditors 2. 4-of-5 emergency multisig votes (1-hour window) 3. Immediate execution if 4+ signatures collected 4. Ratification by governance within 7 days (or rollback)
Upgrade Contract Specification:
interface IESPTokenV2 {
// All functions from V1 (backward compatible)
// Plus new functions:
// Data migration function (called during upgrade)
function initializeV2(bytes calldata migrationData) external;
// Version identifier
function version() external pure returns (uint256);
// Must return exact bytecode hash for relay nodes to verify
function implCodeHash() external pure returns (bytes32);
}
Deployment Plan
Phase 1: Testnet Deployment (Sepolia, Week 1-2)
Contracts to Deploy: 1. ESPToken (UUPS proxy) 2. ESPMinter (UUPS proxy) 3. ESPOracle (with mock Chainlink feeds) 4. ESPCircuitBreaker 5. ESPGovernance (with 2-of-3 test multisig) 6. Sepolia Gnosis Safe (3-of-3: 3 test signers)
Initialization Parameters:
// ESPToken
name: "Esperanto Test"
symbol: "ESP-TEST"
decimals: 18
initialAdmin: governance.address
// ESPMinter
dailyMintCap: 1_000_000e18 // 1M tokens
perTxMintCap: 100_000e18 // 100K tokens
minMintAmount: 10_000e18 // 10K tokens
minterFeeBps: 10 // 10 bps initial
navBandBps: 10 // ±10 bps
// ESPOracle
chainlinkFeed1: 0x... // USDC/USD Sepolia
chainlinkFeed2: 0x... // EUR/USD Sepolia
chainlinkFeed3: 0x... // AUD/USD Sepolia
committeeSigner: 0x... // Test signer
// ESPCircuitBreaker
baseFeeBps: 5
stressFeeBps: 15
volatilityThreshold: 200 // 2%
stalePriceThreshold: 90000 // 25 hours
Testnet Activities (4 weeks): - Week 1: Deploy, smoke test, KYC 5 test counterparties - Week 2: Daily test mints/redeems ($10K-$100K range) - Week 3: Stress test (rapid mints, oracle feed interruptions) - Week 4: Circuit breaker tests, emergency pause/resume
Success Criteria: - All transactions confirmed on-chain - No arithmetic overflows/underflows - Multisig guards working correctly - Fee calculations accurate to 1 wei
Phase 2: Guarded Mainnet Launch (Ethereum L1, Week 3-4)
Initial Caps: - Daily mint cap: $10M - Per-transaction mint cap: $1M - Total initial supply cap: $500M (enforced by code)
Counterparties Phase 2 (5 large institutions): - Kraken custody - Fidelity prime brokerage - Bitcoin Suisse OTC - Wintermute market maker - Lido staking (liquidity provider)
Deployment: 1. Deploy production contracts (audited versions) 2. Initialize with same multisig (3-of-5 Gnosis Safe) 3. Activate oracle with live Chainlink feeds 4. Enable minting with $10M daily cap 5. Monitor continuously for 2 weeks
Monitoring Infrastructure: - Real-time price feed validation (±2% from CME) - Mint/redemption latency tracking (target: < 2 minutes) - Smart contract storage variance detection - Multisig transaction alerting (Gnosis Safe integration)
Go/No-Go Decision Points: | Week | Criteria | Action | |——|———-|——–| | 1 | >$100M minted, zero exploits | Increase daily cap to $25M | | 2 | Consistent NAV tracking within ±15bps | Increase to $50M daily cap | | 3 | Oracle uptime >99.9%, fees collected correctly | Full mainnet activation |
Phase 3: Full Mainnet + Multi-Chain Expansion (Week 5+)
Full Mainnet (Ethereum L1): - Remove daily cap (market-driven) - Per-tx cap: $5M - Activate full governance (48-hour timelock)
Multi-Chain Rollout (sequential):
Arbitrum One (Week 6): - Deploy bridged ESPToken - Activate Arbitrum bridge (native) - Initial cap: $100M total bridged - Deploy ESPBridgeAdapter to relay Chainlink prices
Base (Week 7): - Deploy bridged ESPToken - Activate Base bridge - Target: 30% of Arbitrum volume (for balance)
Polygon PoS (Week 8): - Deploy bridged ESPToken - Limited to $20M initial (PoS security model) - Partner with Uniswap v3 for DEX liquidity
Gas Optimization Notes
Solidity Version & Compiler Settings
pragma solidity ^0.8.20;
// solc: --optimize --optimize-runs=200
// (prioritize deployment gas over runtime calls)
Key Optimizations
1. ESPToken Storage Packing
// ❌ Not optimized (4 slots)
mapping(address => uint256) balances; // slot 0
mapping(address => mapping(address => uint256)) allowances; // slot 1
bool paused; // slot 2
mapping(address => bool) blocklist; // slot 3
// ✅ Optimized (3 slots)
mapping(address => uint256) balances; // slot 0
mapping(address => mapping(address => uint256)) allowances; // slot 1
mapping(address => bool) blocklist; // slot 2 (reuse)
bool paused; // packed into slot 3 with future bool
2. ESPMinter Redemption Queue (Array vs Mapping)
// ❌ Array iteration O(n), worse gas as queue grows
RedemptionRequest[] public redemptionQueue;
// ✅ Mapping + counter (O(1) lookup, O(1) removal)
mapping(uint256 => RedemptionRequest) redemptionRequests;
uint256 public requestCounter;
3. ESPOracle Price History (Circular Buffer)
// ❌ Unbounded array (storage grows forever)
PriceUpdate[] public allPriceHistory;
// ✅ Circular buffer (fixed 52 slots = 1 year daily)
uint256 constant HISTORY_SIZE = 52;
mapping(uint256 => PriceUpdate) priceHistory;
uint256 priceHistoryIndex = 0;
function recordPrice(uint256 nav) internal {
priceHistory[priceHistoryIndex] = PriceUpdate({
timestamp: block.timestamp,
navPerToken: nav
});
priceHistoryIndex = (priceHistoryIndex + 1) % HISTORY_SIZE;
}
4. ESPMinter Fee Calculations (Integer Math)
// Avoid division, use proportional math
uint256 constant BPS_DENOMINATOR = 10_000;
// ❌ Imprecise (rounding errors)
uint256 feeAmount = (espAmount * feeBps) / BPS_DENOMINATOR;
// ✅ Precise (ceiling rounding, no loss)
uint256 feeAmount = (espAmount * feeBps + BPS_DENOMINATOR - 1) / BPS_DENOMINATOR;
5. Multi-call Batch Operations
// ❌ Separate calls (N × SLOAD + SSTORE overhead)
for (uint256 i = 0; i < users.length; i++) {
token.burn(users[i], amounts[i]); // 2 SSTOREs each
}
// ✅ Single batch (1 × SLOAD + 1 × SSTORE)
interface IMulticall {
function batchBurn(address[] users, uint256[] amounts) external;
}
Gas Cost Estimates
| Operation | Gas | Notes |
|---|---|---|
| Mint (request + execute) | 180K | Includes oracle read, settlement delay |
| Burn | 40K | Standard ERC-20 |
| Transfer | 65K | With blocklist check |
| Oracle price update (finalize) | 120K | 2-of-3 median calculation |
| Pause (multisig) | 3K | Just state change; multisig overhead ~50K separate |
| Emergency shutdown | 35K | State machine transition + event logging |
Layer 2 Optimizations
Arbitrum calldata compression: - Use BLS signatures for batch rollups (~100 bytes → 20 bytes) - Estimate: Arbitrum gas ~10x cheaper than Ethereum L1
Base calldata: - Similar to Arbitrum - Expected: $0.05 per transaction
Polygon native: - PoS validators (centralized), lower gas - Expected: $0.01 per transaction
Appendix: Role Management Matrix
| Role | Contract | Granted To | Permissions |
|---|---|---|---|
| DEFAULT_ADMIN | ESPToken | ESPGovernance | Can grant/revoke all roles |
| MINTER_ROLE | ESPToken | ESPMinter | Can call mint() |
| PAUSER_ROLE | ESPToken | ESPCircuitBreaker | Can call pause()/unpause() |
| BLOCKLIST_MANAGER | ESPToken | Admin Multisig | Can add/remove blocklist addresses |
| UPGRADER_ROLE | ESPToken | ESPGovernance | Can authorize proxy upgrades |
| CIRCUIT_BREAKER_ADMIN | ESPCircuitBreaker | 3-of-5 Gnosis Safe | Can trigger pause/resume |
| ORACLE_SIGNER | ESPOracle | Committee members (3) | Can commit/reveal prices |
| GOVERNANCE_PROPOSER | ESPGovernance | 3-of-5 Gnosis Safe | Can propose parameter updates |
| GOVERNANCE_EXECUTOR | ESPGovernance | Timelock contract | Can execute after 48h delay |
Summary
This document specifies a production-ready smart contract architecture for the Esperanto Stablecoin (ESP), a MiCAR-compliant Asset-Referenced Token. The system balances decentralization (DAOs, multisigs) with regulatory compliance (blocklists, pausability) through carefully engineered access controls, time-locked governance, and redundant oracle feeds.
Key design principles: - Safety First: Multiple circuit breakers, oracle staleness detection, emergency pause paths - Transparency: All state changes emit events; on-chain settlement history - Auditability: UUPS proxy pattern allows coordinated upgrades; formal verification for core logic - Scalability: Multi-chain expansion via canonical bridges; low gas via circular buffers and batch operations
The deployment plan progresses through testnet validation, guarded mainnet launch with conservative caps, and full multi-chain expansion. Security is layered: code audits, formal verification, bug bounties, and incident response procedures.
Next Steps: 1. Auditor selection (OpenZeppelin, Trail of Bits, Certora) 2. Testnet deployment and 4-week validation period 3. Mainnet launch with $10M daily cap 4. Progressive expansion to $500M supply across 4 chains
Document License: Creative Commons Attribution 4.0 International (CC BY 4.0)