Skip to content

Commit 239694c

Browse files
david-uniswapclaude
andcommitted
test(universal-router-sdk): encodeSwaps golden byte-identity vs main and 100% fee boundary
- Pin single portion-fee (V2_1_1 and V2_0), single flat-fee, and no-fee encodeSwaps calldata against golden hex captured from unmodified main (48dea05), proving the multi-recipient change leaves single-fee encodings byte-identical - Boundary tests: fees summing to exactly 100% encode the last portion as the full remaining balance (1e18) and zero the settlement sweep floor; 100% + 1 bps throws FEE_TOTAL_GT_AMOUNT_OUT - computeEncodeSwapsAmounts: exactly 100% yields a zero net output without underflow; just over throws at the call site Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 9ba557d commit 239694c

2 files changed

Lines changed: 193 additions & 0 deletions

File tree

sdks/universal-router-sdk/test/unit/encodeSwaps.test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {
2121
import { encodeSwapStep } from '../../src/utils/encodeSwapStep'
2222
import { validateEncodeSwaps } from '../../src/utils/validateEncodeSwaps'
2323
import { encodeFee1e18 } from '../../src/utils/numbers'
24+
import { computeEncodeSwapsAmounts } from '../../src/utils/computeEncodeSwapsAmounts'
25+
import { ENCODE_SWAPS_GOLDEN } from './fixtures/encodeSwapsGolden'
2426
import {
2527
CONTRACT_BALANCE,
2628
ETH_ADDRESS,
@@ -1828,6 +1830,89 @@ describe('encodeSwaps', () => {
18281830
})
18291831
})
18301832

1833+
describe('100% total boundary', () => {
1834+
const P60 = portion(6000, RECIPIENT_A) // 60%
1835+
const P40 = portion(4000, RECIPIENT_B) // 40%
1836+
const P40_PLUS_1BPS = portion(4001, RECIPIENT_B) // 40.01%
1837+
1838+
it('encodes the last portion as the full remaining balance (1e18) at exactly 100%', () => {
1839+
const result = SwapRouter.encodeSwaps(exactInSpec([P60, P40], UniversalRouterVersion.V2_1_1), [
1840+
buildV3ExactInStep(),
1841+
])
1842+
1843+
const cmds = decodeFeeCommands(result.calldata)
1844+
expect(cmds).to.have.length(2)
1845+
expect(cmds[1].params[2].toString()).to.equal(BigNumber.from(10).pow(18).toString())
1846+
1847+
const { commandTypes, inputs } = parseCommands(result.calldata)
1848+
expect(settlementSweep(commandTypes, inputs).decoded[2].toString()).to.equal('0')
1849+
})
1850+
1851+
it('throws just over 100% (100% + 1 bps)', () => {
1852+
expect(() =>
1853+
SwapRouter.encodeSwaps(exactInSpec([P60, P40_PLUS_1BPS], UniversalRouterVersion.V2_1_1), [
1854+
buildV3ExactInStep(),
1855+
])
1856+
).to.throw('FEE_TOTAL_GT_AMOUNT_OUT')
1857+
})
1858+
1859+
it('computeEncodeSwapsAmounts returns a zero net output at exactly 100% without underflowing', () => {
1860+
const spec = buildSpec({ fee: [P60, P40], urVersion: UniversalRouterVersion.V2_1_1 })
1861+
const amounts = computeEncodeSwapsAmounts(spec)
1862+
1863+
const grossMin = exactInputGrossMin(
1864+
BigNumber.from(spec.routing.quote.quotient.toString()),
1865+
spec.slippageTolerance
1866+
)
1867+
expect(amounts.grossMinOrExactAmountOut.toString()).to.equal(grossMin.toString())
1868+
expect(amounts.netMinOrExactAmountOut.toString()).to.equal('0')
1869+
})
1870+
1871+
it('computeEncodeSwapsAmounts throws just over 100% instead of underflowing', () => {
1872+
const spec = buildSpec({ fee: [P60, P40_PLUS_1BPS], urVersion: UniversalRouterVersion.V2_1_1 })
1873+
expect(() => computeEncodeSwapsAmounts(spec)).to.throw('FEE_TOTAL_GT_AMOUNT_OUT')
1874+
})
1875+
})
1876+
1877+
describe('byte-identity with pre-multi-fee encoding (golden calldata from main)', () => {
1878+
// ENCODE_SWAPS_GOLDEN was generated by running SwapRouter.encodeSwaps on the unmodified
1879+
// main branch (commit 48dea05c) with these exact specs and steps. Byte equality here
1880+
// proves the single-fee paths are untouched by the multi-recipient change.
1881+
const GOLDEN_PORTION: Fee = { kind: 'portion', recipient: FEE_RECIPIENT, fee: new Percent(5, 100) }
1882+
const GOLDEN_FLAT: Fee = { kind: 'flat', recipient: FEE_RECIPIENT, amount: '100000000000000000' }
1883+
1884+
it('single portion fee, exact input, V2_1_1 is byte-identical to main', () => {
1885+
const mp = SwapRouter.encodeSwaps(
1886+
buildSpec({ fee: GOLDEN_PORTION, urVersion: UniversalRouterVersion.V2_1_1 }),
1887+
[buildV3ExactInStep()]
1888+
)
1889+
expect(mp.calldata).to.equal(ENCODE_SWAPS_GOLDEN.portionExactInV2_1_1.calldata)
1890+
expect(mp.value).to.equal(ENCODE_SWAPS_GOLDEN.portionExactInV2_1_1.value)
1891+
})
1892+
1893+
it('single portion fee, exact input, V2_0 (bips) is byte-identical to main', () => {
1894+
const mp = SwapRouter.encodeSwaps(buildSpec({ fee: GOLDEN_PORTION, urVersion: UniversalRouterVersion.V2_0 }), [
1895+
buildV3ExactInStep(),
1896+
])
1897+
expect(mp.calldata).to.equal(ENCODE_SWAPS_GOLDEN.portionExactInV2_0.calldata)
1898+
expect(mp.value).to.equal(ENCODE_SWAPS_GOLDEN.portionExactInV2_0.value)
1899+
})
1900+
1901+
it('single flat fee, exact output is byte-identical to main', () => {
1902+
const mp = SwapRouter.encodeSwaps(exactOutSpec(GOLDEN_FLAT), exactOutSteps())
1903+
expect(mp.calldata).to.equal(ENCODE_SWAPS_GOLDEN.flatExactOutV2_1_1.calldata)
1904+
expect(mp.value).to.equal(ENCODE_SWAPS_GOLDEN.flatExactOutV2_1_1.value)
1905+
})
1906+
1907+
it('no fee, exact input is byte-identical to main', () => {
1908+
const mp = SwapRouter.encodeSwaps(buildSpec({ urVersion: UniversalRouterVersion.V2_1_1 }), [
1909+
buildV3ExactInStep(),
1910+
])
1911+
expect(mp.calldata).to.equal(ENCODE_SWAPS_GOLDEN.noFeeExactInV2_1_1.calldata)
1912+
expect(mp.value).to.equal(ENCODE_SWAPS_GOLDEN.noFeeExactInV2_1_1.value)
1913+
})
1914+
})
1915+
18311916
describe('flat fees on exact output', () => {
18321917
it('emits one TRANSFER per recipient and settles the summed net amount', () => {
18331918
const fees = [flat('100000000000000000', RECIPIENT_A), flat('50000000000000000', RECIPIENT_B)]
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Golden calldata captured from the unmodified main branch (commit 48dea05c) by running
2+
// SwapRouter.encodeSwaps on a fixed USDC->WETH V3 spec (see the byte-identity tests for the
3+
// exact spec). These pin the byte-identity claim: single-fee encodeSwaps output must not change.
4+
export const ENCODE_SWAPS_GOLDEN = {
5+
portionExactInV2_1_1: {
6+
calldata:
7+
'0x24856bc300000000000000000000000000000000000000000000000000000000000000400000000000000000000000' +
8+
'000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000' +
9+
'000000000402000704000000000000000000000000000000000000000000000000000000000000000000000000000000' +
10+
'000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000' +
11+
'000000008000000000000000000000000000000000000000000000000000000000000001000000000000000000000000' +
12+
'000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000' +
13+
'00000002e000000000000000000000000000000000000000000000000000000000000000600000000000000000000000' +
14+
'00a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000' +
15+
'000000000200000000000000000000000000000000000000000000000000000000000f42400000000000000000000000' +
16+
'000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000' +
17+
'000000000200000000000000000000000000000000000000000000000000000000000f42400000000000000000000000' +
18+
'000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' +
19+
'00000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' +
20+
'000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000' +
21+
'000000002ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb480001f4c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' +
22+
'000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' +
23+
'000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000' +
24+
'00c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' +
25+
'bbbbbbbbbb00000000000000000000000000000000000000000000000000b1a2bc2ec500000000000000000000000000' +
26+
'000000000000000000000000000000000000000060000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9' +
27+
'083c756cc2000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000000000000000000' +
28+
'000000000000000000000000000643297bf2e52000',
29+
value: '0x00',
30+
},
31+
portionExactInV2_0: {
32+
calldata:
33+
'0x24856bc300000000000000000000000000000000000000000000000000000000000000400000000000000000000000' +
34+
'000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000' +
35+
'000000000402000604000000000000000000000000000000000000000000000000000000000000000000000000000000' +
36+
'000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000' +
37+
'000000008000000000000000000000000000000000000000000000000000000000000001000000000000000000000000' +
38+
'000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000' +
39+
'00000002a000000000000000000000000000000000000000000000000000000000000000600000000000000000000000' +
40+
'00a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000' +
41+
'000000000200000000000000000000000000000000000000000000000000000000000f42400000000000000000000000' +
42+
'000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000' +
43+
'000000000200000000000000000000000000000000000000000000000000000000000f42400000000000000000000000' +
44+
'000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' +
45+
'00000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' +
46+
'00000000000000000000000000000000000000002ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb480001f4c02aaa39' +
47+
'b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000000' +
48+
'000000000000000000000000000000000000000060000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9' +
49+
'083c756cc2000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb0000000000000000000000' +
50+
'0000000000000000000000000000000000000001f4000000000000000000000000000000000000000000000000000000' +
51+
'0000000060000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000' +
52+
'00aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa000000000000000000000000000000000000000000000000064329' +
53+
'7bf2e52000',
54+
value: '0x00',
55+
},
56+
flatExactOutV2_1_1: {
57+
calldata:
58+
'0x24856bc300000000000000000000000000000000000000000000000000000000000000400000000000000000000000' +
59+
'000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000' +
60+
'000000000502010504040000000000000000000000000000000000000000000000000000000000000000000000000000' +
61+
'000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000' +
62+
'00000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000' +
63+
'000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000' +
64+
'000000030000000000000000000000000000000000000000000000000000000000000003800000000000000000000000' +
65+
'000000000000000000000000000000000000000060000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0' +
66+
'ce3606eb4800000000000000000000000000000000000000000000000000000000000000020000000000000000000000' +
67+
'000000000000000000000000000000000000100590000000000000000000000000000000000000000000000000000000' +
68+
'000000014000000000000000000000000000000000000000000000000000000000000000020000000000000000000000' +
69+
'0000000000000000000000000006f05b59d3b20000000000000000000000000000000000000000000000000000000000' +
70+
'000010059000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000' +
71+
'000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' +
72+
'0000000120000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c' +
73+
'4f27ead9083c756cc20001f4a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000' +
74+
'000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' +
75+
'000000000000000000000000000000000000000060000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9' +
76+
'083c756cc2000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb0000000000000000000000' +
77+
'00000000000000000000000000016345785d8a0000000000000000000000000000000000000000000000000000000000' +
78+
'0000000060000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000' +
79+
'00aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa000000000000000000000000000000000000000000000000058d15' +
80+
'e17628000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000' +
81+
'00a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' +
82+
'aaaaaaaaaa0000000000000000000000000000000000000000000000000000000000000000',
83+
value: '0x00',
84+
},
85+
noFeeExactInV2_1_1: {
86+
calldata:
87+
'0x24856bc300000000000000000000000000000000000000000000000000000000000000400000000000000000000000' +
88+
'000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000' +
89+
'000000000302000400000000000000000000000000000000000000000000000000000000000000000000000000000000' +
90+
'000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000' +
91+
'000000006000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000' +
92+
'000000000000000000000000000000000000000240000000000000000000000000000000000000000000000000000000' +
93+
'0000000060000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000' +
94+
'000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000' +
95+
'00000f424000000000000000000000000000000000000000000000000000000000000001400000000000000000000000' +
96+
'000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000' +
97+
'00000f424000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' +
98+
'0000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000' +
99+
'000000000000000000000000000000000000000000000000000000000000000000000001200000000000000000000000' +
100+
'00000000000000000000000000000000000000002ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb480001f4c02aaa39' +
101+
'b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000000' +
102+
'000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' +
103+
'0000000060000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000' +
104+
'00aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa000000000000000000000000000000000000000000000000069789' +
105+
'fbbc4f8000',
106+
value: '0x00',
107+
},
108+
}

0 commit comments

Comments
 (0)