---
id: TIP-1038
title: T3 Hardfork Meta TIP
description: Meta TIP collecting all bug fixes, security hardening, and gas correctness changes gated behind the T3 hardfork.
authors: Rusowsky (@0xrusowsky), Dragan Rakita (@rakita), Federico Gimenez (@fgimenez), Derek Cofausper (@decofe)
status: Scheduled
protocolVersion: T3
---

# TIP-1038: T3 Hardfork Meta TIP

## Abstract

This meta TIP collects bug fixes, gas correctness improvements, and security hardening changes that activate at T3. Each item is small in isolation, but together they define the complete in-scope T3 bug-fix bundle.

## Motivation

Ongoing internal review and audit follow-ups uncovered several correctness and performance issues that require hardfork gating. Because these fixes alter state-function behavior at activation boundaries, they are grouped here as one coordinated rollout under T3.

---

# Changes

## 1. Skip redundant `setUserToken` write and event when token unchanged

**PR**: [#3272](https://github.com/tempoxyz/tempo/pull/3272) · **Author**: @fgimenez

`setUserToken()` unconditionally writes to storage and emits `UserTokenSet` even when the token hasn't changed. Since the event triggers a full O(pool_size) txpool scan via `evict_invalidated_transactions`, any account can force network-wide CPU work each block by repeatedly calling `setUserToken` with the same value. T3+ adds a read-before-write guard that returns early when the stored token already matches, eliminating the redundant write, event, and pool scan.

## 2. Account creation gas deduction and nonce bump

**PR**: [#3207](https://github.com/tempoxyz/tempo/pull/3207) · **Author**: @rakita

Renames `set_code` to `create_account` and adds T3-gated gas deduction (GACC) for account creation operations. On T3, accounts being created deduct the creation gas cost upfront and have their nonce incremented to properly reflect create operation semantics.

## 3. Deduct gas before cold loading storage and accounts

**PR**: [#2974](https://github.com/tempoxyz/tempo/pull/2974) · **Author**: @rakita

Deducts static gas costs before performing cold storage/account loads, preventing cheap load attacks by checking available gas upfront. When insufficient gas remains for cold load costs after static gas deduction, the cold load accounting is skipped. Affects `sload`, `sstore`, and `with_account_info`.

## 4. TIP-20: verify paused state before mint and burn

**PR**: [#3411](https://github.com/tempoxyz/tempo/pull/3411) · **Author**: @0xrusowsky

The TIP-20 pause mechanism was missing from `mint`, `mintWithMemo`, `burn`, `burnWithMemo`, and `burnBlocked` — an unintentional omission that left token-moving operations reachable while the contract was paused. The pause flag is meant to act as a universal kill switch; leaving mint/burn unguarded undermines that guarantee and, in particular, prevents the admin from stopping a compromised `BURN_BLOCKED_ROLE` key from seizing funds.

T3+ adds `check_not_paused()` to all five functions. A new `validate_mint` helper consolidates the pause check, recipient validation, and TIP-403 policy check into a single call. Administrative functions (role management, unpausing) and `transferFeePostTx` remain intentionally exempt.

## 5. Disambiguate optional AA expiry and validity timestamps

**PRs**: [#3500](https://github.com/tempoxyz/tempo/pull/3500), [#3501](https://github.com/tempoxyz/tempo/pull/3501) · **Author**: @legion2002

Several AA timestamp fields were encoded as `Option<u64>` in RLP, but `None` and `Some(0)` both serialize as the empty string. That made `Some(0)` silently roundtrip to `None`, which could invert user intent by turning an immediately expired access key or transaction into one with no expiry bound.

T3+ changes `key_authorization.expiry`, `valid_before`, and `valid_after` to `Option<NonZeroU64>` at the primitives layer so zero-valued bounds are unrepresentable. Serde-backed JSON/request deserialization rejects `0x0` explicitly for these fields, while downstream execution and pool components convert back to plain `u64` only where comparisons or storage require it.

## 6. StablecoinDEX: check token paused in internal balance swaps

**PR**: [#3204](https://github.com/tempoxyz/tempo/pull/3204) · **Author**: @0xrusowsky

The StablecoinDEX `swap_exact_amount_in` and `swap_exact_amount_out` paths operate on internal DEX balances and bypass TIP-20 `transfer`, so the pause check in the token contract is never hit. A paused token could still be swapped through the DEX — including as an intermediate hop in a multi-leg route. T3+ adds `check_not_paused()` to `validate_and_build_route` for every token in the swap path, ensuring paused tokens block DEX swaps the same way they block direct transfers.

## 7. Account-keychain: clamp refunded spending limits to the configured max

**PR**: [#3483](https://github.com/tempoxyz/tempo/pull/3483) · **Author**: @rakita

`refund_spending_limit()` restored spending room with a saturating add, which could raise a T3 key's remaining allowance above the configured max during defensive refund paths. T3+ clamps refunded spending limits to the stored `max`, preserving the invariant that `remaining <= max` for T3 keys while leaving migrated pre-T3 rows on their legacy behavior because they do not persist a max bound.

## 8. TIP-20: propagate OOG from `is_initialized` instead of masking as uninitialized

**PR**: [#3535](https://github.com/tempoxyz/tempo/pull/3535) · **Author**: @0xrusowsky

`is_initialized()` used `.unwrap_or(false)` on the bytecode check, which swallowed out-of-gas errors and surfaced them as `TIP20Error::uninitialized()`. This misclassified a legitimate OOG into a logic error, letting callers retry under the wrong assumption that the contract simply doesn't exist. T3+ replaces the fallback with explicit error propagation so OOG surfaces as `PrecompileError::OutOfGas`.
