-
Notifications
You must be signed in to change notification settings - Fork 102
improve: add l2 token splitter and fix Bridge API transfer tracking #3120
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
Open
bmzig
wants to merge
16
commits into
master
Choose a base branch
from
bz/fixAmount
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
eee8d8f
improve: do not allow flexible transfer amounts in BridgeApi
bmzig 7437455
codex
bmzig ddec5cd
feat: add l2 token splitter bridge (#3034)
bmzig ddb6e63
Merge branch 'master' into bz/fixAmount
bmzig b2722c2
normalize decimals
bmzig 6f1c20b
check each transfer
bmzig ce5e962
Merge branch 'master' into bz/fixAmount
bmzig 2665a5b
Merge branch 'master' into bz/fixAmount
bmzig 125df73
codex
bmzig 48799f7
bridge approvals
bmzig cc8b38c
Merge branch 'master' into bz/fixAmount
bmzig fbe3816
simplify
bmzig e79ceb3
Merge branch 'master' into bz/fixAmount
bmzig 9c3fd91
update
bmzig 82111e4
Merge branch 'master' into bz/fixAmount
bmzig 5084ea3
Merge branch 'master' into bz/fixAmount
bmzig File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or 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
This file contains hidden or 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
This file contains hidden or 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,126 @@ | ||
| import { | ||
| Address, | ||
| BigNumber, | ||
| bnZero, | ||
| Contract, | ||
| EventSearchConfig, | ||
| getNetworkName, | ||
| Signer, | ||
| EvmAddress, | ||
| CHAIN_IDs, | ||
| BRIDGE_API_MINIMUMS, | ||
| toBN, | ||
| BridgeApiClient, | ||
| getTokenInfo, | ||
| floatToBN, | ||
| isDefined, | ||
| assert, | ||
| BRIDGE_API_DESTINATION_TOKEN_SYMBOLS, | ||
| getTimestampForBlock, | ||
| toAddressType, | ||
| BRIDGE_API_DESTINATION_TOKENS, | ||
| createFormatFunction, | ||
| roundAmountToSend, | ||
| } from "../../utils"; | ||
| import { BaseL2BridgeAdapter } from "./BaseL2BridgeAdapter"; | ||
| import { AugmentedTransaction } from "../../clients/TransactionClient"; | ||
| import { TokenInfo } from "../../interfaces"; | ||
| import ERC20_ABI from "../../common/abi/MinimalERC20.json"; | ||
|
|
||
| export class BridgeApi extends BaseL2BridgeAdapter { | ||
| protected api: BridgeApiClient; | ||
| protected l1TokenInfo: TokenInfo; | ||
| protected l2TokenInfo: TokenInfo; | ||
|
|
||
| constructor(l2chainId: number, hubChainId: number, l2Signer: Signer, l1Signer: Signer, l1Token: EvmAddress) { | ||
| if (hubChainId !== CHAIN_IDs.MAINNET) { | ||
| throw new Error("Cannot define a Bridge API bridge for a non-production network"); | ||
| } | ||
| super(l2chainId, hubChainId, l2Signer, l1Signer, l1Token); | ||
|
|
||
| // We need to fetch some API configuration details from environment. | ||
| const { BRIDGE_API_BASE = "https://api.bridge.xyz", BRIDGE_API_KEY, BRIDGE_CUSTOMER_ID } = process.env; | ||
|
|
||
| assert(isDefined(BRIDGE_API_BASE), "BRIDGE_API_BASE must be set in the environment"); | ||
| assert(isDefined(BRIDGE_API_KEY), "BRIDGE_API_KEY must be set in the environment"); | ||
| assert(isDefined(BRIDGE_CUSTOMER_ID), "BRIDGE_CUSTOMER_ID must be set in the environment"); | ||
|
|
||
| this.api = new BridgeApiClient( | ||
| BRIDGE_API_BASE, | ||
| BRIDGE_API_KEY, | ||
| BRIDGE_CUSTOMER_ID, | ||
| this.l2chainId, | ||
| this.hubChainId | ||
| ); | ||
|
|
||
| this.l1TokenInfo = getTokenInfo(l1Token, this.hubChainId); | ||
| this.l2TokenInfo = getTokenInfo( | ||
| toAddressType(BRIDGE_API_DESTINATION_TOKENS[this.l2chainId], this.l2chainId), | ||
| this.l2chainId | ||
| ); | ||
| } | ||
|
|
||
| override getL2Token(): Address { | ||
| return toAddressType(BRIDGE_API_DESTINATION_TOKENS[this.l2chainId], this.l2chainId); | ||
| } | ||
|
|
||
| async constructWithdrawToL1Txns( | ||
| toAddress: EvmAddress, | ||
| l2Token: EvmAddress, | ||
| l1Token: EvmAddress, | ||
| _amount: BigNumber | ||
| ): Promise<AugmentedTransaction[]> { | ||
| const amount = roundAmountToSend(_amount, this.l2TokenInfo.decimals, 2); | ||
| // If amount is less than the network minimums, then throw. | ||
| if (amount.lt(BRIDGE_API_MINIMUMS[this.l2chainId]?.[this.hubChainId] ?? toBN(Number.MAX_SAFE_INTEGER))) { | ||
| throw new Error(`Cannot bridge to ${getNetworkName(this.hubChainId)} due to invalid amount ${amount}`); | ||
| } | ||
| const formatter = createFormatFunction(2, 4, false, this.l2TokenInfo.decimals); | ||
| const l2TokenSymbol = BRIDGE_API_DESTINATION_TOKEN_SYMBOLS[l2Token.toNative()]; | ||
| const transferRouteSource = await this.api.createTransferRouteEscrowAddress( | ||
| toAddress, | ||
| l2TokenSymbol, | ||
| this.l1TokenInfo.symbol, | ||
| formatter(amount) | ||
| ); | ||
| const l2TokenContract = new Contract(l2Token.toNative(), ERC20_ABI, this.l2Signer); | ||
| const transferTxn = { | ||
| contract: l2TokenContract, | ||
| method: "transfer", | ||
| chainId: this.l2chainId, | ||
| args: [transferRouteSource, amount], | ||
| nonMulticall: true, | ||
| canFailInSimulation: false, | ||
| value: bnZero, | ||
| message: `🎰 Withdrew ${getNetworkName(this.l2chainId)} ${this.l2TokenInfo.symbol} to L1`, | ||
| mrkdwn: `Withdrew ${formatter(amount.toString())} ${this.l2TokenInfo.symbol} from ${getNetworkName( | ||
| this.l2chainId | ||
| )} to L1`, | ||
| }; | ||
| return [transferTxn]; | ||
| } | ||
|
|
||
| // @dev We do not filter on origin/destination tokens since there is only one bridge API destination token for any destination chain. | ||
| // e.g. For Tempo, we only use Bridge for pathUSD; other tokens are rebalanced via other methods, so | ||
| // if there is an outstanding transfer from Tempo to Ethereum, then this must be a pathUSD transfer. | ||
| async getL2PendingWithdrawalAmount( | ||
| l2EventConfig: EventSearchConfig, | ||
| l1EventConfig: EventSearchConfig, | ||
| fromAddress: EvmAddress, | ||
| l2Token: EvmAddress | ||
| ): Promise<BigNumber> { | ||
| const fromTimestamp = await getTimestampForBlock(this.l1Signer.provider, l1EventConfig.from); | ||
| const allTransfers = await this.api.getAllTransfersInRange(fromAddress, fromTimestamp * 1000); | ||
|
|
||
| const allInitiatedTransfers = await this.api.filterInitiatedTransfers( | ||
| allTransfers, | ||
| fromAddress, | ||
| l2EventConfig, | ||
| this.l2Signer.provider | ||
| ); | ||
| return allInitiatedTransfers.reduce((acc, transfer) => { | ||
| const { decimals: l2TokenDecimals } = getTokenInfo(l2Token, this.l2chainId); | ||
| return acc.add(floatToBN(transfer.receipt?.final_amount ?? transfer.amount, l2TokenDecimals)); | ||
| }, bnZero); | ||
| } | ||
| } | ||
This file contains hidden or 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,68 @@ | ||
| import { | ||
| BigNumber, | ||
| ConvertDecimals, | ||
| EventSearchConfig, | ||
| getTokenInfo, | ||
| Signer, | ||
| EvmAddress, | ||
| getTranslatedTokenAddress, | ||
| SolanaTransaction, | ||
| } from "../../utils"; | ||
| import { BaseL2BridgeAdapter } from "./BaseL2BridgeAdapter"; | ||
| import { AugmentedTransaction } from "../../clients/TransactionClient"; | ||
| import { L2_TOKEN_SPLITTER_BRIDGES } from "../../common"; | ||
|
|
||
| export class TokenSplitterBridge extends BaseL2BridgeAdapter { | ||
bmzig marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| protected bridge1; | ||
| protected bridge2; | ||
|
|
||
| constructor(l2chainId: number, hubChainId: number, l2Signer: Signer, l1Signer: Signer, l1Token: EvmAddress) { | ||
| super(l2chainId, hubChainId, l2Signer, l1Signer, l1Token); | ||
|
|
||
| const [bridge1Constructor, bridge2Constructor] = L2_TOKEN_SPLITTER_BRIDGES[this.l2chainId][this.l1Token.toNative()]; | ||
| this.bridge1 = new bridge1Constructor(l2chainId, hubChainId, l2Signer, l1Signer, l1Token); | ||
| this.bridge2 = new bridge2Constructor(l2chainId, hubChainId, l2Signer, l1Signer, l1Token); | ||
| } | ||
|
|
||
| getRouteForL2Token(l2Token: EvmAddress): BaseL2BridgeAdapter { | ||
| return getTranslatedTokenAddress(this.l1Token, this.hubChainId, this.l2chainId).eq(l2Token) | ||
| ? this.bridge1 | ||
| : this.bridge2; | ||
| } | ||
|
|
||
| async constructWithdrawToL1Txns( | ||
| toAddress: EvmAddress, | ||
| l2Token: EvmAddress, | ||
| l1Token: EvmAddress, | ||
| amount: BigNumber | ||
| ): Promise<AugmentedTransaction[] | SolanaTransaction[]> { | ||
| return this.getRouteForL2Token(l2Token).constructWithdrawToL1Txns(toAddress, l2Token, l1Token, amount); | ||
| } | ||
|
|
||
| async getL2PendingWithdrawalAmount( | ||
| l2EventConfig: EventSearchConfig, | ||
| l1EventConfig: EventSearchConfig, | ||
| fromAddress: EvmAddress, | ||
| l2Token: EvmAddress | ||
| ): Promise<BigNumber> { | ||
| const [bridge1Pending, bridge2Pending] = await Promise.all([ | ||
| this.bridge1.getL2PendingWithdrawalAmount(l2EventConfig, l1EventConfig, fromAddress, l2Token), | ||
| this.bridge2.getL2PendingWithdrawalAmount(l2EventConfig, l1EventConfig, fromAddress, l2Token), | ||
bmzig marked this conversation as resolved.
Show resolved
Hide resolved
bmzig marked this conversation as resolved.
Show resolved
Hide resolved
bmzig marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ]); | ||
|
|
||
| // Each bridge may return amounts denominated in its own L2 token's decimals. | ||
| // Normalize both to the decimals of the requested l2Token before summing. | ||
| const targetDecimals = getTokenInfo(l2Token, this.l2chainId).decimals; | ||
| const bridge1Decimals = getTokenInfo(this.bridge1.getL2Token(), this.l2chainId).decimals; | ||
| const bridge2Decimals = getTokenInfo(this.bridge2.getL2Token(), this.l2chainId).decimals; | ||
|
|
||
| const normalizedBridge1 = ConvertDecimals(bridge1Decimals, targetDecimals)(bridge1Pending); | ||
| const normalizedBridge2 = ConvertDecimals(bridge2Decimals, targetDecimals)(bridge2Pending); | ||
|
|
||
| return normalizedBridge1.add(normalizedBridge2); | ||
| } | ||
|
|
||
| public override requiredTokenApprovals(): { token: EvmAddress; bridge: EvmAddress }[] { | ||
| return [...this.bridge1.requiredTokenApprovals(), ...this.bridge2.requiredTokenApprovals()]; | ||
| } | ||
| } | ||
This file contains hidden or 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
This file contains hidden or 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
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.