From 7f46ecb90f2790895c732c2c545585ad649bca63 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Thu, 20 Mar 2025 16:39:59 +0530 Subject: [PATCH 01/17] feat: socket cctp, across scripts --- src/const/base.ts | 1 + src/contracts/erc20/index.ts | 1 + src/contracts/socket/index.ts | 166 +++++ src/contracts/socket/types.ts | 618 ++++++++++++++++++ src/contracts/socket/utils.ts | 106 +++ src/index.ts | 6 +- .../swapAndBridgeBungeeAcrossArbitrumBase.ts | 207 ++++++ .../swapAndBridgeBungeeCCTPArbitrumBase.ts | 207 ++++++ 8 files changed, 1311 insertions(+), 1 deletion(-) create mode 100644 src/contracts/socket/index.ts create mode 100644 src/contracts/socket/types.ts create mode 100644 src/contracts/socket/utils.ts create mode 100644 src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts create mode 100644 src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts diff --git a/src/const/base.ts b/src/const/base.ts index 7daeeb2..4902000 100644 --- a/src/const/base.ts +++ b/src/const/base.ts @@ -1 +1,2 @@ export const WETH_ADDRESS = "0x4200000000000000000000000000000000000006"; +export const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; diff --git a/src/contracts/erc20/index.ts b/src/contracts/erc20/index.ts index 34a65d6..a729e77 100644 --- a/src/contracts/erc20/index.ts +++ b/src/contracts/erc20/index.ts @@ -5,6 +5,7 @@ const ERC20_BALANCE_OF_ABI = [ "function approve(address spender, uint256 amount) external returns (bool)", "function decimals() external view returns (uint8)", "function symbol() external view returns (string)", + "function allowance(address owner, address spender) external view returns (uint256)", ] as const; export function getErc20Contract( diff --git a/src/contracts/socket/index.ts b/src/contracts/socket/index.ts new file mode 100644 index 0000000..1e146ca --- /dev/null +++ b/src/contracts/socket/index.ts @@ -0,0 +1,166 @@ +import { SupportedChainId } from '@cowprotocol/cow-sdk'; +import { + Contract as WeirollContract, + Planner as WeirollPlanner, +} from '@weiroll/weiroll.js'; +import { ethers } from 'ethers'; +import { BaseTransaction } from '../../types'; +import { getWallet } from '../../utils'; +import { getCowShedAccount } from '../cowShed'; +import { getErc20Contract } from '../erc20'; +import { CommandFlags, getWeirollTx } from '../weiroll'; +import { bytesLibAbi, socketGatewayAbi } from './types'; +import { + decodeBungeeTxData, + getBungeeQuote, + getBungeeRouteTransactionData, +} from './utils'; + +export interface BridgeWithBungeeParams { + owner: string; + sourceChain: SupportedChainId; + sourceToken: string; + sourceTokenAmount: bigint; + targetToken: string; + targetChain: number; + recipient: string; + includeBridges: string[]; +} + +export async function bridgeWithBungee( + params: BridgeWithBungeeParams +): Promise { + const { + owner, + sourceChain, + sourceToken, + sourceTokenAmount, + targetChain, + targetToken, + recipient, + includeBridges, + } = params; + + // Get cow-shed account + const cowShedAccount = getCowShedAccount(sourceChain, owner); + + const planner = new WeirollPlanner(); + + // Get bungee quote + const quote = await getBungeeQuote({ + fromChainId: sourceChain.toString(), + fromTokenAddress: sourceToken, + toChainId: targetChain.toString(), + toTokenAddress: targetToken, + fromAmount: sourceTokenAmount.toString(), + userAddress: cowShedAccount, // bridge input token will be in cowshed account + recipient: recipient, + sort: 'output', // optimize for output amount + singleTxOnly: true, // should be only single txn on src chain, no destination chain txn + isContractCall: true, // get quotes that are compatible with contracts + disableSwapping: true, // should not show routes that require swapping + includeBridges, + }); + if (!quote) { + throw new Error('No quote found'); + } + console.log('šŸ”— Socket quote:', quote); + // check if routes are found + if (!quote.result.routes.length) { + throw new Error('No routes found'); + } + // check if only single user tx is present + if (quote.result.routes[0].userTxs.length > 1) { + throw new Error('Multiple user txs found'); + } + // check if the user tx is fund-movr + if (quote.result.routes[0].userTxs[0].userTxType !== 'fund-movr') { + throw new Error('User tx is not fund-movr'); + } + + // use the first route to prepare the bridge tx + const route = quote.result.routes[0]; + const txData = await getBungeeRouteTransactionData(route); + const { routeId, encodedFunctionData } = decodeBungeeTxData( + txData.result.txData + ); + + // Create bridged token contract + const bridgedTokenContract = WeirollContract.createContract( + getErc20Contract(sourceToken), + CommandFlags.CALL + ); + + // Get balance of CoW shed proxy + console.log( + `[socket] Get cow-shed balance for ERC20.balanceOf(${cowShedAccount}) for ${bridgedTokenContract.address}` + ); + + // Get bridged amount (balance of the intermediate token at swap time) + const sourceAmountIncludingSurplusBytes = planner.add( + bridgedTokenContract.balanceOf(cowShedAccount).rawValue() + ); + + // Check & set allowance for SocketGateway to transfer bridged tokens + // check if allowance is sufficient + const { + approvalData: { + approvalTokenAddress, + allowanceTarget, + minimumApprovalAmount, + }, + } = txData.result; + const intermediateTokenContract = getErc20Contract( + approvalTokenAddress, + await getWallet(sourceChain) + ); + const allowance = await intermediateTokenContract.allowance( + cowShedAccount, + allowanceTarget + ); + console.log('current cowshed allowance', allowance); + if (allowance < minimumApprovalAmount) { + // set allowance + const approvalTokenContract = WeirollContract.createContract( + getErc20Contract(approvalTokenAddress), + CommandFlags.CALL + ); + console.log( + `[socket] approvalTokenContract.approve(${allowanceTarget}, ${sourceAmountIncludingSurplusBytes}) for ${approvalTokenContract}` + ); + const allowanceToSet = ethers.utils.parseUnits( + '1000', + await intermediateTokenContract.decimals() + ); + planner.add(approvalTokenContract.approve(allowanceTarget, allowanceToSet)); + } + + const bytesLibContractAddress = '0x8f6BA63528De7266d8cDfDdec7ACFA8446c62aB4'; + const bytesLibContract = WeirollContract.createContract( + new ethers.Contract(bytesLibContractAddress, bytesLibAbi), + CommandFlags.CALL + ); + const encodedFunctionDataWithNewAmount = planner.add( + bytesLibContract.replaceBytes( + encodedFunctionData, + 4, // first 4 bytes are the function selector + 32, // first 32 bytes of the params are the amount + sourceAmountIncludingSurplusBytes + ) + ); + + const socketGatewayContract = WeirollContract.createContract( + new ethers.Contract(txData.result.txTarget, socketGatewayAbi), + CommandFlags.CALL + ); + // Call executeRoute on SocketGateway + planner.add( + socketGatewayContract.executeRoute( + routeId, + encodedFunctionDataWithNewAmount + ) + ); + + // Return the transaction + return getWeirollTx({ planner }); +} diff --git a/src/contracts/socket/types.ts b/src/contracts/socket/types.ts new file mode 100644 index 0000000..3e084c4 --- /dev/null +++ b/src/contracts/socket/types.ts @@ -0,0 +1,618 @@ +export type Route = { + routeId: string; + isOnlySwapRoute: boolean; + fromAmount: string; + toAmount: string; + usedBridgeNames: Array; + minimumGasBalances: { + [chainId: string]: string; + }; + chainGasBalances: { + [chainId: string]: { + minGasBalance: string; + hasGasBalance: boolean; + }; + }; + totalUserTx: number; + sender: string; + recipient: string; + totalGasFeesInUsd: number; + receivedValueInUsd: number; + inputValueInUsd: number; + outputValueInUsd: number; + userTxs: Array<{ + userTxType: string; + txType: string; + chainId: number; + toAmount: string; + toAsset: { + chainId: number; + address: string; + symbol: string; + name: string; + decimals: number; + icon: string; + logoURI: string; + chainAgnosticId: string; + }; + stepCount: number; + routePath: string; + sender: string; + approvalData: { + minimumApprovalAmount: string; + approvalTokenAddress: string; + allowanceTarget: string; + owner: string; + }; + steps: Array<{ + type: string; + protocol: { + name: string; + displayName: string; + icon: string; + securityScore: number; + robustnessScore: number; + }; + fromChainId: number; + fromAsset: { + chainId: number; + address: string; + symbol: string; + name: string; + decimals: number; + icon: string; + logoURI: string; + chainAgnosticId: string; + }; + fromAmount: string; + minAmountOut: string; + toChainId: number; + toAsset: { + chainId: number; + address: string; + symbol: string; + name: string; + decimals: number; + icon: string; + logoURI: string; + chainAgnosticId: string; + }; + toAmount: string; + bridgeSlippage: number; + protocolFees: { + asset: { + chainId: number; + address: string; + symbol: string; + name: string; + decimals: number; + icon: string; + logoURI: string; + chainAgnosticId: string; + }; + amount: string; + feesInUsd: number; + }; + gasFees: { + gasAmount: string; + gasLimit: number; + asset: { + chainId: number; + address: string; + symbol: string; + name: string; + decimals: number; + icon: string; + logoURI: string; + chainAgnosticId: any; + }; + feesInUsd: number; + }; + serviceTime: number; + maxServiceTime: number; + extraData: { + rewards: Array; + }; + }>; + gasFees: { + gasAmount: string; + feesInUsd: number; + asset: { + chainId: number; + address: string; + symbol: string; + name: string; + decimals: number; + icon: string; + logoURI: string; + chainAgnosticId: any; + }; + gasLimit: number; + }; + serviceTime: number; + recipient: string; + maxServiceTime: number; + bridgeSlippage: number; + userTxIndex: number; + }>; + serviceTime: number; + maxServiceTime: number; + integratorFee: { + amount: string; + asset: { + chainId: number; + address: string; + symbol: string; + name: string; + decimals: number; + icon: string; + logoURI: string; + chainAgnosticId: string; + }; + }; + extraData: { + rewards: Array; + }; +}; + +export type BungeeQuoteResponse = { + success: boolean; + result: { + routes: Array; + socketRoute: any; + destinationCallData: {}; + fromChainId: number; + fromAsset: { + chainId: number; + address: string; + symbol: string; + name: string; + decimals: number; + icon: string; + logoURI: string; + chainAgnosticId: string; + priceInUsd: number; + }; + toChainId: number; + toAsset: { + chainId: number; + address: string; + symbol: string; + name: string; + decimals: number; + icon: string; + logoURI: string; + chainAgnosticId: string; + priceInUsd: number; + }; + bridgeRouteErrors: {}; + }; +}; + +export type BungeeBuildTxResponse = { + success: boolean; + result: { + userTxType: string; + txType: string; + txData: string; + txTarget: string; + chainId: number; + userTxIndex: number; + value: string; + approvalData: { + minimumApprovalAmount: string; + approvalTokenAddress: string; + allowanceTarget: string; + owner: string; + }; + }; + statusCode: number; +}; + +export const bytesLibAbi = [ + { + inputs: [ + { + internalType: 'bytes', + name: '_original', + type: 'bytes', + }, + { + internalType: 'uint256', + name: '_start', + type: 'uint256', + }, + { + internalType: 'uint256', + name: '_length', + type: 'uint256', + }, + { + internalType: 'bytes', + name: '_replacement', + type: 'bytes', + }, + ], + name: 'replaceBytes', + outputs: [ + { + internalType: 'bytes', + name: '', + type: 'bytes', + }, + ], + stateMutability: 'pure', + type: 'function', + }, +]; + +export const socketGatewayAbi = [ + { + inputs: [ + { internalType: 'address', name: '_owner', type: 'address' }, + { internalType: 'address', name: '_disabledRoute', type: 'address' }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { inputs: [], name: 'ArrayLengthMismatch', type: 'error' }, + { inputs: [], name: 'IncorrectBridgeRatios', type: 'error' }, + { inputs: [], name: 'OnlyNominee', type: 'error' }, + { inputs: [], name: 'OnlyOwner', type: 'error' }, + { inputs: [], name: 'ZeroAddressNotAllowed', type: 'error' }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'uint32', + name: 'controllerId', + type: 'uint32', + }, + { + indexed: true, + internalType: 'address', + name: 'controllerAddress', + type: 'address', + }, + ], + name: 'ControllerAdded', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'uint32', + name: 'controllerId', + type: 'uint32', + }, + ], + name: 'ControllerDisabled', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'uint32', + name: 'routeId', + type: 'uint32', + }, + { + indexed: true, + internalType: 'address', + name: 'route', + type: 'address', + }, + ], + name: 'NewRouteAdded', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'claimer', + type: 'address', + }, + ], + name: 'OwnerClaimed', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'nominee', + type: 'address', + }, + ], + name: 'OwnerNominated', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: '_from', + type: 'address', + }, + { indexed: true, internalType: 'address', name: '_to', type: 'address' }, + ], + name: 'OwnershipTransferRequested', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'uint32', + name: 'routeId', + type: 'uint32', + }, + ], + name: 'RouteDisabled', + type: 'event', + }, + { stateMutability: 'payable', type: 'fallback' }, + { + inputs: [], + name: 'BRIDGE_AFTER_SWAP_SELECTOR', + outputs: [{ internalType: 'bytes4', name: '', type: 'bytes4' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'CENT_PERCENT', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'controllerAddress', type: 'address' }, + ], + name: 'addController', + outputs: [{ internalType: 'uint32', name: '', type: 'uint32' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'routeAddress', type: 'address' }, + ], + name: 'addRoute', + outputs: [{ internalType: 'uint32', name: '', type: 'uint32' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'uint32', name: 'routeId', type: 'uint32' }], + name: 'addressAt', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'claimOwner', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'controllerCount', + outputs: [{ internalType: 'uint32', name: '', type: 'uint32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'uint32', name: '', type: 'uint32' }], + name: 'controllers', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'uint32', name: 'controllerId', type: 'uint32' }], + name: 'disableController', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'uint32', name: 'routeId', type: 'uint32' }], + name: 'disableRoute', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'disabledRouteAddress', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + components: [ + { internalType: 'uint32', name: 'controllerId', type: 'uint32' }, + { internalType: 'bytes', name: 'data', type: 'bytes' }, + ], + internalType: 'struct ISocketGateway.SocketControllerRequest', + name: 'socketControllerRequest', + type: 'tuple', + }, + ], + name: 'executeController', + outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { internalType: 'uint32', name: 'controllerId', type: 'uint32' }, + { internalType: 'bytes', name: 'data', type: 'bytes' }, + ], + internalType: 'struct ISocketGateway.SocketControllerRequest[]', + name: 'controllerRequests', + type: 'tuple[]', + }, + ], + name: 'executeControllers', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint32', name: 'routeId', type: 'uint32' }, + { internalType: 'bytes', name: 'routeData', type: 'bytes' }, + ], + name: 'executeRoute', + outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint32[]', name: 'routeIds', type: 'uint32[]' }, + { internalType: 'bytes[]', name: 'dataItems', type: 'bytes[]' }, + ], + name: 'executeRoutes', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [{ internalType: 'uint32', name: 'controllerId', type: 'uint32' }], + name: 'getController', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'uint32', name: 'routeId', type: 'uint32' }], + name: 'getRoute', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'nominee_', type: 'address' }], + name: 'nominateOwner', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'nominee', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'owner', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address payable', name: 'userAddress', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'rescueEther', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'token', type: 'address' }, + { internalType: 'address', name: 'userAddress', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'rescueFunds', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'uint32', name: '', type: 'uint32' }], + name: 'routes', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'routesCount', + outputs: [{ internalType: 'uint32', name: '', type: 'uint32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address[]', name: 'routeAddresses', type: 'address[]' }, + { internalType: 'address[]', name: 'tokenAddresses', type: 'address[]' }, + { internalType: 'bool', name: 'isMax', type: 'bool' }, + ], + name: 'setApprovalForRouters', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { internalType: 'uint32', name: 'swapRouteId', type: 'uint32' }, + { internalType: 'bytes', name: 'swapImplData', type: 'bytes' }, + { + internalType: 'uint32[]', + name: 'bridgeRouteIds', + type: 'uint32[]', + }, + { + internalType: 'bytes[]', + name: 'bridgeImplDataItems', + type: 'bytes[]', + }, + { + internalType: 'uint256[]', + name: 'bridgeRatios', + type: 'uint256[]', + }, + { internalType: 'bytes[]', name: 'eventDataItems', type: 'bytes[]' }, + ], + internalType: 'struct ISocketRequest.SwapMultiBridgeRequest', + name: 'swapMultiBridgeRequest', + type: 'tuple', + }, + ], + name: 'swapAndMultiBridge', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, + { stateMutability: 'payable', type: 'receive' }, +]; diff --git a/src/contracts/socket/utils.ts b/src/contracts/socket/utils.ts new file mode 100644 index 0000000..224cf42 --- /dev/null +++ b/src/contracts/socket/utils.ts @@ -0,0 +1,106 @@ +import { SupportedChainId } from '@cowprotocol/cow-sdk'; +import axios, { isAxiosError } from 'axios'; +import { BungeeBuildTxResponse, BungeeQuoteResponse, Route } from './types'; + +const API_KEY = '72a5b4b0-e727-48be-8aa1-5da9d62fe635'; // SOCKET PUBLIC API KEY from docs + +export const socketGatewayMapping: Record< + SupportedChainId, + string | undefined +> = { + /** + * #CHAIN-INTEGRATION + * This needs to be changed if you want to support a new chain + */ + [SupportedChainId.MAINNET]: '0x3a23F943181408EAC424116Af7b7790c94Cb97a5', + [SupportedChainId.GNOSIS_CHAIN]: '0x3a23F943181408EAC424116Af7b7790c94Cb97a5', + [SupportedChainId.ARBITRUM_ONE]: '0x3a23F943181408EAC424116Af7b7790c94Cb97a5', + [SupportedChainId.BASE]: '0x3a23F943181408EAC424116Af7b7790c94Cb97a5', + [SupportedChainId.SEPOLIA]: undefined, +}; + +/** + * Makes a GET request to Bungee APIs for quote + * https://docs.bungee.exchange/bungee-manual/socket-api-reference/quote-controller-get-quote/ + */ +export async function getBungeeQuote(params: { + fromChainId: string; + toChainId: string; + fromTokenAddress: string; + toTokenAddress: string; + fromAmount: string; + userAddress: string; + recipient: string; + singleTxOnly: boolean; + sort: string; + isContractCall: boolean; + disableSwapping: boolean; + includeBridges: string[]; +}) { + try { + const response = await axios.get( + `https://api.socket.tech/v2/quote`, + { + headers: { + 'API-KEY': API_KEY, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + params, // query params + } + ); + + const json = response.data; + return json; + } catch (error) { + if (isAxiosError(error)) { + console.error('šŸ”“ Error getting bungee quote:', error.response?.data); + } else { + console.error('šŸ”“ Error getting bungee quote:', error); + } + throw error; + } +} + +/** + * Makes a POST request to Bungee APIs for swap/bridge transaction data + * https://docs.bungee.exchange/bungee-manual/socket-api-reference/app-controller-get-single-tx + */ +export async function getBungeeRouteTransactionData(route: Route) { + try { + const response = await axios.post( + 'https://api.socket.tech/v2/build-tx', + { route }, + { + headers: { + 'API-KEY': API_KEY, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + } + ); + + const json = response.data; + return json; + } catch (error) { + if (isAxiosError(error)) { + console.error( + 'šŸ”“ Error getting bungee route transaction data:', + error.response?.data + ); + } else { + console.error('šŸ”“ Error getting bungee route transaction data:', error); + } + throw error; + } +} + +export const decodeBungeeTxData = (txData: string) => { + // remove first two characters = 0x + const txDataWithout0x = txData.slice(2); + // first four bytes are the routeId + const routeId = `0x${txDataWithout0x.slice(0, 8)}`; + // rest is the encoded function data + const encodedFunctionData = `0x${txDataWithout0x.slice(8)}`; + return { routeId, encodedFunctionData }; +}; diff --git a/src/index.ts b/src/index.ts index e03eec2..2294310 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,6 +27,8 @@ import { run as swapAndBridgeSwapsIo } from "./scripts/bridging/swapAndBridgeSwa import { run as approveTokenArbitrum } from "./scripts/arbitrum/approveTokenArbitrum"; import { run as swapAndBridgeAccrossArbitrum } from "./scripts/bridging/swapAndBridgeAccrossArbitrum"; import { run as swapAndBridgeAccrossMainnet } from "./scripts/bridging/swapAndBridgeAccrossMainnet"; +import { run as swapAndBridgeBungeeCCTPArbitrumBase } from "./scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase"; +import { run as swapAndBridgeBungeeAcrossArbitrumBase } from "./scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase"; dotenv.config(); // Just to dev things easily using watch-mode :) @@ -63,7 +65,9 @@ const JOBS: (() => Promise)[] = [ // approveTokenArbitrum, // swapAndBridgeAccrossArbitrum, - swapAndBridgeAccrossMainnet, + // swapAndBridgeAccrossMainnet, + // swapAndBridgeBungeeCCTPArbitrumBase, + swapAndBridgeBungeeAcrossArbitrumBase, ]; async function main() { diff --git a/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts b/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts new file mode 100644 index 0000000..3ea714a --- /dev/null +++ b/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts @@ -0,0 +1,207 @@ +import { APP_CODE, arbitrum, base } from '../../const'; + +import { + COW_PROTOCOL_VAULT_RELAYER_ADDRESS, + OrderKind, + SupportedChainId, + TradeParameters, + TradingSdk, +} from '@cowprotocol/cow-sdk'; +import { ethers } from 'ethers'; + +import { MetadataApi } from '@cowprotocol/app-data'; +import { createCowShedTx } from '../../contracts/cowShed'; +import { confirm, getWallet, jsonReplacer } from '../../utils'; + +import { getErc20Contract } from '../../contracts/erc20'; +import { bridgeWithBungee } from '../../contracts/socket'; + +export async function run() { + /** + * Swap from USDT to USDC on Arbitrum, + * then bridge USDC to Base using Socket CCTP + */ + const sourceChain = SupportedChainId.ARBITRUM_ONE; + const targetChain = SupportedChainId.BASE; + const sellToken = arbitrum.USDT_ADDRESS; + const sellTokenDecimals = 6; + const sellAmount = ethers.utils.parseUnits('1', sellTokenDecimals).toString(); + const buyToken = base.USDC_ADDRESS; + const buyTokenDecimals = 6; + + const wallet = await getWallet(sourceChain); + const walletAddress = await wallet.getAddress(); + console.log('šŸ”‘ Wallet address:', walletAddress); + + // Initialize the SDK with the wallet + const sdk = new TradingSdk({ + chainId: sourceChain, + signer: wallet, // Use a signer + appCode: APP_CODE, + }); + + // Get the intermediary token + const intermediaryToken = arbitrum.USDC_ADDRESS; + + // Get intermediate token decimals + const intermediateTokenContract = getErc20Contract(intermediaryToken, wallet); + const intermediateTokenDecimals = await intermediateTokenContract.decimals(); + const intermediateTokenSymbol = await intermediateTokenContract.symbol(); + + // Estimate how many intermediate tokens we can bridge + let quote = await sdk.getQuote({ + kind: OrderKind.SELL, + sellToken, + sellTokenDecimals, + buyToken: intermediaryToken, + buyTokenDecimals, + receiver: wallet.address, + amount: sellAmount, + }); + const intermediateTokenAmount = + quote.quoteResults.amountsAndCosts.afterSlippage.buyAmount; + + console.log('quote', JSON.stringify(quote, jsonReplacer, 2)); + + // Get raw transaction to bridge all available DAI from cow-shed using xDAI Bridge + const bridgeWithBungeeTx = await bridgeWithBungee({ + owner: wallet.address, + sourceChain: sourceChain, + sourceToken: intermediaryToken, + sourceTokenAmount: intermediateTokenAmount, + targetChain: targetChain, + targetToken: buyToken, + recipient: wallet.address, + includeBridges: ['across'], + }); + + console.log( + '\nšŸ’° Bridge tx:', + JSON.stringify(bridgeWithBungeeTx, jsonReplacer, 2) + ); + + // Sign and encode the transaction + const { + cowShedAccount, + preAuthenticatedTx: authenticatedBridgeTx, + gasLimit, + } = await createCowShedTx({ + tx: bridgeWithBungeeTx, + chainId: sourceChain, + wallet, + }); + + // Define trade parameters. Sell sell token for intermediary token, to be received by cow-shed + const parameters: TradeParameters = { + kind: OrderKind.SELL, // Sell + amount: sellAmount, + sellToken, + sellTokenDecimals: sellTokenDecimals, + buyToken: intermediaryToken, + buyTokenDecimals: intermediateTokenDecimals, + partiallyFillable: false, // Fill or Kill + receiver: cowShedAccount, + }; + + const metadataApi = new MetadataApi(); + const appData = await metadataApi.generateAppDataDoc({ + appCode: APP_CODE, + metadata: { + hooks: { + post: [ + { + callData: authenticatedBridgeTx.callData, + gasLimit: gasLimit.toString(), + target: authenticatedBridgeTx.to, + dappId: 'bridge-socket', + }, + ], + }, + }, + }); + + console.log( + 'šŸ•£ Getting quote...', + JSON.stringify(parameters, jsonReplacer, 2) + ); + + quote = await sdk.getQuote(parameters, { appData }); + const { postSwapOrderFromQuote, quoteResults } = quote; + + console.log('quoteResults', { + amountsAndCosts: quoteResults.amountsAndCosts, + }); + + const minIntermediateTokenAmount = + quoteResults.amountsAndCosts.afterSlippage.buyAmount; + const minIntermediateTokenAmountFormatted = ethers.utils.formatUnits( + minIntermediateTokenAmount, + intermediateTokenDecimals + ); + const sellAmountFormatted = ethers.utils.formatUnits( + sellAmount, + sellTokenDecimals + ); + + console.log( + `You will sell ${sellAmountFormatted} USDC and receive at least ${minIntermediateTokenAmountFormatted} ${intermediateTokenSymbol} (intermediate token). Then, it will be bridged to Base for USDC via CCTP via Socket.` + ); + + const confirmed = await confirm( + `You will bridge at least ${minIntermediateTokenAmountFormatted} ${intermediateTokenSymbol}. ok?` + ); + if (!confirmed) { + console.log('🚫 Aborted'); + return; + } + + // check owner allowance to VaultRelayer + const vaultRelayerContract = COW_PROTOCOL_VAULT_RELAYER_ADDRESS[sourceChain]; + const sellTokenContract = getErc20Contract(sellToken, wallet); + const sellTokenAllowance = await sellTokenContract.allowance( + walletAddress, + vaultRelayerContract + ); + const sellTokenAllowanceFormatted = ethers.utils.formatUnits( + sellTokenAllowance, + sellTokenDecimals + ); + console.log('sellTokenAllowanceFormatted', sellTokenAllowanceFormatted); + // If allowance is insufficient, grant allowance + if (sellTokenAllowanceFormatted < sellAmount) { + console.log('🚫 Insufficient allowance'); + const confirmed_allowance = await confirm( + `Grant allowance to VaultRelayer?` + ); + if (!confirmed_allowance) { + console.log('🚫 Aborted'); + return; + } + const tx = await sellTokenContract.approve( + vaultRelayerContract, + // sellAmount + ethers.utils.parseUnits('1000', sellTokenDecimals) + ); + console.log('Allowance granted tx', tx); + await tx.wait(2); + } + + // Post the order + const orderId = await postSwapOrderFromQuote(); + + // Print the order creation + console.log( + `ā„¹ļø Order created, id: https://explorer.cow.fi/orders/${orderId}?tab=overview` + ); + + // Wait for the bridge start + console.log('šŸ•£ Waiting for the bridge to start...'); + console.log('šŸ”— Socket link: '); + // TODO: Implement + + // Wait for the bridging to be completed + console.log('šŸ•£ Waiting for the bridging to be completed...'); + // TODO: Implement + + console.log(`šŸŽ‰ The USDC is now waiting for you in Base`); +} diff --git a/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts b/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts new file mode 100644 index 0000000..fd8f13a --- /dev/null +++ b/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts @@ -0,0 +1,207 @@ +import { APP_CODE, arbitrum, base } from '../../const'; + +import { + COW_PROTOCOL_VAULT_RELAYER_ADDRESS, + OrderKind, + SupportedChainId, + TradeParameters, + TradingSdk, +} from '@cowprotocol/cow-sdk'; +import { ethers } from 'ethers'; + +import { MetadataApi } from '@cowprotocol/app-data'; +import { createCowShedTx } from '../../contracts/cowShed'; +import { confirm, getWallet, jsonReplacer } from '../../utils'; + +import { getErc20Contract } from '../../contracts/erc20'; +import { bridgeWithBungee } from '../../contracts/socket'; + +export async function run() { + /** + * Swap from USDT to USDC on Arbitrum, + * then bridge USDC to Base using Socket CCTP + */ + const sourceChain = SupportedChainId.ARBITRUM_ONE; + const targetChain = SupportedChainId.BASE; + const sellToken = arbitrum.USDT_ADDRESS; + const sellTokenDecimals = 6; + const sellAmount = ethers.utils.parseUnits('1', sellTokenDecimals).toString(); + const buyToken = base.USDC_ADDRESS; + const buyTokenDecimals = 6; + + const wallet = await getWallet(sourceChain); + const walletAddress = await wallet.getAddress(); + console.log('šŸ”‘ Wallet address:', walletAddress); + + // Initialize the SDK with the wallet + const sdk = new TradingSdk({ + chainId: sourceChain, + signer: wallet, // Use a signer + appCode: APP_CODE, + }); + + // Get the intermediary token + const intermediaryToken = arbitrum.USDC_ADDRESS; + + // Get intermediate token decimals + const intermediateTokenContract = getErc20Contract(intermediaryToken, wallet); + const intermediateTokenDecimals = await intermediateTokenContract.decimals(); + const intermediateTokenSymbol = await intermediateTokenContract.symbol(); + + // Estimate how many intermediate tokens we can bridge + let quote = await sdk.getQuote({ + kind: OrderKind.SELL, + sellToken, + sellTokenDecimals, + buyToken: intermediaryToken, + buyTokenDecimals, + receiver: wallet.address, + amount: sellAmount, + }); + const intermediateTokenAmount = + quote.quoteResults.amountsAndCosts.afterSlippage.buyAmount; + + console.log('quote', JSON.stringify(quote, jsonReplacer, 2)); + + // Get raw transaction to bridge all available DAI from cow-shed using xDAI Bridge + const bridgeWithBungeeTx = await bridgeWithBungee({ + owner: wallet.address, + sourceChain: sourceChain, + sourceToken: intermediaryToken, + sourceTokenAmount: intermediateTokenAmount, + targetChain: targetChain, + targetToken: buyToken, + recipient: wallet.address, + includeBridges: ['cctp'], + }); + + console.log( + '\nšŸ’° Bridge tx:', + JSON.stringify(bridgeWithBungeeTx, jsonReplacer, 2) + ); + + // Sign and encode the transaction + const { + cowShedAccount, + preAuthenticatedTx: authenticatedBridgeTx, + gasLimit, + } = await createCowShedTx({ + tx: bridgeWithBungeeTx, + chainId: sourceChain, + wallet, + }); + + // Define trade parameters. Sell sell token for intermediary token, to be received by cow-shed + const parameters: TradeParameters = { + kind: OrderKind.SELL, // Sell + amount: sellAmount, + sellToken, + sellTokenDecimals: sellTokenDecimals, + buyToken: intermediaryToken, + buyTokenDecimals: intermediateTokenDecimals, + partiallyFillable: false, // Fill or Kill + receiver: cowShedAccount, + }; + + const metadataApi = new MetadataApi(); + const appData = await metadataApi.generateAppDataDoc({ + appCode: APP_CODE, + metadata: { + hooks: { + post: [ + { + callData: authenticatedBridgeTx.callData, + gasLimit: gasLimit.toString(), + target: authenticatedBridgeTx.to, + dappId: 'bridge-socket', + }, + ], + }, + }, + }); + + console.log( + 'šŸ•£ Getting quote...', + JSON.stringify(parameters, jsonReplacer, 2) + ); + + quote = await sdk.getQuote(parameters, { appData }); + const { postSwapOrderFromQuote, quoteResults } = quote; + + console.log('quoteResults', { + amountsAndCosts: quoteResults.amountsAndCosts, + }); + + const minIntermediateTokenAmount = + quoteResults.amountsAndCosts.afterSlippage.buyAmount; + const minIntermediateTokenAmountFormatted = ethers.utils.formatUnits( + minIntermediateTokenAmount, + intermediateTokenDecimals + ); + const sellAmountFormatted = ethers.utils.formatUnits( + sellAmount, + sellTokenDecimals + ); + + console.log( + `You will sell ${sellAmountFormatted} USDC and receive at least ${minIntermediateTokenAmountFormatted} ${intermediateTokenSymbol} (intermediate token). Then, it will be bridged to Base for USDC via CCTP via Socket.` + ); + + const confirmed = await confirm( + `You will bridge at least ${minIntermediateTokenAmountFormatted} ${intermediateTokenSymbol}. ok?` + ); + if (!confirmed) { + console.log('🚫 Aborted'); + return; + } + + // check owner allowance to VaultRelayer + const vaultRelayerContract = COW_PROTOCOL_VAULT_RELAYER_ADDRESS[sourceChain]; + const sellTokenContract = getErc20Contract(sellToken, wallet); + const sellTokenAllowance = await sellTokenContract.allowance( + walletAddress, + vaultRelayerContract + ); + const sellTokenAllowanceFormatted = ethers.utils.formatUnits( + sellTokenAllowance, + sellTokenDecimals + ); + console.log('sellTokenAllowanceFormatted', sellTokenAllowanceFormatted); + // If allowance is insufficient, grant allowance + if (sellTokenAllowanceFormatted < sellAmount) { + console.log('🚫 Insufficient allowance'); + const confirmed_allowance = await confirm( + `Grant allowance to VaultRelayer?` + ); + if (!confirmed_allowance) { + console.log('🚫 Aborted'); + return; + } + const tx = await sellTokenContract.approve( + vaultRelayerContract, + // sellAmount + ethers.utils.parseUnits("1000", sellTokenDecimals) + ); + console.log('Allowance granted tx', tx); + await tx.wait(2); + } + + // Post the order + const orderId = await postSwapOrderFromQuote(); + + // Print the order creation + console.log( + `ā„¹ļø Order created, id: https://explorer.cow.fi/orders/${orderId}?tab=overview` + ); + + // Wait for the bridge start + console.log('šŸ•£ Waiting for the bridge to start...'); + console.log('šŸ”— Socket link: '); + // TODO: Implement + + // Wait for the bridging to be completed + console.log('šŸ•£ Waiting for the bridging to be completed...'); + // TODO: Implement + + console.log(`šŸŽ‰ The USDC is now waiting for you in Base`); +} From 51acacff08b2cfddcd5ccf73c3281c55df5739e1 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Thu, 20 Mar 2025 17:06:52 +0530 Subject: [PATCH 02/17] chore: logs --- .../bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts b/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts index 3ea714a..8cf78b5 100644 --- a/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts +++ b/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts @@ -194,14 +194,5 @@ export async function run() { `ā„¹ļø Order created, id: https://explorer.cow.fi/orders/${orderId}?tab=overview` ); - // Wait for the bridge start - console.log('šŸ•£ Waiting for the bridge to start...'); - console.log('šŸ”— Socket link: '); - // TODO: Implement - - // Wait for the bridging to be completed - console.log('šŸ•£ Waiting for the bridging to be completed...'); - // TODO: Implement - console.log(`šŸŽ‰ The USDC is now waiting for you in Base`); } From 9a1d85d548766468d49d26c8a29f7acc54b816e4 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Thu, 20 Mar 2025 17:50:15 +0530 Subject: [PATCH 03/17] chore: add comments --- .../swapAndBridgeBungeeCCTPArbitrumBase.ts | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts b/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts index fd8f13a..d6da44a 100644 --- a/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts +++ b/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts @@ -16,6 +16,24 @@ import { confirm, getWallet, jsonReplacer } from '../../utils'; import { getErc20Contract } from '../../contracts/erc20'; import { bridgeWithBungee } from '../../contracts/socket'; +/** + * 1. Get quote from Cowswap for USDT Arb to USDC Arb + * 2. Get quote from Bungee for the quoted USDC Arb to USDC Base via CCTP/Across + * 3. Get transaction calldata for SocketGateway contract to execute the bridge + * 4. Prepare weiroll batch txn post-swap hook with the following steps: + * 1. Fetch cowshed contract balance of USDC Arb + * 2. Approve USDC Arb allowance from Cowshed to SocketGateway contract iff allowance is insufficient + * 3. Modify the SocketGateway execution calldata to replace the bridge input amount with cowshed balance via + * - uses a BytesLib contract that can modify a bytes value + * - this is because SocketGateway is a proxy contract and the impl calldata is supposed to be a bytes param of the bridge function call + * - weiroll calls BytesLib contract to modify the bytes param + * 4. Execute bridge on SocketGateway + * 5. Create authenticated cowshed txn + * 6. Generate app data for hook + * 7. Get final quote from Cowswap for the modified swap + * 8. Approve VaultRelayer contract if allowance is insufficient + * 9. Post order on Cowswap + */ export async function run() { /** * Swap from USDT to USDC on Arbitrum, @@ -180,7 +198,7 @@ export async function run() { const tx = await sellTokenContract.approve( vaultRelayerContract, // sellAmount - ethers.utils.parseUnits("1000", sellTokenDecimals) + ethers.utils.parseUnits('1000', sellTokenDecimals) ); console.log('Allowance granted tx', tx); await tx.wait(2); From e6acf55798dad2352136d12eff3f511edadc13b5 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Fri, 28 Mar 2025 18:27:48 +0530 Subject: [PATCH 04/17] chore: use token symbols in log --- .../swapAndBridgeBungeeAcrossArbitrumBase.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts b/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts index 8cf78b5..b0695f3 100644 --- a/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts +++ b/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts @@ -21,18 +21,23 @@ export async function run() { * Swap from USDT to USDC on Arbitrum, * then bridge USDC to Base using Socket CCTP */ + const sourceChain = SupportedChainId.ARBITRUM_ONE; const targetChain = SupportedChainId.BASE; - const sellToken = arbitrum.USDT_ADDRESS; - const sellTokenDecimals = 6; - const sellAmount = ethers.utils.parseUnits('1', sellTokenDecimals).toString(); - const buyToken = base.USDC_ADDRESS; - const buyTokenDecimals = 6; const wallet = await getWallet(sourceChain); const walletAddress = await wallet.getAddress(); console.log('šŸ”‘ Wallet address:', walletAddress); + const sellToken = arbitrum.USDT_ADDRESS; + const sellTokenDecimals = await getErc20Contract(sellToken, wallet).decimals(); + const sellTokenSymbol = await getErc20Contract(sellToken, wallet).symbol(); + + const sellAmount = ethers.utils.parseUnits('1', sellTokenDecimals).toString(); + const buyToken = base.USDC_ADDRESS; + const buyTokenDecimals = await getErc20Contract(buyToken, wallet).decimals(); + const buyTokenSymbol = await getErc20Contract(buyToken, wallet).symbol(); + // Initialize the SDK with the wallet const sdk = new TradingSdk({ chainId: sourceChain, @@ -144,7 +149,7 @@ export async function run() { ); console.log( - `You will sell ${sellAmountFormatted} USDC and receive at least ${minIntermediateTokenAmountFormatted} ${intermediateTokenSymbol} (intermediate token). Then, it will be bridged to Base for USDC via CCTP via Socket.` + `You will sell ${sellAmountFormatted} ${sellTokenSymbol} and receive at least ${minIntermediateTokenAmountFormatted} ${intermediateTokenSymbol} (intermediate token). Then, it will be bridged to Base for ${buyTokenSymbol} via Across via Socket.` ); const confirmed = await confirm( From b16b3ff1386c2793c49b283215b273afba3fdd92 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Fri, 28 Mar 2025 19:21:02 +0530 Subject: [PATCH 05/17] feat: add verifier validation --- src/contracts/socket/index.ts | 19 +++- src/contracts/socket/types.ts | 200 ++++++++++++++++++++++++++++++++++ src/contracts/socket/utils.ts | 74 ++++++++++++- 3 files changed, 291 insertions(+), 2 deletions(-) diff --git a/src/contracts/socket/index.ts b/src/contracts/socket/index.ts index 1e146ca..6073e35 100644 --- a/src/contracts/socket/index.ts +++ b/src/contracts/socket/index.ts @@ -9,11 +9,13 @@ import { getWallet } from '../../utils'; import { getCowShedAccount } from '../cowShed'; import { getErc20Contract } from '../erc20'; import { CommandFlags, getWeirollTx } from '../weiroll'; -import { bytesLibAbi, socketGatewayAbi } from './types'; +import { bytesLibAbi, socketGatewayAbi, SocketRequest } from './types'; import { decodeBungeeTxData, getBungeeQuote, getBungeeRouteTransactionData, + socketBridgeFunctionSignatures, + verifyBungeeTxData, } from './utils'; export interface BridgeWithBungeeParams { @@ -85,6 +87,21 @@ export async function bridgeWithBungee( txData.result.txData ); + // validate bungee tx data returned from socket API using SocketVerifier contract + const expectedSocketRequest: SocketRequest = { + amount: route.fromAmount, + recipient: route.recipient, + toChainId: targetChain.toString(), + token: targetToken, + signature: socketBridgeFunctionSignatures[includeBridges[0]], + }; + await verifyBungeeTxData( + sourceChain, + txData.result.txData, + routeId, + expectedSocketRequest + ); + // Create bridged token contract const bridgedTokenContract = WeirollContract.createContract( getErc20Contract(sourceToken), diff --git a/src/contracts/socket/types.ts b/src/contracts/socket/types.ts index 3e084c4..c1de574 100644 --- a/src/contracts/socket/types.ts +++ b/src/contracts/socket/types.ts @@ -616,3 +616,203 @@ export const socketGatewayAbi = [ }, { stateMutability: 'payable', type: 'receive' }, ]; + +export const socketVerifierAbi = [ + { + type: 'constructor', + inputs: [ + { name: '_owner', type: 'address', internalType: 'address' }, + { + name: '_socketGateway', + type: 'address', + internalType: 'address', + }, + ], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'addVerifier', + inputs: [ + { name: 'routeId', type: 'uint32', internalType: 'uint32' }, + { name: 'verifier', type: 'address', internalType: 'address' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'claimOwner', + inputs: [], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'nominateOwner', + inputs: [{ name: 'nominee_', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'nominee', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'owner', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'parseCallData', + inputs: [{ name: 'callData', type: 'bytes', internalType: 'bytes' }], + outputs: [ + { + name: '', + type: 'tuple', + internalType: 'struct SocketVerifier.UserRequest', + components: [ + { name: 'routeId', type: 'uint32', internalType: 'uint32' }, + { + name: 'socketRequest', + type: 'bytes', + internalType: 'bytes', + }, + ], + }, + ], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'routeIdsToVerifiers', + inputs: [{ name: '', type: 'uint32', internalType: 'uint32' }], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'socketGateway', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'validateRotueId', + inputs: [ + { name: 'callData', type: 'bytes', internalType: 'bytes' }, + { + name: 'expectedRouteId', + type: 'uint32', + internalType: 'uint32', + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'validateSocketRequest', + inputs: [ + { name: 'callData', type: 'bytes', internalType: 'bytes' }, + { + name: 'expectedRequest', + type: 'tuple', + internalType: 'struct SocketVerifier.UserRequestValidation', + components: [ + { name: 'routeId', type: 'uint32', internalType: 'uint32' }, + { + name: 'socketRequest', + type: 'tuple', + internalType: 'struct SocketVerifier.SocketRequest', + components: [ + { + name: 'amount', + type: 'uint256', + internalType: 'uint256', + }, + { + name: 'recipient', + type: 'address', + internalType: 'address', + }, + { + name: 'toChainId', + type: 'uint256', + internalType: 'uint256', + }, + { + name: 'token', + type: 'address', + internalType: 'address', + }, + { + name: 'signature', + type: 'bytes4', + internalType: 'bytes4', + }, + ], + }, + ], + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'event', + name: 'OwnerClaimed', + inputs: [ + { + name: 'claimer', + type: 'address', + indexed: true, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'OwnerNominated', + inputs: [ + { + name: 'nominee', + type: 'address', + indexed: true, + internalType: 'address', + }, + ], + anonymous: false, + }, + { type: 'error', name: 'AmountNotMatched', inputs: [] }, + { type: 'error', name: 'FailedToVerify', inputs: [] }, + { type: 'error', name: 'OnlyNominee', inputs: [] }, + { type: 'error', name: 'OnlyOwner', inputs: [] }, + { type: 'error', name: 'RecipientNotMatched', inputs: [] }, + { type: 'error', name: 'RouteIdNotFound', inputs: [] }, + { type: 'error', name: 'RouteIdNotMatched', inputs: [] }, + { type: 'error', name: 'SignatureNotMatched', inputs: [] }, + { type: 'error', name: 'ToChainIdNotMatched', inputs: [] }, + { type: 'error', name: 'TokenNotMatched', inputs: [] }, +]; + +export type SocketRequest = { + amount: string; + recipient: string; + toChainId: string; + token: string; + signature: string; +}; + +export type UserRequestValidation = { + routeId: string; + socketRequest: SocketRequest; +}; diff --git a/src/contracts/socket/utils.ts b/src/contracts/socket/utils.ts index 224cf42..a0896d9 100644 --- a/src/contracts/socket/utils.ts +++ b/src/contracts/socket/utils.ts @@ -1,9 +1,38 @@ import { SupportedChainId } from '@cowprotocol/cow-sdk'; import axios, { isAxiosError } from 'axios'; -import { BungeeBuildTxResponse, BungeeQuoteResponse, Route } from './types'; +import { ethers } from 'ethers'; +import { getWallet } from '../../utils'; +import { + BungeeBuildTxResponse, + BungeeQuoteResponse, + Route, + SocketRequest, + socketVerifierAbi, + UserRequestValidation, +} from './types'; const API_KEY = '72a5b4b0-e727-48be-8aa1-5da9d62fe635'; // SOCKET PUBLIC API KEY from docs +/** + * bridgeErc20To() function signatures for each bridge + */ +export const socketBridgeFunctionSignatures: Record = { + ['across']: '0x792ebcb9', + ['cctp']: '0xb7dfe9d0', +}; + +// TODO: deploy socket verifier contracts for all chains +export const socketVerifierMapping: Record< + SupportedChainId, + string | undefined +> = { + [SupportedChainId.MAINNET]: undefined, + [SupportedChainId.GNOSIS_CHAIN]: undefined, + [SupportedChainId.ARBITRUM_ONE]: '0x69D9f76e4cbE81044FE16C399387b12e4DBF27B1', + [SupportedChainId.BASE]: undefined, + [SupportedChainId.SEPOLIA]: undefined, +}; + export const socketGatewayMapping: Record< SupportedChainId, string | undefined @@ -104,3 +133,46 @@ export const decodeBungeeTxData = (txData: string) => { const encodedFunctionData = `0x${txDataWithout0x.slice(8)}`; return { routeId, encodedFunctionData }; }; + +export const verifyBungeeTxData = async ( + chainId: SupportedChainId, + txData: string, + routeId: string, + expectedSocketRequest: SocketRequest +) => { + const socketVerifierAddress = socketVerifierMapping[chainId]; + if (!socketVerifierAddress) { + throw new Error(`Socket verifier not found for chainId: ${chainId}`); + } + const wallet = await getWallet(chainId); + + const socketVerifier = new ethers.Contract( + socketVerifierAddress, + socketVerifierAbi, + wallet + ); + + // should not revert + try { + await socketVerifier.validateRotueId(txData, routeId); + } catch (error) { + console.error('šŸ”“ Error validating routeId:', error); + throw error; + } + + const expectedUserRequestValidation: UserRequestValidation = { + routeId, + socketRequest: expectedSocketRequest, + }; + + // should not revert + try { + await socketVerifier.validateSocketRequest( + txData, + expectedUserRequestValidation + ); + } catch (error) { + console.error('šŸ”“ Error validating socket request:', error); + throw error; + } +}; From 4aa6d6678da97480277cc5a562cffcd410d8dded Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Fri, 28 Mar 2025 21:19:19 +0530 Subject: [PATCH 06/17] fix: use sourceToken --- src/contracts/socket/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contracts/socket/index.ts b/src/contracts/socket/index.ts index 6073e35..da1a646 100644 --- a/src/contracts/socket/index.ts +++ b/src/contracts/socket/index.ts @@ -92,7 +92,7 @@ export async function bridgeWithBungee( amount: route.fromAmount, recipient: route.recipient, toChainId: targetChain.toString(), - token: targetToken, + token: sourceToken, signature: socketBridgeFunctionSignatures[includeBridges[0]], }; await verifyBungeeTxData( From f797030e57697b7bb3ddc102dea9c46a252c3263 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Fri, 28 Mar 2025 21:20:29 +0530 Subject: [PATCH 07/17] chore: logs --- src/contracts/socket/index.ts | 4 +++- .../swapAndBridgeBungeeAcrossArbitrumBase.ts | 17 +++++++++++++---- .../swapAndBridgeBungeeCCTPArbitrumBase.ts | 15 +++++++++------ 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/contracts/socket/index.ts b/src/contracts/socket/index.ts index da1a646..6ba300e 100644 --- a/src/contracts/socket/index.ts +++ b/src/contracts/socket/index.ts @@ -66,7 +66,7 @@ export async function bridgeWithBungee( if (!quote) { throw new Error('No quote found'); } - console.log('šŸ”— Socket quote:', quote); + console.log('šŸ”— Socket quote:', quote.result.routes); // check if routes are found if (!quote.result.routes.length) { throw new Error('No routes found'); @@ -86,6 +86,8 @@ export async function bridgeWithBungee( const { routeId, encodedFunctionData } = decodeBungeeTxData( txData.result.txData ); + console.log('šŸ”— Socket txData:', txData.result.txData); + console.log('šŸ”— Socket routeId:', routeId); // validate bungee tx data returned from socket API using SocketVerifier contract const expectedSocketRequest: SocketRequest = { diff --git a/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts b/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts index b0695f3..9a31167 100644 --- a/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts +++ b/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts @@ -30,13 +30,22 @@ export async function run() { console.log('šŸ”‘ Wallet address:', walletAddress); const sellToken = arbitrum.USDT_ADDRESS; - const sellTokenDecimals = await getErc20Contract(sellToken, wallet).decimals(); + const sellTokenDecimals = await getErc20Contract( + sellToken, + wallet + ).decimals(); const sellTokenSymbol = await getErc20Contract(sellToken, wallet).symbol(); const sellAmount = ethers.utils.parseUnits('1', sellTokenDecimals).toString(); const buyToken = base.USDC_ADDRESS; - const buyTokenDecimals = await getErc20Contract(buyToken, wallet).decimals(); - const buyTokenSymbol = await getErc20Contract(buyToken, wallet).symbol(); + const buyTokenDecimals = await getErc20Contract( + buyToken, + await getWallet(targetChain) + ).decimals(); + const buyTokenSymbol = await getErc20Contract( + buyToken, + await getWallet(targetChain) + ).symbol(); // Initialize the SDK with the wallet const sdk = new TradingSdk({ @@ -66,7 +75,7 @@ export async function run() { const intermediateTokenAmount = quote.quoteResults.amountsAndCosts.afterSlippage.buyAmount; - console.log('quote', JSON.stringify(quote, jsonReplacer, 2)); + // console.log('quote', JSON.stringify(quote, jsonReplacer, 2)); // Get raw transaction to bridge all available DAI from cow-shed using xDAI Bridge const bridgeWithBungeeTx = await bridgeWithBungee({ diff --git a/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts b/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts index d6da44a..f0d968a 100644 --- a/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts +++ b/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts @@ -41,15 +41,19 @@ export async function run() { */ const sourceChain = SupportedChainId.ARBITRUM_ONE; const targetChain = SupportedChainId.BASE; - const sellToken = arbitrum.USDT_ADDRESS; - const sellTokenDecimals = 6; - const sellAmount = ethers.utils.parseUnits('1', sellTokenDecimals).toString(); - const buyToken = base.USDC_ADDRESS; - const buyTokenDecimals = 6; const wallet = await getWallet(sourceChain); const walletAddress = await wallet.getAddress(); console.log('šŸ”‘ Wallet address:', walletAddress); + + const sellToken = arbitrum.USDT_ADDRESS; + const sellTokenContract = getErc20Contract(sellToken, wallet); + const sellTokenDecimals = await sellTokenContract.decimals(); + const sellAmount = ethers.utils.parseUnits('1', sellTokenDecimals).toString(); + const buyToken = base.USDC_ADDRESS; + const buyTokenContract = getErc20Contract(buyToken, await getWallet(targetChain)); + const buyTokenDecimals = await buyTokenContract.decimals(); + // Initialize the SDK with the wallet const sdk = new TradingSdk({ @@ -175,7 +179,6 @@ export async function run() { // check owner allowance to VaultRelayer const vaultRelayerContract = COW_PROTOCOL_VAULT_RELAYER_ADDRESS[sourceChain]; - const sellTokenContract = getErc20Contract(sellToken, wallet); const sellTokenAllowance = await sellTokenContract.allowance( walletAddress, vaultRelayerContract From 05187735f219d9c35a2fbaaa83f5d1836454a3c0 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Mon, 31 Mar 2025 19:36:30 +0530 Subject: [PATCH 08/17] refactor: match both quotes --- src/contracts/cowShed/index.ts | 2 +- .../swapAndBridgeBungeeAcrossArbitrumBase.ts | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/contracts/cowShed/index.ts b/src/contracts/cowShed/index.ts index 59f5837..808e76b 100644 --- a/src/contracts/cowShed/index.ts +++ b/src/contracts/cowShed/index.ts @@ -20,7 +20,7 @@ export interface CowShedTx { gasLimit: bigint; } -function getCowShedHooks(chainId: SupportedChainId) { +export function getCowShedHooks(chainId: SupportedChainId) { let cowShedHooks = COW_SHED_CACHE.get(chainId)!; if (cowShedHooks) { diff --git a/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts b/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts index 9a31167..148d8b8 100644 --- a/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts +++ b/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts @@ -10,7 +10,7 @@ import { import { ethers } from 'ethers'; import { MetadataApi } from '@cowprotocol/app-data'; -import { createCowShedTx } from '../../contracts/cowShed'; +import { createCowShedTx, getCowShedHooks } from '../../contracts/cowShed'; import { confirm, getWallet, jsonReplacer } from '../../utils'; import { getErc20Contract } from '../../contracts/erc20'; @@ -63,14 +63,17 @@ export async function run() { const intermediateTokenSymbol = await intermediateTokenContract.symbol(); // Estimate how many intermediate tokens we can bridge + const cowShedHooks = getCowShedHooks(sourceChain); + const cowShedAccount = cowShedHooks.proxyOf(wallet.address); let quote = await sdk.getQuote({ kind: OrderKind.SELL, + amount: sellAmount, sellToken, sellTokenDecimals, buyToken: intermediaryToken, - buyTokenDecimals, - receiver: wallet.address, - amount: sellAmount, + buyTokenDecimals: intermediateTokenDecimals, + partiallyFillable: false, // Fill or Kill + receiver: cowShedAccount, }); const intermediateTokenAmount = quote.quoteResults.amountsAndCosts.afterSlippage.buyAmount; @@ -95,11 +98,8 @@ export async function run() { ); // Sign and encode the transaction - const { - cowShedAccount, - preAuthenticatedTx: authenticatedBridgeTx, - gasLimit, - } = await createCowShedTx({ + const { preAuthenticatedTx: authenticatedBridgeTx, gasLimit } = + await createCowShedTx({ tx: bridgeWithBungeeTx, chainId: sourceChain, wallet, @@ -110,7 +110,7 @@ export async function run() { kind: OrderKind.SELL, // Sell amount: sellAmount, sellToken, - sellTokenDecimals: sellTokenDecimals, + sellTokenDecimals, buyToken: intermediaryToken, buyTokenDecimals: intermediateTokenDecimals, partiallyFillable: false, // Fill or Kill From ac20b9bf8dec63492ca9d5987a1c59bb9e667fd9 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Mon, 31 Mar 2025 19:38:36 +0530 Subject: [PATCH 09/17] chore: match both scripts --- .../swapAndBridgeBungeeCCTPArbitrumBase.ts | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts b/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts index f0d968a..d6da08d 100644 --- a/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts +++ b/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts @@ -10,7 +10,7 @@ import { import { ethers } from 'ethers'; import { MetadataApi } from '@cowprotocol/app-data'; -import { createCowShedTx } from '../../contracts/cowShed'; +import { createCowShedTx, getCowShedHooks } from '../../contracts/cowShed'; import { confirm, getWallet, jsonReplacer } from '../../utils'; import { getErc20Contract } from '../../contracts/erc20'; @@ -47,13 +47,22 @@ export async function run() { console.log('šŸ”‘ Wallet address:', walletAddress); const sellToken = arbitrum.USDT_ADDRESS; - const sellTokenContract = getErc20Contract(sellToken, wallet); - const sellTokenDecimals = await sellTokenContract.decimals(); + const sellTokenDecimals = await getErc20Contract( + sellToken, + wallet + ).decimals(); + const sellTokenSymbol = await getErc20Contract(sellToken, wallet).symbol(); + const sellAmount = ethers.utils.parseUnits('1', sellTokenDecimals).toString(); const buyToken = base.USDC_ADDRESS; - const buyTokenContract = getErc20Contract(buyToken, await getWallet(targetChain)); - const buyTokenDecimals = await buyTokenContract.decimals(); - + const buyTokenDecimals = await getErc20Contract( + buyToken, + await getWallet(targetChain) + ).decimals(); + const buyTokenSymbol = await getErc20Contract( + buyToken, + await getWallet(targetChain) + ).symbol(); // Initialize the SDK with the wallet const sdk = new TradingSdk({ @@ -71,19 +80,22 @@ export async function run() { const intermediateTokenSymbol = await intermediateTokenContract.symbol(); // Estimate how many intermediate tokens we can bridge + const cowShedHooks = getCowShedHooks(sourceChain); + const cowShedAccount = cowShedHooks.proxyOf(wallet.address); let quote = await sdk.getQuote({ kind: OrderKind.SELL, + amount: sellAmount, sellToken, sellTokenDecimals, buyToken: intermediaryToken, - buyTokenDecimals, - receiver: wallet.address, - amount: sellAmount, + buyTokenDecimals: intermediateTokenDecimals, + partiallyFillable: false, // Fill or Kill + receiver: cowShedAccount, }); const intermediateTokenAmount = quote.quoteResults.amountsAndCosts.afterSlippage.buyAmount; - console.log('quote', JSON.stringify(quote, jsonReplacer, 2)); + // console.log('quote', JSON.stringify(quote, jsonReplacer, 2)); // Get raw transaction to bridge all available DAI from cow-shed using xDAI Bridge const bridgeWithBungeeTx = await bridgeWithBungee({ @@ -103,11 +115,8 @@ export async function run() { ); // Sign and encode the transaction - const { - cowShedAccount, - preAuthenticatedTx: authenticatedBridgeTx, - gasLimit, - } = await createCowShedTx({ + const { preAuthenticatedTx: authenticatedBridgeTx, gasLimit } = + await createCowShedTx({ tx: bridgeWithBungeeTx, chainId: sourceChain, wallet, @@ -118,7 +127,7 @@ export async function run() { kind: OrderKind.SELL, // Sell amount: sellAmount, sellToken, - sellTokenDecimals: sellTokenDecimals, + sellTokenDecimals, buyToken: intermediaryToken, buyTokenDecimals: intermediateTokenDecimals, partiallyFillable: false, // Fill or Kill @@ -166,7 +175,7 @@ export async function run() { ); console.log( - `You will sell ${sellAmountFormatted} USDC and receive at least ${minIntermediateTokenAmountFormatted} ${intermediateTokenSymbol} (intermediate token). Then, it will be bridged to Base for USDC via CCTP via Socket.` + `You will sell ${sellAmountFormatted} ${sellTokenSymbol} and receive at least ${minIntermediateTokenAmountFormatted} ${intermediateTokenSymbol} (intermediate token). Then, it will be bridged to Base for ${buyTokenSymbol} via Across via Socket.` ); const confirmed = await confirm( @@ -179,6 +188,7 @@ export async function run() { // check owner allowance to VaultRelayer const vaultRelayerContract = COW_PROTOCOL_VAULT_RELAYER_ADDRESS[sourceChain]; + const sellTokenContract = getErc20Contract(sellToken, wallet); const sellTokenAllowance = await sellTokenContract.allowance( walletAddress, vaultRelayerContract @@ -215,14 +225,5 @@ export async function run() { `ā„¹ļø Order created, id: https://explorer.cow.fi/orders/${orderId}?tab=overview` ); - // Wait for the bridge start - console.log('šŸ•£ Waiting for the bridge to start...'); - console.log('šŸ”— Socket link: '); - // TODO: Implement - - // Wait for the bridging to be completed - console.log('šŸ•£ Waiting for the bridging to be completed...'); - // TODO: Implement - console.log(`šŸŽ‰ The USDC is now waiting for you in Base`); } From a9371efd4c0e09f74803b8cf52dcc2b0fbfd547d Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Tue, 1 Apr 2025 15:45:36 +0530 Subject: [PATCH 10/17] =?UTF-8?q?refactor:=20includeBridges=20=E2=86=92=20?= =?UTF-8?q?useBridge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/contracts/socket/index.ts | 8 ++++---- .../bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts | 2 +- .../bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/contracts/socket/index.ts b/src/contracts/socket/index.ts index 6ba300e..1a2b237 100644 --- a/src/contracts/socket/index.ts +++ b/src/contracts/socket/index.ts @@ -26,7 +26,7 @@ export interface BridgeWithBungeeParams { targetToken: string; targetChain: number; recipient: string; - includeBridges: string[]; + useBridge: 'cctp' | 'across'; } export async function bridgeWithBungee( @@ -40,7 +40,7 @@ export async function bridgeWithBungee( targetChain, targetToken, recipient, - includeBridges, + useBridge, } = params; // Get cow-shed account @@ -61,7 +61,7 @@ export async function bridgeWithBungee( singleTxOnly: true, // should be only single txn on src chain, no destination chain txn isContractCall: true, // get quotes that are compatible with contracts disableSwapping: true, // should not show routes that require swapping - includeBridges, + includeBridges: [useBridge], }); if (!quote) { throw new Error('No quote found'); @@ -95,7 +95,7 @@ export async function bridgeWithBungee( recipient: route.recipient, toChainId: targetChain.toString(), token: sourceToken, - signature: socketBridgeFunctionSignatures[includeBridges[0]], + signature: socketBridgeFunctionSignatures[useBridge], }; await verifyBungeeTxData( sourceChain, diff --git a/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts b/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts index 148d8b8..bafbbe2 100644 --- a/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts +++ b/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts @@ -89,7 +89,7 @@ export async function run() { targetChain: targetChain, targetToken: buyToken, recipient: wallet.address, - includeBridges: ['across'], + useBridge: 'across', }); console.log( diff --git a/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts b/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts index d6da08d..23d72d7 100644 --- a/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts +++ b/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts @@ -106,7 +106,7 @@ export async function run() { targetChain: targetChain, targetToken: buyToken, recipient: wallet.address, - includeBridges: ['cctp'], + useBridge: 'cctp', }); console.log( From 4ff51283d791fdc7b8dd58c90a3a000df6d91b42 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Wed, 2 Apr 2025 01:55:26 +0530 Subject: [PATCH 11/17] chore: log quotes amounts --- src/contracts/socket/index.ts | 36 +++++++++++++++++++ .../swapAndBridgeBungeeAcrossArbitrumBase.ts | 13 ++++--- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/contracts/socket/index.ts b/src/contracts/socket/index.ts index 1a2b237..f33fe81 100644 --- a/src/contracts/socket/index.ts +++ b/src/contracts/socket/index.ts @@ -168,6 +168,42 @@ export async function bridgeWithBungee( ) ); + // if bridge is across, update the output amount based on pctDiff of the new balance + if (useBridge === 'across') { + // current input amount + const inputAmount_StartBytesIndex = 4; + const inputAmount_BytesLength = 32; + const inputAmount_StartBytesStringIndex = + 2 + inputAmount_StartBytesIndex * 2; + const inputAmount_EndBytesStringIndex = + inputAmount_StartBytesStringIndex + inputAmount_BytesLength * 2; + const currentInputAmount = `0x${encodedFunctionData.slice( + inputAmount_StartBytesStringIndex, + inputAmount_EndBytesStringIndex + )}`; + const currentInputAmountBigNumber = + ethers.BigNumber.from(currentInputAmount); + + // current output amount + const outputAmount_StartBytesIndex = 484; + const outputAmount_BytesLength = 32; + const outputAmount_StartBytesStringIndex = + 2 + outputAmount_StartBytesIndex * 2; + const outputAmount_EndBytesStringIndex = + outputAmount_StartBytesStringIndex + outputAmount_BytesLength * 2; + const currentOutputAmount = `0x${encodedFunctionData.slice( + outputAmount_StartBytesStringIndex, + outputAmount_EndBytesStringIndex + )}`; + const currentOutputAmountBigNumber = + ethers.BigNumber.from(currentOutputAmount); + + console.log('', { + currentInputAmountBigNumber: currentInputAmountBigNumber.toString(), + currentOutputAmountBigNumber: currentOutputAmountBigNumber.toString(), + }); + } + const socketGatewayContract = WeirollContract.createContract( new ethers.Contract(txData.result.txTarget, socketGatewayAbi), CommandFlags.CALL diff --git a/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts b/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts index bafbbe2..b61dfac 100644 --- a/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts +++ b/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts @@ -100,10 +100,10 @@ export async function run() { // Sign and encode the transaction const { preAuthenticatedTx: authenticatedBridgeTx, gasLimit } = await createCowShedTx({ - tx: bridgeWithBungeeTx, - chainId: sourceChain, - wallet, - }); + tx: bridgeWithBungeeTx, + chainId: sourceChain, + wallet, + }); // Define trade parameters. Sell sell token for intermediary token, to be received by cow-shed const parameters: TradeParameters = { @@ -148,6 +148,11 @@ export async function run() { const minIntermediateTokenAmount = quoteResults.amountsAndCosts.afterSlippage.buyAmount; + + const firstQuote = intermediateTokenAmount.toString(); + const secondQuote = minIntermediateTokenAmount.toString(); + console.log('', { firstQuote, secondQuote }); + const minIntermediateTokenAmountFormatted = ethers.utils.formatUnits( minIntermediateTokenAmount, intermediateTokenDecimals From 10eb739ee1bf6b4bb836d286980c572e325c18c1 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Wed, 2 Apr 2025 11:56:56 +0530 Subject: [PATCH 12/17] feat: update outputAmount for across --- src/contracts/socket/index.ts | 90 ++++++++++--------- src/contracts/socket/types.ts | 36 +++++++- src/contracts/socket/utils.ts | 70 +++++++++++++++ .../swapAndBridgeBungeeAcrossArbitrumBase.ts | 1 + .../swapAndBridgeBungeeCCTPArbitrumBase.ts | 5 ++ 5 files changed, 158 insertions(+), 44 deletions(-) diff --git a/src/contracts/socket/index.ts b/src/contracts/socket/index.ts index f33fe81..53ba5fe 100644 --- a/src/contracts/socket/index.ts +++ b/src/contracts/socket/index.ts @@ -9,8 +9,11 @@ import { getWallet } from '../../utils'; import { getCowShedAccount } from '../cowShed'; import { getErc20Contract } from '../erc20'; import { CommandFlags, getWeirollTx } from '../weiroll'; -import { bytesLibAbi, socketGatewayAbi, SocketRequest } from './types'; +import { bungeeCowswapLibAbi, socketGatewayAbi, SocketRequest } from './types'; import { + BungeeCowswapLibAddresses, + BungeeTxDataIndices, + decodeAmountsBungeeTxData, decodeBungeeTxData, getBungeeQuote, getBungeeRouteTransactionData, @@ -154,54 +157,58 @@ export async function bridgeWithBungee( planner.add(approvalTokenContract.approve(allowanceTarget, allowanceToSet)); } - const bytesLibContractAddress = '0x8f6BA63528De7266d8cDfDdec7ACFA8446c62aB4'; - const bytesLibContract = WeirollContract.createContract( - new ethers.Contract(bytesLibContractAddress, bytesLibAbi), + const BungeeCowswapLibContractAddress = + BungeeCowswapLibAddresses[sourceChain]; + if (!BungeeCowswapLibContractAddress) { + throw new Error('BungeeCowswapLib contract not found'); + } + const BungeeCowswapLibContract = WeirollContract.createContract( + new ethers.Contract(BungeeCowswapLibContractAddress, bungeeCowswapLibAbi), CommandFlags.CALL ); - const encodedFunctionDataWithNewAmount = planner.add( - bytesLibContract.replaceBytes( + + // weiroll: replace input amount with new input amount + const encodedFunctionDataWithNewInputAmount = planner.add( + BungeeCowswapLibContract.replaceBytes( encodedFunctionData, - 4, // first 4 bytes are the function selector - 32, // first 32 bytes of the params are the amount + BungeeTxDataIndices[useBridge].inputAmountBytes_startIndex, + BungeeTxDataIndices[useBridge].inputAmountBytes_length, sourceAmountIncludingSurplusBytes ) ); + let finalEncodedFunctionData = encodedFunctionDataWithNewInputAmount; // if bridge is across, update the output amount based on pctDiff of the new balance if (useBridge === 'across') { - // current input amount - const inputAmount_StartBytesIndex = 4; - const inputAmount_BytesLength = 32; - const inputAmount_StartBytesStringIndex = - 2 + inputAmount_StartBytesIndex * 2; - const inputAmount_EndBytesStringIndex = - inputAmount_StartBytesStringIndex + inputAmount_BytesLength * 2; - const currentInputAmount = `0x${encodedFunctionData.slice( - inputAmount_StartBytesStringIndex, - inputAmount_EndBytesStringIndex - )}`; - const currentInputAmountBigNumber = - ethers.BigNumber.from(currentInputAmount); - - // current output amount - const outputAmount_StartBytesIndex = 484; - const outputAmount_BytesLength = 32; - const outputAmount_StartBytesStringIndex = - 2 + outputAmount_StartBytesIndex * 2; - const outputAmount_EndBytesStringIndex = - outputAmount_StartBytesStringIndex + outputAmount_BytesLength * 2; - const currentOutputAmount = `0x${encodedFunctionData.slice( - outputAmount_StartBytesStringIndex, - outputAmount_EndBytesStringIndex - )}`; - const currentOutputAmountBigNumber = - ethers.BigNumber.from(currentOutputAmount); - - console.log('', { - currentInputAmountBigNumber: currentInputAmountBigNumber.toString(), - currentOutputAmountBigNumber: currentOutputAmountBigNumber.toString(), + // decode current input & output amounts + const { inputAmountBigNumber, outputAmountBigNumber } = + decodeAmountsBungeeTxData(encodedFunctionData, useBridge); + console.log('šŸ”— Socket input & output amounts:', { + inputAmountBigNumber: inputAmountBigNumber.toString(), + outputAmountBigNumber: outputAmountBigNumber.toString(), }); + + // new input amount + const newInputAmount = sourceAmountIncludingSurplusBytes; + + // weiroll: increase output amount by pctDiff + const newOutputAmount = planner.add( + BungeeCowswapLibContract.addPctDiff( + inputAmountBigNumber, // base + newInputAmount, // compare + outputAmountBigNumber // target + ).rawValue() + ); + // weiroll: replace output amount bytes with newOutputAmount + const encodedFunctionDataWithNewInputAndOutputAmount = planner.add( + BungeeCowswapLibContract.replaceBytes( + finalEncodedFunctionData, + BungeeTxDataIndices[useBridge].outputAmountBytes_startIndex!, + BungeeTxDataIndices[useBridge].outputAmountBytes_length!, + newOutputAmount + ) + ); + finalEncodedFunctionData = encodedFunctionDataWithNewInputAndOutputAmount; } const socketGatewayContract = WeirollContract.createContract( @@ -210,10 +217,7 @@ export async function bridgeWithBungee( ); // Call executeRoute on SocketGateway planner.add( - socketGatewayContract.executeRoute( - routeId, - encodedFunctionDataWithNewAmount - ) + socketGatewayContract.executeRoute(routeId, finalEncodedFunctionData) ); // Return the transaction diff --git a/src/contracts/socket/types.ts b/src/contracts/socket/types.ts index c1de574..b6b0849 100644 --- a/src/contracts/socket/types.ts +++ b/src/contracts/socket/types.ts @@ -209,7 +209,41 @@ export type BungeeBuildTxResponse = { statusCode: number; }; -export const bytesLibAbi = [ +export const bungeeCowswapLibAbi = [ + { + inputs: [], + name: 'InvalidInput', + type: 'error', + }, + { + inputs: [ + { + internalType: 'uint256', + name: '_base', + type: 'uint256', + }, + { + internalType: 'bytes', + name: '_compare', + type: 'bytes', + }, + { + internalType: 'uint256', + name: '_target', + type: 'uint256', + }, + ], + name: 'addPctDiff', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'pure', + type: 'function', + }, { inputs: [ { diff --git a/src/contracts/socket/utils.ts b/src/contracts/socket/utils.ts index a0896d9..6d1403c 100644 --- a/src/contracts/socket/utils.ts +++ b/src/contracts/socket/utils.ts @@ -48,6 +48,52 @@ export const socketGatewayMapping: Record< [SupportedChainId.SEPOLIA]: undefined, }; +// TODO: deploy BungeeCowswapLib contracts for all chains +export const BungeeCowswapLibAddresses: Record< + SupportedChainId, + string | undefined +> = { + [SupportedChainId.MAINNET]: undefined, + [SupportedChainId.GNOSIS_CHAIN]: undefined, + [SupportedChainId.ARBITRUM_ONE]: '0xAeE8bC0284d795D7662608dD765C8b5F1C6250CD', + [SupportedChainId.BASE]: undefined, + [SupportedChainId.SEPOLIA]: undefined, +}; + +export const BungeeTxDataIndices: Record< + 'across' | 'cctp', + { + // input amount + inputAmountBytes_startIndex: number; + inputAmountBytes_length: number; + inputAmountBytesString_startIndex: number; + inputAmountBytesString_length: number; + // output amount + outputAmountBytes_startIndex?: number; + outputAmountBytes_length?: number; + outputAmountBytesString_startIndex?: number; + outputAmountBytesString_length?: number; + } +> = { + across: { + inputAmountBytes_startIndex: 4, // first 4 bytes are the function selector + inputAmountBytes_length: 32, // first 32 bytes of the params are the amount + inputAmountBytesString_startIndex: 2 + 4 * 2, // first two characters are 0x and 4 bytes = 8 chars for the amount + inputAmountBytesString_length: 32 * 2, // 32 bytes = 64 chars for the amount + // output amount + outputAmountBytes_startIndex: 484, // outputAmount is part of the AcrossBridgeData struct in SocketGateway AcrossV3 impl + outputAmountBytes_length: 32, // 32 bytes of amount + outputAmountBytesString_startIndex: 2 + 484 * 2, // first two characters are 0x and 484 bytes = 968 chars for the amount + outputAmountBytesString_length: 32 * 2, // 32 bytes = 64 chars for the amount + }, + cctp: { + inputAmountBytes_startIndex: 4, // first 4 bytes are the function selector + inputAmountBytes_length: 32, // first 32 bytes of the params are the amount + inputAmountBytesString_startIndex: 2 + 4 * 2, // first two characters are 0x and 4 bytes = 8 chars for the amount + inputAmountBytesString_length: 32 * 2, // 32 bytes = 64 chars for the amount + }, +}; + /** * Makes a GET request to Bungee APIs for quote * https://docs.bungee.exchange/bungee-manual/socket-api-reference/quote-controller-get-quote/ @@ -134,6 +180,30 @@ export const decodeBungeeTxData = (txData: string) => { return { routeId, encodedFunctionData }; }; +export const decodeAmountsBungeeTxData = ( + txData: string, + bridge: 'across' | 'cctp' +) => { + const inputAmountBytes = `0x${txData.slice( + BungeeTxDataIndices[bridge].inputAmountBytesString_startIndex, + BungeeTxDataIndices[bridge].inputAmountBytesString_startIndex + + BungeeTxDataIndices[bridge].inputAmountBytesString_length + )}`; + const inputAmountBigNumber = ethers.BigNumber.from(inputAmountBytes); + const outputAmountBytes = `0x${txData.slice( + BungeeTxDataIndices[bridge].outputAmountBytesString_startIndex, + BungeeTxDataIndices[bridge].outputAmountBytesString_startIndex! + + BungeeTxDataIndices[bridge].outputAmountBytesString_length! + )}`; + const outputAmountBigNumber = ethers.BigNumber.from(outputAmountBytes); + return { + inputAmountBytes, + inputAmountBigNumber, + outputAmountBytes, + outputAmountBigNumber, + }; +}; + export const verifyBungeeTxData = async ( chainId: SupportedChainId, txData: string, diff --git a/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts b/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts index b61dfac..84c67ba 100644 --- a/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts +++ b/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts @@ -77,6 +77,7 @@ export async function run() { }); const intermediateTokenAmount = quote.quoteResults.amountsAndCosts.afterSlippage.buyAmount; + console.log('intermediateTokenAmount', intermediateTokenAmount.toString()); // console.log('quote', JSON.stringify(quote, jsonReplacer, 2)); diff --git a/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts b/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts index 23d72d7..301bd34 100644 --- a/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts +++ b/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts @@ -165,6 +165,11 @@ export async function run() { const minIntermediateTokenAmount = quoteResults.amountsAndCosts.afterSlippage.buyAmount; + + const firstQuote = intermediateTokenAmount.toString(); + const secondQuote = minIntermediateTokenAmount.toString(); + console.log('', { firstQuote, secondQuote }); + const minIntermediateTokenAmountFormatted = ethers.utils.formatUnits( minIntermediateTokenAmount, intermediateTokenDecimals From 2aac8fb50e080a52f00c445d7bde9d2e662ea575 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Wed, 2 Apr 2025 20:46:51 +0530 Subject: [PATCH 13/17] fix: callStatic validation --- src/contracts/socket/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contracts/socket/utils.ts b/src/contracts/socket/utils.ts index 6d1403c..cd77892 100644 --- a/src/contracts/socket/utils.ts +++ b/src/contracts/socket/utils.ts @@ -224,7 +224,7 @@ export const verifyBungeeTxData = async ( // should not revert try { - await socketVerifier.validateRotueId(txData, routeId); + await socketVerifier.callStatic.validateRotueId(txData, routeId); } catch (error) { console.error('šŸ”“ Error validating routeId:', error); throw error; @@ -237,7 +237,7 @@ export const verifyBungeeTxData = async ( // should not revert try { - await socketVerifier.validateSocketRequest( + await socketVerifier.callStatic.validateSocketRequest( txData, expectedUserRequestValidation ); From 3436c8f94a0d4e7b0d64e8add5862bde649d1f4a Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Thu, 3 Apr 2025 18:31:40 +0530 Subject: [PATCH 14/17] feat: add socketscan status api --- src/index.ts | 4 +- src/scripts/bridging/getBungeeBridgeStatus.ts | 124 ++++++++++++++++++ .../swapAndBridgeBungeeAcrossArbitrumBase.ts | 5 + 3 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 src/scripts/bridging/getBungeeBridgeStatus.ts diff --git a/src/index.ts b/src/index.ts index 2294310..8903bbc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,6 +29,7 @@ import { run as swapAndBridgeAccrossArbitrum } from "./scripts/bridging/swapAndB import { run as swapAndBridgeAccrossMainnet } from "./scripts/bridging/swapAndBridgeAccrossMainnet"; import { run as swapAndBridgeBungeeCCTPArbitrumBase } from "./scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase"; import { run as swapAndBridgeBungeeAcrossArbitrumBase } from "./scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase"; +import { run as getBungeeBridgeStatus } from "./scripts/bridging/getBungeeBridgeStatus"; dotenv.config(); // Just to dev things easily using watch-mode :) @@ -67,7 +68,8 @@ const JOBS: (() => Promise)[] = [ // swapAndBridgeAccrossArbitrum, // swapAndBridgeAccrossMainnet, // swapAndBridgeBungeeCCTPArbitrumBase, - swapAndBridgeBungeeAcrossArbitrumBase, + // swapAndBridgeBungeeAcrossArbitrumBase, + getBungeeBridgeStatus, ]; async function main() { diff --git a/src/scripts/bridging/getBungeeBridgeStatus.ts b/src/scripts/bridging/getBungeeBridgeStatus.ts new file mode 100644 index 0000000..ca6f9b3 --- /dev/null +++ b/src/scripts/bridging/getBungeeBridgeStatus.ts @@ -0,0 +1,124 @@ +import { OrderBookApi, SupportedChainId } from '@cowprotocol/cow-sdk'; +import axios from 'axios'; + +type BridgeStatusResponse = { + success: boolean; + result: Result[]; +}; + +type Result = { + orderId: string; + bridgeName: string; + isCowswapTrade: boolean; + srcTransactionHash: string; + destTransactionHash: string; + fromChainId: number; + toChainId: number; + srcTxStatus: string; + destTxStatus: string; +}; + +/* +status: { + orderId: '0x0bfa5c44e95964a907d5f0d69ea65221e3a8fb1871e41aa3195e446c4ce855bbdaee4d2156de6fe6f7d50ca047136d758f96a6f067ee7474', + bridgeName: 'across', + isCowswapTrade: true, + fromChainId: 42161, + toChainId: 8453, + srcTransactionHash: '0x649b6fd231cf97972ccff205925f4582c760db7ce54d1b38a91eedca0e933986', + destTransactionHash: '0xe342cf2cb68ac161968457926f9449084777ca0662c94b88c2c783926552d189', + srcTxStatus: 'COMPLETED', + destTxStatus: 'COMPLETED' +} +*/ + +export async function run() { + const chainId = SupportedChainId.ARBITRUM_ONE; + const _orderId = + '0x0bfa5c44e95964a907d5f0d69ea65221e3a8fb1871e41aa3195e446c4ce855bbdaee4d2156de6fe6f7d50ca047136d758f96a6f067ee7474'; + + const status = await getBridgeStatusWithSrcTxHash(chainId, _orderId); + const statusResponse = { + orderId: status.orderId, + bridgeName: status.bridgeName, + isCowswapTrade: status.isCowswapTrade, + fromChainId: status.fromChainId, + toChainId: status.toChainId, + srcTransactionHash: status.srcTransactionHash, + destTransactionHash: status.destTransactionHash, + srcTxStatus: status.srcTxStatus, + destTxStatus: status.destTxStatus, + }; + console.log('status:', statusResponse); + + const statusViaTxApi = await getBridgeStatusWithOrderIdViaTxApi(_orderId); + const statusViaTxApiResponse = { + orderId: statusViaTxApi.orderId, + bridgeName: statusViaTxApi.bridgeName, + isCowswapTrade: statusViaTxApi.isCowswapTrade, + fromChainId: statusViaTxApi.fromChainId, + toChainId: statusViaTxApi.toChainId, + srcTransactionHash: statusViaTxApi.srcTransactionHash, + destTransactionHash: statusViaTxApi.destTransactionHash, + srcTxStatus: statusViaTxApi.srcTxStatus, + destTxStatus: statusViaTxApi.destTxStatus, + }; + console.log('statusViaTxApi:', statusViaTxApiResponse); + + const statusViaOrderId = await getBridgeStatusWithOrderId(_orderId); + const statusViaOrderIdResponse = { + orderId: statusViaOrderId.orderId, + bridgeName: statusViaOrderId.bridgeName, + isCowswapTrade: statusViaOrderId.isCowswapTrade, + fromChainId: statusViaOrderId.fromChainId, + toChainId: statusViaOrderId.toChainId, + srcTransactionHash: statusViaOrderId.srcTransactionHash, + destTransactionHash: statusViaOrderId.destTransactionHash, + srcTxStatus: statusViaOrderId.srcTxStatus, + destTxStatus: statusViaOrderId.destTxStatus, + }; + console.log('statusViaOrderId:', statusViaOrderIdResponse); + + const socketscanLink = getSocketscanLink(_orderId); + console.log(socketscanLink); +} + +export async function getBridgeStatusWithSrcTxHash( + chainId: SupportedChainId, + orderId: string +) { + // fetch order source tx from Orderbook API + const orderBook = new OrderBookApi({ + chainId, + }); + const trades = await orderBook.getTrades({ orderUid: orderId }); + const srcTxHash = trades[0].txHash; + if (!srcTxHash) { + throw new Error('No source tx hash found'); + } + + // fetch bridge status from Socketscan API using cowswap trade tx hash + const response = await axios.get( + `https://microservices.socket.tech/loki/tx?txHash=${srcTxHash}` + ); + return response.data.result[0]; +} + +export async function getBridgeStatusWithOrderIdViaTxApi(orderId: string) { + // fetch bridge status from Socketscan API using cowswap order id + const response = await axios.get( + `https://microservices.socket.tech/loki/tx?txHash=${orderId}` + ); + return response.data.result[0]; +} + +export async function getBridgeStatusWithOrderId(orderId: string) { + // fetch bridge status from Socketscan API using cowswap order id + const response = await axios.get( + `https://microservices.socket.tech/loki/order?orderId=${orderId}` + ); + return response.data.result[0]; +} + +export const getSocketscanLink = (orderId: string) => + `https://www.socketscan.io/tx/${orderId}`; diff --git a/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts b/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts index 84c67ba..508002a 100644 --- a/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts +++ b/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts @@ -214,5 +214,10 @@ export async function run() { `ā„¹ļø Order created, id: https://explorer.cow.fi/orders/${orderId}?tab=overview` ); + // Print socketscan link + console.log( + `šŸ” After filled on CoWSwap, you can watch bridge status on Socketscan using the order id: https://www.socketscan.io/tx/${orderId}` + ); + console.log(`šŸŽ‰ The USDC is now waiting for you in Base`); } From 76e2cb7b4a0afb17eabf10bb0d19d9c24263c8a4 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Thu, 3 Apr 2025 19:08:10 +0530 Subject: [PATCH 15/17] fix: merge changes from main --- src/contracts/socket/index.ts | 175 +++++++++--------- src/index.ts | 4 +- .../swapAndBridgeBungeeAcrossArbitrumBase.ts | 9 +- .../swapAndBridgeBungeeCCTPArbitrumBase.ts | 22 ++- 4 files changed, 111 insertions(+), 99 deletions(-) diff --git a/src/contracts/socket/index.ts b/src/contracts/socket/index.ts index 53ba5fe..70693bf 100644 --- a/src/contracts/socket/index.ts +++ b/src/contracts/socket/index.ts @@ -1,14 +1,15 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk'; import { - Contract as WeirollContract, - Planner as WeirollPlanner, -} from '@weiroll/weiroll.js'; + createWeirollContract, + createWeirollDelegateCall, + EvmCall, + SupportedChainId, + WeirollCommandFlags, +} from '@cowprotocol/cow-sdk'; +import { Planner as WeirollPlanner } from '@weiroll/weiroll.js'; import { ethers } from 'ethers'; -import { BaseTransaction } from '../../types'; import { getWallet } from '../../utils'; import { getCowShedAccount } from '../cowShed'; import { getErc20Contract } from '../erc20'; -import { CommandFlags, getWeirollTx } from '../weiroll'; import { bungeeCowswapLibAbi, socketGatewayAbi, SocketRequest } from './types'; import { BungeeCowswapLibAddresses, @@ -34,7 +35,7 @@ export interface BridgeWithBungeeParams { export async function bridgeWithBungee( params: BridgeWithBungeeParams -): Promise { +): Promise { const { owner, sourceChain, @@ -108,9 +109,9 @@ export async function bridgeWithBungee( ); // Create bridged token contract - const bridgedTokenContract = WeirollContract.createContract( + const bridgedTokenContract = createWeirollContract( getErc20Contract(sourceToken), - CommandFlags.CALL + WeirollCommandFlags.CALL ); // Get balance of CoW shed proxy @@ -118,13 +119,9 @@ export async function bridgeWithBungee( `[socket] Get cow-shed balance for ERC20.balanceOf(${cowShedAccount}) for ${bridgedTokenContract.address}` ); - // Get bridged amount (balance of the intermediate token at swap time) - const sourceAmountIncludingSurplusBytes = planner.add( - bridgedTokenContract.balanceOf(cowShedAccount).rawValue() - ); - // Check & set allowance for SocketGateway to transfer bridged tokens // check if allowance is sufficient + let setAllowance = false; const { approvalData: { approvalTokenAddress, @@ -142,84 +139,96 @@ export async function bridgeWithBungee( ); console.log('current cowshed allowance', allowance); if (allowance < minimumApprovalAmount) { - // set allowance - const approvalTokenContract = WeirollContract.createContract( - getErc20Contract(approvalTokenAddress), - CommandFlags.CALL - ); - console.log( - `[socket] approvalTokenContract.approve(${allowanceTarget}, ${sourceAmountIncludingSurplusBytes}) for ${approvalTokenContract}` - ); - const allowanceToSet = ethers.utils.parseUnits( - '1000', - await intermediateTokenContract.decimals() - ); - planner.add(approvalTokenContract.approve(allowanceTarget, allowanceToSet)); + setAllowance = true; } - const BungeeCowswapLibContractAddress = - BungeeCowswapLibAddresses[sourceChain]; - if (!BungeeCowswapLibContractAddress) { - throw new Error('BungeeCowswapLib contract not found'); - } - const BungeeCowswapLibContract = WeirollContract.createContract( - new ethers.Contract(BungeeCowswapLibContractAddress, bungeeCowswapLibAbi), - CommandFlags.CALL + // set allowance + const approvalTokenContract = createWeirollContract( + getErc20Contract(approvalTokenAddress), + WeirollCommandFlags.CALL ); - // weiroll: replace input amount with new input amount - const encodedFunctionDataWithNewInputAmount = planner.add( - BungeeCowswapLibContract.replaceBytes( - encodedFunctionData, - BungeeTxDataIndices[useBridge].inputAmountBytes_startIndex, - BungeeTxDataIndices[useBridge].inputAmountBytes_length, - sourceAmountIncludingSurplusBytes - ) + const allowanceToSet = ethers.utils.parseUnits( + '1000', + await intermediateTokenContract.decimals() ); - let finalEncodedFunctionData = encodedFunctionDataWithNewInputAmount; - - // if bridge is across, update the output amount based on pctDiff of the new balance - if (useBridge === 'across') { - // decode current input & output amounts - const { inputAmountBigNumber, outputAmountBigNumber } = - decodeAmountsBungeeTxData(encodedFunctionData, useBridge); - console.log('šŸ”— Socket input & output amounts:', { - inputAmountBigNumber: inputAmountBigNumber.toString(), - outputAmountBigNumber: outputAmountBigNumber.toString(), - }); - - // new input amount - const newInputAmount = sourceAmountIncludingSurplusBytes; - - // weiroll: increase output amount by pctDiff - const newOutputAmount = planner.add( - BungeeCowswapLibContract.addPctDiff( - inputAmountBigNumber, // base - newInputAmount, // compare - outputAmountBigNumber // target - ).rawValue() + + const bridgeDepositCall = createWeirollDelegateCall((planner) => { + // Get bridged amount (balance of the intermediate token at swap time) + const sourceAmountIncludingSurplusBytes = planner.add( + bridgedTokenContract.balanceOf(cowShedAccount).rawValue() + ); + + if (setAllowance) { + planner.add( + approvalTokenContract.approve(allowanceTarget, allowanceToSet) + ); + } + + const BungeeCowswapLibContractAddress = + BungeeCowswapLibAddresses[sourceChain]; + if (!BungeeCowswapLibContractAddress) { + throw new Error('BungeeCowswapLib contract not found'); + } + const BungeeCowswapLibContract = createWeirollContract( + new ethers.Contract(BungeeCowswapLibContractAddress, bungeeCowswapLibAbi), + WeirollCommandFlags.CALL ); - // weiroll: replace output amount bytes with newOutputAmount - const encodedFunctionDataWithNewInputAndOutputAmount = planner.add( + + // weiroll: replace input amount with new input amount + const encodedFunctionDataWithNewInputAmount = planner.add( BungeeCowswapLibContract.replaceBytes( - finalEncodedFunctionData, - BungeeTxDataIndices[useBridge].outputAmountBytes_startIndex!, - BungeeTxDataIndices[useBridge].outputAmountBytes_length!, - newOutputAmount + encodedFunctionData, + BungeeTxDataIndices[useBridge].inputAmountBytes_startIndex, + BungeeTxDataIndices[useBridge].inputAmountBytes_length, + sourceAmountIncludingSurplusBytes ) ); - finalEncodedFunctionData = encodedFunctionDataWithNewInputAndOutputAmount; - } - - const socketGatewayContract = WeirollContract.createContract( - new ethers.Contract(txData.result.txTarget, socketGatewayAbi), - CommandFlags.CALL - ); - // Call executeRoute on SocketGateway - planner.add( - socketGatewayContract.executeRoute(routeId, finalEncodedFunctionData) - ); + let finalEncodedFunctionData = encodedFunctionDataWithNewInputAmount; + + // if bridge is across, update the output amount based on pctDiff of the new balance + if (useBridge === 'across') { + // decode current input & output amounts + const { inputAmountBigNumber, outputAmountBigNumber } = + decodeAmountsBungeeTxData(encodedFunctionData, useBridge); + console.log('šŸ”— Socket input & output amounts:', { + inputAmountBigNumber: inputAmountBigNumber.toString(), + outputAmountBigNumber: outputAmountBigNumber.toString(), + }); + + // new input amount + const newInputAmount = sourceAmountIncludingSurplusBytes; + + // weiroll: increase output amount by pctDiff + const newOutputAmount = planner.add( + BungeeCowswapLibContract.addPctDiff( + inputAmountBigNumber, // base + newInputAmount, // compare + outputAmountBigNumber // target + ).rawValue() + ); + // weiroll: replace output amount bytes with newOutputAmount + const encodedFunctionDataWithNewInputAndOutputAmount = planner.add( + BungeeCowswapLibContract.replaceBytes( + finalEncodedFunctionData, + BungeeTxDataIndices[useBridge].outputAmountBytes_startIndex!, + BungeeTxDataIndices[useBridge].outputAmountBytes_length!, + newOutputAmount + ) + ); + finalEncodedFunctionData = encodedFunctionDataWithNewInputAndOutputAmount; + } + + const socketGatewayContract = createWeirollContract( + new ethers.Contract(txData.result.txTarget, socketGatewayAbi), + WeirollCommandFlags.CALL + ); + // Call executeRoute on SocketGateway + planner.add( + socketGatewayContract.executeRoute(routeId, finalEncodedFunctionData) + ); + }); // Return the transaction - return getWeirollTx({ planner }); + return bridgeDepositCall; } diff --git a/src/index.ts b/src/index.ts index 0216b93..90fe271 100644 --- a/src/index.ts +++ b/src/index.ts @@ -78,8 +78,8 @@ const JOBS: (() => Promise)[] = [ // getTradingQuote, // swapAndBridgeBungeeCCTPArbitrumBase, - // swapAndBridgeBungeeAcrossArbitrumBase, - getBungeeBridgeStatus, + swapAndBridgeBungeeAcrossArbitrumBase, + // getBungeeBridgeStatus, ]; async function main() { diff --git a/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts b/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts index 508002a..b7943ae 100644 --- a/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts +++ b/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts @@ -10,7 +10,7 @@ import { import { ethers } from 'ethers'; import { MetadataApi } from '@cowprotocol/app-data'; -import { createCowShedTx, getCowShedHooks } from '../../contracts/cowShed'; +import { createCowShedTx, getCowShedAccount } from '../../contracts/cowShed'; import { confirm, getWallet, jsonReplacer } from '../../utils'; import { getErc20Contract } from '../../contracts/erc20'; @@ -63,8 +63,7 @@ export async function run() { const intermediateTokenSymbol = await intermediateTokenContract.symbol(); // Estimate how many intermediate tokens we can bridge - const cowShedHooks = getCowShedHooks(sourceChain); - const cowShedAccount = cowShedHooks.proxyOf(wallet.address); + const cowShedAccount = getCowShedAccount(sourceChain, wallet.address); let quote = await sdk.getQuote({ kind: OrderKind.SELL, amount: sellAmount, @@ -101,7 +100,7 @@ export async function run() { // Sign and encode the transaction const { preAuthenticatedTx: authenticatedBridgeTx, gasLimit } = await createCowShedTx({ - tx: bridgeWithBungeeTx, + call: bridgeWithBungeeTx, chainId: sourceChain, wallet, }); @@ -125,7 +124,7 @@ export async function run() { hooks: { post: [ { - callData: authenticatedBridgeTx.callData, + callData: authenticatedBridgeTx.data, gasLimit: gasLimit.toString(), target: authenticatedBridgeTx.to, dappId: 'bridge-socket', diff --git a/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts b/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts index 301bd34..c283c3d 100644 --- a/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts +++ b/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts @@ -10,7 +10,7 @@ import { import { ethers } from 'ethers'; import { MetadataApi } from '@cowprotocol/app-data'; -import { createCowShedTx, getCowShedHooks } from '../../contracts/cowShed'; +import { createCowShedTx, getCowShedAccount } from '../../contracts/cowShed'; import { confirm, getWallet, jsonReplacer } from '../../utils'; import { getErc20Contract } from '../../contracts/erc20'; @@ -45,7 +45,7 @@ export async function run() { const wallet = await getWallet(sourceChain); const walletAddress = await wallet.getAddress(); console.log('šŸ”‘ Wallet address:', walletAddress); - + const sellToken = arbitrum.USDT_ADDRESS; const sellTokenDecimals = await getErc20Contract( sellToken, @@ -80,8 +80,7 @@ export async function run() { const intermediateTokenSymbol = await intermediateTokenContract.symbol(); // Estimate how many intermediate tokens we can bridge - const cowShedHooks = getCowShedHooks(sourceChain); - const cowShedAccount = cowShedHooks.proxyOf(wallet.address); + const cowShedAccount = getCowShedAccount(sourceChain, wallet.address); let quote = await sdk.getQuote({ kind: OrderKind.SELL, amount: sellAmount, @@ -117,10 +116,10 @@ export async function run() { // Sign and encode the transaction const { preAuthenticatedTx: authenticatedBridgeTx, gasLimit } = await createCowShedTx({ - tx: bridgeWithBungeeTx, - chainId: sourceChain, - wallet, - }); + call: bridgeWithBungeeTx, + chainId: sourceChain, + wallet, + }); // Define trade parameters. Sell sell token for intermediary token, to be received by cow-shed const parameters: TradeParameters = { @@ -141,7 +140,7 @@ export async function run() { hooks: { post: [ { - callData: authenticatedBridgeTx.callData, + callData: authenticatedBridgeTx.data, gasLimit: gasLimit.toString(), target: authenticatedBridgeTx.to, dappId: 'bridge-socket', @@ -230,5 +229,10 @@ export async function run() { `ā„¹ļø Order created, id: https://explorer.cow.fi/orders/${orderId}?tab=overview` ); + // Print socketscan link + console.log( + `šŸ” After filled on CoWSwap, you can watch bridge status on Socketscan using the order id: https://www.socketscan.io/tx/${orderId}` + ); + console.log(`šŸŽ‰ The USDC is now waiting for you in Base`); } From cc26cd14b285256f1f450c610fcecbe9c32da499 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Thu, 3 Apr 2025 19:16:17 +0530 Subject: [PATCH 16/17] chore: use USDC.e --- src/const/arbitrum.ts | 5 +++-- .../bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts | 2 +- src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/const/arbitrum.ts b/src/const/arbitrum.ts index 7e28c36..a4f5f3e 100644 --- a/src/const/arbitrum.ts +++ b/src/const/arbitrum.ts @@ -1,2 +1,3 @@ -export const USDC_ADDRESS = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"; -export const USDT_ADDRESS = "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"; +export const USDC_ADDRESS = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831'; +export const USDT_ADDRESS = '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9'; +export const USDCe_ADDRESS = '0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8'; diff --git a/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts b/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts index b7943ae..8c95ffe 100644 --- a/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts +++ b/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts @@ -29,7 +29,7 @@ export async function run() { const walletAddress = await wallet.getAddress(); console.log('šŸ”‘ Wallet address:', walletAddress); - const sellToken = arbitrum.USDT_ADDRESS; + const sellToken = arbitrum.USDCe_ADDRESS; const sellTokenDecimals = await getErc20Contract( sellToken, wallet diff --git a/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts b/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts index c283c3d..39f5842 100644 --- a/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts +++ b/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts @@ -46,7 +46,7 @@ export async function run() { const walletAddress = await wallet.getAddress(); console.log('šŸ”‘ Wallet address:', walletAddress); - const sellToken = arbitrum.USDT_ADDRESS; + const sellToken = arbitrum.USDCe_ADDRESS; const sellTokenDecimals = await getErc20Contract( sellToken, wallet From 24a79b0c1b66e53f3cac1525d0362f17fb5578fc Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Thu, 3 Apr 2025 19:26:13 +0530 Subject: [PATCH 17/17] refactor: should apply any pct diff --- src/contracts/socket/index.ts | 2 +- src/contracts/socket/types.ts | 58 +++++++++++++++++++++++++++++++++++ src/contracts/socket/utils.ts | 2 +- 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/contracts/socket/index.ts b/src/contracts/socket/index.ts index 70693bf..69f54ce 100644 --- a/src/contracts/socket/index.ts +++ b/src/contracts/socket/index.ts @@ -201,7 +201,7 @@ export async function bridgeWithBungee( // weiroll: increase output amount by pctDiff const newOutputAmount = planner.add( - BungeeCowswapLibContract.addPctDiff( + BungeeCowswapLibContract.applyPctDiff( inputAmountBigNumber, // base newInputAmount, // compare outputAmountBigNumber // target diff --git a/src/contracts/socket/types.ts b/src/contracts/socket/types.ts index b6b0849..d23a885 100644 --- a/src/contracts/socket/types.ts +++ b/src/contracts/socket/types.ts @@ -244,6 +244,35 @@ export const bungeeCowswapLibAbi = [ stateMutability: 'pure', type: 'function', }, + { + inputs: [ + { + internalType: 'uint256', + name: '_base', + type: 'uint256', + }, + { + internalType: 'bytes', + name: '_compare', + type: 'bytes', + }, + { + internalType: 'uint256', + name: '_target', + type: 'uint256', + }, + ], + name: 'applyPctDiff', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'pure', + type: 'function', + }, { inputs: [ { @@ -278,6 +307,35 @@ export const bungeeCowswapLibAbi = [ stateMutability: 'pure', type: 'function', }, + { + inputs: [ + { + internalType: 'uint256', + name: '_base', + type: 'uint256', + }, + { + internalType: 'bytes', + name: '_compare', + type: 'bytes', + }, + { + internalType: 'uint256', + name: '_target', + type: 'uint256', + }, + ], + name: 'subPctDiff', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'pure', + type: 'function', + }, ]; export const socketGatewayAbi = [ diff --git a/src/contracts/socket/utils.ts b/src/contracts/socket/utils.ts index cd77892..3b78a9f 100644 --- a/src/contracts/socket/utils.ts +++ b/src/contracts/socket/utils.ts @@ -55,7 +55,7 @@ export const BungeeCowswapLibAddresses: Record< > = { [SupportedChainId.MAINNET]: undefined, [SupportedChainId.GNOSIS_CHAIN]: undefined, - [SupportedChainId.ARBITRUM_ONE]: '0xAeE8bC0284d795D7662608dD765C8b5F1C6250CD', + [SupportedChainId.ARBITRUM_ONE]: '0x73eb30778f7e3958bfd974d10c0be559c2c65e22', [SupportedChainId.BASE]: undefined, [SupportedChainId.SEPOLIA]: undefined, };