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:
getIsApproved checks allowance >= weiAmount. If sufficient, skip approve.
- 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=200 → getIsApproved 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.
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:USDT's
approve, for reference:When it triggers
In
usePayment.ts:getIsApprovedchecksallowance >= weiAmount. If sufficient, skip approve.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=200→getIsApprovedreturns false →approve(spender, 200)→ revert.Won't trigger on the happy path because
payWithERC20Tokenconsumes exactlyweiAmount, leaving allowance at 0. Cancellation mid-flow or quantity changes between attempts can land users in the non-zero-leftover state.Affected tokens
0xdac17f958d2ee523a2206206994597c13d831ec7) — listed inTOKENS[1]inusePayment.ts.Sepolia "USDT" (
0xaa8e23fb...) is a standard OZ ERC-20 — not affected. USDC on either network — not affected.Fix options
approve(0)thenapprove(amount). Works for all ERC-20s, costs an extra tx + signature.allowance > 0, doapprove(0)first; thenapprove(amount). Optimal for non-USDT tokens.safeIncreaseAllowance/safeDecreaseAllowancepattern.(b) minimizes signature load for the common case.