5. ERC20 Token Transfers
Encode ERC-20 function calls and propose them as Safe transactions through the Ledger Multisig API.
What you'll learn
Encode ERC-20
transfer()andapprove()calldata using viemCreate a Safe transaction that calls a token contract (value = 0, data = calldata)
Sign and propose the token transaction to the Transaction Service
Prerequisites
Node.js 18+
A Safe on a supported chain where you control an owner key
A private key for a Safe owner: testnet only
An ERC-20 token balance in the Safe (for actual execution)
viem: used for ABI encoding
Install dependencies:
npm install @safe-global/api-kit @safe-global/protocol-kit @safe-global/types-kit viemConfiguration
import SafeApiKitModule from "@safe-global/api-kit";
import SafeModule from "@safe-global/protocol-kit";
import { OperationType } from "@safe-global/types-kit";
import { encodeFunctionData, parseAbi } from "viem";
import { createPublicClient, http } from "viem";
import { sepolia } from "viem/chains";
const SafeApiKit =
typeof SafeApiKitModule === "function"
? SafeApiKitModule
: (SafeApiKitModule as unknown as { default: typeof SafeApiKitModule }).default;
const Safe =
typeof SafeModule === "function"
? SafeModule
: (SafeModule as unknown as { default: typeof SafeModule }).default;
const CHAIN_ID = 11155111n;
const TX_SERVICE_URL = `https://app.multisig.ledger.com/api/safe-transaction-service/${CHAIN_ID}`;
const RPC_URL = "https://ethereum-sepolia-rpc.publicnode.com";
const SAFE_ADDRESS = "0xYourSafeAddress";
const OWNER_PRIVATE_KEY = "your-private-key";The TX_SERVICE_URL points at the Ledger-hosted Transaction Service. Transactions proposed here appear in the Ledger Multisig UI.
Supported chains: Ethereum (1), Optimism (10), BSC (56), Polygon (137), Base (8453), Arbitrum (42161), Sepolia (11155111).
Step-by-step
Key concepts
How ERC-20 calls work through a Safe:
When a Safe sends tokens, it doesn't transfer ETH. It calls a function on the token contract. The Safe is the msg.sender, and the token contract's internal accounting moves the balance.
Safe → token.transfer(recipient, amount) ↓ Token contract: balances[Safe] -= amount balances[recipient] += amount
In Safe transaction terms:
to
Recipient address
Token contract address
value
Amount in wei
"0"
data
"0x"
Encoded transfer(to, amount)
operation
Call
Call
Common ERC-20 patterns
Transfer tokens
Approve a spender
Batch: approve + swap
Combine approval and a DeFi interaction in a single Safe transaction using batch transactions:
Tips and pitfalls
tois the token contract, not the recipient. A common mistake is settingtoto the address you want tokens sent to. Thetofield must be the token contract address. The actual recipient is encoded in thedatafield.valuemust be"0"for ERC-20 calls. Unless you're also sending ETH alongside the token call (rare), set value to zero.Check the token's decimals. Not all tokens use 18 decimals. USDC uses 6, WBTC uses 8. Encoding the wrong amount is a common source of errors:
Example:
Use viem for encoding. The
encodeFunctionDatafunction from viem handles ABI encoding correctly and provides TypeScript type safety. Don't manually construct calldata.Any contract call follows this pattern. ERC-20 is just one example. The same approach works for any smart contract interaction: encode the function call as
data, settoto the contract address, and setvalueto the ETH amount needed (usually"0").Tested playground script is propose-only. The verified example script uses a dummy token address and intentionally does not execute on-chain. For execution, use a real ERC-20 token contract address on your target chain.
Next steps
6. Multi Signature FlowLast updated