3
フォロー
1444
フォロワー

分散型取引所で注文を実装する方法 - Curve を例に

作成日:: 2025-03-13 14:28:33, 更新日:: 2025-03-14 17:27:53
comments   1
hits   933

なぜ注文する必要があるのですか?

暗号通貨市場では、取引機会はつかの間のものであることが多く、特に裁定取引の機会は数分間しか続かない場合があります。手動操作に頼ると、ユーザーは最適な機会を時間内に捉えることができなかったり、最適な価格を逃したりする可能性があります。たとえば、少し前にBybitへのハッカー攻撃によりUSDeが切り離され、価格が大きく変動しました。当時、sUSDeを購入して償還することによる年率収益率は300%を超えていました。この高利回りウィンドウは通常非常に短い時間しか続かず、事前に注文を設定せずに手動取引で市場のリズムに追いつくのは困難です。そのため、注文配置機能は分散型取引所(DEX)ユーザーにとって重要なツールとなり、取引効率が大幅に向上しました。

DEX注文の原則と欠点

複数の DEX が指値注文機能を提供しており、それぞれ実装と料金体系が異なります。 Cow Swap と Odos を例に挙げてみましょう。基本原則は、アグリゲーター機能を使用して、複数の DEX の流動性と価格をリアルタイムで監視することです。市場価格が制限条件を満たすと、テイカーによって注文がトリガーされ、スマートコントラクトが自動的に取引を実行し、ガス料金を支払います。 Cow はユーザーの注文をオフチェーンでも保存し、その後「ソルバー」と呼ばれる分散ノードのグループによって競争的にマッチングされますが、最終的にはオンチェーンで実行されます。つまり、GAS は免除されますが、これらの DEX によって完了したトランザクションによって生成された追加利益で GAS 料金を賄うことができます。

しかし、これは問題を引き起こします。注文の 1 つが 90U の価格で保留中であり、アグリゲータが特定の DEX の価格が 80U に下がったことを監視すると、アグリゲータがトランザクションを実行します。注文は最終的に 90U の価格で実行され、10U の利益が発生します。この利益はどのように分配されるのでしょうか。実際の操作では、プラットフォームによって処理方法が異なります。 Cow Swap を例にとると、そのメカニズムでは、実行価格が制限価格よりも良い場合、生成された余剰 (つまり、10U) がプラットフォームとユーザーの間で分割され、Cow Swap が 50%、ユーザーが残りの 50% を取得することが明確に規定されています。オドスは余剰金を国庫に預ける。本質的には、あなたの注文は DEX 取引所によってリスクなしで裁定取引されます。

さらに、取引手数料を節約するために、DEX 注文は集約取引、つまり、一度に多くの取引がパッケージ化され、ETH のブロックは 12 秒であるため、いくつかの可能性のある機会を逃すことになります。もちろん、DEX には、より多くのパスの検索、オフライン マッチング、GAS の節約など、ほとんどのユーザーにとって十分な多くの利点がまだあります。

独自のプログラムで注文するメリット

DEX の集約された注文に依存する場合と比較して、スマート コントラクトを介して直接取引することには独自の利点があります。まず、ユーザーは注文実行ロジックとすべての収益を完全に制御できます。第二に、独自の注文を行うことで、集約された取引のパッケージング遅延を回避し、市場の変化に迅速に対応し、特にボラティリティが高いときに 12 秒以内にチャンスをつかむことができます。さらに、カスタム保留注文では、プラットフォームのプリセット機能に制限されることなく、複雑な条件(マルチアセットポートフォリオ取引やストッププロフィット、ストップロスなど)を柔軟に設定できます。ただし、これには一定のプログラミングスキルが必要であり、ガス料金を自分で支払う必要があり、チェーンにセキュリティ上のリスクが生じる可能性があります。したがって、独自の注文を出すことは、高い技術力を持ち、最大限の利益を追求する上級ユーザーに適しています。

秘密鍵の保護

独自のプログラムでスマート コントラクトを操作したい場合、秘密鍵のセキュリティが間違いなく最大の懸念事項となります。私が思いついた解決策は、Python を使用して自分の秘密鍵をオフラインで暗号化し、暗号化された暗号文をホストを実行しているサーバー上に保存することです。復号化されたパスワードは、FMZ プラットフォーム ロボットのパラメータを使用して渡され、プログラムはホスト上で実行され、それを読み取って復号化します (プログラムの実行後に削除できます)。この方法であれば、サーバーがハッキングされても問題はありませんが、FMZ アカウントが漏洩しないように注意する必要があります。オフネットワークのプレーンテキストが 1 つだけ含まれるため、セキュリティは許容範囲内です。 具体的なコードは以下のとおりで、オフグリッドでローカルに実行できます。

from web3 import Web3  # Web3.py 是与以太坊区块链交互的Python库
import json  # 用于处理JSON数据
import time  # 用于设置时间间隔
import requests  # 用于发送HTTP请求
from cryptography.fernet import Fernet
import base64
import hashlib
from datetime import datetime
from hexbytes import HexBytes

def generate_key(password: str) -> bytes:
    """通过用户密码生成 AES 密钥"""
    return base64.urlsafe_b64encode(hashlib.sha256(password.encode()).digest())

def encrypt_private_key(private_key: str, password: str) -> str:
    """使用密码加密私钥"""
    key = generate_key(password)
    cipher = Fernet(key)
    return cipher.encrypt(private_key.encode()).decode()

def decrypt_private_key(encrypted_key: str, password: str) -> str:
    """使用密码解密私钥"""
    key = generate_key(password)
    cipher = Fernet(key)
    return cipher.decrypt(encrypted_key.encode()).decode()

def save_key_to_file(key, file_path):
    # 将加密密钥保存到txt文件
    with open(file_path, 'w') as file:
        file.write(key) 

def load_key_from_file(file_path):
    # 从txt文件读取加密密钥
    with open(file_path, 'r') as file:
        return str(file.read())

def main():
    encrypt_key = encrypt_private_key('my_private_key', 'password') # my_private_key是自己的私钥,password是自己设置的密码
    save_key_to_file(encrypt_key,'encrypt_key.txt')
    print("加密后的私钥", encrypt_key)
    decrypt_key = decrypt_private_key(load_key_from_file('encrypt_key.txt'), 'password')
        print("解密的私钥", decrypt_key)

ETHをリンクする前の準備

Web3.py は、Ethereum ネットワークと対話するための強力な Python ライブラリです。次のコマンドでインストールします: pip install web3。その機能は、開発者と Ethereum ノード間の通信を簡素化し、残高の照会、スマート コントラクトの呼び出し、トランザクションの送信などの操作をサポートすることです。たとえば、口座残高を確認するには数行のコードしか必要ないため、Web3.py は DApp の構築やトランザクションの自動化に最適なツールになります。

python
from web3 import Web3
w3 = Web3(Web3.HTTPProvider('你的RPC地址'))
balance = w3.eth.get_balance('0x某个地址')
print(w3.from_wei(balance, 'ether'))

RPC (リモート プロシージャ コール) は、Ethereum ノードによって提供される通信インターフェイスであり、HTTP または WebSocket プロトコルを介して JSON リクエストを送信することでブロックチェーンと対話します。たとえば、eth_blockNumber は最新のブロックの高さを照会できます。ローカルノードの実行にはコストがかかるため、開発者は通常、サードパーティの RPC プロバイダーに依存します。一般的な選択肢は次のとおりです。

  • Infura: MetaMask のデフォルト サービス。使いやすいですが、無料割り当てが少ないです (1 日あたり 100,000 リクエスト)。
  • Alchemy: 優れたパフォーマンス、高い無料割り当て、およびより多くの機能のサポート。
  • QuickNode: 高いパフォーマンス要件に適していますが、有料ユーザー向けです。
from web3 import Web3
w3 = Web3(Web3.HTTPProvider('https://eth-mainnet.g.alchemy.com/v2/你的API密钥'))
print(w3.is_connected())

Alchemy の使用をお勧めします。MetaMask の Infura と比較すると、Alchemy はより高い無料割り当てを提供します。登録後、https://eth-mainnet.g.alchemy.com/v2/your API key などの RPC アドレスを取得し、Web3.py に設定して使用できます。

Curve コントラクトアドレスと ABI

sDAI/sUSDe の Crve プール (https://curve.fi/dex/ethereum/pools/factory-stable-ng-102/swap/) を例にとると、2 つのトークンのアドレスとプールのアドレスを簡単に見つけることができます。 分散型取引所で注文を実装する方法 - Curve を例に

ABI はコントラクトとのやり取り方法を定義するため、これも取得する必要があります。ethscan でコントラクトを表示します https://etherscan.io/address/0x167478921b907422f8e88b43c4af2b8bea278d3a#code。コントラクトページで ABI を確認し、直接コピーすることができます。

価格を取得するには契約をリンクしてください

まず、ETH ウォレットをリンクします。ウォレット アドレスが印刷されれば成功です。

def main():
    # 文件路径
    file_path = 'encrypted_key.txt'

    # 从文件中读取加密的私钥
    encrypted_private_key = load_key_from_file(file_path)
    private_key = decrypt_private_key(encrypted_private_key, Password) #Password为密码,定义为策略的参数
    web3 = Web3(Web3.HTTPProvider(HTTPProvider)) # HTTPProvider为RPC的链接,定义为参数
    account = web3.eth.account.from_key(private_key)
    address = account.address  # 获取账户的公开地址
    Log('链接账户', address)

以下は価格を取得するプロセスです。最終的に、GAS料金を考慮しない場合、現在の投資額100,000 SDAIは、1週間後に335Uの利益を生み出します。少し複雑に見えるかもしれませんが、実際に理解するのは難しくありません。

    # --------------- 合约设置 ---------------
    # Curve.fi sDAI/sUSDe 池合约地址和ABI
    pool_address = '0x167478921b907422F8E88B43C4Af2B8BEa278d3A'  # Curve池子的合约地址
    # 以下是简化的ABI,仅包含我们需要的函数
    pool_abi = json.loads('''[{"stateMutability":"view","type":"function","name":"get_dy","inputs":[{"name":"i","type":"int128"},{"name":"j","type":"int128"},{"name":"dx","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]}]''')
    # 创建池子合约对象
    pool_contract = web3.eth.contract(address=pool_address, abi=pool_abi)

    # ERC20 代币标准合约ABI
    erc20_abi = json.loads('''[
        {"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"type":"function"},
        {"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"success","type":"bool"}],"type":"function"},
        {"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"}
    ]''')

    sdai_address = '0x83F20F44975D03b1b09e64809B757c47f942BEeA'  # sDAI代币的合约地址
    susde_address = '0x9D39A5DE30e57443BfF2A8307A4256c8797A3497'  # sUSDE代币的合约地址
    # 池子中代币的索引
    SDAI_INDEX = 0  # sDAI代币在池子中的索引
    SUSDE_INDEX = 1  # sUSDE代币在池子中的索引
    # 创建代币合约对象
    sdai_contract = web3.eth.contract(address=sdai_address, abi=erc20_abi)
    susde_contract = web3.eth.contract(address=susde_address, abi=erc20_abi)
    SUSDE_PRICE = 1.1623 #这个价格是ethena官网价格,1周后可赎回
    SDAI_PRICE = 1.15 #sDAI是收益代币,价值会累计,目前价格1.15

    try:
        SDAI_DECIMALS = sdai_contract.functions.decimals().call()
        SUSDE_DECIMALS = susde_contract.functions.decimals().call()
    except:
        # 如果无法获取,假设为标准的18位精度
        SDAI_DECIMALS = 18
        SUSDE_DECIMALS = 18
    amount_in = 100000
    amount_out = pool_contract.functions.get_dy(
                    SDAI_INDEX,  # 输入代币索引
                    SUSDE_INDEX,  # 输出代币索引
                    int(amount_in *  10**SDAI_DECIMALS)   # 输入数量(wei)
                ).call()
    profit =  SUSDE_PRICE * amount_out / 10**SUSDE_DECIMALS -  amount_in * SDAI_PRICE
    Log(round(profit, 2), round(amount_out / 10**SUSDE_DECIMALS, 2))

フルプログラム

最後に、ポーリングを使用して継続的に価格を取得し、期待利益に達したときに注文を出します。このコードに注意してください。これは単なるサンプルコードなので、直接使用しないでください。読者は実際にさまざまな問題に遭遇する可能性がありますが、AIは現在非常に強力であり、基本的にさまざまな質問に答えることができます。また、コードの作成を直接支援することもできます。FMZのコードエディターにはChatGPTも統合されており、より頻繁に使用できます。

def execute_trade(amount_in, min_amount_out, direction):
    gas_price = web3.eth.gas_price
    index_in = SUSDE_INDEX
    index_out = SDAI_INDEX
    if direction == 'buy':
        index_in = SDAI_INDEX
        index_out = SUSDE_INDEX
    # 第二步:执行代币交换交易
    swap_tx = pool_contract.functions.exchange(
        index_in,  # 输入代币索引
        index_out,  # 输出代币索引
        int(amount_in*10**SDAI_DECIMALS),  # 输入数量
        int(min_amount_out*10**SUSDE_DECIMALS)  # 最小输出数量(考虑滑点)
    ).build_transaction({
        'from': address,  # 交易发送方
        'gas': 600000,  # gas限制
        'gasPrice': int(2*gas_price) ,
        'nonce': web3.eth.get_transaction_count(address),  # 获取新的nonce值
    })
    
    # 签名并发送交换交易
    signed_swap_tx = web3.eth.account.sign_transaction(swap_tx, private_key)
    swap_tx_hash = web3.eth.send_raw_transaction(signed_swap_tx.rawTransaction)
    
    Log(f"交换交易已提交,交易哈希: {swap_tx_hash.hex()}")

def get_buy_profit(amount_in):
    amount_out = pool_contract.functions.get_dy(
                    SDAI_INDEX,  # 输入代币索引
                    SUSDE_INDEX,  # 输出代币索引
                    int(amount_in *  10**SDAI_DECIMALS)   # 输入数量(wei)
                ).call()
    return  SUSDE_PRICE * amount_out / 10**SUSDE_DECIMALS -  amount_in * SDAI_PRICE, amount_out / 10**SUSDE_DECIMALS

def main():
    while True:
        try:
            sdai_balance = sdai_contract.functions.balanceOf(address).call() / 10**SDAI_DECIMALS
            susde_balance = susde_contract.functions.balanceOf(address).call() / 10**SUSDE_DECIMALS
            amount_in = 100000 #交易的DAI数量
            profit, amount_out = get_buy_profit(amount_in)
            LogStatus(f"SDAI数量:{sdai_balance}, SUSDE数量:{susde_balance}, 收益:{profit}")
            if profit > 1000 and sdai_balance > amount_in: #利润空间
                Log("\n开始执行SDAI->SUSDE交易...")
                execute_trade(amount_in, 0.999*amount_out, 'buy') #一定要设置滑点
            wait_time = 3  # 等待时间(秒)
            time.sleep(wait_time)
            
        except Exception as e:
            # 全局错误处理
            print(f"程序发生错误: {e}")
            print("60秒后重试...")
            time.sleep(60)  # 出错后等待更长时间

リスクリマインダー

オンチェーン操作は初心者にとって比較的リスクが高いです。前述の秘密鍵漏洩のリスクに加えて、他にもさまざまなリスクがあります。

  • MEV ロボットは、最悪の場合でも利益を確保するために、トランザクションを実行するときに最小出力 min_amount_out を設定する必要があります。そうしないと、MEV によって悪用されてしまいます。今日、ある人が Uniswap で 220,000 USDC を使って 5272 USDT と交換しました。その理由は、amountOutMinimum が 0 に設定されていたためです。
  • 戦略エラーは、取引所APIと同様に、オンチェーンプログラムトランザクションにバグがある場合、GASが頻繁に消費されます。

オンチェーン取引の初心者は、ガス、スリッページ、MEV などの概念を理解するなど、基礎を学ぶ必要があります。 常に少量から始めて、徐々に量を増やしてください。 Etherscan などを使用してトランザクションを監視します。資本を失うリスクを負うよりは、チャンスを逃すほうがましです。