Skip to content
Open
Show file tree
Hide file tree
Changes from 14 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
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";
2 changes: 1 addition & 1 deletion src/contracts/cowShed/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
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
225 changes: 225 additions & 0 deletions src/contracts/socket/index.ts
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
);
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 = 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
Copy link
Owner

@anxolin anxolin Mar 21, 2025

Choose a reason for hiding this comment

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

How does this work?

We update the sell amount, but does it increase the buy amount too so we get more tokens?

Copy link
Author

Choose a reason for hiding this comment

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

We're not updating the sell amount here. We check the cowshed balance and use it to update the input amount for the bridge contract function call here.

Copy link
Author

Choose a reason for hiding this comment

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

Output amount on the destination chain depends on this bridge input amount

Copy link
Owner

Choose a reason for hiding this comment

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

Output amount on the destination chain depends on this bridge input amount

If we just modify the input amount, what guarantees the rate the user receive?

Copy link
Owner

Choose a reason for hiding this comment

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

If we show the user a quote where they can bridge 100 DAI in ethereum and receive 100 DAI in Arbitrum (1:1).
If we get 110 DAI instead, does the user have any guarantees they get at least 10 extra DAI in Arbutrum?

Copy link
Author

Choose a reason for hiding this comment

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

In case of CCTP:
CCTP will mint whatever amount of input token is burned on the source chain. And we're only cutting a small standard fee on source. So, since the burned amount includes swap surplus, the surplus will be included in the minted amount in dest chain

In case of Across:
We've pushed a few changes that also updates the outputAmount param for depositV3(). We calculate the % diff bw original input amount and the new input amount including surplus and apply the same % diff on the original outputAmount at runtime. This helps avoid any issues due to decimals or price bw the src and dest tokens
Commit: 10eb739

Copy link
Author

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