Withdraw
Bulk Withdrawal ERC20 Tokens
Overview
The ERC20 Bulk Withdrawal enables you to send multiple token transfers to different addresses in a single transaction. This functionality utilizes a special batcher contract to combine multiple token transfers, reducing gas costs and simplifying transaction management compared to sending multiple individual token transfers.
Key benefits:
- Lower gas costs: Consolidate multiple transfers into a single on-chain transaction
- Simplified transaction management: Single approval workflow for multiple transfers
- Supported on multiple EVM chains: Available on Ethereum, Polygon, Arbitrum, Optimism, and other EVM-compatible networks
Technical Details
When using the bulk withdrawal feature:
- A batcher contract specific to your network processes your transaction.
- The contract calls
batchTransferFromto send tokens to multiple recipients. - The batcher contract uses the ERC20
transferFrommethod to move tokens from your wallet to the recipients. - The transaction appears on-chain as a single contract interaction.
- Each recipient receives their tokens directly from your wallet.
Limitations
- All transfers in a batch must use the same token type.
- Maximum batch size varies by network (typically around 100 recipients per transaction).
- BitGo does not support batch transfers for CryptoPunks and other non-standard ERC20 tokens.
Prerequisites
- Get Started
- Create Wallets
- Deposit Assets
- Enable Bulk Withdrawals of ERC20 Tokens
- Have sufficient balance of the native coin to cover gas fees.
Cookbooks
Need just the steps? Expand a cookbook below to get started:
Bulk Withdrawal ERC20 (MPC)Open Cookbook Bulk Withdrawal ERC20 (Multisig)Open Cookbook1. Build a Bulk Withdrawal Transaction
To create a bulk withdrawal transaction, you'll provide multiple recipients in your transaction request, each specifying the recipient address and amount.
async function buildBulkTokenWithdrawal() {
const walletInstance = await bitgo
.coin('opeth')
.wallets()
.get({ id: '<YOUR_WALLET_ID>' });
// Build a transaction with multiple recipients
const buildResult = await walletInstance.prebuildTransaction({
recipients: [
{
address: '0x1111111111111111111111111111111111111111',
amount: '100000000'
},
{
address: '0x2222222222222222222222222222222222222222',
amount: '200000000'
},
{
address: '0x3333333333333333333333333333333333333333',
amount: '300000000'
}
],
tokenName: 'opeth:usdc'
});
return buildResult;
}
POST /api/v2/{coin}/wallet/{walletId}/tx/build
{
"recipients": [
{
"address": "0x1111111111111111111111111111111111111111",
"amount": "100000000"
},
{
"address": "0x2222222222222222222222222222222222222222",
"amount": "200000000"
},
{
"address": "0x3333333333333333333333333333333333333333",
"amount": "300000000"
}
],
"tokenName": "opeth:usdc"
}
async function sendBulkTokenWithdrawal() {
const walletInstance = await bitgo
.coin('opeth')
.wallets()
.get({ id: '<YOUR_WALLET_ID>' });
// Send a transaction with multiple recipients
return walletInstance.sendMany({
type: 'transfer',
recipients: [
{
address: '0x1111111111111111111111111111111111111111',
amount: '100000000',
tokenName: 'opeth:usdc',
},
{
address: '0x2222222222222222222222222222222222222222',
amount: '200000000',
tokenName: 'opeth:usdc',
},
{
address: '0x3333333333333333333333333333333333333333',
amount: '300000000',
tokenName: 'opeth:usdc',
}
],
walletPassphrase: 'VerySecurePassword1234',
feeOptions: {
maxFeePerGas: '81130354893',
maxPriorityFeePerGas: '71130354893',
}
});
}
In a bulk withdrawal transaction, the batcher contract consolidates the recipients array into a single on-chain interaction. The batcher contract then distributes the tokens to each recipient address according to the specified amounts.
2. Sign and Send Transaction
After building the transaction, sign it and send it to BitGo for processing. The transaction signing process follows the standard pattern for your wallet type.
async function sendBulkTokenWithdrawal() {
const walletInstance = await bitgo
.coin('opeth')
.wallets()
.get({ id: '<YOUR_WALLET_ID>' });
// Build, sign and send in one operation
const result = await walletInstance.sendMany({
recipients: [
{
address: '0x1111111111111111111111111111111111111111',
amount: '100000000'
},
{
address: '0x2222222222222222222222222222222222222222',
amount: '200000000'
},
{
address: '0x3333333333333333333333333333333333333333',
amount: '300000000'
}
],
tokenName: 'opeth:usdc',
walletPassphrase: 'VerySecurePassword1234',
});
return result;
}
Sign the MPC transaction by passing the transaction request ID along with either your unencrypted private key or your wallet passphrase with the encrypted private key. BitGo Express handles the signature-share exchange with BitGo and broadcasts the transaction to the blockchain.
Endpoint: Sign MPC transaction
export BITGO_EXPRESS_HOST="<YOUR_LOCAL_HOST>"
export COIN="<ASSET_ID>"
export WALLET_ID="<YOUR_WALLET_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
export TX_REQUEST_ID="<TX_REQUEST_ID>"
curl -X POST \
http://$BITGO_EXPRESS_HOST/api/v2/$COIN/wallet/$WALLET_ID/signtxtss \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"txRequestId": "'"$TX_REQUEST_ID"'",
"prv": "string, # Either pass just your `prv` or pass your `walletPassphrase`, `keychain`, and `encryptedPrv`
"walletPassphrase": "string",
"keychain": {
"encryptedPrv": "string"
}
}'
export BITGO_EXPRESS_HOST="<YOUR_LOCAL_HOST>"
export COIN="<ASSET_ID>"
export WALLET_ID="<YOUR_WALLET_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
export TX_REQUEST_ID="<TX_REQUEST_ID>"
curl -X POST \
http://$BITGO_EXPRESS_HOST/api/v2/$COIN/wallet/$WALLET_ID/signtxtss \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"txRequestId": "e2f0f3fc-8704-4c9c-946b-601e889175c1"
}'
import dotenv from "dotenv";
dotenv.config();
import { BitGoAPI } from "@bitgo/sdk-api";
import { Tsol } from "@bitgo/sdk-coin-sol";
import { z } from "zod";
const env = z
.object({
USERNAME: z.string().min(1),
PASSWORD: z.string().min(1),
ENV: z.union([z.literal("test"), z.literal("staging"), z.literal("prod")]),
OTP: z.string().optional(),
WALLET_ID: z.string(),
})
.parse(process.env);
const bitgo = new BitGoAPI({ env: env.ENV });
bitgo.register("tsol", Tsol.createInstance);
const coin = bitgo.coin("tsol");
async function auth() {
await bitgo.authenticate({
username: env.USERNAME,
password: env.PASSWORD,
otp: env.OTP,
});
await bitgo.lock();
await bitgo.unlock({ otp: "000000", duration: 3600 });
}
async function main() {
await auth();
const wallet = await coin.wallets().get({ id: env.WALLET_ID });
const params = {
txRequestId: "string", // previously created txRequestId
// one of:
prv: "un-encrypted prv",
// or
keychain: {
encryptedPrv: "encrypted prv"
},
walletPassphrase: "password to decrypt the encryptedPrv"
};
wallet.signTransaction(params).then(function (result) {
// print result details
console.dir(result);
});
}
Step Result
The response includes the transaction details and the status of the transaction:
{
"transfer": {
"coin": "opeth:usdc",
"id": "63727a81cdbc820007b27caa7b76016d",
"wallet": "63726fde0a3c94000758f2790536041d",
"txid": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"height": 0,
"date": "2023-11-14T17:27:29.128Z",
"type": "send",
"value": "-600000000",
"valueString": "-600000000",
"baseValueString": "-600000000",
"baseValueWithoutFeesString": "-600000000",
"feeString": "0",
"payGoFee": 0,
"payGoFeeString": "0",
"state": "signed",
"instant": false
},
"txid": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"status": "signed"
}