はじめに
DeFi(分散型金融)のBot開発は、CEX(中央集権型取引所)のBot開発とは根本的に異なります。取引所APIの代わりにブロックチェーンのスマートコントラクトと直接対話し、注文の概念がなく、トランザクションの送信とガス代の支払いが取引の基本単位となります。
本記事では、Ethereum(web3.py/ethers.js)とSolana(Anchor/Raydium SDK)の両方をカバーし、オンチェーン自動取引システムの実践的な構築手法を解説します。
CEX Bot vs DeFi Botの比較
| 項目 | CEX Bot | DeFi Bot |
|---|
| 取引の仕組み | 注文→マッチング | トランザクション→オンチェーン実行 |
| コスト | 取引手数料 | ガス代 + プロトコル手数料 |
| 速度制約 | APIレート制限 | ブロック生成時間(12秒/Eth) |
| カストディ | 取引所に預託 | 自己管理(ウォレット) |
| 透明性 | 不明(ブラックボックス) | 完全に公開(オンチェーン) |
| MEVリスク | なし | サンドイッチ攻撃、フロントランニング |
| 障害時 | 取引所が対応 | 自己責任(資金ロスの可能性) |
web3.py / ethers.js の基礎
web3.py(Python)
from web3 import Web3
from eth_account import Account
import os
w3 = Web3(Web3.HTTPProvider(os.getenv("ETH_RPC_URL")))
print(f"接続状態: {w3.is_connected()}")
print(f"最新ブロック: {w3.eth.block_number}")
account = Account.from_key(os.getenv("PRIVATE_KEY"))
print(f"アドレス: {account.address}")
balance = w3.eth.get_balance(account.address)
print(f"ETH残高: {w3.from_wei(balance, 'ether')} ETH")
ERC20_ABI = [
{
"inputs": [{"name": "account", "type": "address"}],
"name": "balanceOf",
"outputs": [{"name": "", "type": "uint256"}],
"stateMutability": "view",
"type": "function",
}
]
USDC_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
usdc = w3.eth.contract(address=USDC_ADDRESS, abi=ERC20_ABI)
usdc_balance = usdc.functions.balanceOf(account.address).call()
print(f"USDC残高: {usdc_balance / 1e6}")
ethers.js(TypeScript/JavaScript)
import { ethers } from "ethers";
const provider = new ethers.JsonRpcProvider(process.env.ETH_RPC_URL);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
console.log(`アドレス: ${wallet.address}`);
const balance = await provider.getBalance(wallet.address);
console.log(`ETH残高: ${ethers.formatEther(balance)} ETH`);
const usdcAbi = ["function balanceOf(address) view returns (uint256)"];
const usdc = new ethers.Contract(
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
usdcAbi,
provider
);
const usdcBalance = await usdc.balanceOf(wallet.address);
console.log(`USDC残高: ${ethers.formatUnits(usdcBalance, 6)}`);
RPCプロバイダーの比較
| プロバイダー | 無料枠 | レート制限 | WebSocket | 特徴 |
|---|
| Alchemy | 300M CU/月 | 660 req/sec | 対応 | 最も安定、Enhanced API |
| Infura | 100K req/日 | 10 req/sec | 対応 | 老舗、MetaMask使用 |
| QuickNode | 制限あり | プランによる | 対応 | 高速、マルチチェーン |
| Helius (Solana) | 50K req/日 | — | 対応 | Solana特化 |
| 自前ノード | 無制限 | なし | 対応 | 低レイテンシー、運用コスト高 |
DEXインタラクション
Uniswap V3でのスワップ実装
from web3 import Web3
import json
SWAP_ROUTER = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"
SWAP_ROUTER_ABI = json.loads("""[{
"inputs": [{
"components": [
{"name": "tokenIn", "type": "address"},
{"name": "tokenOut", "type": "address"},
{"name": "fee", "type": "uint24"},
{"name": "recipient", "type": "address"},
{"name": "amountIn", "type": "uint256"},
{"name": "amountOutMinimum", "type": "uint256"},
{"name": "sqrtPriceLimitX96", "type": "uint160"}
],
"name": "params",
"type": "tuple"
}],
"name": "exactInputSingle",
"outputs": [{"name": "amountOut", "type": "uint256"}],
"stateMutability": "payable",
"type": "function"
}]""")
def swap_exact_input(
w3: Web3,
account,
token_in: str,
token_out: str,
amount_in: int,
fee: int = 3000,
slippage_pct: float = 0.5,
) -> str:
"""Uniswap V3でトークンをスワップ"""
router = w3.eth.contract(
address=SWAP_ROUTER, abi=SWAP_ROUTER_ABI
)
amount_out_min = 0
token_contract = w3.eth.contract(
address=token_in,
abi=[{
"inputs": [
{"name": "spender", "type": "address"},
{"name": "amount", "type": "uint256"},
],
"name": "approve",
"outputs": [{"name": "", "type": "bool"}],
"stateMutability": "nonpayable",
"type": "function",
}],
)
approve_tx = token_contract.functions.approve(
SWAP_ROUTER, amount_in
).build_transaction({
"from": account.address,
"nonce": w3.eth.get_transaction_count(account.address),
"gas": 60000,
"maxFeePerGas": w3.eth.gas_price * 2,
"maxPriorityFeePerGas": w3.to_wei(2, "gwei"),
})
signed = account.sign_transaction(approve_tx)
w3.eth.send_raw_transaction(signed.raw_transaction)
swap_params = {
"tokenIn": token_in,
"tokenOut": token_out,
"fee": fee,
"recipient": account.address,
"amountIn": amount_in,
"amountOutMinimum": amount_out_min,
"sqrtPriceLimitX96": 0,
}
tx = router.functions.exactInputSingle(swap_params).build_transaction({
"from": account.address,
"nonce": w3.eth.get_transaction_count(account.address),
"gas": 250000,
"maxFeePerGas": w3.eth.gas_price * 2,
"maxPriorityFeePerGas": w3.to_wei(2, "gwei"),
})
signed = account.sign_transaction(tx)
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
return receipt["transactionHash"].hex()
Uniswap手数料ティアの選択
| プール手数料 | 用途 | 主なペア例 |
|---|
| 0.01% (100) | ステーブルコイン同士 | USDC/USDT |
| 0.05% (500) | 安定ペア | ETH/USDC |
| 0.30% (3000) | 標準ペア | ETH/WBTC |
| 1.00% (10000) | エキゾチックペア | 新規トークン |
ガス最適化
EIP-1559ガス戦略
def estimate_gas_params(w3: Web3) -> dict:
"""EIP-1559ガスパラメータを最適化"""
latest_block = w3.eth.get_block("latest")
base_fee = latest_block["baseFeePerGas"]
priority_fees = []
for i in range(5):
block = w3.eth.get_block(latest_block["number"] - i)
if block["transactions"]:
priority_fees.append(w3.to_wei(2, "gwei"))
avg_priority = sum(priority_fees) // len(priority_fees)
return {
"maxFeePerGas": base_fee * 2 + avg_priority,
"maxPriorityFeePerGas": avg_priority,
}
def estimate_transaction_cost(
w3: Web3, gas_used: int
) -> dict:
"""トランザクションコストを見積もり"""
gas_params = estimate_gas_params(w3)
eth_price_usd = 3500
cost_wei = gas_used * gas_params["maxFeePerGas"]
cost_eth = w3.from_wei(cost_wei, "ether")
cost_usd = float(cost_eth) * eth_price_usd
return {
"gas_used": gas_used,
"cost_eth": float(cost_eth),
"cost_usd": cost_usd,
}
ガスコストの目安
| 操作 | ガス使用量 | ETH (@30 gwei) | USD (@$3,500) |
|---|
| ETH送金 | 21,000 | 0.00063 | $2.2 |
| ERC-20 Transfer | 65,000 | 0.00195 | $6.8 |
| ERC-20 Approve | 46,000 | 0.00138 | $4.8 |
| Uniswap V3 Swap | 150,000 | 0.0045 | $15.8 |
| フラッシュローン | 300,000+ | 0.009+ | $31.5+ |
| Multicall (3tx) | 200,000 | 0.006 | $21.0 |
フラッシュローンの実装
Aaveフラッシュローン
フラッシュローンは、同一トランザクション内で借入と返済を完了する無担保ローンです。アービトラージや清算に活用されます。
pragma solidity ^0.8.20;
import "@aave/v3-core/contracts/flashloan/base/FlashLoanSimpleReceiverBase.sol";
import "@aave/v3-core/contracts/interfaces/IPoolAddressesProvider.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract FlashLoanArbitrage is FlashLoanSimpleReceiverBase {
address public owner;
constructor(address _addressProvider)
FlashLoanSimpleReceiverBase(
IPoolAddressesProvider(_addressProvider)
)
{
owner = msg.sender;
}
function executeOperation(
address asset,
uint256 amount,
uint256 premium,
address initiator,
bytes calldata params
) external override returns (bool) {
uint256 amountOwed = amount + premium;
IERC20(asset).approve(
address(POOL), amountOwed
);
return true;
}
function requestFlashLoan(
address asset,
uint256 amount
) external {
require(msg.sender == owner, "Not owner");
POOL.flashLoanSimple(
address(this),
asset,
amount,
"",
0
);
}
}
Balancerフラッシュローン
import "@balancer-labs/v2-interfaces/contracts/vault/IVault.sol";
import "@balancer-labs/v2-interfaces/contracts/vault/IFlashLoanRecipient.sol";
contract BalancerFlashLoan is IFlashLoanRecipient {
IVault private constant vault =
IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);
function receiveFlashLoan(
IERC20[] memory tokens,
uint256[] memory amounts,
uint256[] memory feeAmounts,
bytes memory userData
) external override {
require(msg.sender == address(vault), "Not vault");
for (uint256 i = 0; i < tokens.length; i++) {
tokens[i].transfer(
address(vault),
amounts[i] + feeAmounts[i]
);
}
}
}
フラッシュローンプロバイダーの比較
| プロバイダー | 手数料 | 最大借入額 | 対応チェーン |
|---|
| Aave V3 | 0.05% | プール残高まで | Ethereum, Arbitrum, Optimism等 |
| Balancer V2 | 0% | Vault残高まで | Ethereum, Polygon等 |
| dYdX | 0% | プール残高まで | Ethereum |
| Uniswap V3 | スワップ手数料 | プール流動性まで | Ethereum, 主要L2 |
MEV保護
MEVとは
MEV(Maximal Extractable Value)は、トランザクションの順序操作によってバリデーターや検索者が得られる利益です。一般ユーザーにとってはサンドイッチ攻撃などの形で損失の原因となります。
サンドイッチ攻撃の仕組み
1. あなた: USDC → ETH のスワップTXをメンプールに送信
2. 攻撃者: あなたのTXの前に ETH を大量購入(フロントラン)
3. あなた: ETH価格が上昇した状態で購入(不利な価格で約定)
4. 攻撃者: あなたのTXの後に ETH を売却(バックラン)→ 利益確保
Flashbots Protectの活用
from web3 import Web3
FLASHBOTS_RPC = "https://rpc.flashbots.net"
w3_flashbots = Web3(Web3.HTTPProvider(FLASHBOTS_RPC))
def send_private_transaction(w3, account, tx_data):
"""Flashbots経由でプライベートトランザクションを送信"""
signed_tx = account.sign_transaction(tx_data)
tx_hash = w3.eth.send_raw_transaction(
signed_tx.raw_transaction
)
return tx_hash
MEV保護の選択肢
| 方法 | 保護レベル | コスト | 対応チェーン |
|---|
| Flashbots Protect | 高 | 無料 | Ethereum |
| MEV Blocker | 高 | 無料 | Ethereum |
| プライベートRPC | 中 | 有料 | 各チェーン |
| 高スリッページ設定回避 | 低 | なし | 全チェーン |
| 分割注文 | 中 | ガス増加 | 全チェーン |
Multicallバッチ処理
複数コントラクト呼び出しの一括実行
from multicall import Call, Multicall
def batch_balance_check(
w3: Web3,
wallet: str,
tokens: list[dict],
) -> dict:
"""複数トークン残高を一括取得(ガス節約)"""
calls = [
Call(
token["address"],
["balanceOf(address)(uint256)", wallet],
[[token["symbol"], lambda x: x]],
)
for token in tokens
]
result = Multicall(calls, _w3=w3)()
return result
tokens = [
{"symbol": "USDC", "address": "0xA0b8..."},
{"symbol": "WETH", "address": "0xC02a..."},
{"symbol": "DAI", "address": "0x6B17..."},
]
balances = batch_balance_check(w3, account.address, tokens)
イベント監視:清算候補の検出
import asyncio
from web3 import Web3
async def monitor_liquidation_candidates(w3: Web3):
"""Aaveの清算候補をリアルタイム監視"""
AAVE_POOL = "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2"
borrow_filter = w3.eth.filter({
"address": AAVE_POOL,
"topics": [
w3.keccak(text="Borrow(address,address,address,uint256,uint8,uint256,uint16)")
],
})
while True:
events = borrow_filter.get_new_entries()
for event in events:
user = event["args"]["onBehalfOf"]
user_data = aave_pool.functions.getUserAccountData(
user
).call()
health_factor = user_data[5] / 1e18
if health_factor < 1.1:
print(
f"清算候補検出: {user} "
f"HF={health_factor:.4f}"
)
await asyncio.sleep(1)
Solana版:Anchor + Raydium SDK
Solana開発環境
sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"
cargo install --git https://github.com/coral-xyz/anchor anchor-cli
anchor init my-defi-bot
cd my-defi-bot
Raydium SDKでのスワップ
import { Connection, Keypair, PublicKey } from "@solana/web3.js";
import { Raydium } from "@raydium-io/raydium-sdk-v2";
const connection = new Connection(process.env.SOLANA_RPC_URL!);
const wallet = Keypair.fromSecretKey(
Buffer.from(JSON.parse(process.env.SOLANA_PRIVATE_KEY!))
);
async function swapOnRaydium(
inputMint: string,
outputMint: string,
amountIn: number,
slippageBps: number = 50,
) {
const raydium = await Raydium.load({
connection,
owner: wallet,
cluster: "mainnet",
});
const routes = await raydium.api.fetchSwapRoutes({
inputMint,
outputMint,
amount: amountIn,
slippageBps,
});
const { execute } = await raydium.swap({
swapResponse: routes,
});
const txId = await execute();
console.log(`スワップ完了: https://solscan.io/tx/${txId}`);
return txId;
}
await swapOnRaydium(
"So11111111111111111111111111111111111111112",
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
1_000_000_000,
);
Ethereum vs Solanaの比較
| 項目 | Ethereum | Solana |
|---|
| ブロック時間 | 12秒 | 400ms |
| トランザクション費用 | $5-50 | $0.001-0.01 |
| TPS | 15-30 | 3,000-65,000 |
| プログラミング | Solidity | Rust (Anchor) |
| MEVリスク | 高(サンドイッチ) | 中(Jito MEV) |
| DeFi TVL | $50B+ | $8B+ |
| 開発者ツール | 成熟 | 急速に改善中 |
テストネット検証フロー
本番デプロイ前のチェックリスト
| ステップ | 内容 | 必須 |
|---|
| 1 | ユニットテスト(Foundry/Hardhat) | 必須 |
| 2 | Goerli/Sepoliaテストネットでの動作確認 | 必須 |
| 3 | Forkテスト(本番状態のフォーク) | 推奨 |
| 4 | ガスコスト vs 期待利益のシミュレーション | 必須 |
| 5 | スリッページ・MEVの影響分析 | 推奨 |
| 6 | 少額での本番テスト($100以下) | 必須 |
Foundryによるフォークテスト
forge test --fork-url $ETH_RPC_URL \
--fork-block-number 19500000 \
-vvv
forge test --gas-report
ガスコスト vs 利益のシミュレーション
def simulate_profitability(
gross_profit_usd: float,
gas_cost_usd: float,
success_rate: float = 0.6,
attempts_per_day: int = 10,
) -> dict:
"""DeFi Bot運用の損益シミュレーション"""
expected_profit = gross_profit_usd * success_rate
expected_cost = gas_cost_usd
net_per_attempt = expected_profit - expected_cost
daily_pnl = net_per_attempt * attempts_per_day
monthly_pnl = daily_pnl * 30
annual_pnl = daily_pnl * 365
return {
"net_per_attempt": net_per_attempt,
"daily_pnl": daily_pnl,
"monthly_pnl": monthly_pnl,
"annual_pnl": annual_pnl,
"breakeven_success_rate": gas_cost_usd / gross_profit_usd,
}
result = simulate_profitability(
gross_profit_usd=50,
gas_cost_usd=20,
success_rate=0.5,
attempts_per_day=20,
)
print(f"日次PnL: ${result['daily_pnl']:.2f}")
print(f"月次PnL: ${result['monthly_pnl']:.2f}")
print(f"損益分岐成功率: {result['breakeven_success_rate']:.1%}")
まとめ
DeFi Bot開発は、CEX Botと比較して技術的な複雑性が高く、リスクも大きい反面、透明性が高く、フラッシュローンなどの独自機能を活用できるメリットがあります。
開発の推奨ステップ
- テストネットでweb3.py/ethers.jsの基本操作を習得
- フォークテストでスマートコントラクトの動作を検証
- ガスコスト vs 期待利益のシミュレーションで収益性を確認
- MEV保護(Flashbots)を必ず導入
- 少額($100以下)で本番検証
- 段階的にスケールアップ
重要な注意点
- 秘密鍵の管理: 環境変数やシークレットマネージャーで管理。コードにハードコードしない
- Approve量の制限: 無制限approveを避け、必要最小限に設定
- コントラクト検証: 相互作用するコントラクトのソースコードを必ず確認
- ガス代の変動: Ethereumのガスはネットワーク混雑で10倍以上変動する可能性がある
- スマートコントラクトリスク: ハッキングやバグによる資金喪失リスクが常に存在する