Why use permit for deposits?

Traditional ERC20 flows require two transactions:

  1. approve(spender, amount)
  2. deposit(amount)

With permit, the user signs a message off-chain, and the contract submits that signature on-chain as part of the deposit. The result is a single transaction from the user’s perspective, or even a gas-sponsored transaction if a relayer submits it.

Common use cases

  • User deposits into a vault without a separate approval step
  • One-click onboarding for DeFi applications
  • Gasless or relayed token deposits
  • Batch deposit flows where UX matters more than manual approval management

What this pattern is not

This is not a generic token rescue function, a vesting contract, or a multi-step allowance manager. The goal here is narrow: safely combine permit with a token pull operation.


How ERC20 permit works

EIP-2612 extends ERC20 with a permit function that sets allowance using a signed authorization. The signature includes:

  • owner address
  • spender address
  • value
  • nonce
  • deadline

The token contract verifies the signature and updates allowance if it is valid and not expired.

Important security properties

  • Nonce-based replay protection: each signature can only be used once
  • Deadline: signatures expire, limiting long-lived risk
  • Domain separation: signatures are bound to a specific token contract and chain

Because the token contract enforces these rules, your deposit contract should focus on correct integration and safe token transfer handling.


Contract design goals

A good permit-driven deposit contract should:

  • accept a token address at deployment or per call
  • verify that the signature is used only for the intended deposit
  • transfer tokens using transferFrom after permit succeeds
  • track deposits per user
  • avoid unsafe assumptions about token behavior
  • use checked external calls and clear error handling

The contract below uses OpenZeppelin interfaces and a minimal internal accounting model.


Example: permit-based deposit vault

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

interface IERC20 {
    function transferFrom(address from, address to, uint256 value) external returns (bool);
}

interface IERC20Permit {
    function permit(
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external;
}

contract PermitDepositVault {
    IERC20 public immutable token;

    mapping(address => uint256) public deposited;

    event Deposited(address indexed user, uint256 amount);

    error ZeroAmount();
    error TransferFailed();

    constructor(address tokenAddress) {
        token = IERC20(tokenAddress);
    }

    function depositWithPermit(
        uint256 amount,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external {
        if (amount == 0) revert ZeroAmount();

        // 1. Set allowance using the user's signature
        IERC20Permit(address(token)).permit(
            msg.sender,
            address(this),
            amount,
            deadline,
            v,
            r,
            s
        );

        // 2. Pull tokens into the vault
        bool ok = token.transferFrom(msg.sender, address(this), amount);
        if (!ok) revert TransferFailed();

        // 3. Record the deposit
        deposited[msg.sender] += amount;

        emit Deposited(msg.sender, amount);
    }
}

This contract is intentionally small. It demonstrates the core flow without adding unrelated withdrawal logic or governance features.


Step-by-step walkthrough

1. Store the token as immutable

The vault accepts one ERC20 token, set during deployment:

IERC20 public immutable token;

Using immutable saves gas and prevents accidental token replacement after deployment.

2. Use the token’s permit interface

The contract casts the token address to IERC20Permit and calls permit before pulling funds:

IERC20Permit(address(token)).permit(...);

This assumes the token supports EIP-2612. If it does not, the call reverts. In production, document the supported token explicitly.

3. Pull tokens after approval is set

After permit succeeds, the vault calls:

token.transferFrom(msg.sender, address(this), amount);

This is the actual token movement. The allowance created by permit enables the transfer.

4. Track deposits internally

The contract records how much each user deposited:

deposited[msg.sender] += amount;

This is useful for later withdrawals, reward calculations, or application-specific accounting.


Why this pattern is safer than manual approval flows

A permit-based deposit reduces the time window between approval and transfer. With a traditional approval, a user may approve a contract and leave the allowance open until they later call deposit. If the spender address is compromised or the user misconfigures the allowance, tokens may be at risk.

With permit, the approval is tied to a specific signature and deadline. The contract can immediately consume the allowance in the same transaction, minimizing exposure.

Comparison table

ApproachUser transactionsApproval lifetimeUXRisk profile
approve then deposit2Can remain open until changedModerateHigher if allowance is left unused
permit then deposit1Short, signature-boundBetterLower exposure window
Infinite approval2Long-livedConvenientHighest allowance risk

Best practices for production use

Validate token compatibility

Not every ERC20 token supports permit. Some tokens implement non-standard variants, and some do not support it at all. Before integrating, confirm:

  • the token implements EIP-2612
  • the DOMAIN_SEPARATOR is correct
  • the nonce behavior matches the standard
  • the signature format is v, r, s

If you need broader compatibility, consider a fallback path that supports plain approve plus deposit.

Keep the permit and transfer in one transaction

The main safety advantage comes from atomic execution. Do not split permit and transferFrom into separate user interactions unless you have a strong reason.

Reject zero-value deposits

Zero-value calls can create noise in event logs and accounting. Reverting on zero amounts keeps the state cleaner.

Prefer explicit errors

Custom errors are cheaper and clearer than generic require strings. They also make debugging easier in tests.

Be careful with non-standard ERC20 behavior

Some ERC20 tokens do not return a boolean from transferFrom, while others may behave inconsistently. If you need maximum compatibility, use a well-tested safe transfer library. For a focused example, the interface above assumes standard behavior.


Handling replay and deadline concerns

The contract itself does not need to manage signature replay protection. That responsibility belongs to the token’s permit implementation through nonces.

However, your application should still treat deadlines seriously:

  • short deadlines reduce signature reuse risk
  • very long deadlines increase exposure if a signature is leaked
  • relayers should submit signed permits promptly

A practical policy is to use deadlines measured in minutes or hours, not days, unless the business case requires otherwise.


Extending the pattern safely

You can extend this vault in several directions without changing the core deposit flow.

Add per-user withdrawal balances

If the vault is meant to hold user funds, you can add a withdrawal function that decreases deposited[user] and transfers tokens back. Make sure withdrawals are protected against reentrancy and use checks-effects-interactions.

Support multiple tokens

You can generalize the contract by storing balances per token and per user:

mapping(address => mapping(address => uint256)) public deposited;

Here, the first key is the token address and the second key is the user. This is useful for multi-asset vaults, but it increases complexity and testing scope.

Add relayer support

If a relayer submits the transaction, the signer may not be msg.sender. In that case, the contract should accept the owner address as an argument and use it in the permit call and transferFrom source.

A relayer-friendly signature flow often looks like this:

  • user signs permit off-chain
  • relayer submits depositWithPermit(owner, amount, deadline, v, r, s)
  • contract uses owner as the token holder

This is a common pattern for gasless UX.


Testing scenarios you should cover

A permit-driven deposit contract should be tested with realistic edge cases.

Suggested test cases

  • valid permit and successful deposit
  • expired deadline reverts
  • invalid signature reverts
  • reused signature fails because nonce changed
  • zero amount reverts
  • token without permit support reverts
  • transferFrom failure reverts
  • deposit accounting updates correctly

Example test focus areas

Test caseExpected outcome
Valid signatureDeposit succeeds and balance increases
Expired signatureTransaction reverts
Wrong spender in signatureTransaction reverts
Reused permitTransaction reverts
Insufficient token balanceTransfer fails
Zero amountCustom error ZeroAmount()

Testing against a local fork or a mock token with EIP-2612 support is especially valuable because signature handling is easy to get wrong.


Common mistakes to avoid

Assuming all ERC20s support permit

This is the most common integration error. Always verify token support before deployment.

Forgetting to bind the spender correctly

The signed permit must authorize the vault contract, not a different address. If the spender is wrong, the transfer will fail.

Using overly permissive deadlines

Long deadlines weaken the security benefit of permit. Keep them short unless there is a user experience reason not to.

Ignoring accounting after transfer

Do not update internal balances before the token transfer succeeds. If the transfer fails, state should remain unchanged.

Mixing unrelated logic into the deposit function

Keep the deposit path simple. Complex side effects make it harder to reason about signature usage and failure modes.


When to choose this pattern

Use a permit-driven deposit contract when:

  • you want a smoother ERC20 deposit UX
  • the token supports EIP-2612
  • you want to avoid a separate approval transaction
  • you need atomic approval plus transfer in one call

Avoid it when:

  • the token does not support permit
  • you need to support many legacy tokens
  • your application requires a more complex authorization model
  • the deposit flow is already abstracted by a router or aggregator that handles approvals elsewhere

Summary

A safe ERC20 permit-driven deposit contract combines off-chain authorization with on-chain token movement. The key idea is simple: verify the signed permit, immediately pull the tokens, and record the deposit in the same transaction. This reduces friction for users while keeping the approval window narrow.

For production systems, focus on token compatibility, deadline discipline, and precise accounting. If you keep the deposit path atomic and well-tested, permit can significantly improve both usability and security.

Learn more with useful resources