diff --git a/README.md b/README.md index 63e2994..742c38a 100644 --- a/README.md +++ b/README.md @@ -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` 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` - guarantees nothing: it sets `Token Out` and `Min. received` only when the - output side holds no minimum, not even a zero one, at the point the check is - reached, and otherwise leaves the current token and figure in place. A nonzero + guarantees nothing. A check with a zero `minBalance` sets `Token Out` and + `Min. received` only when no earlier step set the output side. A nonzero `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 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, 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 - as `Balance Check (could not be decoded)`. When a swap step, `TAKE` or - `TAKE_PORTION` pays the router any token, the swap's output or another, a - `WRAP_ETH` pays it WETH, or an `UNWRAP_WETH` into the router turns WETH a step - paid it into ETH, that token stays in the router unless a later 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 + as `Balance Check (could not be decoded)`. A check with a zero `minBalance` + never changes an output side that an earlier step set. When a swap step, + `TAKE` or `TAKE_PORTION` pays the router any token, the swap's output or + another, a `WRAP_ETH` pays it WETH, or an `UNWRAP_WETH` into the router turns + WETH a step paid it into ETH, that token stays in the router unless a later + 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 `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 diff --git a/src/shared/uniswap.js b/src/shared/uniswap.js index 5e98da6..a7a8f67 100644 --- a/src/shared/uniswap.js +++ b/src/shared/uniswap.js @@ -1629,13 +1629,14 @@ function decode(data, toAddress, chainId, sources, fromAddress) { } else { // The router passes this check whenever the owner // holds at least minBalance, so a zero one guarantees - // nothing and does not replace a minimum an earlier - // step stated. A check the router lets fail guarantees - // nothing either, so it is read as a zero one. Any - // other minBalance sets the output side as a swap - // does. It pays nobody. + // nothing. A check with a zero minBalance leaves + // alone an output side that an earlier step set, + // token included. A check the router lets fail + // guarantees nothing either, so it is read as a zero + // one. Any other minBalance sets the output side as a + // swap does. The check pays nobody. const minBalance = allowedToFail ? 0n : b.minBalance; - if (!(minBalance === 0n && present(minOutput))) { + if (!(minBalance === 0n && outputEstablished)) { setOutput(i, b.token, minBalance, []); } keepsRouterWeth = true; diff --git a/tests/uniswapRecipient.test.js b/tests/uniswapRecipient.test.js index 4e9b9e6..78ab545 100644 --- a/tests/uniswapRecipient.test.js +++ b/tests/uniswapRecipient.test.js @@ -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. 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(