
Building a Safe ERC-20 Allowance Wrapper Library in Solidity
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:
- Approval race conditions
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.
- Non-standard token behavior
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.
- Repeated boilerplate
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:
| Function | Purpose | Typical use case |
|---|---|---|
forceApprove | Set allowance safely, even for tokens that require zero-first updates | USDT-style tokens |
safeIncreaseAllowance | Increase allowance without overwriting existing approvals | recurring spending limits |
safeDecreaseAllowance | Reduce allowance and prevent underflow | revoking 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.
| Function | Behavior | Best for | Risk profile |
|---|---|---|---|
forceApprove | Sets allowance to an exact value, using zero-first fallback if needed | routers, vaults, protocol adapters | low, if spender is trusted |
safeIncreaseAllowance | Adds to the current allowance | incremental spending limits | safer than overwrite |
safeDecreaseAllowance | Subtracts from the current allowance | revocation and tightening permissions | prevents accidental underflow |
Practical guidance
- Use
forceApprovewhen a token is known to reject direct allowance changes. - Use
safeIncreaseAllowancewhen you want to preserve existing approval and add a delta. - Use
safeDecreaseAllowancewhen 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:
- approve
0 - 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:
- Standard ERC-20 token
approve returns true.
- Zero-first token
direct non-zero-to-non-zero approval fails, but zero then new value succeeds.
- No-return token
approve succeeds without return data.
- False-return token
approve returns false and the library reverts.
- Decrease below zero
safeDecreaseAllowance must revert if the subtraction exceeds the current allowance.
A simple Foundry-style test matrix might look like this:
| Test case | Expected result |
|---|---|
forceApprove on standard token | succeeds in one call |
forceApprove on zero-first token | succeeds via fallback |
safeIncreaseAllowance | allowance increases by delta |
safeDecreaseAllowance with too much subtraction | reverts 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.
