DeFi 套利機器人秘密技術。如何將所有內容整合到單個事務中

胡家維 Hu Kenneth
My blockchain development Journey
11 min readSep 3, 2023

source : https://medium.com/coinmonks/defi-arbitrage-bots-secret-technique-how-to-fit-everything-into-single-transaction-ac9aa13df33f by Alexander Koval

套利機器人是複雜而有效的工具,可以為您帶來被動收入。隨著 DeFi 空間的增長,它們變得越來越受歡迎。但為了盈利,您應該遵循最佳實踐並避免典型錯誤。

通常 DeFi 交易機器人由以下組件組成:

  • 監控流動性池並發現套利機會的鏈下腳本
  • 鏈上合約 — — 包含套利邏輯的智能合約
  • 熱錢包 — — 保留資產用於套利並獲利

想像一下,鏈下腳本在 DeFi 領域的某個地方發現了一個有利可圖的套利機會,您需要搶在競爭對手之前利用它。套利必須在單筆交易中完成,否則您將浪費大量時間,並且可能會輸給其他機器人。

典型的套利涉及使用 ETH 和 ERC-20 代幣進行操作,因此首先想到的想法是預先部署智能合約並用代幣提供資金,每當發現套利機會時,腳本就會觸發交易具體參數

這種方法很幼稚,並且有嚴重的缺點:

  • 你必須預先部署你的合約並支付 Gas 費 — — 但是如果永遠找不到機會怎麼辦 — — 你會失去為部署而支付的 Gas 費 — — 這在 Ehtereum 主網中可能會很多
  • 你必須提前用ERC-20 代幣為你的合約提供資金,然後讓我們持有它們一段時間 — — 這會帶來安全風險,因為你必須仔細審核你的合約,以確保沒有人可以破解它並竊取你的資產 — — 很多如果您的資金被鎖定在熱錢包中而不是合約中,則更容易只管理您的私鑰
  • 一旦部署,任何人都可以讀取您的合約並竊取您的邏輯,以便將其重新用於他們自己的套利機器人,這些機器人將與您的套利機器人競爭

這是更好的策略

  • 理想情況下,您希望將 ETH 和 ERC-20 代幣保留在熱錢包上,並將智能合約代碼保留在鏈外,直到找到套利機會
  • 一旦發現套利機會,您的機器人必須部署智能合約,用 ETH 和 ERC-20 代幣為其提供資金,進行套利並將利潤轉移回熱錢包
  • 所有這些操作都必須在單個事務中完成

請記住,部署會導致事務,這意味著所有邏輯都必須在構造函數內完成。我們可以在部署期間用 ETH 為合約提供資金,但轉移 ERC-20 代幣是一個棘手的部分。
轉移 ERC-20 有兩種方法:轉移批准這兩種方法都需要向代幣合約進行額外交易,其中發送者是代幣持有者。但是我們只有一個已用於部署的交易,這意味著令牌傳輸必須在構造函數內完成,以便將我們的套利融入到單個交易中。那麼怎樣才能讓它成為可能呢?

該解決方案是新的EIP-2616代幣標準 — — 它包含使用數字簽名並允許無 Gas 代幣傳輸的許可功能。很多代幣已經支持這個標準。我第一次遇到它是在1Inch交易所進行交換時 — — 我很驚訝地看到簽名請求而不是批准交易,並開始進一步挖掘以發現支出批准的新方法。這裡還有一篇文章解釋它是如何工作的:https://www.quicknode.com/guides/ethereum-development/transactions/how-to-use-erc20-permit-approval/

EIP-2616讓我們在單筆交易中使用新的代幣轉移和套利策略:

  • 熱錢包為許可功能準備簽名 — — 這是在鏈外完成的,不會創建交易
  • 簽名作為參數傳遞給構造函數
  • 在構造函數中,智能合約調用 Permit 函數,而不是TransferFrom函數並獲取代幣的所有權
  • 執行套利邏輯
  • 利潤轉回熱錢包

因此,熱錢包只需提交一筆交易 — — 具有適當參數的合約部署,一旦挖礦,利潤就會被拿走。

然而有一個問題 — — 讓我們看一下permit函數接口

/**
* @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens,
* given ``owner``'s signed approval.
*
* IMPORTANT: The same issues {IERC20-approve} has related to transaction
* ordering also apply here.
*
* Emits an {Approval} event.
*
* Requirements:
*
* - `spender` cannot be the zero address.
* - `deadline` must be a timestamp in the future.
* - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner`
* over the EIP712-formatted function arguments.
* - the signature must use ``owner``'s current nonce (see {nonces}).
*
* For more information on the signature format, see the
* https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP
* section].
*/

function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external;

正如我們所看到的,我們需要提供支出者的地址 — 但在我們的例子中,支出者是一個尚未部署的合約!我們如何知道部署後合約將在哪個地址上?

幸運的是,合約地址是根據部署者的地址和部署者的隨機數預先確定的。這是 stackexchange 上的一個線程,解釋瞭如何在 EVM 中計算合約地址:https://ethereum.stackexchange.com/questions/760/how-is-the-address-of-an-ethereum-contract-compulated/ 761#第761章

以下是在部署之前根據部署者的地址和隨機數預測合約地址的 JS 代碼:

const rlp = require("rlp");
const keccak = require("keccak");

async function predictContractAddress(deployer) {

const nonce = await ethers.provider.getTransactionCount(deployer.address);
const sender = deployer.address;

const input_arr = [sender, nonce];
const rlp_encoded = rlp.encode(input_arr);

const contract_address_long = keccak("keccak256")
.update(rlp_encoded)
.digest("hex");

const contract_address = contract_address_long.substring(24); //Trim the first 24 characters.
return contract_address;
}

現在我們有了準備支出批准簽名的所有數據,我們可以將其傳遞給構造函數。以下是在 Hardhat 上運行的腳本的完整代碼:

async function doArbitrage(deployer, value, tokenBalance, ...params) {
const chainId = (await ethers.provider.getNetwork()).chainId;
// set the domain parameters
const domain = {
name: await token.name(),
version: "1",
chainId: chainId,
verifyingContract: token.address
};

// set the Permit type parameters
const types = {
Permit: [{
name: "owner",
type: "address"
},
{
name: "spender",
type: "address"
},
{
name: "value",
type: "uint256"
},
{
name: "nonce",
type: "uint256"
},
{
name: "deadline",
type: "uint256"
},
],
};

const predictedAddress = await predictContractAddress(deployer);
console.log("Predicted address: " + predictedAddress);

const deadline = (await time.latest()) + 9999999;
const values = {
owner: deployer.address,
spender: predictedAddress,
value: tokenBalance,
nonce: await token.nonces(deployer.address),
deadline: deadline,
};
const signature = await deployer._signTypedData(domain, types, values);
const sig = ethers.utils.splitSignature(signature);

const arbitrageContract = await (await ethers.getContractFactory('Arbitrage', deployer)).deploy(...params, sig.v, sig.r, sig.s, { value });

console.log(`Actual address: ${arbitrageContract.address}`);
}

以及智能合約構造函數的代碼:

import "@openzeppelin/contracts/interfaces/IERC2612.sol";
import "@openzeppelin/contracts/utils/Address.sol";

contract Arbitrage {
using Address for address payable;
constructor (/* parameters for arbitrage */, address _token, uint256 tokenBalance, uint256 _deadline, uint8 v, bytes32 r, bytes32 s) payable {
IERC2612 token = IERC2612(_token);
token.permit(
msg.sender,
address(this),
tokenBalance,
_deadline,
v,
r,
s
);
token.transferFrom(msg.sender, address(this), tokenBalance);
// now contract has tokens and ETH and you can do your arbitrage here
// ...
token.transfer(msg.sender, token.balanceOf(address(this))); // send tokens back to hot wallet
payable(msg.sender).sendValue(address(this).balance); // send ETH back to hot wallet
}
}

並且不要忘記使用Flashbots RPC而不是Infura來保護您的套利交易免遭搶先交易

受到Damn Vulnerable DeFi上“Puppet”挑戰的啟發

--

--

胡家維 Hu Kenneth
My blockchain development Journey

撰寫任何事情,O型水瓶混魔羯,咖啡愛好者,Full stack/blockchain Web3 developer,Founder of Blockchain&Dapps meetup ,Udemy teacher。 My Linktree: https://linktr.ee/kennethhutw