Skip to content
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

feat: start using a custom Address class #904

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
"async": "^3.2.5",
"axios": "^0.27.2",
"big-number": "^2.0.0",
"bs58": "^6.0.0",
"decimal.js": "^10.3.1",
"ethers": "^5.7.2",
"lodash": "^4.17.21",
Expand Down
16 changes: 9 additions & 7 deletions src/clients/AcrossConfigStoreClient/AcrossConfigStoreClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
toBN,
toWei,
utf8ToHex,
EvmAddress,
} from "../../utils";
import { PROTOCOL_DEFAULT_CHAIN_ID_INDICES } from "../../constants";
import {
Expand Down Expand Up @@ -108,7 +109,7 @@ export class AcrossConfigStoreClient extends BaseAbstractClient {
}

getRateModelForBlockNumber(
l1Token: string,
l1Token: EvmAddress,
originChainId: number | string,
destinationChainId: number | string,
blockNumber: number | undefined = undefined
Expand All @@ -122,7 +123,7 @@ export class AcrossConfigStoreClient extends BaseAbstractClient {

const defaultRateModelUpdate = sortEventsDescending(this.cumulativeRateModelUpdates).find(
(config) =>
config.blockNumber <= (blockNumber ?? 0) && config.l1Token === l1Token && config.rateModel !== undefined
config.blockNumber <= (blockNumber ?? 0) && config.l1Token.eq(l1Token) && config.rateModel !== undefined
);
if (!defaultRateModelUpdate) {
throw new Error(`Could not find TokenConfig update for ${l1Token} at block ${blockNumber}`);
Expand All @@ -131,12 +132,12 @@ export class AcrossConfigStoreClient extends BaseAbstractClient {
}

getRouteRateModelForBlockNumber(
l1Token: string,
l1Token: EvmAddress,
route: string,
blockNumber: number | undefined = undefined
): RateModel | undefined {
const config = (sortEventsDescending(this.cumulativeRouteRateModelUpdates) as RouteRateModelUpdate[]).find(
(config) => config.blockNumber <= (blockNumber ?? 0) && config.l1Token === l1Token
(config) => config.blockNumber <= (blockNumber ?? 0) && config.l1Token.eq(l1Token)
);
if (config?.routeRateModel[route] === undefined) {
return undefined;
Expand Down Expand Up @@ -212,12 +213,12 @@ export class AcrossConfigStoreClient extends BaseAbstractClient {
}

getSpokeTargetBalancesForBlock(
l1Token: string,
l1Token: EvmAddress,
chainId: number,
blockNumber: number = Number.MAX_SAFE_INTEGER
): SpokePoolTargetBalance {
const config = (sortEventsDescending(this.cumulativeSpokeTargetBalanceUpdates) as SpokeTargetBalanceUpdate[]).find(
(config) => config.l1Token === l1Token && config.blockNumber <= blockNumber
(config) => config.l1Token.eq(l1Token) && config.blockNumber <= blockNumber
);
const targetBalance = config?.spokeTargetBalances?.[chainId];
return targetBalance || { target: toBN(0), threshold: toBN(0) };
Expand Down Expand Up @@ -390,9 +391,10 @@ export class AcrossConfigStoreClient extends BaseAbstractClient {

try {
const { rateModel, routeRateModel, spokeTargetBalances } = this.validateTokenConfigUpdate(args);
const { value, key: l1Token, ...eventData } = args;
const { value, key: _l1Token, ...eventData } = args;

if (rateModel !== undefined) {
const l1Token = EvmAddress.fromHex(_l1Token);
this.cumulativeRateModelUpdates.push({ ...eventData, rateModel, l1Token });
this.cumulativeSpokeTargetBalanceUpdates.push({
...eventData,
Expand Down
69 changes: 40 additions & 29 deletions src/clients/BundleDataClient/BundleDataClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import {
findFillEvent,
isZeroValueFillOrSlowFillRequest,
chainIsEvm,
isValidEvmAddress,
Address,
duplicateEvent,
} from "../../utils";
import winston from "winston";
Expand All @@ -67,18 +67,20 @@ type DataCache = Record<string, Promise<LoadDataReturnValue>>;
// V3 dictionary helper functions
function updateExpiredDepositsV3(dict: ExpiredDepositsToRefundV3, deposit: V3DepositWithBlock): void {
// A deposit refund for a deposit is invalid if the depositor has a bytes32 address input for an EVM chain. It is valid otherwise.
if (chainIsEvm(deposit.originChainId) && !isValidEvmAddress(deposit.depositor)) {
if (chainIsEvm(deposit.originChainId) && !deposit.depositor.isValidEvmAddress()) {
Copy link
Contributor

@pxrl pxrl Mar 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wdyt about internalising the chain -> address type mapping within the Address class, such that we could write: !deposit.depositor.isValidOn(deposit.originChainId) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. Done here 16dbd7e

return;
}
const { originChainId, inputToken } = deposit;
const { originChainId, inputToken: _inputToken } = deposit;
const inputToken = _inputToken.toString();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dohaki @gsteenkamp89 Can you think of any way we can make the toString automatic, so we can rely on it happening in the background when inputToken is supplied as a key to an Object? Not sure if that's even possible, but if not, this usage scenario represents a sharp edge that we'll probably run into.

if (!dict?.[originChainId]?.[inputToken]) {
assign(dict, [originChainId, inputToken], []);
}
dict[originChainId][inputToken].push(deposit);
}

function updateBundleDepositsV3(dict: BundleDepositsV3, deposit: V3DepositWithBlock): void {
const { originChainId, inputToken } = deposit;
const { originChainId, inputToken: _inputToken } = deposit;
const inputToken = _inputToken.toString();
if (!dict?.[originChainId]?.[inputToken]) {
assign(dict, [originChainId, inputToken], []);
}
Expand All @@ -90,16 +92,16 @@ function updateBundleFillsV3(
fill: V3FillWithBlock,
lpFeePct: BigNumber,
repaymentChainId: number,
repaymentToken: string,
repaymentAddress: string
repaymentToken: Address,
repaymentAddress: Address
): void {
// We shouldn't pass any unrepayable fills into this function, so we perform an extra safety check.
assert(
chainIsEvm(repaymentChainId) && isValidEvmAddress(fill.relayer),
chainIsEvm(repaymentChainId) && fill.relayer.isValidEvmAddress(),
"validatedBundleV3Fills dictionary should only contain fills with valid repayment information"
);
if (!dict?.[repaymentChainId]?.[repaymentToken]) {
assign(dict, [repaymentChainId, repaymentToken], {
if (!dict?.[repaymentChainId]?.[repaymentToken.toString()]) {
assign(dict, [repaymentChainId, repaymentToken.toString()], {
fills: [],
totalRefundAmount: bnZero,
realizedLpFees: bnZero,
Expand All @@ -113,7 +115,7 @@ function updateBundleFillsV3(
assign(dict, [repaymentChainId, repaymentToken, "fills"], [bundleFill]);

// All fills update the bundle LP fees.
const refundObj = dict[repaymentChainId][repaymentToken];
const refundObj = dict[repaymentChainId][repaymentToken.toString()];
const realizedLpFee = bundleFill.inputAmount.mul(bundleFill.lpFeePct).div(fixedPointAdjustment);
refundObj.realizedLpFees = refundObj.realizedLpFees ? refundObj.realizedLpFees.add(realizedLpFee) : realizedLpFee;

Expand All @@ -127,10 +129,11 @@ function updateBundleFillsV3(
// Instantiate dictionary if it doesn't exist.
refundObj.refunds ??= {};

if (refundObj.refunds[bundleFill.relayer]) {
refundObj.refunds[bundleFill.relayer] = refundObj.refunds[bundleFill.relayer].add(refundAmount);
if (refundObj.refunds[bundleFill.relayer.toString()]) {
refundObj.refunds[bundleFill.relayer.toString()] =
refundObj.refunds[bundleFill.relayer.toString()].add(refundAmount);
} else {
refundObj.refunds[bundleFill.relayer] = refundAmount;
refundObj.refunds[bundleFill.relayer.toString()] = refundAmount;
}
}
}
Expand All @@ -139,18 +142,20 @@ function updateBundleExcessSlowFills(
dict: BundleExcessSlowFills,
deposit: V3DepositWithBlock & { lpFeePct: BigNumber }
): void {
const { destinationChainId, outputToken } = deposit;
const { destinationChainId, outputToken: _outputToken } = deposit;
const outputToken = _outputToken.toString();
if (!dict?.[destinationChainId]?.[outputToken]) {
assign(dict, [destinationChainId, outputToken], []);
}
dict[destinationChainId][outputToken].push(deposit);
}

function updateBundleSlowFills(dict: BundleSlowFills, deposit: V3DepositWithBlock & { lpFeePct: BigNumber }): void {
if (chainIsEvm(deposit.destinationChainId) && !isValidEvmAddress(deposit.recipient)) {
if (chainIsEvm(deposit.destinationChainId) && !deposit.recipient.isValidEvmAddress()) {
return;
}
const { destinationChainId, outputToken } = deposit;
const { destinationChainId, outputToken: _outputToken } = deposit;
const outputToken = _outputToken.toString();
if (!dict?.[destinationChainId]?.[outputToken]) {
assign(dict, [destinationChainId, outputToken], []);
}
Expand Down Expand Up @@ -410,21 +415,23 @@ export class BundleDataClient {
// worst from the relayer's perspective.
const { relayer, inputAmount: refundAmount } = fill;
refundsForChain[chainToSendRefundTo] ??= {};
refundsForChain[chainToSendRefundTo][repaymentToken] ??= {};
const existingRefundAmount = refundsForChain[chainToSendRefundTo][repaymentToken][relayer] ?? bnZero;
refundsForChain[chainToSendRefundTo][repaymentToken][relayer] = existingRefundAmount.add(refundAmount);
refundsForChain[chainToSendRefundTo][repaymentToken.toString()] ??= {};
const existingRefundAmount =
refundsForChain[chainToSendRefundTo][repaymentToken.toString()][relayer.toString()] ?? bnZero;
refundsForChain[chainToSendRefundTo][repaymentToken.toString()][relayer.toString()] =
existingRefundAmount.add(refundAmount);
});
}
return refundsForChain;
}

getUpcomingDepositAmount(chainId: number, l2Token: string, latestBlockToSearch: number): BigNumber {
getUpcomingDepositAmount(chainId: number, l2Token: Address, latestBlockToSearch: number): BigNumber {
if (this.spokePoolClients[chainId] === undefined) {
return toBN(0);
}
return this.spokePoolClients[chainId]
.getDeposits()
.filter((deposit) => deposit.blockNumber > latestBlockToSearch && deposit.inputToken === l2Token)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dohaki @gsteenkamp89 Are you aware of any eslint rule we could use to flag checking equality between Address instances? @bmzig has removed all known instances from this PR but it seems like something we'd trip on in future.

.filter((deposit) => deposit.blockNumber > latestBlockToSearch && deposit.inputToken.eq(l2Token))
.reduce((acc, deposit) => {
return acc.add(deposit.inputAmount);
}, toBN(0));
Expand Down Expand Up @@ -546,14 +553,14 @@ export class BundleDataClient {
.filter((leaf) => leaf.rootBundleId === bundle.rootBundleId);
const executedRefunds: { [tokenAddress: string]: { [relayer: string]: BigNumber } } = {};
for (const refundLeaf of executedRefundLeaves) {
const tokenAddress = refundLeaf.l2TokenAddress;
const tokenAddress = refundLeaf.l2TokenAddress.toString();
if (executedRefunds[tokenAddress] === undefined) {
executedRefunds[tokenAddress] = {};
}
const executedTokenRefunds = executedRefunds[tokenAddress];

for (let i = 0; i < refundLeaf.refundAddresses.length; i++) {
const relayer = refundLeaf.refundAddresses[i];
const relayer = refundLeaf.refundAddresses[i].toString();
const refundAmount = refundLeaf.refundAmounts[i];
if (executedTokenRefunds[relayer] === undefined) {
executedTokenRefunds[relayer] = bnZero;
Expand Down Expand Up @@ -599,15 +606,15 @@ export class BundleDataClient {
return allRefunds;
}

getRefundsFor(bundleRefunds: CombinedRefunds, relayer: string, chainId: number, token: string): BigNumber {
if (!bundleRefunds[chainId] || !bundleRefunds[chainId][token]) {
getRefundsFor(bundleRefunds: CombinedRefunds, relayer: Address, chainId: number, token: Address): BigNumber {
if (!bundleRefunds[chainId] || !bundleRefunds[chainId][token.toString()]) {
return BigNumber.from(0);
}
const allRefunds = bundleRefunds[chainId][token];
return allRefunds && allRefunds[relayer] ? allRefunds[relayer] : BigNumber.from(0);
const allRefunds = bundleRefunds[chainId][token.toString()];
return allRefunds && allRefunds[relayer.toString()] ? allRefunds[relayer.toString()] : BigNumber.from(0);
}

getTotalRefund(refunds: CombinedRefunds[], relayer: string, chainId: number, refundToken: string): BigNumber {
getTotalRefund(refunds: CombinedRefunds[], relayer: Address, chainId: number, refundToken: Address): BigNumber {
return refunds.reduce((totalRefund, refunds) => {
return totalRefund.add(this.getRefundsFor(refunds, relayer, chainId, refundToken));
}, bnZero);
Expand Down Expand Up @@ -820,7 +827,11 @@ export class BundleDataClient {
"Not using correct bundle deposit hash key"
);
if (deposit.blockNumber >= originChainBlockRange[0]) {
if (bundleDepositsV3?.[originChainId]?.[deposit.inputToken]?.find((d) => duplicateEvent(deposit, d))) {
if (
bundleDepositsV3?.[originChainId]?.[deposit.inputToken.toString()]?.find((d) =>
duplicateEvent(deposit, d)
)
) {
this.logger.debug({
at: "BundleDataClient#loadData",
message: "Duplicate deposit detected",
Expand Down
19 changes: 12 additions & 7 deletions src/clients/BundleDataClient/utils/DataworkerUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
fixedPointAdjustment,
count2DDictionaryValues,
count3DDictionaryValues,
Address,
} from "../../../utils";
import {
addLastRunningBalance,
Expand Down Expand Up @@ -65,10 +66,10 @@ export function getRefundsFromBundle(
Object.entries(depositsForChain).forEach(([l2TokenAddress, deposits]) => {
deposits.forEach((deposit) => {
if (combinedRefunds[originChainId][l2TokenAddress] === undefined) {
combinedRefunds[originChainId][l2TokenAddress] = { [deposit.depositor]: deposit.inputAmount };
combinedRefunds[originChainId][l2TokenAddress] = { [deposit.depositor.toString()]: deposit.inputAmount };
} else {
const existingRefundAmount = combinedRefunds[originChainId][l2TokenAddress][deposit.depositor];
combinedRefunds[originChainId][l2TokenAddress][deposit.depositor] = deposit.inputAmount.add(
const existingRefundAmount = combinedRefunds[originChainId][l2TokenAddress][deposit.depositor.toString()];
combinedRefunds[originChainId][l2TokenAddress][deposit.depositor.toString()] = deposit.inputAmount.add(
existingRefundAmount ?? bnZero
);
}
Expand Down Expand Up @@ -145,7 +146,8 @@ export function _buildPoolRebalanceRoot(
Object.entries(bundleFillsV3).forEach(([_repaymentChainId, fillsForChain]) => {
const repaymentChainId = Number(_repaymentChainId);
Object.entries(fillsForChain).forEach(
([l2TokenAddress, { realizedLpFees: totalRealizedLpFee, totalRefundAmount }]) => {
([_l2TokenAddress, { realizedLpFees: totalRealizedLpFee, totalRefundAmount }]) => {
const l2TokenAddress = Address.fromHex(_l2TokenAddress);
const l1TokenCounterpart = clients.hubPoolClient.getL1TokenForL2TokenAtBlock(
l2TokenAddress,
repaymentChainId,
Expand All @@ -167,7 +169,8 @@ export function _buildPoolRebalanceRoot(
// Increment the updatedOutputAmount to the destination chain.
Object.entries(bundleSlowFillsV3).forEach(([_destinationChainId, depositsForChain]) => {
const destinationChainId = Number(_destinationChainId);
Object.entries(depositsForChain).forEach(([outputToken, deposits]) => {
Object.entries(depositsForChain).forEach(([_outputToken, deposits]) => {
const outputToken = Address.fromHex(_outputToken);
deposits.forEach((deposit) => {
const l1TokenCounterpart = clients.hubPoolClient.getL1TokenForL2TokenAtBlock(
outputToken,
Expand All @@ -191,7 +194,8 @@ export function _buildPoolRebalanceRoot(
// the updatedOutputAmount = inputAmount - lpFees.
Object.entries(unexecutableSlowFills).forEach(([_destinationChainId, slowFilledDepositsForChain]) => {
const destinationChainId = Number(_destinationChainId);
Object.entries(slowFilledDepositsForChain).forEach(([outputToken, slowFilledDeposits]) => {
Object.entries(slowFilledDepositsForChain).forEach(([_outputToken, slowFilledDeposits]) => {
const outputToken = Address.fromHex(_outputToken);
slowFilledDeposits.forEach((deposit) => {
const l1TokenCounterpart = clients.hubPoolClient.getL1TokenForL2TokenAtBlock(
outputToken,
Expand Down Expand Up @@ -227,7 +231,8 @@ export function _buildPoolRebalanceRoot(
// Add origin chain running balance for expired v3 deposits. These should refund the inputAmount.
Object.entries(expiredDepositsToRefundV3).forEach(([_originChainId, depositsForChain]) => {
const originChainId = Number(_originChainId);
Object.entries(depositsForChain).forEach(([inputToken, deposits]) => {
Object.entries(depositsForChain).forEach(([_inputToken, deposits]) => {
const inputToken = Address.fromHex(_inputToken);
deposits.forEach((deposit) => {
const l1TokenCounterpart = clients.hubPoolClient.getL1TokenForL2TokenAtBlock(
inputToken,
Expand Down
11 changes: 6 additions & 5 deletions src/clients/BundleDataClient/utils/FillUtils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import _ from "lodash";
import { providers } from "ethers";
import { Deposit, DepositWithBlock, Fill, FillWithBlock } from "../../../interfaces";
import { getBlockRangeForChain, isSlowFill, isValidEvmAddress, isDefined, chainIsEvm } from "../../../utils";
import { getBlockRangeForChain, isSlowFill, toAddress, isDefined, chainIsEvm, Address } from "../../../utils";
import { HubPoolClient } from "../../HubPoolClient";

export function getRefundInformationFromFill(
Expand All @@ -12,7 +12,7 @@ export function getRefundInformationFromFill(
fromLiteChain: boolean
): {
chainToSendRefundTo: number;
repaymentToken: string;
repaymentToken: Address;
} {
// Handle slow relay where repaymentChainId = 0. Slow relays always pay recipient on destination chain.
// So, save the slow fill under the destination chain, and save the fast fill under its repayment chain.
Expand Down Expand Up @@ -100,13 +100,14 @@ export async function verifyFillRepayment(
repaymentChainId = fill.destinationChainId;
}

if (!isValidEvmAddress(fill.relayer)) {
if (!fill.relayer.isValidEvmAddress()) {
// TODO: Handle case where fill was sent on non-EVM chain, in which case the following call would fail
// or return something unexpected. We'd want to return undefined here.
const fillTransaction = await destinationChainProvider.getTransaction(fill.transactionHash);
const destinationRelayer = fillTransaction?.from;
const replacementRelayer = toAddress(destinationRelayer, repaymentChainId, false);
// Repayment chain is still an EVM chain, but the msg.sender is a bytes32 address, so the fill is invalid.
if (!isDefined(destinationRelayer) || !isValidEvmAddress(destinationRelayer)) {
if (!isDefined(destinationRelayer) || !replacementRelayer.isValidEvmAddress()) {
return undefined;
}
if (!matchedDeposit.fromLiteChain) {
Expand All @@ -118,7 +119,7 @@ export async function verifyFillRepayment(
return undefined;
}
}
fill.relayer = destinationRelayer;
fill.relayer = replacementRelayer;
}

// Repayment address is now valid and repayment chain is either origin chain for lite chain or the destination
Expand Down
Loading