Skip to content
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

feat(connext): add support for ETH transactions on "ANY TOKEN" quests #145

Merged
merged 19 commits into from
Dec 27, 2023
Merged
Show file tree
Hide file tree
Changes from 16 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: 5 additions & 0 deletions .changeset/shaggy-lamps-leave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rabbitholegg/questdk-plugin-connext": minor
---

combine filters for xcall and multisend
192 changes: 73 additions & 119 deletions packages/connext/src/Connext.test.ts
Original file line number Diff line number Diff line change
@@ -1,135 +1,89 @@
import { getDeployedMultisendContract } from '@connext/nxtp-txservice'
import { MultisendAbi } from '@connext/nxtp-utils'
import { GreaterThanOrEqual, apply } from '@rabbitholegg/questdk/filter'
import type { TransactionEIP1559 } from 'viem'
import { apply } from '@rabbitholegg/questdk/filter'
import { describe, expect, test } from 'vitest'
import { bridge } from './Connext.js'
import {
bridge,
getSupportedChainIds,
getSupportedTokenAddresses,
} from './Connext.js'
import { MultisendAbi } from '@connext/nxtp-utils'
import { XCALL_ABI_FRAGMENTS } from './abi.js'
import { passingTestCases, failingTestCases } from './test-transactions.js'

describe('Connext', () => {
const ETH = '0x0000000000000000000000000000000000000000'
const OP_WETH = '0x4200000000000000000000000000000000000006'

describe('Bridge', () => {
const USDC = '0x7F5c764cBc14f9669B88837ca1490cCa17c31607'

test('should return a valid bridge action filter', async () => {
const filter = await bridge({
sourceChainId: 10,
destinationChainId: 137,
tokenAddress: USDC,
amount: GreaterThanOrEqual(100000n),
})

expect(filter).to.deep.equal({
chainId: 10,
to: '0x8f7492DE823025b4CfaAB1D34c58963F2af5DEDA',
input: {
$abi: XCALL_ABI_FRAGMENTS,
_destination: 1886350457,
_asset: '0x7F5c764cBc14f9669B88837ca1490cCa17c31607',
_amount: {
$gte: '100000',
describe('Given the Connext plugin', () => {
describe('When handling the bridge action', () => {
describe('should return a valid action filter', () => {
test('when doing a valid bridge action', async () => {
const { params } = passingTestCases[0]
const filter = await bridge(params)
expect(filter).to.deep.equal({
chainId: 10,
to: {
$or: [
'0x8f7492de823025b4cfaab1d34c58963f2af5deda',
'0xb0eef3e1de973d045c3858e072c540299585252d',
],
},
from: '0xd59a74e615c9d55422ed8c5ce64cb50fda0bb58d',
input: {
$or: [
{
$abi: MultisendAbi,
transactions: {
$regex: 'd59a74e615c9d55422ed8c5ce64cb50fda0bb58d',
},
},
{
$abi: XCALL_ABI_FRAGMENTS,
_destination: 6778479,
_asset: '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1',
_amount: {
$gte: '2000000000000000000',
},
_delegate: '0xd59a74e615c9d55422ed8c5ce64cb50fda0bb58d',
},
],
},
},
})
})
})
})

test('should use the WETH wrapper multisend contract when bridging ETH', async () => {
const filter = await bridge({
sourceChainId: 10,
destinationChainId: 137,
tokenAddress: ETH,
amount: GreaterThanOrEqual(100000n),
})

const multiSendContract = getDeployedMultisendContract(10)

expect(filter).to.deep.equal({
chainId: 10,
to: multiSendContract?.address,
value: {
$gte: '100000',
},
input: {
$abi: MultisendAbi,
transactions: {
$regex: OP_WETH.slice(2),
},
},
describe('should pass filter with valid transactions', () => {
passingTestCases.forEach((testCase) => {
const { transaction, description, params } = testCase
test(description, async () => {
const filter = await bridge(params)
expect(apply(transaction, filter)).to.be.true
})
})
})
})

describe('Apply filter', () => {
test('transaction should pass filter', async () => {
const transaction = {
blockHash:
'0x80ca121924779dc65f575409be05e7c2cbaf718858e6e20d66d130cf1acec4f3',
blockNumber: '0x6606396',
from: '0xd59a74e615c9d55422ed8c5ce64cb50fda0bb58d',
gas: '0x73766',
gasPrice: '0xc9',
maxFeePerGas: '0x17a',
maxPriorityFeePerGas: '0x71',
hash: '0x22d3715ca5ae0bd0d87f9341fafc7a330fd6962e13bf318a6a541c93e4e6bc04',
input:
'0x93f18ac50000000000000000000000000000000000000000000000000000000000676e6f000000000000000000000000642c27a96dffb6f21443a89b789a3194ff8399fa000000000000000000000000da10009cbd5d07dd0cecc66161fc93d7c9000da1000000000000000000000000d59a74e615c9d55422ed8c5ce64cb50fda0bb58d000000000000000000000000000000000000000000000000204d764a78eac238000000000000000000000000000000000000000000000000000000000000012c000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000050b91c62117dc0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000d59a74e615c9d55422ed8c5ce64cb50fda0bb58d',
nonce: '0x3',
to: '0x8f7492de823025b4cfaab1d34c58963f2af5deda',
transactionIndex: '0x3',
value: '0x0',
type: '0x2',
accessList: [],
chainId: 10,
v: '0x0',
r: '0x4fcc9247c0b29a50b9db146191e93fbd503be520998f62c9445aa2eeb944d613',
s: '0x78e1ace158ae1166687db900743fc7db36cc80e42a43a800577895c5cb2b95b3',
}

const filter = await bridge({
sourceChainId: 10,
destinationChainId: 100, // xDAI Chain
tokenAddress: '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1',
amount: GreaterThanOrEqual(2000000000000000000n),
describe('should not pass filter with invalid transactions', () => {
failingTestCases.forEach((testCase) => {
const { transaction, description, params } = testCase
test(description, async () => {
const filter = await bridge(params)
expect(apply(transaction, filter)).to.be.false
})
})

expect(apply(transaction, filter)).to.be.true
})

test('ETH bridge should pass filter', async () => {
const transaction: TransactionEIP1559 = {
blockHash:
'0xfdb722e4a99e3422490bc12d15fafab54ebb7e2e83ff08e9fe20d70045e94889',
blockNumber: BigInt('0x67812f1'),
from: '0xa4c8bb4658bc44bac430699c8b7b13dab28e0f4e',
gas: BigInt('0x8820a'),
maxFeePerGas: BigInt('0x11e2'),
maxPriorityFeePerGas: BigInt('0x112d'),
hash: '0xb8e2c0baf137b64553c91f286bde62cc37275d0b9f9d3e6c0041c6be79de45af',
input:
'0x8d80ff0a0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000026b004200000000000000000000000000000000000006000000000000000000000000000000000000000000000000008af8a1fa5fcc180000000000000000000000000000000000000000000000000000000000000004d0e30db000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044095ea7b30000000000000000000000008f7492de823025b4cfaab1d34c58963f2af5deda000000000000000000000000000000000000000000000000008af8a1fa5fcc18008f7492de823025b4cfaab1d34c58963f2af5deda0000000000000000000000000000000000000000000000000026aa1a3465338400000000000000000000000000000000000000000000000000000000000001248aac16ba0000000000000000000000000000000000000000000000000000000000657468000000000000000000000000268682b7d9992ae7e2ca4a8bcc9d9655fb06056f0000000000000000000000004200000000000000000000000000000000000006000000000000000000000000a4c8bb4658bc44bac430699c8b7b13dab28e0f4e000000000000000000000000000000000000000000000000008af8a1fa5fcc18000000000000000000000000000000000000000000000000000000000000012c00000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000a4c8bb4658bc44bac430699c8b7b13dab28e0f4e000000000000000000000000000000000000000000',
nonce: 45,
to: '0xb0eef3e1de973d045c3858e072c540299585252d',
transactionIndex: 6,
value: BigInt('0xb1a2bc2ec4ff9c'),
type: 'eip1559',
accessList: [],
typeHex: '0x2',
chainId: 10,
v: BigInt('0x1'),
r: '0x11d6fd962cf4090c1464404492a36ef7323c6173908883c87fbc695219e6d026',
s: '0x739b7240f8dd466881a66971b476f07c2fcce9a6f519d3c06f95133ad983092e',
}

const filter = await bridge({
sourceChainId: 10,
destinationChainId: 137,
tokenAddress: ETH,
amount: GreaterThanOrEqual(1000000000000000n),
describe('should return a valid list of tokens for each supported chain', async () => {
const chainIdArray = await getSupportedChainIds()
chainIdArray.forEach((chainId) => {
test(`for chainId: ${chainId}`, async () => {
const tokens = await getSupportedTokenAddresses(chainId)
const addressRegex = /^0x[a-fA-F0-9]{40}$/
expect(tokens).to.be.an('array')
expect(tokens).to.have.length.greaterThan(0)
expect(tokens).to.have.length.lessThan(100)
tokens.forEach((token) => {
expect(token).to.match(
addressRegex,
`Token address ${token} is not a valid Ethereum address`,
)
})
})
})

expect(apply(transaction, filter)).to.be.true
})
})
})
112 changes: 43 additions & 69 deletions packages/connext/src/Connext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import {
type TransactionFilter,
compressJson,
} from '@rabbitholegg/questdk'
import { type Address } from 'viem'
import { type Address, zeroAddress } from 'viem'
import { XCALL_ABI_FRAGMENTS } from './abi.js'
import { ConnextContract } from './contract-addresses.js'

let _chainDataCache: Map<string, ChainData> | null = null

const ETH_TOKEN_ADDRESS = '0x0000000000000000000000000000000000000000'
const ETH_TOKEN_ADDRESS = zeroAddress

const _getChainData = async () => {
if (!_chainDataCache) {
Expand All @@ -28,82 +28,56 @@ const _getChainData = async () => {
return _chainDataCache
}

export const getWETHAddress = async (chainId: number) => {
const chains = await _getChainData()
const domainId = chainIdToDomain(chainId)
const chainData = chains?.get(String(domainId))
const assets = Object.keys(chainData?.assetId || {})

let wethAddress
for (const address of assets) {
if (chainData?.assetId[address].symbol === 'WETH') {
wethAddress = address
break
}
}
return wethAddress
}

export const bridge = async (
bridge: BridgeActionParams,
): Promise<TransactionFilter> => {
const {
sourceChainId,
destinationChainId,
contractAddress,
tokenAddress,
amount,
recipient,
} = bridge

const defaultContractAddress = ConnextContract[sourceChainId]
const destinationDomain = chainIdToDomain(destinationChainId)
const requiresWrapperMultisend = tokenAddress === ETH_TOKEN_ADDRESS

/*
Connext uses a MultiSend to wrap when briding ETH.
https://github.com/connext/monorepo/issues/2905
https://github.com/connext/monorepo/issues/4218
Contract addresses: https://github.com/search?q=repo%3Aconnext%2Fmonorepo%20MultiSend.json&type=code
*/

if (requiresWrapperMultisend) {
const multiSendContract = getDeployedMultisendContract(sourceChainId)

if (!multiSendContract) {
throw new Error(
`No multisend contract deployed on chain ${sourceChainId}`,
)
}

const wethAddress = await getWETHAddress(sourceChainId)

if (!wethAddress) {
throw new Error(`No WETH address found on chain ${sourceChainId}`)
}
const { sourceChainId, destinationChainId, tokenAddress, amount, recipient } =
bridge

const xcallContractAddress = ConnextContract[sourceChainId]
const destinationDomain = destinationChainId
? chainIdToDomain(destinationChainId)
: undefined
const multiSendContractAddress =
getDeployedMultisendContract(sourceChainId)?.address
const ethUsedIn = tokenAddress === ETH_TOKEN_ADDRESS

if (!xcallContractAddress) {
throw new Error(`No xcall contract deployed on chain ${sourceChainId}`)
}

return compressJson({
chainId: sourceChainId,
to: multiSendContract.address,
value: amount,
input: {
$abi: MultisendAbi,
transactions: {
$regex: wethAddress.slice(2),
},
},
})
if (!multiSendContractAddress) {
throw new Error(`No multisend contract deployed on chain ${sourceChainId}`)
}

return compressJson({
chainId: sourceChainId,
to: contractAddress || defaultContractAddress,
to: {
$or: [
xcallContractAddress.toLowerCase(),
multiSendContractAddress.toLowerCase(),
],
},
from: recipient,
value: ethUsedIn ? amount : undefined,
input: {
$abi: XCALL_ABI_FRAGMENTS,
_destination: Number(destinationDomain),
_asset: tokenAddress,
_amount: amount,
_to: recipient,
$or: [
{
$abi: MultisendAbi,
transactions: {
$regex: recipient?.toLowerCase().slice(2),
},
},
{
$abi: XCALL_ABI_FRAGMENTS,
_destination: destinationDomain
? Number(destinationDomain)
: undefined,
_asset: tokenAddress,
_amount: amount,
_delegate: recipient,
},
],
},
})
}
Expand Down
12 changes: 12 additions & 0 deletions packages/connext/src/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Connext Plugin

## Overview
Connext is a cross chain bridge protocol on Ethereum which services many popular L2 chains. The bridge transaction will use one of two method depending on wether ETH or tokens were bridged. For ETH, it will use `multiSend` and for tokens it will use `xCall`

## Limitations
- There should be no limitations with the current bridge plugin. Everything should work as expected.

## Sample Transactions
- [multiSend](https://etherscan.io/tx/0xb8e2c0baf137b64553c91f286bde62cc37275d0b9f9d3e6c0041c6be79de45af)
- [xCall](https://optimistic.etherscan.io/tx/0x22d3715ca5ae0bd0d87f9341fafc7a330fd6962e13bf318a6a541c93e4e6bc04)

Loading
Loading