Skip to content

Commit 1f41ea6

Browse files
authored
feat(relayFeeCalculator): add support for origin chain config override (#1021)
Signed-off-by: Gerhard Steenkamp <gerhard@umaproject.org>
1 parent 6cf5cc8 commit 1f41ea6

File tree

2 files changed

+171
-11
lines changed

2 files changed

+171
-11
lines changed

src/relayFeeCalculator/relayFeeCalculator.ts

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export interface CapitalCostConfigOverride {
5050
default: CapitalCostConfig;
5151
routeOverrides?: Record<ChainIdAsString, Record<ChainIdAsString, CapitalCostConfig>>;
5252
destinationChainOverrides?: Record<ChainIdAsString, CapitalCostConfig>;
53+
originChainOverrides?: Record<ChainIdAsString, CapitalCostConfig>;
5354
}
5455
export type RelayCapitalCostConfig = CapitalCostConfigOverride | CapitalCostConfig;
5556
export interface BaseRelayFeeCalculatorConfig {
@@ -155,6 +156,8 @@ export class RelayFeeCalculator {
155156
);
156157
assert(Object.keys(this.capitalCostsConfig).length > 0, "capitalCostsConfig must have at least one entry");
157158
this.logger = logger || DEFAULT_LOGGER;
159+
// warn developer of any possible conflicting config overrides
160+
this.checkAllConfigConflicts();
158161
}
159162

160163
/**
@@ -194,8 +197,11 @@ export class RelayFeeCalculator {
194197
for (const toChainIdRoutes of Object.values(config.routeOverrides || {})) {
195198
Object.values(toChainIdRoutes).forEach(this.validateCapitalCostsConfig);
196199
}
200+
// Validate origin chain overrides
201+
Object.values(config.originChainOverrides || {}).forEach(this.validateCapitalCostsConfig);
197202
// Validate destination chain overrides
198203
Object.values(config.destinationChainOverrides || {}).forEach(this.validateCapitalCostsConfig);
204+
199205
return config;
200206
}
201207

@@ -326,9 +332,26 @@ export class RelayFeeCalculator {
326332
// bound to an upper bound. After the kink, the fee % increase will be fixed, and slowly approach the upper bound
327333
// for very large amount inputs.
328334
else {
329-
const destinationChainOverride = tokenCostConfig?.destinationChainOverrides?.[_destinationRoute || ""];
335+
// Order of specificity (most specific to least specific):
336+
// 1. Route overrides (both origin and destination)
337+
// 2. Destination chain overrides
338+
// 3. Origin chain overrides
339+
// 4. Default config
330340
const routeOverride = tokenCostConfig?.routeOverrides?.[_originRoute || ""]?.[_destinationRoute || ""];
331-
const config: CapitalCostConfig = routeOverride ?? destinationChainOverride ?? tokenCostConfig.default;
341+
const destinationChainOverride = tokenCostConfig?.destinationChainOverrides?.[_destinationRoute || ""];
342+
const originChainOverride = tokenCostConfig?.originChainOverrides?.[_originRoute || ""];
343+
const config: CapitalCostConfig =
344+
routeOverride ?? destinationChainOverride ?? originChainOverride ?? tokenCostConfig.default;
345+
346+
// Check and log warnings for configuration conflicts
347+
this.warnIfConfigConflicts(
348+
_tokenSymbol,
349+
_originRoute || "",
350+
_destinationRoute || "",
351+
routeOverride,
352+
destinationChainOverride,
353+
originChainOverride
354+
);
332355

333356
// Scale amount "y" to 18 decimals.
334357
const y = toBN(_amountToRelay).mul(toBNWei("1", 18 - config.decimals));
@@ -356,6 +379,88 @@ export class RelayFeeCalculator {
356379
}
357380
}
358381

382+
/**
383+
* Checks for configuration conflicts across all token symbols and their associated chain configurations.
384+
* This method examines the capital costs configuration for each token and identifies any overlapping
385+
* or conflicting configurations between route overrides, destination chain overrides, and origin chain overrides.
386+
* If conflicts are found, warnings will be logged via the warnIfConfigConflicts method.
387+
*/
388+
private checkAllConfigConflicts(): void {
389+
for (const [tokenSymbol, tokenConfig] of Object.entries(this.capitalCostsConfig)) {
390+
// Get all origin chains that have specific configurations
391+
const originChains = new Set<string>(Object.keys(tokenConfig.originChainOverrides || {}));
392+
// Get all destination chains that have specific configurations
393+
const destChains = new Set<string>(Object.keys(tokenConfig.destinationChainOverrides || {}));
394+
395+
// Add all chains from route overrides
396+
if (tokenConfig.routeOverrides) {
397+
Object.keys(tokenConfig.routeOverrides).forEach((originChain) => {
398+
originChains.add(originChain);
399+
Object.keys(tokenConfig.routeOverrides![originChain]).forEach((destChain) => {
400+
destChains.add(destChain);
401+
});
402+
});
403+
}
404+
405+
// If there are no specific chain configurations, just check the default case
406+
if (originChains.size === 0 && destChains.size === 0) {
407+
continue;
408+
}
409+
410+
// Check for conflicts between all combinations of origin and destination chains
411+
for (const originChain of Array.from(originChains)) {
412+
for (const destChain of Array.from(destChains)) {
413+
const routeOverride = tokenConfig.routeOverrides?.[originChain]?.[destChain];
414+
const destinationChainOverride = tokenConfig.destinationChainOverrides?.[destChain];
415+
const originChainOverride = tokenConfig.originChainOverrides?.[originChain];
416+
417+
this.warnIfConfigConflicts(
418+
tokenSymbol,
419+
originChain,
420+
destChain,
421+
routeOverride,
422+
destinationChainOverride,
423+
originChainOverride
424+
);
425+
}
426+
}
427+
}
428+
}
429+
430+
/**
431+
* Log a warning if multiple configuration types apply to the same route
432+
* @private
433+
*/
434+
private warnIfConfigConflicts(
435+
tokenSymbol: string,
436+
originChain: string,
437+
destChain: string,
438+
routeOverride?: CapitalCostConfig,
439+
destinationChainOverride?: CapitalCostConfig,
440+
originChainOverride?: CapitalCostConfig
441+
): void {
442+
const overrideCount = [routeOverride, destinationChainOverride, originChainOverride].filter(Boolean).length;
443+
444+
if (overrideCount > 1) {
445+
const configUsed = routeOverride
446+
? "route override"
447+
: destinationChainOverride
448+
? "destination chain override"
449+
: originChainOverride
450+
? "origin chain override"
451+
: "default override";
452+
453+
this.logger.warn({
454+
at: "RelayFeeCalculator",
455+
message: `Multiple configurations found for token ${tokenSymbol} from chain ${originChain} to chain ${destChain}`,
456+
configUsed,
457+
routeOverride,
458+
destinationChainOverride,
459+
originChainOverride,
460+
});
461+
}
462+
}
463+
359464
/**
360465
* Retrieves the relayer fee details for a deposit.
361466
* @param deposit A valid deposit object to reason about

test/relayFeeCalculator.test.ts

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,14 @@ describe("RelayFeeCalculator", () => {
293293
cutoff: toBNWei("15").toString(),
294294
decimals: token.decimals,
295295
},
296+
originChainOverrides: {
297+
[CHAIN_IDs.MAINNET]: {
298+
lowerBound: toBNWei("0.0004").toString(),
299+
upperBound: toBNWei("0.005").toString(),
300+
cutoff: toBNWei("12").toString(),
301+
decimals: token.decimals,
302+
},
303+
},
296304
destinationChainOverrides: {
297305
// Override for destination chain ARBITRUM
298306
[CHAIN_IDs.ARBITRUM]: {
@@ -323,14 +331,30 @@ describe("RelayFeeCalculator", () => {
323331

324332
// Get config values for cleaner assertions
325333
const defaultConfig = customCapitalCostsConfig[token.symbol].default;
326-
const chainOverride = customCapitalCostsConfig[token.symbol].destinationChainOverrides[CHAIN_IDs.ARBITRUM];
334+
const originChainOverride = customCapitalCostsConfig[token.symbol].originChainOverrides[CHAIN_IDs.MAINNET];
335+
const destChainOverride = customCapitalCostsConfig[token.symbol].destinationChainOverrides[CHAIN_IDs.ARBITRUM];
327336
const routeOverride = customCapitalCostsConfig[token.symbol].routeOverrides[CHAIN_IDs.MAINNET][CHAIN_IDs.ARBITRUM];
328337

329338
// Test using default config (no routes specified)
330339
const defaultFee = client.capitalFeePercent(toBNWei("1", token.decimals), token.symbol);
331340
assert.ok(toBN(defaultFee).gte(toBN(defaultConfig.lowerBound)), "Default fee should be at least the lower bound");
332341
assert.ok(toBN(defaultFee).lt(toBN(defaultConfig.upperBound)), "Default fee should be less than the upper bound");
333342

343+
// Test using origin chain override only
344+
const originFee = client.capitalFeePercent(
345+
toBNWei("1", token.decimals),
346+
token.symbol,
347+
CHAIN_IDs.MAINNET.toString()
348+
);
349+
assert.ok(
350+
toBN(originFee).gte(toBN(originChainOverride.lowerBound)),
351+
"Origin override fee should be at least its lower bound"
352+
);
353+
assert.ok(
354+
toBN(originFee).lt(toBN(originChainOverride.upperBound)),
355+
"Origin override fee should be less than its upper bound"
356+
);
357+
334358
// Test using destination chain override (only destination specified)
335359
const destinationFee = client.capitalFeePercent(
336360
toBNWei("1", token.decimals),
@@ -339,11 +363,11 @@ describe("RelayFeeCalculator", () => {
339363
CHAIN_IDs.ARBITRUM.toString()
340364
);
341365
assert.ok(
342-
toBN(destinationFee).gte(toBN(chainOverride.lowerBound)),
366+
toBN(destinationFee).gte(toBN(destChainOverride.lowerBound)),
343367
"Destination override fee should be at least its lower bound"
344368
);
345369
assert.ok(
346-
toBN(destinationFee).lt(toBN(chainOverride.upperBound)),
370+
toBN(destinationFee).lt(toBN(destChainOverride.upperBound)),
347371
"Destination override fee should be less than its upper bound"
348372
);
349373

@@ -363,20 +387,51 @@ describe("RelayFeeCalculator", () => {
363387
"Route override fee should be less than its upper bound"
364388
);
365389

366-
// Test fallback to destination chain override when a route is not specifically overridden
367-
const fallbackToDestFee = client.capitalFeePercent(
390+
// Test priority order: when both origin and destination chain are specified but no route override exists,
391+
// destination chain override should take priority over origin chain override
392+
const destOverOriginFee = client.capitalFeePercent(
368393
toBNWei("1", token.decimals),
369394
token.symbol,
370395
CHAIN_IDs.OPTIMISM.toString(),
371396
CHAIN_IDs.ARBITRUM.toString()
372397
);
373398
assert.ok(
374-
toBN(fallbackToDestFee).gte(toBN(chainOverride.lowerBound)),
375-
"Fallback to destination fee should be at least the destination lower bound"
399+
toBN(destOverOriginFee).gte(toBN(destChainOverride.lowerBound)),
400+
"Destination should take priority over origin - fee should match destination lower bound"
401+
);
402+
assert.ok(
403+
toBN(destOverOriginFee).lt(toBN(destChainOverride.upperBound)),
404+
"Destination should take priority over origin - fee should match destination upper bound"
405+
);
406+
407+
// Test priority order: route override takes priority over both origin and destination chain overrides
408+
const routeOverBothFee = client.capitalFeePercent(
409+
toBNWei("1", token.decimals),
410+
token.symbol,
411+
CHAIN_IDs.MAINNET.toString(),
412+
CHAIN_IDs.ARBITRUM.toString()
413+
);
414+
assert.ok(
415+
toBN(routeOverBothFee).gte(toBN(routeOverride.lowerBound)),
416+
"Route should take priority over both origin and destination - fee should match route lower bound"
376417
);
377418
assert.ok(
378-
toBN(fallbackToDestFee).lt(toBN(chainOverride.upperBound)),
379-
"Fallback to destination fee should be less than the destination upper bound"
419+
toBN(routeOverBothFee).lt(toBN(routeOverride.upperBound)),
420+
"Route should take priority over both origin and destination - fee should match route upper bound"
421+
);
422+
423+
// Verify that route override != origin override
424+
assert.notEqual(
425+
routeOverBothFee.toString(),
426+
originFee.toString(),
427+
"Route override fee should be different from origin override fee"
428+
);
429+
430+
// Verify that route override != destination override
431+
assert.notEqual(
432+
routeOverBothFee.toString(),
433+
destinationFee.toString(),
434+
"Route override fee should be different from destination override fee"
380435
);
381436

382437
// Test fallback to default when route doesn't match and no destination chain override exists

0 commit comments

Comments
 (0)