Transfer Operations

For years, token transfers behaved like fire-and-forget mail: you put a destination address on the envelope, drop it in the box, and the protocol updates balances without asking whether the recipient can actually handle what arrives. That simplicity fueled adoption, but it also caused avoidable losses when tokens were sent to contracts that didn't implement a receiving flow.

A Safer Alternative

OP-20 introduces a safer alternative: a handshaked "safe" transfer that confirms a contract recipient can accept and process tokens before finalizing the move. If the recipient can't or won't acknowledge, the operation cleanly cancels and the funds never leave the sender.

Tip
OP-20 supports both approaches. However, safe mode is the preferred default whenever a contract may be the recipient, as it prevents tokens from being permanently locked in non-compliant contracts.

The diagram below illustrates the execution flow of the safeTransfer() method.

safeTransfer Flow Transfer Process Flow 1. Initiate safeTransfer safeTransfer(recipient, amount, data) Sender: Alice | Recipient: Bob/Contract | Amount: 1000 2. Validate Sender Balance sender balance >= amount ✓ Sufficient balance confirmed 3. Safety Address Checks ✓ recipient != 0x0000...0000 (zero address) ✓ recipient != genesis block (dead address) ✓ amount > 0 4. Update Balances Atomically sender balance -= amount recipient balance += amount 5. Is Recipient a Contract? NO (EOA) 6a. Complete Transfer Emit Transferred event Transfer successful! YES (Contract) Contract Receiver Handshake 6b. Call Contract Hook onOP20Received(operator, from, amount, data) Contract must implement this function Includes arbitrary data payload for composability 7. Contract Processes Transfer Contract validates and executes custom logic Decides: Accept or Reject 8. Contract Returns Selector Must return: 0xd83e7dbc (Magic value = function selector) 9. Selector Correct? YES 10. Transfer Successful! Emit Transferred event Tokens safely delivered to contract Certified delivery confirmed! NO Revert Wrong selector Tokens stay

The diagram below illustrates the execution flow of the safeTransferFrom() method.

safeTransferFrom Flow Transfer Process Flow 1. Initiate safeTransferFrom safeTransferFrom(from, to, amount, data) Sender: Bob | From: Alice | To: Contract | Amount: 1000 2. sender == from? (Self-transfer check) YES Skip allowance NO 3. Verify & Spend Allowance ✓ Check allowance >= amount ✓ If allowance != uint256.max (infinite): allowance -= amount 4. Validate Owner Balance balance >= amount ✓ Sufficient balance confirmed 5. Safety Address Checks ✓ to != 0x0000...0000 (zero address) ✓ to != genesis block (dead address) ✓ amount > 0 6. Update Balances Atomically from balance -= amount recipient balance += amount 7. Is Recipient a Contract? NO (EOA) 8a. Complete Transfer Emit Transferred event Transfer successful! YES (Contract) Contract Receiver Handshake 8b. Call Contract Hook onOP20Received(operator, from, amount, data) Contract must implement this function Includes arbitrary data payload for composability 9. Contract Processes Transfer Contract validates and executes custom logic Decides: Accept or Reject 10. Contract Returns Selector Must return: 0xd83e7dbc (Magic value = function selector) 11. Selector Correct? YES 12. Transfer Successful! Emit Transferred event Tokens safely delivered to contract Certified delivery confirmed! NO Revert Wrong selector Tokens stay

Using Transfer Methods in a Contract

The following example demonstrates how to call the safeTransfer() method:
safeTransfer exampleassemblyscript
import { u256 } from '@btc-vision/as-bignum/assembly';
import { 
    Address,
    Blockchain, 
    BytesWriter,
    ReentrancyGuard,
    Revert,
    TransferHelper,
    SELECTOR_BYTE_LENGTH,
    U256_BYTE_LENGTH,
    U32_BYTE_LENGTH,
    ADDRESS_BYTE_LENGTH
} from '@btc-vision/btc-runtime/runtime';

@final
export class MyContract extends ReentrancyGuard {
    public constructor() {
        super();
    }
    
    ...

    // This is only to demonstrate how to call another contract method.
    // The TransferHelper class already contains a safeTransfer method ready to be used.
    private callSafeTransfer(token: Address, to: Address, amount: u256, data: Uint8Array) : void{
        // Encode safeTransfer Calldata
        // 1st parameter: selector (method to call->safeTransfer)
        // 2nd parameter: to address
        // 3rd parameter: amount
        // 4th parameter: data
        const calldata = new BytesWriter(
            SELECTOR_BYTE_LENGTH +
            ADDRESS_BYTE_LENGTH +
            U256_BYTE_LENGTH +
            U32_BYTE_LENGTH +
            data.length,
        );
        calldata.writeSelector(TransferHelper.SAFE_TRANSFER_SELECTOR);
        calldata.writeAddress(to);
        calldata.writeU256(amount);
        calldata.writeBytesWithLength(data);

        Blockchain.call(token, calldata);
    }
}
The following example demonstrates how to call the safeTransferFrom() method:
safeTransferFrom exampleassemblyscript
import { u256 } from '@btc-vision/as-bignum/assembly';
import { 
    Address,
    Blockchain, 
    BytesWriter,
    ReentrancyGuard,
    Revert,
    TransferHelper,
    SELECTOR_BYTE_LENGTH,
    U256_BYTE_LENGTH,
    U32_BYTE_LENGTH,
    ADDRESS_BYTE_LENGTH
} from '@btc-vision/btc-runtime/runtime';

@final
export class MyContract extends ReentrancyGuard {
    public constructor() {
        super();
    }
    
    ...

    // This is only to demonstrate how to call another contract method.
    // The TransferHelper class already contains a safeTransferFrom method ready to be used.
    private callSafeTransferFrom(token: Address, from: Address, to: Address, amount: u256, data: Uint8Array) : void{
        // Encode safeTransferFrom Calldata
        // 1st parameter: selector (method to call->safeTransferFrom)
        // 2nd parameter: from address
        // 3rd parameter: to address
        // 4th parameter: amount
        // 5th parameter: data
        const calldata = new BytesWriter(
            SELECTOR_BYTE_LENGTH +
            ADDRESS_BYTE_LENGTH * 2 +
            U256_BYTE_LENGTH +
            U32_BYTE_LENGTH +
            data.length,
        );
        calldata.writeSelector(TransferHelper.SAFE_TRANSFER_FROM_SELECTOR);
        calldata.writeAddress(from);
        calldata.writeAddress(to);
        calldata.writeU256(amount);
        calldata.writeBytesWithLength(data);

        Blockchain.call(token, calldata);
    }
}
The following example demonstrates how to call the transfer() method:
transfer Exampleassemblyscript
import { u256 } from '@btc-vision/as-bignum/assembly';
import { 
    Address,
    Blockchain, 
    BytesWriter,
    ReentrancyGuard,
    Revert,
    TransferHelper,
    SELECTOR_BYTE_LENGTH,
    U256_BYTE_LENGTH,
    U32_BYTE_LENGTH,
    ADDRESS_BYTE_LENGTH
} from '@btc-vision/btc-runtime/runtime';

@final
export class MyContract extends ReentrancyGuard {
    public constructor() {
        super();
    }
    
    ...

    // This is only to demonstrate how to call another contract method.
    // The TransferHelper class already contains a transfer method ready to be used.
    private callTransfer(token: Address, to: Address, amount: u256) : void{
        // Encode transfer Calldata
        // 1st parameter: selector (method to call->transfer)
        // 2nd parameter: to address
        // 3rd parameter: amount
        const calldata = new BytesWriter(
            SELECTOR_BYTE_LENGTH +
            ADDRESS_BYTE_LENGTH +
            U256_BYTE_LENGTH
        );
        calldata.writeSelector(TransferHelper.TRANSFER_SELECTOR);
        calldata.writeAddress(to);
        calldata.writeU256(amount);

        Blockchain.call(token, calldata);
    }
}
The following example demonstrates how to call the transferFrom() method:
transferFrom exampleassemblyscript
import { u256 } from '@btc-vision/as-bignum/assembly';
import { 
    Address,
    Blockchain, 
    BytesWriter,
    ReentrancyGuard,
    Revert,
    TransferHelper,
    SELECTOR_BYTE_LENGTH,
    U256_BYTE_LENGTH,
    U32_BYTE_LENGTH,
    ADDRESS_BYTE_LENGTH
} from '@btc-vision/btc-runtime/runtime';

@final
export class MyContract extends ReentrancyGuard {
    public constructor() {
        super();
    }
    
    ...

    // This is only to demonstrate how to call another contract method.
    // The TransferHelper class already contains a transferFrom method ready to be used.
    private callTransferFrom(token: Address, from: Address, to: Address, amount: u256) : void{
        // Encode transferFrom Calldata
        // 1st parameter: selector (method to call->transferFrom)
        // 2nd parameter: from address
        // 3rd parameter: to address
        // 4th parameter: amount
        const calldata = new BytesWriter(
            SELECTOR_BYTE_LENGTH +
            ADDRESS_BYTE_LENGTH * 2 +
            U256_BYTE_LENGTH
        );
        calldata.writeSelector(TransferHelper.TRANSFER_SELECTOR);
        calldata.writeAddress(from);
        calldata.writeAddress(to);
        calldata.writeU256(amount);

        Blockchain.call(token, calldata);
    }
}