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/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..69f54ce --- /dev/null +++ b/src/contracts/socket/index.ts @@ -0,0 +1,234 @@ +import { + createWeirollContract, + createWeirollDelegateCall, + EvmCall, + SupportedChainId, + WeirollCommandFlags, +} from '@cowprotocol/cow-sdk'; +import { Planner as WeirollPlanner } from '@weiroll/weiroll.js'; +import { ethers } from 'ethers'; +import { getWallet } from '../../utils'; +import { getCowShedAccount } from '../cowShed'; +import { getErc20Contract } from '../erc20'; +import { bungeeCowswapLibAbi, socketGatewayAbi, SocketRequest } from './types'; +import { + BungeeCowswapLibAddresses, + BungeeTxDataIndices, + decodeAmountsBungeeTxData, + decodeBungeeTxData, + getBungeeQuote, + getBungeeRouteTransactionData, + socketBridgeFunctionSignatures, + verifyBungeeTxData, +} from './utils'; + +export interface BridgeWithBungeeParams { + owner: string; + sourceChain: SupportedChainId; + sourceToken: string; + sourceTokenAmount: bigint; + targetToken: string; + targetChain: number; + recipient: string; + useBridge: 'cctp' | 'across'; +} + +export async function bridgeWithBungee( + params: BridgeWithBungeeParams +): Promise { + const { + owner, + sourceChain, + sourceToken, + sourceTokenAmount, + targetChain, + targetToken, + recipient, + useBridge, + } = 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: [useBridge], + }); + if (!quote) { + throw new Error('No quote found'); + } + console.log('šŸ”— Socket quote:', quote.result.routes); + // 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 + ); + 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 = { + amount: route.fromAmount, + recipient: route.recipient, + toChainId: targetChain.toString(), + token: sourceToken, + signature: socketBridgeFunctionSignatures[useBridge], + }; + await verifyBungeeTxData( + sourceChain, + txData.result.txData, + routeId, + expectedSocketRequest + ); + + // Create bridged token contract + const bridgedTokenContract = createWeirollContract( + getErc20Contract(sourceToken), + WeirollCommandFlags.CALL + ); + + // Get balance of CoW shed proxy + console.log( + `[socket] Get cow-shed balance for ERC20.balanceOf(${cowShedAccount}) for ${bridgedTokenContract.address}` + ); + + // Check & set allowance for SocketGateway to transfer bridged tokens + // check if allowance is sufficient + let setAllowance = false; + 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) { + setAllowance = true; + } + + // set allowance + const approvalTokenContract = createWeirollContract( + getErc20Contract(approvalTokenAddress), + WeirollCommandFlags.CALL + ); + + const allowanceToSet = ethers.utils.parseUnits( + '1000', + await intermediateTokenContract.decimals() + ); + + 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 input amount with new input amount + const encodedFunctionDataWithNewInputAmount = planner.add( + BungeeCowswapLibContract.replaceBytes( + encodedFunctionData, + 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') { + // 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.applyPctDiff( + 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 bridgeDepositCall; +} diff --git a/src/contracts/socket/types.ts b/src/contracts/socket/types.ts new file mode 100644 index 0000000..d23a885 --- /dev/null +++ b/src/contracts/socket/types.ts @@ -0,0 +1,910 @@ +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 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: [ + { + 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: [ + { + 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', + }, + { + 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 = [ + { + 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' }, +]; + +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 new file mode 100644 index 0000000..3b78a9f --- /dev/null +++ b/src/contracts/socket/utils.ts @@ -0,0 +1,248 @@ +import { SupportedChainId } from '@cowprotocol/cow-sdk'; +import axios, { isAxiosError } from 'axios'; +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 +> = { + /** + * #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, +}; + +// TODO: deploy BungeeCowswapLib contracts for all chains +export const BungeeCowswapLibAddresses: Record< + SupportedChainId, + string | undefined +> = { + [SupportedChainId.MAINNET]: undefined, + [SupportedChainId.GNOSIS_CHAIN]: undefined, + [SupportedChainId.ARBITRUM_ONE]: '0x73eb30778f7e3958bfd974d10c0be559c2c65e22', + [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/ + */ +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 }; +}; + +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, + 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.callStatic.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.callStatic.validateSocketRequest( + txData, + expectedUserRequestValidation + ); + } catch (error) { + console.error('šŸ”“ Error validating socket request:', error); + throw error; + } +}; diff --git a/src/index.ts b/src/index.ts index 9f06fb2..90fe271 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,36 +1,39 @@ -import dotenv from "dotenv"; +import dotenv from 'dotenv'; -import { run as approveTokenSepolia } from "./scripts/sepolia/approveTokenSepolia"; +import { run as approveTokenSepolia } from './scripts/sepolia/approveTokenSepolia'; -import { run as swapWithPk } from "./scripts/sepolia/swapWithPk"; -import { run as swapBuy } from "./scripts/sepolia/swapBuy"; -import { run as swapSell } from "./scripts/sepolia/swapSell"; -import { run as swapWithReceiver } from "./scripts/sepolia/swapWithReceiver"; -import { run as swapPartialFill } from "./scripts/sepolia/swapPartialFill"; -import { run as swapWithPartnerFee } from "./scripts/sepolia/swapWithPartnerFee"; -import { run as swapInBarn } from "./scripts/sepolia/swapInBarn"; -import { run as getQuoteAndPostOrder } from "./scripts/sepolia/getQuoteAndPostOrder"; -import { run as swapSellWithValidFor } from "./scripts/sepolia/swapSellWithValidFor"; -import { run as getQuoteAndPreSign } from "./scripts/sepolia/getQuoteAndPreSign"; -import { run as preSign } from "./scripts/sepolia/presign"; -import { run as swapSellNative } from "./scripts/sepolia/swapSellNative"; -import { run as limitSell } from "./scripts/sepolia/limitSell"; -import { run as nativeSell } from "./scripts/sepolia/nativeSell"; -import { run as swapWithAppData } from "./scripts/sepolia/swapWithAppData"; -import { run as swapAndBridgeUsingOmnibridge } from "./scripts/bridging/swapAndBridgeUsingOmnibridge"; -import { run as swapAndBridgeUsingXdaiBridge } from "./scripts/bridging/swapAndBridgeUsingXdaiBridge"; -import { run as swapSellWithSlippageTolerance } from "./scripts/sepolia/swapSellWithSlippageTolerance"; +import { run as swapWithPk } from './scripts/sepolia/swapWithPk'; +import { run as swapBuy } from './scripts/sepolia/swapBuy'; +import { run as swapSell } from './scripts/sepolia/swapSell'; +import { run as swapWithReceiver } from './scripts/sepolia/swapWithReceiver'; +import { run as swapPartialFill } from './scripts/sepolia/swapPartialFill'; +import { run as swapWithPartnerFee } from './scripts/sepolia/swapWithPartnerFee'; +import { run as swapInBarn } from './scripts/sepolia/swapInBarn'; +import { run as getQuoteAndPostOrder } from './scripts/sepolia/getQuoteAndPostOrder'; +import { run as swapSellWithValidFor } from './scripts/sepolia/swapSellWithValidFor'; +import { run as getQuoteAndPreSign } from './scripts/sepolia/getQuoteAndPreSign'; +import { run as preSign } from './scripts/sepolia/presign'; +import { run as swapSellNative } from './scripts/sepolia/swapSellNative'; +import { run as limitSell } from './scripts/sepolia/limitSell'; +import { run as nativeSell } from './scripts/sepolia/nativeSell'; +import { run as swapWithAppData } from './scripts/sepolia/swapWithAppData'; +import { run as swapAndBridgeUsingOmnibridge } from './scripts/bridging/swapAndBridgeUsingOmnibridge'; +import { run as swapAndBridgeUsingXdaiBridge } from './scripts/bridging/swapAndBridgeUsingXdaiBridge'; +import { run as swapSellWithSlippageTolerance } from './scripts/sepolia/swapSellWithSlippageTolerance'; -import { run as approveTokenMainnet } from "./scripts/mainnet/approveTokenMainnet"; -import { run as approveTokenGnosis } from "./scripts/gnosis/approveTokenGnosis"; -import { run as swapAndBridgeSwapsIo } from "./scripts/bridging/swapAndBridgeSwapsIO"; -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 swapAndBridgeSdk } from "./scripts/bridging/swapAndBridgeSdk"; -import { run as getOrderbookQuote } from "./orderbook/getQuote"; -import { run as getOrderbookQuoteWithAppData } from "./orderbook/getQuoteWithAppData"; -import { run as getTradingQuote } from "./scripts/trading/getQuote"; +import { run as approveTokenMainnet } from './scripts/mainnet/approveTokenMainnet'; +import { run as approveTokenGnosis } from './scripts/gnosis/approveTokenGnosis'; +import { run as swapAndBridgeSwapsIo } from './scripts/bridging/swapAndBridgeSwapsIO'; +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'; +import { run as getBungeeBridgeStatus } from './scripts/bridging/getBungeeBridgeStatus'; +import { run as swapAndBridgeSdk } from './scripts/bridging/swapAndBridgeSdk'; +import { run as getOrderbookQuote } from './orderbook/getQuote'; +import { run as getOrderbookQuoteWithAppData } from './orderbook/getQuoteWithAppData'; +import { run as getTradingQuote } from './scripts/trading/getQuote'; dotenv.config(); // Just to dev things easily using watch-mode :) @@ -69,15 +72,19 @@ const JOBS: (() => Promise)[] = [ // swapAndBridgeAccrossArbitrum, // swapAndBridgeAccrossMainnet, - swapAndBridgeSdk, + // swapAndBridgeSdk, // getOrderbookQuote, // getOrderbookQuoteWithAppData, // getTradingQuote, + + // swapAndBridgeBungeeCCTPArbitrumBase, + swapAndBridgeBungeeAcrossArbitrumBase, + // getBungeeBridgeStatus, ]; async function main() { if (!JOBS.length) { - console.log("šŸ¤·ā€ā™€ļø No jobs to run"); + console.log('šŸ¤·ā€ā™€ļø No jobs to run'); return; } @@ -85,7 +92,7 @@ async function main() { await job(); } - console.log("šŸ‘Œ All jobs done\n\n"); + console.log('šŸ‘Œ All jobs done\n\n'); } main().catch(console.error); 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 new file mode 100644 index 0000000..8c95ffe --- /dev/null +++ b/src/scripts/bridging/swapAndBridgeBungeeAcrossArbitrumBase.ts @@ -0,0 +1,222 @@ +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, getCowShedAccount } 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 wallet = await getWallet(sourceChain); + const walletAddress = await wallet.getAddress(); + console.log('šŸ”‘ Wallet address:', walletAddress); + + const sellToken = arbitrum.USDCe_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, + await getWallet(targetChain) + ).decimals(); + const buyTokenSymbol = await getErc20Contract( + buyToken, + await getWallet(targetChain) + ).symbol(); + + // 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 + const cowShedAccount = getCowShedAccount(sourceChain, wallet.address); + let quote = await sdk.getQuote({ + kind: OrderKind.SELL, + amount: sellAmount, + sellToken, + sellTokenDecimals, + buyToken: intermediaryToken, + buyTokenDecimals: intermediateTokenDecimals, + partiallyFillable: false, // Fill or Kill + receiver: cowShedAccount, + }); + const intermediateTokenAmount = + quote.quoteResults.amountsAndCosts.afterSlippage.buyAmount; + console.log('intermediateTokenAmount', intermediateTokenAmount.toString()); + + // 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, + useBridge: 'across', + }); + + console.log( + '\nšŸ’° Bridge tx:', + JSON.stringify(bridgeWithBungeeTx, jsonReplacer, 2) + ); + + // Sign and encode the transaction + const { preAuthenticatedTx: authenticatedBridgeTx, gasLimit } = + await createCowShedTx({ + call: 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, + 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.data, + 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 firstQuote = intermediateTokenAmount.toString(); + const secondQuote = minIntermediateTokenAmount.toString(); + console.log('', { firstQuote, secondQuote }); + + const minIntermediateTokenAmountFormatted = ethers.utils.formatUnits( + minIntermediateTokenAmount, + intermediateTokenDecimals + ); + const sellAmountFormatted = ethers.utils.formatUnits( + sellAmount, + sellTokenDecimals + ); + + console.log( + `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( + `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` + ); + + // 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`); +} diff --git a/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts b/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts new file mode 100644 index 0000000..39f5842 --- /dev/null +++ b/src/scripts/bridging/swapAndBridgeBungeeCCTPArbitrumBase.ts @@ -0,0 +1,238 @@ +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, getCowShedAccount } from '../../contracts/cowShed'; +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, + * then bridge USDC to Base using Socket CCTP + */ + const sourceChain = SupportedChainId.ARBITRUM_ONE; + const targetChain = SupportedChainId.BASE; + + const wallet = await getWallet(sourceChain); + const walletAddress = await wallet.getAddress(); + console.log('šŸ”‘ Wallet address:', walletAddress); + + const sellToken = arbitrum.USDCe_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, + await getWallet(targetChain) + ).decimals(); + const buyTokenSymbol = await getErc20Contract( + buyToken, + await getWallet(targetChain) + ).symbol(); + + // 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 + const cowShedAccount = getCowShedAccount(sourceChain, wallet.address); + let quote = await sdk.getQuote({ + kind: OrderKind.SELL, + amount: sellAmount, + sellToken, + sellTokenDecimals, + buyToken: intermediaryToken, + buyTokenDecimals: intermediateTokenDecimals, + partiallyFillable: false, // Fill or Kill + receiver: cowShedAccount, + }); + 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, + useBridge: 'cctp', + }); + + console.log( + '\nšŸ’° Bridge tx:', + JSON.stringify(bridgeWithBungeeTx, jsonReplacer, 2) + ); + + // Sign and encode the transaction + const { preAuthenticatedTx: authenticatedBridgeTx, gasLimit } = + await createCowShedTx({ + call: 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, + 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.data, + 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 firstQuote = intermediateTokenAmount.toString(); + const secondQuote = minIntermediateTokenAmount.toString(); + console.log('', { firstQuote, secondQuote }); + + const minIntermediateTokenAmountFormatted = ethers.utils.formatUnits( + minIntermediateTokenAmount, + intermediateTokenDecimals + ); + const sellAmountFormatted = ethers.utils.formatUnits( + sellAmount, + sellTokenDecimals + ); + + console.log( + `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( + `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` + ); + + // 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`); +}