diff --git a/.gitignore b/.gitignore index 7ae53958..60cfd6ea 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,6 @@ src/pages.gen.ts *.sw? .env .vercel -.vocs \ No newline at end of file +.vocs + +src/pages/protocol/tips/tip-* diff --git a/src/pages/protocol/tips/tip-1000.mdx b/src/pages/protocol/tips/tip-1000.mdx deleted file mode 100644 index 72233b64..00000000 --- a/src/pages/protocol/tips/tip-1000.mdx +++ /dev/null @@ -1,335 +0,0 @@ ---- -id: TIP-1000 -title: State Creation Cost Increase -description: Increased gas costs for state creation operations to protect Tempo from adversarial state growth attacks. -authors: Dankrad Feist @dankrad -status: Approved ---- - -# TIP-1000: State Creation Cost Increase - -- **Protocol Version**: T1 - -## Abstract - -This TIP increases the gas cost for creating new state elements, accounts, and contract code to provide economic protection against state growth spam attacks. The proposal increases the cost of writing a new state element from 20,000 gas to 250,000 gas, introduces a 250,000 gas charge for account creation (when the account's nonce is first written), and implements a new contract creation cost model: 1,000 gas per byte of contract code plus 500,000 gas for keccak hash and codesize fields. - -## Motivation - -Tempo's high throughput capability (approximately 20,000 transactions per second) creates a vulnerability where an adversary could create a massive amount of state with the intent of permanently slowing the chain down. If each transaction is used to create a new account, and each account requires approximately 200 bytes of storage, then over 120 TB of storage could be created in a single year. Even if this storage is technically feasible, the database performance implications are unknown and would likely require significant R&D on state management much earlier than needed for business requirements. - -The current EVM gas schedule charges 20,000 gas for writing a new state element and has no cost for creating an account. This makes state creation attacks economically viable for adversaries. By increasing these costs to 250,000 gas each, we create a meaningful economic barrier: creating 1 TB of state would cost approximately $50 million, and creating 10 TB would cost approximately $500 million (based on the assumption that a TIP-20 transfer costs 50,000 gas = 0.1 cent, implying 1 cent per 500,000 gas). - -### Alternatives Considered - -1. **Storage rent**: Implementing a periodic fee for holding state. This was rejected due to complexity and poor user experience. -2. **State expiry**: Automatically removing unused state after a time period. This was rejected due to technical complexity and breaking changes to existing applications. -3. **Lower cost increases**: Using smaller multipliers (e.g., 50,000 gas instead of 250,000 gas). This was rejected as it would not provide sufficient economic deterrent against well-funded attackers. - ---- - -# Specification - -## Gas Cost Changes - -### New State Element Creation - -**Current Behavior:** -- Writing a new state element (SSTORE to a zero slot) costs 20,000 gas - -**Proposed Behavior:** -- Writing a new state element (SSTORE to a zero slot) costs 250,000 gas - -This applies to all storage slot writes that transition from zero to non-zero, including: -- Contract storage slots -- TIP-20 token balances -- Nonce key storage in the Nonce precompile (when a new nonce key is first used) -- Rewards-related storage (userRewardInfo mappings, reward balances) -- Active key count tracking in the Nonce precompile -- Any other state elements stored in the EVM state trie - -**Note:** Since Tempo-specific operations (nonce keys, rewards processing, etc.) ultimately use EVM storage operations (SSTORE), they are automatically subject to the new state creation pricing. The implementation must ensure all new state element creation is correctly charged at 250,000 gas, regardless of which precompile or contract creates the state. - -### Account Creation - -**Current Behavior:** -- Account creation has no explicit gas cost -- The account is created implicitly when its nonce is first written - -**Proposed Behavior:** -- Account creation incurs a 250,000 gas charge when the account's nonce is first written -- This charge applies when the account is first used (e.g., sends its first transaction), not when it first receives tokens - -**Implementation Details:** -- The charge is applied when `account.nonce` transitions from 0 to 1 -- The charge also applies to other nonces with [nonce keys](/protocol/transactions/spec-tempo-transaction#specification) (2D nonces) -- Transactions with a nonce value of 0 need to supply at least 271,000 gas and are otherwise invalid -- For EOA accounts: charged on the first transaction sent from that address (when the account is first used) -- For contract accounts: charged when the contract is deployed (CREATE or CREATE2) -- **Important:** When tokens are transferred TO a new address, the recipient's nonce remains 0, so no account creation cost is charged. The account creation cost only applies when the account is first used (sends a transaction). -- The charge is in addition to any other gas costs for the transaction - -### Contract Creation - -**Current Behavior:** -- Contract creation (CREATE/CREATE2) has a base cost of 32,000 gas plus 200 gas per byte of contract code -- Total cost formula: `32,000 + (code_size × 200)` gas -- Example: A 1,000 byte contract costs 32,000 + (1,000 × 200) = 232,000 gas - -**Proposed Behavior:** -- Contract creation replaces the existing EVM per-byte cost with a new pricing model: - - Each byte: 1,000 gas per byte (linear pricing) - - TX create cost: 2 × 250,000 gas = 500,000 gas for keccak hash and nonce fields -- This pricing applies to the contract code size (the bytecode being deployed) - -**Implementation Details:** -- The code storage cost is calculated as: `code_size × 1,000` -- Additional state creation costs: 2 × 250,000 gas = 500,000 gas for keccak hash and codesize fields -- Total contract creation cost: `(code_size × 1,000) + 500,000` gas -- This replaces the existing EVM per-byte cost for contract creation (not an additional charge) -- Applies to both CREATE and CREATE2 operations -- The account creation cost (250,000 gas) is separate and still applies when the contract account's nonce transitions from 0 to 1 - -### Intrinsic transaction gas - -A transaction is invalid if the minimal costs of a (reverting) transaction can't be covered by caller's balance. Those checks are done in the transaction pool as a DOS prevention measure as well as when a transaction is first executed as part of a block. - -* Transaction with `nonce == 0` require an additional 250,000 gas -* Tempo transactions with any `nonce_key` and `nonce == 0` require an additional 250,000 gas -* Changes to EIP-7702 authorization lists: - * EIP-7702 authorisation list entries with `auth_list.nonce == 0` require an additional 250,000 gas. - * The base cost per authorization is reduced to 12,500 gas - * There is no refund if the account already exists -* The additional initial cost for CREATE transactions that deploy a contract is increased to 500,000 from currently 32,000 (to reflect the upfront cost in contract creation) - * If the first transaction in a batch is a CREATE transaction, the additional cost of 500,000 needs to be charged - - -### Other changes - -The transaction gas cap is changed from 16M to 30M to accommodate the deployment of 24kb contracts. - -Tempo transaction key authorisations can't determine whether it is going to create new storage or not. If the transaction cannot pay for the key authorization storage costs, the transaction reverts any authorization key that has been set. - -## Gas Schedule Summary - -| Operation | Current Gas Cost | Proposed Gas Cost | Change | -|-----------|------------------|-------------------|--------| -| New state element (SSTORE zero → non-zero) | 20,000 | 250,000 | +230,000 | -| Account creation (first nonce write) | 0 | 250,000 | +250,000 | -| Contract creation (per byte) | 200 | 1,000 | +800 | -| Contract creation (keccak + codesize fields) | Included in base | 500,000 | +500,000 | -| Existing state element (SSTORE non-zero → non-zero) | 5,000 | 5,000 | No change | -| Existing state element (SSTORE non-zero → zero) | -15,000 (refund) | -15,000 (refund) | No change | - -## Economic Impact Analysis - -### Cost Calculations - -Based on the assumptions: -- TIP-20 transfer cost (to existing address, including base transaction and state update): 50,000 gas = 0.1 cent -- Implied gas price: 1 cent per 500,000 gas - -**New State Element Creation:** -- Gas cost: 250,000 gas -- Dollar cost: 250,000 / 500,000 = **0.5 cents per state element** - -**Account Creation:** -- Gas cost: 250,000 gas -- Dollar cost: 250,000 / 500,000 = **0.5 cents per account** - -**Contract Creation:** -- Per byte: 1,000 gas = **0.002 cents per byte** -- Keccak + codesize fields: 500,000 gas (2 × 250,000) = **1.0 cent** -- Example: 1,000 byte contract = (1,000 × 1,000) + 500,000 = 1,500,000 gas = **3.0 cents** - -### Attack Cost Analysis - -**Creating 1 TB of state:** -- 1 TB = 1,000,000,000,000 bytes -- Assuming ~100 bytes per state element: 10,000,000,000 state elements -- Cost: 10,000,000,000 × 0.5 cents = **$50,000,000** - -**Creating 10 TB of state:** -- 10 TB = 10,000,000,000,000 bytes -- Assuming ~100 bytes per state element: 100,000,000,000 state elements -- Cost: 100,000,000,000 × 0.5 cents = **$500,000,000** - -These costs serve as a significant economic deterrent against state growth spam attacks. - -## Impact on Normal Operations - -### Transfer to New Address - -**Current Cost:** -- TIP-20 transfer (base + operation): 50,000 gas -- New state element (balance): 20,000 gas -- **Total: ~70,000 gas ≈ 0.14 cents** -- Note: Account creation cost does not apply here because the recipient's nonce remains 0 - -**Proposed Cost:** -- TIP-20 transfer (base + operation): 50,000 gas -- New state element (balance): 250,000 gas -- **Total: ~300,000 gas ≈ 0.6 cents** -- Note: Account creation cost does not apply here because the recipient's nonce remains 0 - -**Impact:** A transfer to a new address increases from 0.14 cents to 0.6 cents, representing a 4.3x increase. The account creation cost (0.5 cents) will be charged separately when the recipient first uses their account. - -### First Use of New Account - -**Current Cost:** -- TIP-20 transfer (base + operation + state update): 50,000 gas -- Account creation: 0 gas -- **Total: 50,000 gas ≈ 0.1 cents** - -**Proposed Cost:** -- TIP-20 transfer (base + operation + state update): 50,000 gas -- Account creation (nonce 0 → 1): 250,000 gas -- **Total: ~300,000 gas ≈ 0.6 cents** - -**Impact:** The first transaction from a new account increases from 0.1 cents to 0.6 cents, representing a 6x increase. Combined with the initial transfer cost (0.6 cents), the total onboarding cost for a new user is approximately 1.2 cents. - -### Transfer to Existing Address - -**Current Cost:** -- TIP-20 transfer (base + operation + state update): 50,000 gas -- **Total: 50,000 gas ≈ 0.1 cents** - -**Proposed Cost:** -- TIP-20 transfer (base + operation + state update): 50,000 gas -- **Total: 50,000 gas ≈ 0.1 cents** - -**Impact:** No change for transfers to existing addresses. - -### Contract Deployment - -**Current Cost:** -- Contract code storage: 32,000 gas base + 200 gas per byte -- Example for 1,000 byte contract: 32,000 + (1,000 × 200) = 232,000 gas ≈ 0.46 cents - -**Proposed Cost:** -- Account creation: 250,000 gas -- Contract code storage: code_size × 1,000 gas -- Keccak + codesize fields: 500,000 gas (2 × 250,000) -- Example for 1,000 byte contract: 250,000 + (1,000 × 1,000) + 500,000 = 1,750,000 gas ≈ **3.5 cents** - -**Impact:** Contract deployment costs increase significantly, especially for larger contracts. A 100 byte contract costs (100 × 1,000) + 500,000 + 250,000 = 850,000 gas = 1.7 cents total. - -## Implementation Requirements - -### Node Implementation - -The node implementation must: - -1. **Detect new state element creation:** - - Track SSTORE operations that write to a zero slot - - Charge 250,000 gas instead of 20,000 gas for these operations - -2. **Detect account creation:** - - Track when an account's nonce transitions from 0 to 1 - - Charge 250,000 gas for this transition - - Apply to both EOA and contract account creation - -3. **Implement contract creation pricing:** - - Replace existing EVM per-byte cost for contract code storage - - Charge 1,000 gas per byte of contract code (linear pricing) - - Charge 500,000 gas (2 × 250,000) for keccak hash and codesize fields - - Total formula: `(code_size × 1,000) + 500,000` - - Apply to both CREATE and CREATE2 operations - -4. **Maintain backward compatibility:** - - Existing state operations (non-zero to non-zero, non-zero to zero) remain unchanged - - Gas refunds for storage clearing remain unchanged - -### Test Suite Requirements - -The test suite must verify: - -1. **New state element creation:** - - SSTORE to zero slot charges 250,000 gas - - Multiple new state elements in one transaction are each charged 250,000 gas - - Existing state element updates (non-zero to non-zero) remain at 5,000 gas - -2. **Account creation:** - - First transaction from EOA charges 250,000 gas for account creation (when nonce transitions 0 → 1) - - Contract deployment (CREATE) charges 250,000 gas for account creation - - Contract deployment (CREATE2) charges 250,000 gas for account creation - - Transfer TO a new address does NOT charge account creation fee (recipient's nonce remains 0) - - Subsequent transactions from the same account do not charge account creation fee - -3. **Contract creation:** - - Contract code storage replaces EVM per-byte cost with new pricing model - - Each byte of contract code costs 1,000 gas (linear pricing) - - Keccak hash and codesize fields cost 500,000 gas (2 × 250,000) total - - Total cost formula: `(code_size × 1,000) + 500,000` gas - - Example: 100 byte contract costs (100 × 1,000) + 500,000 = 600,000 gas - - Both CREATE and CREATE2 use the same pricing - -4. **Tempo-specific state creation operations:** - - Nonce key creation: First use of a new nonce key (nonce key > 0) creates storage in Nonce precompile - - Active key count tracking: First nonce key for an account creates active key count storage - - Rewards opt-in: `setRewardRecipient` creates new `userRewardInfo` mapping entry - - Rewards recipient delegation: Setting reward recipient for a new recipient creates storage - - Rewards balance creation: First reward accrual to a recipient creates storage if needed - - All Tempo-specific operations that create new state elements must charge 250,000 gas per new storage slot - -5. **Edge cases:** - - Self-destruct and recreation of account - - Contracts that create accounts via CREATE/CREATE2 - - Batch operations creating multiple accounts/state elements - - Contract deployment with various code sizes (small, medium, large) - - Multiple Tempo-specific operations in a single transaction - -6. **Economic calculations:** - - Verify gas costs match expected dollar amounts - - Verify attack cost calculations for large-scale state creation - - Verify contract creation costs match formula: `(code_size × 1,000) + 500,000 + 250,000` (including account creation) - - Verify Tempo-specific operations charge correctly for new state creation - ---- - -# Invariants - -The following invariants must always hold: - -1. **State Creation Cost Invariant:** Any SSTORE operation that writes a non-zero value to a zero slot MUST charge exactly 250,000 gas (not 20,000 gas). - -2. **Account Creation Cost Invariant:** The first transaction that causes an account's nonce to transition from 0 to 1 MUST charge exactly 250,000 gas for account creation. - -3. **Existing State Invariant:** SSTORE operations that modify existing non-zero state (non-zero to non-zero) MUST continue to charge 5,000 gas and MUST NOT be affected by this change. - -4. **Storage Clearing Invariant:** SSTORE operations that clear storage (non-zero to zero) MUST continue to provide a 15,000 gas refund and MUST NOT be affected by this change. - -5. **Gas Accounting Invariant:** The total gas charged for a transaction creating N new state elements and M new accounts (where M is the number of accounts whose nonce transitions from 0 to 1 in this transaction) MUST equal: base_transaction_gas + operation_gas + (N × 250,000) + (M × 250,000). Note: Transferring tokens TO a new address does not create the account (nonce remains 0), so M = 0 in that case. - -6. **Contract Creation Cost Invariant:** Contract creation (CREATE/CREATE2) MUST charge exactly `(code_size × 1,000) + 500,000` gas for code storage, replacing the existing EVM per-byte cost. This includes: 1,000 gas per byte of contract code (linear pricing) and 500,000 gas (2 × 250,000) for keccak hash and codesize fields. The account creation cost (250,000 gas) is charged separately. - -7. **Economic Deterrent Invariant:** The cost to create 1 TB of state MUST be at least $50 million, and the cost to create 10 TB of state MUST be at least $500 million, based on the assumed gas price of 1 cent per 500,000 gas. - -## Critical Test Cases - -The test suite must cover: - -1. **Basic state creation:** Single SSTORE to zero slot charges 250,000 gas -2. **Multiple state creation:** Multiple SSTORE operations to zero slots each charge 250,000 gas -3. **Account creation (EOA):** First transaction from new EOA charges 250,000 gas -4. **Account creation (CREATE):** Contract deployment via CREATE charges 250,000 gas for account creation -5. **Account creation (CREATE2):** Contract deployment via CREATE2 charges 250,000 gas for account creation -6. **Contract creation (small):** Contract with 100 bytes charges (100 × 1,000) + 500,000 = 600,000 gas for code storage -7. **Contract creation (medium):** Contract with 1,000 bytes charges (1,000 × 1,000) + 500,000 = 1,500,000 gas for code storage -8. **Contract creation (large):** Contract with 10,000 bytes charges (10,000 × 1,000) + 500,000 = 10,500,000 gas for code storage -9. **Existing state updates:** SSTORE to existing non-zero slot charges 5,000 gas (unchanged) -10. **Storage clearing:** SSTORE clearing storage provides 15,000 gas refund (unchanged) -11. **Mixed operations:** Transaction creating both new accounts and new state elements charges correctly for both -12. **Transfer to new address:** Complete transaction cost matches expected ~300,000 gas (no account creation cost, only new state element cost) -13. **First use of new account:** Complete transaction cost matches expected ~300,000 gas (account creation cost applies) -14. **Transfer to existing address:** Complete transaction cost matches expected 50,000 gas (unchanged) -15. **Batch operations:** Multiple account creations in one transaction each charge 250,000 gas -16. **Self-destruct and recreate:** Account that self-destructs and is recreated charges account creation fee again -17. **Transfer to new address does not create account:** Transferring tokens to a new address does not charge account creation fee (only new state element fee applies) -18. **Nonce key creation:** First use of a new nonce key creates a new storage slot and charges 250,000 gas -19. **Active key count tracking:** First nonce key for an account creates storage for active key count and charges 250,000 gas -20. **Rewards opt-in:** First call to `setRewardRecipient` creates a new entry and charges 250,000 gas -21. **Rewards recipient delegation:** Setting a new reward recipient creates storage and charges 250,000 gas -22. **Rewards balance creation:** First reward accrual creates storage and charges 250,000 gas (if needed) -23. **Multiple nonce keys:** Creating multiple nonce keys in one transaction each charges 250,000 gas -24. **Nonce key and rewards combined:** Transaction creating both nonce key and rewards storage charges 250,000 gas for each new state element diff --git a/src/pages/protocol/tips/tip-1001.mdx b/src/pages/protocol/tips/tip-1001.mdx deleted file mode 100644 index 9a3c0934..00000000 --- a/src/pages/protocol/tips/tip-1001.mdx +++ /dev/null @@ -1,114 +0,0 @@ ---- -id: TIP-1001 -title: Place-only mode for next quote token -description: A new DEX function for creating trading pairs against a token's staged next quote token, to allow orders to be placed on it. -authors: Dan Robinson -status: Draft ---- - -# TIP-1001: Place-only mode for next quote token - -## Abstract - -This TIP adds a `createNextPair` function to the Stablecoin DEX that creates a trading pair between a base token and its `nextQuoteToken()`, along with `place` and `placeFlip` overloads that accept a book key to target specific pairs. This enables market makers to place orders on the new pair before a quote token update is finalized, providing a smooth liquidity transition. - -## Motivation - -When a token issuer decides to change their quote token (via `setNextQuoteToken` and `completeQuoteTokenUpdate`), there is currently no way to establish liquidity on the new pair before the transition completes. This means that market makers will need to wait until the quote token has been updated before they can place orders, which could cause a period where there is no liquidity, or limited liquidity, for the token, which will interrupt swaps involving that token. - -By allowing pair creation against `nextQuoteToken()`, this change allows users and market makers to add liquidity to the DEX before it is used on swaps. Since swaps route through `quoteToken()` (not `nextQuoteToken()`), the new pair operates in "place-only" mode: orders can be placed and cancelled, but no swaps route through it until `completeQuoteTokenUpdate()` is called. - ---- - -# Specification - -## New functions - -Add the following functions to the Stablecoin DEX interface: - -```solidity -/// @notice Creates a trading pair between a base token and its next quote token -/// @param base The base token address -/// @return key The pair key for the created pair -/// @dev Reverts if: -/// - The base token has no next quote token staged (nextQuoteToken is zero) -/// - The pair already exists -/// - Either token is not USD-denominated -function createNextPair(address base) external returns (bytes32 key); - -/// @notice Places an order on a specific pair identified by book key -/// @param bookKey The pair key identifying the orderbook -/// @param token The base token of the pair -/// @param amount The order amount in base tokens -/// @param isBid True for buy orders, false for sell orders -/// @param tick The price tick for the order -/// @return orderId The ID of the placed order -function place(bytes32 bookKey, address token, uint128 amount, bool isBid, int16 tick) external returns (uint128 orderId); - -/// @notice Places a flip order on a specific pair identified by book key -/// @param bookKey The pair key identifying the orderbook -/// @param token The base token of the pair -/// @param amount The order amount in base tokens -/// @param isBid True for buy orders, false for sell orders -/// @param tick The price tick for the order -/// @param flipTick The price tick for the flipped order when filled -/// @param internalBalanceOnly If true, only use internal balance for the flipped order -/// @return orderId The ID of the placed order -function placeFlip(bytes32 bookKey, address token, uint128 amount, bool isBid, int16 tick, int16 flipTick, bool internalBalanceOnly) external returns (uint128 orderId); -``` - -## Behavior - -### Pair creation - -`createNextPair(base)` creates a pair between `base` and `base.nextQuoteToken()`. The function: - -1. Calls `nextQuoteToken()` on the base token -2. Reverts with `NO_NEXT_QUOTE_TOKEN` if the result is `address(0)` -3. Validates both tokens are USD-denominated (same as `createPair`) -4. Creates the pair using the same mechanism as `createPair` -5. Emits `PairCreated(key, base, nextQuoteToken)` - -### Place-only mode - -Once the pair exists, it supports the full order lifecycle: - -- `place(bookKey, ...)` and `placeFlip(bookKey, ...)` allow placing orders on the pair -- `cancel` and `cancelStaleOrder` work normally (they use order ID, not pair lookup) -- `books` returns accurate data (it takes the book key directly) - -The new `place` and `placeFlip` overloads are required because the existing functions derive the pair from `token.quoteToken()`, which would look up the wrong pair. The overloads accept a `bookKey` parameter to target the correct pair. - -Swap functions (`swapExactAmountIn`, `swapExactAmountOut`) and quote functions (`quoteSwapExactAmountIn`, `quoteSwapExactAmountOut`) do not route through this pair because routing uses `quoteToken()` to find paths between tokens. - -### After quote token update - -When the token issuer calls `completeQuoteTokenUpdate()`: - -1. The token's `quoteToken()` changes to what was `nextQuoteToken()` -2. The token's `nextQuoteToken()` becomes `address(0)` -3. The existing pair (created via `createNextPair`) is now the active pair -4. Swaps begin routing through the pair - -The old pair (against the previous quote token) remains but will no longer be used for routing swaps involving this base token. Orders on it can be canceled using their ID. - -## New error - -```solidity -/// @notice The base token has no next quote token staged -error NO_NEXT_QUOTE_TOKEN(); -``` - -## Events - -No new events. The existing `PairCreated` event is emitted by `createNextPair`, and the existing `OrderPlaced` event is emitted by the `place` and `placeFlip` overloads. - ---- - -# Invariants - -- A pair created via `createNextPair` must be identical to one created via `createPair` once `completeQuoteTokenUpdate` is called -- `createNextPair` must revert if `nextQuoteToken()` returns `address(0)` -- `createNextPair` must revert if the pair already exists (same as `createPair`) -- Orders placed on a next-quote-token pair must be executable via swaps after the quote token update completes -- Swap routing must not change until `completeQuoteTokenUpdate` is called on the base token diff --git a/src/pages/protocol/tips/tip-1002.mdx b/src/pages/protocol/tips/tip-1002.mdx deleted file mode 100644 index 034ab91c..00000000 --- a/src/pages/protocol/tips/tip-1002.mdx +++ /dev/null @@ -1,83 +0,0 @@ ---- -id: TIP-1002 -title: Prevent crossed orders and allow same-tick flip orders -description: Changes to the Stablecoin DEX that prevent placing orders that would cross existing orders on the opposite side of the book, and allow flip orders to flip to the same tick. -authors: Dan Robinson -status: Draft ---- - -# TIP-1002: Prevent crossed orders and allow same-tick flip orders - -## Abstract - -This TIP makes two related changes to the Stablecoin DEX: - -1. **Prevent crossed orders**: Modify `place` and `placeFlip` to reject orders that would cross existing orders on the opposite side of the book. An order "crosses" when a bid is placed at a tick higher than the best ask, or an ask is placed at a tick lower than the best bid. - -2. **Allow same-tick flip orders**: Relax the `placeFlip` validation to allow `flipTick` to equal `tick`, enabling flip orders that flip to the same price. - -## Motivation - -### Preventing crossed orders - -Currently, the Stablecoin DEX allows orders to be placed at any valid tick, even if they would cross existing orders. Since matching only occurs during swaps (not during order placement), crossed orders can accumulate in the order book. This is unusual behavior and could confuse market makers who are accustomed to books that do not allow crossing. - -By preventing crossed orders at placement time, the order book maintains a clean invariant: `best_bid_tick <= best_ask_tick`. - -### Allowing same-tick flip orders - -Currently, `placeFlip` requires `flipTick` to be strictly on the opposite side of `tick` (e.g., for a bid, `flipTick > tick`). This prevents use cases like instant token convertibility, where an issuer wants to place flip orders on both sides at the same tick to create a stable two-sided market that automatically replenishes when orders are filled. - ---- - -# Specification - -## Modified behavior - -The `place` and `placeFlip` functions (including the `bookKey` overloads from TIP-1001) are modified to check for crossing before accepting an order: - -- **For bids**: Revert if `tick > best_ask_tick` (when `best_ask_tick` exists) -- **For asks**: Revert if `tick < best_bid_tick` (when `best_bid_tick` exists) - -### Same-tick orders - -Orders at the same tick as the best order on the opposite side are **allowed**. This means: - -- A bid at `tick == best_ask_tick` is allowed -- An ask at `tick == best_bid_tick` is allowed - -While this is non-standard behavior for most order books (which would immediately match same-tick orders), it is intentionally permitted to support flip orders that flip to the same tick (see below). - -## Same-tick flip orders - -The `placeFlip` validation is relaxed to allow `flipTick == tick`: - -- **Current behavior**: For bids, `flipTick > tick` required; for asks, `flipTick < tick` required -- **New behavior**: For bids, `flipTick >= tick` required; for asks, `flipTick <= tick` required - -This enables use cases like instant token convertibility, where an issuer places flip orders on both sides at the same tick to create a stable two-sided market that automatically replenishes when orders are filled. - -## Interaction with TIP-1001 - -If TIP-1001 is accepted, the crossing check only applies when the pair is **active**—that is, when the pair's quote token equals the base token's current `quoteToken()`. - -For pairs created via `createNextPair` (where the quote token is the base token's `nextQuoteToken()`), the crossing check is skipped. This allows orders to accumulate freely during "place-only mode" before the quote token update is finalized. Such orders would likely be arbitraged nearly instantly once the pair launches, but this prevents someone from causing a denial-of-service to one side of the book by placing an extremely aggressive order on the other side. - -## New error - -```solidity -/// @notice The order would cross existing orders on the opposite side -error ORDER_WOULD_CROSS(); -``` - -## Events - -No new events. - ---- - -# Invariants - -- On active pairs, `best_bid_tick <= best_ask_tick` after any successful `place` or `placeFlip` call -- On inactive pairs (per TIP-1001), no crossing check is enforced -- Flip orders may create orders at the same tick as the opposite side, potentially resulting in `best_bid_tick == best_ask_tick` diff --git a/src/pages/protocol/tips/tip-1003.mdx b/src/pages/protocol/tips/tip-1003.mdx deleted file mode 100644 index 801bafe6..00000000 --- a/src/pages/protocol/tips/tip-1003.mdx +++ /dev/null @@ -1,174 +0,0 @@ ---- -id: TIP-1003 -title: Client order IDs -description: Addition of client order IDs to the Stablecoin DEX, allowing users to specify their own order identifiers for idempotency and easier order tracking. -authors: Dan Robinson -status: Draft ---- - -# TIP-1003: Client order IDs - -## Abstract - -This TIP adds support for optional client order IDs (`clientOrderId`) to the Stablecoin DEX. Users can specify a `uint128` identifier when placing orders, which serves as an idempotency key and a predictable handle for the order. The system-generated `orderId` is not predictable before transaction execution, making client order IDs useful for order management. - -## Motivation - -Traditional exchanges allow users to specify a client order ID (called `ClOrdID` in FIX protocol, `cloid` in Hyperliquid) for several reasons: - -1. **Idempotency**: If a transaction is submitted twice (e.g., due to network issues), the duplicate can be detected and rejected -2. **Predictable reference**: Users know the order identifier before the transaction confirms, enabling them to prepare cancel requests or track orders without waiting for confirmation -3. **Integration**: External systems can use their own ID schemes to correlate orders - ---- - -# Specification - -## New storage - -A new mapping tracks active client order IDs per user: - -```solidity -mapping(address user => mapping(uint128 clientOrderId => uint128 orderId)) public clientOrderIds; -``` - -## Modified functions - -All order placement functions gain an optional `clientOrderId` parameter: - -```solidity -/// @notice Places an order with an optional client order ID -/// @param token The base token of the pair -/// @param amount The order amount in base tokens -/// @param isBid True for buy orders, false for sell orders -/// @param tick The price tick for the order -/// @param clientOrderId Optional client-specified ID (0 for none) -/// @return orderId The system-assigned order ID -function place( - address token, - uint128 amount, - bool isBid, - int16 tick, - uint128 clientOrderId -) external returns (uint128 orderId); - -/// @notice Places an order on a specific pair with an optional client order ID -/// @dev Overload from TIP-1001 -function place( - bytes32 bookKey, - address token, - uint128 amount, - bool isBid, - int16 tick, - uint128 clientOrderId -) external returns (uint128 orderId); - -/// @notice Places a flip order with an optional client order ID -function placeFlip( - address token, - uint128 amount, - bool isBid, - int16 tick, - int16 flipTick, - bool internalBalanceOnly, - uint128 clientOrderId -) external returns (uint128 orderId); - -/// @notice Places a flip order on a specific pair with an optional client order ID -/// @dev Overload from TIP-1001 -function placeFlip( - bytes32 bookKey, - address token, - uint128 amount, - bool isBid, - int16 tick, - int16 flipTick, - bool internalBalanceOnly, - uint128 clientOrderId -) external returns (uint128 orderId); -``` - -## New functions - -```solidity -/// @notice Cancels an order by its client order ID -/// @param clientOrderId The client-specified order ID -function cancelByClientOrderId(uint128 clientOrderId) external; - -/// @notice Gets the system order ID for a client order ID -/// @param user The user who placed the order -/// @param clientOrderId The client-specified order ID -/// @return orderId The system-assigned order ID, or 0 if not found -function getOrderByClientOrderId(address user, uint128 clientOrderId) external view returns (uint128 orderId); -``` - -## Behavior - -### Placing orders with clientOrderId - -When `clientOrderId` is non-zero: - -1. Check if `clientOrderIds[msg.sender][clientOrderId]` maps to an active order -2. If it does, revert with `DUPLICATE_CLIENT_ORDER_ID` -3. Otherwise, proceed with order placement and set `clientOrderIds[msg.sender][clientOrderId] = orderId` - -When `clientOrderId` is zero, no client order ID tracking occurs. - -### Uniqueness and reuse - -A `clientOrderId` must be unique among a user's **active orders**. Once an order is filled or cancelled, its `clientOrderId` can be reused. This matches the standard FIX protocol behavior where `ClOrdID` uniqueness is required only for working orders. - -When an order reaches a terminal state (filled or cancelled), the `clientOrderIds` mapping entry is cleared. - -### Flip orders - -When a flip order is filled and creates a new order on the opposite side: - -1. The new (flipped) order inherits the original order's `clientOrderId` -2. The `clientOrderIds` mapping is updated to point to the new order ID -3. This allows users to track their position across flips using a single `clientOrderId` - -If the original order had no `clientOrderId` (was zero), the flipped order also has no `clientOrderId`. - -### Cancellation - -`cancelByClientOrderId(clientOrderId)` looks up `clientOrderIds[msg.sender][clientOrderId]` and cancels that order. It reverts if no active order exists for that `clientOrderId`. - -## New event - -```solidity -/// @notice Emitted when an order is placed (V2 with clientOrderId) -/// @dev Replaces OrderPlaced for new orders -event OrderPlacedV2( - uint128 indexed orderId, - address indexed maker, - address token, - uint128 amount, - bool isBid, - int16 tick, - bool isFlipOrder, - int16 flipTick, - uint128 clientOrderId -); -``` - -`OrderPlacedV2` is identical to `OrderPlaced` but adds the `clientOrderId` field. When an order is placed, only `OrderPlacedV2` is emitted (not both events). - -## New errors - -```solidity -/// @notice The client order ID is already in use by an active order -error DUPLICATE_CLIENT_ORDER_ID(); - -/// @notice No active order found for the given client order ID -error CLIENT_ORDER_ID_NOT_FOUND(); -``` - ---- - -# Invariants - -- A non-zero `clientOrderId` maps to at most one active order per user -- `clientOrderIds[user][clientOrderId]` is cleared when the order is filled or cancelled -- Flip orders inherit `clientOrderId` and update the mapping atomically -- `clientOrderId = 0` is reserved to mean "no client order ID" diff --git a/src/pages/protocol/tips/tip-1004.mdx b/src/pages/protocol/tips/tip-1004.mdx deleted file mode 100644 index 1d55cd34..00000000 --- a/src/pages/protocol/tips/tip-1004.mdx +++ /dev/null @@ -1,255 +0,0 @@ ---- -id: TIP-1004 -title: Permit for TIP-20 -description: Addition of EIP-2612 permit functionality to TIP-20 tokens, enabling gasless approvals via off-chain signatures. -authors: Dan Robinson -status: Draft ---- - -# TIP-1004: Permit for TIP-20 - -## Abstract - -TIP-1004 adds EIP-2612 compatible `permit()` functionality to TIP-20 tokens, enabling gasless approvals via off-chain signatures. This allows users to approve token spending without submitting an on-chain transaction, with the approval being executed by any third party who submits the signed permit. - -## Motivation - -The standard ERC-20 approval flow requires users to submit a transaction to approve a spender before that spender can transfer tokens on their behalf. Among other things, this makes it difficult for a transaction to "sweep" tokens from multiple addresses that have never sent a transaction onchain. - -EIP-2612 introduced the `permit()` function which allows approvals to be granted via a signed message rather than an on-chain transaction. This enables: - -- **Gasless approvals**: Users can sign a permit off-chain, and a relayer or the spender can submit the transaction -- **Single-transaction flows**: DApps can batch the permit with the subsequent action (e.g., approve + swap) in one transaction -- **Improved UX**: Users don't need to wait for or pay for a separate approval transaction - -Since TIP-20 aims to be a superset of ERC-20 with additional functionality, adding EIP-2612 permit support ensures TIP-20 tokens work seamlessly with existing DeFi protocols and tooling that expect permit functionality. - -### Alternatives - -While Tempo transactions provide solutions for most of the common problems that are solved by account abstraction, they do not provide a way to transfer tokens from an address that has never sent a transaction onchain, which means it does not provide an easy way for a batched transaction to "sweep" tokens from many addresses. - -While we plan to have Permit2 deployed on the chain, it, too, requires an initial transaction from the address being transferred from. - -Adding a function for `transferWithAuthorization`, which we are also considering, would also solve this problem. But `permit` is somewhat more flexible, and we think these functions are not mutually exclusive. - ---- - -# Specification - -## New functions - -The following functions are added to the TIP-20 interface: - -```solidity -interface ITIP20Permit { - /// @notice Approves `spender` to spend `value` tokens on behalf of `owner` via a signed permit - /// @param owner The address granting the approval - /// @param spender The address being approved to spend tokens - /// @param value The amount of tokens to approve - /// @param deadline Unix timestamp after which the permit is no longer valid - /// @param v The recovery byte of the signature - /// @param r Half of the ECDSA signature pair - /// @param s Half of the ECDSA signature pair - /// @dev The permit is valid only if: - /// - The current block timestamp is <= deadline - /// - The signature is valid and was signed by `owner` - /// - The nonce in the signature matches the current nonce for `owner` - /// Upon successful execution, increments the nonce for `owner` by 1. - /// Emits an {Approval} event. - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) external; - - /// @notice Returns the current nonce for an address - /// @param owner The address to query - /// @return The current nonce, which must be included in any permit signature for this owner - /// @dev The nonce starts at 0 and increments by 1 each time a permit is successfully used - function nonces(address owner) external view returns (uint256); - - /// @notice Returns the EIP-712 domain separator for this token - /// @return The domain separator bytes32 value - /// @dev The domain separator is computed dynamically on each call as: - /// keccak256(abi.encode( - /// keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), - /// keccak256(bytes(name())), - /// keccak256(bytes("1")), - /// block.chainid, - /// address(this) - /// )) - /// Dynamic computation ensures correct behavior after chain forks where chainId changes. - function DOMAIN_SEPARATOR() external view returns (bytes32); -} -``` - -## EIP-712 Typed Data - -The permit signature must conform to EIP-712 typed structured data signing. The domain and message types are defined as follows: - -### Domain Separator - -The domain separator is computed using the following parameters: - -| Parameter | Value | -|-----------|-------| -| name | The token's `name()` | -| version | `"1"` | -| chainId | The chain ID where the token is deployed | -| verifyingContract | The TIP-20 token contract address | - -```solidity -bytes32 DOMAIN_SEPARATOR = keccak256(abi.encode( - keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), - keccak256(bytes(name())), - keccak256(bytes("1")), - block.chainid, - address(this) -)); -``` - -### Permit Typehash - -The permit message type is: - -```solidity -bytes32 constant PERMIT_TYPEHASH = keccak256( - "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" -); -``` - -### Signature Construction - -To create a valid permit signature, the signer must sign the following EIP-712 digest: - -```solidity -bytes32 structHash = keccak256(abi.encode( - PERMIT_TYPEHASH, - owner, - spender, - value, - nonces[owner], - deadline -)); - -bytes32 digest = keccak256(abi.encodePacked( - "\x19\x01", - DOMAIN_SEPARATOR, - structHash -)); -``` - -The signature `(v, r, s)` must be produced by signing `digest` with the private key of `owner`. - -## Behavior - -### Nonces - -Each address has an associated nonce that: -- Starts at `0` for all addresses -- Increments by `1` each time a permit is successfully executed for that address -- Must be included in the permit signature to prevent replay attacks - -### Deadline - -The `deadline` parameter is a Unix timestamp. The permit is only valid if `block.timestamp <= deadline`. This allows signers to limit the validity window of their permits. - -### Pause State - -The `permit()` function follows the same pause behavior as `approve()`. Since setting an allowance does not move tokens, `permit()` is allowed to execute even when the token is paused. - -### TIP-403 Transfer Policy - -The `permit()` function does not perform TIP-403 authorization checks, consistent with the behavior of `approve()`. Transfer policy checks are only enforced when tokens are actually transferred. - -### Signature Validation - -The implementation must: -1. Verify that `block.timestamp <= deadline`, otherwise revert with `PermitExpired` -2. Attempt to validate the signature: - - First, use `ecrecover` to recover a signer address from the signature - - If `ecrecover` returns a non-zero address that equals `owner`, the signature is valid (EOA case) - - Otherwise, if `owner` has code, call `owner.isValidSignature(digest, signature)` per [EIP-1271](https://eips.ethereum.org/EIPS/eip-1271) - - If `isValidSignature` returns the magic value `0x1626ba7e`, the signature is valid (smart contract wallet case) - - Otherwise, revert with `InvalidSignature` -3. Verify the nonce matches `nonces[owner]` -4. Increment `nonces[owner]` -5. Set `allowance[owner][spender] = value` -6. Emit an `Approval(owner, spender, value)` event - -### Smart Contract Wallet Support (EIP-1271) - -TIP-1004 supports permits signed by smart contract wallets via [EIP-1271](https://eips.ethereum.org/EIPS/eip-1271). When the `owner` address has code deployed, the implementation calls: - -```solidity -bytes4 constant EIP1271_MAGIC_VALUE = 0x1626ba7e; - -// Pack signature for EIP-1271 call -bytes memory signature = abi.encodePacked(r, s, v); - -// Call isValidSignature on the owner contract -(bool success, bytes memory result) = owner.staticcall( - abi.encodeWithSelector( - IERC1271.isValidSignature.selector, - digest, - signature - ) -); - -// Signature is valid if call succeeds and returns magic value -bool isValid = success - && result.length == 32 - && abi.decode(result, (bytes4)) == EIP1271_MAGIC_VALUE; -``` - -This enables multisigs, smart contract wallets (e.g., Safe, Argent), and account abstraction wallets to use gasless permits. - -## New errors - -```solidity -/// @notice The permit signature has expired (block.timestamp > deadline) -error PermitExpired(); - -/// @notice The permit signature is invalid (wrong signer, malformed, or zero address recovered) -error InvalidSignature(); -``` - -## New events - -None. Successful permit execution emits the existing `Approval` event from TIP-20. - ---- - -# Invariants - -- `nonces(owner)` must only ever increase, never decrease -- `nonces(owner)` must increment by exactly 1 on each successful `permit()` call for that owner -- A permit signature can only be used once (enforced by nonce increment) -- A permit with a deadline in the past must always revert -- The recovered signer from a valid permit signature must exactly match the `owner` parameter -- After a successful `permit(owner, spender, value, ...)`, `allowance(owner, spender)` must equal `value` -- `DOMAIN_SEPARATOR()` must be computed dynamically and reflect the current `block.chainid` - -## Test Cases - -The test suite must cover: - -1. **Happy path**: Valid permit sets allowance correctly -2. **Expired permit**: Reverts with `PermitExpired` when `deadline < block.timestamp` -3. **Invalid signature**: Reverts with `InvalidSignature` for malformed signatures -4. **Wrong signer**: Reverts with `InvalidSignature` when signature is valid but signer ≠ owner -5. **Replay protection**: Second use of same signature reverts (nonce already incremented) -6. **Nonce tracking**: Verify nonce increments correctly after each permit -7. **Zero address recovery**: Reverts with `InvalidSignature` if ecrecover returns zero address -8. **Pause state**: Permit works when token is paused -9. **Domain separator**: Verify correct EIP-712 domain separator computation -10. **Domain separator chain ID**: Verify domain separator changes if chain ID changes -11. **Max allowance**: Permit with `type(uint256).max` value works correctly -12. **Allowance override**: Permit can override existing allowance (including to zero) -13. **EIP-1271 smart contract wallet**: Permit works with smart contract wallet that implements `isValidSignature` -14. **EIP-1271 rejection**: Reverts with `InvalidSignature` if smart contract wallet returns wrong magic value -15. **EIP-1271 revert**: Reverts with `InvalidSignature` if `isValidSignature` call reverts diff --git a/src/pages/protocol/tips/tip-1005.mdx b/src/pages/protocol/tips/tip-1005.mdx deleted file mode 100644 index d10492d6..00000000 --- a/src/pages/protocol/tips/tip-1005.mdx +++ /dev/null @@ -1,118 +0,0 @@ ---- -id: TIP-1005 -title: Fix ask swap rounding loss -description: A fix for a rounding bug in the Stablecoin DEX where partial fills on ask orders can cause small amounts of quote tokens to be lost. -authors: Dan Robinson -status: Draft ---- - -# TIP-1005: Fix ask swap rounding loss - -## Abstract - -This TIP fixes a rounding bug in the `swapExactAmountIn` function when filling ask orders. Due to double-rounding, the maker can receive slightly less quote tokens than the taker paid, causing tokens to be lost. - -## Motivation - -When a taker swaps quote tokens for base tokens against an ask order, the following calculation occurs: - -1. Convert taker's `amountIn` (quote) to base: `base_out = floor(amountIn / price)` -2. Credit maker with quote: `makerReceives = ceil(base_out * price)` - -Due to the floor in step 1, `makerReceives` can be less than `amountIn`. For example: - -- Taker pays `amountIn = 102001` quote at price 1.02 (tick 2000) -- `base_out = floor(102001 / 1.02) = 100000` -- `makerReceives = ceil(100000 * 1.02) = 102000` -- **1 token is lost** - -This violates the zero-sum invariant: the taker pays more than the maker receives. It also means there is no canonical amount swapped—the trade for the maker is different from the trade for the taker. - ---- - -# Specification - -## Bug location - -The bug is in `_fillOrdersExactIn` when processing ask orders (the `baseForQuote = false` path). Specifically, when a partial fill occurs: - -1. `fillAmount` (base) is calculated by rounding down: `baseOut = (remainingIn * PRICE_SCALE) / price` -2. `_fillOrder` is called with `fillAmount` -3. Inside `_fillOrder`, the maker's quote credit is re-derived: `quoteAmount = ceil(fillAmount * price)` - -The re-derivation in step 3 loses the original `remainingIn` information. - -## Fix - -For partial fills in the ask path, pass the actual `remainingIn` (quote) to `_fillOrder` and use it directly for the maker's credit, rather than re-deriving it from `fillAmount`. - -The fix requires: - -1. Modify `_fillOrder` to accept an optional `quoteOverride` parameter for ask orders -2. In `_fillOrdersExactIn`, when partially filling an ask, pass `remainingIn` as the quote override -3. When `quoteOverride` is provided, use it directly for the maker's balance increment instead of computing `ceil(fillAmount * price)` - -## Reference implementation changes - -The fix requires changes to two functions in [`docs/specs/src/StablecoinDEX.sol`](https://github.com/tempoxyz/tempo/blob/main/docs/specs/src/StablecoinDEX.sol): - -### 1. `_fillOrder` ([line 551-556](https://github.com/tempoxyz/tempo/blob/main/docs/specs/src/StablecoinDEX.sol#L551-L556)) - -Add an optional `quoteOverride` parameter. When non-zero and the order is an ask, use `quoteOverride` directly for the maker's balance increment instead of computing `ceil(fillAmount * price)`. - -```solidity -// Before: -uint128 quoteAmount = - uint128((uint256(fillAmount) * uint256(price) + PRICE_SCALE - 1) / PRICE_SCALE); -balances[order.maker][book.quote] += quoteAmount; - -// After: -uint128 quoteAmount = quoteOverride > 0 - ? quoteOverride - : uint128((uint256(fillAmount) * uint256(price) + PRICE_SCALE - 1) / PRICE_SCALE); -balances[order.maker][book.quote] += quoteAmount; -``` - -### 2. `_fillOrdersExactIn` ([line 923-926](https://github.com/tempoxyz/tempo/blob/main/docs/specs/src/StablecoinDEX.sol#L923-L926)) - -In the partial fill branch for asks, pass `remainingIn` as the quote override: - -```solidity -// Before: -orderId = _fillOrder(orderId, fillAmount); - -// After (for partial fills where fillAmount == baseOut): -orderId = _fillOrder(orderId, fillAmount, remainingIn); -``` - -## Affected code paths - -- `_fillOrdersExactIn` with `baseForQuote = false` (ask path), partial fill case only -- Full fills are not affected because the quote amount is derived from `order.remaining`, not `remainingIn` -- Bid swaps are not affected because the taker pays base tokens directly - -## Example: Before and after - -**Before (buggy):** -``` -amountIn = 102001 quote -base_out = floor(102001 / 1.02) = 100000 -makerReceives = ceil(100000 * 1.02) = 102000 -Lost: 1 token -``` - -**After (fixed):** -``` -amountIn = 102001 quote -base_out = floor(102001 / 1.02) = 100000 -makerReceives = 102001 (passed directly) -Lost: 0 tokens -``` - ---- - -# Invariants - -- Zero-sum: for any swap, `takerPaid == makerReceived` (within the same token) -- Taker receives `floor(amountIn / price)` base tokens (rounds in favor of protocol) -- Maker receives exactly what taker paid in quote tokens diff --git a/src/pages/protocol/tips/tip-1006.mdx b/src/pages/protocol/tips/tip-1006.mdx deleted file mode 100644 index 303534e7..00000000 --- a/src/pages/protocol/tips/tip-1006.mdx +++ /dev/null @@ -1,110 +0,0 @@ ---- -id: TIP-1006 -title: Burn At for TIP-20 Tokens -description: The burnAt function for TIP-20 tokens, enabling authorized administrators to burn tokens from any address. -authors: Dan Robinson -status: Draft ---- - -# TIP-1006: Burn At for TIP-20 Tokens - -## Abstract - -This specification introduces a `burnAt` function to TIP-20 tokens, allowing holders of a new `BURN_AT_ROLE` to burn tokens from any address without transfer policy restrictions. This complements the existing `burnBlocked` function which is limited to burning from addresses blocked by the transfer policy. - -## Motivation - -The existing TIP-20 burn mechanisms have the following limitations: - -1. `burn()` - Only burns from the caller's own balance, requires `ISSUER_ROLE` -2. `burnBlocked()` - Can burn from other addresses, but only if the target address is blocked by the transfer policy - -There are legitimate use cases where token administrators may want a privileged caller to have the ability to burn tokens from any address regardless of their policy status, such as allowing a bridge contract to burn tokens that are being bridged out without requiring approval (as in the `crosschainBurn` function proposed in [ERC 7802](https://github.com/ethereum/ERCs/blob/master/ERCS/erc-7802.md)). - -The `burnAt` function provides this capability with appropriate access controls via a dedicated role. - ---- - -# Specification - -## New Role - -A new role constant is added to TIP-20: - -```solidity -bytes32 public constant BURN_AT_ROLE = keccak256("BURN_AT_ROLE"); -``` - -This role is administered by the `DEFAULT_ADMIN_ROLE` (same as other TIP-20 roles). - -## New Event - -```solidity -/// @notice Emitted when tokens are burned from any account. -/// @param from The address from which tokens were burned. -/// @param amount The amount of tokens burned. -event BurnAt(address indexed from, uint256 amount); -``` - -## New Function - -```solidity -/// @notice Burns tokens from any account. -/// @dev Requires BURN_AT_ROLE. Cannot burn from protected precompile addresses. -/// @param from The address to burn tokens from. -/// @param amount The amount of tokens to burn. -function burnAt(address from, uint256 amount) external; -``` - -### Behavior - -1. **Access Control**: Reverts with `Unauthorized` if caller does not have `BURN_AT_ROLE` -2. **Protected Addresses**: Reverts with `ProtectedAddress` if `from` is: - - `TIP_FEE_MANAGER_ADDRESS` (0xfeEC000000000000000000000000000000000000) - - `STABLECOIN_DEX_ADDRESS` (0xDEc0000000000000000000000000000000000000) -3. **Balance Check**: Reverts with `InsufficientBalance` if `from` has insufficient balance -4. **No Policy Check**: Unlike `burnBlocked`, this function does NOT check transfer policy authorization -5. **State Changes**: - - Decrements `balanceOf[from]` by `amount` - - Decrements `_totalSupply` by `amount` - - Updates reward accounting if `from` is opted into rewards -6. **Events**: Emits `Transfer(from, address(0), amount)` and `BurnAt(from, amount)` - -### Interface Addition - -The `ITIP20` interface is extended with: - -```solidity -/// @notice Returns the role identifier for burning tokens from any account. -/// @return The burn-at role identifier. -function BURN_AT_ROLE() external view returns (bytes32); - -/// @notice Burns tokens from any account. -/// @param from The address to burn tokens from. -/// @param amount The amount of tokens to burn. -function burnAt(address from, uint256 amount) external; -``` - -# Invariants - -1. **Role Required**: `burnAt` must always revert if caller lacks `BURN_AT_ROLE` -2. **Protected Addresses**: `burnAt` must never succeed when `from` is a protected precompile address -3. **Supply Conservation**: After `burnAt(from, amount)`: - - `totalSupply` decreases by exactly `amount` - - `balanceOf[from]` decreases by exactly `amount` -4. **Balance Constraint**: `burnAt` must revert if `amount > balanceOf[from]` -5. **Reward Accounting**: If `from` is opted into rewards, `optedInSupply` must decrease by `amount` -6. **Policy Independence**: `burnAt` must succeed regardless of transfer policy status of `from` - -## Test Cases - -The test suite must verify: - -1. Successful burn with `BURN_AT_ROLE` -2. Revert without `BURN_AT_ROLE` (Unauthorized) -3. Revert when burning from `TIP_FEE_MANAGER_ADDRESS` (ProtectedAddress) -4. Revert when burning from `STABLECOIN_DEX_ADDRESS` (ProtectedAddress) -5. Successful burn from policy-blocked address (differs from `burnBlocked`) -6. Revert on insufficient balance -7. Correct event emissions (`Transfer` and `BurnAt`) -8. Correct reward accounting updates diff --git a/src/pages/protocol/tips/tip-1007.mdx b/src/pages/protocol/tips/tip-1007.mdx deleted file mode 100644 index fec20077..00000000 --- a/src/pages/protocol/tips/tip-1007.mdx +++ /dev/null @@ -1,132 +0,0 @@ ---- -id: TIP-1007 -title: Fee Token Introspection -description: Addition of fee token introspection functionality to the FeeManager precompile, enabling smart contracts to query the fee token being used for the current transaction. -authors: Georgios Konstantopoulos -status: Draft ---- - -# TIP-1007: Fee Token Introspection - -## Abstract - -TIP-1007 adds a `getFeeToken()` view function to the FeeManager precompile that returns the fee token address being used for the current transaction. This enables smart contracts to introspect which TIP-20 token is paying for gas fees during execution, allowing for dynamic logic based on the fee token choice. - -## Motivation - -Tempo transactions support paying gas fees in any USD-denominated TIP-20 token via the fee token preference system. However, prior to this TIP, there was no way for a smart contract to determine which fee token is being used for the current transaction during execution. - -This capability was requested by a partner. It could be useful for contracts that want to: - -- Adjust their internal logic based on which fee token is being used -- Provide fee token-aware pricing or routing decisions -- Emit events or logs that include the fee token for off-chain indexing -- Implement fee token-specific behavior in cross-chain messaging - ---- - -# Specification - -## New Function - -The following function is added to the `IFeeManager` interface: - -```solidity -interface IFeeManager { - // ... existing functions ... - - /// @notice Returns the fee token being used for the current transaction - /// @return The address of the TIP-20 token paying for gas fees - /// @dev This value is set by the protocol before transaction execution begins. - /// Returns address(0) if no fee token has been set (e.g., in eth_call - /// simulations where the transaction handler does not run). - function getFeeToken() external view returns (address); -} -``` - -## Behavior - -### Fee Token Resolution - -The fee token returned by `getFeeToken()` is the same token that was resolved by the protocol during transaction validation, following the [fee token preference rules](/protocol/fees/spec-fee#fee-token-resolution). - -### Storage - -The fee token is stored in **transient storage** (EIP-1153) within the FeeManager precompile. This means: - -- The value is automatically cleared at the end of each transaction -- No persistent storage writes occur, minimizing gas costs -- The value is consistent across all calls within a transaction (including internal calls and subcalls) - -### Timing - -The fee token is set by the protocol in the `validate_against_state_and_deduct_caller` handler phase, before any user code executes. This ensures the value is available throughout the entire transaction execution. - -### Gas Cost - -Reading the fee token costs the standard warm transient storage read cost (100 gas for TLOAD). This is the cost of calling `getFeeToken()` itself; callers should account for additional gas used by the CALL opcode to invoke the precompile. - -### Edge Cases - -| Scenario | Return Value | -|----------|--------------| -| Normal transaction | The resolved fee token address | -| Free transaction (zero gas price) | The resolved fee token (may still be set) | -| `eth_call` simulation | `address(0)` (no transaction context) | - -The only case where `address(0)` is returned is in simulation contexts (e.g., `eth_call`) where the protocol handler does not execute. - -## Example Usage - -```solidity -import { IFeeManager } from "./interfaces/IFeeManager.sol"; - -contract FeeTokenAware { - IFeeManager constant FEE_MANAGER = IFeeManager(0xfeeC000000000000000000000000000000000000); - address constant PATH_USD = 0x20C0000000000000000000000000000000000000; - - function doSomething() external { - address feeToken = FEE_MANAGER.getFeeToken(); - - if (feeToken == PATH_USD) { - // User is paying fees in pathUSD - } else if (feeToken != address(0)) { - // User is paying fees in a different USD stablecoin - } else { - // No fee token context (e.g., eth_call simulation) - } - } -} -``` - -## Interface Addition - -The following function is added to `IFeeManager`: - -```solidity -/// @notice Returns the fee token being used for the current transaction -/// @return The address of the TIP-20 token paying for gas fees -function getFeeToken() external view returns (address); -``` - ---- - -# Invariants - -- `getFeeToken()` must return a consistent value across all calls within the same transaction -- `getFeeToken()` must return `address(0)` in simulation contexts (e.g., `eth_call`) where no transaction handler runs -- `getFeeToken()` must be callable from `staticcall` contexts without reverting -- The fee token returned must match the token used for actual fee deduction in `collectFeePreTx` and `collectFeePostTx` -- Reading the fee token must not modify any state (view function) - -## Test Cases - -The test suite must cover: - -1. **Basic functionality**: `getFeeToken()` returns the correct fee token address -2. **Zero when unset**: Returns `address(0)` when no fee token is set -3. **Consistency**: Same value returned from nested calls within a transaction -4. **Static call safety**: Works correctly when called via `staticcall` -5. **Transient storage**: Value is cleared between transactions -6. **Different fee tokens**: Works with various TIP-20 fee tokens (pathUSD, USDC, etc.) -7. **Dispatch coverage**: Function selector is correctly dispatched by the precompile diff --git a/src/pages/protocol/tips/tip-1009.mdx b/src/pages/protocol/tips/tip-1009.mdx deleted file mode 100644 index 39fc0c95..00000000 --- a/src/pages/protocol/tips/tip-1009.mdx +++ /dev/null @@ -1,298 +0,0 @@ ---- -id: TIP-1009 -title: Expiring Nonces -description: Time-bounded replay protection using transaction hashes instead of sequential nonce management. -authors: Tempo Team -status: Approved -related: TIP-20, Transactions -protocolVersion: T1 ---- - -# TIP-1009: Expiring Nonces - -## Abstract - -TIP-1009 introduces expiring nonces, an alternative replay protection mechanism where transactions are valid only within a specified time window. Instead of tracking sequential nonces, the protocol uses transaction hashes with expiry timestamps to prevent replay attacks. This enables use cases like gasless transactions, meta-transactions, and simplified UX where users don't need to manage nonce ordering. - -## Motivation - -Traditional sequential nonces require careful ordering—if transaction N fails or is delayed, all subsequent transactions (N+1, N+2, ...) are blocked. This creates friction for: - -1. **Gasless/Meta-transactions**: Relayers need complex nonce management across multiple users -2. **Parallel submission**: Users cannot submit multiple independent transactions simultaneously -3. **Recovery from failures**: Stuck transactions require explicit cancellation with the same nonce - -Expiring nonces solve these problems by using time-based validity instead of sequence-based ordering. Each transaction is uniquely identified by its hash and is valid only until a specified `validBefore` timestamp. - ---- - -# Specification - -## Nonce Key - -Expiring nonce transactions use a reserved nonce key: - -``` -TEMPO_EXPIRING_NONCE_KEY = uint256.max (2^256 - 1) -``` - -When a Tempo transaction specifies `nonceKey = uint256.max`, the protocol treats it as an expiring nonce transaction. - -## Transaction Fields - -Expiring nonce transactions require: - -| Field | Type | Description | -|-------|------|-------------| -| `nonceKey` | `uint256` | Must be `uint256.max` to indicate expiring nonce mode | -| `nonce` | `uint64` | Must be `0` (unused, validated for consistency) | -| `validBefore` | `uint64` | Unix timestamp (seconds) after which the transaction is invalid | - -## Validity Window - -The `validBefore` timestamp must satisfy: - -``` -now < validBefore <= now + MAX_EXPIRY_SECS -``` - -Where: -- `now` is the current block timestamp -- `MAX_EXPIRY_SECS = 30` seconds - -Transactions with `validBefore` in the past or more than 30 seconds in the future are rejected. - -## Replay Protection - -Replay protection uses a **circular buffer** data structure in the Nonce precompile: - -### Storage Layout - -```solidity -contract Nonce { - // Existing 2D nonce storage - mapping(address => mapping(uint256 => uint64)) public nonces; // slot 0 - - // Expiring nonce storage - mapping(bytes32 => uint64) public expiringNonceSeen; // slot 1: txHash => expiry - mapping(uint32 => bytes32) public expiringNonceRing; // slot 2: circular buffer - uint32 public expiringNonceRingPtr; // slot 3: buffer pointer -} -``` - -### Circular Buffer Design - -The circular buffer has a fixed capacity: - -``` -EXPIRING_NONCE_SET_CAPACITY = 300,000 -``` - -This capacity is sized for 10,000 TPS × 30 seconds = 300,000 transactions, ensuring entries expire before being overwritten. - -### Algorithm - -When processing an expiring nonce transaction: - -1. **Validate expiry window**: Reject if `validBefore <= now` or `validBefore > now + 30` - -2. **Replay check**: Read `expiringNonceSeen[txHash]` - - If entry exists and `expiry > now`, reject as replay - -3. **Get buffer position**: Read `expiringNonceRingPtr`, compute `idx = ptr % CAPACITY` - -4. **Read existing entry**: Read `expiringNonceRing[idx]` to get `oldHash` - -5. **Eviction check** (safety): If `oldHash != 0`: - - Read `expiringNonceSeen[oldHash]` - - If `expiry > now`, reject (buffer full of valid entries) - - Clear `expiringNonceSeen[oldHash] = 0` - -6. **Insert new entry**: - - Write `expiringNonceRing[idx] = txHash` - - Write `expiringNonceSeen[txHash] = validBefore` - -7. **Advance pointer**: Write `expiringNonceRingPtr = ptr + 1` - -### Pseudocode - -```solidity -function checkAndMarkExpiringNonce( - bytes32 txHash, - uint64 validBefore, - uint64 now -) internal { - // 1. Validate expiry window - require(validBefore > now && validBefore <= now + 30, "InvalidExpiry"); - - // 2. Replay check - uint64 seenExpiry = expiringNonceSeen[txHash]; - require(seenExpiry == 0 || seenExpiry <= now, "Replay"); - - // 3-4. Get buffer position and existing entry - uint32 ptr = expiringNonceRingPtr; - uint32 idx = ptr % CAPACITY; - bytes32 oldHash = expiringNonceRing[idx]; - - // 5. Eviction check (safety) - if (oldHash != bytes32(0)) { - uint64 oldExpiry = expiringNonceSeen[oldHash]; - require(oldExpiry == 0 || oldExpiry <= now, "BufferFull"); - expiringNonceSeen[oldHash] = 0; - } - - // 6. Insert new entry - expiringNonceRing[idx] = txHash; - expiringNonceSeen[txHash] = validBefore; - - // 7. Advance pointer - expiringNonceRingPtr = ptr + 1; -} -``` - -## Gas Costs - -The intrinsic gas cost for expiring nonce transactions includes: - -``` -EXPIRING_NONCE_GAS = 2 * COLD_SLOAD_COST + WARM_SLOAD_COST + 3 * WARM_SSTORE_RESET - = 2 * 2100 + 100 + 3 * 2900 - = 13,000 gas -``` - -**Included operations:** -- 2 cold SLOADs: `seen[txHash]`, `ring[idx]` (unique slots per tx) -- 1 warm SLOAD: `seen[oldHash]` (warm because we just read `ring[idx]` which points to it) -- 3 SSTOREs at RESET price: `seen[oldHash]=0`, `ring[idx]`, `seen[txHash]` - -**Excluded operations (amortized):** -- `ring_ptr` SLOAD/SSTORE: Accessed by almost every expiring nonce tx in a block, so amortized cost approaches ~200 gas. May be moved out of EVM storage in the future. - -**Why SSTORE_RESET (2,900) instead of SSTORE_SET (20,000) for `seen[txHash]`:** -- SSTORE_SET cost exists to penalize permanent state growth -- Expiring nonce data is ephemeral: evicted within 30 seconds, fixed-size buffer (300k entries) -- No permanent state growth, so the 20k penalty doesn't apply - -## Transaction Pool Validation - -The transaction pool performs preliminary validation: - -1. Verify `nonceKey == uint256.max` -2. Verify `nonce == 0` -3. Verify `validBefore` is present -4. Verify `validBefore > currentTime` (not expired) -5. Verify `validBefore <= currentTime + MAX_EXPIRY_SECS` (within window) -6. Query `expiringNonceSeen[txHash]` storage slot to check for existing entry - -Transactions failing these checks are rejected before entering the pool. - -## Interaction with Other Features - -### 2D Nonces - -Expiring nonces and 2D nonces are mutually exclusive: -- `nonceKey = 0`: Protocol nonce (standard sequential) -- `nonceKey = 1..uint256.max-1`: 2D nonce keys -- `nonceKey = uint256.max`: Expiring nonce mode - -### Access Keys (Keychain) - -Expiring nonces work with access key signatures. The `validBefore` provides an additional security boundary—even if an access key is compromised, transactions signed with it become invalid after the expiry window. - -### Fee Tokens - -Expiring nonce transactions pay fees in TIP-20 fee tokens like any other Tempo transaction. - ---- - -# Invariants - -## Must Hold - -| ID | Invariant | Description | -|----|-----------|-------------| -| **E1** | No replay | A transaction hash can never be executed twice (changing `validBefore` produces a different hash) | -| **E2** | Expiry enforcement | Transactions with `validBefore <= now` must be rejected | -| **E3** | Window bounds | Transactions with `validBefore > now + MAX_EXPIRY_SECS` must be rejected | -| **E4** | Nonce must be zero | Expiring nonce transactions must have `nonce == 0` | -| **E5** | Valid before required | Expiring nonce transactions must have `validBefore` set | -| **E6** | No nonce mutation | Expiring nonce txs do not increment protocol nonce or any 2D nonce | -| **E7** | Concurrent independence | Multiple expiring nonce txs from same sender can execute in same block | - -## Invariant Tests - -These invariants are tested in the Foundry invariant test suite (`TempoTransactionInvariant.t.sol`): - -| Handler | Tests | Description | -|---------|-------|-------------| -| `handler_expiringNonceBasic` | Basic flow | Execute valid expiring nonce tx | -| `handler_expiringNonceReplay` | E1 | Replay must be rejected | -| `handler_expiringNonceExpired` | E2 | Tx with `validBefore <= now` must be rejected | -| `handler_expiringNonceWindowTooFar` | E3 | Tx with `validBefore > now + 30s` must be rejected | -| `handler_expiringNonceNonZeroNonce` | E4 | Tx with `nonce != 0` must be rejected | -| `handler_expiringNonceMissingValidBefore` | E5 | Tx without `validBefore` must be rejected | -| `handler_expiringNonceNoNonceMutation` | E6 | Protocol and 2D nonces unchanged after execution | -| `handler_expiringNonceConcurrent` | E7 | Multiple concurrent txs from same sender succeed | - -## Test Cases - -1. **Basic flow**: Submit transaction, verify execution, attempt replay (should fail) - -2. **Expiry validation**: - - `validBefore` in past → reject - - `validBefore = now` → reject - - `validBefore = now + 31` → reject - - `validBefore = now + 30` → accept - -3. **Nonce validation**: - - `nonce = 0` → accept - - `nonce > 0` → reject - -4. **Required fields**: - - `validBefore` missing → reject - - `nonceKey != uint256.max` → not expiring nonce (uses 2D nonce rules) - -5. **Post-expiry replay**: Submit tx, wait for expiry, submit same tx with new `validBefore` (should succeed) - -6. **Buffer eviction**: Fill buffer, verify old entries are evicted when expired - -7. **Concurrent transactions**: Submit multiple transactions with same `validBefore`, verify all succeed - ---- - -# Open Questions - -## Safety Check for Buffer Eviction - -The current implementation includes a safety check that reads `expiringNonceSeen[oldHash]` before evicting an entry from the ring buffer. This check verifies the entry is actually expired before overwriting. - -**Rationale for keeping the check:** -- Protects against unexpected TPS spikes that could cause the buffer to fill with valid entries -- Defense-in-depth: prevents replay attacks if capacity assumptions are violated -- Cost is only incurred in the rare case when eviction is needed - -**Rationale for removing the check:** -- The buffer is sized (300k entries) to guarantee entries expire before being overwritten at 10k TPS -- Removes 1 SLOAD (2,100 gas) from the critical path -- Simplifies the algorithm - -**Current decision**: Keep the check but exclude it from gas accounting (charged as if it won't trigger in normal operation). - -**Question**: Should this safety check be: -1. Kept with current gas accounting (not charged for the extra SLOAD)? -2. Removed entirely, trusting the capacity sizing? -3. Kept and fully charged (add 2,100 gas to `EXPIRING_NONCE_GAS`)? - -## Buffer Capacity Sizing - -The current capacity of 300,000 assumes: -- Maximum 10,000 TPS sustained -- 30 second expiry window - -**Question**: Should the capacity be configurable per-chain or hardcoded? What happens if TPS requirements increase significantly? - -## Transaction Hash Computation - -The transaction hash used for replay protection must be computed before signature recovery. - -**Question**: Should the spec explicitly define the hash computation (which fields, encoding) or reference the Tempo Transaction spec? diff --git a/src/pages/protocol/tips/tip-1010.mdx b/src/pages/protocol/tips/tip-1010.mdx deleted file mode 100644 index 92a00837..00000000 --- a/src/pages/protocol/tips/tip-1010.mdx +++ /dev/null @@ -1,153 +0,0 @@ ---- -id: TIP-1010 -title: Mainnet Gas Parameters -description: Initial gas parameters for Tempo mainnet launch including base fee pricing, payment lane capacity, and transaction gas limits. -authors: Dankrad Feist @dankrad -status: Approved -related: TIP-1000, Payment Lane Specification, Sub block Specification -protocolVersion: T1 ---- - -# TIP-1010: Mainnet Gas Parameters - -## Abstract - -This TIP specifies the initial gas parameters for Tempo mainnet, including base fee pricing, payment lane capacity, and main transaction gas limits. These parameters are calibrated to support Tempo's target of approximately 20,000 TPS for payment transactions while maintaining economically sustainable fee levels. - -## Motivation - -Tempo is designed as a high-throughput blockchain optimized for stablecoin payments. To achieve this, the gas parameters must be carefully calibrated to: - -1. **Enable high throughput**: Support ~20,000 TPS for payment transactions -2. **Maintain low fees**: Target 0.1 cent per standard TIP-20 transfer -3. **Prevent spam**: Ensure fees are high enough to deter abuse -4. **Balance capacity**: Allocate appropriate gas limits between payment lane and general transactions - -The parameters defined in this TIP represent the initial mainnet configuration and may be adjusted through future governance processes. - ---- - -# Specification - -## Base Fee - -**Value**: `2 × 10^10` wei (20 gwei) - -**Rationale**: -- A standard TIP-20 transfer costs approximately 50,000 gas -- At 20 gwei base fee: `50,000 × 20 × 10^9 = 10^15 wei = 0.001 USD` (assuming 1 ETH = $1000 for unit conversion reference) -- This targets approximately **0.1 cent per TIP-20 transfer** - -**Note**: The base fee may fluctuate based on network demand through the existing EIP-1559-style mechanism. This value represents the target equilibrium base fee. - -## Payment Lane Gas Limit - -**Value**: 500,000,000 gas per block (total block gas limit) - -**Rationale**: -- At 50,000 gas per TIP-20 transfer: `500,000,000 / 50,000 = 10,000 transfers per block` -- With 500ms block time: `10,000 × 2 = 20,000 TPS` for payment transactions -- This capacity supports Tempo's target throughput for payment use cases - -**Constraints**: -- Only transactions qualifying for the payment lane (simple TIP-20 transfers, memos, etc.) may exceed the `general_gas_limit` -- Complex contract interactions use the general gas limit instead - -:::info -**Shared capacity model**: The payment lane is non-dedicated. General and payment transactions selected by the proposer share the non-shared gas budget (`block_gas_limit - shared_gas_limit` = 450M). General transactions are capped at `general_gas_limit` (30M), guaranteeing that at least 420M gas remains available for proposer payment transactions. The remaining 50M (`shared_gas_limit`) is reserved for validator subblocks as defined in the Sub-block Specification. -::: - -## Main Transaction Gas Limit - -**Value**: 30,000,000 gas per block (`general_gas_limit`) - -**Rationale**: -- Aligned with the transaction gas cap to ensure maximum-sized contract deployments can be included in a block -- Supports general smart contract interactions beyond simple payments -- Provides capacity for: - - Contract deployments (including max 24KB contracts) - - DEX swaps - - Complex multi-step transactions - - Other non-payment use cases - -:::warning -**Transactions exceeding 16,000,000 gas are not recommended.** The elevated gas limits (30M) exist solely to accommodate maximum-sized contract deployments under TIP-1000 state creation costs. Applications should not rely on transactions consuming more than 16M gas for normal operations. When storage pricing is moved to a separate mechanism (e.g., storage rent or state expiry), the transaction gas cap is expected to return to 16,000,000 gas. -::: - -## Transaction Gas Cap - -**Value**: 30,000,000 gas per transaction - -**Rationale**: -- Increased from the previous 16,000,000 gas limit -- Accommodates deployment of maximum-size contracts (24,576 bytes per EIP-170) under TIP-1000 state creation costs: - - Base transaction cost: 21,000 gas - - Calldata for initcode (up to 49,152 bytes per EIP-3860): ~500,000-800,000 gas - - CREATE base cost (TIP-1000, includes keccak/codesize fields): 500,000 gas (replaces old 32,000) - - Initcode execution: variable (~3,000 gas minimum) - - Contract code storage (TIP-1000): `24,576 bytes × 1,000 gas/byte = 24,576,000 gas` - - Account creation (TIP-1000): 250,000 gas - - **Total**: ~25,850,000-26,150,000 gas (fits within 30M limit) - -## Gas Schedule Summary - -| Parameter | Value | Purpose | -|-----------|-------|---------| -| Base fee | `2 × 10^10` wei (20 gwei) | Target 0.1 cent per TIP-20 transfer | -| Total block gas limit | 500,000,000 gas/block | Total block capacity | -| Non-shared gas limit | 450,000,000 gas/block | Proposer pool transactions | -| Shared gas limit | 50,000,000 gas/block | Validator subblocks (see Sub-block Specification) | -| General gas limit | 30,000,000 gas/block | Cap for non-payment transactions | -| Transaction gas cap | 30,000,000 gas | Allow max-size contract deployment | - -## Economic Analysis - -### Fee Revenue Projections - -At full payment lane utilization: -- 10,000 transfers per block × 50,000 gas × 20 gwei = 10^16 wei per block -- At 2 blocks/second: 2 × 10^16 wei/second = ~$0.02/second (at reference pricing) -- Daily: ~$1,728 in base fees from payment lane alone - -### Cost Per Operation - -| Operation | Gas Cost | USD Cost (at target base fee) | -|-----------|----------|-------------------------------| -| TIP-20 transfer (existing recipient) | 50,000 | $0.001 (0.1 cent) | -| TIP-20 transfer (new recipient) | 300,000 | $0.006 (0.6 cent) | -| First transaction from new account | 300,000 | $0.006 (0.6 cent) | -| Small contract deployment (1KB) | ~1,800,000 | $0.036 (3.6 cents) | -| Max contract deployment (24,576 bytes) | ~26,200,000 | $0.524 (~52 cents) | - ---- - -# Invariants - -1. **Base Fee Floor**: The base fee MUST NOT fall below a minimum threshold that would enable economically viable spam attacks. - -2. **Payment Lane Priority**: Transactions qualifying for the payment lane MUST be able to consume up to the remaining block gas capacity (total gas limit minus gas already consumed by general transactions). - -3. **Shared Gas Pool**: Proposer pool transactions (payment and general) share the non-shared gas budget (450M). General transactions are additionally constrained by `general_gas_limit` (30M). The remaining 50M is reserved for validator subblocks. - -4. **Transaction Gas Cap**: No single transaction MUST be allowed to consume more than the transaction gas cap (30,000,000 gas). - -5. **Block Gas Validity**: A block MUST be invalid if either: - - Payment lane transactions exceed the payment lane gas limit - - General transactions exceed the general gas limit - -## Implementation Notes - -These parameters are configured at the chainspec level and applied during block validation. Future adjustments may be made through: - -1. Hard fork upgrades (for significant changes) -2. Governance proposals (if on-chain governance is implemented) -3. Emergency response procedures (for critical security issues) - -## Test Cases - -1. **Base fee targeting**: Verify that at equilibrium, TIP-20 transfers cost approximately 0.1 cent -2. **Payment lane capacity**: Verify that 10,000 TIP-20 transfers can be included in a single block -3. **General gas limit**: Verify that general transactions are correctly bounded by the 25M gas limit -4. **Transaction gas cap**: Verify that transactions exceeding 30M gas are rejected -5. **Contract deployment**: Verify that a 24KB contract can be deployed within the transaction gas cap -6. **Lane separation**: Verify that payment lane and general transactions are independently tracked