-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #13 from rabbitholegg/arbitrum_bridge
Feat(arbitrum): Arbitrum bridge
- Loading branch information
Showing
17 changed files
with
1,107 additions
and
465 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}, | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(), | ||
} |
Oops, something went wrong.