20101210 Gelato Network
#tadashim
https://gyazo.com/6bdac20a6c77b70b7bd746a727b93601
公式サイト: https://gelato.network/
Gelatoの概要
簡単に言うと、Ethereumのスマートコントラクトを指定した条件で自動実行するためのプロトコルで、IFTTT(if this then that)みたいなやつ。
実態は、ユーザーに代わってトランザクションを実行する分散型ボットネットワークで、ネットワークで常時稼働しているボットにタスクを割り当てて実行することができる。
ユースケース
Gas Priceが低い時に Chi Gas Tokenを自動でmintする
トレード戦略の自動実行
100USDC分のETHを毎日購入
あるトークンが指定価格を下回ったらUniswapで売り注文を実行(ストップロスオーダー)
価格が下がったときに自動的にETHを購入し、価格が再び上がった後に再度販売します(自動ボラティリティトレーダー)
複数のレンディングプロトコル間でローンを自動的に借り換えて、常に最高の利回りを得る
目まぐるしく変化するDeFiマーケットで自己資産を24時間365日監視するのは困難だが、Galatoを使えばマーケットに貼りつかなくても良くなる。
Gelatoの概要図
https://gyazo.com/9852128676bd02573ecc4798c80169fc
Users : 将来のある時点および条件でタスクを実行したいユーザー
Conditions : 実行条件
Actions : 実行したい処理
Executors : ユーザーの代わりにトランザクションを送信するリレーノード(ボット)群
GelatoProvider :Executorsへ報酬の支払いを行う
Gelatoは自動化されたタスク実行のためのオープンな分散ネットワークを構築することが目標で、誰でもExecutorノードを実行して、タスク実行の報酬を得ることができる。
Executorsがユーザーの代わりにタスク実行のトランザクションを送信するための報酬(ガス代含む)は、GelatoProviderからGelatoCoreコントラクトにあらかじめデポジットされたETH残高から従量課金で支払われる。この時のガス価格はChainlinkのGas Prices Oracleを使用して決定される。
Gelatoはユーザーの資金は管理せず(ノンカストディアル)、資金はユーザーのコントラクトウォレット(プロキシコントラクト)に保持されている。コントラクトウォレット(プロキシコントラクト)を介すことで、Executorsはユーザーの秘密鍵にはアクセスせずに、ユーザーに代わってトランザクションを送信することができる。
コントラクトウォレットとして、Gnosis SafeやDsProxy、GelatoUserProxyが利用可能。
Galatoの実装例
1,Conditionsを定義する。
Conditions
GelatoConditionsStandard.solを継承するスマートコントラクト
トランザクション送信前にGelatoCore.solから呼び出され、事前に定義した条件を満たしているかどうかチェックする
Gelatoで条件として使うには、次のようなok関数を実装する必要がある(この例では、ブロックのタイムスタンプが条件の時間以降かをチェックしている)
code:js
pragma solidity ^0.6.10;
import {GelatoConditionsStandard} from "../../GelatoConditionsStandard.sol";
contract ConditionTime is GelatoConditionsStandard {
function ok(uint256, bytes calldata _conditionData, uint256)
public
view
virtual
override
returns(string memory)
{
uint256 timestamp = abi.decode(_conditionData, (uint256));
return timeCheck(timestamp);
}
// Specific implementation
function timeCheck(uint256 _timestamp) public view virtual returns(string memory) {
if (_timestamp <= block.timestamp) return OK;
return "NotOkTimestampDidNotPass";
}
}
Conditionsは独自のスマートコントラクトとして作成しても良いし、Gelatoのコミュニティで既に作成されているものを再利用することも可能。
また、どのConditionsをトラッキングするかGelatoに伝えるためには下記の2つのパラメータを渡す必要がある。
Conditionsのアドレス
_conditionData:データを含むペイロード
Javascriptだと下記サンプルコードのように扱うことができる
code:js
const ethers = require("ethers");
const GelatoCoreLib = require("@gelatonetwork/core");
const conditionAddress = "0x63129681c487d231aa9148e1e21837165f38deaf"
const conditionAbi = "function timeCheck(uint256 _timestamp) view returns(string memory)"
const iFace = new ethers.utils.Interface(conditionAbi)
const futureTimestamp = 1599800000
// #### Create the condition object
const condition = new GelatoCoreLib.Condition({
inst: conditionAddress,
data: iFace.encodeFunctionData("timeCheck", [
futureTimestamp
]),
})
2、Actionsを定義する。
Actions
Ethereumにデプロイされているスマートコントラクトを呼び出して実行
次の2つの方法がある
スマートコントラクトの関数を直接Callして呼び出す方法
スマートコントラクトの関数をDelegatecallで呼び出す方法(msg.senderとmsg.valueが変更されず、呼び出し元のコントラクトのコンテクストで実行される)
次の例では、UniswapV2のRouter02コントラクトのswapExactTokensForTokens関数を上記2パターンで呼び出している。
https://uniswap.org/docs/v2/smart-contracts/router02/#swapexacttokensfortokens
ActionsでCallする対象のコントラクト:UniswapV2Router02.sol
code:js
contract UniswapV2Router02 {
...
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external
virtual
override
ensure(deadline)
returns (uint[] memory amounts)
{
amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
require(amountsamounts.length - 1 >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
TransferHelper.safeTransferFrom(
path0, msg.sender, UniswapV2Library.pairFor(factory, path0, path1), amounts0
);
_swap(amounts, path, to);
}
}
上記コントラクトのswapExactTokensForTokens関数をCallで呼び出す
code:js
const ethers = require("ethers");
const GelatoCoreLib = require("@gelatonetwork/core");
// Address of UniswapV2Router2
const actionAddress = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"
const actionAbi = [
"function swapExactTokensForTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) returns (uint256[] memory amounts)"
]
const iFace = new ethers.utils.Interface(actionAbi)
// #### Create the action object
const action = new GelatoCoreLib.Action({
addr: actionAddress,
data: iFace.encodeFunctionData("swapExactTokensForTokens", [
1000000000000000,
5000000000000000,
[
"0x6B175474E89094C44Da98b954EedeAC495271d0F",
"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
],
"0x2464e6E2c963CC1810FAF7c2B3205819C93833f7",
1599800000
]),
operation: GelatoCoreLib.Operation.Call,
dataFlow: GelatoCoreLib.DataFlow.None,
value: 0,
termsOkCheck: false
})
ActionsでUniswapV2Router02.solのswapExactTokensForTokens関数をDelegatecallするためのコントラクト:ActionUniswapV2Trade.sol
code:js
contract ActionUniswapV2Trade {
...
function action(
address _sellToken,
uint256 _sellAmount,
address _buyToken,
uint256 _minBuyAmount,
address _receiver,
address _origin
)
public
virtual
delegatecallOnly("ActionKyberTrade.action")
{
address receiver = _receiver == address(0) ? address(this) : _receiver;
address buyToken = _buyToken;
// If sellToken == ETH, wrap ETH to WETH
// IF ETH, we assume the proxy already has ETH and we dont transferFrom it
if (_sellToken == ETH_ADDRESS) {
_sellToken = address(WETH);
WETH.deposit{value: _sellAmount}();
} else {
if (_origin != address(0) && _origin != address(this)) {
IERC20(_sellToken).safeTransferFrom(
_origin, address(this), _sellAmount, "ActionUniswapV2Trade.safeTransferFrom"
);
}
}
IERC20 sellToken = IERC20(_sellToken);
// Uniswap only knows WETH
if(_buyToken == ETH_ADDRESS) buyToken = address(WETH);
address[] memory tokenPath = getPaths(_sellToken, buyToken);
// UserProxy approves Uniswap Router
// 該当コードが省略されているが、uniRouterにはUniswapV2Router02コントラクトのインターフェースが格納されている
sellToken.safeIncreaseAllowance(
address(uniRouter), _sellAmount, "ActionUniswapV2Trade.safeIncreaseAllowance"
);
require(sellToken.allowance(address(this), address(uniRouter)) >= _sellAmount, "Invalid token allowance");
uint256 buyAmount;
// UniswapV2Router02.solのswapExactTokensForTokens関数を呼び出している
try uniRouter.swapExactTokensForTokens(
_sellAmount,
_minBuyAmount,
tokenPath,
address(this),
now + 1
) returns (uint256[] memory buyAmounts) {
buyAmount = buyAmounts1;
} catch {
revert("ActionUniswapV2Trade.action: trade with ERC20 Error");
}
// If sellToken == ETH, unwrap WETH to ETH
if (_buyToken == ETH_ADDRESS) {
WETH.withdraw(buyAmount);
if (receiver != address(this)) payable(receiver).sendValue(buyAmount);
} else if (receiver != address(this)) IERC20(_buyToken).safeTransfer(receiver, buyAmount, "ActionUniswapV2Trade.safeTransfer");
emit LogGelatoUniswapTrade(
_sellToken,
_sellAmount,
_buyToken,
_minBuyAmount,
buyAmount,
receiver,
_origin
);
}
}
上記コントラクトをDelegatecallで呼び出す
code:js
const ethers = require("ethers");
const GelatoCoreLib = require("@gelatonetwork/core");
// Address of ActionUniswapV2Trade.sol
const actionAddress = "0x926Ef4Fe67B8d88d2cC2E109B6b7fae4A92cB1c1"
const actionAbi = [
"function action(
address _sellToken,
uint256 _sellAmount,
address _buyToken,
uint256 _minBuyAmount,
address _receiver,
address _origin
)"
]
const iFace = new ethers.utils.Interface(actionAbi)
// #### Create the action object
const action = new GelatoCoreLib.Action({
addr: actionAddress,
data: iFace.encodeFunctionData("action", [
"0x6B175474E89094C44Da98b954EedeAC495271d0F",
1000000000000000,
"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
5000000000000000,
"0x2464e6E2c963CC1810FAF7c2B3205819C93833f7",
"0x0000000000000000000000000000000000000000"
]),
operation: GelatoCoreLib.Operation.Delegatecall,
dataFlow: GelatoCoreLib.DataFlow.None,
value: 0,
termsOkCheck: true
})
JavascriptのActionオブジェクトは下記フィールドで構成
addr: CallまたはDelegatecallで呼び出すコントラクトのアドレス
data: 関数識別子(シグネチャ)とペイロードを含むエンコードされたデータ
operation: CallとDelegatecallを区別
dataFlow: Delegatecall使用時に複数のアクション間でデータを渡すために使用する機能
value: 実行時に送信する必要があるETH(msg.value)。Payableな関数を呼び出す場合に使用
termsOkCheck: GelatoActionsStandardに準拠したActionかどうかをチェックする
3、ConditionsとActionsを組み合わせてTaskを定義する。
複数のConditionsと複数のActionsを組み合わせることが可能
code:js
// https://hardhat.org/
const bre = require("@nomiclabs/buidler");
const { ethers } = bre;
const GelatoCoreLib = require("@gelatonetwork/core");
const task = new GelatoCoreLib.Task({
conditions: condition,
actions: action,
selfProviderGasLimit: 0,
selfProviderGasPriceCeil: 0
})
JavascriptのTaskオブジェクトは下記のフィールドで構成
conditions: Conditionsの配列
actions: Actionsの配列
selfProviderGasLimit: タスク実行のガスリミット
selfProviderGasPriceCeil: タスク実行の最大ガスプライス
4、支払い元を定義する。
gelatoProviderとして誰がタスク実行の報酬を支払うかを定義。下記の例ではエンドユーザーのGelatoUserProxy(コントラクトウォレット)を支払い元として定義している。
code:js
const bre = require("@nomiclabs/buidler");
const { ethers } = bre;
const GelatoCoreLib = require("@gelatonetwork/core");
// Gelato User Proxy
const gelatoUserProxyAddress = "YOUR_PROXY_ADDRESS"
const providerModuleGelatoUserProxy = "0x4372692C2D28A8e5E15BC2B91aFb62f5f8812b93"
// Gelato provider object
const gelatoProvider = new GelatoCoreLib.GelatoProvider({
addr: gelatoUserProxyAddress,
module: providerModuleGelatoUserProxy
})
JavascriptのGelatoProviderオブジェクトは下記のフィールドで構成
addr: 報酬を支払うコントラクトウォレットのアドレス
module: コントラクトウォレットのタイプ
5、タスクの実行
1回だけ実行するタスク
code:js
const bre = require("@nomiclabs/buidler");
const { ethers } = bre;
const GelatoCoreLib = require("@gelatonetwork/core");
const gelatoUserProxyAddress = "YOUR_PROXY_ADDRESS"
gelatoUserProxy = await ethers.getContractAt(
GelatoCoreLib.GelatoUserProxy.abi,
gelatoUserProxyAddress
);
// Submit one-time task
await gelatoUserProxy.submitTask(gelatoProvider, task, expiryData)
複数回リピート実行するタスク
code:js
const bre = require("@nomiclabs/buidler");
const { ethers } = bre;
const GelatoCoreLib = require("@gelatonetwork/core");
const gelatoUserProxyAddress = "YOUR_PROXY_ADDRESS"
gelatoUserProxy = await ethers.getContractAt(
GelatoCoreLib.GelatoUserProxy.abi,
gelatoUserProxyAddress
);
// Submit reapting task
await gelatoUserProxy.submitTaskCycle(
gelatoProvider,
task,
expiryData,
numOfCycle
)
numOfCycle変数でリピート回数を指定。0を設定すると無限になる。
6、Gelatoのセットアップ
タスクをGelatoに送信する前に下記のセットアップを行う。
Executorsネットワークをホワイトリストに登録
ユーザーがどの種類のプロキシであるかをGelatoに通知
GelatoにETHをデポジットして、Executorsへ支払うことができる残高を確保
code:js
const bre = require("@nomiclabs/buidler");
const { ethers } = bre;
const GelatoCoreLib = require("@gelatonetwork/core");
const executorNetwork = "0xd70D5fb9582cC3b5B79BBFAECbb7310fd0e3B582"
const gelatoUserProxyProviderModule = "0x4372692C2D28A8e5E15BC2B91aFb62f5f8812b93"
const ethToDeposit = ethers.utils.parseEther("3");
const gelatoUserProxyAddress = "YOUR_PROXY_ADDRESS"
const gelatoCoreAddress = "0x1d681d76ce96E4d70a88A00EBbcfc1E47808d0b8"
gelatoUserProxy = await ethers.getContractAt(
GelatoCoreLib.GelatoUserProxy.abi,
gelatoUserProxyAddress
);
const iFace = new ethers.utils.Interface(GelatoCoreLib.GelatoCore.abi)
// Encode Multiprovide function of GelatoCore.sol
const multiProvideData = iFace.encodeFunctionData("multiProvide", [executorNetwork, [], gelatoUserProxyProviderModule]);
const multiProvideAction = new GelatoCoreLib.Action({
addr: gelatoCoreAddress,
data: multiProvideData,
value: ethers.utils.parseEther("1"),
operation: Operation.Call,
dataFlow: GelatoCoreLib.DataFlow.None,
termsOkCheck: false
});
await gelatoUserProxy.execAction(multiProvideAction, {
value: utils.parseEther("1"),
});
タスクがGelatoに送信されると、Executorsネットワークはトランザクションを実行できるかどうかを常にチェックし、実行できる場合は実行する。