Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-8255: Expiring Token Approvals

Token approvals expire after a bounded duration.

Authors Moody Salem (@moodysalem) <moody.salem@gmail.com>
Created 2026-05-06
Discussion Link https://ethereum-magicians.org/t/eip-8255-expiring-token-approvals/28456
Requires EIP-20, EIP-2612

Abstract

This specification extends ERC-20 approvals with an expiration timestamp. Existing approve(address,uint256) calls remain valid, but approvals created through that function expire after the token contract’s default maximum approval duration. If the token also implements ERC-2612, approvals created through permit expire under the same default-duration rule without changing the permit signature. A new function allows token owners to approve a spender for a shorter duration, and a new view function exposes the stored allowance and its expiration.

Motivation

ERC-20 approvals are commonly granted for values much larger than the intended immediate spend, including unlimited approvals. These allowances remain valid until explicitly changed, creating a durable authorization that can be used long after the user has forgotten the original interaction.

Expiring approvals preserve the existing ERC-20 approval workflow while bounding the lifetime of each authorization. Wallets and applications can continue to call approve(address,uint256) or, where supported, permit, while contracts and interfaces that understand this extension can request shorter-lived approvals and display expiration information to users.

Specification

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.

Compliant contracts MUST implement the following interface in addition to ERC-20:

interface IERC8255 /* is IERC20 */ {
    /// @notice Optional event emitted when an approval expiration is set.
    event ApprovalExpiration(
        address indexed owner,
        address indexed spender,
        uint64 expiration
    );

    /// @notice Returns the contract-defined constant maximum approval duration, in seconds.
    function maxApprovalDuration() external pure returns (uint32);

    /// @notice Returns the stored expiration timestamp and stored allowance, even if expired.
    function allowanceAndExpiration(address owner, address spender)
        external
        view
        returns (uint64 expiration, uint256 allowance);

    /// @notice Approves `spender` for `amount` tokens for `duration` seconds.
    function approveForDuration(address spender, uint256 amount, uint32 duration)
        external
        returns (bool success);
}

Approval expiration

maxApprovalDuration() MUST return the contract-defined constant maximum duration, in seconds, that any approval can remain valid after it is created. The same value is the default duration used by approve(address spender, uint256 amount).

For every successful call to approve(address spender, uint256 amount), the contract MUST set spender’s allowance from msg.sender to amount and MUST set its expiration to block.timestamp + maxApprovalDuration().

For every successful call to approveForDuration(address spender, uint256 amount, uint32 duration), the contract MUST set spender’s allowance from msg.sender to amount and MUST set its expiration to block.timestamp + duration.

The duration argument MUST be less than or equal to maxApprovalDuration(). A call with a longer duration MUST revert or return false.

If amount is zero, the contract MUST set the allowance to zero. The contract SHOULD set the corresponding expiration to zero.

Implementations MAY support type(uint256).max as a maximum allowance sentinel. If such a sentinel is used, allowance(owner, spender) MUST return type(uint256).max while the approval is unexpired, and allowanceAndExpiration(owner, spender) MUST return the stored maximum allowance value.

Implementations MUST NOT create an approval whose expiration is greater than type(uint64).max. Implementations MAY revert if block.timestamp + duration cannot be represented as a uint64.

Signed approvals

If a compliant contract also implements ERC-2612, every successful call to permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) MUST set spender’s allowance from owner to value and MUST set its expiration to block.timestamp + maxApprovalDuration().

The permit function signature, signed typed data, and nonce behavior MUST remain unchanged from ERC-2612. This specification does not add a duration parameter to permit.

The ERC-2612 deadline parameter MUST continue to define only the latest timestamp at which the signed permit may be submitted. It MUST NOT be treated as the approval expiration timestamp.

Allowance accounting

The ERC-20 allowance(address owner, address spender) function MUST return zero when the allowance has expired. Otherwise, it MUST return the unexpired allowance.

An allowance is expired only when its expiration timestamp is less than block.timestamp. An allowance with expiration equal to the current block timestamp is unexpired.

The allowanceAndExpiration(address owner, address spender) function MUST return the stored expiration timestamp for the approval and the stored allowance amount, even if the approval has expired. Consumers that need the effective allowance MUST compare expiration < block.timestamp or call allowance(owner, spender).

The ERC-20 transferFrom(address from, address to, uint256 amount) function MUST treat an expired allowance as zero. If the allowance is unexpired and sufficient, transferFrom MUST decrease the allowance by amount unless the implementation uses an allowance sentinel that is not decreased by ERC-20 transfers. Implementations MAY distinguish expired approvals from insufficient approvals when reverting.

When transferFrom decreases an unexpired allowance, the expiration timestamp MUST remain unchanged. If the resulting allowance is zero, the implementation MAY zero the allowance storage slot. If the slot is zeroed, allowanceAndExpiration(owner, spender) returns expiration 0 even if the original approval expiration had not passed.

Events

Every successful call to either approve(address spender, uint256 amount) or approveForDuration(address spender, uint256 amount, uint32 duration) MUST emit the ERC-20 Approval event.

Every successful call to ERC-2612 permit, if supported, MUST emit the ERC-20 Approval event.

Implementations MAY emit ApprovalExpiration(owner, spender, expiration) after each successful approve, approveForDuration, or ERC-2612 permit call that sets an approval expiration. This event is informational only. Consumers MUST use allowance(owner, spender) or allowanceAndExpiration(owner, spender) to determine the current effective allowance.

Storage layout

This specification does not require a particular storage layout.

Implementations MAY store the expiration timestamp in the upper 64 bits of a token allowance storage word and the allowance amount in the lower 192 bits:

uint256 packed = (uint256(expiration) << 192) | allowance;

This layout leaves 192 bits for the allowance amount. 192 bits is more than enough to represent the total supply of every ERC-20 token in existence at the time of writing, while preserving a single storage slot for the owner-spender allowance entry.

Implementations that use this layout MUST ensure that the stored allowance amount fits in 192 bits, is exactly type(uint256).max, or uses a separate representation for larger allowances. Packed implementations that do not use a separate representation SHOULD reject approval amounts greater than type(uint192).max and less than type(uint256).max.

If a packed implementation represents type(uint256).max by reserving one lower-192-bit value, it MUST also reject approval of that reserved value unless the requested amount is type(uint256).max.

Rationale

Using approve(address,uint256) as an expiring approval with a default duration preserves the existing ERC-20 approval flow. Applications that are unaware of this extension can keep using the existing ABI, and users receive a bounded authorization instead of a permanent one.

The approveForDuration(address,uint256,uint32) function allows applications to request a shorter duration without changing the meaning of ERC-20 approve. It uses a distinct function name to avoid tooling ambiguity around overloaded approval functions. A uint32 duration is sufficient to express approximately 136 years in seconds, which is longer than any reasonable expiring approval.

The ERC-2612 permit signature is unchanged so that existing wallets, typed-data encoders, and permit-aware applications do not need to support a second signed approval format. This means signed approvals use the token’s default maximum approval duration. Bundling exact-spend approvals into transactions is expected to become more common over time, which reduces the need for a duration-specific permit variant.

allowanceAndExpiration returns expiration before allowance so callers can decode both values without ambiguity and can present the expiration and stored allowance even when the effective allowance is zero.

An approval expires only when expiration < block.timestamp, rather than when expiration == block.timestamp, so approveForDuration(spender, amount, 0) can authorize a bundled approve-and-spend flow that executes in the same transaction or block.

The packed storage layout is optional because some tokens may need to preserve full-width uint256 allowance values or existing storage layouts. For new tokens with bounded supply and ordinary allowance semantics, the packed layout allows this extension to be implemented without adding a second storage slot per allowance.

Some ERC-20 implementations treat type(uint256).max as an infinite-approval sentinel and do not decrement that allowance during transferFrom, saving gas for repeated transfers. Allowing this single full-width value preserves compatibility with applications that request maximum approvals while still rejecting intermediate values that cannot be represented in the 192-bit packed amount field.

Backwards Compatibility

The new methods are ABI-compatible with ERC-20 because they use new function selectors. Existing calls to approve(address,uint256), allowance(address,address), and transferFrom(address,address,uint256) remain valid.

This specification changes the long-term behavior of allowances created by approve(address,uint256) and, if supported, ERC-2612 permit: they expire after maxApprovalDuration() seconds instead of remaining valid indefinitely. Contracts that assume an ERC-20 allowance remains valid forever SHOULD refresh approvals before use or query allowanceAndExpiration.

Applications that use unlimited approvals MAY need to request a new approval after expiration. The approval amount can remain unchanged; only the approval lifetime is bounded.

This specification does not change the ERC-2612 permit ABI or signed typed data.

Migrating existing allowance storage

Upgradeable contracts that already store each allowance as a single uint256 value MAY migrate to the packed layout without rewriting every existing allowance slot. When an existing allowance value is less than type(uint192).max, interpreting that slot as (uint64 expiration, uint192 allowance) yields an expiration of 0 and the original allowance amount. Because 0 < block.timestamp after deployment, those existing allowances are expired by default while remaining visible through allowanceAndExpiration.

Existing allowance values greater than or equal to type(uint192).max do not have a meaningful packed interpretation unless the implementation defines one. This behavior does not affect effective allowance safety because such values either expire by default, are rejected or remapped by migration logic, or are treated under the implementation’s maximum-allowance sentinel rules.

Test Cases

  1. If maxApprovalDuration() returns 86400 and approve(spender, 100) is called at timestamp 1_000_000, then allowanceAndExpiration(owner, spender) returns expiration 1_086_400 and allowance 100.

  2. If approveForDuration(spender, 100, 3600) is called at timestamp 1_000_000, then allowanceAndExpiration(owner, spender) returns expiration 1_003_600 and allowance 100.

  3. If approveForDuration(spender, 100, maxApprovalDuration() + 1) is called, the call reverts or returns false.

  4. If an allowance has expiration 1_003_600 and the current timestamp is 1_003_600, allowance(owner, spender) returns the stored allowance and transferFrom(owner, to, 1) may succeed if the allowance is otherwise sufficient.

  5. If an allowance has expiration 1_003_600, stored allowance 100, and the current timestamp is 1_003_601, allowance(owner, spender) returns 0, allowanceAndExpiration(owner, spender) returns expiration 1_003_600 and allowance 100, and transferFrom(owner, to, 1) fails unless another authorization applies.

  6. If approveForDuration(spender, 100, 0) is called and transferFrom(owner, to, 100) is executed in the same transaction or block, the approval is unexpired during that transaction or block.

  7. If an unexpired allowance is 100 and transferFrom(owner, to, 25) succeeds, allowanceAndExpiration(owner, spender) returns the same expiration timestamp and allowance 75.

  8. If an unexpired allowance is 25 and transferFrom(owner, to, 25) succeeds, the implementation may clear the storage slot so allowanceAndExpiration(owner, spender) returns expiration 0 and allowance 0.

  9. If approve(spender, type(uint256).max) succeeds, allowance(owner, spender) returns type(uint256).max until the approval expires and transferFrom may leave the allowance unchanged.

  10. If a packed implementation does not use a separate representation for larger allowances, approve(spender, type(uint192).max + 1) reverts or returns false.

  11. If a packed implementation reserves type(uint192).max to represent type(uint256).max, approve(spender, type(uint192).max) reverts or returns false.

  12. If an ERC-2612 permit(owner, spender, 100, deadline, v, r, s) succeeds at timestamp 1_000_000 and maxApprovalDuration() returns 86400, then allowanceAndExpiration(owner, spender) returns expiration 1_086_400 and allowance 100.

  13. If an ERC-2612 permit has deadline 1_200_000 and succeeds at timestamp 1_000_000, the approval expiration is still block.timestamp + maxApprovalDuration(), not 1_200_000.

Reference Implementation

The following example shows the core packing behavior. It omits unrelated ERC-20 balance and supply logic.

abstract contract ERC20ExpiringApprovals {
    uint32 internal constant _MAX_APPROVAL_DURATION = 86400;
    uint256 internal constant _AMOUNT_MASK = (uint256(1) << 192) - 1;
    uint256 internal constant _MAX_AMOUNT_SENTINEL = _AMOUNT_MASK;

    mapping(address owner => mapping(address spender => uint256 packed)) internal _allowances;

    event Approval(address indexed owner, address indexed spender, uint256 value);
    event ApprovalExpiration(address indexed owner, address indexed spender, uint64 expiration);

    function maxApprovalDuration() public pure returns (uint32) {
        return _MAX_APPROVAL_DURATION;
    }

    function allowance(address owner, address spender) public view returns (uint256) {
        (uint64 expiration, uint256 amount) = allowanceAndExpiration(owner, spender);
        return expiration < block.timestamp ? 0 : amount;
    }

    function allowanceAndExpiration(address owner, address spender)
        public
        view
        returns (uint64 expiration, uint256 amount)
    {
        uint256 packed = _allowances[owner][spender];
        expiration = uint64(packed >> 192);
        amount = packed & _AMOUNT_MASK;

        if (amount == 0) {
            return (0, 0);
        }

        if (amount == _MAX_AMOUNT_SENTINEL) {
            amount = type(uint256).max;
        }
    }

    function approve(address spender, uint256 amount) public returns (bool) {
        _approve(msg.sender, spender, amount, maxApprovalDuration());
        return true;
    }

    function approveForDuration(address spender, uint256 amount, uint32 duration) public returns (bool) {
        _approve(msg.sender, spender, amount, duration);
        return true;
    }

    // ERC-2612 implementations call this after validating the permit signature and nonce.
    function _approveWithDefaultDuration(address owner, address spender, uint256 amount) internal {
        _approve(owner, spender, amount, maxApprovalDuration());
    }

    function _approve(address owner, address spender, uint256 amount, uint32 duration) internal {
        require(duration <= maxApprovalDuration(), "duration exceeds maximum");
        require(
            amount < type(uint192).max || amount == type(uint256).max,
            "unsupported allowance"
        );

        uint256 expirationValue = amount == 0 ? 0 : block.timestamp + duration;
        require(expirationValue <= type(uint64).max, "expiration exceeds 64 bits");

        uint64 expiration = uint64(expirationValue);
        uint256 storedAmount = amount == type(uint256).max ? _MAX_AMOUNT_SENTINEL : amount;
        _allowances[owner][spender] = (uint256(expiration) << 192) | storedAmount;

        emit Approval(owner, spender, amount);
        emit ApprovalExpiration(owner, spender, expiration);
    }
}

Security Considerations

Expiring approvals reduce the duration of approval risk but do not remove the ERC-20 approval race condition. User interfaces SHOULD continue to follow ERC-20 guidance for changing a non-zero allowance to another non-zero allowance.

Contracts that pull tokens using transferFrom SHOULD be prepared for approvals to expire between transaction construction and execution. This is especially relevant for transactions submitted through public mempools or delayed execution systems.

Short approval durations can improve user safety but can also cause failed transactions if a user signs an approval and the intended use is delayed. Wallets and applications SHOULD choose durations that account for expected transaction latency.

Wallets and applications displaying ERC-2612 permits SHOULD distinguish the permit submission deadline from the resulting approval expiration. The former controls signature validity; the latter controls allowance validity after the permit is submitted.

Implementations using packed storage MUST avoid truncating allowance values silently. If an approval amount does not fit in the lower 192 bits, the implementation MUST reject it or store it using another representation.

The expiration timestamp is based on block.timestamp, which block producers can influence within normal consensus bounds. Approval durations SHOULD include enough margin that small timestamp variation does not change the user’s expected outcome.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Moody Salem (@moodysalem) <moody.salem@gmail.com>, "ERC-8255: Expiring Token Approvals [DRAFT]," Ethereum Improvement Proposals, no. 8255, May 2026. Available: https://eips.ethereum.org/EIPS/eip-8255.