Skip to content

Commit

Permalink
Merge pull request #13 from rabbitholegg/arbitrum_bridge
Browse files Browse the repository at this point in the history
Feat(arbitrum): Arbitrum bridge
  • Loading branch information
Quazia authored Sep 15, 2023
2 parents 1251738 + c549eb4 commit b89d302
Show file tree
Hide file tree
Showing 17 changed files with 1,107 additions and 465 deletions.
6 changes: 6 additions & 0 deletions .changeset/twelve-kiwis-destroy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rabbitholegg/questdk-plugin-arbitrum": minor
"@rabbitholegg/questdk-plugin-registry": minor
---

Add support for bridging on Arbitrum
Empty file added packages/arbitrum/CHANGELOG.md
Empty file.
45 changes: 45 additions & 0 deletions packages/arbitrum/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Arbitrum Plugin
This plugin allows for the decoding of Arbitrum transactions by way of action spec.

## General Overview
Arbitrum's native token bridge is a general messaging bridge allowing for transfer of ETH, and any token.

They support exchange to/from mainnet to their two main networks (One, and Nova).

Arbitrum uses different paths for ETH vs Tokens, and relies on precompiles when routing the base network currency (AEth) _from_ L2 _to_ L1.

For a given bridge action we generally have 4 types of transactions we want to ensure we're parsing:
1. ETH from L1 to L2
1. Tokens from L1 to L2
1. ETH from L2 to L1
1. Tokens from L2 to L1

In some cases there won't be a difference between L1/L2 leading to two types of transactions to parse, but in general this enumerates the upper bound of transactions a bridge action should be responsible for parsing. It's also possible for different tokens to route differently, this _would_ be the case with Arbitrum if they didn't pipe transactions through their router first.

## Specific Examples

[Token Transfers from L1 get routed through the L1 Gateway Router](https://etherscan.io/address/0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef)


[This is an example of an Outbound Transfer from the L1 Gateway Router](https://etherscan.io/tx/0xcdbcb66c6a194ae2f0a58b00c1e6ec0daea08c901590ba056cc6806581bf5a94
)

[This is the function call on the `L1GatewayRouter.sol` contract.](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/ethereum/gateway/L1GatewayRouter.sol#L229)

[Token transfers from L2 get routed through the L2 Gateway Router](https://arbiscan.io/address/0x5288c571Fd7aD117beA99bF60FE0846C4E84F933
)

[This is an example of an outbound transaction from the L2](https://arbiscan.io/tx/0xc98cb709c9f00e436911ce764fe7712fd0467f6e56ffc89b3a92a6fe35b5e069
)


[ETH transfer from L1 get routed through the Delayed Inbox using the Deposit ETH function](https://etherscan.io/address/0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f
)



[ETH transfers from the L2 use the ArbSys contract using the Withdraw ETH function](https://arbiscan.io/address/0x0000000000000000000000000000000000000064
)

[This is an example of an ETH withdrawl through the ArbSys contract](https://arbiscan.io/tx/0x6b2ed2676131d1a4bddef33dcf4575ef88fe34adafa77959899cdfd7cc0705b2
)
50 changes: 50 additions & 0 deletions packages/arbitrum/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "@rabbitholegg/questdk-plugin-arbitrum",
"version": "1.0.0-alpha.3",
"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.9",
"viem": "1.2.15"
}
}
96 changes: 96 additions & 0 deletions packages/arbitrum/src/Arbitrum.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { bridge } from './Arbitrum.js'
import { GATEWAY_OUTBOUND_TRANSFER_FRAG } from './abi.js'
import { GreaterThanOrEqual, apply } from '@rabbitholegg/questdk/filter'
import { describe, expect, test } from 'vitest'
import { ETH_CHAIN_ID, ARB_ONE_CHAIN_ID } from './chain-ids'
import { MAINNET_TO_ARB_ONE_GATEWAY } from './contract-addresses'
import { parseEther } from 'viem'
import { DEPOSIT_ERC20 } from './test-transactions.js'
describe('Given the arbitrum plugin', () => {
describe('When handling the bridge', () => {
const DAI = '0x6b175474e89094c44da98b954eedeac495271d0f'
const recipient = '0x8f7492DE823025b4CfaAB1D34c58963F2af5DEDA'

test('should return a valid action filter', async () => {
const filter = await bridge({
sourceChainId: ETH_CHAIN_ID,
destinationChainId: ARB_ONE_CHAIN_ID,
tokenAddress: DAI,
recipient: recipient,
amount: GreaterThanOrEqual(100000n),
})
expect(filter).to.deep.equal({
chainId: ETH_CHAIN_ID,
to: MAINNET_TO_ARB_ONE_GATEWAY,
input: {
$abi: GATEWAY_OUTBOUND_TRANSFER_FRAG,
_token: DAI,
_to: recipient,
_amount: {
$gte: '100000',
},
},
})
})

test('should pass filter with valid transactions', async () => {
const recipient = '0xf9ce182b0fbe597773ab9bb5159b7479047de8fe'

const transaction = DEPOSIT_ERC20

const filter = await bridge({
sourceChainId: ETH_CHAIN_ID,
destinationChainId: ARB_ONE_CHAIN_ID,
tokenAddress: '0x514910771AF9Ca656af840dff83E8264EcF986CA', // LINK
amount: GreaterThanOrEqual(parseEther('2000')),
recipient: recipient,
})

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

test('should not pass filter with invalid amount', async () => {
const recipient = '0xf9ce182b0fbe597773ab9bb5159b7479047de8fe'
const transaction = DEPOSIT_ERC20

const filter = await bridge({
sourceChainId: ETH_CHAIN_ID,
destinationChainId: ARB_ONE_CHAIN_ID,
tokenAddress: '0x514910771AF9Ca656af840dff83E8264EcF986CA', // LINK
amount: GreaterThanOrEqual(parseEther('5000')), // 5000 LINK
recipient: recipient,
})

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

test('should not pass filter with invalid token', async () => {
const recipient = '0xf9ce182b0fbe597773ab9bb5159b7479047de8fe'
const transaction = DEPOSIT_ERC20

const filter = await bridge({
sourceChainId: ETH_CHAIN_ID,
destinationChainId: ARB_ONE_CHAIN_ID,
tokenAddress: '0x6b175474e89094c44da98b954eedeac495271d0f', // DAI
amount: GreaterThanOrEqual(parseEther('2000')),
recipient: recipient,
})

expect(apply(transaction, filter)).to.be.false
})
test('should not pass filter with invalid recipient', async () => {
const recipient = '0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef'
const transaction = DEPOSIT_ERC20

const filter = await bridge({
sourceChainId: ETH_CHAIN_ID,
destinationChainId: ARB_ONE_CHAIN_ID,
tokenAddress: '0x514910771AF9Ca656af840dff83E8264EcF986CA', // LINK
amount: GreaterThanOrEqual(parseEther('2000')),
recipient: recipient,
})

expect(apply(transaction, filter)).to.be.false
})
})
})
105 changes: 105 additions & 0 deletions packages/arbitrum/src/Arbitrum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { type BridgeActionParams, compressJson } from '@rabbitholegg/questdk'
import { type Address } from 'viem'
import {
CHAIN_ID_ARRAY,
ETH_CHAIN_ID,
ARB_ONE_CHAIN_ID,
ARB_NOVA_CHAIN_ID,
} from './chain-ids.js'
import {
MAINNET_TO_ARB_NOVA_GATEWAY,
MAINNET_TO_ARB_ONE_GATEWAY,
ARB_NOVA_TO_MAINNET_GATEWAY,
ARB_ONE_TO_MAINNET_GATEWAY,
UNIVERSAL_ARBSYS_PRECOMPILE,
ARB_ONE_DELAYED_INBOX,
ARB_NOVA_DELAYED_INBOX,
} from './contract-addresses.js'
import { ArbitrumTokens } from './supported-token-addresses.js'
import {
GATEWAY_OUTBOUND_TRANSFER_FRAG,
ARBSYS_WITHDRAW_ETH_FRAG,
INBOX_DEPOSIT_ETH_FRAG,
} from './abi.js'
const ETH_TOKEN_ADDRESS = '0x0000000000000000000000000000000000000000'

export const bridge = async (bridge: BridgeActionParams) => {
// This is the information we'll use to compose the Transaction object
const {
sourceChainId,
destinationChainId,
contractAddress,
tokenAddress,
amount,
recipient,
} = bridge

const isBridgingToken = tokenAddress !== ETH_TOKEN_ADDRESS

if (isBridgingToken) {
const networkGateway = getContractAddressFromChainId(
sourceChainId,
destinationChainId,
)
// We're targeting a gateway contract
return compressJson({
chainId: sourceChainId, // The chainId of the source chain
to: contractAddress || networkGateway, // The contract address of the bridge
input: {
$abi: GATEWAY_OUTBOUND_TRANSFER_FRAG,
_token: tokenAddress,
_to: recipient,
_amount: amount,
},
})
}
if (sourceChainId === ETH_CHAIN_ID) {
const networkInbox =
destinationChainId === ARB_NOVA_CHAIN_ID
? ARB_NOVA_DELAYED_INBOX
: ARB_ONE_DELAYED_INBOX
// We're targeting the Delayed Inbox
return compressJson({
chainId: sourceChainId, // The chainId of the source chain
to: contractAddress || networkInbox, // The contract address of the bridge
value: amount,
input: {
$abi: INBOX_DEPOSIT_ETH_FRAG,
},
})
}
// Otherwise we're targeting the chain specific precompile
// We always want to return a compressed JSON object which we'll transform into a TransactionFilter
return compressJson({
chainId: sourceChainId, // The chainId of the source chain
to: contractAddress || UNIVERSAL_ARBSYS_PRECOMPILE, // The contract address of the bridge
input: {
$abi: ARBSYS_WITHDRAW_ETH_FRAG,
destination: recipient,
},
})
}

export const getSupportedTokenAddresses = async (_chainId: number) => {
return ArbitrumTokens[_chainId] as Address[]
}

export const getSupportedChainIds = async () => {
return CHAIN_ID_ARRAY as number[]
}

const getContractAddressFromChainId = (
sourceChainId: number,
destinationChainId: number,
): Address => {
// This is klunky but the alternative is some sort of convoluted 2D mapping
if (sourceChainId === ARB_NOVA_CHAIN_ID) return ARB_NOVA_TO_MAINNET_GATEWAY
if (sourceChainId === ARB_ONE_CHAIN_ID) return ARB_ONE_TO_MAINNET_GATEWAY
if (sourceChainId === ETH_CHAIN_ID) {
if (destinationChainId === ARB_NOVA_CHAIN_ID)
return MAINNET_TO_ARB_NOVA_GATEWAY
if (destinationChainId === ARB_ONE_CHAIN_ID)
return MAINNET_TO_ARB_ONE_GATEWAY
}
return '0x0'
}
37 changes: 37 additions & 0 deletions packages/arbitrum/src/abi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/ethereum/gateway/L1GatewayRouter.sol#L229
export const GATEWAY_OUTBOUND_TRANSFER_FRAG = [
{
inputs: [
{ internalType: 'address', name: '_token', type: 'address' },
{ internalType: 'address', name: '_to', type: 'address' },
{ internalType: 'uint256', name: '_amount', type: 'uint256' },
{ internalType: 'uint256', name: '_maxGas', type: 'uint256' },
{ internalType: 'uint256', name: '_gasPriceBid', type: 'uint256' },
{ internalType: 'bytes', name: '_data', type: 'bytes' },
],
name: 'outboundTransfer',
outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }],
stateMutability: 'payable',
type: 'function',
},
]

export const ARBSYS_WITHDRAW_ETH_FRAG = [
{
inputs: [{ internalType: 'address', name: 'destination', type: 'address' }],
name: 'withdrawEth',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'payable',
type: 'function',
},
]

export const INBOX_DEPOSIT_ETH_FRAG = [
{
inputs: [],
name: 'depositEth',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'payable',
type: 'function',
},
]
10 changes: 10 additions & 0 deletions packages/arbitrum/src/chain-ids.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// The arbitrum Native Bridge only supports three chains, Arbitrum, Arbitrum Nova, and Ethereum
export const ETH_CHAIN_ID = 1
export const ARB_ONE_CHAIN_ID = 42161
export const ARB_NOVA_CHAIN_ID = 42170

export const CHAIN_ID_ARRAY = [
ETH_CHAIN_ID,
ARB_ONE_CHAIN_ID,
ARB_NOVA_CHAIN_ID,
]
15 changes: 15 additions & 0 deletions packages/arbitrum/src/contract-addresses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// https://docs.arbitrum.io/for-devs/useful-addresses
export const MAINNET_TO_ARB_NOVA_GATEWAY =
'0xC840838Bc438d73C16c2f8b22D2Ce3669963cD48'
export const MAINNET_TO_ARB_ONE_GATEWAY =
'0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef'
export const ARB_NOVA_TO_MAINNET_GATEWAY =
'0x21903d3F8176b1a0c17E953Cd896610Be9fFDFa8'
export const ARB_ONE_TO_MAINNET_GATEWAY =
'0x5288c571Fd7aD117beA99bF60FE0846C4E84F933'
export const ARB_ONE_DELAYED_INBOX =
'0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f'
export const ARB_NOVA_DELAYED_INBOX =
'0xc4448b71118c9071Bcb9734A0EAc55D18A153949'
export const UNIVERSAL_ARBSYS_PRECOMPILE =
'0x0000000000000000000000000000000000000064'
19 changes: 19 additions & 0 deletions packages/arbitrum/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {
type IActionPlugin,
PluginActionNotImplementedError,
} from '@rabbitholegg/questdk'

import {
bridge,
getSupportedChainIds,
getSupportedTokenAddresses,
} from './Arbitrum.js'

export const Arbitrum: IActionPlugin = {
pluginId: 'arbitrum',
getSupportedTokenAddresses,
getSupportedChainIds,
bridge,
swap: async () => new PluginActionNotImplementedError(),
mint: async () => new PluginActionNotImplementedError(),
}
Loading

0 comments on commit b89d302

Please sign in to comment.