Skip to main content
TIP-20 provides multiple transfer methods optimized for different use cases, from standard ERC-20 compatibility to memo-attached payments.

Transfer Methods

Standard Transfer

Transfer tokens from the caller to a recipient:
function transfer(address to, uint256 amount) external returns (bool)
Checks:
  • Contract is not paused
  • Recipient is not the zero address or another TIP-20 contract
  • Caller has sufficient balance
  • Both caller and recipient are authorized by the transfer policy
Gas costs:
  • To existing address: ~50,000 gas (0.1 cent)
  • To new address: ~300,000 gas (0.6 cent, includes 250,000 gas state creation cost from TIP-1000)

Transfer From

Transfer tokens on behalf of another address using allowance:
function transferFrom(
    address from,
    address to,
    uint256 amount
) external returns (bool)
Additional checks:
  • Caller has sufficient allowance from from address
  • Allowance is decremented (unless set to type(uint256).max)
Gas costs:
  • Similar to transfer() plus ~5,000 gas for allowance update

Transfer With Memo

Transfer tokens with an attached 32-byte memo:
function transferWithMemo(
    address to,
    uint256 amount,
    bytes32 memo
) external
Emits:
event TransferWithMemo(
    address indexed from,
    address indexed to,
    uint256 amount,
    bytes32 indexed memo
)
Gas costs:
  • Adds ~5,000 gas to standard transfer for memo emission
  • Memo is indexed, enabling efficient event filtering
See Memos for use cases.

Transfer From With Memo

Combines allowance-based transfer with memo attachment:
function transferFromWithMemo(
    address from,
    address to,
    uint256 amount,
    bytes32 memo
) external returns (bool)
Use cases:
  • Payment processors spending on behalf of users
  • Invoice payments with reference tracking
  • Multi-party payment coordination

System Transfers

System Transfer From

Internal precompile-only transfer function:
function systemTransferFrom(
    address from,
    address to,
    uint256 amount
) external returns (bool)
Restrictions:
  • Only callable by the FeeManager precompile (0xfeeC000000000000000000000000000000000000)
  • Bypasses allowance checks
  • Still enforces transfer policy authorization
Purpose: Enables the FeeManager to collect transaction fees without requiring users to approve the precompile.

Fee Management

TIP-20 integrates with the FeeManager precompile for transaction fee deduction:
// Called before transaction execution
function transferFeePreTx(address from, uint256 amount) external

// Called after transaction execution  
function transferFeePostTx(
    address to,
    uint256 refund,
    uint256 actualUsed
) external
Flow:
  1. transferFeePreTx moves estimated fee from user to FeeManager
  2. Transaction executes
  3. transferFeePostTx refunds unused gas to user
Gas accounting:
  • Pre-transfer moves funds without full transfer checks
  • Post-transfer only refunds, minimizing gas overhead
  • Emits Transfer event for actual fee used

Transfer Authorization

All transfers check policy authorization via TIP-403:
modifier transferAuthorized(address from, address to) {
    if (!TIP403_REGISTRY.isAuthorizedSender(transferPolicyId, from)
        || !TIP403_REGISTRY.isAuthorizedRecipient(transferPolicyId, to)) {
        revert PolicyForbids();
    }
    _;
}
Directional checks (TIP-1015):
  • Sender authorization checks if from can send tokens
  • Recipient authorization checks if to can receive tokens
  • Allows asymmetric policies (e.g., vendor credits)
See Compliance for policy details.

Protected Recipients

Transfers cannot be sent to:
modifier validRecipient(address to) {
    // Reject zero address
    if (to == address(0)) revert InvalidRecipient();
    
    // Reject other TIP-20 tokens (0x20C0...)
    if ((uint160(to) >> 64) == 0x20c000000000000000000000) {
        revert InvalidRecipient();
    }
    _;
}
Prevented addresses:
  • address(0) (burning must use burn() functions)
  • Other TIP-20 token contracts (prevents accidental token loss)

Pause State

When a token is paused, all transfer functions revert:
modifier notPaused() {
    if (paused) revert ContractPaused();
    _;
}
Paused functions:
  • transfer(), transferFrom()
  • transferWithMemo(), transferFromWithMemo()
  • mint(), burn()
  • Reward distribution and claims
Not paused:
  • approve() (setting allowances)
  • permit() (gasless approvals)
  • View functions (balances, allowances)

Gas Cost Breakdown

Transfer to Existing Address

ComponentGas Cost
Base transaction cost~21,000
Transfer logic~24,000
Balance updates (2 × SSTORE)~5,000
Total~50,000
Cost at baseline0.1 cent

Transfer to New Address

ComponentGas Cost
Base transaction cost~21,000
Transfer logic~24,000
New state element (balance)250,000
Balance updates~5,000
Total~300,000
Cost at baseline0.6 cent
Note: The 250,000 gas state creation cost is from TIP-1000, which protects against adversarial state growth attacks.

First Transaction from New Account

ComponentGas Cost
Base transaction cost~21,000
Account creation (nonce 0→1)250,000
Transfer logic~24,000
Balance updates~5,000
Total~300,000
Cost at baseline0.6 cent
Combined onboarding cost:
  • Receive tokens (new balance): ~300,000 gas (0.6 cent)
  • First send (account creation): ~300,000 gas (0.6 cent)
  • Total: ~600,000 gas (1.2 cent)

Transfer With Memo

ComponentGas Cost
Standard transfer~50,000 or ~300,000
Memo event emission~5,000
Total~55,000 or ~305,000

Allowances

Approve

Set spending allowance for another address:
function approve(address spender, uint256 amount) external returns (bool)
Gas cost: ~45,000 gas (new allowance) or ~5,000 gas (update) Common pattern:
// Infinite approval (saves gas on future transfers)
token.approve(spender, type(uint256).max);

Permit (EIP-2612)

Gasless approval via off-chain signature (TIP-1004):
function permit(
    address owner,
    address spender,
    uint256 value,
    uint256 deadline,
    uint8 v,
    bytes32 r,
    bytes32 s
) external
Benefits:
  • No separate approval transaction needed
  • User signs off-chain message
  • Relayer or spender submits permit + action in one transaction
Gas cost: ~50,000 gas (paid by transaction submitter)

Reward Accounting

Transfers automatically update reward accounting for opted-in addresses:
function _transfer(address from, address to, uint256 amount) internal {
    // Update rewards for sender (if opted-in)
    address fromsRewardRecipient = _updateRewardsAndGetRecipient(from);
    
    // Update rewards for receiver (if opted-in)
    address tosRewardRecipient = _updateRewardsAndGetRecipient(to);
    
    // Update opted-in supply tracking
    if (fromsRewardRecipient != address(0)) {
        if (tosRewardRecipient == address(0)) {
            optedInSupply -= uint128(amount);
        }
    } else if (tosRewardRecipient != address(0)) {
        optedInSupply += uint128(amount);
    }
    
    // Perform balance updates
    balanceOf[from] -= amount;
    balanceOf[to] += amount;
    
    emit Transfer(from, to, amount);
}
Reward updates:
  • Accrues pending rewards before balance changes
  • Updates opted-in supply tracking
  • No additional gas for non-opted-in transfers
  • Opted-in transfers add ~10,000 gas

Error Conditions

error ContractPaused();              // Token is paused
error InsufficientBalance(...);      // Sender has insufficient balance
error InsufficientAllowance();       // Insufficient allowance for transferFrom
error InvalidRecipient();            // Invalid recipient address
error PolicyForbids();               // Transfer blocked by policy
error ProtectedAddress();            // Cannot transfer to protected address

Integration Example

import { ITIP20 } from "./interfaces/ITIP20.sol";

contract PaymentProcessor {
    ITIP20 public immutable token;
    
    constructor(ITIP20 _token) {
        token = _token;
    }
    
    // Process invoice payment with reference
    function payInvoice(
        address vendor,
        uint256 amount,
        bytes32 invoiceId
    ) external {
        // Transfer with memo for reconciliation
        token.transferFromWithMemo(
            msg.sender,
            vendor,
            amount,
            invoiceId
        );
        
        emit InvoicePaid(msg.sender, vendor, invoiceId, amount);
    }
    
    event InvoicePaid(
        address indexed payer,
        address indexed vendor,
        bytes32 indexed invoiceId,
        uint256 amount
    );
}

Best Practices

Gas Optimization

  • Use infinite approvals (type(uint256).max) to save gas on repeated transfers
  • Batch transfers when possible to amortize transaction costs
  • Use memos only when needed for reconciliation
  • Consider permit for gasless approval flows

Error Handling

  • Check token balance before attempting transfers
  • Verify transfer policy authorization if known
  • Handle paused state gracefully
  • Use try/catch for external token transfers

Security

  • Never transfer to computed addresses without validation
  • Verify recipient is not another TIP-20 contract
  • Check transfer success return value
  • Consider reentrancy when transferring to contracts

See Also

  • Memos - Payment references and reconciliation
  • Compliance - Transfer policy integration
  • TIP-1000 - State creation costs
  • TIP-1015 - Directional authorization