-
Notifications
You must be signed in to change notification settings - Fork 2
feat: socket cctp, across scripts #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 14 commits
7f46ecb
51acacf
9a1d85d
e6acf55
b16b3ff
4aa6d66
f797030
0518773
ac20b9b
a9371ef
4ff5128
10eb739
2aac8fb
3436c8f
0456f3f
76e2cb7
cc26cd1
24a79b0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| export const WETH_ADDRESS = "0x4200000000000000000000000000000000000006"; | ||
| export const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,225 @@ | ||
| 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 { 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<BaseTransaction> { | ||
| 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 = 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 BungeeCowswapLibContractAddress = | ||
| BungeeCowswapLibAddresses[sourceChain]; | ||
| if (!BungeeCowswapLibContractAddress) { | ||
| throw new Error('BungeeCowswapLib contract not found'); | ||
| } | ||
| const BungeeCowswapLibContract = WeirollContract.createContract( | ||
| new ethers.Contract(BungeeCowswapLibContractAddress, bungeeCowswapLibAbi), | ||
| CommandFlags.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.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( | ||
| new ethers.Contract(txData.result.txTarget, socketGatewayAbi), | ||
| CommandFlags.CALL | ||
| ); | ||
| // Call executeRoute on SocketGateway | ||
| planner.add( | ||
| socketGatewayContract.executeRoute(routeId, finalEncodedFunctionData) | ||
| ); | ||
|
|
||
| // Return the transaction | ||
| return getWeirollTx({ planner }); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will we be able to verify this data to guarantee we don't ask users to blind-sign on something returned from the API?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We've added a calldata verification step in our PoC script
Basically we have a verifier contract which you could input the encoded calldata received from our backend and the expected values for key params like the input amount, recipient, destination chain id, token contract etc. and the verifier will validate if the encoded calldata does match the expected values.
You'll find it implemented in this commit: b16b3ff
These are the relevant verified contracts:
SocketVerifier: https://arbiscan.io/address/0x69D9f76e4cbE81044FE16C399387b12e4DBF27B1#code
AcrossV3Verification: https://arbiscan.io/address/0x2493Ac43A301d0217abAD6Ff320C5234aEe0931d#code
CCTPVerification: https://arbiscan.io/address/0xF58Db19f264359D6687b5693ee516bf316BE3Ba6#code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let us know if this helps the verifiability of the bridge txn calldata
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's great, thank you!
BTW, there's a typo in this contract function:
validateRotueIdBetter to fix it while it still in a single chain IMO :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually
SocketVerifieris already deployed on a few chains like arbitrum, optimism, polygon at the same address for a few other bridges and actively used for similar purposesThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I see! I guess I'll have to fight with my OCD and accept
rotuethen 😅