harden: resolve the swap approval screen's token scale, or refuse to format (closes #340)
tokenInfo() in src/shared/uniswap.js returned decimals: 18 for any token absent from the bundled token list, the same guessed scale that issue 306 removed from the ERC-20 amount line of the same screen. A 1,000-token swap of a 6-decimal token was therefore stated as 0.000000001, a wrong number rather than an imprecise one, and every newly listed token reached it. The swap's Amount and Min. received lines now resolve the scale through resolveTokenDecimals() -- the bundled list, then the tokens the user tracks, then the decimals the block explorer reported -- and where none of those answers they render unknownDecimalsAmount(), the base-unit integer with the scale stated, which is the same refusal the ERC-20 line already makes. No new data source and no network call: the scale comes only from what the wallet already holds, so the screen still makes no request before showing what is being signed. uniswap.decode() takes those sources as a third argument, supplied by decodeCalldata() from state alongside the ones the ERC-20 path already used. An unbounded permit needs no scale to describe and is still shown as Unlimited. README.md records the rule as a Display Consistency exception.
This commit is contained in:
19
README.md
19
README.md
@@ -734,6 +734,25 @@ transaction detail view is the authoritative record and already shows exact
|
|||||||
precision. The 4-decimal rule is unchanged everywhere else, including for
|
precision. The 4-decimal rule is unchanged everywhere else, including for
|
||||||
amounts at or above the floor on the approval screens.
|
amounts at or above the floor on the approval screens.
|
||||||
|
|
||||||
|
The floor applies only where the token's scale is known. Where it is not, the
|
||||||
|
approval screen states base units instead of a quantity — see Unknown token
|
||||||
|
scale below — and no truncation happens at all.
|
||||||
|
|
||||||
|
**Specific Exception — unknown token scale:** Calldata carries base units and no
|
||||||
|
scale, so every amount on the dApp approval screen needs the token's `decimals`.
|
||||||
|
It is resolved from the bundled token list, then from the tokens the user
|
||||||
|
tracks, then from what the block explorer reported for the contract
|
||||||
|
(`resolveTokenDecimals()` in `src/shared/approvalAmount.js`). Where none of them
|
||||||
|
answers, the amount is not formatted: the line reads
|
||||||
|
`5000000000 base units (decimals unknown)`. A guessed scale is not an
|
||||||
|
approximation but a different number — 1,000 units of a 6-decimal token
|
||||||
|
formatted at 18 decimals reads `0.000000001` — on the screen whose only job is
|
||||||
|
to state what is being authorized. Both amount paths of that screen take this
|
||||||
|
rule: the ERC-20 `transfer`/`approve` line (`src/popup/views/approval.js`) and
|
||||||
|
the swap's `Amount` and `Min. received` lines (`src/shared/uniswap.js`). An
|
||||||
|
unbounded allowance or permit needs no scale to describe and is still shown as
|
||||||
|
`Unlimited`.
|
||||||
|
|
||||||
#### Partial USD totals
|
#### Partial USD totals
|
||||||
|
|
||||||
Prices are fetched for the top 25 tokens only, so an address can hold assets the
|
Prices are fetched for the top 25 tokens only, so an address can hold assets the
|
||||||
|
|||||||
14
TODO.md
14
TODO.md
@@ -45,6 +45,20 @@ but the review is broader than any of them.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-08-23: The swap approval screen no longer guesses 18 decimals for a token
|
||||||
|
outside the bundled list
|
||||||
|
([#340](https://git.eeqj.de/sneak/AutistMask/issues/340)). `tokenInfo()` in
|
||||||
|
`src/shared/uniswap.js` returned `decimals: 18` for any such token — the same
|
||||||
|
assumption [#306](https://git.eeqj.de/sneak/AutistMask/issues/306) removed
|
||||||
|
from the ERC-20 amount line — so a 1,000-unit swap of a 6-decimal token was
|
||||||
|
stated as `0.000000001`, and every newly listed token reached it. The swap's
|
||||||
|
`Amount` and `Min. received` lines now resolve the scale through
|
||||||
|
`resolveTokenDecimals()`, the same bundled-list-then-tracked-then-explorer
|
||||||
|
order the ERC-20 line uses, and where nothing knows it they render
|
||||||
|
`unknownDecimalsAmount()` — base units with the scale stated — instead of a
|
||||||
|
number. No new data source and no network call: the scale comes only from what
|
||||||
|
the wallet already holds. An unbounded permit is still shown as `Unlimited`.
|
||||||
|
`README.md` records the rule as a Display Consistency exception.
|
||||||
- 2026-08-23: A failed release build no longer leaves a loadable debug bundle in
|
- 2026-08-23: A failed release build no longer leaves a loadable debug bundle in
|
||||||
`dist/` ([#333](https://git.eeqj.de/sneak/AutistMask/issues/333)). With
|
`dist/` ([#333](https://git.eeqj.de/sneak/AutistMask/issues/333)). With
|
||||||
`AUTISTMASK_DEBUG=1` exported, `make build` compiled a debug bundle and failed
|
`AUTISTMASK_DEBUG=1` exported, `make build` compiled a debug bundle and failed
|
||||||
|
|||||||
@@ -73,6 +73,14 @@ function tokenLabel(address) {
|
|||||||
function decodeCalldata(data, toAddress) {
|
function decodeCalldata(data, toAddress) {
|
||||||
if (!data || data === "0x" || data.length < 10) return null;
|
if (!data || data === "0x" || data.length < 10) return null;
|
||||||
|
|
||||||
|
// Where a token's scale is looked for, for every decoder below: the ERC-20
|
||||||
|
// amount line and the swap's Amount and Min. received lines resolve it the
|
||||||
|
// same way, and refuse to format the same way when it is nowhere.
|
||||||
|
const decimalsSources = {
|
||||||
|
trackedTokens: state.trackedTokens,
|
||||||
|
wallets: state.wallets,
|
||||||
|
};
|
||||||
|
|
||||||
// Try ERC-20 (approve / transfer)
|
// Try ERC-20 (approve / transfer)
|
||||||
try {
|
try {
|
||||||
const parsed = erc20Iface.parseTransaction({ data });
|
const parsed = erc20Iface.parseTransaction({ data });
|
||||||
@@ -84,10 +92,10 @@ function decodeCalldata(data, toAddress) {
|
|||||||
// the wrong number, and for a token with fewer decimals than the
|
// the wrong number, and for a token with fewer decimals than the
|
||||||
// guess it is the wrong number in the direction that reads as
|
// guess it is the wrong number in the direction that reads as
|
||||||
// zero. See tokenAmountText().
|
// zero. See tokenAmountText().
|
||||||
const tokenDecimals = resolveTokenDecimals(toAddress, {
|
const tokenDecimals = resolveTokenDecimals(
|
||||||
trackedTokens: state.trackedTokens,
|
toAddress,
|
||||||
wallets: state.wallets,
|
decimalsSources,
|
||||||
});
|
);
|
||||||
const contractLabel = tokenSymbol
|
const contractLabel = tokenSymbol
|
||||||
? tokenSymbol + " (" + toAddress + ")"
|
? tokenSymbol + " (" + toAddress + ")"
|
||||||
: toAddress;
|
: toAddress;
|
||||||
@@ -167,7 +175,7 @@ function decodeCalldata(data, toAddress) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try Uniswap Universal Router
|
// Try Uniswap Universal Router
|
||||||
const routerResult = uniswap.decode(data, toAddress);
|
const routerResult = uniswap.decode(data, toAddress, decimalsSources);
|
||||||
if (routerResult) return routerResult;
|
if (routerResult) return routerResult;
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -14,6 +14,10 @@
|
|||||||
// them answers, unknownDecimalsAmount() renders the base-unit integer with the
|
// them answers, unknownDecimalsAmount() renders the base-unit integer with the
|
||||||
// unknown scale stated, and no formatUnits() call is reached at all.
|
// unknown scale stated, and no formatUnits() call is reached at all.
|
||||||
//
|
//
|
||||||
|
// The Uniswap decoder's Amount and Min. received lines land on this same
|
||||||
|
// screen and use these same two functions, so there is one way of resolving a
|
||||||
|
// scale and one way of saying there is none.
|
||||||
|
//
|
||||||
// This is the display counterpart to transferAmount.js, which takes the same
|
// This is the display counterpart to transferAmount.js, which takes the same
|
||||||
// stance on the wallet's own send path: an amount whose scale is unknown or
|
// stance on the wallet's own send path: an amount whose scale is unknown or
|
||||||
// disputed is refused rather than guessed at.
|
// disputed is refused rather than guessed at.
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
const { Interface, AbiCoder, getBytes, formatUnits } = require("ethers");
|
const { Interface, AbiCoder, getBytes, formatUnits } = require("ethers");
|
||||||
const { TOKEN_BY_ADDRESS } = require("./tokenList");
|
const { TOKEN_BY_ADDRESS } = require("./tokenList");
|
||||||
const { truncateAmountNeverZero } = require("./amountDisplay");
|
const { truncateAmountNeverZero } = require("./amountDisplay");
|
||||||
|
const {
|
||||||
|
resolveTokenDecimals,
|
||||||
|
unknownDecimalsAmount,
|
||||||
|
} = require("./approvalAmount");
|
||||||
|
|
||||||
const coder = AbiCoder.defaultAbiCoder();
|
const coder = AbiCoder.defaultAbiCoder();
|
||||||
|
|
||||||
@@ -44,13 +48,37 @@ function formatAmount(raw, decimals) {
|
|||||||
return truncateAmountNeverZero(formatUnits(raw, decimals));
|
return truncateAmountNeverZero(formatUnits(raw, decimals));
|
||||||
}
|
}
|
||||||
|
|
||||||
function tokenInfo(address) {
|
// `decimals` is null when nothing knows this token's scale. It is not
|
||||||
|
// defaulted to 18: the swap lines land on the same approval screen as the
|
||||||
|
// ERC-20 line, and a scale guessed there is what showed a 1,000 USDT swap as
|
||||||
|
// 0.000000000001. `sources` is { trackedTokens, wallets }, shaped as they are
|
||||||
|
// on `state`; resolveTokenDecimals() reads the bundled list, then those.
|
||||||
|
function tokenInfo(address, sources) {
|
||||||
if (!address || address === "0x0000000000000000000000000000000000000000") {
|
if (!address || address === "0x0000000000000000000000000000000000000000") {
|
||||||
return { symbol: "ETH", decimals: 18, address: null };
|
return { symbol: "ETH", decimals: 18, address: null };
|
||||||
}
|
}
|
||||||
const t = TOKEN_BY_ADDRESS.get(address.toLowerCase());
|
const t = TOKEN_BY_ADDRESS.get(address.toLowerCase());
|
||||||
if (t) return { symbol: t.symbol, decimals: t.decimals, address };
|
return {
|
||||||
return { symbol: null, decimals: 18, address };
|
symbol: t ? t.symbol : null,
|
||||||
|
decimals: resolveTokenDecimals(address, sources),
|
||||||
|
address,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// A swap amount line. With a scale it is the token quantity; with none it is
|
||||||
|
// the base-unit integer with the unknown scale stated, the same refusal the
|
||||||
|
// ERC-20 amount line makes, so the screen has one way of saying it. `display`
|
||||||
|
// is the line on the screen, `raw` is what the status screens carry.
|
||||||
|
function amountText(raw, info) {
|
||||||
|
if (info.decimals === null) {
|
||||||
|
const unknown = unknownDecimalsAmount(raw);
|
||||||
|
return { raw: unknown, display: unknown };
|
||||||
|
}
|
||||||
|
const formatted = formatAmount(raw, info.decimals);
|
||||||
|
return {
|
||||||
|
raw: formatted,
|
||||||
|
display: formatted + (info.symbol ? " " + info.symbol : ""),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode PERMIT2_PERMIT (command 0x0a) input bytes.
|
// Decode PERMIT2_PERMIT (command 0x0a) input bytes.
|
||||||
@@ -323,7 +351,7 @@ function decodeV4Swap(input) {
|
|||||||
// Try to decode a Universal Router execute() call.
|
// Try to decode a Universal Router execute() call.
|
||||||
// Returns { name, description, details } matching the format used by
|
// Returns { name, description, details } matching the format used by
|
||||||
// the approval UI, or null if the calldata is not a recognised execute().
|
// the approval UI, or null if the calldata is not a recognised execute().
|
||||||
function decode(data, toAddress) {
|
function decode(data, toAddress, sources) {
|
||||||
try {
|
try {
|
||||||
const parsed = ROUTER_IFACE.parseTransaction({ data });
|
const parsed = ROUTER_IFACE.parseTransaction({ data });
|
||||||
if (!parsed) return null;
|
if (!parsed) return null;
|
||||||
@@ -416,10 +444,10 @@ function decode(data, toAddress) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Resolve token info
|
// Resolve token info
|
||||||
const inInfo = tokenInfo(inputToken);
|
const inInfo = tokenInfo(inputToken, sources);
|
||||||
const outInfo = hasUnwrapWeth
|
const outInfo = hasUnwrapWeth
|
||||||
? { symbol: "ETH", decimals: 18, address: null }
|
? { symbol: "ETH", decimals: 18, address: null }
|
||||||
: tokenInfo(outputToken);
|
: tokenInfo(outputToken, sources);
|
||||||
|
|
||||||
const inSymbol = inInfo.symbol;
|
const inSymbol = inInfo.symbol;
|
||||||
const outSymbol = outInfo.symbol;
|
const outSymbol = outInfo.symbol;
|
||||||
@@ -456,16 +484,15 @@ function decode(data, toAddress) {
|
|||||||
"0xffffffffffffffffffffffffffffffffffffffff",
|
"0xffffffffffffffffffffffffffffffffffffffff",
|
||||||
);
|
);
|
||||||
const isUnlimited = inputAmount >= maxUint160;
|
const isUnlimited = inputAmount >= maxUint160;
|
||||||
const amountRaw = isUnlimited
|
// An unbounded permit needs no scale to describe, so it is still
|
||||||
? "Unlimited"
|
// named rather than refused.
|
||||||
: formatAmount(inputAmount, inInfo.decimals);
|
const amount = isUnlimited
|
||||||
const amountStr = isUnlimited
|
? { raw: "Unlimited", display: "Unlimited" }
|
||||||
? "Unlimited"
|
: amountText(inputAmount, inInfo);
|
||||||
: amountRaw + (inSymbol ? " " + inSymbol : "");
|
|
||||||
details.push({
|
details.push({
|
||||||
label: "Amount",
|
label: "Amount",
|
||||||
value: amountStr,
|
value: amount.display,
|
||||||
rawValue: amountRaw,
|
rawValue: amount.raw,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,10 +513,10 @@ function decode(data, toAddress) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (minOutput !== null && minOutput !== undefined) {
|
if (minOutput !== null && minOutput !== undefined) {
|
||||||
const minStr =
|
details.push({
|
||||||
formatAmount(minOutput, outInfo.decimals) +
|
label: "Min. received",
|
||||||
(outSymbol ? " " + outSymbol : "");
|
value: amountText(minOutput, outInfo).display,
|
||||||
details.push({ label: "Min. received", value: minStr });
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
details.push({ label: "Steps", value: commandNames.join(" \u2192 ") });
|
details.push({ label: "Steps", value: commandNames.join(" \u2192 ") });
|
||||||
|
|||||||
176
tests/uniswapUnknownDecimals.test.js
Normal file
176
tests/uniswapUnknownDecimals.test.js
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
// The scale the swap lines of the dApp approval screen are displayed with.
|
||||||
|
//
|
||||||
|
// Issue #340: `tokenInfo()` in `src/shared/uniswap.js` returned `decimals: 18`
|
||||||
|
// for any token absent from the bundled list — the same guess
|
||||||
|
// https://git.eeqj.de/sneak/AutistMask/issues/306 removed from the ERC-20
|
||||||
|
// amount line, still live on the swap path. A 1,000-token swap of a 6-decimal
|
||||||
|
// token then rendered as `0.000000001` on the one screen whose job is to state
|
||||||
|
// what is being authorized, and every newly listed token reaches it.
|
||||||
|
//
|
||||||
|
// What is asserted here is the rule #306 established: resolve the real scale
|
||||||
|
// wherever the wallet already has it, and where nothing has it refuse to
|
||||||
|
// format — base units with the scale stated, never a quantity.
|
||||||
|
|
||||||
|
globalThis.chrome = {
|
||||||
|
storage: { local: { get: async () => ({}), set: async () => {} } },
|
||||||
|
};
|
||||||
|
|
||||||
|
const { AbiCoder, Interface } = require("ethers");
|
||||||
|
const { state } = require("../src/shared/state");
|
||||||
|
const { unknownDecimalsAmount } = require("../src/shared/approvalAmount");
|
||||||
|
const { decodeCalldata } = require("../src/popup/views/approval");
|
||||||
|
|
||||||
|
const ROUTER = "0x66a9893cc07d91d95644aedd05d03f95e1dba8af";
|
||||||
|
const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
|
||||||
|
// Outside the bundled list, as every newly listed token is.
|
||||||
|
const NOVEL = "0xE2E0000000000000000000000000000000000E2e";
|
||||||
|
// Also outside it, standing in for the swap's output side.
|
||||||
|
const NOVEL_OUT = "0xd0d0000000000000000000000000000000000d0d";
|
||||||
|
// In the bundled list, at 18 decimals.
|
||||||
|
const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
|
||||||
|
|
||||||
|
// 1,000.00 of a 6-decimal token — the amount from the issue, which the 18
|
||||||
|
// guess rendered as 0.000000001.
|
||||||
|
const THOUSAND_AT_SIX = 1000000000n;
|
||||||
|
// 0.5 WETH out, so the Min. received line has a real number of its own.
|
||||||
|
const HALF_WETH = 500000000000000000n;
|
||||||
|
|
||||||
|
const coder = AbiCoder.defaultAbiCoder();
|
||||||
|
const routerIface = new Interface([
|
||||||
|
"function execute(bytes commands, bytes[] inputs, uint256 deadline)",
|
||||||
|
]);
|
||||||
|
|
||||||
|
// A V2_SWAP_EXACT_IN (command 0x08) execute() call: `amountIn` of `tokenIn`
|
||||||
|
// for at least `amountOutMin` of `tokenOut`.
|
||||||
|
function swapData(tokenIn, amountIn, tokenOut, amountOutMin) {
|
||||||
|
const input = coder.encode(
|
||||||
|
["address", "uint256", "uint256", "address[]", "bool"],
|
||||||
|
[RECIPIENT, amountIn, amountOutMin, [tokenIn, tokenOut], true],
|
||||||
|
);
|
||||||
|
return routerIface.encodeFunctionData("execute", [
|
||||||
|
"0x08",
|
||||||
|
[input],
|
||||||
|
9999999999n,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A wallet holding `token` with the decimals the block explorer reported,
|
||||||
|
// shaped as balances.js writes it onto state.
|
||||||
|
function walletsHolding(token, decimals) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: "Wallet 1",
|
||||||
|
addresses: [
|
||||||
|
{
|
||||||
|
address: "0x" + "a".repeat(40),
|
||||||
|
balance: "1.0",
|
||||||
|
tokenBalances: [
|
||||||
|
{
|
||||||
|
address: token,
|
||||||
|
symbol: "NOVEL",
|
||||||
|
decimals,
|
||||||
|
balance: "1000.0",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// The swap detail line as the approval screen renders it. It goes through
|
||||||
|
// decodeCalldata() rather than uniswap.decode() directly, because the scale
|
||||||
|
// sources the screen supplies are part of what is under test.
|
||||||
|
function swapDetail(data, label) {
|
||||||
|
const decoded = decodeCalldata(data, ROUTER);
|
||||||
|
return decoded.details.find((d) => d.label === label);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
state.trackedTokens = [];
|
||||||
|
state.wallets = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("a swap of a token outside the bundled list", () => {
|
||||||
|
const data = () => swapData(NOVEL, THOUSAND_AT_SIX, WETH, HALF_WETH);
|
||||||
|
|
||||||
|
test("shows the true quantity when the user tracks the token", () => {
|
||||||
|
state.trackedTokens = [
|
||||||
|
{ address: NOVEL, symbol: "NOVEL", decimals: 6 },
|
||||||
|
];
|
||||||
|
expect(swapDetail(data(), "Amount").value).toBe("1000.0000");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shows the true quantity from the explorer's decimals", () => {
|
||||||
|
state.wallets = walletsHolding(NOVEL, "6");
|
||||||
|
expect(swapDetail(data(), "Amount").value).toBe("1000.0000");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("refuses to format when nothing knows the scale", () => {
|
||||||
|
const detail = swapDetail(data(), "Amount");
|
||||||
|
expect(detail.value).toBe("1000000000 base units (decimals unknown)");
|
||||||
|
expect(detail.value).toBe(unknownDecimalsAmount(THOUSAND_AT_SIX));
|
||||||
|
// The defect: an 18-decimal guess renders this swap as 0.000000001, a
|
||||||
|
// quantity, and a wrong one.
|
||||||
|
expect(detail.value).not.toMatch(/^0\./);
|
||||||
|
expect(detail.value).not.toMatch(/[0-9]\.[0-9]/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the amount carried to the status screens is the same refusal", () => {
|
||||||
|
expect(swapDetail(data(), "Amount").rawValue).toBe(
|
||||||
|
"1000000000 base units (decimals unknown)",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a bundled token on the other side still formats", () => {
|
||||||
|
expect(swapDetail(data(), "Min. received").value).toBe("0.5000 WETH");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("the Min. received line takes the same rule", () => {
|
||||||
|
test("refuses to format an output token of unknown scale", () => {
|
||||||
|
const data = swapData(WETH, HALF_WETH, NOVEL_OUT, THOUSAND_AT_SIX);
|
||||||
|
const detail = swapDetail(data, "Min. received");
|
||||||
|
expect(detail.value).toBe("1000000000 base units (decimals unknown)");
|
||||||
|
expect(detail.value).not.toMatch(/[0-9]\.[0-9]/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shows the true quantity when the user tracks the output token", () => {
|
||||||
|
state.trackedTokens = [
|
||||||
|
{ address: NOVEL_OUT, symbol: "NOVEL", decimals: 6 },
|
||||||
|
];
|
||||||
|
const data = swapData(WETH, HALF_WETH, NOVEL_OUT, THOUSAND_AT_SIX);
|
||||||
|
expect(swapDetail(data, "Min. received").value).toBe("1000.0000");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("the permit amount takes the same rule", () => {
|
||||||
|
// PERMIT2_PERMIT (command 0x0a): the input token and amount come from the
|
||||||
|
// permit rather than from a swap step.
|
||||||
|
function permitData(token, amount) {
|
||||||
|
const input = coder.encode(
|
||||||
|
[
|
||||||
|
"tuple(tuple(address,uint160,uint48,uint48),address,uint256)",
|
||||||
|
"bytes",
|
||||||
|
],
|
||||||
|
[[[token, amount, 0, 0], ROUTER, 9999999999], "0x1234"],
|
||||||
|
);
|
||||||
|
return routerIface.encodeFunctionData("execute", [
|
||||||
|
"0x0a",
|
||||||
|
[input],
|
||||||
|
9999999999n,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
test("refuses to format a permit on a token of unknown scale", () => {
|
||||||
|
const detail = swapDetail(permitData(NOVEL, THOUSAND_AT_SIX), "Amount");
|
||||||
|
expect(detail.value).toBe("1000000000 base units (decimals unknown)");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("an unbounded permit is still named, with or without a scale", () => {
|
||||||
|
const maxUint160 = (1n << 160n) - 1n;
|
||||||
|
expect(swapDetail(permitData(NOVEL, maxUint160), "Amount").value).toBe(
|
||||||
|
"Unlimited",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user