Skip to content

Commit

Permalink
feat(lite-chain): ensure all inbound fills have lite repayments (#663)
Browse files Browse the repository at this point in the history
Signed-off-by: james-a-morris <[email protected]>
  • Loading branch information
james-a-morris authored Jun 24, 2024
1 parent a0e8a53 commit 43a2cdf
Show file tree
Hide file tree
Showing 10 changed files with 181 additions and 19 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@across-protocol/sdk",
"author": "UMA Team",
"version": "3.1.0",
"version": "3.1.1",
"license": "AGPL-3.0",
"homepage": "https://docs.across.to/reference/sdk",
"files": [
Expand Down
35 changes: 33 additions & 2 deletions src/clients/AcrossConfigStoreClient/AcrossConfigStoreClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
ConfigStoreVersionUpdate,
DisabledChainsUpdate,
GlobalConfigUpdate,
LiteChainsIdListUpdate,
ParsedTokenConfig,
RouteRateModelUpdate,
SpokePoolTargetBalance,
Expand Down Expand Up @@ -68,7 +69,7 @@ export class AcrossConfigStoreClient extends BaseAbstractClient {
public cumulativeMaxRefundCountUpdates: GlobalConfigUpdate[] = [];
public cumulativeMaxL1TokenCountUpdates: GlobalConfigUpdate[] = [];
public chainIdIndicesUpdates: GlobalConfigUpdate<number[]>[] = [];
public liteChainIndicesUpdates: GlobalConfigUpdate<number[]>[] = [];
public liteChainIndicesUpdates: LiteChainsIdListUpdate[] = [];
public cumulativeSpokeTargetBalanceUpdates: SpokeTargetBalanceUpdate[] = [];
public cumulativeConfigStoreVersionUpdates: ConfigStoreVersionUpdate[] = [];
public cumulativeDisabledChainUpdates: DisabledChainsUpdate[] = [];
Expand Down Expand Up @@ -166,6 +167,27 @@ export class AcrossConfigStoreClient extends BaseAbstractClient {
return liteChainIdList.find((update) => update.blockNumber <= blockNumber)?.value ?? [];
}

/**
* Resolves the lite chain ids that were available to the protocol at a given timestamp.
* @param timestamp Timestamp to search for. Defaults to latest time - in seconds.
* @returns List of lite chain IDs that were available to the protocol at the given timestamp.
* @note This dynamic functionality has been added after the launch of Across.
*/
getLiteChainIdIndicesForTimestamp(timestamp: number = Number.MAX_SAFE_INTEGER): number[] {
const liteChainIdList = sortEventsDescending(this.liteChainIndicesUpdates);
return liteChainIdList.find((update) => update.timestamp <= timestamp)?.value ?? [];
}

/**
* Checks if a chain ID was a lite chain at a given timestamp.
* @param chainId The chain ID to check.
* @param timestamp The timestamp to check. Defaults to latest time - in seconds.
* @returns True if the chain ID was a lite chain at the given timestamp. False otherwise.
*/
isChainLiteChainAtTimestamp(chainId: number, timestamp: number = Number.MAX_SAFE_INTEGER): boolean {
return this.getLiteChainIdIndicesForTimestamp(timestamp).includes(chainId);
}

getSpokeTargetBalancesForBlock(
l1Token: string,
chainId: number,
Expand Down Expand Up @@ -425,6 +447,15 @@ export class AcrossConfigStoreClient extends BaseAbstractClient {
// the on-chain string has quotes around the array, which will parse our JSON as a
// string instead of an array. We need to remove these quotes before parsing.
// To be sure, we can check for single quotes, double quotes, and spaces.

// Use a regular expression to check if the string is a valid array. We need to check for
// leading and trailing quotes, as well as leading and trailing whitespace. We also need to
// check for commas between the numbers. Alternatively, this can be an empty array.
if (!/^\s*["']?\[(\d+(,\d+)*)?\]["']?\s*$/.test(args.value)) {
this.logger.warn({ at: "ConfigStore", message: `The array ${args.value} is invalid.` });
// If not a valid array, skip.
continue;
}
const chainIndices = JSON.parse(args.value.replace(/['"\s]/g, ""));
// Check that the array is valid and that every element is a number.
if (!isArrayOf<number>(chainIndices, isPositiveInteger)) {
Expand All @@ -442,7 +473,7 @@ export class AcrossConfigStoreClient extends BaseAbstractClient {
continue;
}
// If all else passes, we can add this update.
this.liteChainIndicesUpdates.push({ ...args, value: chainIndices });
this.liteChainIndicesUpdates.push({ ...args, value: chainIndices, timestamp: globalConfigUpdateTimes[i] });
} else if (args.key === utf8ToHex(GLOBAL_CONFIG_STORE_KEYS.CHAIN_ID_INDICES)) {
try {
// We need to parse the chain ID indices array from the stringified JSON. However,
Expand Down
16 changes: 16 additions & 0 deletions src/clients/SpokePoolClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { getNetworkName } from "../utils/NetworkUtils";
import { getBlockRangeForDepositId, getDepositIdAtBlock } from "../utils/SpokeUtils";
import { BaseAbstractClient, isUpdateFailureReason, UpdateFailureReason } from "./BaseAbstractClient";
import { HubPoolClient } from "./HubPoolClient";
import { AcrossConfigStoreClient } from "./AcrossConfigStoreClient";

type SpokePoolUpdateSuccess = {
success: true;
Expand Down Expand Up @@ -73,6 +74,7 @@ export class SpokePoolClient extends BaseAbstractClient {
protected rootBundleRelays: RootBundleRelayWithBlock[] = [];
protected relayerRefundExecutions: RelayerRefundExecutionWithBlock[] = [];
protected queryableEventNames: string[] = [];
protected configStoreClient: AcrossConfigStoreClient | undefined;
public earliestDepositIdQueried = Number.MAX_SAFE_INTEGER;
public latestDepositIdQueried = 0;
public firstDepositIdForSpokePool = Number.MAX_SAFE_INTEGER;
Expand Down Expand Up @@ -101,6 +103,7 @@ export class SpokePoolClient extends BaseAbstractClient {
this.firstBlockToSearch = eventSearchConfig.fromBlock;
this.latestBlockSearched = 0;
this.queryableEventNames = Object.keys(this._queryableEventNames());
this.configStoreClient = hubPoolClient?.configStoreClient;
}

public _queryableEventNames(): { [eventName: string]: EventFilter } {
Expand Down Expand Up @@ -543,6 +546,7 @@ export class SpokePoolClient extends BaseAbstractClient {
// Derive and append the common properties that are not part of the onchain event.
const { quoteBlock: quoteBlockNumber } = dataForQuoteTime[index];
const deposit = { ...(rawDeposit as DepositWithBlock), originChainId: this.chainId, quoteBlockNumber };
deposit.originatesFromLiteChain = this.doesDepositOriginateFromLiteChain(deposit);
if (deposit.outputToken === ZERO_ADDRESS) {
deposit.outputToken = this.getDestinationTokenForDeposit(deposit);
}
Expand Down Expand Up @@ -616,6 +620,7 @@ export class SpokePoolClient extends BaseAbstractClient {
...(spreadEventWithBlockNumber(event) as FillWithBlock),
destinationChainId: this.chainId,
};

assign(this.fills, [fill.originChainId], [fill]);
assign(this.depositHashesToFills, [this.getDepositHash(fill)], [fill]);
}
Expand Down Expand Up @@ -838,6 +843,7 @@ export class SpokePoolClient extends BaseAbstractClient {
? this.getDestinationTokenForDeposit({ ...partialDeposit, originChainId: this.chainId })
: partialDeposit.outputToken,
};
deposit.originatesFromLiteChain = this.doesDepositOriginateFromLiteChain(deposit);

this.logger.debug({
at: "SpokePoolClient#findDeposit",
Expand All @@ -848,4 +854,14 @@ export class SpokePoolClient extends BaseAbstractClient {

return deposit;
}

/**
* Determines whether a deposit originates from a lite chain.
* @param deposit The deposit to evaluate.
* @returns True if the deposit originates from a lite chain, false otherwise. If the hub pool client is not defined,
* this method will return false.
*/
protected doesDepositOriginateFromLiteChain(deposit: DepositWithBlock): boolean {
return this.configStoreClient?.isChainLiteChainAtTimestamp(deposit.originChainId, deposit.quoteTimestamp) ?? false;
}
}
5 changes: 5 additions & 0 deletions src/clients/mocks/MockSpokePoolClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { bnZero, toBN, toBNWei, forEachAsync, getCurrentTime, randomAddress } fr
import { SpokePoolClient, SpokePoolUpdate } from "../SpokePoolClient";
import { HubPoolClient } from "../HubPoolClient";
import { EventManager, EventOverrides, getEventManager } from "./MockEvents";
import { AcrossConfigStoreClient } from "../AcrossConfigStoreClient";

type Block = providers.Block;

Expand Down Expand Up @@ -51,6 +52,10 @@ export class MockSpokePoolClient extends SpokePoolClient {
this.realizedLpFeePctOverride = true;
}

setConfigStoreClient(configStore?: AcrossConfigStoreClient): void {
this.configStoreClient = configStore;
}

clearDefaultRealizedLpFeePct(): void {
this.realizedLpFeePctOverride = false;
}
Expand Down
4 changes: 4 additions & 0 deletions src/interfaces/ConfigStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ export interface DisabledChainsUpdate extends SortableEvent {
chainIds: number[];
}

export interface LiteChainsIdListUpdate<ValueStore = number[]> extends GlobalConfigUpdate<ValueStore> {
timestamp: number;
}

/**
* A generic type of a dictionary that has string keys and values of type T. This
* record is enforced to have a default entry within the "default" key.
Expand Down
1 change: 1 addition & 0 deletions src/interfaces/SpokePool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface Deposit extends RelayData {
updatedRecipient?: string;
updatedOutputAmount?: BigNumber;
updatedMessage?: string;
originatesFromLiteChain: boolean;
}

export interface DepositWithBlock extends Deposit, SortableEvent {
Expand Down
58 changes: 57 additions & 1 deletion test/ConfigStoreClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
MAX_L1_TOKENS_PER_POOL_REBALANCE_LEAF,
MAX_REFUNDS_PER_RELAYER_REFUND_LEAF,
destinationChainId,
originChainId,
} from "./constants";
import { DEFAULT_CONFIG_STORE_VERSION, MockConfigStoreClient } from "./mocks";
import {
Expand All @@ -17,7 +18,6 @@ import {
getContractFactory,
hubPoolFixture,
mineRandomBlocks,
originChainId,
toBN,
toWei,
utf8ToHex,
Expand Down Expand Up @@ -367,6 +367,62 @@ describe("AcrossConfigStoreClient", function () {
configStoreClient.getMaxL1TokenCountForPoolRebalanceLeafForBlock(initialUpdate.blockNumber - 1)
).to.throw(/Could not find MaxL1TokenCount/);
});
it("Should fail if lite chain ID updates are invalid", async function () {
// Push invalid update
await configStore.updateGlobalConfig(
utf8ToHex(GLOBAL_CONFIG_STORE_KEYS.LITE_CHAIN_ID_INDICES),
JSON.stringify(["4a"])
);
// Push invalid update
await configStore.updateGlobalConfig(
utf8ToHex(GLOBAL_CONFIG_STORE_KEYS.LITE_CHAIN_ID_INDICES),
JSON.stringify([1, 1])
);
// Push valid update
await configStore.updateGlobalConfig(
utf8ToHex(GLOBAL_CONFIG_STORE_KEYS.LITE_CHAIN_ID_INDICES),
JSON.stringify([1])
);
await configStoreClient.update();
expect(configStoreClient.liteChainIndicesUpdates.length).to.equal(1);
});
it("Should test lite chain ID updates", async function () {
const update1 = await configStore.updateGlobalConfig(
utf8ToHex(GLOBAL_CONFIG_STORE_KEYS.LITE_CHAIN_ID_INDICES),
JSON.stringify([1])
);
let timestamp = (await configStore.provider.getBlock(update1.blockNumber!)).timestamp;
expect(timestamp).to.not.be.undefined;

// Set the bounds for the new lite chain
const timestampBeforeNewLiteChains = timestamp - 5;
const timestampAfterNewLiteChains = timestamp + 5;

const blockBeforeNewLiteChains = update1.blockNumber! - 1;
const blockAfterNewLiteChains = update1.blockNumber! + 1;

await configStoreClient.update();
// Test the getliteChainIdIndicesForTimestamp function
expect(configStoreClient.getLiteChainIdIndicesForTimestamp(timestampBeforeNewLiteChains)).to.deep.equal([]);
expect(configStoreClient.getLiteChainIdIndicesForTimestamp(timestampAfterNewLiteChains)).to.deep.equal([1]);
// Test the getliteChainIdIndicesForBlock function
expect(configStoreClient.getLiteChainIdIndicesForBlock(blockBeforeNewLiteChains)).to.deep.equal([]);
expect(configStoreClient.getLiteChainIdIndicesForBlock(blockAfterNewLiteChains)).to.deep.equal([1]);

const update2 = await configStore.updateGlobalConfig(
utf8ToHex(GLOBAL_CONFIG_STORE_KEYS.LITE_CHAIN_ID_INDICES),
JSON.stringify([1, 15])
);
timestamp = (await configStore.provider.getBlock(update2.blockNumber!)).timestamp;
expect(timestamp).to.not.be.undefined;
const timestampAfterLiteChainUpdate = timestamp + 5;
const blockAfterLiteChainUpdate = update2.blockNumber! + 1;

await configStoreClient.update();

expect(configStoreClient.getLiteChainIdIndicesForTimestamp(timestampAfterLiteChainUpdate)).to.deep.equal([1, 15]);
expect(configStoreClient.getLiteChainIdIndicesForBlock(blockAfterLiteChainUpdate)).to.deep.equal([1, 15]);
});
it("Get disabled chain IDs for block range", async function () {
// set all possible chains for the next several tests
const allPossibleChains = [1, 19, 21, 23];
Expand Down
2 changes: 1 addition & 1 deletion test/SpokePoolClient.SpeedUp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ describe("SpokePoolClient: SpeedUp", function () {
deepEqualsWithBigNumber(
spokePoolClient.getDepositsForDestinationChain(destinationChainId)[0],
expectedDepositData,
[...ignoredFields, "realizedLpFeePct"]
[...ignoredFields, "realizedLpFeePct", "originatesFromLiteChain"]
)
).to.be.true;
expect(spokePoolClient.getDepositsForDestinationChain(destinationChainId).length).to.equal(1);
Expand Down
4 changes: 2 additions & 2 deletions test/SpokePoolClient.ValidateFill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ describe("SpokePoolClient: Fill Validation", function () {
await spokePoolClient1.update();

expect(spokePoolClient1.getDepositForFill(fill))
.excludingEvery(["realizedLpFeePct", "quoteBlockNumber"])
.excludingEvery(["realizedLpFeePct", "quoteBlockNumber", "originatesFromLiteChain"])
.to.deep.equal(deposit);
});

Expand Down Expand Up @@ -631,7 +631,7 @@ describe("SpokePoolClient: Fill Validation", function () {
expect(fill_2.relayExecutionInfo.fillType === FillType.FastFill).to.be.true;

expect(spokePoolClient1.getDepositForFill(fill_1))
.excludingEvery(["quoteBlockNumber", "realizedLpFeePct"])
.excludingEvery(["quoteBlockNumber", "realizedLpFeePct", "originatesFromLiteChain"])
.to.deep.equal(deposit_1);
expect(spokePoolClient1.getDepositForFill(fill_2)).to.equal(undefined);

Expand Down
Loading

0 comments on commit 43a2cdf

Please sign in to comment.