From 6835043aeba40c4e95fcea691c1f52f2b9ceebc5 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Mon, 7 Jul 2025 23:22:10 +0200 Subject: [PATCH 1/9] Draft collateral swap script --- package.json | 2 +- src/index.ts | 5 +- src/scripts/bridging/swapAndBridgeSdk.ts | 2 +- .../flash-loans/OrderHelperFactoryAbi.ts | 112 +++++++++ src/scripts/flash-loans/collateralSwapAave.ts | 225 ++++++++++++++++++ src/utils.ts | 26 ++ yarn.lock | 18 +- 7 files changed, 378 insertions(+), 12 deletions(-) create mode 100644 src/scripts/flash-loans/OrderHelperFactoryAbi.ts create mode 100644 src/scripts/flash-loans/collateralSwapAave.ts diff --git a/package.json b/package.json index 8ea810c..7f9eef2 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "typescript": "^5.7.3" }, "dependencies": { - "@cowprotocol/cow-sdk": "6.0.0-RC.42", + "@cowprotocol/cow-sdk": "6.0.0-RC.47", "@cowprotocol/app-data": "3.0.0-rc.1", "@cowprotocol/contracts": "^1.7.0", "axios": "^1.7.9", diff --git a/src/index.ts b/src/index.ts index 0c8aa4a..6ee52ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,6 +35,7 @@ import { run as getTradingQuote } from "./scripts/trading/getQuote"; import { run as getAcrossBridgingId } from "./scripts/bridging/getAcrossBridgingId"; import { run as getEthFlowId } from "./scripts/ethflow/getEthFlowId"; import { run as minimalAppData } from "./scripts/app-data/minimalAppData"; +import { run as collateralSwapAave } from "./scripts/flash-loans/collateralSwapAave"; dotenv.config(); @@ -42,6 +43,7 @@ dotenv.config(); const JOBS: (() => Promise)[] = [ // approveTokenSepolia, // Required to approve the token before trading // getQuoteAndPostOrder, // Simplest way to integrate! + // getQuoteAndPostOrderNative, // // swapWithPk, // swapSell, @@ -64,7 +66,7 @@ const JOBS: (() => Promise)[] = [ // nativeSell, // FIXME: Throws error creating the eth flow order. Doesn't recognize 'gas' property - I believe expects 'gasLimit') // // approveTokenMainnet, - getQuoteAndPostOrderMainnet, + // getQuoteAndPostOrderMainnet, // swapAndBridgeUsingOmnibridge, // swapAndBridgeUsingXdaiBridge, // @@ -83,6 +85,7 @@ const JOBS: (() => Promise)[] = [ // getAcrossBridgingId, // getEthFlowId, // minimalAppData, + collateralSwapAave, ]; async function main() { diff --git a/src/scripts/bridging/swapAndBridgeSdk.ts b/src/scripts/bridging/swapAndBridgeSdk.ts index 60dd34f..a06afa1 100644 --- a/src/scripts/bridging/swapAndBridgeSdk.ts +++ b/src/scripts/bridging/swapAndBridgeSdk.ts @@ -122,7 +122,7 @@ export async function run() { } // Post the order - const orderId = await quote.postSwapOrderFromQuote(); + const { orderId } = await quote.postSwapOrderFromQuote(); // Print the order creation console.log( diff --git a/src/scripts/flash-loans/OrderHelperFactoryAbi.ts b/src/scripts/flash-loans/OrderHelperFactoryAbi.ts new file mode 100644 index 0000000..fabab55 --- /dev/null +++ b/src/scripts/flash-loans/OrderHelperFactoryAbi.ts @@ -0,0 +1,112 @@ +export const orderHelperFactoryAbi = [ + { + type: "constructor", + inputs: [ + { + name: "_helperImplementation", + type: "address", + internalType: "address", + }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "deployOrderHelper", + inputs: [ + { name: "_owner", type: "address", internalType: "address" }, + { name: "_borrower", type: "address", internalType: "address" }, + { + name: "_oldCollateral", + type: "address", + internalType: "address", + }, + { + name: "_oldCollateralAmount", + type: "uint256", + internalType: "uint256", + }, + { + name: "_newCollateral", + type: "address", + internalType: "address", + }, + { + name: "_minSupplyAmount", + type: "uint256", + internalType: "uint256", + }, + { name: "_validTo", type: "uint32", internalType: "uint32" }, + ], + outputs: [ + { + name: "orderHelperAddress", + type: "address", + internalType: "address", + }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "getOrderHelperAddress", + inputs: [ + { name: "_owner", type: "address", internalType: "address" }, + { name: "_borrower", type: "address", internalType: "address" }, + { + name: "_oldCollateral", + type: "address", + internalType: "address", + }, + { + name: "_oldCollateralAmount", + type: "uint256", + internalType: "uint256", + }, + { + name: "_newCollateral", + type: "address", + internalType: "address", + }, + { + name: "_minSupplyAmount", + type: "uint256", + internalType: "uint256", + }, + { name: "_validTo", type: "uint32", internalType: "uint32" }, + ], + outputs: [ + { + name: "orderHelperAddress", + type: "address", + internalType: "address", + }, + ], + stateMutability: "view", + }, + { + type: "event", + name: "NewOrderHelper", + inputs: [ + { + name: "helper", + type: "address", + indexed: true, + internalType: "address", + }, + ], + anonymous: false, + }, + { type: "error", name: "ContractAlreadyDeployed", inputs: [] }, + { type: "error", name: "FailedDeployment", inputs: [] }, + { + type: "error", + name: "InsufficientBalance", + inputs: [ + { name: "balance", type: "uint256", internalType: "uint256" }, + { name: "needed", type: "uint256", internalType: "uint256" }, + ], + }, + { type: "error", name: "InvalidImplementationContract", inputs: [] }, + { type: "error", name: "OrderHelperDeploymentFailed", inputs: [] }, +] as const; diff --git a/src/scripts/flash-loans/collateralSwapAave.ts b/src/scripts/flash-loans/collateralSwapAave.ts new file mode 100644 index 0000000..01d0d45 --- /dev/null +++ b/src/scripts/flash-loans/collateralSwapAave.ts @@ -0,0 +1,225 @@ +import { sepolia, APP_CODE } from "../../const"; +const { WETH_ADDRESS, COW_ADDRESS } = sepolia; +import { + SupportedChainId, + OrderKind, + TradeParameters, + TradingSdk, + SigningScheme, +} from "@cowprotocol/cow-sdk"; +import { ethers } from "ethers"; +import { confirm, getWallet, printQuote } from "../../utils"; +import { getErc20Contract } from "../../contracts/erc20"; +import { latest } from "@cowprotocol/app-data"; +import { orderHelperFactoryAbi } from "./OrderHelperFactoryAbi"; + +// * Collateral - aETHWeth: https://sepolia.etherscan.io/token/0x5b071b590a59395fe4025a0ccc1fcc931aac1830 +// * Underlying - WETH: https://sepolia.etherscan.io/address/0xc558dbdd856501fcd9aaf1e62eae57a9f0629a3c#code +// * Supply 10 aETHWeth: https://sepolia.etherscan.io/tx/0x7cf4f7853963292ff7819d4a5cd5e31c55e7f679e49237c93315b47029486698 +// * Borrowed 1000 GHO: https://sepolia.etherscan.io/tx/0xb470bbf7e98d1b4cad7fa79e97b64e295bb2e077f0e91f9220d39c48f339641c + +const TOKENS = { + oldUnderlying: "0xc558dbdd856501fcd9aaf1e62eae57a9f0629a3c", // WETH + oldCollateral: "0x5b071b590a59395fe4025a0ccc1fcc931aac1830", // aETHWeth + debt: "0xc4bf5cbdabe595361438f8c6a187bdc330539c60", // GHO + newUnderlying: "0x94a9d9ac8a22534e3faca9f4e7f2e2cf85d5e4c8", // USDC + newCollateral: "0x40d16fc0236f5686f0a7030063ca493c4dd83358", // aUSDC +} as const; + +const AAVE_POOL_ADDRESS = "0x6Ae43d3271ff6888e7Fc43Fd7321a503ff738951"; // See https://search.onaave.com/?q=sepolia +const COW_AAVE_BORROWER = "0x7d9C4DeE56933151Bc5C909cfe09DEf0d315CB4A"; // See https://github.com/cowprotocol/flash-loan-router/blob/main/networks.json +const COW_AAVE_HELPER_FACTORY = "0xc55098a66d2225c37bf33c1f7b8b9b0abc8fd32f"; // https://sepolia.etherscan.io/address/0xc55098a66d2225c37bf33c1f7b8b9b0abc8fd32f#code +const DEFAULT_GAS_LIMIT = "1000000"; // FIXME: This should not be necessary, it should estimate correctly! + +const CHAIN_ID = SupportedChainId.SEPOLIA; + +export async function run() { + const wallet = await getWallet(CHAIN_ID); + const trader = wallet.address; + + // Initialize the SDK with the wallet + const sdk = new TradingSdk({ + chainId: CHAIN_ID, + signer: wallet, // Use a signer + appCode: APP_CODE, + }); + + // Get ERC20 balance for oldUnderlying using ethersjs + const oldUnderlying = await getErc20Contract(TOKENS.oldUnderlying, wallet); + const oldCollateral = await getErc20Contract(TOKENS.oldCollateral, wallet); + const [oldUnderlyingSymbol, oldUnderlyingDecimals, oldUnderlingBalance] = + await Promise.all([ + oldUnderlying.symbol(), + oldUnderlying.decimals(), + oldCollateral.balanceOf(trader), + ]); + const oldUnderlyingBalanceFormatted = ethers.utils.formatUnits( + oldUnderlingBalance, + oldUnderlyingDecimals + ); + + console.log( + `Old underlying balance: ${oldUnderlyingBalanceFormatted} ${oldUnderlyingSymbol}` + ); + + const newUnderlying = await getErc20Contract(TOKENS.newUnderlying, wallet); + const [newUnderlyingSymbol, newUnderlyingDecimals] = await Promise.all([ + newUnderlying.symbol(), + newUnderlying.decimals(), + ]); + + const expirationTimestamp = 60 * 30; // 30 minutes from now + console.log("expirationTimestamp", expirationTimestamp); + + const minReceivedAmount = "1"; // 1 Wei. Technically I would need to ask for a quote. Its a bit tricky, because we would need to ask for a quote with the helper contract as owner. Could be possible with a dirty trick (find an user with balance for the oldUnderlying and ask for a quote to dump it for the newUnderlying) + + const orderHelperParams = [ + trader, // owner + AAVE_POOL_ADDRESS, // borrower + TOKENS.oldCollateral, + oldUnderlingBalance, + TOKENS.newCollateral, + minReceivedAmount, + expirationTimestamp, + ]; + + // Ger factory contract instance + const orderHelperFactory = new ethers.Contract( + COW_AAVE_HELPER_FACTORY, + orderHelperFactoryAbi, + wallet + ); + console.log("Get helper contract", orderHelperParams); + const helperContract: string = await orderHelperFactory.getOrderHelperAddress( + ...orderHelperParams + ); + + // TODO: We might want to use this function to save one RPC call + // const helperContract = predictDeterministicAddress({ + // implementation, + // salt: trader, + // factoryAddress: COW_AAVE_COLLATERAL_SWAP_HELPER_FACTORY, + // }); + + // Define trade parameters + console.log( + `Get quote for selling ${oldUnderlyingBalanceFormatted} ${oldUnderlyingSymbol} for ${newUnderlyingSymbol}` + ); + const parameters: TradeParameters = { + kind: OrderKind.SELL, + amount: oldUnderlingBalance.toString(), // All underlying balance + sellToken: TOKENS.oldUnderlying, + sellTokenDecimals: oldUnderlyingDecimals, + buyToken: TOKENS.newUnderlying, + buyTokenDecimals: newUnderlyingDecimals, + + partiallyFillable: false, + owner: helperContract as `0x${string}`, + receiver: helperContract, + validFor: expirationTimestamp, + }; + console.log("Trade parameters", parameters); + + // Flash loan + const flashLoanHint = { + lender: AAVE_POOL_ADDRESS, + borrower: COW_AAVE_BORROWER, + token: TOKENS.oldUnderlying, + amount: oldUnderlingBalance, + // TODO: how would we tell the hint we want to send the tokens to the helper? + loanReceiver: helperContract // TODO: not implemented in backend + }; + console.log("flashLoanHint", flashLoanHint); + + // Prepare deployment of the helper contract + const deployOrderHelperData = orderHelperFactory.interface.encodeFunctionData( + "deployOrderHelper", + orderHelperParams + ); + console.log("deployOrderHelperData", deployOrderHelperData); + + const gasEstimate = await wallet + .estimateGas({ + to: COW_AAVE_HELPER_FACTORY, + data: deployOrderHelperData, + value: ethers.constants.Zero, + }) + .catch((error) => { + console.error("error estimating gas", error); + console.log("Check the call", { + to: COW_AAVE_HELPER_FACTORY, + data: deployOrderHelperData, + }); + return DEFAULT_GAS_LIMIT; + }); + console.log("gasEstimate", gasEstimate); + + const helperContractDeployment: latest.CoWHook = { + target: COW_AAVE_HELPER_FACTORY, + callData: deployOrderHelperData, + gasLimit: gasEstimate.toString(), + dappId: "cow-sdk-scripts://flash-loans/collateralSwapAave", + }; + + // Post the order + const { quoteResults, postSwapOrderFromQuote } = await sdk.getQuote( + parameters, + { + additionalParams: { + signingScheme: SigningScheme.EIP1271, + }, + appData: { + appCode: APP_CODE, + metadata: { + // @ts-ignore The flash-loan hint is still not added officially to https://github.com/cowprotocol/app-data + flashLoan: flashLoanHint, + hooks: { + pre: [helperContractDeployment], + }, + }, + }, + } + ); + + printQuote(quoteResults); + const buyAmount = quoteResults.amountsAndCosts.afterSlippage.buyAmount; + + const confirmed = await confirm( + `You will get at least ${buyAmount} COW. ok?` + ); + if (confirmed) { + // Approve the helper contract to spend the old collateral + const oldCollateral = await getErc20Contract(TOKENS.oldCollateral, wallet); + + // Get the allowance for the helper contract + const allowance = await oldCollateral.allowance(trader, helperContract); + const allowanceFormatted = ethers.utils.formatUnits( + allowance, + oldUnderlyingDecimals + ); + console.log( + `Allowance for the helper contract: ${allowanceFormatted} ${oldUnderlyingSymbol}` + ); + + if (allowance < oldUnderlingBalance) { + console.log( + "Alright! First make sure the helper contract has an approval (we could use permit pre-hook instead too)" + ); + + const tx = await oldCollateral.approve( + helperContract, + oldUnderlingBalance + ); + await tx.wait(); + } else { + console.log("The helper contract has enough allowance to post the order"); + } + + // Post the order + const { orderId } = await postSwapOrderFromQuote(); + + console.log( + `Order created, id: https://explorer.cow.fi/sepolia/orders/${orderId}?tab=overview` + ); + } +} diff --git a/src/utils.ts b/src/utils.ts index bfb42c3..32d4eff 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -125,3 +125,29 @@ export function getExplorerUrl(chainId: SupportedChainId, txHash: string) { throw new Error(`Unsupported Explorer for chainId ${chainId}`); } + +export function predictDeterministicAddress(params: { + implementation: string; + salt: string; + factoryAddress: string; +}): string { + const { implementation, salt, factoryAddress } = params; + // EIP-1167 minimal proxy creation code + const creationCode = + "0x3d602d80600a3d3981f3363d3d373d3d3d363d73" + + implementation.slice(2) + + "5af43d82803e903d91602b57fd5bf3"; + + // CREATE2 formula: keccak256(0xff ++ address ++ salt ++ keccak256(creationCode)) + const initCodeHash = ethers.utils.keccak256(creationCode); + + const packed = ethers.utils.solidityPack( + ["bytes1", "address", "bytes32", "bytes32"], + ["0xff", factoryAddress, salt, initCodeHash] + ); + + const hash = ethers.utils.keccak256(packed); + + // Return last 20 bytes (40 characters) as address + return ethers.utils.getAddress("0x" + hash.slice(-40)); +} diff --git a/yarn.lock b/yarn.lock index 9aa616c..0acaf7e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -37,10 +37,10 @@ json-stringify-deterministic "^1.0.8" multiformats "^9.6.4" -"@cowprotocol/app-data@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@cowprotocol/app-data/-/app-data-3.0.0.tgz#e5e7676ee8884c5348553883d0e65562c2f09e66" - integrity sha512-EyNoyoq3VnVIzvylLdn4Q1r0tU7HOikkjXzYY9PXJ/DUfj9gka7lOyNK7Ot8En2eUYTl3DNAle9RP7kt2sEpeg== +"@cowprotocol/app-data@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@cowprotocol/app-data/-/app-data-3.1.0.tgz#2ba5ec7d958b2510f17b08c7e18e05157032c4da" + integrity sha512-hdIWp6fGz/vx3gdoJgwd8Dx8rYAblpZEBiXss5mNzgF/LBb21VVhF075sDl9oxyesKuJayKjgiAgaFiZYVgblA== dependencies: ajv "^8.11.0" cross-fetch "^3.1.5" @@ -58,12 +58,12 @@ resolved "https://registry.yarnpkg.com/@cowprotocol/contracts/-/contracts-1.8.0.tgz#daffbd9846231c11a74b15a186bb754627e420b0" integrity sha512-rMEHo1UBB6k4kRoWejHZNGggg6IBVt7vAd8x0FhEvjxhbq3zlAex61f9HpAcDExJNuvfwwDjsOc/7UGztCzhSw== -"@cowprotocol/cow-sdk@6.0.0-RC.42": - version "6.0.0-RC.42" - resolved "https://registry.yarnpkg.com/@cowprotocol/cow-sdk/-/cow-sdk-6.0.0-RC.42.tgz#8ce000a1e9c6c3c1f70d336e62b8a1f363bbdd68" - integrity sha512-COskvWpwrzgjDRzI/X0WErK1UENbjXGJTaave4Fh/SrINN5I+1bJsiRJQVN72d9vq8VwM7rENjJG1+GzmaTs0A== +"@cowprotocol/cow-sdk@6.0.0-RC.47": + version "6.0.0-RC.47" + resolved "https://registry.yarnpkg.com/@cowprotocol/cow-sdk/-/cow-sdk-6.0.0-RC.47.tgz#45e993e2d96c49de375d948ad5d51c2623734afd" + integrity sha512-ilatk1z2MGa50huTezzknWXpxISQHE2XTsv5C08l529q7ZzPVlnbBpdVuilJrDp8hbBEFD4fuLy+tCESZLRk3w== dependencies: - "@cowprotocol/app-data" "^3.0.0" + "@cowprotocol/app-data" "^3.1.0" "@cowprotocol/contracts" "^1.8.0" "@ethersproject/abstract-signer" "^5.8.0" "@openzeppelin/merkle-tree" "^1.0.8" From 8db036fb3cd364efd269413941663e700d5baa53 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Tue, 8 Jul 2025 10:09:37 +0200 Subject: [PATCH 2/9] Move to abi folder --- .../orderHelperFactoryAbi.ts} | 0 src/scripts/flash-loans/collateralSwapAave.ts | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/scripts/flash-loans/{OrderHelperFactoryAbi.ts => abi/orderHelperFactoryAbi.ts} (100%) diff --git a/src/scripts/flash-loans/OrderHelperFactoryAbi.ts b/src/scripts/flash-loans/abi/orderHelperFactoryAbi.ts similarity index 100% rename from src/scripts/flash-loans/OrderHelperFactoryAbi.ts rename to src/scripts/flash-loans/abi/orderHelperFactoryAbi.ts diff --git a/src/scripts/flash-loans/collateralSwapAave.ts b/src/scripts/flash-loans/collateralSwapAave.ts index 01d0d45..7800174 100644 --- a/src/scripts/flash-loans/collateralSwapAave.ts +++ b/src/scripts/flash-loans/collateralSwapAave.ts @@ -11,7 +11,7 @@ import { ethers } from "ethers"; import { confirm, getWallet, printQuote } from "../../utils"; import { getErc20Contract } from "../../contracts/erc20"; import { latest } from "@cowprotocol/app-data"; -import { orderHelperFactoryAbi } from "./OrderHelperFactoryAbi"; +import { orderHelperFactoryAbi } from "./abi/orderHelperFactoryAbi"; // * Collateral - aETHWeth: https://sepolia.etherscan.io/token/0x5b071b590a59395fe4025a0ccc1fcc931aac1830 // * Underlying - WETH: https://sepolia.etherscan.io/address/0xc558dbdd856501fcd9aaf1e62eae57a9f0629a3c#code @@ -127,7 +127,7 @@ export async function run() { token: TOKENS.oldUnderlying, amount: oldUnderlingBalance, // TODO: how would we tell the hint we want to send the tokens to the helper? - loanReceiver: helperContract // TODO: not implemented in backend + loanReceiver: helperContract, // TODO: not implemented in backend }; console.log("flashLoanHint", flashLoanHint); From b323dc4dbce3170bbbb06d157b70449aabd44fec Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Tue, 8 Jul 2025 10:51:42 +0200 Subject: [PATCH 3/9] Cleanup --- ...FactoryAbi.ts => OrderHelperFactoryAbi.ts} | 0 src/scripts/flash-loans/collateralSwapAave.ts | 236 ++++++++++++------ 2 files changed, 165 insertions(+), 71 deletions(-) rename src/scripts/flash-loans/abi/{orderHelperFactoryAbi.ts => OrderHelperFactoryAbi.ts} (100%) diff --git a/src/scripts/flash-loans/abi/orderHelperFactoryAbi.ts b/src/scripts/flash-loans/abi/OrderHelperFactoryAbi.ts similarity index 100% rename from src/scripts/flash-loans/abi/orderHelperFactoryAbi.ts rename to src/scripts/flash-loans/abi/OrderHelperFactoryAbi.ts diff --git a/src/scripts/flash-loans/collateralSwapAave.ts b/src/scripts/flash-loans/collateralSwapAave.ts index 7800174..e8664b8 100644 --- a/src/scripts/flash-loans/collateralSwapAave.ts +++ b/src/scripts/flash-loans/collateralSwapAave.ts @@ -1,17 +1,19 @@ import { sepolia, APP_CODE } from "../../const"; -const { WETH_ADDRESS, COW_ADDRESS } = sepolia; + import { SupportedChainId, OrderKind, TradeParameters, TradingSdk, SigningScheme, + WithPartialTraderParams, + SwapAdvancedSettings, } from "@cowprotocol/cow-sdk"; import { ethers } from "ethers"; import { confirm, getWallet, printQuote } from "../../utils"; import { getErc20Contract } from "../../contracts/erc20"; import { latest } from "@cowprotocol/app-data"; -import { orderHelperFactoryAbi } from "./abi/orderHelperFactoryAbi"; +import { orderHelperFactoryAbi } from "./abi/OrderHelperFactoryAbi"; // * Collateral - aETHWeth: https://sepolia.etherscan.io/token/0x5b071b590a59395fe4025a0ccc1fcc931aac1830 // * Underlying - WETH: https://sepolia.etherscan.io/address/0xc558dbdd856501fcd9aaf1e62eae57a9f0629a3c#code @@ -44,6 +46,113 @@ export async function run() { appCode: APP_CODE, }); + // Get some info about the assets + const { + oldUnderlingBalance, + oldUnderlyingSymbol, + oldUnderlyingDecimals, + oldUnderlyingBalanceFormatted, + newUnderlyingSymbol, + newUnderlyingDecimals, + } = await getAssetsInfo({ wallet, trader }); + + // Define trade parameters + console.log( + `Get quote for selling ${oldUnderlyingBalanceFormatted} ${oldUnderlyingSymbol} for ${newUnderlyingSymbol}` + ); + + // Get the order details + const { parameters, advancedSettings, helperContract } = + await getOrderDetails({ + trader, + oldUnderlingBalance, + oldUnderlyingDecimals, + newUnderlyingDecimals, + wallet, + }); + + // Post the 1271 order (including the flash-loan hint and the pre-hook) + const { quoteResults, postSwapOrderFromQuote } = await sdk.getQuote( + parameters, + advancedSettings + ); + + // Print the quote + printQuote(quoteResults); + const buyAmount = quoteResults.amountsAndCosts.afterSlippage.buyAmount; + + // Ask for confirmation before posting the order + const confirmed = await confirm( + `You will get at least ${buyAmount} COW. ok?` + ); + if (confirmed) { + // User allows to transfer the old collateral to the helper contract + await approveOldCollateral({ + wallet, + trader, + helperContract, + oldUnderlingBalance, + oldUnderlyingDecimals, + oldUnderlyingSymbol, + }); + + // Post the order + const { orderId } = await postSwapOrderFromQuote(); + + console.log( + `Order created, id: https://explorer.cow.fi/sepolia/orders/${orderId}?tab=overview` + ); + } +} + +async function approveOldCollateral(params: { + wallet: ethers.Wallet; + trader: string; + helperContract: string; + oldUnderlingBalance: ethers.BigNumberish; + oldUnderlyingDecimals: number; + oldUnderlyingSymbol: string; +}) { + const { + wallet, + trader, + helperContract, + oldUnderlingBalance, + oldUnderlyingDecimals, + oldUnderlyingSymbol, + } = params; + + // Approve the helper contract to spend the old collateral + const oldCollateral = await getErc20Contract(TOKENS.oldCollateral, wallet); + + // Get the allowance for the helper contract + const allowance = await oldCollateral.allowance(trader, helperContract); + const allowanceFormatted = ethers.utils.formatUnits( + allowance, + oldUnderlyingDecimals + ); + console.log( + `Allowance for the helper contract: ${allowanceFormatted} ${oldUnderlyingSymbol}` + ); + + if (allowance < oldUnderlingBalance) { + console.log( + "Alright! First make sure the helper contract has an approval (we could use permit pre-hook instead too)" + ); + + const tx = await oldCollateral.approve(helperContract, oldUnderlingBalance); + await tx.wait(); + } else { + console.log("The helper contract has enough allowance to post the order"); + } +} + +async function getAssetsInfo(params: { + wallet: ethers.Wallet; + trader: string; +}) { + const { wallet, trader } = params; + // Get ERC20 balance for oldUnderlying using ethersjs const oldUnderlying = await getErc20Contract(TOKENS.oldUnderlying, wallet); const oldCollateral = await getErc20Contract(TOKENS.oldCollateral, wallet); @@ -68,10 +177,41 @@ export async function run() { newUnderlying.decimals(), ]); - const expirationTimestamp = 60 * 30; // 30 minutes from now - console.log("expirationTimestamp", expirationTimestamp); + return { + // Old underlying info + oldUnderlingBalance, + oldUnderlyingSymbol, + oldUnderlyingDecimals, + oldUnderlyingBalanceFormatted, + + // New underlying info + newUnderlyingSymbol, + newUnderlyingDecimals, + }; +} + +async function getOrderDetails(props: { + trader: string; + oldUnderlingBalance: ethers.BigNumberish; + oldUnderlyingDecimals: number; + newUnderlyingDecimals: number; + wallet: ethers.Wallet; +}): Promise<{ + parameters: WithPartialTraderParams; + advancedSettings?: SwapAdvancedSettings; + helperContract: string; +}> { + const { + trader, + oldUnderlingBalance, + oldUnderlyingDecimals, + newUnderlyingDecimals, + wallet, + } = props; - const minReceivedAmount = "1"; // 1 Wei. Technically I would need to ask for a quote. Its a bit tricky, because we would need to ask for a quote with the helper contract as owner. Could be possible with a dirty trick (find an user with balance for the oldUnderlying and ask for a quote to dump it for the newUnderlying) + // Get the minimum receive + const minReceivedAmount = "1"; // 1 Wei. Technically I would need to ask for a quote. Its a bit tricky, because we would need to ask for a quote with the helper contract as owner. Could be possible with a dirty trick (find an user with balance for the oldUnderlying and ask for a quote to dump it for the newUnderlying). For simplicity, I start hardcoding to 1 web. + const validFor = 60 * 30; // 30 minutes from now const orderHelperParams = [ trader, // owner @@ -80,7 +220,7 @@ export async function run() { oldUnderlingBalance, TOKENS.newCollateral, minReceivedAmount, - expirationTimestamp, + validFor, ]; // Ger factory contract instance @@ -93,18 +233,13 @@ export async function run() { const helperContract: string = await orderHelperFactory.getOrderHelperAddress( ...orderHelperParams ); - // TODO: We might want to use this function to save one RPC call // const helperContract = predictDeterministicAddress({ // implementation, - // salt: trader, + // salt: getSalt(orderHelperParams), // factoryAddress: COW_AAVE_COLLATERAL_SWAP_HELPER_FACTORY, // }); - // Define trade parameters - console.log( - `Get quote for selling ${oldUnderlyingBalanceFormatted} ${oldUnderlyingSymbol} for ${newUnderlyingSymbol}` - ); const parameters: TradeParameters = { kind: OrderKind.SELL, amount: oldUnderlingBalance.toString(), // All underlying balance @@ -116,7 +251,7 @@ export async function run() { partiallyFillable: false, owner: helperContract as `0x${string}`, receiver: helperContract, - validFor: expirationTimestamp, + validFor, }; console.log("Trade parameters", parameters); @@ -161,65 +296,24 @@ export async function run() { dappId: "cow-sdk-scripts://flash-loans/collateralSwapAave", }; - // Post the order - const { quoteResults, postSwapOrderFromQuote } = await sdk.getQuote( - parameters, - { - additionalParams: { - signingScheme: SigningScheme.EIP1271, - }, - appData: { - appCode: APP_CODE, - metadata: { - // @ts-ignore The flash-loan hint is still not added officially to https://github.com/cowprotocol/app-data - flashLoan: flashLoanHint, - hooks: { - pre: [helperContractDeployment], - }, + const advancedSettings: SwapAdvancedSettings = { + additionalParams: { + signingScheme: SigningScheme.EIP1271, + }, + appData: { + appCode: APP_CODE, + metadata: { + // @ts-ignore The flash-loan hint is still not added officially to https://github.com/cowprotocol/app-data + flashLoan: flashLoanHint, + hooks: { + pre: [helperContractDeployment], }, }, - } - ); - - printQuote(quoteResults); - const buyAmount = quoteResults.amountsAndCosts.afterSlippage.buyAmount; - - const confirmed = await confirm( - `You will get at least ${buyAmount} COW. ok?` - ); - if (confirmed) { - // Approve the helper contract to spend the old collateral - const oldCollateral = await getErc20Contract(TOKENS.oldCollateral, wallet); - - // Get the allowance for the helper contract - const allowance = await oldCollateral.allowance(trader, helperContract); - const allowanceFormatted = ethers.utils.formatUnits( - allowance, - oldUnderlyingDecimals - ); - console.log( - `Allowance for the helper contract: ${allowanceFormatted} ${oldUnderlyingSymbol}` - ); - - if (allowance < oldUnderlingBalance) { - console.log( - "Alright! First make sure the helper contract has an approval (we could use permit pre-hook instead too)" - ); - - const tx = await oldCollateral.approve( - helperContract, - oldUnderlingBalance - ); - await tx.wait(); - } else { - console.log("The helper contract has enough allowance to post the order"); - } - - // Post the order - const { orderId } = await postSwapOrderFromQuote(); + }, + }; - console.log( - `Order created, id: https://explorer.cow.fi/sepolia/orders/${orderId}?tab=overview` - ); - } + return { + parameters, + advancedSettings, + }; } From fd2b8134891dfdc1cc7f9d4559745bb0d98848fb Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Tue, 8 Jul 2025 11:00:08 +0200 Subject: [PATCH 4/9] Add better docs --- src/scripts/flash-loans/collateralSwapAave.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/scripts/flash-loans/collateralSwapAave.ts b/src/scripts/flash-loans/collateralSwapAave.ts index e8664b8..96f1e8f 100644 --- a/src/scripts/flash-loans/collateralSwapAave.ts +++ b/src/scripts/flash-loans/collateralSwapAave.ts @@ -15,10 +15,17 @@ import { getErc20Contract } from "../../contracts/erc20"; import { latest } from "@cowprotocol/app-data"; import { orderHelperFactoryAbi } from "./abi/OrderHelperFactoryAbi"; -// * Collateral - aETHWeth: https://sepolia.etherscan.io/token/0x5b071b590a59395fe4025a0ccc1fcc931aac1830 -// * Underlying - WETH: https://sepolia.etherscan.io/address/0xc558dbdd856501fcd9aaf1e62eae57a9f0629a3c#code -// * Supply 10 aETHWeth: https://sepolia.etherscan.io/tx/0x7cf4f7853963292ff7819d4a5cd5e31c55e7f679e49237c93315b47029486698 -// * Borrowed 1000 GHO: https://sepolia.etherscan.io/tx/0xb470bbf7e98d1b4cad7fa79e97b64e295bb2e077f0e91f9220d39c48f339641c +// To setup an account to test this script: +// 1. Create a test account (PK to use in the script) +// 2. Go to https://app.aave.com, enable sepolia (in the gear icon on the top right), and supply sepolia ETH +// Example: https://sepolia.etherscan.io/tx/0x7cf4f7853963292ff7819d4a5cd5e31c55e7f679e49237c93315b47029486698 +// 3. Borrow some GHO +// Example: https://sepolia.etherscan.io/tx/0xb470bbf7e98d1b4cad7fa79e97b64e295bb2e077f0e91f9220d39c48f339641c +// 4. Add the private key and the RPC URL to the `.env` file: +// ```ini +// RPC_URL_11155111=your-rpc +// PRIVATE_KEY=your-pk +// ``` const TOKENS = { oldUnderlying: "0xc558dbdd856501fcd9aaf1e62eae57a9f0629a3c", // WETH From 2ccca1e5ccc36fd412002d6d6b0466cdbc910d45 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Tue, 8 Jul 2025 11:00:39 +0200 Subject: [PATCH 5/9] Add helper contract --- src/scripts/flash-loans/collateralSwapAave.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/scripts/flash-loans/collateralSwapAave.ts b/src/scripts/flash-loans/collateralSwapAave.ts index 96f1e8f..244631b 100644 --- a/src/scripts/flash-loans/collateralSwapAave.ts +++ b/src/scripts/flash-loans/collateralSwapAave.ts @@ -322,5 +322,6 @@ async function getOrderDetails(props: { return { parameters, advancedSettings, + helperContract, }; } From dbe2e5dca8390a107932aa56a01c5e620f3e1117 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Tue, 8 Jul 2025 11:22:45 +0200 Subject: [PATCH 6/9] Add todo to fix the SDK --- src/scripts/flash-loans/collateralSwapAave.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/scripts/flash-loans/collateralSwapAave.ts b/src/scripts/flash-loans/collateralSwapAave.ts index 244631b..0f69e29 100644 --- a/src/scripts/flash-loans/collateralSwapAave.ts +++ b/src/scripts/flash-loans/collateralSwapAave.ts @@ -79,6 +79,8 @@ export async function run() { }); // Post the 1271 order (including the flash-loan hint and the pre-hook) + // TODO: I believe the SDK doesn't handle very well 1271 orders, we might need to use another specific method to pass also the signature either in the quote, or at the time of posting the order. + // TODO: The signature should contain the order, so it can be decoded: `GPv2Order.Data memory _order = abi.decode(_signature, (GPv2Order.Data));` const { quoteResults, postSwapOrderFromQuote } = await sdk.getQuote( parameters, advancedSettings From 58f1d9c564217eca15db5a29a09340694793df23 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Tue, 8 Jul 2025 11:40:15 +0200 Subject: [PATCH 7/9] Simplify hook creation --- src/scripts/flash-loans/collateralSwapAave.ts | 132 +++++++++++------- 1 file changed, 85 insertions(+), 47 deletions(-) diff --git a/src/scripts/flash-loans/collateralSwapAave.ts b/src/scripts/flash-loans/collateralSwapAave.ts index 0f69e29..c1b3ca3 100644 --- a/src/scripts/flash-loans/collateralSwapAave.ts +++ b/src/scripts/flash-loans/collateralSwapAave.ts @@ -80,7 +80,7 @@ export async function run() { // Post the 1271 order (including the flash-loan hint and the pre-hook) // TODO: I believe the SDK doesn't handle very well 1271 orders, we might need to use another specific method to pass also the signature either in the quote, or at the time of posting the order. - // TODO: The signature should contain the order, so it can be decoded: `GPv2Order.Data memory _order = abi.decode(_signature, (GPv2Order.Data));` + // TODO: The signature should contain the order, so it can be decoded: `GPv2Order.Data memory _order = abi.decode(_signature, (GPv2Order.Data));`. . Keep in mind the signature will be simpler in a future implementation, because we don't need all the order data (most of them are already constants in the contract) const { quoteResults, postSwapOrderFromQuote } = await sdk.getQuote( parameters, advancedSettings @@ -199,28 +199,25 @@ async function getAssetsInfo(params: { }; } -async function getOrderDetails(props: { +async function getHelperDeploymentPreHook(params: { trader: string; oldUnderlingBalance: ethers.BigNumberish; - oldUnderlyingDecimals: number; - newUnderlyingDecimals: number; + minReceivedAmount: string; + validFor: number; + orderHelperFactory: ethers.Contract; wallet: ethers.Wallet; }): Promise<{ - parameters: WithPartialTraderParams; - advancedSettings?: SwapAdvancedSettings; helperContract: string; + helperContractDeploymentHook: latest.CoWHook; }> { const { trader, oldUnderlingBalance, - oldUnderlyingDecimals, - newUnderlyingDecimals, + minReceivedAmount, + validFor, + orderHelperFactory, wallet, - } = props; - - // Get the minimum receive - const minReceivedAmount = "1"; // 1 Wei. Technically I would need to ask for a quote. Its a bit tricky, because we would need to ask for a quote with the helper contract as owner. Could be possible with a dirty trick (find an user with balance for the oldUnderlying and ask for a quote to dump it for the newUnderlying). For simplicity, I start hardcoding to 1 web. - const validFor = 60 * 30; // 30 minutes from now + } = params; const orderHelperParams = [ trader, // owner @@ -232,12 +229,6 @@ async function getOrderDetails(props: { validFor, ]; - // Ger factory contract instance - const orderHelperFactory = new ethers.Contract( - COW_AAVE_HELPER_FACTORY, - orderHelperFactoryAbi, - wallet - ); console.log("Get helper contract", orderHelperParams); const helperContract: string = await orderHelperFactory.getOrderHelperAddress( ...orderHelperParams @@ -249,32 +240,6 @@ async function getOrderDetails(props: { // factoryAddress: COW_AAVE_COLLATERAL_SWAP_HELPER_FACTORY, // }); - const parameters: TradeParameters = { - kind: OrderKind.SELL, - amount: oldUnderlingBalance.toString(), // All underlying balance - sellToken: TOKENS.oldUnderlying, - sellTokenDecimals: oldUnderlyingDecimals, - buyToken: TOKENS.newUnderlying, - buyTokenDecimals: newUnderlyingDecimals, - - partiallyFillable: false, - owner: helperContract as `0x${string}`, - receiver: helperContract, - validFor, - }; - console.log("Trade parameters", parameters); - - // Flash loan - const flashLoanHint = { - lender: AAVE_POOL_ADDRESS, - borrower: COW_AAVE_BORROWER, - token: TOKENS.oldUnderlying, - amount: oldUnderlingBalance, - // TODO: how would we tell the hint we want to send the tokens to the helper? - loanReceiver: helperContract, // TODO: not implemented in backend - }; - console.log("flashLoanHint", flashLoanHint); - // Prepare deployment of the helper contract const deployOrderHelperData = orderHelperFactory.interface.encodeFunctionData( "deployOrderHelper", @@ -298,13 +263,86 @@ async function getOrderDetails(props: { }); console.log("gasEstimate", gasEstimate); - const helperContractDeployment: latest.CoWHook = { + const helperContractDeploymentHook: latest.CoWHook = { target: COW_AAVE_HELPER_FACTORY, callData: deployOrderHelperData, gasLimit: gasEstimate.toString(), dappId: "cow-sdk-scripts://flash-loans/collateralSwapAave", }; + return { + helperContract, + helperContractDeploymentHook, + }; +} + +async function getOrderDetails(props: { + trader: string; + oldUnderlingBalance: ethers.BigNumberish; + oldUnderlyingDecimals: number; + newUnderlyingDecimals: number; + wallet: ethers.Wallet; +}): Promise<{ + parameters: WithPartialTraderParams; + advancedSettings?: SwapAdvancedSettings; + helperContract: string; +}> { + const { + trader, + oldUnderlingBalance, + oldUnderlyingDecimals, + newUnderlyingDecimals, + wallet, + } = props; + + // Get the minimum receive + const minReceivedAmount = "1"; // 1 Wei. Technically I would need to ask for a quote. Its a bit tricky, because we would need to ask for a quote with the helper contract as owner. Could be possible with a dirty trick (find an user with balance for the oldUnderlying and ask for a quote to dump it for the newUnderlying). For simplicity, I start hardcoding to 1 web. + const validFor = 60 * 30; // 30 minutes from now + + // Ger factory contract instance + const orderHelperFactory = new ethers.Contract( + COW_AAVE_HELPER_FACTORY, + orderHelperFactoryAbi, + wallet + ); + + // Get the hook to deploy the helper contract + const { helperContractDeploymentHook, helperContract } = + await getHelperDeploymentPreHook({ + trader, + oldUnderlingBalance, + minReceivedAmount, + validFor, + orderHelperFactory, + wallet, + }); + + const parameters: TradeParameters = { + kind: OrderKind.SELL, + amount: oldUnderlingBalance.toString(), // All underlying balance + sellToken: TOKENS.oldUnderlying, + sellTokenDecimals: oldUnderlyingDecimals, + buyToken: TOKENS.newUnderlying, + buyTokenDecimals: newUnderlyingDecimals, + + partiallyFillable: false, + owner: helperContract as `0x${string}`, + receiver: helperContract, + validFor, + }; + console.log("Trade parameters", parameters); + + // Flash loan + const flashLoanHint = { + lender: AAVE_POOL_ADDRESS, + borrower: COW_AAVE_BORROWER, + token: TOKENS.oldUnderlying, + amount: oldUnderlingBalance, + // TODO: how would we tell the hint we want to send the tokens to the helper? + loanReceiver: helperContract, // TODO: not implemented in backend + }; + console.log("flashLoanHint", flashLoanHint); + const advancedSettings: SwapAdvancedSettings = { additionalParams: { signingScheme: SigningScheme.EIP1271, @@ -315,7 +353,7 @@ async function getOrderDetails(props: { // @ts-ignore The flash-loan hint is still not added officially to https://github.com/cowprotocol/app-data flashLoan: flashLoanHint, hooks: { - pre: [helperContractDeployment], + pre: [helperContractDeploymentHook], }, }, }, From 7dcc827abb9972067c8152f45153718f2b1c9963 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Tue, 8 Jul 2025 11:47:49 +0200 Subject: [PATCH 8/9] Add collateral swap hook --- src/scripts/flash-loans/abi/OrderHelperAbi.ts | 156 ++++++++++++++++++ src/scripts/flash-loans/collateralSwapAave.ts | 29 +++- 2 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 src/scripts/flash-loans/abi/OrderHelperAbi.ts diff --git a/src/scripts/flash-loans/abi/OrderHelperAbi.ts b/src/scripts/flash-loans/abi/OrderHelperAbi.ts new file mode 100644 index 0000000..a60d0ea --- /dev/null +++ b/src/scripts/flash-loans/abi/OrderHelperAbi.ts @@ -0,0 +1,156 @@ +export const orderHelperAbi = [ + { + type: "function", + name: "AAVE_LENDING_POOL", + inputs: [], + outputs: [{ name: "", type: "address", internalType: "address" }], + stateMutability: "view", + }, + { + type: "function", + name: "SETTLEMENT", + inputs: [], + outputs: [{ name: "", type: "address", internalType: "address" }], + stateMutability: "view", + }, + { + type: "function", + name: "appData", + inputs: [], + outputs: [{ name: "", type: "bytes32", internalType: "bytes32" }], + stateMutability: "view", + }, + { + type: "function", + name: "borrower", + inputs: [], + outputs: [{ name: "", type: "address", internalType: "address" }], + stateMutability: "view", + }, + { + type: "function", + name: "initialize", + inputs: [ + { name: "_owner", type: "address", internalType: "address" }, + { name: "_borrower", type: "address", internalType: "address" }, + { + name: "_oldCollateral", + type: "address", + internalType: "address", + }, + { + name: "_oldCollateralAmount", + type: "uint256", + internalType: "uint256", + }, + { + name: "_newCollateral", + type: "address", + internalType: "address", + }, + { + name: "_minSupplyAmount", + type: "uint256", + internalType: "uint256", + }, + { name: "_validTo", type: "uint32", internalType: "uint32" }, + { name: "_appData", type: "bytes32", internalType: "bytes32" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "isValidSignature", + inputs: [ + { name: "_orderHash", type: "bytes32", internalType: "bytes32" }, + { name: "_signature", type: "bytes", internalType: "bytes" }, + ], + outputs: [{ name: "", type: "bytes4", internalType: "bytes4" }], + stateMutability: "view", + }, + { + type: "function", + name: "minSupplyAmount", + inputs: [], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "newCollateral", + inputs: [], + outputs: [{ name: "", type: "address", internalType: "contract IERC20" }], + stateMutability: "view", + }, + { + type: "function", + name: "oldCollateral", + inputs: [], + outputs: [{ name: "", type: "address", internalType: "contract IERC20" }], + stateMutability: "view", + }, + { + type: "function", + name: "oldCollateralAmount", + inputs: [], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "owner", + inputs: [], + outputs: [{ name: "", type: "address", internalType: "address" }], + stateMutability: "view", + }, + { + type: "function", + name: "swapCollateral", + inputs: [], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "validTo", + inputs: [], + outputs: [{ name: "", type: "uint32", internalType: "uint32" }], + stateMutability: "view", + }, + { + type: "event", + name: "Initialized", + inputs: [ + { + name: "version", + type: "uint64", + indexed: false, + internalType: "uint64", + }, + ], + anonymous: false, + }, + { type: "error", name: "AppDataDoesNotMatch", inputs: [] }, + { type: "error", name: "BadBuyToken", inputs: [] }, + { type: "error", name: "BadParameters", inputs: [] }, + { type: "error", name: "BadReceiver", inputs: [] }, + { type: "error", name: "BadSellAmount", inputs: [] }, + { type: "error", name: "BadSellToken", inputs: [] }, + { type: "error", name: "FeeIsNotZero", inputs: [] }, + { type: "error", name: "InvalidInitialization", inputs: [] }, + { type: "error", name: "NoPartiallyFillable", inputs: [] }, + { type: "error", name: "NotEnoughBuyAmount", inputs: [] }, + { type: "error", name: "NotEnoughSupplyAmount", inputs: [] }, + { type: "error", name: "NotInitializing", inputs: [] }, + { type: "error", name: "NotLongerValid", inputs: [] }, + { type: "error", name: "NotSellOrder", inputs: [] }, + { type: "error", name: "OnlyBalanceERC20", inputs: [] }, + { type: "error", name: "OrderDoesNotMatchMessageHash", inputs: [] }, + { + type: "error", + name: "SafeERC20FailedOperation", + inputs: [{ name: "token", type: "address", internalType: "address" }], + }, + { type: "error", name: "WrongValidTo", inputs: [] }, +]; diff --git a/src/scripts/flash-loans/collateralSwapAave.ts b/src/scripts/flash-loans/collateralSwapAave.ts index c1b3ca3..75cc957 100644 --- a/src/scripts/flash-loans/collateralSwapAave.ts +++ b/src/scripts/flash-loans/collateralSwapAave.ts @@ -14,6 +14,7 @@ import { confirm, getWallet, printQuote } from "../../utils"; import { getErc20Contract } from "../../contracts/erc20"; import { latest } from "@cowprotocol/app-data"; import { orderHelperFactoryAbi } from "./abi/OrderHelperFactoryAbi"; +import { orderHelperAbi } from "./abi/OrderHelperAbi"; // To setup an account to test this script: // 1. Create a test account (PK to use in the script) @@ -276,6 +277,28 @@ async function getHelperDeploymentPreHook(params: { }; } +function getCollateralSwapPostHook(params: { + helperContract: string; +}): latest.CoWHook { + const { helperContract } = params; + + // Get the helper contract + const helperContractInstance = new ethers.Contract( + helperContract, + orderHelperAbi + ); + + const collateralSwapHook: latest.CoWHook = { + target: helperContract, + callData: + helperContractInstance.interface.encodeFunctionData("swapCollateral"), + gasLimit: DEFAULT_GAS_LIMIT, // TODO: Estimate gas + dappId: "cow-sdk-scripts://flash-loans/collateralSwapAave", + }; + + return collateralSwapHook; +} + async function getOrderDetails(props: { trader: string; oldUnderlingBalance: ethers.BigNumberish; @@ -317,6 +340,9 @@ async function getOrderDetails(props: { wallet, }); + // Get the hook to swap the collateral + const collateralSwapHook = getCollateralSwapPostHook({ helperContract }); + const parameters: TradeParameters = { kind: OrderKind.SELL, amount: oldUnderlingBalance.toString(), // All underlying balance @@ -338,8 +364,6 @@ async function getOrderDetails(props: { borrower: COW_AAVE_BORROWER, token: TOKENS.oldUnderlying, amount: oldUnderlingBalance, - // TODO: how would we tell the hint we want to send the tokens to the helper? - loanReceiver: helperContract, // TODO: not implemented in backend }; console.log("flashLoanHint", flashLoanHint); @@ -354,6 +378,7 @@ async function getOrderDetails(props: { flashLoan: flashLoanHint, hooks: { pre: [helperContractDeploymentHook], + post: [collateralSwapHook], }, }, }, From 38edf8e392d86c0d9d650adad151f4100c0861a3 Mon Sep 17 00:00:00 2001 From: carlos-cow Date: Wed, 9 Jul 2025 04:17:49 -0400 Subject: [PATCH 9/9] fix: minor issues (#10) --- src/scripts/flash-loans/collateralSwapAave.ts | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/scripts/flash-loans/collateralSwapAave.ts b/src/scripts/flash-loans/collateralSwapAave.ts index 75cc957..b581911 100644 --- a/src/scripts/flash-loans/collateralSwapAave.ts +++ b/src/scripts/flash-loans/collateralSwapAave.ts @@ -9,8 +9,8 @@ import { WithPartialTraderParams, SwapAdvancedSettings, } from "@cowprotocol/cow-sdk"; -import { ethers } from "ethers"; -import { confirm, getWallet, printQuote } from "../../utils"; +import { ethers, providers } from "ethers"; +import { confirm, getRpcProvider, getWallet, printQuote } from "../../utils"; import { getErc20Contract } from "../../contracts/erc20"; import { latest } from "@cowprotocol/app-data"; import { orderHelperFactoryAbi } from "./abi/OrderHelperFactoryAbi"; @@ -33,12 +33,12 @@ const TOKENS = { oldCollateral: "0x5b071b590a59395fe4025a0ccc1fcc931aac1830", // aETHWeth debt: "0xc4bf5cbdabe595361438f8c6a187bdc330539c60", // GHO newUnderlying: "0x94a9d9ac8a22534e3faca9f4e7f2e2cf85d5e4c8", // USDC - newCollateral: "0x40d16fc0236f5686f0a7030063ca493c4dd83358", // aUSDC + newCollateral: "0x16dA4541aD1807f4443d92D26044C1147406EB80", // aUSDC } as const; const AAVE_POOL_ADDRESS = "0x6Ae43d3271ff6888e7Fc43Fd7321a503ff738951"; // See https://search.onaave.com/?q=sepolia const COW_AAVE_BORROWER = "0x7d9C4DeE56933151Bc5C909cfe09DEf0d315CB4A"; // See https://github.com/cowprotocol/flash-loan-router/blob/main/networks.json -const COW_AAVE_HELPER_FACTORY = "0xc55098a66d2225c37bf33c1f7b8b9b0abc8fd32f"; // https://sepolia.etherscan.io/address/0xc55098a66d2225c37bf33c1f7b8b9b0abc8fd32f#code +const COW_AAVE_HELPER_FACTORY = "0xe7De9F737135AEE2d154D1b6b23414C1bf115109"; // https://sepolia.etherscan.io/address/0xe7De9F737135AEE2d154D1b6b23414C1bf115109#code const DEFAULT_GAS_LIMIT = "1000000"; // FIXME: This should not be necessary, it should estimate correctly! const CHAIN_ID = SupportedChainId.SEPOLIA; @@ -47,6 +47,8 @@ export async function run() { const wallet = await getWallet(CHAIN_ID); const trader = wallet.address; + console.log(`Trader ${trader}`); + // Initialize the SDK with the wallet const sdk = new TradingSdk({ chainId: CHAIN_ID, @@ -178,7 +180,7 @@ async function getAssetsInfo(params: { ); console.log( - `Old underlying balance: ${oldUnderlyingBalanceFormatted} ${oldUnderlyingSymbol}` + `Old underlying balance as collateral: ${oldUnderlyingBalanceFormatted} ${oldUnderlyingSymbol}` ); const newUnderlying = await getErc20Contract(TOKENS.newUnderlying, wallet); @@ -318,9 +320,14 @@ async function getOrderDetails(props: { wallet, } = props; + // validFor is based on block.timestamp + const rpc = await getRpcProvider(CHAIN_ID); + const block = await rpc.getBlock("latest"); + const validFor = block.timestamp + 60 * 5; // 5 minutes from now + // Get the minimum receive const minReceivedAmount = "1"; // 1 Wei. Technically I would need to ask for a quote. Its a bit tricky, because we would need to ask for a quote with the helper contract as owner. Could be possible with a dirty trick (find an user with balance for the oldUnderlying and ask for a quote to dump it for the newUnderlying). For simplicity, I start hardcoding to 1 web. - const validFor = 60 * 30; // 30 minutes from now + // Ger factory contract instance const orderHelperFactory = new ethers.Contract( @@ -342,6 +349,7 @@ async function getOrderDetails(props: { // Get the hook to swap the collateral const collateralSwapHook = getCollateralSwapPostHook({ helperContract }); + const orderValidFor = validFor - block.timestamp; const parameters: TradeParameters = { kind: OrderKind.SELL, @@ -354,7 +362,7 @@ async function getOrderDetails(props: { partiallyFillable: false, owner: helperContract as `0x${string}`, receiver: helperContract, - validFor, + validFor: orderValidFor, }; console.log("Trade parameters", parameters);