Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/const/arbitrum.ts
Original file line number Diff line number Diff line change
@@ -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';
1 change: 1 addition & 0 deletions src/const/base.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const WETH_ADDRESS = "0x4200000000000000000000000000000000000006";
export const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
1 change: 1 addition & 0 deletions src/contracts/erc20/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
234 changes: 234 additions & 0 deletions src/contracts/socket/index.ts
Original file line number Diff line number Diff line change
@@ -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<EvmCall> {
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
);
Copy link
Owner

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?

Copy link
Author

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

Copy link
Author

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

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: validateRotueId
Better to fix it while it still in a single chain IMO :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually SocketVerifier is already deployed on a few chains like arbitrum, optimism, polygon at the same address for a few other bridges and actively used for similar purposes

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 rotue then 😅

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;
}
Loading