Skip to content

bug(ui): USDT mainnet payments revert when leftover allowance is non-zero #2083

Description

@wa0x6e

The Pro/Turbo payment flow uses a naive approve(spender, amount) that doesn't handle USDT's legacy approve guard. Mainnet USDT (0xdac17f958d2ee523a2206206994597c13d831ec7) reverts when both the current allowance and the new value are non-zero. Users with a leftover non-zero allowance hit a revert when trying to upgrade to a different non-zero amount.

Affected code

apps/ui/src/helpers/token.ts:38-47:

export async function approve(
  web3: Web3Provider,
  tokenAddress: string,
  spenderAddress: string,
  amount: bigint
) {
  const contract = new Contract(tokenAddress, abis.erc20, web3.getSigner());

  return contract.approve(spenderAddress, amount);
}

USDT's approve, for reference:

function approve(address _spender, uint _value) public {
    if ((_value != 0) && (allowed[msg.sender][_spender] != 0)) revert();
    allowed[msg.sender][_spender] = _value;
    Approval(msg.sender, _spender, _value);
}

When it triggers

In usePayment.ts:

  1. getIsApproved checks allowance >= weiAmount. If sufficient, skip approve.
  2. Otherwise, call approve(spender, weiAmount).

The revert surfaces when leftover allowance is non-zero but insufficient — e.g. user approved 100, paid 50, now tries to pay 200. allowance=50, weiAmount=200getIsApproved returns false → approve(spender, 200) → revert.

Won't trigger on the happy path because payWithERC20Token consumes exactly weiAmount, leaving allowance at 0. Cancellation mid-flow or quantity changes between attempts can land users in the non-zero-leftover state.

Affected tokens

  • Mainnet USDT (0xdac17f958d2ee523a2206206994597c13d831ec7) — listed in TOKENS[1] in usePayment.ts.

Sepolia "USDT" (0xaa8e23fb...) is a standard OZ ERC-20 — not affected. USDC on either network — not affected.

Fix options

  • (a) Always emit approve(0) then approve(amount). Works for all ERC-20s, costs an extra tx + signature.
  • (b) Conditional reset: if allowance > 0, do approve(0) first; then approve(amount). Optimal for non-USDT tokens.
  • (c) Use OpenZeppelin's safeIncreaseAllowance / safeDecreaseAllowance pattern.

(b) minimizes signature load for the common case.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions