diff --git a/src/adapter/bridges/OFTBridge.ts b/src/adapter/bridges/OFTBridge.ts index 4be12ee822..b828faeec0 100644 --- a/src/adapter/bridges/OFTBridge.ts +++ b/src/adapter/bridges/OFTBridge.ts @@ -18,6 +18,12 @@ import * as OFT from "../../utils/OFTUtils"; import { OFT_DEFAULT_FEE_CAP, OFT_FEE_CAP_OVERRIDES } from "../../common/Constants"; import { IOFT_ABI_FULL } from "../../common/ContractAddresses"; +type OFTBridgeArguments = { + sendParamStruct: OFT.SendParamStruct; + feeStruct: OFT.MessagingFeeStruct; + refundAddress: string; +}; + export class OFTBridge extends BaseBridgeAdapter { public readonly l2TokenAddress: string; private readonly l1ChainEid: number; @@ -62,6 +68,36 @@ export class OFTBridge extends BaseBridgeAdapter { _l2Token: Address, amount: BigNumber ): Promise { + const { sendParamStruct, feeStruct, refundAddress } = await this.buildOftTransactionArgs( + toAddress, + l1Token, + amount + ); + return { + contract: this.l1Bridge, + method: "send", + args: [sendParamStruct, feeStruct, refundAddress], + value: BigNumber.from(feeStruct.nativeFee), + }; + } + + /** + * Rounds send amount so that dust doesn't get subtracted from it in the OFT contract. + * @param amount amount to round + * @returns amount rounded down + */ + async roundAmountToSend(amount: BigNumber): Promise { + // Fetch `sharedDecimals` if not already fetched + this.sharedDecimals ??= await this.l1Bridge.sharedDecimals(); + + return OFT.roundAmountToSend(amount, this.l1TokenInfo.decimals, this.sharedDecimals); + } + + async buildOftTransactionArgs( + toAddress: Address, + l1Token: EvmAddress, + amount: BigNumber + ): Promise { // Verify the token matches the one this bridge was constructed for assert( l1Token.eq(this.l1TokenAddress), @@ -96,24 +132,12 @@ export class OFTBridge extends BaseBridgeAdapter { // Set refund address to signer's address. This should technically never be required as all of our calcs // are precise, set it just in case const refundAddress = await this.l1Bridge.signer.getAddress(); - return { - contract: this.l1Bridge, - method: "send", - args: [sendParamStruct, feeStruct, refundAddress], - value: BigNumber.from(feeStruct.nativeFee), - }; - } - /** - * Rounds send amount so that dust doesn't get subtracted from it in the OFT contract. - * @param amount amount to round - * @returns amount rounded down - */ - private async roundAmountToSend(amount: BigNumber): Promise { - // Fetch `sharedDecimals` if not already fetched - this.sharedDecimals ??= await this.l1Bridge.sharedDecimals(); - - return OFT.roundAmountToSend(amount, this.l1TokenInfo.decimals, this.sharedDecimals); + return { + sendParamStruct, + feeStruct, + refundAddress, + } satisfies OFTBridgeArguments; } async queryL1BridgeInitiationEvents( diff --git a/src/adapter/bridges/OFTWethBridge.ts b/src/adapter/bridges/OFTWethBridge.ts new file mode 100644 index 0000000000..3345b25212 --- /dev/null +++ b/src/adapter/bridges/OFTWethBridge.ts @@ -0,0 +1,96 @@ +import { Contract, Signer } from "ethers"; +import { BridgeTransactionDetails, BridgeEvents } from "./BaseBridgeAdapter"; +import { CONTRACT_ADDRESSES } from "../../common"; +import { + BigNumber, + Provider, + EvmAddress, + Address, + winston, + bnZero, + EventSearchConfig, + paginatedEventQuery, +} from "../../utils"; +import { OFTBridge } from "./"; +import { processEvent } from "../utils"; + +export class OFTWethBridge extends OFTBridge { + private readonly atomicDepositor: Contract; + + constructor( + l2ChainId: number, + l1ChainId: number, + l1Signer: Signer, + l2SignerOrProvider: Signer | Provider, + public readonly l1TokenAddress: EvmAddress, + logger: winston.Logger + ) { + super(l2ChainId, l1ChainId, l1Signer, l2SignerOrProvider, l1TokenAddress, logger); + + const { address: atomicDepositorAddress, abi: atomicDepositorAbi } = + CONTRACT_ADDRESSES[this.hubChainId].atomicDepositor; + this.atomicDepositor = new Contract(atomicDepositorAddress, atomicDepositorAbi, l1Signer); + + // Overwrite the l1 gateway to the atomic depositor address. + this.l1Gateways = [EvmAddress.from(atomicDepositorAddress)]; + } + + async constructL1ToL2Txn( + toAddress: Address, + l1Token: EvmAddress, + _l2Token: Address, + amount: BigNumber + ): Promise { + const { sendParamStruct, feeStruct, refundAddress } = await this.buildOftTransactionArgs( + toAddress, + l1Token, + amount + ); + const bridgeCalldata = this.getL1Bridge().interface.encodeFunctionData("send", [ + sendParamStruct, + feeStruct, + refundAddress, + ]); + const netValue = feeStruct.nativeFee.add(sendParamStruct.amountLD); + return { + contract: this.atomicDepositor, + method: "bridgeWeth", + args: [this.l2chainId, netValue, sendParamStruct.amountLD, bnZero, bridgeCalldata], + }; + } + + // We must override the OFTBridge's `queryL1BridgeInitiationEvents` since the depositor into the OFT adapter is the atomic depositor. + // This means if we query off of the OFT adapter, we wouldn't be able to distinguish which deposits correspond to which EOAs. + async queryL1BridgeInitiationEvents( + l1Token: EvmAddress, + fromAddress: Address, + toAddress: Address, + eventConfig: EventSearchConfig + ): Promise { + // Return no events if the query is for a different l1 token + if (!l1Token.eq(this.l1TokenAddress)) { + return {}; + } + + // Return no events if the query is for hubPool + if (fromAddress.eq(this.hubPoolAddress)) { + return {}; + } + + const isAssociatedSpokePool = this.spokePoolAddress.eq(toAddress); + const events = await paginatedEventQuery( + this.atomicDepositor, + this.atomicDepositor.filters.AtomicWethDepositInitiated( + isAssociatedSpokePool ? this.hubPoolAddress.toNative() : fromAddress.toNative(), // from + this.l2chainId // destinationChainId + ), + eventConfig + ); + + return { + [this.l2TokenAddress]: events.map((event) => { + return processEvent(event, "amount"); + }), + }; + } +} diff --git a/src/adapter/bridges/index.ts b/src/adapter/bridges/index.ts index 43728210de..591a779b12 100644 --- a/src/adapter/bridges/index.ts +++ b/src/adapter/bridges/index.ts @@ -20,3 +20,4 @@ export * from "./ZKStackUSDCBridge"; export * from "./ZKStackWethBridge"; export * from "./SolanaUsdcCCTPBridge"; export * from "./OFTBridge"; +export * from "./OFTWethBridge"; diff --git a/src/adapter/l2Bridges/OFTL2Bridge.ts b/src/adapter/l2Bridges/OFTL2Bridge.ts index 83585211e2..7ac7c63b33 100644 --- a/src/adapter/l2Bridges/OFTL2Bridge.ts +++ b/src/adapter/l2Bridges/OFTL2Bridge.ts @@ -13,6 +13,7 @@ import { bnZero, EventSearchConfig, getTokenInfo, + fixedPointAdjustment, } from "../../utils"; import { interfaces as sdkInterfaces } from "@across-protocol/sdk"; import { BaseL2BridgeAdapter } from "./BaseL2BridgeAdapter"; @@ -27,6 +28,7 @@ export class OFTL2Bridge extends BaseL2BridgeAdapter { private sharedDecimals?: number; private readonly nativeFeeCap: BigNumber; private l2ToL1AmountConverter: (amount: BigNumber) => BigNumber; + private readonly feePct: BigNumber = BigNumber.from(5 * 10 ** 15); // Default fee percent of 0.5% constructor( l2chainId: number, @@ -75,12 +77,16 @@ export class OFTL2Bridge extends BaseL2BridgeAdapter { // We round `amount` to a specific precision to prevent rounding on the contract side. This way, we // receive the exact amount we sent in the transaction const roundedAmount = await this.roundAmountToSend(amount, this.l2TokenInfo.decimals); + const appliedFee = OFT.isStargateBridge(this.l2chainId) + ? roundedAmount.mul(this.feePct).div(fixedPointAdjustment) // Set a max slippage of 0.5%. + : bnZero; + const expectedOutputAmount = roundedAmount.sub(appliedFee); const sendParamStruct: OFT.SendParamStruct = { dstEid: this.l1ChainEid, to: OFT.formatToAddress(toAddress), amountLD: roundedAmount, // @dev Setting `minAmountLD` equal to `amountLD` ensures we won't hit contract-side rounding - minAmountLD: roundedAmount, + minAmountLD: expectedOutputAmount, extraOptions: "0x", composeMsg: "0x", oftCmd: "0x", diff --git a/src/common/Constants.ts b/src/common/Constants.ts index 880dc1fcca..db447d79f9 100644 --- a/src/common/Constants.ts +++ b/src/common/Constants.ts @@ -37,6 +37,7 @@ import { BinanceCEXBridge, BinanceCEXNativeBridge, SolanaUsdcCCTPBridge, + OFTWethBridge, } from "../adapter/bridges"; import { BaseL2BridgeAdapter, @@ -387,7 +388,7 @@ export const SUPPORTED_TOKENS: { [chainId: number]: string[] } = { "VLR", "ezETH", ], - [CHAIN_IDs.PLASMA]: ["USDT"], + [CHAIN_IDs.PLASMA]: ["USDT", "WETH"], [CHAIN_IDs.POLYGON]: ["USDC", "USDT", "WETH", "DAI", "WBTC", "UMA", "BAL", "ACX", "POOL"], [CHAIN_IDs.REDSTONE]: ["WETH"], [CHAIN_IDs.SCROLL]: ["WETH", "USDC", "USDT", "WBTC", "POOL"], @@ -546,7 +547,7 @@ export const CUSTOM_BRIDGE: Record> = new Map( [CHAIN_IDs.UNICHAIN, EvmAddress.from("0xc07bE8994D035631c36fb4a89C918CeFB2f03EC3")], ]), ], + [ + TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.MAINNET], + new Map([ + [CHAIN_IDs.MAINNET, EvmAddress.from("0x77b2043768d28E9C9aB44E1aBfC95944bcE57931")], + [CHAIN_IDs.PLASMA, EvmAddress.from("0x0cEb237E109eE22374a567c6b09F373C73FA4cBb")], + ]), + ], ]); // 0.1 ETH is a default cap for chains that use ETH as their gas token diff --git a/src/dataworker/PoolRebalanceUtils.ts b/src/dataworker/PoolRebalanceUtils.ts index 4663a6367b..fe6aacd012 100644 --- a/src/dataworker/PoolRebalanceUtils.ts +++ b/src/dataworker/PoolRebalanceUtils.ts @@ -3,11 +3,11 @@ import { HubPoolClient } from "../clients"; import { PendingRootBundle, PoolRebalanceLeaf, RelayerRefundLeaf, SlowFillLeaf } from "../interfaces"; import { BigNumber, + bnZero, MerkleTree, convertFromWei, formatFeePct, shortenHexString, - shortenHexStrings, toBN, winston, assert, @@ -74,75 +74,83 @@ export function generateMarkdownForRootBundle( const convertTokenListFromWei = (chainId: number, tokenAddresses: Address[], weiVals: string[]) => { return tokenAddresses.map((token, index) => { - try { - const { decimals } = hubPoolClient.getTokenInfoForAddress(token, chainId); - return convertFromWei(weiVals[index], decimals); - } catch (error) { - hubPoolClient.logger.debug({ - at: "PoolRebalanceUtils#generateMarkdownForRootBundle#convertTokenListFromWei", - message: `Error getting token info for address ${token} on chain ${chainId}`, - error, - }); - return weiVals[index].toString(); - } + const { decimals } = hubPoolClient.getTokenInfoForAddress(token, chainId); + return convertFromWei(weiVals[index], decimals); }); }; - const convertTokenAddressToSymbol = (chainId: number, tokenAddress: Address) => { - try { - return hubPoolClient.getTokenInfoForAddress(tokenAddress, chainId).symbol; - } catch (error) { - hubPoolClient.logger.debug({ - at: "PoolRebalanceUtils#generateMarkdownForRootBundle#convertTokenAddressToSymbol", - message: `Error getting token info for address ${tokenAddress} on chain ${chainId}`, - error, - }); - return "UNKNOWN TOKEN"; - } - }; + + const convertTokenAddressToSymbol = (chainId: number, tokenAddress: Address) => + hubPoolClient.getTokenInfoForAddress(tokenAddress, chainId).symbol; + const convertL1TokenAddressesToSymbols = (l1Tokens: EvmAddress[]) => { return l1Tokens.map((l1Token) => { return convertTokenAddressToSymbol(hubPoolChainId, l1Token); }); }; + + const tokenPad = 5; + const amountPad = 18; + const balancePad = 18; + const feePad = 18; let poolRebalanceLeavesPretty = ""; - poolRebalanceLeaves.forEach((leaf, index) => { - // Shorten keys for ease of reading from Slack. - delete leaf.leafId; - leaf.groupId = leaf.groupIndex; - delete leaf.groupIndex; - leaf.bundleLpFees = convertTokenListFromWei(hubPoolChainId, leaf.l1Tokens, leaf.bundleLpFees); - leaf.runningBalances = convertTokenListFromWei(hubPoolChainId, leaf.l1Tokens, leaf.runningBalances); - leaf.netSendAmounts = convertTokenListFromWei(hubPoolChainId, leaf.l1Tokens, leaf.netSendAmounts); - leaf.l1Tokens = convertL1TokenAddressesToSymbols(leaf.l1Tokens); - poolRebalanceLeavesPretty += `\n\t\t\t${index}: ${JSON.stringify(leaf)}`; + poolRebalanceLeaves.forEach((leaf, leafId) => { + const { chainId, groupIndex: groupId } = leaf; + const l1Tokens = convertL1TokenAddressesToSymbols(leaf.l1Tokens); + const fees = convertTokenListFromWei(hubPoolChainId, leaf.l1Tokens, leaf.bundleLpFees); + const runningBalances = convertTokenListFromWei(hubPoolChainId, leaf.l1Tokens, leaf.runningBalances); + const netSendAmounts = convertTokenListFromWei(hubPoolChainId, leaf.l1Tokens, leaf.netSendAmounts); + + poolRebalanceLeavesPretty += "```"; + poolRebalanceLeavesPretty += `${getNetworkName(chainId)} (${leafId}-${groupId}):\n`; + poolRebalanceLeavesPretty += `${"".padStart(tokenPad, " ")}`; + poolRebalanceLeavesPretty += `\t${"balance".padStart(balancePad)}`; + poolRebalanceLeavesPretty += `\t${"netSendAmount".padStart(amountPad, " ")}`; + poolRebalanceLeavesPretty += `\t${"fees".padStart(feePad, " ")}`; + + l1Tokens.forEach((l1Token, idx) => { + const netSendAmount = netSendAmounts[idx]; + const runningBalance = runningBalances[idx]; + const fee = fees[idx]; + poolRebalanceLeavesPretty += `\n${(l1Token + ":").padStart(tokenPad, " ")}`; + poolRebalanceLeavesPretty += `\t${runningBalance.padStart(balancePad, " ")}`; + poolRebalanceLeavesPretty += `\t${netSendAmount.padStart(amountPad, " ")}`; + poolRebalanceLeavesPretty += `\t${fee.padStart(feePad, " ")}`; + poolRebalanceLeavesPretty += "\n"; + }); + poolRebalanceLeavesPretty += "```\n"; }); let relayerRefundLeavesPretty = ""; - relayerRefundLeaves.forEach((leaf, index) => { - // Shorten keys for ease of reading from Slack. - delete leaf.leafId; - try { - leaf.amountToReturn = convertFromWei( - leaf.amountToReturn, - hubPoolClient.getTokenInfoForAddress(leaf.l2TokenAddress, leaf.chainId).decimals - ); - } catch (error) { - hubPoolClient.logger.debug({ - at: "PoolRebalanceUtils#generateMarkdownForRootBundle", - message: `Error getting token info for address ${leaf.l2TokenAddress} on chain ${leaf.chainId}`, - error, - }); - leaf.amountToReturn = leaf.amountToReturn.toString(); - } - leaf.refundAmounts = convertTokenListFromWei( - leaf.chainId, - Array(leaf.refundAmounts.length).fill(leaf.l2TokenAddress), + relayerRefundLeaves.forEach((leaf) => { + const { chainId, l2TokenAddress } = leaf; + const l2Token = convertTokenAddressToSymbol(chainId, l2TokenAddress); + const refundAddresses = leaf.refundAddresses.map((addr) => shortenHexString(addr.toNative(), 14)); + const refundAmounts = convertTokenListFromWei( + chainId, + Array(leaf.refundAmounts.length).fill(l2TokenAddress), leaf.refundAmounts ); - leaf.l2Token = convertTokenAddressToSymbol(leaf.chainId, leaf.l2TokenAddress); - delete leaf.l2TokenAddress; - leaf.refundAddresses = shortenHexStrings(leaf.refundAddresses.map((refundAddress) => refundAddress.toBytes32())); - relayerRefundLeavesPretty += `\n\t\t\t${index}: ${JSON.stringify(leaf)}`; + + relayerRefundLeavesPretty += "```\n"; + relayerRefundLeavesPretty += `${getNetworkName(chainId)} ${l2Token}:`; + if (leaf.amountToReturn.gt(bnZero)) { + const { decimals } = hubPoolClient.getTokenInfoForAddress(l2TokenAddress, chainId); + const amountToReturn = convertFromWei(leaf.amountToReturn, decimals); + relayerRefundLeavesPretty += ` (return ${amountToReturn})`; + } + const nCols = 2; + const nRows = Math.ceil(refundAddresses.length / nCols); + for (let row = 0; row < nRows; ++row) { + relayerRefundLeavesPretty += "\n\t"; + for (let col = 0; col < nCols; ++col) { + const idx = row + nRows * col; + if (idx >= refundAddresses.length) { + break; + } + relayerRefundLeavesPretty += `\t${refundAddresses[idx]}: ${refundAmounts[idx].padStart(14, " ")}`; + } + } + relayerRefundLeavesPretty += "```\n"; }); let slowRelayLeavesPretty = ""; @@ -167,8 +175,8 @@ export function generateMarkdownForRootBundle( // @todo: When v2 types are removed, update the slowFill definition to be more precise about the member fields. const slowFill = { // Shorten select keys for ease of reading from Slack. - depositor: shortenHexString(leaf.relayData.depositor.toBytes32()), - recipient: shortenHexString(leaf.relayData.recipient.toBytes32()), + depositor: shortenHexString(leaf.relayData.depositor.toNative()), + recipient: shortenHexString(leaf.relayData.recipient.toNative()), originChainId: leaf.relayData.originChainId.toString(), destinationChainId: destinationChainId.toString(), depositId: leaf.relayData.depositId.toString(), @@ -187,13 +195,11 @@ export function generateMarkdownForRootBundle( : "No slow relay leaves"; return ( "\n" + - `\t*Bundle blocks*:${bundleBlockRangePretty}\n` + - "\t*PoolRebalance*:\n" + - `\t\troot:${shortenHexString(poolRebalanceRoot)}...\n` + - `\t\tleaves:${poolRebalanceLeavesPretty}\n` + - "\t*RelayerRefund*\n" + - `\t\troot:${shortenHexString(relayerRefundRoot)}...\n` + - `\t\tleaves:${relayerRefundLeavesPretty}\n` + + `\t*Bundle blocks*: ${bundleBlockRangePretty}\n` + + `\t*PoolRebalance*: ${shortenHexString(poolRebalanceRoot, 20)}...\n` + + `${poolRebalanceLeavesPretty}\n` + + `\t*RelayerRefund*: ${shortenHexString(relayerRefundRoot, 20)}\n` + + `\t\t${relayerRefundLeavesPretty}\n` + "\t*SlowRelay*\n" + `\t${slowRelayMsg}` ); diff --git a/src/utils/OFTUtils.ts b/src/utils/OFTUtils.ts index 6a68595ad2..8880a223d8 100644 --- a/src/utils/OFTUtils.ts +++ b/src/utils/OFTUtils.ts @@ -1,5 +1,5 @@ import { OFT_NO_EID } from "@across-protocol/constants"; -import { BigNumber, BigNumberish, EvmAddress, PUBLIC_NETWORKS, assert, isDefined } from "."; +import { BigNumber, BigNumberish, EvmAddress, PUBLIC_NETWORKS, assert, isDefined, CHAIN_IDs } from "."; import { BytesLike } from "ethers"; import { EVM_OFT_MESSENGERS } from "../common/Constants"; @@ -14,7 +14,7 @@ export type SendParamStruct = { }; export type MessagingFeeStruct = { - nativeFee: BigNumberish; + nativeFee: BigNumber; lzTokenFee: BigNumberish; }; @@ -40,6 +40,14 @@ export function getMessengerEvm(l1TokenAddress: EvmAddress, chainId: number): Ev return messenger; } +/** + * @param chainId The chain Id of the network to check + * @returns If the input chain ID's OFT adapter requires payment in the input token. + */ +export function isStargateBridge(chainId: number): boolean { + return [CHAIN_IDs.PLASMA].includes(chainId); +} + /** * @param receiver Address to receive the OFT transfer on target chain * @returns A 32-byte string to be used when calling on-chain OFT contracts