diff --git a/src/const/gnosis.ts b/src/const/gnosis.ts index 5dd296e..623eee9 100644 --- a/src/const/gnosis.ts +++ b/src/const/gnosis.ts @@ -1 +1,8 @@ export const GNO_ADDRESS = "0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb"; + +// ComposableCowPoller: just-in-time funding for composable conditional orders. +// `id`-keyed deployment (schedule key is independent of the order's appData, so +// `pollFunds(id)` can be embedded as a pre-hook in the order's own appData). +// See https://github.com/cowprotocol/composable-cow/pull/116 +export const COMPOSABLE_COW_POLLER_ADDRESS = + "0xA360eE11eD0d2025604518CF4B8F6e6CB76C7Df7"; diff --git a/src/contracts/composable-cow-poller/index.ts b/src/contracts/composable-cow-poller/index.ts new file mode 100644 index 0000000..807796f --- /dev/null +++ b/src/contracts/composable-cow-poller/index.ts @@ -0,0 +1,41 @@ +import { ethers } from "ethers"; + +/** + * Minimal ABI for the `ComposableCowPoller` contract. + * + * It enables just-in-time funding for composable conditional orders: instead of + * locking the whole notional up front, `pollFunds` pulls exactly the current discrete + * order's `sellAmount` from a funder into the order owner, immediately before that + * order settles. + * + * @see https://github.com/cowprotocol/composable-cow/pull/116 + */ +const COMPOSABLE_COW_POLLER_ABI = [ + "function composableCow() external view returns (address)", + "function scheduleId(address funder, address handler, address owner, bytes32 salt) external pure returns (bytes32)", + "function register((address handler, address funder, address owner, bytes32 salt, bytes staticInput) schedule) external returns (bytes32 id)", + "function revoke(bytes32 id) external", + "function pollFunds(bytes32 id) external", + "function schedules(bytes32 id) external view returns (address handler, address funder, address owner, bytes32 salt, bytes staticInput)", + "function lastFunded(bytes32 id) external view returns (bytes32)", +] as const; + +export interface PollerSchedule { + /** The conditional-order handler to poll (e.g. the TWAP type). */ + handler: string; + /** Source of funds (the EOA in the TWAP-for-EOA flow); the only registrant. */ + funder: string; + /** Order owner (cow-shed / Safe); the fixed pull destination. */ + owner: string; + /** The conditional order's `salt`; lets the poller rebuild `ctx` on-chain. */ + salt: string; + /** The order's `staticInput`, passed verbatim to `getTradeableOrder`. */ + staticInput: string; +} + +export function getComposableCowPollerContract( + pollerAddress: string, + signer?: ethers.Signer | ethers.providers.Provider +): ethers.Contract { + return new ethers.Contract(pollerAddress, COMPOSABLE_COW_POLLER_ABI, signer); +} diff --git a/src/index.ts b/src/index.ts index a8cda04..73cb5c9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,6 +37,7 @@ import { run as getEthFlowId } from "./scripts/ethflow/getEthFlowId"; import { run as minimalAppData } from "./scripts/app-data/minimalAppData"; import { run as getIpfsForLegacyDoc } from "./scripts/app-data/getIpfsForLegacyDoc"; import { run as postTwapForEOA } from "./scripts/composable-cow/postTwapForEOA"; +import { run as postTwapForEOAWithJitFunds } from "./scripts/composable-cow/postTwapForEOAWithJitFunds"; dotenv.config(); @@ -86,7 +87,8 @@ const JOBS: (() => Promise)[] = [ // getEthFlowId, // minimalAppData, // getIpfsForLegacyDoc, - postTwapForEOA, + // postTwapForEOA, + postTwapForEOAWithJitFunds, ]; async function main() { diff --git a/src/scripts/composable-cow/cowShed.ts b/src/scripts/composable-cow/cowShed.ts new file mode 100644 index 0000000..091ab57 --- /dev/null +++ b/src/scripts/composable-cow/cowShed.ts @@ -0,0 +1,20 @@ +import { CowShedSdk } from "@cowprotocol/cow-sdk"; + +// Factory options for the COWShedForComposableCoW deployment on Gnosis Chain. +// See https://gnosisscan.io/address/0x4f4350bf2c74aacd508d598a1ba94ef84378793d#readContract +// and https://github.com/cowdao-grants/cow-shed/pull/53 +const COW_SHED_FACTORY_OPTIONS = { + factoryAddress: "0x4f4350bf2c74aacd508d598a1ba94ef84378793d", + implementationAddress: "0x6773d5aA31A1EAD34127D564D6E258E66254EbDb", // COWShedForComposableCoW implementation + proxyCreationCode: + // See https://gnosisscan.io/address/0x4f4350bf2c74aacd508d598a1ba94ef84378793d#readContract + "0x60a03461009557601f61033d38819003918201601f19168301916001600160401b0383118484101761009957808492604094855283398101031261009557610052602061004b836100ad565b92016100ad565b6080527f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5560405161027b90816100c28239608051818181608b01526101750152f35b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b51906001600160a01b03821682036100955756fe60806040526004361015610018575b3661019757610197565b5f3560e01c8063025b22bc146100375763f851a4400361000e57610116565b346101125760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101125760043573ffffffffffffffffffffffffffffffffffffffff81169081810361011257337f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff160361010d577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b5f80a2005b61023d565b5f80fd5b34610112575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011257602061014e61016c565b73ffffffffffffffffffffffffffffffffffffffff60405191168152f35b33300361010d577f000000000000000000000000000000000000000000000000000000000000000090565b60ff7f68df44b1011761f481358c0f49a711192727fb02c377d697bcb0ea8ff8393ac0541615806101f0575b1561023d577ff92ee8a9000000000000000000000000000000000000000000000000000000005f5260045ffd5b507fc4d66de8000000000000000000000000000000000000000000000000000000007fffffffff000000000000000000000000000000000000000000000000000000005f351614156101c3565b5f807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc54368280378136915af43d5f803e15610277573d5ff35b3d5ffd", +} as const; + +/** + * Build a `CowShedSdk` configured for the COWShedForComposableCoW deployment, + * which is the cow-shed flavour with support for Composable CoW. + */ +export function getCowShedSdk(): CowShedSdk { + return new CowShedSdk({ factoryOptions: { ...COW_SHED_FACTORY_OPTIONS } }); +} diff --git a/src/scripts/composable-cow/postTwapForEOA.ts b/src/scripts/composable-cow/postTwapForEOA.ts index 527c220..6851947 100644 --- a/src/scripts/composable-cow/postTwapForEOA.ts +++ b/src/scripts/composable-cow/postTwapForEOA.ts @@ -5,7 +5,6 @@ import { OrderKind, TradingSdk, Twap, - CowShedSdk, COMPOSABLE_COW_CONTRACT_ADDRESS, OrderBookApi, } from "@cowprotocol/cow-sdk"; @@ -14,6 +13,7 @@ import { MetadataApi } from "@cowprotocol/app-data"; import { BigNumber, ethers } from "ethers"; import { confirm, debugStringify, getWallet, printQuote } from "../../utils"; import { getErc20Contract } from "../../contracts/erc20"; +import { getCowShedSdk } from "./cowShed"; // import { latest } from "@cowprotocol/app-data"; const DEFAULT_GAS_LIMIT = 500_000n; @@ -52,20 +52,13 @@ export async function run() { const { beforeTwapSellToken, twapSellToken, twapBuyToken } = await getAssetsInfo({ wallet, trader: eoaTrader }); - const sellAmount = ethers.utils.parseUnits("0.2", twapSellToken.decimals); // 0.1 EURe + const sellAmount = ethers.utils.parseUnits("0.2", twapSellToken.decimals); // 0.1 sDAI const sellAmountFormatted = ethers.utils.formatUnits( sellAmount, - twapSellToken.decimals + twapSellToken.decimals, ); - const cowShedSdk = new CowShedSdk({ - factoryOptions: { - factoryAddress: "0x4f4350bf2c74aacd508d598a1ba94ef84378793d", - implementationAddress: "0x6773d5aA31A1EAD34127D564D6E258E66254EbDb", - proxyCreationCode: - "0x60a03461009557601f61033d38819003918201601f19168301916001600160401b0383118484101761009957808492604094855283398101031261009557610052602061004b836100ad565b92016100ad565b6080527f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5560405161027b90816100c28239608051818181608b01526101750152f35b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b51906001600160a01b03821682036100955756fe60806040526004361015610018575b3661019757610197565b5f3560e01c8063025b22bc146100375763f851a4400361000e57610116565b346101125760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101125760043573ffffffffffffffffffffffffffffffffffffffff81169081810361011257337f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff160361010d577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b5f80a2005b61023d565b5f80fd5b34610112575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011257602061014e61016c565b73ffffffffffffffffffffffffffffffffffffffff60405191168152f35b33300361010d577f000000000000000000000000000000000000000000000000000000000000000090565b60ff7f68df44b1011761f481358c0f49a711192727fb02c377d697bcb0ea8ff8393ac0541615806101f0575b1561023d577ff92ee8a9000000000000000000000000000000000000000000000000000000005f5260045ffd5b507fc4d66de8000000000000000000000000000000000000000000000000000000007fffffffff000000000000000000000000000000000000000000000000000000005f351614156101c3565b5f807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc54368280378136915af43d5f803e15610277573d5ff35b3d5ffd", - }, - }); + const cowShedSdk = getCowShedSdk(); const cowShed = cowShedSdk.getCowShedAccount(CHAIN_ID, eoaTrader); console.log("CowShed account:", cowShed); @@ -74,7 +67,7 @@ export async function run() { `TWAP sell ${sellAmountFormatted} ${twapSellToken.symbol} for ${twapBuyToken.symbol} in ${PARTS} parts. To create the TWAP we we will use for this PoC an intermediate order with a post hook: - Buy ${sellAmountFormatted} ${twapSellToken.symbol} with ${beforeTwapSellToken.symbol}, sent to ${cowShed} - - Post-hook will create the TWAP using cow-shed. Each part sells ${twapSellToken.symbol} for ${twapBuyToken.symbol}` + - Post-hook will create the TWAP using cow-shed. Each part sells ${twapSellToken.symbol} for ${twapBuyToken.symbol}`, ); // Generate app data for TWAP order @@ -125,16 +118,16 @@ To create the TWAP we we will use for this PoC an intermediate order with a post const approveSellTokenGasLimit = await twapSellToken.contract.estimateGas.approve( COW_VAULT_RELAYER_CONTRACT, - sellAmount + sellAmount, ); console.log( "Approve sell token gas limit:", - approveSellTokenGasLimit.toString() + approveSellTokenGasLimit.toString(), ); const deadline = BigInt(Math.ceil(Date.now() / 1000)) + 1800n; console.log( - `Deadline: ${deadline} (${new Date(Number(deadline) * 1000).toISOString()})` + `Deadline: ${deadline} (${new Date(Number(deadline) * 1000).toISOString()})`, ); const { signedMulticall: approveAndTwap, gasLimit: approveAndTwapGasLimit } = @@ -195,7 +188,7 @@ To create the TWAP we we will use for this PoC an intermediate order with a post }, }, }, - } + }, ); // Print the quote @@ -204,29 +197,29 @@ To create the TWAP we we will use for this PoC an intermediate order with a post quoteResults.amountsAndCosts.afterSlippage.sellAmount; const sellAmountIntialTradeFormatted = ethers.utils.formatUnits( sellAmountIntialTrade, - beforeTwapSellToken.decimals + beforeTwapSellToken.decimals, ); // Ask for confirmation before posting the order const confirmed = await confirm( - `Your CoW Shed will get exactly ${sellAmountFormatted} ${twapSellToken.symbol} for at most ${sellAmountIntialTradeFormatted} ${beforeTwapSellToken.symbol}. Then a TWAP will be created with each part selling ${twapSellToken.symbol} for ${twapBuyToken.symbol}. ok?` + `Your CoW Shed will get exactly ${sellAmountFormatted} ${twapSellToken.symbol} for at most ${sellAmountIntialTradeFormatted} ${beforeTwapSellToken.symbol}. Then a TWAP will be created with each part selling ${twapSellToken.symbol} for ${twapBuyToken.symbol}. ok?`, ); if (confirmed) { const allowance = await beforeTwapSellToken.contract.allowance( eoaTrader, - COW_VAULT_RELAYER_CONTRACT + COW_VAULT_RELAYER_CONTRACT, ); console.log( - `Allowance for Vault Relayer: ${allowance} ${beforeTwapSellToken.symbol}` + `Allowance for Vault Relayer: ${allowance} ${beforeTwapSellToken.symbol}`, ); if (allowance < sellAmountIntialTrade) { console.log( - `Approving sell token for: ${sellAmountIntialTradeFormatted} ${beforeTwapSellToken.symbol}` + `Approving sell token for: ${sellAmountIntialTradeFormatted} ${beforeTwapSellToken.symbol}`, ); const tx = await beforeTwapSellToken.contract.approve( COW_VAULT_RELAYER_CONTRACT, - ethers.constants.MaxUint256 + ethers.constants.MaxUint256, // sellAmountIntialTrade ); console.log(`Approving ${beforeTwapSellToken.symbol}. tx:`, tx.hash); @@ -238,7 +231,7 @@ To create the TWAP we we will use for this PoC an intermediate order with a post const { orderId } = await postSwapOrderFromQuote(); console.log( - `Order created, id: https://explorer.cow.fi/gc/orders/${orderId}?tab=overview` + `Order created, id: https://explorer.cow.fi/gc/orders/${orderId}?tab=overview`, ); } } @@ -256,7 +249,7 @@ async function getAssetsInfo(params: { // Get ERC20 balance for oldUnderlying using ethersjs const beforeTwapSellToken = await getErc20Contract( TOKENS.beforeTwapSellToken, - wallet + wallet, ); const twapSellToken = await getErc20Contract(TOKENS.twapSellToken, wallet); const twapBuyToken = await getErc20Contract(TOKENS.twapBuyToken, wallet); diff --git a/src/scripts/composable-cow/postTwapForEOAWithJitFunds.ts b/src/scripts/composable-cow/postTwapForEOAWithJitFunds.ts new file mode 100644 index 0000000..cde7762 --- /dev/null +++ b/src/scripts/composable-cow/postTwapForEOAWithJitFunds.ts @@ -0,0 +1,464 @@ +import { APP_CODE, COW_VAULT_RELAYER_CONTRACT } from "../../const"; +import { COMPOSABLE_COW_POLLER_ADDRESS } from "../../const/gnosis"; + +import { + SupportedChainId, + OrderKind, + TradingSdk, + Twap, + COMPOSABLE_COW_CONTRACT_ADDRESS, + OrderBookApi, +} from "@cowprotocol/cow-sdk"; + +import { MetadataApi } from "@cowprotocol/app-data"; +import { BigNumber, ethers } from "ethers"; +import { + confirm, + debugStringify, + getExplorerUrl, + getWallet, + printQuote, +} from "../../utils"; +import { getErc20Contract } from "../../contracts/erc20"; +import { getComposableCowPollerContract } from "../../contracts/composable-cow-poller"; +import { getCowShedSdk } from "./cowShed"; + +const DEFAULT_GAS_LIMIT = 500_000n; + +interface Token { + symbol: string; + address: string; + decimals: number; + contract: ethers.Contract; +} + +const TOKENS = { + twapSellToken: "0xaf204776c7245bF4147c2612BF6e5972Ee483701", // sDAI + twapBuyToken: "0x177127622c4A00F3d409B75571e12cB3c8973d3c", // COW +} as const; + +const TWAP_PARTS = 2; +const TWAP_TIME_BETWEEN_PARTS = 120; // 2min +const TWAP_SLIPPAGE_BPS = 1000; // 1000 bps (10%) +const FIRST_ORDER_SLIPPAGE_BPS = 20000000000; // 200,000,000% // TODO: This was a test because I could see backend not executing the order if the sellAmount was subcent. But this should be something like 0.5% + +// The TWAP handler (ComposableCoW order type). Deterministic across chains. +const TWAP_HANDLER = "0x6cF1e9cA41f7611dEf408122793c358a3d11E5a5"; +// Gas budget for the pollFunds pre-hook on each part (SLOADs + getTradeableOrder + transferFrom). +const POLL_FUNDS_HOOK_GAS_LIMIT = "350000"; + +const CHAIN_ID = SupportedChainId.GNOSIS_CHAIN; + +export async function run() { + const wallet = await getWallet(CHAIN_ID); + const eoaTrader = wallet.address as `0x${string}`; + + // Initialize the SDK with the wallet + const sdk = new TradingSdk({ + chainId: CHAIN_ID, + signer: wallet, // Use a signer + appCode: APP_CODE, + }); + + // Get some info about the assets + const { twapSellToken, twapBuyToken } = await getAssetsInfo({ wallet }); + + // First order (sell=buy order): + // The sell=buy order'sonly purpose is to get the post-hook (which creates the + // TWAP) executed gaslessly via a settlement. + // + // It does NOT move the full TWAP sell amount: This is why no the recipient of the funds is still the EOA. + // as opposed to what src/scripts/composable-cow/postTwapForEOA.ts does + // + // Thanks to JIT funding, each part is pulled from the EOA right before it settles. + const firstOrderBuyAmount = BigNumber.from("1"); // sell=buy order: Buy 1 wei of sDAI + + // TWAP Order: + const fullSellAmount = ethers.utils.parseUnits("0.2", twapSellToken.decimals); // TWAP order: Sell a total of 0.2 sDAI + const fullSellAmountFormatted = ethers.utils.formatUnits( + fullSellAmount, + twapSellToken.decimals, + ); + + const partSellAmount = fullSellAmount.div(TWAP_PARTS); + const partSellAmountFormatted = ethers.utils.formatUnits( + partSellAmount, + twapSellToken.decimals, + ); + + const cowShedSdk = getCowShedSdk(); + const cowShed = cowShedSdk.getCowShedAccount(CHAIN_ID, eoaTrader); + console.log("CowShed account:", cowShed); + + // The poller schedule key. It is derived from appData-INDEPENDENT fields + // (funder, handler, owner, salt), which is exactly what lets us embed + // `pollFunds(id)` as a pre-hook inside the TWAP's own appData: the order's `ctx` + // contains the appData hash, so keying on `ctx` would be circular, but `id` + // is not. We choose the salt, so we can compute `id` before the appData. + const poller = getComposableCowPollerContract( + COMPOSABLE_COW_POLLER_ADDRESS, + wallet, + ); + const twapSalt = ethers.utils.hexlify(ethers.utils.randomBytes(32)); + const id: string = await poller.scheduleId( + eoaTrader, + TWAP_HANDLER, + cowShed, + twapSalt, + ); + console.log("Poller schedule id:", id); + + // Describe the flow + console.log( + `TWAP sell ${fullSellAmountFormatted} ${twapSellToken.symbol} for ${twapBuyToken.symbol} in ${TWAP_PARTS} parts (funded just-in-time). + +The setup is done with a gasless sell=buy order with a post-hook: + - Sell=buy order: BUY 1 wei of ${twapSellToken.symbol} with ${twapSellToken.symbol} (sell == buy) + - Order executes a post-hook (via cow-shed): + - Approve the Vault Relayer + - Create the TWAP. Owner of the TWAP is cow-shed (${cowShed}). + - Technically, we can include more things here which are left out of this PoC but are a good idea for the production flow: + - Approve the ComposableCowPoller + - Register the JIT funding schedule on ComposableCowPoller + +The EOA keeps the 1 wei of ${twapSellToken.symbol}, which means that the EOA is the recipient of the first order. +The order will have the side-effects described above. + +Watch Tower will detect the TWAP and create each part, which will settle and send the proceeds back to the EOA. +Each part carries a pre-hook (baked into the TWAP appData) that calls poller.pollFunds(id), pulling exactly that part's +sell amount from the EOA into cow-shed right before it settles. No external keeper is needed. +`, + ); + + // Generate app data for the TWAP, embedding a pre-hook with the polling + const metadataApi = new MetadataApi(); + const pollFundsCalldata = poller.interface.encodeFunctionData("pollFunds", [id]); + const twapAppData = await metadataApi.generateAppDataDoc({ + appCode: APP_CODE, + environment: "prod", + metadata: { + hooks: { + pre: [ + // Call: poll.pollFunds(id) + { + target: COMPOSABLE_COW_POLLER_ADDRESS, + callData: pollFundsCalldata, + gasLimit: POLL_FUNDS_HOOK_GAS_LIMIT, + }, + ], + }, + }, + }); + const { appDataContent: twapAppDataContent, appDataHex: twapAppDataHex } = + await metadataApi.getAppDataInfo(twapAppData); + + const orderBookApi = new OrderBookApi({ + chainId: CHAIN_ID, + }); + + // Quote a single part (sell token -> buy token) to derive a sensible buy amount + // limit. We pass our slippage tolerance so `afterSlippage.buyAmount` is the + // minimum we are willing to receive per part. The TWAP's total buy amount is + // then that per-part minimum scaled by the number of parts. + const { quoteResults: partQuote } = await sdk.getQuote({ + kind: OrderKind.SELL, + sellToken: twapSellToken.address, + sellTokenDecimals: twapSellToken.decimals, + buyToken: twapBuyToken.address, + buyTokenDecimals: twapBuyToken.decimals, + amount: partSellAmount.toString(), + owner: eoaTrader, + slippageBps: TWAP_SLIPPAGE_BPS, + }); + // Expected amount (net of costs, before slippage) and the minimum we will sign + // (after slippage), both per part and scaled to the whole TWAP. + const expectedPartBuyAmount = BigNumber.from( + partQuote.amountsAndCosts.afterNetworkCosts.buyAmount, + ); + const partBuyAmount = BigNumber.from( + partQuote.amountsAndCosts.afterSlippage.buyAmount, + ); + const expectedTwapBuyAmount = expectedPartBuyAmount.mul(TWAP_PARTS); + const twapBuyAmount = partBuyAmount.mul(TWAP_PARTS); + const fmt = (amount: BigNumber) => + `${ethers.utils.formatUnits(amount, twapBuyToken.decimals)} ${twapBuyToken.symbol}`; + console.log( + `TWAP buy amount per part: ~${fmt(expectedPartBuyAmount)} expected, ${fmt(partBuyAmount)} min (after ${TWAP_SLIPPAGE_BPS / 100}% slippage). +TWAP buy amount total: ~${fmt(expectedTwapBuyAmount)} expected, ${fmt(twapBuyAmount)} min.`, + ); + + // Build the TWAP + const twap = Twap.fromData( + { + receiver: eoaTrader, // bought tokens are sent to the trader (the EOA) + sellAmount: fullSellAmount, + buyAmount: twapBuyAmount, + numberOfParts: BigNumber.from(TWAP_PARTS), + timeBetweenParts: BigNumber.from(TWAP_TIME_BETWEEN_PARTS), + sellToken: twapSellToken.address, + buyToken: twapBuyToken.address, + appData: twapAppDataHex, // appData, including the pre-hook to poll funds + }, + twapSalt, // controlled salt, so the schedule `id` matches the embedded hook + ); + + // `ctx == ComposableCoW.hash(params) == twap.id` is the order's cabinet key. + // The poller schedule is keyed by the appData-independent `id` instead. + const ctx = twap.id; + const { handler, salt, staticInput } = twap.leaf; + + // Sanity: the handler/salt must be exactly what we derived `id` from, so the + // `pollFunds(id)` hook baked into the appData resolves to this very schedule. + if ( + handler.toLowerCase() !== TWAP_HANDLER.toLowerCase() || + salt.toLowerCase() !== twapSalt.toLowerCase() + ) { + throw new Error( + `TWAP handler/salt mismatch: handler=${handler} salt=${salt} (expected handler=${TWAP_HANDLER} salt=${twapSalt})`, + ); + } + + console.log("TWAP context (ctx):", ctx); + console.log("TWAP params for creation of order", { + twapParams: twap.leaf, + twapData: debugStringify(twap.data), + twapAppDataContent: twapAppDataContent, + }); + + console.log("Uploading TWAP app data to API..."); + await orderBookApi.uploadAppData(twapAppDataHex, twapAppDataContent); + + // Post-hook: approve the Vault Relayer (so each part can settle from cow-shed) + // and create the TWAP. Both are fund-less calls, so cow-shed never needs to + // hold the full TWAP sell amount up front. + const approveSellTokenCalldata = + twapSellToken.contract.interface.encodeFunctionData("approve", [ + COW_VAULT_RELAYER_CONTRACT, + ethers.constants.MaxUint256, + ]); + + const deadline = BigInt(Math.ceil(Date.now() / 1000)) + 1800n; + console.log( + `Deadline: ${deadline} (${new Date(Number(deadline) * 1000).toISOString()})`, + ); + + const { signedMulticall: approveAndTwap, gasLimit: approveAndTwapGasLimit } = + await cowShedSdk.signCalls({ + chainId: CHAIN_ID, + calls: [ + { + callData: approveSellTokenCalldata, + target: twapSellToken.address, + value: 0n, + isDelegateCall: false, + allowFailure: true, + }, + { + callData: twap.createCalldata, + target: COMPOSABLE_COW_CONTRACT_ADDRESS[CHAIN_ID], + value: 0n, + isDelegateCall: false, + allowFailure: true, + }, + ], + deadline, + signer: wallet, + defaultGasLimit: DEFAULT_GAS_LIMIT, + }); + console.log("Signed approve+twap calldata:", approveAndTwap); + + // Sell=buy order (so, it's a no-operation order) + // Both sellToken and buyOrder matches the TWAP's sellToken + // The post-hook that creates the TWAP. + // It does not move the full TWAP sell amount: + // - This is why the trade amount tries to be minimal (BUY 1 wei, for whatever the quote endpoint said I need to pay for gas) + // - Because funds don't arrive to cow-shed, the recipient can be the EOA (this is where the 1 wei is bought) + // - The funds will be pulled in follow up order's hook (orders will automatically be created by watch-tower) + const { quoteResults, postSwapOrderFromQuote } = await sdk.getQuote( + { + kind: OrderKind.BUY, + sellToken: twapSellToken.address, + sellTokenDecimals: twapSellToken.decimals, + buyToken: twapSellToken.address, // sell == buy + buyTokenDecimals: twapSellToken.decimals, + amount: firstOrderBuyAmount.toString(), // buy 1 wei of sDAI + receiver: eoaTrader, // bought tokens stay with the trader; cow-shed needs no funds until each part is settled + owner: eoaTrader, + partiallyFillable: false, + validFor: 1800, + slippageBps: FIRST_ORDER_SLIPPAGE_BPS, + }, + { + appData: { + appCode: APP_CODE, + metadata: { + hooks: { + post: [ + // Approve the Vault Relayer and create the TWAP + { + callData: approveAndTwap.data, + gasLimit: approveAndTwapGasLimit.toString(), + target: approveAndTwap.to, + dappId: + "cow-sdk-scripts://composable-cow/post-twap-for-eoa-jit", + }, + ], + }, + }, + }, + }, + ); + + // Print the quote + printQuote(quoteResults); + + // Calculate the maximum fee we will pay for the first order + const firstOrderMaxFee = BigNumber.from( + quoteResults.amountsAndCosts.afterSlippage.sellAmount - 1n, // deduct the 1 wei we get back + ); + const firstOrderMaxFeeFormatted = ethers.utils.formatUnits( + firstOrderMaxFee, + twapSellToken.decimals, + ); + + // Ask for confirmation before doing anything on-chain + const confirmed = await confirm( + `This will: + 1. Approve the Vault Relayer to spend ${twapSellToken.symbol} (for the sell=buy order). + 2. Approve the ComposableCowPoller to spend up to ${fullSellAmountFormatted} ${twapSellToken.symbol} (the full TWAP sell amount, pulled JIT). + 3. Register the JIT funding schedule on the poller (funder: your EOA, owner: cow-shed). + 4. Place the sell=buy order, whose post-hook creates the TWAP. + ... + 5. [watch-tower] Detects the TWAP and creates each part, which settle and proceeds are sent back to the EOA. + +🥳 Each part will poll ${partSellAmountFormatted} ${twapSellToken.symbol} from your EOA before filling. + +Your EOA will receive: ~${fmt(expectedTwapBuyAmount)} (expected), at least ${fmt(twapBuyAmount)} (min, after ${TWAP_SLIPPAGE_BPS / 100}% slippage) across the ${TWAP_PARTS} parts. +You will pay at most ${firstOrderMaxFeeFormatted} ${twapSellToken.symbol} for placing and setting up the TWAP. + +ok?`, + ); + if (!confirmed) { + console.log("Aborted"); + return; + } + + // 1. Approve the Vault Relayer for the sell=buy order's sell token (the max the + // sell=buy order could spend to buy 1 wei, i.e. mostly the fee). + await ensureAllowance({ + token: twapSellToken, + owner: eoaTrader, + spender: COW_VAULT_RELAYER_CONTRACT, + requiredAmount: firstOrderMaxFee, + label: "Vault Relayer", + }); + + // 2. Approve the poller to pull the full TWAP sell amount from the EOA over time + // NOTE: For permit tokens, we can include the permit call as part of the first order + await ensureAllowance({ + token: twapSellToken, + owner: eoaTrader, + spender: COMPOSABLE_COW_POLLER_ADDRESS, + requiredAmount: fullSellAmount, + label: "ComposableCowPoller", + }); + + // 3. Register the JIT funding schedule (only the funder may register). + // NOTE: We could make the poller registration also accept a signature. + // This way, this part can always be chained as part of the first-order post-hook and we don't need this transaction + const existing = await poller.schedules(id); + if (existing.funder !== ethers.constants.AddressZero) { + console.log( + `Schedule already registered for id ${id} (funder: ${existing.funder}). Skipping register.`, + ); + } else { + console.log("Registering JIT funding schedule on the poller..."); + const registerTx = await poller.register({ + handler, + funder: eoaTrader, + owner: cowShed, + salt, + staticInput, + }); + console.log("Register tx:", getExplorerUrl(CHAIN_ID, registerTx.hash)); + await registerTx.wait(); + console.log("Schedule registered"); + } + + // 4. Place the sell=buy order. Its post-hook creates the TWAP gaslessly. + const { orderId } = await postSwapOrderFromQuote(); + console.log( + `Sell=buy order created, id: https://explorer.cow.fi/gc/orders/${orderId}?tab=overview`, + ); + console.log( + `Once it settles, the TWAP (ctx ${ctx}) will be live and funded (just-in-time). + +Monitor parts in https://explorer.cow.fi/gc/address/${cowShed}`, + ); +} + +async function ensureAllowance(params: { + token: Token; + owner: string; + spender: string; + requiredAmount: BigNumber; + label: string; +}) { + const { token, owner, spender, requiredAmount, label } = params; + const allowance: BigNumber = await token.contract.allowance(owner, spender); + console.log( + `Allowance for ${label}: ${ethers.utils.formatUnits( + allowance, + token.decimals, + )} ${token.symbol}`, + ); + if (allowance.gte(requiredAmount)) { + return; + } + + console.log(`Approving ${token.symbol} for ${label}...`); + const tx = await token.contract.approve(spender, ethers.constants.MaxUint256); + console.log( + `Approving ${token.symbol} for ${label}. tx:`, + getExplorerUrl(CHAIN_ID, tx.hash), + ); + await tx.wait(); + console.log(`${token.symbol} approved for ${label}`); +} + +async function getAssetsInfo(params: { wallet: ethers.Wallet }): Promise<{ + twapSellToken: Token; + twapBuyToken: Token; +}> { + const { wallet } = params; + + const twapSellToken = await getErc20Contract(TOKENS.twapSellToken, wallet); + const twapBuyToken = await getErc20Contract(TOKENS.twapBuyToken, wallet); + + const [ + twapSellTokenSymbol, + twapSellTokenDecimals, + twapBuyTokenSymbol, + twapBuyTokenDecimals, + ] = await Promise.all([ + twapSellToken.symbol(), + twapSellToken.decimals(), + twapBuyToken.symbol(), + twapBuyToken.decimals(), + ]); + + return { + twapSellToken: { + symbol: twapSellTokenSymbol, + address: twapSellToken.address, + decimals: twapSellTokenDecimals, + contract: twapSellToken, + }, + twapBuyToken: { + symbol: twapBuyTokenSymbol, + address: twapBuyToken.address, + decimals: twapBuyTokenDecimals, + contract: twapBuyToken, + }, + }; +}