Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
58 changes: 41 additions & 17 deletions src/adapter/bridges/OFTBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -62,6 +68,36 @@ export class OFTBridge extends BaseBridgeAdapter {
_l2Token: Address,
amount: BigNumber
): Promise<BridgeTransactionDetails> {
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<BigNumber> {
// 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<OFTBridgeArguments> {
// Verify the token matches the one this bridge was constructed for
assert(
l1Token.eq(this.l1TokenAddress),
Expand Down Expand Up @@ -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<BigNumber> {
// 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(
Expand Down
96 changes: 96 additions & 0 deletions src/adapter/bridges/OFTWethBridge.ts
Original file line number Diff line number Diff line change
@@ -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<BridgeTransactionDetails> {
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<BridgeEvents> {
// 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");
}),
};
}
}
1 change: 1 addition & 0 deletions src/adapter/bridges/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ export * from "./ZKStackUSDCBridge";
export * from "./ZKStackWethBridge";
export * from "./SolanaUsdcCCTPBridge";
export * from "./OFTBridge";
export * from "./OFTWethBridge";
8 changes: 7 additions & 1 deletion src/adapter/l2Bridges/OFTL2Bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
bnZero,
EventSearchConfig,
getTokenInfo,
fixedPointAdjustment,
} from "../../utils";
import { interfaces as sdkInterfaces } from "@across-protocol/sdk";
import { BaseL2BridgeAdapter } from "./BaseL2BridgeAdapter";
Expand All @@ -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,
Expand Down Expand Up @@ -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",
Expand Down
13 changes: 11 additions & 2 deletions src/common/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
BinanceCEXBridge,
BinanceCEXNativeBridge,
SolanaUsdcCCTPBridge,
OFTWethBridge,
} from "../adapter/bridges";
import {
BaseL2BridgeAdapter,
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -546,7 +547,7 @@ export const CUSTOM_BRIDGE: Record<number, Record<string, L1BridgeConstructor<Ba
[TOKEN_SYMBOLS_MAP.ezETH.addresses[CHAIN_IDs.MAINNET]]: HyperlaneXERC20Bridge,
},
[CHAIN_IDs.PLASMA]: {
[TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.MAINNET]]: OFTBridge,
[TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.MAINNET]]: OFTWethBridge,
[TOKEN_SYMBOLS_MAP.USDT.addresses[CHAIN_IDs.MAINNET]]: OFTBridge,
},
[CHAIN_IDs.POLYGON]: {
Expand Down Expand Up @@ -679,6 +680,7 @@ export const CUSTOM_L2_BRIDGE: {
},
[CHAIN_IDs.PLASMA]: {
[TOKEN_SYMBOLS_MAP.USDT.addresses[CHAIN_IDs.MAINNET]]: OFTL2Bridge,
[TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.MAINNET]]: OFTL2Bridge,
},
[CHAIN_IDs.POLYGON]: {
[TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.MAINNET]]: L2UsdcCCTPBridge,
Expand Down Expand Up @@ -1039,6 +1041,13 @@ export const EVM_OFT_MESSENGERS: Map<string, Map<number, EvmAddress>> = new Map(
[CHAIN_IDs.UNICHAIN, EvmAddress.from("0xc07bE8994D035631c36fb4a89C918CeFB2f03EC3")],
]),
],
[
TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.MAINNET],
new Map<number, EvmAddress>([
[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
Expand Down
Loading