Skip to main content

Overview

The Dynamic SDK provides multiple ways to send ERC-20 tokens. You can use the writeContract method with the built-in ABI, or encode the transfer calldata manually for more control.

Prerequisites

Using writeContract

The SDK provides a writeContract method with the built-in Erc20.abi that handles all the encoding for you:
import DynamicSDKSwift
import SwiftBigInt

let sdk = DynamicSDK.instance()

func sendERC20(
    wallet: BaseWallet,
    tokenAddress: String,
    recipient: String,
    amount: String,  // Human-readable amount (e.g., "1.5")
    decimals: Int = 18
) async {
    do {
        // Convert human-readable amount to base units
        let baseUnits = try parseDecimalToBaseUnits(amount, decimals: decimals)

        // Parse the built-in ERC-20 ABI
        guard let abiData = Erc20.abi.data(using: .utf8),
              let abiArray = try JSONSerialization.jsonObject(with: abiData) as? [[String: Any]] else {
            throw DynamicSDKError.custom("Invalid Erc20 ABI")
        }

        let input = WriteContractInput(
            address: tokenAddress,
            abi: abiArray,
            functionName: "transfer",
            args: [recipient, baseUnits]
        )

        let txHash = try await sdk.evm.writeContract(
            wallet: wallet,
            input: input
        )

        print("ERC20 transfer sent!")
        print("Hash: \(txHash)")
    } catch {
        print("Failed to send ERC20: \(error)")
    }
}

// Helper function to convert decimal string to base units
func parseDecimalToBaseUnits(_ value: String, decimals: Int) throws -> BigUInt {
    guard decimals >= 0 && decimals <= 77 else {
        throw TokenError.invalidDecimals
    }

    let parts = value.split(separator: ".", omittingEmptySubsequences: false)
    let wholePart = String(parts[0].isEmpty ? "0" : parts[0])
    let fracPartRaw = parts.count == 2 ? String(parts[1]) : ""

    guard let whole = BigUInt(wholePart) else {
        throw TokenError.invalidAmount
    }

    // Trim fractional part to token's decimal places
    let trimmedFrac = String(fracPartRaw.prefix(decimals))
    let fracPadded = trimmedFrac.padding(toLength: decimals, withPad: "0", startingAt: 0)
    let frac = fracPadded.isEmpty ? BigUInt(0) : (BigUInt(fracPadded) ?? BigUInt(0))

    return whole * BigUInt(10).power(decimals) + frac
}

enum TokenError: LocalizedError {
    case invalidDecimals
    case invalidAmount

    var errorDescription: String? {
        switch self {
        case .invalidDecimals: return "Token decimals must be between 0 and 77"
        case .invalidAmount: return "Invalid token amount"
        }
    }
}
The SDK includes a built-in Erc20.abi constant that contains the standard ERC-20 ABI, including the transfer function. Using writeContract handles all the encoding automatically.

Complete ERC20 Transfer View

import SwiftUI
import DynamicSDKSwift
import SwiftBigInt

struct SendERC20View: View {
    let wallet: BaseWallet

    @State private var tokenAddress = ""
    @State private var recipient = ""
    @State private var amount = ""
    @State private var decimals = "18"
    @State private var txHash: String?
    @State private var isLoading = false
    @State private var error: String?

    var body: some View {
        VStack(spacing: 16) {
            TextField("Token Contract (0x...)", text: $tokenAddress)
                .textFieldStyle(.roundedBorder)
                .autocapitalization(.none)

            TextField("Recipient (0x...)", text: $recipient)
                .textFieldStyle(.roundedBorder)
                .autocapitalization(.none)

            HStack {
                TextField("Amount", text: $amount)
                    .textFieldStyle(.roundedBorder)
                    .keyboardType(.decimalPad)

                TextField("Decimals", text: $decimals)
                    .textFieldStyle(.roundedBorder)
                    .keyboardType(.numberPad)
                    .frame(width: 80)
            }

            Button("Send Tokens") { sendTokens() }
                .buttonStyle(.borderedProminent)
                .disabled(isLoading)

            if let hash = txHash {
                Text("Success: \(hash)")
                    .font(.caption)
                    .foregroundColor(.green)
            }

            if let error = error {
                Text(error).foregroundColor(.red).font(.caption)
            }
        }
        .padding()
    }

    private func sendTokens() {
        isLoading = true
        error = nil

        Task { @MainActor in
            do {
                let tokenDecimals = Int(decimals) ?? 18
                let baseUnits = try parseDecimalToBaseUnits(amount, decimals: tokenDecimals)

                // Parse the built-in ERC-20 ABI
                guard let abiData = Erc20.abi.data(using: .utf8),
                      let abiArray = try JSONSerialization.jsonObject(with: abiData) as? [[String: Any]] else {
                    throw DynamicSDKError.custom("Invalid Erc20 ABI")
                }

                let input = WriteContractInput(
                    address: tokenAddress,
                    abi: abiArray,
                    functionName: "transfer",
                    args: [recipient, baseUnits]
                )

                txHash = try await DynamicSDK.instance().evm.writeContract(
                    wallet: wallet,
                    input: input
                )
            } catch {
                self.error = error.localizedDescription
            }
            isLoading = false
        }
    }

    private func parseDecimalToBaseUnits(_ value: String, decimals: Int) throws -> BigUInt {
        let parts = value.split(separator: ".", omittingEmptySubsequences: false)
        let whole = BigUInt(String(parts[0])) ?? 0
        let fracRaw = parts.count == 2 ? String(parts[1]) : ""
        let fracPadded = String(fracRaw.prefix(decimals)).padding(toLength: decimals, withPad: "0", startingAt: 0)
        let frac = BigUInt(fracPadded) ?? 0
        return whole * BigUInt(10).power(decimals) + frac
    }
}

Common Token Decimals

TokenDecimalsNotes
ETH, WETH18Most ERC-20 tokens use 18
USDC6Circle’s USD Coin
USDT6Tether USD
WBTC8Wrapped Bitcoin
DAI18MakerDAO stablecoin

Best Practices

1. Validate Token Address

Always validate the token contract address before sending:
func isValidEthereumAddress(_ address: String) -> Bool {
    let pattern = "^0x[a-fA-F0-9]{40}$"
    guard let regex = try? NSRegularExpression(pattern: pattern) else {
        return false
    }
    let range = NSRange(location: 0, length: address.utf16.count)
    return regex.firstMatch(in: address, range: range) != nil
}

2. Check Token Balance Before Transfer

// Query token balance using balanceOf(address)
func getTokenBalance(
    wallet: BaseWallet,
    tokenAddress: String,
    chainId: Int
) async throws -> BigUInt {
    let client = sdk.evm.createPublicClient(chainId: chainId)
    // Use readContract to call balanceOf
    let balance = try await client.readContract(
        address: tokenAddress,
        functionName: "balanceOf",
        args: [wallet.address]
    )
    return BigUInt(balance) ?? 0
}

3. Use Appropriate Gas Limits

struct GasLimits {
    static let erc20Transfer = 65_000       // ERC-20 token transfer
    static let erc20Approve = 50_000        // ERC-20 approve
}

Next Steps