Skip to content

Commit 3623945

Browse files
bmzigpxrl
authored andcommitted
feat: add atomic depositor entrypoint for OFT bridges (#2617)
* feat: add atomic depositor entrypoint for OFT bridges Signed-off-by: bennett <bennett@umaproject.org> * import bridge Signed-off-by: bennett <bennett@umaproject.org> * update Signed-off-by: bennett <bennett@umaproject.org> * comment Signed-off-by: bennett <bennett@umaproject.org> * use big number Signed-off-by: bennett <bennett@umaproject.org> --------- Signed-off-by: bennett <bennett@umaproject.org>
1 parent a4760ff commit 3623945

File tree

6 files changed

+166
-22
lines changed

6 files changed

+166
-22
lines changed

src/adapter/bridges/OFTBridge.ts

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ import * as OFT from "../../utils/OFTUtils";
1818
import { OFT_DEFAULT_FEE_CAP, OFT_FEE_CAP_OVERRIDES } from "../../common/Constants";
1919
import { IOFT_ABI_FULL } from "../../common/ContractAddresses";
2020

21+
type OFTBridgeArguments = {
22+
sendParamStruct: OFT.SendParamStruct;
23+
feeStruct: OFT.MessagingFeeStruct;
24+
refundAddress: string;
25+
};
26+
2127
export class OFTBridge extends BaseBridgeAdapter {
2228
public readonly l2TokenAddress: string;
2329
private readonly l1ChainEid: number;
@@ -62,6 +68,36 @@ export class OFTBridge extends BaseBridgeAdapter {
6268
_l2Token: Address,
6369
amount: BigNumber
6470
): Promise<BridgeTransactionDetails> {
71+
const { sendParamStruct, feeStruct, refundAddress } = await this.buildOftTransactionArgs(
72+
toAddress,
73+
l1Token,
74+
amount
75+
);
76+
return {
77+
contract: this.l1Bridge,
78+
method: "send",
79+
args: [sendParamStruct, feeStruct, refundAddress],
80+
value: BigNumber.from(feeStruct.nativeFee),
81+
};
82+
}
83+
84+
/**
85+
* Rounds send amount so that dust doesn't get subtracted from it in the OFT contract.
86+
* @param amount amount to round
87+
* @returns amount rounded down
88+
*/
89+
async roundAmountToSend(amount: BigNumber): Promise<BigNumber> {
90+
// Fetch `sharedDecimals` if not already fetched
91+
this.sharedDecimals ??= await this.l1Bridge.sharedDecimals();
92+
93+
return OFT.roundAmountToSend(amount, this.l1TokenInfo.decimals, this.sharedDecimals);
94+
}
95+
96+
async buildOftTransactionArgs(
97+
toAddress: Address,
98+
l1Token: EvmAddress,
99+
amount: BigNumber
100+
): Promise<OFTBridgeArguments> {
65101
// Verify the token matches the one this bridge was constructed for
66102
assert(
67103
l1Token.eq(this.l1TokenAddress),
@@ -96,24 +132,12 @@ export class OFTBridge extends BaseBridgeAdapter {
96132
// Set refund address to signer's address. This should technically never be required as all of our calcs
97133
// are precise, set it just in case
98134
const refundAddress = await this.l1Bridge.signer.getAddress();
99-
return {
100-
contract: this.l1Bridge,
101-
method: "send",
102-
args: [sendParamStruct, feeStruct, refundAddress],
103-
value: BigNumber.from(feeStruct.nativeFee),
104-
};
105-
}
106135

107-
/**
108-
* Rounds send amount so that dust doesn't get subtracted from it in the OFT contract.
109-
* @param amount amount to round
110-
* @returns amount rounded down
111-
*/
112-
private async roundAmountToSend(amount: BigNumber): Promise<BigNumber> {
113-
// Fetch `sharedDecimals` if not already fetched
114-
this.sharedDecimals ??= await this.l1Bridge.sharedDecimals();
115-
116-
return OFT.roundAmountToSend(amount, this.l1TokenInfo.decimals, this.sharedDecimals);
136+
return {
137+
sendParamStruct,
138+
feeStruct,
139+
refundAddress,
140+
} satisfies OFTBridgeArguments;
117141
}
118142

119143
async queryL1BridgeInitiationEvents(
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { Contract, Signer } from "ethers";
2+
import { BridgeTransactionDetails, BridgeEvents } from "./BaseBridgeAdapter";
3+
import { CONTRACT_ADDRESSES } from "../../common";
4+
import {
5+
BigNumber,
6+
Provider,
7+
EvmAddress,
8+
Address,
9+
winston,
10+
bnZero,
11+
EventSearchConfig,
12+
paginatedEventQuery,
13+
} from "../../utils";
14+
import { OFTBridge } from "./";
15+
import { processEvent } from "../utils";
16+
17+
export class OFTWethBridge extends OFTBridge {
18+
private readonly atomicDepositor: Contract;
19+
20+
constructor(
21+
l2ChainId: number,
22+
l1ChainId: number,
23+
l1Signer: Signer,
24+
l2SignerOrProvider: Signer | Provider,
25+
public readonly l1TokenAddress: EvmAddress,
26+
logger: winston.Logger
27+
) {
28+
super(l2ChainId, l1ChainId, l1Signer, l2SignerOrProvider, l1TokenAddress, logger);
29+
30+
const { address: atomicDepositorAddress, abi: atomicDepositorAbi } =
31+
CONTRACT_ADDRESSES[this.hubChainId].atomicDepositor;
32+
this.atomicDepositor = new Contract(atomicDepositorAddress, atomicDepositorAbi, l1Signer);
33+
34+
// Overwrite the l1 gateway to the atomic depositor address.
35+
this.l1Gateways = [EvmAddress.from(atomicDepositorAddress)];
36+
}
37+
38+
async constructL1ToL2Txn(
39+
toAddress: Address,
40+
l1Token: EvmAddress,
41+
_l2Token: Address,
42+
amount: BigNumber
43+
): Promise<BridgeTransactionDetails> {
44+
const { sendParamStruct, feeStruct, refundAddress } = await this.buildOftTransactionArgs(
45+
toAddress,
46+
l1Token,
47+
amount
48+
);
49+
const bridgeCalldata = this.getL1Bridge().interface.encodeFunctionData("send", [
50+
sendParamStruct,
51+
feeStruct,
52+
refundAddress,
53+
]);
54+
const netValue = feeStruct.nativeFee.add(sendParamStruct.amountLD);
55+
return {
56+
contract: this.atomicDepositor,
57+
method: "bridgeWeth",
58+
args: [this.l2chainId, netValue, sendParamStruct.amountLD, bnZero, bridgeCalldata],
59+
};
60+
}
61+
62+
// We must override the OFTBridge's `queryL1BridgeInitiationEvents` since the depositor into the OFT adapter is the atomic depositor.
63+
// This means if we query off of the OFT adapter, we wouldn't be able to distinguish which deposits correspond to which EOAs.
64+
async queryL1BridgeInitiationEvents(
65+
l1Token: EvmAddress,
66+
fromAddress: Address,
67+
toAddress: Address,
68+
eventConfig: EventSearchConfig
69+
): Promise<BridgeEvents> {
70+
// Return no events if the query is for a different l1 token
71+
if (!l1Token.eq(this.l1TokenAddress)) {
72+
return {};
73+
}
74+
75+
// Return no events if the query is for hubPool
76+
if (fromAddress.eq(this.hubPoolAddress)) {
77+
return {};
78+
}
79+
80+
const isAssociatedSpokePool = this.spokePoolAddress.eq(toAddress);
81+
const events = await paginatedEventQuery(
82+
this.atomicDepositor,
83+
this.atomicDepositor.filters.AtomicWethDepositInitiated(
84+
isAssociatedSpokePool ? this.hubPoolAddress.toNative() : fromAddress.toNative(), // from
85+
this.l2chainId // destinationChainId
86+
),
87+
eventConfig
88+
);
89+
90+
return {
91+
[this.l2TokenAddress]: events.map((event) => {
92+
return processEvent(event, "amount");
93+
}),
94+
};
95+
}
96+
}

src/adapter/bridges/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ export * from "./ZKStackUSDCBridge";
2020
export * from "./ZKStackWethBridge";
2121
export * from "./SolanaUsdcCCTPBridge";
2222
export * from "./OFTBridge";
23+
export * from "./OFTWethBridge";

src/adapter/l2Bridges/OFTL2Bridge.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
bnZero,
1414
EventSearchConfig,
1515
getTokenInfo,
16+
fixedPointAdjustment,
1617
} from "../../utils";
1718
import { interfaces as sdkInterfaces } from "@across-protocol/sdk";
1819
import { BaseL2BridgeAdapter } from "./BaseL2BridgeAdapter";
@@ -27,6 +28,7 @@ export class OFTL2Bridge extends BaseL2BridgeAdapter {
2728
private sharedDecimals?: number;
2829
private readonly nativeFeeCap: BigNumber;
2930
private l2ToL1AmountConverter: (amount: BigNumber) => BigNumber;
31+
private readonly feePct: BigNumber = BigNumber.from(5 * 10 ** 15); // Default fee percent of 0.5%
3032

3133
constructor(
3234
l2chainId: number,
@@ -75,12 +77,16 @@ export class OFTL2Bridge extends BaseL2BridgeAdapter {
7577
// We round `amount` to a specific precision to prevent rounding on the contract side. This way, we
7678
// receive the exact amount we sent in the transaction
7779
const roundedAmount = await this.roundAmountToSend(amount, this.l2TokenInfo.decimals);
80+
const appliedFee = OFT.isStargateBridge(this.l2chainId)
81+
? roundedAmount.mul(this.feePct).div(fixedPointAdjustment) // Set a max slippage of 0.5%.
82+
: bnZero;
83+
const expectedOutputAmount = roundedAmount.sub(appliedFee);
7884
const sendParamStruct: OFT.SendParamStruct = {
7985
dstEid: this.l1ChainEid,
8086
to: OFT.formatToAddress(toAddress),
8187
amountLD: roundedAmount,
8288
// @dev Setting `minAmountLD` equal to `amountLD` ensures we won't hit contract-side rounding
83-
minAmountLD: roundedAmount,
89+
minAmountLD: expectedOutputAmount,
8490
extraOptions: "0x",
8591
composeMsg: "0x",
8692
oftCmd: "0x",

src/common/Constants.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
BinanceCEXBridge,
3838
BinanceCEXNativeBridge,
3939
SolanaUsdcCCTPBridge,
40+
OFTWethBridge,
4041
} from "../adapter/bridges";
4142
import {
4243
BaseL2BridgeAdapter,
@@ -387,7 +388,7 @@ export const SUPPORTED_TOKENS: { [chainId: number]: string[] } = {
387388
"VLR",
388389
"ezETH",
389390
],
390-
[CHAIN_IDs.PLASMA]: ["USDT"],
391+
[CHAIN_IDs.PLASMA]: ["USDT", "WETH"],
391392
[CHAIN_IDs.POLYGON]: ["USDC", "USDT", "WETH", "DAI", "WBTC", "UMA", "BAL", "ACX", "POOL"],
392393
[CHAIN_IDs.REDSTONE]: ["WETH"],
393394
[CHAIN_IDs.SCROLL]: ["WETH", "USDC", "USDT", "WBTC", "POOL"],
@@ -546,7 +547,7 @@ export const CUSTOM_BRIDGE: Record<number, Record<string, L1BridgeConstructor<Ba
546547
[TOKEN_SYMBOLS_MAP.ezETH.addresses[CHAIN_IDs.MAINNET]]: HyperlaneXERC20Bridge,
547548
},
548549
[CHAIN_IDs.PLASMA]: {
549-
[TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.MAINNET]]: OFTBridge,
550+
[TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.MAINNET]]: OFTWethBridge,
550551
[TOKEN_SYMBOLS_MAP.USDT.addresses[CHAIN_IDs.MAINNET]]: OFTBridge,
551552
},
552553
[CHAIN_IDs.POLYGON]: {
@@ -679,6 +680,7 @@ export const CUSTOM_L2_BRIDGE: {
679680
},
680681
[CHAIN_IDs.PLASMA]: {
681682
[TOKEN_SYMBOLS_MAP.USDT.addresses[CHAIN_IDs.MAINNET]]: OFTL2Bridge,
683+
[TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.MAINNET]]: OFTL2Bridge,
682684
},
683685
[CHAIN_IDs.POLYGON]: {
684686
[TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.MAINNET]]: L2UsdcCCTPBridge,
@@ -1039,6 +1041,13 @@ export const EVM_OFT_MESSENGERS: Map<string, Map<number, EvmAddress>> = new Map(
10391041
[CHAIN_IDs.UNICHAIN, EvmAddress.from("0xc07bE8994D035631c36fb4a89C918CeFB2f03EC3")],
10401042
]),
10411043
],
1044+
[
1045+
TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.MAINNET],
1046+
new Map<number, EvmAddress>([
1047+
[CHAIN_IDs.MAINNET, EvmAddress.from("0x77b2043768d28E9C9aB44E1aBfC95944bcE57931")],
1048+
[CHAIN_IDs.PLASMA, EvmAddress.from("0x0cEb237E109eE22374a567c6b09F373C73FA4cBb")],
1049+
]),
1050+
],
10421051
]);
10431052

10441053
// 0.1 ETH is a default cap for chains that use ETH as their gas token

src/utils/OFTUtils.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { OFT_NO_EID } from "@across-protocol/constants";
2-
import { BigNumber, BigNumberish, EvmAddress, PUBLIC_NETWORKS, assert, isDefined } from ".";
2+
import { BigNumber, BigNumberish, EvmAddress, PUBLIC_NETWORKS, assert, isDefined, CHAIN_IDs } from ".";
33
import { BytesLike } from "ethers";
44
import { EVM_OFT_MESSENGERS } from "../common/Constants";
55

@@ -14,7 +14,7 @@ export type SendParamStruct = {
1414
};
1515

1616
export type MessagingFeeStruct = {
17-
nativeFee: BigNumberish;
17+
nativeFee: BigNumber;
1818
lzTokenFee: BigNumberish;
1919
};
2020

@@ -40,6 +40,14 @@ export function getMessengerEvm(l1TokenAddress: EvmAddress, chainId: number): Ev
4040
return messenger;
4141
}
4242

43+
/**
44+
* @param chainId The chain Id of the network to check
45+
* @returns If the input chain ID's OFT adapter requires payment in the input token.
46+
*/
47+
export function isStargateBridge(chainId: number): boolean {
48+
return [CHAIN_IDs.PLASMA].includes(chainId);
49+
}
50+
4351
/**
4452
* @param receiver Address to receive the OFT transfer on target chain
4553
* @returns A 32-byte string to be used when calling on-chain OFT contracts

0 commit comments

Comments
 (0)