Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
786613ce2d |
@@ -911,9 +911,10 @@ 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
|
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
|
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
|
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
|
the swap's `Amount` and `Min. received` lines (`src/shared/uniswap.js`). The
|
||||||
unbounded allowance or permit needs no scale to describe and is still shown as
|
token permission warning on the signature screen takes the same rule for its
|
||||||
`Unlimited`.
|
amounts. An unbounded allowance or permit needs no scale to describe and is
|
||||||
|
still shown as `Unlimited`.
|
||||||
|
|
||||||
The rule holds only if nothing invents a scale UPSTREAM of it. Those three
|
The rule holds only if nothing invents a scale UPSTREAM of it. Those three
|
||||||
sources are read as authoritative, so a value written into one of them cannot be
|
sources are read as authoritative, so a value written into one of them cannot be
|
||||||
@@ -1801,10 +1802,21 @@ view would leave a wallet one click from deletion.
|
|||||||
- Type: "Personal message" or "Typed data (EIP-712)"
|
- Type: "Personal message" or "Typed data (EIP-712)"
|
||||||
- From: color dot + full address + etherscan link
|
- From: color dot + full address + etherscan link
|
||||||
- Message: decoded UTF-8 text (personal_sign) or formatted domain/type/
|
- Message: decoded UTF-8 text (personal_sign) or formatted domain/type/
|
||||||
message fields (EIP-712 typed data)
|
message fields (EIP-712 typed data). The primary type shown is the one
|
||||||
|
ethers signs, derived from the typed data's `types`, not the type the site
|
||||||
|
states.
|
||||||
|
- Token permission warning, at the top of the message (typed data whose
|
||||||
|
primary type is EIP-2612's `Permit` or one of Permit2's signature types):
|
||||||
|
"⚠️ TOKEN PERMISSION: Signing this lets the spender below take the tokens
|
||||||
|
listed here from your address, without asking you again.", then the
|
||||||
|
spender's full address and, for each token, its symbol, full address and
|
||||||
|
amount (`Unlimited` for the largest amount the field holds)
|
||||||
- Password input and an error line
|
- Password input and an error line
|
||||||
- "Sign" / "Reject" buttons
|
- "Sign" / "Reject" buttons
|
||||||
- **Transitions**:
|
- **Transitions**:
|
||||||
|
- Typed data that states no primary type, or one other than the type it
|
||||||
|
would be signed as, or that cannot be read → shown with the error line
|
||||||
|
saying so and "Sign" disabled; only "Reject" remains
|
||||||
- "Sign" (correct password) → signs locally → closes popup (returns
|
- "Sign" (correct password) → signs locally → closes popup (returns
|
||||||
signature)
|
signature)
|
||||||
- "Sign" (wrong password, or a signing failure) → error line, no screen
|
- "Sign" (wrong password, or a signing failure) → error line, no screen
|
||||||
|
|||||||
@@ -45,6 +45,22 @@ but the review is broader than any of them.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-03: The typed-data signing screen warns for a token permission, and
|
||||||
|
names the primary type ethers signs
|
||||||
|
([#400](https://git.eeqj.de/sneak/AutistMask/issues/400)). A Permit or Permit2
|
||||||
|
signature lets its spender take tokens from the signer's address, and the
|
||||||
|
screen listed it as plain key/value lines, exactly like a sign-in message. For
|
||||||
|
EIP-2612's `Permit` (and DAI's older permit of the same name) and Permit2's
|
||||||
|
six signature types, `src/popup/views/approval.js` now shows a red warning at
|
||||||
|
the top of the message naming the spender and each token and amount, with
|
||||||
|
`Unlimited` for the largest amount the field holds and the existing
|
||||||
|
unknown-scale wording otherwise. The screen printed the page's `primaryType`,
|
||||||
|
but ethers signs the type it derives from `types`; the screen now shows the
|
||||||
|
derived type, and typed data whose stated type is missing or differs, or that
|
||||||
|
cannot be read, is shown with an error line and Sign disabled, and is refused
|
||||||
|
again where signing starts. The warning names no deadline or expiry: those
|
||||||
|
fields mean different things across the shapes, and a date could read as the
|
||||||
|
permission ending when it does not.
|
||||||
- 2026-09-21: The network fee a transaction can commit is bounded by the product
|
- 2026-09-21: The network fee a transaction can commit is bounded by the product
|
||||||
of the gas limit and the fee per gas, not by each field alone, and the
|
of the gas limit and the fee per gas, not by each field alone, and the
|
||||||
wallet's own send is bounded the same way
|
wallet's own send is bounded the same way
|
||||||
|
|||||||
+170
-6
@@ -14,9 +14,13 @@ const { networkByChainId } = require("../../shared/networks");
|
|||||||
const {
|
const {
|
||||||
formatEther,
|
formatEther,
|
||||||
formatUnits,
|
formatUnits,
|
||||||
|
getAddress,
|
||||||
|
getBigInt,
|
||||||
getBytes,
|
getBytes,
|
||||||
Interface,
|
Interface,
|
||||||
|
MaxUint256,
|
||||||
toUtf8String,
|
toUtf8String,
|
||||||
|
TypedDataEncoder,
|
||||||
} = require("ethers");
|
} = require("ethers");
|
||||||
const { getPrice, formatUsd } = require("../../shared/prices");
|
const { getPrice, formatUsd } = require("../../shared/prices");
|
||||||
const { ERC20_ABI } = require("../../shared/constants");
|
const { ERC20_ABI } = require("../../shared/constants");
|
||||||
@@ -391,10 +395,149 @@ function decodeHexMessage(hex) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The type ethers will sign typed data as. ethers does not read the page's
|
||||||
|
// `primaryType`: it takes the one struct in `types` that no other struct
|
||||||
|
// refers to. Throws when the types name no such single struct, which ethers
|
||||||
|
// would refuse to sign as well.
|
||||||
|
function signedPrimaryType(types) {
|
||||||
|
const structs = { ...types };
|
||||||
|
// ethers derives EIP712Domain itself and rejects it as an input.
|
||||||
|
delete structs.EIP712Domain;
|
||||||
|
return TypedDataEncoder.getPrimaryType(structs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Why a signature request cannot be signed, as a sentence for the error line,
|
||||||
|
// or null when it can. Only typed data is refused: the `primaryType` the page
|
||||||
|
// states has to be the type ethers will sign, or this screen would name one
|
||||||
|
// message while another is signed.
|
||||||
|
function typedDataRefusal(sp) {
|
||||||
|
if (sp.method === "personal_sign" || sp.method === "eth_sign") return null;
|
||||||
|
let data;
|
||||||
|
let signed;
|
||||||
|
try {
|
||||||
|
data = JSON.parse(sp.typedData);
|
||||||
|
signed = signedPrimaryType(data.types);
|
||||||
|
} catch {
|
||||||
|
return "This typed data cannot be read, so it cannot be signed.";
|
||||||
|
}
|
||||||
|
if (!data.primaryType) {
|
||||||
|
return "This typed data does not name its primary type, so it cannot be signed.";
|
||||||
|
}
|
||||||
|
if (data.primaryType !== signed) {
|
||||||
|
return (
|
||||||
|
"This typed data names its primary type as " +
|
||||||
|
data.primaryType +
|
||||||
|
", but it would be signed as " +
|
||||||
|
signed +
|
||||||
|
", so it cannot be signed."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The largest amount a Permit2 allowance can hold, a uint160. Permit2 treats
|
||||||
|
// it as an allowance that is never used up.
|
||||||
|
const MAX_UINT160 = (1n << 160n) - 1n;
|
||||||
|
|
||||||
|
// A warning for typed data that lets a spender take your tokens, naming the
|
||||||
|
// spender and each token and amount, or "" for any other typed data. These
|
||||||
|
// signatures are how most wallet drains are done, and as plain key/value
|
||||||
|
// lines they read exactly like a sign-in message. The shapes are EIP-2612's
|
||||||
|
// `Permit` and Permit2's six signature types, recognised by the type ethers
|
||||||
|
// signs: a token contract checks the type's exact name, so a renamed copy of
|
||||||
|
// one of these would not be honoured.
|
||||||
|
function permitWarningHtml(primaryType, domain, message) {
|
||||||
|
// Each entry is a token, the amount, and the largest value its amount
|
||||||
|
// field holds, which the contract treats as unlimited.
|
||||||
|
let grants;
|
||||||
|
switch (primaryType) {
|
||||||
|
case "Permit": {
|
||||||
|
// EIP-2612 states a `value`. DAI's older permit, signed under the
|
||||||
|
// same name, states only `allowed`: unlimited, or nothing.
|
||||||
|
let amount = message.value;
|
||||||
|
if ("allowed" in message) {
|
||||||
|
amount = message.allowed ? MaxUint256 : 0n;
|
||||||
|
}
|
||||||
|
grants = [
|
||||||
|
{ token: domain.verifyingContract, amount, max: MaxUint256 },
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "PermitSingle":
|
||||||
|
grants = [
|
||||||
|
{
|
||||||
|
token: message.details.token,
|
||||||
|
amount: message.details.amount,
|
||||||
|
max: MAX_UINT160,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
case "PermitBatch":
|
||||||
|
grants = message.details.map((d) => ({
|
||||||
|
token: d.token,
|
||||||
|
amount: d.amount,
|
||||||
|
max: MAX_UINT160,
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
case "PermitTransferFrom":
|
||||||
|
case "PermitWitnessTransferFrom":
|
||||||
|
grants = [
|
||||||
|
{
|
||||||
|
token: message.permitted.token,
|
||||||
|
amount: message.permitted.amount,
|
||||||
|
max: MaxUint256,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
case "PermitBatchTransferFrom":
|
||||||
|
case "PermitBatchWitnessTransferFrom":
|
||||||
|
grants = message.permitted.map((p) => ({
|
||||||
|
token: p.token,
|
||||||
|
amount: p.amount,
|
||||||
|
max: MaxUint256,
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const sources = {
|
||||||
|
trackedTokens: state.trackedTokens,
|
||||||
|
wallets: state.wallets,
|
||||||
|
};
|
||||||
|
let html = `<div class="mb-2 p-2 font-bold bg-red-100 text-red-800 border-2 border-red-600 rounded-md">`;
|
||||||
|
html += `<div class="mb-2">⚠️ TOKEN PERMISSION: Signing this lets the spender below take the tokens listed here from your address, without asking you again.</div>`;
|
||||||
|
html += `<div class="mb-2"><div>Spender</div>${approvalAddressHtml(getAddress(message.spender))}</div>`;
|
||||||
|
for (const grant of grants) {
|
||||||
|
const token = getAddress(grant.token);
|
||||||
|
const amount = getBigInt(grant.amount);
|
||||||
|
// `Unlimited` as on the ERC-20 approve line; otherwise the quantity,
|
||||||
|
// or base units when nothing knows the token's scale.
|
||||||
|
const amountText =
|
||||||
|
amount === grant.max
|
||||||
|
? "Unlimited"
|
||||||
|
: tokenAmountText(
|
||||||
|
amount,
|
||||||
|
resolveTokenDecimals(token, sources),
|
||||||
|
tokenLabel(token),
|
||||||
|
).display;
|
||||||
|
html += `<div class="mb-2"><div>Token</div>`;
|
||||||
|
html += `<div>${escapeHtml(tokenLabel(token) || "Unknown token")}</div>`;
|
||||||
|
html += `${approvalAddressHtml(token)}</div>`;
|
||||||
|
html += `<div class="mb-2"><div>Amount</div><div>${escapeHtml(amountText)}</div></div>`;
|
||||||
|
}
|
||||||
|
html += `</div>`;
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The typed data as the screen shows it. The primary type shown is the one
|
||||||
|
// ethers signs, never the page's word for it; typedDataRefusal() keeps the two
|
||||||
|
// from differing on anything that can be signed.
|
||||||
function formatTypedDataHtml(jsonStr) {
|
function formatTypedDataHtml(jsonStr) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(jsonStr);
|
const data = JSON.parse(jsonStr);
|
||||||
let html = "";
|
const primaryType = signedPrimaryType(data.types);
|
||||||
|
let html = permitWarningHtml(primaryType, data.domain, data.message);
|
||||||
|
|
||||||
if (data.domain) {
|
if (data.domain) {
|
||||||
html += `<div class="mb-2"><div class="text-muted">Domain</div>`;
|
html += `<div class="mb-2"><div class="text-muted">Domain</div>`;
|
||||||
@@ -404,10 +547,8 @@ function formatTypedDataHtml(jsonStr) {
|
|||||||
html += `</div>`;
|
html += `</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.primaryType) {
|
html += `<div class="mb-2"><div class="text-muted">Primary type</div>`;
|
||||||
html += `<div class="mb-2"><div class="text-muted">Primary type</div>`;
|
html += `<div class="font-bold">${escapeHtml(primaryType)}</div></div>`;
|
||||||
html += `<div class="font-bold">${escapeHtml(data.primaryType)}</div></div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.message) {
|
if (data.message) {
|
||||||
html += `<div class="mb-2"><div class="text-muted">Message</div>`;
|
html += `<div class="mb-2"><div class="text-muted">Message</div>`;
|
||||||
@@ -477,6 +618,13 @@ function showSignApproval(details) {
|
|||||||
|
|
||||||
showView("approve-sign");
|
showView("approve-sign");
|
||||||
attachCopyHandlers("view-approve-sign");
|
attachCopyHandlers("view-approve-sign");
|
||||||
|
const refusal = typedDataRefusal(sp);
|
||||||
|
if (refusal) {
|
||||||
|
showError("approve-sign-error", refusal);
|
||||||
|
$("btn-approve-sign").disabled = true;
|
||||||
|
$("btn-approve-sign").classList.add("text-muted");
|
||||||
|
return;
|
||||||
|
}
|
||||||
gateOnWalletDefect(
|
gateOnWalletDefect(
|
||||||
"approve-sign-error",
|
"approve-sign-error",
|
||||||
"btn-approve-sign",
|
"btn-approve-sign",
|
||||||
@@ -782,6 +930,16 @@ function init(_ctx) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checked again where the signing starts, not only when the screen
|
||||||
|
// was drawn, and the button stays disabled: this request can never be
|
||||||
|
// signed.
|
||||||
|
const refusal = typedDataRefusal(pendingSignParams);
|
||||||
|
if (refusal) {
|
||||||
|
password = null;
|
||||||
|
showError("approve-sign-error", refusal);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Decrypt here, in the popup. The password must never cross the
|
// Decrypt here, in the popup. The password must never cross the
|
||||||
// extension messaging boundary; only the signature does.
|
// extension messaging boundary; only the signature does.
|
||||||
let decryptedSecret;
|
let decryptedSecret;
|
||||||
@@ -873,4 +1031,10 @@ function init(_ctx) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { init, show, decodeCalldata };
|
module.exports = {
|
||||||
|
init,
|
||||||
|
show,
|
||||||
|
decodeCalldata,
|
||||||
|
formatTypedDataHtml,
|
||||||
|
typedDataRefusal,
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
// The typed-data signing screen
|
||||||
|
// (https://git.eeqj.de/sneak/AutistMask/issues/400).
|
||||||
|
//
|
||||||
|
// A Permit or Permit2 signature lets its spender take tokens from the signer's
|
||||||
|
// address, and it is how most wallet drains are done. Listed as plain
|
||||||
|
// key/value lines it reads exactly like a sign-in message, so the screen has
|
||||||
|
// to warn for it, naming the spender and the amount, and must not warn for a
|
||||||
|
// sign-in message.
|
||||||
|
//
|
||||||
|
// ethers signs the type it derives from `types`, not the page's
|
||||||
|
// `primaryType`, so the screen names the derived type, and a request whose
|
||||||
|
// stated type is missing or differs is refused rather than shown under a name
|
||||||
|
// it is not signed as.
|
||||||
|
|
||||||
|
globalThis.chrome = {
|
||||||
|
storage: { local: { get: async () => ({}), set: async () => {} } },
|
||||||
|
};
|
||||||
|
|
||||||
|
const { getAddress } = require("ethers");
|
||||||
|
const { state } = require("../src/shared/state");
|
||||||
|
const {
|
||||||
|
formatTypedDataHtml,
|
||||||
|
typedDataRefusal,
|
||||||
|
} = require("../src/popup/views/approval");
|
||||||
|
|
||||||
|
const SPENDER = getAddress("0xbad000000000000000000000000000000000bad0");
|
||||||
|
const OWNER = getAddress("0x0000000000000000000000000000000000000a11");
|
||||||
|
// Bundled, so the symbol and the 6-decimal scale come from the list.
|
||||||
|
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
|
||||||
|
const DAI = "0x6B175474E89094C44Da98b954EedeAC495271d0F";
|
||||||
|
const PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
|
||||||
|
|
||||||
|
const WARNING = "TOKEN PERMISSION";
|
||||||
|
|
||||||
|
// Permit2's PermitSingle, granting the largest uint160 allowance there is.
|
||||||
|
const PERMIT_SINGLE = {
|
||||||
|
types: {
|
||||||
|
EIP712Domain: [
|
||||||
|
{ name: "name", type: "string" },
|
||||||
|
{ name: "chainId", type: "uint256" },
|
||||||
|
{ name: "verifyingContract", type: "address" },
|
||||||
|
],
|
||||||
|
PermitSingle: [
|
||||||
|
{ name: "details", type: "PermitDetails" },
|
||||||
|
{ name: "spender", type: "address" },
|
||||||
|
{ name: "sigDeadline", type: "uint256" },
|
||||||
|
],
|
||||||
|
PermitDetails: [
|
||||||
|
{ name: "token", type: "address" },
|
||||||
|
{ name: "amount", type: "uint160" },
|
||||||
|
{ name: "expiration", type: "uint48" },
|
||||||
|
{ name: "nonce", type: "uint48" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
primaryType: "PermitSingle",
|
||||||
|
domain: { name: "Permit2", chainId: 1, verifyingContract: PERMIT2 },
|
||||||
|
message: {
|
||||||
|
details: {
|
||||||
|
token: USDC,
|
||||||
|
amount: ((1n << 160n) - 1n).toString(),
|
||||||
|
expiration: "1790000000",
|
||||||
|
nonce: "0",
|
||||||
|
},
|
||||||
|
spender: SPENDER,
|
||||||
|
sigDeadline: "1790000000",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// EIP-2612's Permit for 5 USDC; the token is the domain's contract.
|
||||||
|
const PERMIT = {
|
||||||
|
types: {
|
||||||
|
EIP712Domain: [
|
||||||
|
{ name: "name", type: "string" },
|
||||||
|
{ name: "version", type: "string" },
|
||||||
|
{ name: "chainId", type: "uint256" },
|
||||||
|
{ name: "verifyingContract", type: "address" },
|
||||||
|
],
|
||||||
|
Permit: [
|
||||||
|
{ name: "owner", type: "address" },
|
||||||
|
{ name: "spender", type: "address" },
|
||||||
|
{ name: "value", type: "uint256" },
|
||||||
|
{ name: "nonce", type: "uint256" },
|
||||||
|
{ name: "deadline", type: "uint256" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
primaryType: "Permit",
|
||||||
|
domain: {
|
||||||
|
name: "USD Coin",
|
||||||
|
version: "2",
|
||||||
|
chainId: 1,
|
||||||
|
verifyingContract: USDC,
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
owner: OWNER,
|
||||||
|
spender: SPENDER,
|
||||||
|
value: "5000000",
|
||||||
|
nonce: "0",
|
||||||
|
deadline: "1790000000",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// DAI's older permit: the same name, no amount, all or nothing by `allowed`.
|
||||||
|
const DAI_PERMIT = {
|
||||||
|
types: {
|
||||||
|
EIP712Domain: PERMIT.types.EIP712Domain,
|
||||||
|
Permit: [
|
||||||
|
{ name: "holder", type: "address" },
|
||||||
|
{ name: "spender", type: "address" },
|
||||||
|
{ name: "nonce", type: "uint256" },
|
||||||
|
{ name: "expiry", type: "uint256" },
|
||||||
|
{ name: "allowed", type: "bool" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
primaryType: "Permit",
|
||||||
|
domain: {
|
||||||
|
name: "Dai Stablecoin",
|
||||||
|
version: "1",
|
||||||
|
chainId: 1,
|
||||||
|
verifyingContract: DAI,
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
holder: OWNER,
|
||||||
|
spender: SPENDER,
|
||||||
|
nonce: "0",
|
||||||
|
expiry: "0",
|
||||||
|
allowed: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const LOGIN = {
|
||||||
|
types: {
|
||||||
|
EIP712Domain: [
|
||||||
|
{ name: "name", type: "string" },
|
||||||
|
{ name: "version", type: "string" },
|
||||||
|
{ name: "chainId", type: "uint256" },
|
||||||
|
],
|
||||||
|
Login: [
|
||||||
|
{ name: "contents", type: "string" },
|
||||||
|
{ name: "nonce", type: "uint256" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
primaryType: "Login",
|
||||||
|
domain: { name: "Example", version: "1", chainId: 1 },
|
||||||
|
message: { contents: "Sign in to example.com", nonce: "7" },
|
||||||
|
};
|
||||||
|
|
||||||
|
// The warning box alone. It comes before the key/value lines, which list the
|
||||||
|
// spender and the raw amount too, so an assertion on the whole screen would
|
||||||
|
// pass with no warning at all.
|
||||||
|
function warningOf(data) {
|
||||||
|
const html = formatTypedDataHtml(JSON.stringify(data));
|
||||||
|
return html.slice(0, html.indexOf(">Domain<"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function request(data) {
|
||||||
|
return { method: "eth_signTypedData_v4", typedData: JSON.stringify(data) };
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
state.trackedTokens = [];
|
||||||
|
state.wallets = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("a token permission is warned about, naming spender and amount", () => {
|
||||||
|
test("a Permit2 PermitSingle for an unlimited allowance", () => {
|
||||||
|
const warning = warningOf(PERMIT_SINGLE);
|
||||||
|
expect(warning).toContain(WARNING);
|
||||||
|
expect(warning).toContain(SPENDER);
|
||||||
|
expect(warning).toContain(USDC);
|
||||||
|
expect(warning).toContain("Unlimited");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("an EIP-2612 Permit for 5 USDC", () => {
|
||||||
|
const warning = warningOf(PERMIT);
|
||||||
|
expect(warning).toContain(WARNING);
|
||||||
|
expect(warning).toContain(SPENDER);
|
||||||
|
expect(warning).toContain("5.0000 USDC");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("DAI's older permit, which grants everything", () => {
|
||||||
|
const warning = warningOf(DAI_PERMIT);
|
||||||
|
expect(warning).toContain(WARNING);
|
||||||
|
expect(warning).toContain(SPENDER);
|
||||||
|
expect(warning).toContain("Unlimited");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a sign-in message carries no warning, and is still shown", () => {
|
||||||
|
const html = formatTypedDataHtml(JSON.stringify(LOGIN));
|
||||||
|
expect(html).not.toContain(WARNING);
|
||||||
|
expect(html).toContain("Sign in to example.com");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("the primary type shown is the one ethers signs", () => {
|
||||||
|
// A drain stated as a sign-in: the page says Login, the types say
|
||||||
|
// PermitSingle, and ethers would sign PermitSingle.
|
||||||
|
const disguised = { ...PERMIT_SINGLE, primaryType: "Login" };
|
||||||
|
|
||||||
|
test("a stated type that differs from the signed one is refused", () => {
|
||||||
|
expect(typedDataRefusal(request(disguised))).toBe(
|
||||||
|
"This typed data names its primary type as Login, but it would be signed as PermitSingle, so it cannot be signed.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the screen names the signed type, and still warns", () => {
|
||||||
|
const html = formatTypedDataHtml(JSON.stringify(disguised));
|
||||||
|
expect(html).toContain(">PermitSingle<");
|
||||||
|
expect(html).not.toContain(">Login<");
|
||||||
|
expect(html).toContain(WARNING);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a missing primary type is refused", () => {
|
||||||
|
const unnamed = { ...PERMIT_SINGLE, primaryType: undefined };
|
||||||
|
expect(typedDataRefusal(request(unnamed))).toBe(
|
||||||
|
"This typed data does not name its primary type, so it cannot be signed.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a stated type that is the signed one is not refused", () => {
|
||||||
|
expect(typedDataRefusal(request(PERMIT_SINGLE))).toBeNull();
|
||||||
|
expect(typedDataRefusal(request(LOGIN))).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user