Why allowance handling needs a wrapper

The ERC-20 standard exposes a minimal interface:

  • approve(spender, amount)
  • allowance(owner, spender)
  • transferFrom(from, to, amount)

That simplicity hides a few practical problems:

  1. Approval race conditions
  2. If a user changes an allowance from one non-zero value to another, a spender may front-run the transaction and spend both the old and new allowance.

  1. Non-standard token behavior
  2. Some tokens return false instead of reverting, some do not return a boolean at all, and some require allowance to be set to zero before updating it.

  1. Repeated boilerplate
  2. Every contract that interacts with tokens ends up reimplementing the same checks and low-level call handling.

A dedicated library helps centralize these concerns and makes your contracts easier to audit.

Design goals for the library

A useful allowance wrapper library should:

  • work with standard and non-standard ERC-20 tokens
  • support safe allowance updates
  • provide explicit semantics for “set”, “increase”, and “decrease”
  • fail loudly on unexpected token behavior
  • keep the calling contract simple

We will build a library that exposes three core operations:

FunctionPurposeTypical use case
forceApproveSet allowance safely, even for tokens that require zero-first updatesUSDT-style tokens
safeIncreaseAllowanceIncrease allowance without overwriting existing approvalsrecurring spending limits
safeDecreaseAllowanceReduce allowance and prevent underflowrevoking or tightening permissions

Minimal ERC-20 interface

Start with a compact interface. We only need the functions relevant to allowance management.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

interface IERC20Minimal {
    function approve(address spender, uint256 value) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
}

This interface is intentionally small. In production, you may already have a full ERC-20 interface in your codebase, but keeping the library focused makes it easier to reuse.

Implementing the allowance wrapper library

The library will use low-level calls so it can handle tokens that do not strictly follow the ERC-20 return-value convention.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

library SafeAllowance {
    error ApproveFailed();
    error DecreaseBelowZero();

    function forceApprove(
        IERC20Minimal token,
        address spender,
        uint256 value
    ) internal {
        // Try direct approval first.
        if (_callApprove(token, spender, value)) {
            return;
        }

        // Some tokens require resetting allowance to zero before setting a new value.
        if (!_callApprove(token, spender, 0)) {
            revert ApproveFailed();
        }

        if (!_callApprove(token, spender, value)) {
            revert ApproveFailed();
        }
    }

    function safeIncreaseAllowance(
        IERC20Minimal token,
        address spender,
        uint256 addedValue
    ) internal {
        uint256 current = token.allowance(address(this), spender);
        uint256 newAllowance = current + addedValue;
        forceApprove(token, spender, newAllowance);
    }

    function safeDecreaseAllowance(
        IERC20Minimal token,
        address spender,
        uint256 subtractedValue
    ) internal {
        uint256 current = token.allowance(address(this), spender);
        if (current < subtractedValue) {
            revert DecreaseBelowZero();
        }

        uint256 newAllowance = current - subtractedValue;
        forceApprove(token, spender, newAllowance);
    }

    function _callApprove(
        IERC20Minimal token,
        address spender,
        uint256 value
    ) private returns (bool) {
        (bool success, bytes memory data) = address(token).call(
            abi.encodeWithSelector(token.approve.selector, spender, value)
        );

        if (!success) {
            return false;
        }

        if (data.length == 0) {
            // Non-standard tokens may not return a value but still succeed.
            return true;
        }

        if (data.length == 32) {
            return abi.decode(data, (bool));
        }

        return false;
    }
}

How it works

The key idea is _callApprove, which tolerates three token behaviors:

  • standard success with true
  • success with no return data
  • failure or malformed return data

If a direct approval fails, forceApprove falls back to the zero-first pattern. This is especially useful for tokens that reject changing a non-zero allowance directly.

Using the library in a contract

Here is a simple vault contract that collects deposits and grants a router permission to spend tokens on behalf of the vault.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "./SafeAllowance.sol";

interface IERC20Transfer {
    function transferFrom(address from, address to, uint256 value) external returns (bool);
    function transfer(address to, uint256 value) external returns (bool);
}

contract TokenVault {
    using SafeAllowance for IERC20Minimal;

    address public immutable router;

    constructor(address router_) {
        router = router_;
    }

    function approveRouter(IERC20Minimal token, uint256 amount) external {
        token.forceApprove(router, amount);
    }

    function increaseRouterAllowance(IERC20Minimal token, uint256 amount) external {
        token.safeIncreaseAllowance(router, amount);
    }

    function decreaseRouterAllowance(IERC20Minimal token, uint256 amount) external {
        token.safeDecreaseAllowance(router, amount);
    }
}

This contract keeps allowance logic out of the business flow. Any future token integration can reuse the same library without duplicating approval edge-case handling.

When to use each function

Choosing the right allowance operation matters. The table below summarizes the intended behavior.

FunctionBehaviorBest forRisk profile
forceApproveSets allowance to an exact value, using zero-first fallback if neededrouters, vaults, protocol adapterslow, if spender is trusted
safeIncreaseAllowanceAdds to the current allowanceincremental spending limitssafer than overwrite
safeDecreaseAllowanceSubtracts from the current allowancerevocation and tightening permissionsprevents accidental underflow

Practical guidance

  • Use forceApprove when a token is known to reject direct allowance changes.
  • Use safeIncreaseAllowance when you want to preserve existing approval and add a delta.
  • Use safeDecreaseAllowance when reducing permissions after a task is complete.

Avoid calling raw approve directly in application contracts unless you have a very specific reason and have audited the token behavior.

Handling real-world token quirks

Not all tokens behave the same way. Your library should be resilient to the most common deviations.

Tokens that return no boolean

Some older or non-standard tokens succeed without returning true. A strict ABI call would revert when decoding the return value. The low-level call pattern in _callApprove avoids that problem.

Tokens that require zero-first approval

Certain tokens reject changing a non-zero allowance directly. The fallback sequence:

  1. approve 0
  2. approve newValue

is a widely used compatibility pattern.

Tokens that return false

A token may execute without reverting but still return false. In that case, the library treats the operation as failed and tries the fallback or reverts.

Best practices for safe allowance management

A wrapper library improves consistency, but you still need good operational practices.

Prefer exact allowances over unlimited approvals

Unlimited approvals are convenient, but they expand blast radius if a spender is compromised. Whenever possible:

  • approve only what is needed
  • refresh allowances per operation
  • revoke allowances after use

Keep spender trust boundaries explicit

Only approve contracts that are:

  • audited
  • immutable or well-governed
  • necessary for the workflow

If the spender is upgradeable, treat the approval as a long-lived security dependency.

Reduce allowances after execution

If your contract approves a router or external module for a one-time action, decrease the allowance immediately after the action completes. This is especially important for contracts that hold user funds temporarily.

Avoid approval churn in hot paths

If a contract repeatedly approves the same spender for the same token, consider whether a persistent allowance model is actually needed. Repeated approval updates increase gas usage and complexity.

Extending the library for safer workflows

You can extend the wrapper with higher-level helpers that combine allowance management with token movement.

For example, a contract may want to ensure a spender can pull exactly amount tokens, execute an action, and then revoke the remaining allowance.

function approveExecuteRevoke(
    IERC20Minimal token,
    address spender,
    uint256 amount
) external {
    token.forceApprove(spender, amount);

    // External action would happen here.

    token.forceApprove(spender, 0);
}

This pattern is useful for:

  • swap routers
  • bridge adapters
  • escrow settlement
  • one-off protocol interactions

The exact execution step depends on your application, but the allowance lifecycle stays consistent.

Testing the library

A library like this should be tested against multiple token behaviors. At minimum, include these cases:

  1. Standard ERC-20 token
  2. approve returns true.

  1. Zero-first token
  2. direct non-zero-to-non-zero approval fails, but zero then new value succeeds.

  1. No-return token
  2. approve succeeds without return data.

  1. False-return token
  2. approve returns false and the library reverts.

  1. Decrease below zero
  2. safeDecreaseAllowance must revert if the subtraction exceeds the current allowance.

A simple Foundry-style test matrix might look like this:

Test caseExpected result
forceApprove on standard tokensucceeds in one call
forceApprove on zero-first tokensucceeds via fallback
safeIncreaseAllowanceallowance increases by delta
safeDecreaseAllowance with too much subtractionreverts with DecreaseBelowZero

When testing, verify both the final allowance and the revert behavior. For compatibility logic, the fallback path is just as important as the success path.

Common mistakes to avoid

Overwriting allowances blindly

Calling approve(spender, amount) repeatedly can create race conditions if the spender is active. Prefer increase/decrease semantics or zero-first updates.

Assuming all tokens return true

Many integrations fail because they assume strict ERC-20 compliance. Use low-level calls and validate return data carefully.

Ignoring allowance state after external calls

If your contract approves a spender and then calls out to another contract, remember that the spender may use the allowance during that call. Design the sequence so the allowance window is as small as possible.

Using the library for untrusted spenders

A safe wrapper does not make an unsafe spender trustworthy. The library only reduces token-compatibility issues and approval bugs; it does not solve authorization design.

A production-ready checklist

Before shipping a contract that uses this library, confirm the following:

  • allowance changes are limited to trusted spenders
  • exact approval amounts are used where possible
  • zero-first fallback is tested against known problematic tokens
  • allowance is revoked after one-time operations
  • custom errors are documented for integrators
  • tests cover both standard and non-standard ERC-20 behavior

A small utility like this can remove a surprising amount of repetitive code from your contracts while improving security and compatibility.

Conclusion

Allowance management is one of the most practical places to introduce a Solidity library. By wrapping approve logic in a reusable utility, you can handle token quirks, reduce boilerplate, and make your contracts safer to integrate with real-world ERC-20 tokens.

The most important takeaway is not the code itself, but the pattern: treat allowances as a lifecycle, not a one-line call. Set them deliberately, adjust them carefully, and revoke them when they are no longer needed.

Learn more with useful resources