Skip to content

Commit

Permalink
Merge pull request #44 from mmackz/hyphen-bridge
Browse files Browse the repository at this point in the history
feat(hyphen): add support for hyphen bridge
  • Loading branch information
Quazia authored Oct 24, 2023
2 parents 1857f6e + 87def8b commit 8ce97b5
Show file tree
Hide file tree
Showing 18 changed files with 885 additions and 1,355 deletions.
6 changes: 6 additions & 0 deletions .changeset/poor-ducks-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rabbitholegg/questdk-plugin-hyphen": minor
"@rabbitholegg/questdk-plugin-registry": minor
---

add bridge support for hyphen
Empty file added packages/hyphen/CHANGELOG.md
Empty file.
31 changes: 31 additions & 0 deletions packages/hyphen/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Hyphen Plugin

This plugin is designed to filter valid bridge transaction on the biconomy hyphen bridge

## Overview

The Hyphen Bridge is relatively simple compared to other bridges. The same function is used regardless of wether you are bridging to or from L1. There is 3 functions we are watching, ```depositNative``` for native tokens like ETH (or MATIC for Polygon), ```depositErc20``` for supported ERC-20 tokens, and ```depositErc20AndSwap``` which will swap out some of the bridged tokens for some of the native gas token on the receiving chain.

There is also a ```depositNativeAndSwap``` function, but I cannot find any instances of it being used in the wild, so I have chosen to exclude it from the filter.

## Example Transactions

### depositNative
- [From L1 to L2](https://etherscan.io/tx/0x8658d84686792ff03e4749dcd08cd750ec00632965d423214381595f32673dea)
- [From L2 to L1](https://optimistic.etherscan.io/tx/0x39349b8bc309e3e861565b2a08efa6fb5bb1726713ba17ff166396c15147e625)


### depositErc20
- https://etherscan.io/tx/0xbb7a23d915fd2b7e2df1e5116a785210c48671b0db5b790659db7f922d2c18ca

### depositErc20AndSwap
- https://polygonscan.com/tx/0x826839c49ecb2e25e263ad2299ac444d8e0bc92d92f8934d326a4ecd7ea8bc39


## Bridge Contract Addresses
- Ethereum - https://etherscan.io/address/0x2a5c2568b10a0e826bfa892cf21ba7218310180b
- Polygon - https://polygonscan.com/address/0x2a5c2568b10a0e826bfa892cf21ba7218310180b
- Avalanche - https://snowtrace.io/address/0x2a5c2568b10a0e826bfa892cf21ba7218310180b
- Optimism - https://optimistic.etherscan.io/address/0x856cb5c3cBBe9e2E21293A644aA1f9363CEE11E8
- Arbitrum - https://arbiscan.io/address/0x856cb5c3cBBe9e2E21293A644aA1f9363CEE11E8
- Binance - https://bscscan.com/address/0x94D3E62151B12A12A4976F60EdC18459538FaF08
51 changes: 51 additions & 0 deletions packages/hyphen/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"name": "@rabbitholegg/questdk-plugin-hyphen",
"private": true,
"version": "1.0.0-alpha.4",
"type": "module",
"exports": {
"require": "./dist/cjs/index.js",
"import": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts"
},
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"packageManager": "pnpm@8.3.1",
"description": "",
"scripts": {
"bench": "vitest bench",
"bench:ci": "CI=true vitest bench",
"build": "pnpm run clean && pnpm run build:cjs && pnpm run build:esm && pnpm run build:types",
"build:cjs": "tsc --project tsconfig.build.json --module commonjs --outDir ./dist/cjs --removeComments --verbatimModuleSyntax false && echo > ./dist/cjs/package.json '{\"type\":\"commonjs\"}'",
"build:esm": "tsc --project tsconfig.build.json --module es2015 --outDir ./dist/esm && echo > ./dist/esm/package.json '{\"type\":\"module\",\"sideEffects\":false}'",
"build:types": "tsc --project tsconfig.build.json --module esnext --declarationDir ./dist/types --emitDeclarationOnly --declaration --declarationMap",
"clean": "rimraf dist",
"format": "rome format . --write",
"lint": "rome check .",
"lint:fix": "pnpm lint --apply",
"preinstall": "npx only-allow pnpm",
"test": "vitest dev",
"test:cov": "vitest dev --coverage",
"test:ci": "CI=true vitest --coverage",
"test:ui": "vitest dev --ui"
},
"keywords": [],
"author": "",
"license": "ISC",
"types": "./dist/types/index.d.ts",
"typings": "./dist/types/index.d.ts",
"devDependencies": {
"@types/node": "^20.4.5",
"@vitest/coverage-v8": "^0.33.0",
"rimraf": "^5.0.1",
"rome": "^12.1.3",
"ts-node": "^10.9.1",
"tsconfig": "workspace:*",
"typescript": "^5.1.6",
"vitest": "^0.33.0"
},
"dependencies": {
"@rabbitholegg/questdk": "1.0.1-alpha.10",
"viem": "^1.2.15"
}
}
114 changes: 114 additions & 0 deletions packages/hyphen/src/Hyphen.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { GreaterThanOrEqual, apply } from '@rabbitholegg/questdk/filter'
import { bridge } from './Hyphen'
import { describe, expect, test } from 'vitest'
import { ABI } from './abi'
import { CHAIN_TO_CONTRACT } from './chain-to-contract'
import { NATIVE_TOKEN_ADDRESS, CHAIN_TO_TOKENS } from './chain-to-tokens'
import {
ARBITRUM_CHAIN_ID,
BINANCE_CHAIN_ID,
ETH_CHAIN_ID,
OPTIMISM_CHAIN_ID,
POLYGON_CHAIN_ID,
} from './chain-ids'
import {
NATIVE_TRANSFER,
ERC20_TRANSFER,
ERC20_BRIDGE_SWAP,
} from './test-transactions'

describe('Given the Hyphen plugin', () => {
describe('When handling the bridge', () => {
const TEST_ADDRESS = '0x081F992BB28E1D32E138FFAB57AF8F8B932573B5'

test('should return a valid bridge action filter using native token', async () => {
// bridge ETH from Mainnet to Polygon
const sourceChainId = ETH_CHAIN_ID
const destinationChainId = POLYGON_CHAIN_ID

const filter = await bridge({
sourceChainId,
destinationChainId,
tokenAddress: NATIVE_TOKEN_ADDRESS,
recipient: TEST_ADDRESS,
amount: GreaterThanOrEqual(100000n),
})
expect(filter).to.deep.equal({
chainId: sourceChainId,
to: CHAIN_TO_CONTRACT[sourceChainId],
value: {
$gte: '100000',
},
input: {
$abi: ABI,
toChainId: destinationChainId,
receiver: TEST_ADDRESS,
},
})
})

test('should return a valid bridge action filter using erc-20 token', async () => {
// bridge erc-20 from Optimism to Polygon
const tokenAddress = CHAIN_TO_TOKENS[OPTIMISM_CHAIN_ID][0]
const sourceChainId = OPTIMISM_CHAIN_ID
const destinationChainId = POLYGON_CHAIN_ID

const filter = await bridge({
sourceChainId,
destinationChainId,
tokenAddress,
recipient: TEST_ADDRESS,
amount: GreaterThanOrEqual(100000n),
})
expect(filter).to.deep.equal({
chainId: sourceChainId,
to: CHAIN_TO_CONTRACT[sourceChainId],
input: {
$abi: ABI,
toChainId: destinationChainId,
receiver: TEST_ADDRESS,
tokenAddress,
amount: {
$gte: '100000',
},
},
})
})

test('should pass filter when bridging native token', async () => {
// Bridge ETH from Optimism to Arbitrum
const filter = await bridge({
sourceChainId: OPTIMISM_CHAIN_ID,
destinationChainId: ARBITRUM_CHAIN_ID,
tokenAddress: NATIVE_TOKEN_ADDRESS,
amount: GreaterThanOrEqual(100000n),
})

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

test('should pass filter when bridging erc20 token', async () => {
// Bridge USDT from Mainnet to Binance Chain
const filter = await bridge({
sourceChainId: ETH_CHAIN_ID,
destinationChainId: BINANCE_CHAIN_ID,
tokenAddress: CHAIN_TO_TOKENS[ETH_CHAIN_ID][0], // USDT
amount: GreaterThanOrEqual(100000n),
})

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

test('should pass filter when using bridge and swap function', async () => {
// Bridge USDC from Mainnet to Optimism
const filter = await bridge({
sourceChainId: ETH_CHAIN_ID,
destinationChainId: OPTIMISM_CHAIN_ID,
tokenAddress: CHAIN_TO_TOKENS[ETH_CHAIN_ID][1], // USDC
amount: GreaterThanOrEqual(100000n),
})

expect(apply(ERC20_BRIDGE_SWAP, filter)).to.be.true
})
})
})
65 changes: 65 additions & 0 deletions packages/hyphen/src/Hyphen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {
type BridgeActionParams,
type TransactionFilter,
compressJson,
} from '@rabbitholegg/questdk'
import { type Address } from 'viem'
import { ABI } from './abi.js'
import { NATIVE_TOKEN_ADDRESS, CHAIN_TO_TOKENS } from './chain-to-tokens'
import { CHAIN_TO_CONTRACT } from './chain-to-contract'
import { type ChainIds, CHAIN_ID_ARRAY } from './chain-ids'

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

const bridgeContract =
contractAddress ?? CHAIN_TO_CONTRACT[sourceChainId as ChainIds]

// if transfer is using the native gas token
if (tokenAddress === NATIVE_TOKEN_ADDRESS) {
return compressJson({
chainId: sourceChainId, // The chainId of the source chain
to: bridgeContract, // The contract address of the bridge
value: amount,
input: {
$abi: ABI,
toChainId: destinationChainId,
receiver: recipient,
},
})
}

// if transfer is for ERC-20 tokens
return compressJson({
chainId: sourceChainId,
to: bridgeContract,
input: {
$abi: ABI,
toChainId: destinationChainId,
tokenAddress,
receiver: recipient,
amount,
},
})
}

export const getSupportedTokenAddresses = async (
_chainId: number,
): Promise<Address[]> => {
// Given a specific chain we would expect this function to return a list of supported token addresses
return CHAIN_TO_TOKENS[_chainId as ChainIds] ?? []
}

export const getSupportedChainIds = async (): Promise<number[]> => {
// This should return all of the ChainIds that are supported by the Project we're integrating
return CHAIN_ID_ARRAY
}
57 changes: 57 additions & 0 deletions packages/hyphen/src/abi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { type Abi } from 'viem'

export const ABI: Abi = [
{
inputs: [
{ internalType: 'address', name: 'receiver', type: 'address' },
{ internalType: 'uint256', name: 'toChainId', type: 'uint256' },
{ internalType: 'string', name: 'tag', type: 'string' },
],
name: 'depositNative',
outputs: [],
stateMutability: 'payable',
type: 'function',
},
{
inputs: [
{ internalType: 'uint256', name: 'toChainId', type: 'uint256' },
{ internalType: 'address', name: 'tokenAddress', type: 'address' },
{ internalType: 'address', name: 'receiver', type: 'address' },
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
{ internalType: 'string', name: 'tag', type: 'string' },
],
name: 'depositErc20',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{ internalType: 'address', name: 'tokenAddress', type: 'address' },
{ internalType: 'address', name: 'receiver', type: 'address' },
{ internalType: 'uint256', name: 'toChainId', type: 'uint256' },
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
{ internalType: 'string', name: 'tag', type: 'string' },
{
components: [
{ internalType: 'address', name: 'tokenAddress', type: 'address' },
{ internalType: 'uint256', name: 'percentage', type: 'uint256' },
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
{
internalType: 'enum SwapOperation',
name: 'operation',
type: 'uint8',
},
{ internalType: 'bytes', name: 'path', type: 'bytes' },
],
internalType: 'struct SwapRequest[]',
name: 'swapRequest',
type: 'tuple[]',
},
],
name: 'depositAndSwapErc20',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
]
23 changes: 23 additions & 0 deletions packages/hyphen/src/chain-ids.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const ETH_CHAIN_ID = 1
export const OPTIMISM_CHAIN_ID = 10
export const BINANCE_CHAIN_ID = 56
export const POLYGON_CHAIN_ID = 137
export const ARBITRUM_CHAIN_ID = 42161
export const AVALANCHE_CHAIN_ID = 43114

export const CHAIN_ID_ARRAY = [
ETH_CHAIN_ID,
OPTIMISM_CHAIN_ID,
BINANCE_CHAIN_ID,
POLYGON_CHAIN_ID,
ARBITRUM_CHAIN_ID,
AVALANCHE_CHAIN_ID,
]

export type ChainIds =
| typeof ETH_CHAIN_ID
| typeof OPTIMISM_CHAIN_ID
| typeof BINANCE_CHAIN_ID
| typeof POLYGON_CHAIN_ID
| typeof ARBITRUM_CHAIN_ID
| typeof AVALANCHE_CHAIN_ID
26 changes: 26 additions & 0 deletions packages/hyphen/src/chain-to-contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Address } from 'viem'

import {
MAIN_SPOKE, // mainnet, polygon, avalanche
LAYER2_SPOKE, // arbitrum, optimism
BINANCE_SPOKE,
} from './contract-addresses.js'

import {
type ChainIds,
ETH_CHAIN_ID,
OPTIMISM_CHAIN_ID,
BINANCE_CHAIN_ID,
POLYGON_CHAIN_ID,
ARBITRUM_CHAIN_ID,
AVALANCHE_CHAIN_ID,
} from './chain-ids.js'

export const CHAIN_TO_CONTRACT: { [chainId in ChainIds]: Address } = {
[ETH_CHAIN_ID]: MAIN_SPOKE,
[OPTIMISM_CHAIN_ID]: LAYER2_SPOKE,
[BINANCE_CHAIN_ID]: BINANCE_SPOKE,
[POLYGON_CHAIN_ID]: MAIN_SPOKE,
[ARBITRUM_CHAIN_ID]: LAYER2_SPOKE,
[AVALANCHE_CHAIN_ID]: MAIN_SPOKE,
} as const
Loading

0 comments on commit 8ce97b5

Please sign in to comment.