Skip to content

Commit ac00af8

Browse files
dohakigsteenkamp89
andauthored
feat(api): field outputAmount + param allowUnmatchedDecimals (#1562)
* feat(api): field `outputAmount` + param `allowUnmatchedDecimals` There might be edge cases where the decimals for the same token do not match on different chains (USDC/USDT on BNB). To handle these cases we add a correctly scaled `outputAmount` response field. We also let consumers opt-in for these routes via the query param `allowUnmatchedDecimals`. * fixup * fixup * add `inputToken` and `outputToken` * Update api/_utils.ts Co-authored-by: Gerhard Steenkamp <[email protected]> * review requests --------- Co-authored-by: Gerhard Steenkamp <[email protected]>
1 parent 1eaaed8 commit ac00af8

File tree

3 files changed

+85
-19
lines changed

3 files changed

+85
-19
lines changed

api/_utils.ts

+31-9
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ export const validateChainAndTokenParams = (
235235
outputToken: string;
236236
originChainId: string;
237237
destinationChainId: string;
238+
allowUnmatchedDecimals: string;
238239
}>
239240
) => {
240241
let {
@@ -243,6 +244,7 @@ export const validateChainAndTokenParams = (
243244
outputToken: outputTokenAddress,
244245
originChainId,
245246
destinationChainId: _destinationChainId,
247+
allowUnmatchedDecimals: _allowUnmatchedDecimals,
246248
} = queryParams;
247249

248250
if (!_destinationChainId) {
@@ -272,6 +274,7 @@ export const validateChainAndTokenParams = (
272274
outputTokenAddress = outputTokenAddress
273275
? _getAddressOrThrowInputError(outputTokenAddress, "outputToken")
274276
: undefined;
277+
const allowUnmatchedDecimals = _allowUnmatchedDecimals === "true";
275278

276279
const { l1Token, outputToken, inputToken, resolvedOriginChainId } =
277280
getRouteDetails(
@@ -295,12 +298,25 @@ export const validateChainAndTokenParams = (
295298
});
296299
}
297300

301+
if (!allowUnmatchedDecimals && inputToken.decimals !== outputToken.decimals) {
302+
throw new InvalidParamError({
303+
message:
304+
`Decimals of input and output tokens do not match. ` +
305+
`This is likely due to unmatched decimals for USDC/USDT on BNB Chain. ` +
306+
`Make sure to have followed the migration guide: ` +
307+
`https://docs.across.to/introduction/migration-guides/bnb-chain-migration-guide ` +
308+
`and set the query param 'allowUnmatchedDecimals=true' to allow this.`,
309+
param: "allowUnmatchedDecimals",
310+
});
311+
}
312+
298313
return {
299314
l1Token,
300315
inputToken,
301316
outputToken,
302317
destinationChainId,
303318
resolvedOriginChainId,
319+
allowUnmatchedDecimals,
304320
};
305321
};
306322

@@ -926,7 +942,8 @@ export const getCachedLimits = async (
926942
amount?: string,
927943
recipient?: string,
928944
relayer?: string,
929-
message?: string
945+
message?: string,
946+
allowUnmatchedDecimals?: boolean
930947
): Promise<{
931948
minDeposit: string;
932949
maxDeposit: string;
@@ -954,6 +971,7 @@ export const getCachedLimits = async (
954971
message,
955972
recipient,
956973
relayer,
974+
allowUnmatchedDecimals,
957975
},
958976
})
959977
).data;
@@ -2057,17 +2075,21 @@ export async function fetchStakingPool(
20572075
};
20582076
}
20592077

2060-
// Copied from @uma/common
2078+
/**
2079+
* Factory function that creates a function that converts an amount from one number of decimals to another.
2080+
* Copied from @uma/common
2081+
* @param fromDecimals The number of decimals of the input amount.
2082+
* @param toDecimals The number of decimals of the output amount.
2083+
* @returns A function that converts an amount from `fromDecimals` to `toDecimals`.
2084+
*/
20612085
export const ConvertDecimals = (fromDecimals: number, toDecimals: number) => {
2062-
// amount: string, BN, number - integer amount in fromDecimals smallest unit that want to convert toDecimals
2063-
// returns: string with toDecimals in smallest unit
2064-
return (amount: BigNumber): string => {
2086+
return (amount: BigNumber): BigNumber => {
20652087
amount = BigNumber.from(amount);
2066-
if (amount.isZero()) return amount.toString();
2088+
if (amount.isZero()) return amount;
20672089
const diff = fromDecimals - toDecimals;
2068-
if (diff === 0) return amount.toString();
2069-
if (diff > 0) return amount.div(BigNumber.from("10").pow(diff)).toString();
2070-
return amount.mul(BigNumber.from("10").pow(-1 * diff)).toString();
2090+
if (diff === 0) return amount;
2091+
if (diff > 0) return amount.div(BigNumber.from("10").pow(diff));
2092+
return amount.mul(BigNumber.from("10").pow(-1 * diff));
20712093
};
20722094
};
20732095

api/limits.ts

+32-9
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
getCachedNativeGasCost,
4040
getCachedOpStackL1DataFee,
4141
getLimitCap,
42+
boolStr,
4243
} from "./_utils";
4344
import { MissingParamError } from "./_errors";
4445
import { getEnvs } from "./_env";
@@ -58,6 +59,7 @@ const LimitsQueryParamsSchema = type({
5859
message: optional(string()),
5960
recipient: optional(validAddress()),
6061
relayer: optional(validAddress()),
62+
allowUnmatchedDecimals: optional(boolStr()),
6163
});
6264

6365
type LimitsQueryParams = Infer<typeof LimitsQueryParamsSchema>;
@@ -124,7 +126,7 @@ const handler = async (
124126
);
125127
}
126128
const amount = BigNumber.from(
127-
amountInput ?? ethers.BigNumber.from("10").pow(l1Token.decimals)
129+
amountInput ?? ethers.BigNumber.from("10").pow(inputToken.decimals)
128130
);
129131
let minDepositUsdForDestinationChainId = Number(
130132
getEnvs()[`MIN_DEPOSIT_USD_${destinationChainId}`] ?? MIN_DEPOSIT_USD
@@ -200,9 +202,9 @@ const handler = async (
200202
const [
201203
opStackL1GasCost,
202204
multicallOutput,
203-
fullRelayerBalances,
204-
transferRestrictedBalances,
205-
fullRelayerMainnetBalances,
205+
_fullRelayerBalances,
206+
_transferRestrictedBalances,
207+
_fullRelayerMainnetBalances,
206208
] = await Promise.all([
207209
nativeGasCost && sdk.utils.chainIsOPStack(destinationChainId)
208210
? // Only use cached gas units if message is not defined, i.e. standard for standard bridges
@@ -261,7 +263,7 @@ const handler = async (
261263
relayerFeeDetails,
262264
});
263265

264-
let { liquidReserves } = multicallOutput[1];
266+
const { liquidReserves: _liquidReserves } = multicallOutput[1];
265267
const [liteChainIdsEncoded] = multicallOutput[2];
266268
const liteChainIds: number[] =
267269
liteChainIdsEncoded === "" ? [] : JSON.parse(liteChainIdsEncoded);
@@ -271,6 +273,23 @@ const handler = async (
271273
const routeInvolvesLiteChain =
272274
originChainIsLiteChain || destinationChainIsLiteChain;
273275

276+
// Base every amount on the input token decimals.
277+
let liquidReserves = ConvertDecimals(
278+
l1Token.decimals,
279+
inputToken.decimals
280+
)(_liquidReserves);
281+
const fullRelayerBalances = _fullRelayerBalances.map((balance) =>
282+
ConvertDecimals(outputToken.decimals, inputToken.decimals)(balance)
283+
);
284+
const fullRelayerMainnetBalances = _fullRelayerMainnetBalances.map(
285+
(balance) =>
286+
ConvertDecimals(l1Token.decimals, inputToken.decimals)(balance)
287+
);
288+
const transferRestrictedBalances = _transferRestrictedBalances.map(
289+
(balance) =>
290+
ConvertDecimals(outputToken.decimals, inputToken.decimals)(balance)
291+
);
292+
274293
const transferBalances = fullRelayerBalances.map((balance, i) =>
275294
balance.add(fullRelayerMainnetBalances[i])
276295
);
@@ -283,7 +302,7 @@ const handler = async (
283302
: ethers.utils
284303
.parseUnits(
285304
minDepositUsdForDestinationChainId.toString(),
286-
l1Token.decimals
305+
inputToken.decimals
287306
)
288307
.mul(ethers.utils.parseUnits("1"))
289308
.div(tokenPriceUsd);
@@ -299,10 +318,14 @@ const handler = async (
299318
); // balances on destination chain + mainnet
300319

301320
if (!routeInvolvesLiteChain) {
302-
const lpCushion = ethers.utils.parseUnits(
321+
const _lpCushion = ethers.utils.parseUnits(
303322
getLpCushion(l1Token.symbol, computedOriginChainId, destinationChainId),
304323
l1Token.decimals
305324
);
325+
const lpCushion = ConvertDecimals(
326+
l1Token.decimals,
327+
inputToken.decimals
328+
)(_lpCushion);
306329
liquidReserves = maxBN(
307330
liquidReserves.sub(lpCushion),
308331
ethers.BigNumber.from(0)
@@ -407,8 +430,8 @@ const handler = async (
407430
}
408431

409432
const limitCap = getLimitCap(
410-
l1Token.symbol,
411-
l1Token.decimals,
433+
inputToken.symbol,
434+
inputToken.decimals,
412435
destinationChainId
413436
);
414437

api/suggested-fees.ts

+22-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
OPT_IN_CHAINS,
2929
parseL1TokenConfigSafe,
3030
getL1TokenConfigCache,
31+
ConvertDecimals,
3132
} from "./_utils";
3233
import { selectExclusiveRelayer } from "./_exclusivity";
3334
import {
@@ -100,6 +101,7 @@ const handler = async (
100101
outputToken,
101102
destinationChainId,
102103
resolvedOriginChainId: computedOriginChainId,
104+
allowUnmatchedDecimals,
103105
} = validateChainAndTokenParams(query);
104106

105107
relayer = relayer
@@ -224,7 +226,8 @@ const handler = async (
224226
// likely to hit the /limits cache using the above parameters that are not specific to this deposit.
225227
depositWithMessage ? recipient : undefined,
226228
depositWithMessage ? relayer : undefined,
227-
depositWithMessage ? message : undefined
229+
depositWithMessage ? message : undefined,
230+
allowUnmatchedDecimals
228231
),
229232
getFillDeadline(destinationChainId),
230233
]);
@@ -279,6 +282,11 @@ const handler = async (
279282
relayerFeeDetails.relayFeePercent
280283
).add(lpFeePct);
281284

285+
const outputAmount = ConvertDecimals(
286+
inputToken.decimals,
287+
outputToken.decimals
288+
)(amount.sub(totalRelayFee));
289+
282290
const { exclusiveRelayer, exclusivityPeriod: exclusivityDeadline } =
283291
await selectExclusiveRelayer(
284292
computedOriginChainId,
@@ -364,6 +372,19 @@ const handler = async (
364372
recommendedDepositInstant: limits.recommendedDepositInstant,
365373
},
366374
fillDeadline: fillDeadline.toString(),
375+
outputAmount: outputAmount.toString(),
376+
inputToken: {
377+
address: inputToken.address,
378+
symbol: inputToken.symbol,
379+
decimals: inputToken.decimals,
380+
chainId: computedOriginChainId,
381+
},
382+
outputToken: {
383+
address: outputToken.address,
384+
symbol: outputToken.symbol,
385+
decimals: outputToken.decimals,
386+
chainId: destinationChainId,
387+
},
367388
};
368389

369390
logger.info({

0 commit comments

Comments
 (0)