A wallet can lose tokens without its owner ever pressing the Send button.
The owner may not have submitted a transaction, paid a gas fee, or approved a visible withdrawal. The only action they remember is signing a message on a website.
Hours later, tokens leave the wallet.
That sequence sounds like someone obtained the private key. But there is another explanation. A valid signature can authorize a separate party to execute a token transfer, provided the token or permission contract recognizes that authorization.
The distinction matters because signing a message and sending a transaction are different actions.
A signature does not necessarily move an asset when it is created. It can instead establish permission that someone else uses later.
Uniswap’s Permit2 system makes this possible for ERC-20 tokens through a standardized authorization contract. It is designed to make token interactions more convenient, reducing repeated approval transactions while allowing developers to place conditions on spending.
The same architecture creates a question that is surprisingly easy to overlook: what exactly does a user authorize when they sign, and which decisions remain in someone else’s hands?
An examination of Permit2’s source code reveals that the answer depends on which permission mechanism is being used. In one standard transfer flow, the signature specifies the token, spending limit, authorized spender, nonce, and deadline.
The destination receiving the tokens, however, is not necessarily part of that signed information.
That small technical distinction changes how the entire authorization should be understood.
A Signature Can Authorize an Action Without Executing It
Most wallet users recognize a transaction when they see one.
Sending ETH, swapping USDC, or depositing tokens into a lending protocol generally involves approving an operation that will be submitted to the blockchain.
The transaction becomes part of the chain’s history, and the user or another designated party pays the associated execution fee.
A message signature works differently.
The wallet uses its signing key to generate cryptographic proof that the account authorized particular data. Producing that signature does not, by itself, require an Ethereum transaction.
This is useful for ordinary authentication.
Under the Sign-In with Ethereum standard, a website can request a signed authentication message to verify that someone controls a wallet address.
The website receives proof of authorization without the user making an on-chain payment.
A legitimate sign-in message is not a token-spending permission.
But Ethereum applications can also ask users to sign structured data representing financial authorizations.
The EIP-712 standard defines a way to sign structured messages containing recognizable fields rather than arbitrary strings of bytes.
Those fields might describe a token, an authorized spender, a maximum amount, and an expiration.
A smart contract can subsequently verify the signature and act on the permission it represents.
This creates an important separation between authorization and execution.
The person signing does not always need to be the person submitting the eventual transaction.
A relayer, application, or authorized spender can submit the signed permission to an appropriate contract. If the contract accepts it and all required conditions are satisfied, the operation can proceed.
The blockchain still records the resulting transaction. It simply does not need to originate from the wallet whose tokens are being moved.
That is why searching exclusively for transactions sent by a wallet can provide an incomplete picture of how its tokens were spent.
A transfer may have been authorized through an earlier signature and executed by a different address.
Three Ways a Wallet Can Give Someone Spending Permission
The ability to spend tokens on another account’s behalf did not begin with Permit2.
It is part of the design of ERC-20 tokens.
A decentralized exchange, for example, typically needs permission to transfer tokens from a user’s wallet when processing a swap.
The conventional method is an on-chain approval.
A user calls the token contract’s approve() function and specifies which address may spend the tokens and how much it may spend.
The permission is stored in the token contract. The authorized spender can later call transferFrom() within the applicable allowance.
This is a standing authorization, not necessarily a one-time transfer.
The ERC-20 approval mechanism explained by Revoke.cash illustrates why these permissions can remain relevant long after the application interaction that originally created them.
Suppose a user approves a decentralized exchange to spend 5,000 USDC.
The exchange does not automatically receive 5,000 USDC when the approval is granted.
Instead, the user has permitted the designated spender to transfer up to that amount, subject to the token’s allowance rules.
The second approach uses signatures rather than requiring a separate approval transaction.
Under EIP-2612, compatible ERC-20 tokens implement a permit() function.
A user signs a message specifying the owner, spender, authorized value, nonce, and deadline.
Someone else can submit that signature to the token contract.
If the signature is valid, the nonce matches, and the deadline has not passed, the contract updates the allowance.
The spender can then use the resulting permission to transfer tokens.
There is an important intermediate step: the signature authorizes the allowance, but the allowance must be established through an on-chain permit call before it can be used.
Permit2 introduces a third architecture.
Rather than requiring every individual token to implement EIP-2612, it provides a separate permission contract that supports signature-based spending after the user has granted the necessary ERC-20 approval to Permit2 itself.
These mechanisms solve related problems but create different forms of authorization.
| Mechanism | What the user initially authorizes | Can permission remain available? |
|---|---|---|
ERC-20 approve() | Spending allowance through an on-chain transaction | Yes, until used, changed, or revoked |
EIP-2612 permit() | A signed authorization to establish an allowance | Yes, after a valid permit is submitted |
Permit2 SignatureTransfer | A nonce-scoped token transfer | One successful use per signature nonce |
Permit2 AllowanceTransfer | A spending allowance managed inside Permit2 | Yes, until exhausted, expired, or revoked |
The difference is not simply whether a signature exists.
It specifies where the permission is stored, how often it may be exercised, and what conditions must be met before an asset can move.
Permit2 Has Two Permission Layers, Not One
The official Permit2 documentation describes two modules with different purposes: SignatureTransfer and AllowanceTransfer.
Both rely on an important prerequisite.
Before Permit2 can move an ERC-20 token, the user must first approve the Permit2 contract through that token’s own approval system.
Applications commonly request a large allowance, sometimes the maximum possible value.
That initial authorization does not mean every third-party application immediately gains unrestricted access to the user’s tokens.
Permit2 imposes additional rules governing which spender can act and under what conditions.
But the initial approval matters because it establishes the underlying permission needed for Permit2 to move the tokens.
Consider a hypothetical wallet holding 2,400 USDC.
The owner previously approved Permit2 to spend up to 10,000 USDC. That allowance remains active.
Several days later, the wallet encounters a website requesting a Permit2 signature.
If the website persuades the owner to sign a valid transfer authorization for an attacker-controlled spender, the spender may be able to execute the transfer through Permit2 without asking the owner to submit another transaction.
The result depends on the particular signed parameters and on whether the underlying ERC-20 allowance remains sufficient.
Now consider how the two Permit2 modules behave.
With SignatureTransfer, a signature can authorize a particular transfer can be authorised within a maximum amount and before a specified deadline. Once a successful transfer consumes its nonce, the signature cannot be used again.
With AllowanceTransfer, the user can authorize a spender to use a standing allowance.
The spender can make multiple qualifying transfers while the allowance remains active, provided the amount and expiration conditions are respected.
Both are useful designs.
A one-time transfer avoids leaving a separate standing spender allowance inside Permit2. A time-bounded allowance can make recurring operations more convenient.
Yet both designs still require the user to understand the signed message.
The authorization remains only as safe as the conditions it actually enforces.
And that leads to an unusual detail in the implementation.
The Spender Is Signed. The Recipient May Not Be.
Ravel examined the structures and hashing logic in Uniswap’s public Permit2 repository to identify what is cryptographically included in a standard SignatureTransfer authorization.
In the PermitHash.sol implementation, the signature hash incorporates token permissions, the spender, a nonce, and a deadline.
The spender is bound to the signature through msg.sender when the hash is reconstructed during verification.
This prevents an unrelated address from simply presenting the signature as though it were the authorized spender’s.
The permitted token and maximum amount are likewise part of the authorization.
But the ordinary signed PermitTransferFrom structure does not contain a recipient address.
Instead, the recipient appears in a separate structure called SignatureTransferDetails, alongside the requested transfer amount.
That distinction becomes visible in the SignatureTransfer.sol source code.
Inside the internal _permitTransferFrom function, the contract checks whether the deadline has passed and whether the requested transfer amount exceeds the signed maximum.
It then processes the nonce, verifies the signature, and calls the token’s transfer function using the recipient specified in the transfer details.
In other words, the standard signature establishes who is authorized to spend, which token can be spent, the maximum amount, and the validity conditions.
The recipient is supplied when the authorized transfer is executed.
This is not automatically a vulnerability.
A legitimate trading application might need flexibility over the eventual destination, and its contract logic can impose further restrictions.
The critical issue is whether the authorized spender enforces the transaction context that the signer intended.
Suppose a user signs a transfer authorization for a legitimate router.
An external attacker cannot simply replace the authorized spender with their own address and expect Permit2 to accept the signature.
But if the authorized router exposes a poorly protected function that allows arbitrary callers to submit someone else’s signed permit while controlling the destination, the router’s integration may become the weak point.
Uniswap’s SignatureTransfer security documentation explicitly warns developers to bind permit usage to the intended caller context.
There is also a stronger signing option.
Permit2 supports permitWitnessTransferFrom, allowing additional structured information to be incorporated into the signature.
For example, an application can bind a trade to extra conditions by including those conditions in the witness and enforcing them correctly.
That additional context could be used to restrict aspects of the intended operation that a basic transfer authorization does not cover.
The broader lesson is that permission boundaries depend on the exact fields signed and the checks performed by the integrating contract.
A wallet displaying a familiar protocol name does not, by itself, explain everything a signature may allow.
How Much Can One Signature Actually Move?
The spending limit in a signature is a meaningful restriction.
It is not merely a warning displayed by the wallet.
Return to the hypothetical account holding 2,400 USDC with a 10,000 USDC underlying approval to Permit2.
Assume the owner signs a SignatureTransfer authorization with these conditions:
| Parameter | Illustrative value |
|---|---|
| Token | USDC |
| Signed maximum | 800 USDC |
| Spender | A specified third-party address |
| Signature deadline | 24 hours after signing |
| Nonce | 513 |
| Underlying ERC-20 allowance | 10,000 USDC |
These are illustrative inputs, not details from a real exploit or an observed wallet.
If the authorized spender attempts to transfer 900 USDC through this signature, Permit2 rejects the request because it exceeds the signed maximum.
If the spender requests 750 USDC within the deadline and satisfies the other requirements, the transfer can succeed.
The wallet’s balance would fall from 2,400 to 1,650 USDC, assuming no other activity and disregarding changes unrelated to the transfer.
Crucially, the spender cannot use the same successful signature again to collect the remaining 50 USDC.
The nonce has been consumed.
This scenario is different from a reusable allowance.
Under Permit2’s AllowanceTransfer module, a standing authorization for 800 USDC could permit a 300 USDC transfer followed by another 500 USDC transfer, while the allowance and underlying approval remain sufficient and unexpired.
The distinction between a single authorized execution and a reusable allowance is central to understanding exposure.
There is another subtle protection in SignatureTransfer: its unordered nonce system.
Instead of requiring signatures to execute in a single numerical sequence, Permit2 stores nonce usage in bitmaps.
A nonce is divided into a word position and a bit position.
Using the hypothetical nonce of 513, Ravel’s calculation gives a word position of 2 and a bit position of 1. The corresponding bit mask is 2.
That result follows directly from the contract’s logic: the upper bits select the bitmap word, while the lowest eight bits identify one of its 256 positions.
Once the bit associated with nonce 513 has been marked as used, another attempt to use that same nonce fails.
The design allows multiple independently authorized signatures to exist without requiring them to be redeemed in sequence.
It also provides a mechanism for invalidating selected signatures before they are used.
These protections are real, but they do not tell the user whether the authorized spender is trustworthy.
An 800 USDC limit can prevent a 900 USDC transfer. It does not prevent the entire permitted 800 USDC from going to an unwanted recipient if the spender has the necessary control and the authorization permits that outcome.
Cryptographic limits define the maximum authority granted.
They do not guarantee that the authority will be exercised for the user’s benefit.
Revoking an Approval Is Not Always the Same as Cancelling a Signature
Discovering a suspicious approval is often straightforward.
A blockchain explorer or approval-management service can inspect allowances that are already recorded on-chain.
Off-chain signatures create a different challenge.
A signed message may exist only in the user’s wallet history, a website’s records, or the possession of whoever received it.
Until the signature is submitted on-chain, there may be no corresponding approval event or token movement for an explorer to display.
Revoke.cash highlights this limitation in its explanation of permit signatures, distinguishing between permissions visible on-chain and signed authorizations that have not yet been exercised.
The distinction becomes particularly important during cancellation.
For a conventional ERC-20 allowance, the owner can generally submit a new approval setting the spender’s allowance to zero.
For an EIP-2612 signature that has already been redeemed, the resulting allowance can likewise be modified or revoked.
But an unused EIP-2612 signature is a different problem.
Simply setting an existing allowance to zero does not necessarily invalidate a previously signed permit whose nonce remains valid.
That permit could still be submitted before its deadline and establish the signed allowance again.
Preventing that outcome requires invalidating the pending signature through a mechanism supported by the token, such as consuming the relevant nonce with another valid permit, or waiting until the original signature expires.
Permit2 has its own mechanisms.
In SignatureTransfer, the owner can use invalidateUnorderedNonces to mark selected nonce bits as unavailable.
If the relevant nonce is invalidated before the signature is redeemed, an attempt to use that signature fails.
AllowanceTransfer works differently.
The AllowanceTransfer.sol implementation includes a lockdown function that can set specified token-spender allowances to zero.
It also provides invalidateNonces, which advances the nonce for a particular owner-token-spender combination.
The distinction matters because setting an existing Permit2 allowance to zero does not, by itself, necessarily invalidate an unused signature capable of establishing that allowance again.
Changing the nonce can prevent a previously signed permit with the old nonce from being accepted.
Users may also revoke the underlying ERC-20 approval granted to Permit2.
That removes a prerequisite for Permit2 to spend the affected token through the revoked approval, even if a separate signature remains cryptographically valid.
It does not erase the signed message itself, and future approval changes must be considered before assuming the old authorization can never matter again.
These are different defenses against different permission mechanisms.
There is no universal cancellation operation that safely covers every possible wallet signature.
The correct response depends on what was signed, which contract verifies it, whether the signature has already been used, and which permissions remain active.
What the Wallet’s Signature Screen Does Not Always Explain
The convenience of signature-based authorization is easy to understand.
Applications can reduce repeated approval transactions, avoid making users pay gas for every authorization, and combine permission management with the operation they want to perform.
For everyday users, those improvements can make decentralized applications substantially easier to use.
The difficulty is that a simple signature prompt may represent a complex legal-like grant of technical authority.
A permission for a particular spender to move 800 USDC before a deadline is not equivalent to a request that proves someone owns a wallet.
Neither is equivalent to a standing unlimited allowance.
A meaningful review of a token-spending signature therefore has to identify the token being authorized, the spender receiving authority, the maximum amount, and how long the authorization remains valid.
For Permit2, it should also distinguish between one-time SignatureTransfer permission and reusable AllowanceTransfer permission.
Those distinctions are not cosmetic.
They determine whether the spender has a single opportunity to execute an authorized transfer or can repeatedly exercise an allowance until it expires or is exhausted.
The destination and any additional trade conditions are equally important.
In a basic SignatureTransfer, the destination is supplied during execution rather than included as an ordinary signed field. That makes the authorized spender’s behavior and the integrating application’s checks especially relevant.
A spender may be a reputable router with carefully implemented restrictions. It may also be an unfamiliar contract whose operations the signer has never examined.
The cryptographic signature can be perfectly valid in either case.
That is what makes this problem different from a simple story about compromised private keys.
The wallet owner may retain exclusive control of those keys throughout the incident.
The signature itself can still grant another party limited authority to move assets.
And because creating the signature does not submit a transaction, the owner may never see the familiar sequence of gas estimation, transaction confirmation, and network execution until the resulting token transfer has already happened.
The missing Send transaction is not necessarily evidence that someone bypassed Ethereum’s security.
It may indicate that Ethereum enforced an authorization the owner had already signed.
The decisive question is not whether the user pressed Send. It is what the user permitted someone else to send.


