Allowance

An allowance is the amount of tokens an owner authorizes a third party (spender) to transfer on their behalf. This mechanism enables smart contracts and decentralized applications to interact with a user's tokens without requiring direct ownership.

The ERC20 Absolute Allowance

In ERC20, spending permission is an absolute allowance stored as allowance(owner, spender). You grant it with approve(spender, amount) , and if you later want a different limit you call approve() again with the new number.

Because these updates happen in separate transactions and miners can reorder transactions (or a watcher can front-run from the public mempool), there's a race:

"while your change to 150 approval is pending, the spender can first call transferFrom to consume the existing 100, and then, after your update lands, still have 150 available, effectively extracting 250."

This is the classic ERC20 allowance race condition: non-atomic transitions between absolute values create a window where old and new allowances can both be honored depending on ordering.

ERC20 Allowance - Race Condition Vulnerability Normal Flow 1. Initial State Allowance = 0 Owner: Alice | Spender: Bob 2. Alice Approves Bob approve(Bob, 100) Allowance = 100 3. Bob Uses Allowance transferFrom(Alice, Bob, 50) Allowance = 50 4. Alice Changes Approval approve(Bob, 200) Allowance = 200 5. Final State Bob spent: 50 tokens Remaining allowance: 200 Race Condition Attack 1. Initial State Allowance = 0 Owner: Alice | Spender: Bob 2. Alice Approves Bob approve(Bob, 100) Allowance = 100 3. Bob Uses Allowance transferFrom(Alice, Bob, 100) Allowance = 0 4. Alice Changes Approval approve(Bob, 50) [in mempool] Bob sees this transaction! Bob front-runs with higher gas 5. Bob Front-Runs! transferFrom(Alice, Bob, 100) BEFORE Alice's new approval! Allowance = 0 → 50 6. Bob Exploits Again! transferFrom(Alice, Bob, 50) Total stolen: 150 tokens! (100 + 50 instead of just 50)

The OP-20 Safe Adjustments

Delta-based

OP-20 replaces ERC20's absolute allowance model with delta-based adjusments: permissions change only via explicit increases or decreases that are applied atomically to the current allowance.

Because you never replace one absolute number with another, there's no window where "old" and "new" approvals can both be valid based on transaction ordering, the contract simply adjusts the live value and emits an event, eliminating the classic race that arises during updates.

OP-20 Delta-Based Adjustments Delta-Based Adjustments Initial State Allowance: 0 tokens Owner: Alice | Spender: Bob Alice Increases Allowance increaseAllowance(Bob, +100) 0 + 100 = 100 tokens Atomic operation - no race window! Bob Uses Allowance safeTransferFrom(Alice, Bob, 60) Allowance: 100 - 60 = 40 tokens Alice Decreases Allowance decreaseAllowance(Bob, -20) 40 - 20 = 20 tokens No double-spend possible! Delta applied atomically to current value Final State - Safe Current Allowance: 20 tokens Bob used: 60 tokens total ✓ No race condition vulnerability Signature-Based Approval 1. Alice Creates Off-Chain Signature Sign: {spender: Bob, delta: +500, deadline: block, signature: Schnorr} No gas cost for Alice! 2. Bob/Relayer Submits On-Chain increaseAllowanceBySignature(...) Bob pays gas, Alice doesn't! Just-in-time approval for DEX trade 3. Contract Verifies Signature ✓ Valid signature from Alice ✓ Deadline not expired 4. Delta Applied Atomically Allowance: 0 + 500 = 500 tokens Nonce marked as used All in single atomic transaction 5. DEX Executes Trade safeTransferFrom(Alice, DEX, 500) Allowance: 500 - 500 = 0 Both methods are race-condition free!

Infinite Allowances

OP-20 recognizes infinite allowances for explicitly trusted spenders:

  • Once granted, the allowance does not decrement on use.
  • This is an opt-in optimization, revocable at any time.

Signature-based Variants

For signature-based allowance documentation, see the Allowance By Signatures section.

Using Allowance Methods in a Contract

The following example demonstrates how to call the increaseAllowance() and decreaseAllowance() methods:
Allowance exampleassemblyscript
import { u256 } from '@btc-vision/as-bignum/assembly';
import { 
    Address,
    Blockchain, 
    BytesWriter,
    ReentrancyGuard,
    Revert,
    TransferHelper,
    SELECTOR_BYTE_LENGTH,
    U256_BYTE_LENGTH,
    ADDRESS_BYTE_LENGTH
} from '@btc-vision/btc-runtime/runtime';

@final
export class MyContract extends ReentrancyGuard {
    public constructor() {
        super();
    }
    
    ...

    // This is only to demonstrate how to call another contract method.
    // The TransferHelper class already contains an increaseAllowance method ready to be used.
    private callIncreaseAllowance(token: Address, owner: Address, spender: Address, amount: u256) : void{
        // Encode increaseAllowance Calldata
        // 1st parameter: selector (method to call->increaseAllowance)
        // 2nd parameter: spender address
        // 3rd parameter: amount
        const calldata = new BytesWriter(
            SELECTOR_BYTE_LENGTH +
            ADDRESS_BYTE_LENGTH +
            U256_BYTE_LENGTH);

        calldata.writeSelector(TransferHelper.INCREASE_ALLOWANCE_SELECTOR);
        calldata.writeAddress(spender);
        calldata.writeU256(amount);

        const response = Blockchain.call(token, calldata);

        if (response.data.byteLength > 0){
            thow new Revert('Invalid response from increaseAllowance.');
        }
    }

    // This is only to demonstrate how to call another contract method.
    // The TransferHelper class already contains a decreaseAllowance method ready to be used.
    private callDecreaseAllowance(token: Address, owner: Address, spender: Address, amount: u256) : void{
        // Encode decreaseAllowance Calldata
        // 1st parameter: selector (method to call->decreaseAllowance)
        // 2nd parameter: spender address
        // 3rd parameter: amount
        const calldata = new BytesWriter(
            SELECTOR_BYTE_LENGTH +
            ADDRESS_BYTE_LENGTH +
            U256_BYTE_LENGTH);

        calldata.writeSelector(TransferHelper.DECREASE_ALLOWANCE_SELECTOR);
        calldata.writeAddress(spender);
        calldata.writeU256(amount);

        const response = Blockchain.call(token, calldata);

        if (response.data.byteLength > 0){
            thow new Revert('Invalid response from decreaseAllowance.');
        }
    }
}