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(arbitrum): Arbitrum bridge #13

Merged
merged 47 commits into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
4809ffd
feat(arbitrum): dupe template for arbitrum
Quazia Aug 22, 2023
a58d51d
Docs(readme): Rough notes on Arb bridging
Quazia Aug 23, 2023
0504689
Feat(template): chain-ids file constants
Quazia Aug 23, 2023
c773939
Feat(template): supported tokens with just DAI
Quazia Aug 23, 2023
8db3f3d
Feat(template): contract address constants
Quazia Aug 23, 2023
0910df7
Feat(arbitrum): getSupportedChainIds
Quazia Aug 23, 2023
fe3d947
Feat(arb): getSupportedTokenAddresses
Quazia Aug 23, 2023
154495c
Feat(arbitrum): default address for bridge
Quazia Aug 23, 2023
1e5f6a2
Docs(arbitrum): cleanup readme with links
Quazia Aug 23, 2023
fc79f7c
Feat(arbitrum): outbound abi fragment
Quazia Aug 23, 2023
8256153
Feat(arbitrum): ABI for arbsys and inbox
Quazia Aug 23, 2023
a6c3af8
Feat(arbitrum): setup ABI logic in Asbitrum.ts
Quazia Aug 23, 2023
8d11758
Feat(arbitrum): fill input values in Arbitrum.ts
Quazia Aug 23, 2023
f104594
test(arbitrum): valid action filter
Quazia Aug 23, 2023
6ba1308
Chore(arbitrum): refresh lock
Quazia Aug 24, 2023
cd743d2
Fix(arbitrum:abi.ts): make ABIs arrays
Quazia Aug 24, 2023
30d8d70
Fix(Arbitrum.ts): calc inbox by destination chain
Quazia Aug 24, 2023
7d54eac
Test(Arbitrum.test): add filter test
Quazia Aug 24, 2023
e6f1a84
Test(Arbitrum.test): extra fail filter cases
Quazia Aug 24, 2023
0c8560c
Chore(global): refresh lock
Quazia Aug 25, 2023
baa47f6
Fix(arbitrum): import issues preventing build
Quazia Aug 25, 2023
a3cb837
Merge branch 'main' of https://github.com/rabbitholegg/questdk-plugin…
Quazia Aug 25, 2023
f54d777
chore(global): refresh lock
Quazia Aug 26, 2023
78bdade
chore: format
Quazia Aug 26, 2023
47111bb
Fix(arbitrum): remove toHex
Quazia Sep 11, 2023
ab8afb9
Chore(arbitrum): move test transactions
Quazia Sep 11, 2023
a946b7f
Merge branch 'arbitrum_bridge' of https://github.com/rabbitholegg/que…
Quazia Sep 11, 2023
dad1d43
Fix(build): specify moduleResoltion inline
Quazia Sep 11, 2023
3887ba7
chore: format
Quazia Sep 11, 2023
c6efcda
Docs(arbitrum): small template comment cleanup
Quazia Sep 12, 2023
95b4cc4
Chore(arbitrum): use variable for CHAIN_ID_ARRAY
Quazia Sep 12, 2023
811c9b3
Merge branch 'main' of https://github.com/rabbitholegg/questdk-plugin…
Quazia Sep 12, 2023
0df558b
Chore(arbitrum): remove inliine module resolution
Quazia Sep 12, 2023
f7aa0fd
Merge branch 'arbitrum_bridge' of https://github.com/rabbitholegg/que…
Quazia Sep 12, 2023
bcd6d7c
chore: format
Quazia Sep 12, 2023
d5bf77d
Chore(arbitrum): update questdk
Quazia Sep 13, 2023
5bdaa03
chore(arbitrum): stabilize packages
Quazia Sep 13, 2023
50d6b3f
Merge branch 'main' of https://github.com/rabbitholegg/questdk-plugin…
Quazia Sep 13, 2023
d85d70a
Merge branch 'arbitrum_bridge' of https://github.com/rabbitholegg/que…
Quazia Sep 13, 2023
d7497da
Chore(pnpm): update lock
Quazia Sep 13, 2023
7db0159
chore: format
Quazia Sep 13, 2023
69357e6
Merge branch 'main' of https://github.com/rabbitholegg/questdk-plugin…
Quazia Sep 13, 2023
b7496d9
Merge branch 'arbitrum_bridge' of https://github.com/rabbitholegg/que…
Quazia Sep 13, 2023
82d1166
Chore(arbitrum): add changeset
Quazia Sep 14, 2023
1f33551
Feat(registry): add arbitrum
Quazia Sep 15, 2023
cb854ce
Chore(pnpm): regen lock
Quazia Sep 15, 2023
c549eb4
chore: format
Quazia Sep 15, 2023
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
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.
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": "[email protected]",
"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