fix: a zero-minimum balance check never changes the output side (closes #649)
A BALANCE_CHECK_ERC20 of the sender with a zero minimum guarantees nothing. It replaced the output side whenever that side held no minimum, so after an UNWRAP_WETH that drops the minimum, a zero check of USDC named USDC as Token Out. Such a check now leaves alone an output side that an earlier step set. It still sets the output side when no earlier step did. A check with a positive minimum is unchanged. Model: opus-5-5
This commit is contained in:
@@ -1283,9 +1283,8 @@ see Transaction Decoding), do not always read as a number. They can read:
|
|||||||
[#359](https://git.eeqj.de/sneak/AutistMask/issues/359), a zero `minBalance`
|
[#359](https://git.eeqj.de/sneak/AutistMask/issues/359), a zero `minBalance`
|
||||||
read `0.0000` when the token's scale was known. The router passes a balance
|
read `0.0000` when the token's scale was known. The router passes a balance
|
||||||
check whenever the balance is at least `minBalance`, so a zero `minBalance`
|
check whenever the balance is at least `minBalance`, so a zero `minBalance`
|
||||||
guarantees nothing: it sets `Token Out` and `Min. received` only when the
|
guarantees nothing. A check with a zero `minBalance` sets `Token Out` and
|
||||||
output side holds no minimum, not even a zero one, at the point the check is
|
`Min. received` only when no earlier step set the output side. A nonzero
|
||||||
reached, and otherwise leaves the current token and figure in place. A nonzero
|
|
||||||
`minBalance` sets both lines, as a swap step does. Only a check of the
|
`minBalance` sets both lines, as a swap step does. Only a check of the
|
||||||
transaction's sender's own balance sets either line; a check of any other
|
transaction's sender's own balance sets either line; a check of any other
|
||||||
address sets neither (see Transaction Decoding). A `BALANCE_CHECK_ERC20` step
|
address sets neither (see Transaction Decoding). A `BALANCE_CHECK_ERC20` step
|
||||||
@@ -3397,12 +3396,13 @@ Currently supported:
|
|||||||
`Balance Check (of another address)`. A check whose input does not decode,
|
`Balance Check (of another address)`. A check whose input does not decode,
|
||||||
such as one whose owner word has a nonzero top byte (the router reads the
|
such as one whose owner word has a nonzero top byte (the router reads the
|
||||||
owner from the word's last 20 bytes), likewise sets neither, and it is listed
|
owner from the word's last 20 bytes), likewise sets neither, and it is listed
|
||||||
as `Balance Check (could not be decoded)`. When a swap step, `TAKE` or
|
as `Balance Check (could not be decoded)`. A check with a zero `minBalance`
|
||||||
`TAKE_PORTION` pays the router any token, the swap's output or another, a
|
never changes an output side that an earlier step set. When a swap step,
|
||||||
`WRAP_ETH` pays it WETH, or an `UNWRAP_WETH` into the router turns WETH a step
|
`TAKE` or `TAKE_PORTION` pays the router any token, the swap's output or
|
||||||
paid it into ETH, that token stays in the router unless a later step pays out
|
another, a `WRAP_ETH` pays it WETH, or an `UNWRAP_WETH` into the router turns
|
||||||
or spends all the router holds of it: a `SWEEP` of that token to anyone but
|
WETH a step paid it into ETH, that token stays in the router unless a later
|
||||||
the router, an `UNWRAP_WETH` when it is WETH, a `WRAP_ETH` of
|
step pays out or spends all the router holds of it: a `SWEEP` of that token to
|
||||||
|
anyone but the router, an `UNWRAP_WETH` when it is WETH, a `WRAP_ETH` of
|
||||||
`CONTRACT_BALANCE` when it is ETH, a `TRANSFER` of `CONTRACT_BALANCE`, a
|
`CONTRACT_BALANCE` when it is ETH, a `TRANSFER` of `CONTRACT_BALANCE`, a
|
||||||
`PAY_PORTION` of 10,000 bips or a `PAY_PORTION_FULL_PRECISION` of 1e18 of that
|
`PAY_PORTION` of 10,000 bips or a `PAY_PORTION_FULL_PRECISION` of 1e18 of that
|
||||||
token to anyone but the router, an `ACROSS_V4_DEPOSIT_V3` of
|
token to anyone but the router, an `ACROSS_V4_DEPOSIT_V3` of
|
||||||
|
|||||||
@@ -1629,13 +1629,14 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
} else {
|
} else {
|
||||||
// The router passes this check whenever the owner
|
// The router passes this check whenever the owner
|
||||||
// holds at least minBalance, so a zero one guarantees
|
// holds at least minBalance, so a zero one guarantees
|
||||||
// nothing and does not replace a minimum an earlier
|
// nothing. A check with a zero minBalance leaves
|
||||||
// step stated. A check the router lets fail guarantees
|
// alone an output side that an earlier step set,
|
||||||
// nothing either, so it is read as a zero one. Any
|
// token included. A check the router lets fail
|
||||||
// other minBalance sets the output side as a swap
|
// guarantees nothing either, so it is read as a zero
|
||||||
// does. It pays nobody.
|
// one. Any other minBalance sets the output side as a
|
||||||
|
// swap does. The check pays nobody.
|
||||||
const minBalance = allowedToFail ? 0n : b.minBalance;
|
const minBalance = allowedToFail ? 0n : b.minBalance;
|
||||||
if (!(minBalance === 0n && present(minOutput))) {
|
if (!(minBalance === 0n && outputEstablished)) {
|
||||||
setOutput(i, b.token, minBalance, []);
|
setOutput(i, b.token, minBalance, []);
|
||||||
}
|
}
|
||||||
keepsRouterWeth = true;
|
keepsRouterWeth = true;
|
||||||
|
|||||||
@@ -688,6 +688,34 @@ describe("an UNWRAP_WETH makes the output ETH only when the router holds it", ()
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The same route, then a balance check of the sender's USDC with a zero
|
||||||
|
// minimum. The check guarantees nothing, so Token Out stays ETH
|
||||||
|
// (https://git.eeqj.de/sneak/AutistMask/issues/649).
|
||||||
|
test("the same route, then a zero balance check of USDC, still reads as ETH with no minimum", () => {
|
||||||
|
const result = decode(
|
||||||
|
execute(
|
||||||
|
v4Swap(
|
||||||
|
SWAP_USDT_FOR_WETH,
|
||||||
|
SETTLE_USDT,
|
||||||
|
take(WETH, MSG_SENDER, (ONE_WETH * 6n) / 10n),
|
||||||
|
take(WETH, ADDRESS_THIS),
|
||||||
|
),
|
||||||
|
unwrapWeth(MSG_SENDER),
|
||||||
|
balanceCheck(USDC, 0n),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(result.name).toBe("Swap USDT → ETH");
|
||||||
|
expect(detail(result, "Token Out").value).toBe("ETH");
|
||||||
|
expect(labels(result)).toEqual([
|
||||||
|
"Protocol",
|
||||||
|
"Token In",
|
||||||
|
"Amount",
|
||||||
|
"Token Out",
|
||||||
|
"Steps",
|
||||||
|
"Deadline",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
// A TAKE the decoder cannot read may pay part of the WETH to the user.
|
// A TAKE the decoder cannot read may pay part of the WETH to the user.
|
||||||
test("one V4 step paying WETH to the router beside a TAKE that cannot be read, then an UNWRAP_WETH, reads as ETH with no minimum", () => {
|
test("one V4 step paying WETH to the router beside a TAKE that cannot be read, then an UNWRAP_WETH, reads as ETH with no minimum", () => {
|
||||||
const result = decode(
|
const result = decode(
|
||||||
|
|||||||
Reference in New Issue
Block a user