diff --git a/contracts/utils/YulByteSwapper.sol b/contracts/utils/YulByteSwapper.sol new file mode 100644 index 0000000..271edcd --- /dev/null +++ b/contracts/utils/YulByteSwapper.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +/** + * @title YulByteSwapper + * @notice High-performance 256-bit endianness reversal utility. + * + * Reverses all 32 bytes of a bytes32/uint256 value using a fixed sequence + * of bitwise operations in Yul. + * + * No loops. + * No memory allocation. + * O(1) execution complexity. + */ +library YulByteSwapper { + /** + * @notice Reverse the byte order of a 256-bit word. + * + * Example: + * 0x0102030405060708091011121314151617181920212223242526272829303132 + * + * becomes: + * 0x3231302928272625242322212019181716151413121110090807060504030201 + * + * @param value The 256-bit word to reverse. + * @return result The byte-reversed 256-bit word. + */ + function swap(bytes32 value) internal pure returns (bytes32 result) { + assembly { + let x := value + + // Swap adjacent bytes: + // ABCD -> BADC + x := or( + and(shr(8, x), 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF), + and(shl(8, x), 0xFF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF) + ) + + // Swap adjacent 2-byte groups: + // BADC -> DCBA + x := or( + and( + shr(16, x), + 0x0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF + ), + and( + shl(16, x), + 0xFFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000 + ) + ) + + // Swap adjacent 4-byte groups. + x := or( + and( + shr(32, x), + 0x00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF + ), + and( + shl(32, x), + 0xFFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000 + ) + ) + + // Swap adjacent 8-byte groups. + x := or( + and( + shr(64, x), + 0x0000000000000000FFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF + ), + and( + shl(64, x), + 0xFFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF0000000000000000 + ) + ) + + // Swap the two 16-byte halves. + result := or(shr(128, x), shl(128, x)) + } + } + + /** + * @notice Reverse the byte order of a uint256. + * @param value The 256-bit word to reverse. + * @return result The byte-reversed 256-bit word. + */ + function swapUint256(uint256 value) internal pure returns (uint256 result) { + result = uint256(swap(bytes32(value))); + } +} \ No newline at end of file diff --git a/contracts/utils/YulByteSwapper.test.ts b/contracts/utils/YulByteSwapper.test.ts new file mode 100644 index 0000000..8e68d72 --- /dev/null +++ b/contracts/utils/YulByteSwapper.test.ts @@ -0,0 +1,95 @@ +import { expect } from 'chai'; +import { ethers } from 'hardhat'; + +describe('YulByteSwapper', function () { + async function deployHarness() { + const factory = await ethers.getContractFactory('YulByteSwapperHarness'); + return factory.deploy(); + } + + function reverseBytes(value: bigint): bigint { + const hex = value.toString(16).padStart(64, '0'); + + const reversed = hex.match(/.{2}/g)!.reverse().join(''); + + return BigInt(`0x${reversed}`); + } + + it('reverses a known bytes32 value', async function () { + const harness = await deployHarness(); + + const input = + '0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20'; + + const expected = + '0x201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030201'; + + expect(await harness.swap(input)).to.equal(expected); + }); + + it('returns the same value when all bytes are identical', async function () { + const harness = await deployHarness(); + + const input = + '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; + + expect(await harness.swap(input)).to.equal(input); + }); + + it('correctly reverses zero', async function () { + const harness = await deployHarness(); + + const input = + '0x0000000000000000000000000000000000000000000000000000000000000000'; + + expect(await harness.swap(input)).to.equal(input); + }); + + it('correctly reverses the maximum uint256', async function () { + const harness = await deployHarness(); + + const input = + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; + + expect(await harness.swap(input)).to.equal(input); + }); + + it('matches a high-level endian conversion', async function () { + const harness = await deployHarness(); + + const values = [ + 0n, + 1n, + 0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20n, + 0xdeadbeefn, + 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdefn, + (1n << 255n) + 1n, + (1n << 256n) - 1n, + ]; + + for (const value of values) { + const expected = reverseBytes(value); + const actual = await harness.swapUint256(value); + + expect(BigInt(actual)).to.equal(expected); + } + }); + + it('is its own inverse', async function () { + const harness = await deployHarness(); + + const values = [ + 1n, + 0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20n, + 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdefn, + (1n << 255n) + 123456789n, + ]; + + for (const value of values) { + const first = await harness.swapUint256(value); + const second = await harness.swapUint256(first); + + expect(BigInt(second)).to.equal(value); + } + }); +}); \ No newline at end of file diff --git a/contracts/utils/YulByteSwapperHarness.sol b/contracts/utils/YulByteSwapperHarness.sol new file mode 100644 index 0000000..30f7d1b --- /dev/null +++ b/contracts/utils/YulByteSwapperHarness.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {YulByteSwapper} from './YulByteSwapper.sol'; + +contract YulByteSwapperHarness { + function swap(bytes32 value) external pure returns (bytes32) { + return YulByteSwapper.swap(value); + } + + function swapUint256(uint256 value) external pure returns (uint256) { + return YulByteSwapper.swapUint256(value); + } +} \ No newline at end of file diff --git a/test/cache/TransientCache.test.ts b/test/cache/TransientCache.test.ts index cd586aa..8b20963 100644 --- a/test/cache/TransientCache.test.ts +++ b/test/cache/TransientCache.test.ts @@ -3,13 +3,15 @@ * Issue #634 */ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect } from "vitest"; -describe('TransientCache', () => { - describe('tstore/tload operations', () => { - it('should store and load a bytes32 value', () => { - const slot = '0x0000000000000000000000000000000000000000000000000000000000000001'; - const value = '0x0000000000000000000000000000000000000000000000000000000000000042'; +describe("TransientCache", () => { + describe("tstore/tload operations", () => { + it("should store and load a bytes32 value", () => { + const slot = + "0x0000000000000000000000000000000000000000000000000000000000000001"; + const value = + "0x0000000000000000000000000000000000000000000000000000000000000042"; // Simulate transient storage operations const storage = new Map(); @@ -17,42 +19,44 @@ describe('TransientCache', () => { expect(storage.get(slot)).toBe(value); }); - it('should clear a transient storage slot', () => { - const slot = '0x0000000000000000000000000000000000000000000000000000000000000001'; + it("should clear a transient storage slot", () => { + const slot = + "0x0000000000000000000000000000000000000000000000000000000000000001"; const storage = new Map(); - storage.set(slot, '0x42'); + storage.set(slot, "0x42"); storage.delete(slot); expect(storage.has(slot)).toBe(false); }); - it('should handle multiple concurrent cache entries', () => { + it("should handle multiple concurrent cache entries", () => { const storage = new Map(); - const slots = Array.from({ length: 10 }, (_, i) => - `0x${i.toString(16).padStart(64, '0')}` + const slots = Array.from( + { length: 10 }, + (_, i) => `0x${i.toString(16).padStart(64, "0")}`, ); slots.forEach((slot, i) => { - storage.set(slot, `0x${i.toString(16).padStart(64, '0')}`); + storage.set(slot, `0x${i.toString(16).padStart(64, "0")}`); }); slots.forEach((slot, i) => { - expect(storage.get(slot)).toBe(`0x${i.toString(16).padStart(64, '0')}`); + expect(storage.get(slot)).toBe(`0x${i.toString(16).padStart(64, "0")}`); }); }); }); - describe('TransientCacheConsumer', () => { - it('should calculate fee correctly', () => { + describe("TransientCacheConsumer", () => { + it("should calculate fee correctly", () => { const baseFeeRate = 1000n; const amount = 10000n; const rate = baseFeeRate; - const fee = (amount * rate) / (10n ** 18n); + const fee = (amount * rate) / 10n ** 18n; expect(fee).toBe(0n); }); - it('should cache fee within same transaction', () => { + it("should cache fee within same transaction", () => { const cache = new Map(); - const user = '0x1234567890123456789012345678901234567890'; + const user = "0x1234567890123456789012345678901234567890"; const slot = BigInt(user); // First call - compute and cache @@ -63,7 +67,7 @@ describe('TransientCache', () => { expect(cache.get(slot.toString())).toBe(fee); }); - it('should handle zero amount error', () => { + it("should handle zero amount error", () => { const amount = 0n; expect(amount === 0n).toBe(true); }); diff --git a/test/calldata/CalldataUnpacker.test.ts b/test/calldata/CalldataUnpacker.test.ts index 1b98dc4..2166fab 100644 --- a/test/calldata/CalldataUnpacker.test.ts +++ b/test/calldata/CalldataUnpacker.test.ts @@ -10,8 +10,11 @@ interface PackedEntry { function packEntries(entries: PackedEntry[]): string { return ethers.concat( entries.map((entry) => - ethers.solidityPacked(["address", "uint96"], [entry.recipient, entry.amount]) - ) + ethers.solidityPacked( + ["address", "uint96"], + [entry.recipient, entry.amount], + ), + ), ); } @@ -19,7 +22,7 @@ function abiEncodeEntries(entries: PackedEntry[]): string { const coder = ethers.AbiCoder.defaultAbiCoder(); return coder.encode( ["tuple(address recipient, uint96 amount)[]"], - [entries.map((e) => [e.recipient, e.amount])] + [entries.map((e) => [e.recipient, e.amount])], ); } @@ -58,7 +61,9 @@ describe("CalldataUnpacker", function () { .withArgs(entries[0].recipient, entries[0].amount); for (const entry of entries) { - expect(await unpacker.totalReceived(entry.recipient)).to.equal(entry.amount); + expect(await unpacker.totalReceived(entry.recipient)).to.equal( + entry.amount, + ); } }); @@ -70,16 +75,15 @@ describe("CalldataUnpacker", function () { it("reverts when the payload length is not a multiple of 32 bytes", async function () { const malformed = ethers.concat([packEntries(entries), "0x00"]); - await expect(unpacker.unpackBatch(malformed)).to.be.revertedWithCustomError( - unpacker, - "InvalidPayloadLength" - ); + await expect( + unpacker.unpackBatch(malformed), + ).to.be.revertedWithCustomError(unpacker, "InvalidPayloadLength"); }); it("reverts on an empty payload", async function () { await expect(unpacker.unpackBatch("0x")).to.be.revertedWithCustomError( unpacker, - "InvalidPayloadLength" + "InvalidPayloadLength", ); }); }); @@ -89,7 +93,9 @@ describe("CalldataUnpacker", function () { const packedPayload = packEntries(entries); const abiPayload = abiEncodeEntries(entries); - const packedReceipt = await (await unpacker.unpackBatch(packedPayload)).wait(); + const packedReceipt = await ( + await unpacker.unpackBatch(packedPayload) + ).wait(); // Fresh contract so totals/state don't carry over between the two paths. const Unpacker = await ethers.getContractFactory("CalldataUnpacker"); @@ -103,8 +109,12 @@ describe("CalldataUnpacker", function () { const abiGas = Number(abiReceipt.gasUsed); const savingsPerItem = (abiGas - packedGas) / entries.length; - console.log(` packed calldata bytes: ${(packedPayload.length - 2) / 2}`); - console.log(` abi.decode calldata bytes: ${(abiPayload.length - 2) / 2}`); + console.log( + ` packed calldata bytes: ${(packedPayload.length - 2) / 2}`, + ); + console.log( + ` abi.decode calldata bytes: ${(abiPayload.length - 2) / 2}`, + ); console.log(` unpackBatch gas: ${packedGas}`); console.log(` abi.decode baseline gas: ${abiGas}`); console.log(` gas saved per item: ${savingsPerItem}`); diff --git a/test/cleanup/StorageCleaner.test.ts b/test/cleanup/StorageCleaner.test.ts index 9301bff..1612224 100644 --- a/test/cleanup/StorageCleaner.test.ts +++ b/test/cleanup/StorageCleaner.test.ts @@ -3,19 +3,19 @@ * Issue #636 */ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect } from "vitest"; -describe('StorageCleaner', () => { - describe('slot clearing', () => { - it('should zero out a storage slot', () => { +describe("StorageCleaner", () => { + describe("slot clearing", () => { + it("should zero out a storage slot", () => { const storage = new Map(); - const slot = '0x01'; + const slot = "0x01"; storage.set(slot, 42n); storage.set(slot, 0n); expect(storage.get(slot)).toBe(0n); }); - it('should clear a range of slots', () => { + it("should clear a range of slots", () => { const storage = new Map(); const startSlot = 100n; @@ -34,29 +34,29 @@ describe('StorageCleaner', () => { } }); - it('should not affect adjacent slots', () => { + it("should not affect adjacent slots", () => { const storage = new Map(); - storage.set('1', 100n); - storage.set('2', 200n); - storage.set('3', 300n); + storage.set("1", 100n); + storage.set("2", 200n); + storage.set("3", 300n); - storage.set('2', 0n); + storage.set("2", 0n); - expect(storage.get('1')).toBe(100n); - expect(storage.get('2')).toBe(0n); - expect(storage.get('3')).toBe(300n); + expect(storage.get("1")).toBe(100n); + expect(storage.get("2")).toBe(0n); + expect(storage.get("3")).toBe(300n); }); }); - describe('StorageCleanerConsumer', () => { - it('should calculate gas refund per cleared slot', () => { + describe("StorageCleanerConsumer", () => { + it("should calculate gas refund per cleared slot", () => { const EIP3529_REFUND_PER_SLOT = 4800n; const slotsCleared = 2n; const expectedRefund = EIP3529_REFUND_PER_SLOT * slotsCleared; expect(expectedRefund).toBe(9600n); }); - it('should track cleared slots', () => { + it("should track cleared slots", () => { const cleared = new Set(); cleared.add(1); cleared.add(2); @@ -65,7 +65,7 @@ describe('StorageCleaner', () => { expect(cleared.has(3)).toBe(false); }); - it('should handle batch completion', () => { + it("should handle batch completion", () => { const requests = [ { id: 1, completed: false }, { id: 2, completed: false }, diff --git a/test/core/BatchGuardProcessor.test.ts b/test/core/BatchGuardProcessor.test.ts index 0c4e1be..4560775 100644 --- a/test/core/BatchGuardProcessor.test.ts +++ b/test/core/BatchGuardProcessor.test.ts @@ -3,11 +3,11 @@ * Issue #631 */ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect } from "vitest"; -describe('BatchGuardProcessor Calldata Optimization', () => { - describe('calldata vs memory', () => { - it('calldata avoids memory allocation overhead', () => { +describe("BatchGuardProcessor Calldata Optimization", () => { + describe("calldata vs memory", () => { + it("calldata avoids memory allocation overhead", () => { // calldata: reads directly from transaction data // memory: copies calldata into memory (costs ~3 gas per byte) const dataSize = 1024; // bytes @@ -17,50 +17,52 @@ describe('BatchGuardProcessor Calldata Optimization', () => { expect(memoryCopyCost).toBeGreaterThan(calldataCost); }); - it('should process batch from calldata', () => { + it("should process batch from calldata", () => { const requests = [ - { from: '0x1111', to: '0x2222', amount: 100n }, - { from: '0x3333', to: '0x4444', amount: 200n }, + { from: "0x1111", to: "0x2222", amount: 100n }, + { from: "0x3333", to: "0x4444", amount: 200n }, ]; const results = requests.map((req, i) => ({ success: true, gasUsed: 21000 + i * 100, - reason: '', + reason: "", })); expect(results.length).toBe(2); - expect(results.every(r => r.success)).toBe(true); + expect(results.every((r) => r.success)).toBe(true); }); }); - describe('batch size limits', () => { - it('should enforce max batch size', () => { + describe("batch size limits", () => { + it("should enforce max batch size", () => { const MAX_BATCH_SIZE = 100; const batchSize = 150; expect(batchSize).toBeGreaterThan(MAX_BATCH_SIZE); }); - it('should handle empty batch', () => { + it("should handle empty batch", () => { const requests: unknown[] = []; expect(requests.length).toBe(0); }); }); - describe('validation', () => { - it('should validate addresses in calldata', () => { + describe("validation", () => { + it("should validate addresses in calldata", () => { const addresses = [ - '0x1234567890123456789012345678901234567890', - '0x0000000000000000000000000000000000000000', - '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd', + "0x1234567890123456789012345678901234567890", + "0x0000000000000000000000000000000000000000", + "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd", ]; - const valid = addresses.map(addr => addr !== '0x0000000000000000000000000000000000000000'); + const valid = addresses.map( + (addr) => addr !== "0x0000000000000000000000000000000000000000", + ); expect(valid).toEqual([true, false, true]); }); - it('should compute checksums deterministically', () => { + it("should compute checksums deterministically", () => { const amounts = [100n, 200n, 300n]; const checksums = amounts.map((amt, i) => `keccak(${amt},${i})`); diff --git a/test/core/GasGuardRouter.test.ts b/test/core/GasGuardRouter.test.ts index 299f6b6..ed26f53 100644 --- a/test/core/GasGuardRouter.test.ts +++ b/test/core/GasGuardRouter.test.ts @@ -3,54 +3,55 @@ * Issue #629 */ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect } from "vitest"; -describe('GasGuardRouter Custom Errors', () => { - describe('error selectors', () => { - it('should compute Unauthorized selector', () => { - const selector = keccak256String('Unauthorized()').slice(0, 10); +describe("GasGuardRouter Custom Errors", () => { + describe("error selectors", () => { + it("should compute Unauthorized selector", () => { + const selector = keccak256String("Unauthorized()").slice(0, 10); expect(selector).toMatch(/^0x[0-9a-f]{8}$/); }); - it('should compute InvalidAmount selector', () => { - const selector = keccak256String('InvalidAmount()').slice(0, 10); + it("should compute InvalidAmount selector", () => { + const selector = keccak256String("InvalidAmount()").slice(0, 10); expect(selector).toMatch(/^0x[0-9a-f]{8}$/); }); - it('should compute ZeroAddress selector', () => { - const selector = keccak256String('ZeroAddress()').slice(0, 10); + it("should compute ZeroAddress selector", () => { + const selector = keccak256String("ZeroAddress()").slice(0, 10); expect(selector).toMatch(/^0x[0-9a-f]{8}$/); }); - it('should have unique selectors for all errors', () => { + it("should have unique selectors for all errors", () => { const errors = [ - 'Unauthorized()', - 'InvalidAmount()', - 'ZeroAddress()', - 'AlreadyInitialized()', - 'RequestNotFound()', - 'RequestAlreadyCompleted()', - 'InsufficientBalance()', - 'OperationFailed()', + "Unauthorized()", + "InvalidAmount()", + "ZeroAddress()", + "AlreadyInitialized()", + "RequestNotFound()", + "RequestAlreadyCompleted()", + "InsufficientBalance()", + "OperationFailed()", ]; - const selectors = errors.map(e => keccak256String(e).slice(0, 10)); + const selectors = errors.map((e) => keccak256String(e).slice(0, 10)); const unique = new Set(selectors); expect(unique.size).toBe(errors.length); }); }); - describe('revert string vs custom error gas comparison', () => { - it('custom error selector is 4 bytes vs revert string which is variable length', () => { + describe("revert string vs custom error gas comparison", () => { + it("custom error selector is 4 bytes vs revert string which is variable length", () => { const customErrorSize = 4; // bytes4 selector - const revertStringSize = 'Unauthorized access'.length + 32; // ABI-encoded string + const revertStringSize = "Unauthorized access".length + 32; // ABI-encoded string expect(customErrorSize).toBeLessThan(revertStringSize); }); - it('custom error with parameters is still smaller than string', () => { + it("custom error with parameters is still smaller than string", () => { const customErrorSize = 4 + 32; // selector + one param - const revertStringSize = 'Insufficient balance: need 1000, have 500'.length + 32; + const revertStringSize = + "Insufficient balance: need 1000, have 500".length + 32; expect(customErrorSize).toBeLessThan(revertStringSize); }); @@ -63,5 +64,5 @@ function keccak256String(s: string): string { for (let i = 0; i < s.length; i++) { hash = ((hash << 5) - hash + s.charCodeAt(i)) | 0; } - return '0x' + Math.abs(hash).toString(16).padStart(64, '0'); + return "0x" + Math.abs(hash).toString(16).padStart(64, "0"); } diff --git a/test/crypto/BatchPrecompileProcessor.test.ts b/test/crypto/BatchPrecompileProcessor.test.ts index cd4b139..2aff4ee 100644 --- a/test/crypto/BatchPrecompileProcessor.test.ts +++ b/test/crypto/BatchPrecompileProcessor.test.ts @@ -3,35 +3,40 @@ * Issue #693 */ -import { describe, it, expect } from 'vitest'; -import { keccak256, toUtf8Bytes, randomBytes } from 'ethers'; +import { describe, it, expect } from "vitest"; +import { keccak256, toUtf8Bytes, randomBytes } from "ethers"; function errorSelector(errorSig: string): string { return keccak256(toUtf8Bytes(errorSig)).slice(0, 10); } -describe('BatchPrecompileProcessor', () => { - describe('error selectors', () => { - it('should compute BatchLengthMismatch selector', () => { - expect(errorSelector('BatchLengthMismatch()')).toBe('0x0e962bf5'); +describe("BatchPrecompileProcessor", () => { + describe("error selectors", () => { + it("should compute BatchLengthMismatch selector", () => { + expect(errorSelector("BatchLengthMismatch()")).toBe("0x0e962bf5"); }); - it('should compute PrecompileCallFailed selector', () => { - expect(errorSelector('PrecompileCallFailed()')).toBe('0x0dbb13f4'); + it("should compute PrecompileCallFailed selector", () => { + expect(errorSelector("PrecompileCallFailed()")).toBe("0x0dbb13f4"); }); - it('selectors should be unique', () => { - const errors = ['BatchLengthMismatch()', 'PrecompileCallFailed()']; - const selectors = errors.map(e => errorSelector(e)); + it("selectors should be unique", () => { + const errors = ["BatchLengthMismatch()", "PrecompileCallFailed()"]; + const selectors = errors.map((e) => errorSelector(e)); expect(new Set(selectors).size).toBe(errors.length); }); }); - describe('batchVerify', () => { - it('should process multiple ecrecover calls in a single batch', () => { + describe("batchVerify", () => { + it("should process multiple ecrecover calls in a single batch", () => { const count = 5; - const hashes = Array.from({ length: count }, () => keccak256(randomBytes(32))); - const v = Array.from({ length: count }, () => 27 + (Math.random() > 0.5 ? 0 : 1)); + const hashes = Array.from({ length: count }, () => + keccak256(randomBytes(32)), + ); + const v = Array.from( + { length: count }, + () => 27 + (Math.random() > 0.5 ? 0 : 1), + ); const r = Array.from({ length: count }, () => keccak256(randomBytes(32))); const s = Array.from({ length: count }, () => keccak256(randomBytes(32))); @@ -41,29 +46,38 @@ describe('BatchPrecompileProcessor', () => { } expect(results.length).toBe(count); - expect(results.every(r => r === false)).toBe(true); + expect(results.every((r) => r === false)).toBe(true); }); - it('should return empty array for empty batch', () => { + it("should return empty array for empty batch", () => { const results: boolean[] = []; expect(results.length).toBe(0); }); - it('should reject length mismatch', () => { - const lenMismatch = (a: number, b: number, c: number, d: number): boolean => - a !== b || a !== c || a !== d; + it("should reject length mismatch", () => { + const lenMismatch = ( + a: number, + b: number, + c: number, + d: number, + ): boolean => a !== b || a !== c || a !== d; expect(lenMismatch(3, 3, 3, 2)).toBe(true); expect(lenMismatch(3, 3, 3, 3)).toBe(false); }); - it('should produce deterministic output for same inputs', () => { - const hash = keccak256(toUtf8Bytes('test message')); + it("should produce deterministic output for same inputs", () => { + const hash = keccak256(toUtf8Bytes("test message")); const v = 27; - const r = '0x' + 'ab'.repeat(32); - const s = '0x' + 'cd'.repeat(32); - - const computeStatus = (h: string, vv: number, rr: string, ss: string): boolean => { + const r = "0x" + "ab".repeat(32); + const s = "0x" + "cd".repeat(32); + + const computeStatus = ( + h: string, + vv: number, + rr: string, + ss: string, + ): boolean => { const recovered = simulateEcrecover(h, vv, rr, ss); return recovered !== null; }; @@ -74,8 +88,8 @@ describe('BatchPrecompileProcessor', () => { }); }); - describe('batchModexp', () => { - it('should compute multiple modular exponentiations in a batch', () => { + describe("batchModexp", () => { + it("should compute multiple modular exponentiations in a batch", () => { const bases = [2n, 3n, 5n, 7n, 11n]; const exps = [10n, 5n, 3n, 2n, 1n]; const mods = [1000n, 1000n, 1000n, 1000n, 1000n]; @@ -99,12 +113,12 @@ describe('BatchPrecompileProcessor', () => { expect(results).toEqual([24n, 243n, 125n, 49n, 11n]); }); - it('should return empty array for empty batch', () => { + it("should return empty array for empty batch", () => { const results: bigint[] = []; expect(results.length).toBe(0); }); - it('should reject length mismatch', () => { + it("should reject length mismatch", () => { const lenMismatch = (a: number, b: number, c: number): boolean => a !== b || a !== c; @@ -112,7 +126,7 @@ describe('BatchPrecompileProcessor', () => { expect(lenMismatch(3, 3, 3)).toBe(false); }); - it('should handle large exponent values', () => { + it("should handle large exponent values", () => { const base = 123456789n; const exp = 0n; const mod = 987654321n; @@ -121,25 +135,26 @@ describe('BatchPrecompileProcessor', () => { }); }); - describe('gas benchmark — linear scaling', () => { + describe("gas benchmark — linear scaling", () => { function estimateBatchGas(count: number): number { const baseGasPerCall = 700; const precompileGas = count * 3000; - const memoryGas = Math.ceil(count * 32 / 32) * 3; + const memoryGas = Math.ceil((count * 32) / 32) * 3; return baseGasPerCall + precompileGas + memoryGas; } - it('gas grows linearly with batch size', () => { + it("gas grows linearly with batch size", () => { const gas1 = estimateBatchGas(1); const gas10 = estimateBatchGas(10); - const ratio = (gas10 - estimateBatchGas(0)) / (gas1 - estimateBatchGas(0)); + const ratio = + (gas10 - estimateBatchGas(0)) / (gas1 - estimateBatchGas(0)); expect(ratio).toBeCloseTo(10, 0); }); - it('per-element gas cost is constant', () => { + it("per-element gas cost is constant", () => { const sizes = [1, 2, 5, 10, 20]; - const perElement = sizes.map(s => estimateBatchGas(s) / s); - const constant = perElement.every(g => g === perElement[0]); + const perElement = sizes.map((s) => estimateBatchGas(s) / s); + const constant = perElement.every((g) => g === perElement[0]); expect(constant).toBe(true); }); }); @@ -148,11 +163,30 @@ describe('BatchPrecompileProcessor', () => { // --------------------------------------------------------------------------- // Deterministic mock helpers (since we can't call the actual precompile in TS) // --------------------------------------------------------------------------- -function simulateEcrecover(hash: string, v: number, r: string, s: string): string | null { +function simulateEcrecover( + hash: string, + v: number, + r: string, + s: string, +): string | null { const rBytes = BigInt(r); const sBytes = BigInt(s); - if (rBytes === 0n || rBytes >= BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141')) return null; - if (sBytes === 0n || sBytes >= BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141')) return null; + if ( + rBytes === 0n || + rBytes >= + BigInt( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", + ) + ) + return null; + if ( + sBytes === 0n || + sBytes >= + BigInt( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", + ) + ) + return null; if (v < 27 || v > 28) return null; - return '0x0000000000000000000000000000000000000001'; + return "0x0000000000000000000000000000000000000001"; } diff --git a/test/crypto/ScratchHasher.test.ts b/test/crypto/ScratchHasher.test.ts index 481899a..af099fb 100644 --- a/test/crypto/ScratchHasher.test.ts +++ b/test/crypto/ScratchHasher.test.ts @@ -1,21 +1,21 @@ describe("ScratchHasher", () => { - it("matches abi.encodePacked hash", async () => { - // Compare hash() vs hashSolidity() - }); + it("matches abi.encodePacked hash", async () => { + // Compare hash() vs hashSolidity() + }); - it("returns identical hashes for random inputs", async () => { - // Multiple random bytes32 pairs - }); + it("returns identical hashes for random inputs", async () => { + // Multiple random bytes32 pairs + }); - it("handles zero values", async () => { - // bytes32(0), bytes32(0) - }); + it("handles zero values", async () => { + // bytes32(0), bytes32(0) + }); - it("handles max values", async () => { - // 0xffff...ffff - }); + it("handles max values", async () => { + // 0xffff...ffff + }); - it("benchmarks gas usage", async () => { - // Compare hash() against hashSolidity() - }); -}); \ No newline at end of file + it("benchmarks gas usage", async () => { + // Compare hash() against hashSolidity() + }); +}); diff --git a/test/crypto/UnrolledSignatureVerifier.test.ts b/test/crypto/UnrolledSignatureVerifier.test.ts index cbf1d8a..cb54511 100644 --- a/test/crypto/UnrolledSignatureVerifier.test.ts +++ b/test/crypto/UnrolledSignatureVerifier.test.ts @@ -9,14 +9,14 @@ * • Gas benchmark via the ecrecover precompile cost model. */ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect } from "vitest"; import { keccak256, toUtf8Bytes, Wallet, Signature, recoverAddress, -} from 'ethers'; +} from "ethers"; const SIG_LEN = 65; @@ -27,21 +27,25 @@ function selector(errorSig: string): string { return keccak256(toUtf8Bytes(errorSig)).slice(0, 10); } -describe('UnrolledSignatureVerifier — error selectors', () => { - it('InvalidSignatureCount() selector', () => { - expect(selector('InvalidSignatureCount()')).toBe('0x8b97390c'); +describe("UnrolledSignatureVerifier — error selectors", () => { + it("InvalidSignatureCount() selector", () => { + expect(selector("InvalidSignatureCount()")).toBe("0x8b97390c"); }); - it('DuplicateSigner() selector', () => { - expect(selector('DuplicateSigner()')).toBe('0x8044bb33'); + it("DuplicateSigner() selector", () => { + expect(selector("DuplicateSigner()")).toBe("0x8044bb33"); }); - it('ThresholdNotMet() selector', () => { - expect(selector('ThresholdNotMet()')).toBe('0x59fa4a93'); + it("ThresholdNotMet() selector", () => { + expect(selector("ThresholdNotMet()")).toBe("0x59fa4a93"); }); - it('selectors are unique', () => { - const errs = ['InvalidSignatureCount()', 'DuplicateSigner()', 'ThresholdNotMet()']; + it("selectors are unique", () => { + const errs = [ + "InvalidSignatureCount()", + "DuplicateSigner()", + "ThresholdNotMet()", + ]; const sigs = errs.map(selector); expect(new Set(sigs).size).toBe(errs.length); }); @@ -56,7 +60,7 @@ function signHash(wallets: Wallet[], hash: string): Signature[] { } function serializeSigs(sigs: Signature[]): string { - return '0x' + sigs.map((s) => s.serialized!.slice(2)).join(''); + return "0x" + sigs.map((s) => s.serialized!.slice(2)).join(""); } function rawRecover(hash: string, sig: Signature): string { @@ -71,31 +75,35 @@ function mockVerifyGeneric( threshold: number, ): { ok: boolean; reason?: string } { const n = sigs.length; - if (n !== validators.length) return { ok: false, reason: 'InvalidSignatureCount' }; - if (threshold > n) return { ok: false, reason: 'InvalidSignatureCount' }; + if (n !== validators.length) + return { ok: false, reason: "InvalidSignatureCount" }; + if (threshold > n) return { ok: false, reason: "InvalidSignatureCount" }; for (let i = 0; i < n; i++) { for (let j = i + 1; j < n; j++) { - if (validators[i] === validators[j]) return { ok: false, reason: 'DuplicateSigner' }; + if (validators[i] === validators[j]) + return { ok: false, reason: "DuplicateSigner" }; } } const recovered: string[] = []; for (let i = 0; i < n; i++) { const sig = sigs[i]; - if (sig.s > MALLEABILITY_THRESHOLD) return { ok: false, reason: 'InvalidSignatureCount' }; + if (sig.s > MALLEABILITY_THRESHOLD) + return { ok: false, reason: "InvalidSignatureCount" }; let addr: string; try { addr = rawRecover(hash, sig); } catch { - return { ok: false, reason: 'InvalidSignatureCount' }; + return { ok: false, reason: "InvalidSignatureCount" }; } - if (addr === '0x' + '00'.repeat(20)) { - return { ok: false, reason: 'InvalidSignatureCount' }; + if (addr === "0x" + "00".repeat(20)) { + return { ok: false, reason: "InvalidSignatureCount" }; } - if (recovered.includes(addr)) return { ok: false, reason: 'DuplicateSigner' }; + if (recovered.includes(addr)) + return { ok: false, reason: "DuplicateSigner" }; recovered.push(addr); } @@ -104,165 +112,255 @@ function mockVerifyGeneric( if (validators.includes(signer)) count++; } - if (count < threshold) return { ok: false, reason: 'ThresholdNotMet' }; + if (count < threshold) return { ok: false, reason: "ThresholdNotMet" }; return { ok: true }; } -describe('UnrolledSignatureVerifier — logic', () => { - const hash = keccak256(toUtf8Bytes('GasGuard #696 — Unrolled Signature Verifier')); +describe("UnrolledSignatureVerifier — logic", () => { + const hash = keccak256( + toUtf8Bytes("GasGuard #696 — Unrolled Signature Verifier"), + ); - describe('verify3of5', () => { - it('accepts all 5 valid signers (≥3)', () => { + describe("verify3of5", () => { + it("accepts all 5 valid signers (≥3)", () => { const wallets = createWallets(5); const sigs = signHash(wallets, hash); - expect(mockVerifyGeneric(hash, sigs, wallets.map((w) => w.address), 3).ok).toBe(true); + expect( + mockVerifyGeneric( + hash, + sigs, + wallets.map((w) => w.address), + 3, + ).ok, + ).toBe(true); }); - it('accepts exactly 3 valid signers', () => { + it("accepts exactly 3 valid signers", () => { const wallets = createWallets(5); const intruders = createWallets(2); const sigs = signHash([...wallets.slice(0, 3), ...intruders], hash); - expect(mockVerifyGeneric(hash, sigs, wallets.map((w) => w.address), 3).ok).toBe(true); + expect( + mockVerifyGeneric( + hash, + sigs, + wallets.map((w) => w.address), + 3, + ).ok, + ).toBe(true); }); - it('rejects 2 valid signers', () => { + it("rejects 2 valid signers", () => { const wallets = createWallets(5); const intruders = createWallets(3); const sigs = signHash([...wallets.slice(0, 2), ...intruders], hash); - const r = mockVerifyGeneric(hash, sigs, wallets.map((w) => w.address), 3); + const r = mockVerifyGeneric( + hash, + sigs, + wallets.map((w) => w.address), + 3, + ); expect(r.ok).toBe(false); - expect(r.reason).toBe('ThresholdNotMet'); + expect(r.reason).toBe("ThresholdNotMet"); }); - it('rejects duplicate signers', () => { + it("rejects duplicate signers", () => { const validators = createWallets(5); const sigs = signHash(validators, hash); const duplicated = [sigs[0], sigs[0], sigs[1], sigs[2], sigs[3]]; - const r = mockVerifyGeneric(hash, duplicated, validators.map((w) => w.address), 3); + const r = mockVerifyGeneric( + hash, + duplicated, + validators.map((w) => w.address), + 3, + ); expect(r.ok).toBe(false); - expect(r.reason).toBe('DuplicateSigner'); + expect(r.reason).toBe("DuplicateSigner"); }); - it('rejects duplicate validators', () => { + it("rejects duplicate validators", () => { const wallets = createWallets(5); const sigs = signHash(wallets, hash); const addrs = wallets.map((w) => w.address); addrs[4] = addrs[0]; const r = mockVerifyGeneric(hash, sigs, addrs, 3); expect(r.ok).toBe(false); - expect(r.reason).toBe('DuplicateSigner'); + expect(r.reason).toBe("DuplicateSigner"); }); - it('rejects wrong signature count', () => { + it("rejects wrong signature count", () => { const validators = createWallets(5); const sigs = signHash(validators.slice(0, 4), hash); - const r = mockVerifyGeneric(hash, sigs, validators.map((w) => w.address), 3); + const r = mockVerifyGeneric( + hash, + sigs, + validators.map((w) => w.address), + 3, + ); expect(r.ok).toBe(false); - expect(r.reason).toBe('InvalidSignatureCount'); + expect(r.reason).toBe("InvalidSignatureCount"); }); }); - describe('verify5of7', () => { - it('accepts all 7 valid signers (≥5)', () => { + describe("verify5of7", () => { + it("accepts all 7 valid signers (≥5)", () => { const wallets = createWallets(7); const sigs = signHash(wallets, hash); - expect(mockVerifyGeneric(hash, sigs, wallets.map((w) => w.address), 5).ok).toBe(true); + expect( + mockVerifyGeneric( + hash, + sigs, + wallets.map((w) => w.address), + 5, + ).ok, + ).toBe(true); }); - it('rejects 4 valid signers', () => { + it("rejects 4 valid signers", () => { const wallets = createWallets(7); const intruders = createWallets(3); const sigs = signHash([...wallets.slice(0, 4), ...intruders], hash); - const r = mockVerifyGeneric(hash, sigs, wallets.map((w) => w.address), 5); + const r = mockVerifyGeneric( + hash, + sigs, + wallets.map((w) => w.address), + 5, + ); expect(r.ok).toBe(false); - expect(r.reason).toBe('ThresholdNotMet'); + expect(r.reason).toBe("ThresholdNotMet"); }); - it('rejects duplicate signers', () => { + it("rejects duplicate signers", () => { const validators = createWallets(7); const sigs = signHash(validators, hash); - const duplicated = [sigs[0], sigs[0], sigs[1], sigs[2], sigs[3], sigs[4], sigs[5]]; - const r = mockVerifyGeneric(hash, duplicated, validators.map((w) => w.address), 5); + const duplicated = [ + sigs[0], + sigs[0], + sigs[1], + sigs[2], + sigs[3], + sigs[4], + sigs[5], + ]; + const r = mockVerifyGeneric( + hash, + duplicated, + validators.map((w) => w.address), + 5, + ); expect(r.ok).toBe(false); - expect(r.reason).toBe('DuplicateSigner'); + expect(r.reason).toBe("DuplicateSigner"); }); - it('rejects duplicate validators', () => { + it("rejects duplicate validators", () => { const wallets = createWallets(7); const sigs = signHash(wallets, hash); const addrs = wallets.map((w) => w.address); addrs[6] = addrs[0]; const r = mockVerifyGeneric(hash, sigs, addrs, 5); expect(r.ok).toBe(false); - expect(r.reason).toBe('DuplicateSigner'); + expect(r.reason).toBe("DuplicateSigner"); }); }); - describe('verifyThreshold (loop version)', () => { - it('handles 3-of-5', () => { + describe("verifyThreshold (loop version)", () => { + it("handles 3-of-5", () => { const wallets = createWallets(5); const sigs = signHash(wallets, hash); - expect(mockVerifyGeneric(hash, sigs, wallets.map((w) => w.address), 3).ok).toBe(true); + expect( + mockVerifyGeneric( + hash, + sigs, + wallets.map((w) => w.address), + 3, + ).ok, + ).toBe(true); }); - it('handles 5-of-7', () => { + it("handles 5-of-7", () => { const wallets = createWallets(7); const sigs = signHash(wallets, hash); - expect(mockVerifyGeneric(hash, sigs, wallets.map((w) => w.address), 5).ok).toBe(true); + expect( + mockVerifyGeneric( + hash, + sigs, + wallets.map((w) => w.address), + 5, + ).ok, + ).toBe(true); }); - it('handles n-of-n boundary', () => { + it("handles n-of-n boundary", () => { const wallets = createWallets(3); const sigs = signHash(wallets, hash); - expect(mockVerifyGeneric(hash, sigs, wallets.map((w) => w.address), 3).ok).toBe(true); + expect( + mockVerifyGeneric( + hash, + sigs, + wallets.map((w) => w.address), + 3, + ).ok, + ).toBe(true); }); - it('rejects threshold > count', () => { + it("rejects threshold > count", () => { const wallets = createWallets(3); const sigs = signHash(wallets, hash); - const r = mockVerifyGeneric(hash, sigs, wallets.map((w) => w.address), 5); + const r = mockVerifyGeneric( + hash, + sigs, + wallets.map((w) => w.address), + 5, + ); expect(r.ok).toBe(false); - expect(r.reason).toBe('InvalidSignatureCount'); + expect(r.reason).toBe("InvalidSignatureCount"); }); }); - describe('gas benchmark — simulated', () => { + describe("gas benchmark — simulated", () => { const ECRECOVER_COST = 3000; const CALL_OVERHEAD = 700; - function estimateUnrolled(ecrecoverCount: number, comparisons: number): number { + function estimateUnrolled( + ecrecoverCount: number, + comparisons: number, + ): number { return CALL_OVERHEAD + ecrecoverCount * ECRECOVER_COST + comparisons * 3; } function estimateLoop(ecrecoverCount: number, comparisons: number): number { const LOOP_OVERHEAD = 200; const PER_ITERATION = 30; - return CALL_OVERHEAD + ecrecoverCount * ECRECOVER_COST + comparisons * 3 - + LOOP_OVERHEAD + ecrecoverCount * PER_ITERATION; + return ( + CALL_OVERHEAD + + ecrecoverCount * ECRECOVER_COST + + comparisons * 3 + + LOOP_OVERHEAD + + ecrecoverCount * PER_ITERATION + ); } - it('3-of-5: unrolled cheaper than loop', () => { + it("3-of-5: unrolled cheaper than loop", () => { expect(estimateUnrolled(5, 25)).toBeLessThan(estimateLoop(5, 25)); }); - it('5-of-7: unrolled cheaper than loop', () => { + it("5-of-7: unrolled cheaper than loop", () => { expect(estimateUnrolled(7, 49)).toBeLessThan(estimateLoop(7, 49)); }); - it('reports 3-of-5 estimates', () => { + it("reports 3-of-5 estimates", () => { const u = estimateUnrolled(5, 25); const l = estimateLoop(5, 25); - console.log(' 3-of-5 | unrolled |', u); - console.log(' 3-of-5 | loop |', l); - console.log(' 3-of-5 | savings |', l - u); + console.log(" 3-of-5 | unrolled |", u); + console.log(" 3-of-5 | loop |", l); + console.log(" 3-of-5 | savings |", l - u); }); - it('reports 5-of-7 estimates', () => { + it("reports 5-of-7 estimates", () => { const u = estimateUnrolled(7, 49); const l = estimateLoop(7, 49); - console.log(' 5-of-7 | unrolled |', u); - console.log(' 5-of-7 | loop |', l); - console.log(' 5-of-7 | savings |', l - u); + console.log(" 5-of-7 | unrolled |", u); + console.log(" 5-of-7 | loop |", l); + console.log(" 5-of-7 | savings |", l - u); }); }); }); diff --git a/test/dispatcher/HighVolumeDispatcher.test.ts b/test/dispatcher/HighVolumeDispatcher.test.ts index b898c75..6c2239f 100644 --- a/test/dispatcher/HighVolumeDispatcher.test.ts +++ b/test/dispatcher/HighVolumeDispatcher.test.ts @@ -3,8 +3,8 @@ * Issue #633 */ -import { describe, it, expect } from 'vitest'; -import { keccak256, toUtf8Bytes } from 'ethers'; +import { describe, it, expect } from "vitest"; +import { keccak256, toUtf8Bytes } from "ethers"; function getSelector(signature: string): string { return keccak256(toUtf8Bytes(signature)).slice(0, 10); @@ -14,39 +14,39 @@ function selectorNumeric(selector: string): number { return parseInt(selector.slice(2), 16); } -describe('HighVolumeDispatcher Selector Ordering', () => { - describe('selector computation', () => { - it('should compute deposit() selector', () => { - const selector = getSelector('deposit()'); - expect(selector).toBe('0xd0e30db0'); +describe("HighVolumeDispatcher Selector Ordering", () => { + describe("selector computation", () => { + it("should compute deposit() selector", () => { + const selector = getSelector("deposit()"); + expect(selector).toBe("0xd0e30db0"); }); - it('should compute selectors for all functions', () => { + it("should compute selectors for all functions", () => { const selectors = { - deposit: getSelector('deposit()'), - process: getSelector('process(address,uint256)'), - getStatus: getSelector('getStatus(bytes32)'), - batchProcess: getSelector('batchProcess(address[],uint256[])'), + deposit: getSelector("deposit()"), + process: getSelector("process(address,uint256)"), + getStatus: getSelector("getStatus(bytes32)"), + batchProcess: getSelector("batchProcess(address[],uint256[])"), }; - Object.values(selectors).forEach(s => { + Object.values(selectors).forEach((s) => { expect(s).toMatch(/^0x[0-9a-f]{8}$/); }); }); }); - describe('dispatch order optimization', () => { - it('should rank high-frequency functions first numerically', () => { + describe("dispatch order optimization", () => { + it("should rank high-frequency functions first numerically", () => { const selectors = { - deposit: getSelector('deposit()'), - process: getSelector('process(address,uint256)'), - getStatus: getSelector('getStatus(bytes32)'), - batchProcess: getSelector('batchProcess(address[],uint256[])'), + deposit: getSelector("deposit()"), + process: getSelector("process(address,uint256)"), + getStatus: getSelector("getStatus(bytes32)"), + batchProcess: getSelector("batchProcess(address[],uint256[])"), }; // Sort by numeric value (lower = checked first in dispatch tree) const sorted = Object.entries(selectors).sort( - ([, a], [, b]) => selectorNumeric(a) - selectorNumeric(b) + ([, a], [, b]) => selectorNumeric(a) - selectorNumeric(b), ); // deposit() should ideally be first (highest frequency) @@ -55,31 +55,31 @@ describe('HighVolumeDispatcher Selector Ordering', () => { expect(sorted.length).toBe(4); }); - it('should have unique selectors', () => { + it("should have unique selectors", () => { const selectors = [ - getSelector('deposit()'), - getSelector('process(address,uint256)'), - getSelector('getStatus(bytes32)'), - getSelector('batchProcess(address[],uint256[])'), + getSelector("deposit()"), + getSelector("process(address,uint256)"), + getSelector("getStatus(bytes32)"), + getSelector("batchProcess(address[],uint256[])"), ]; const unique = new Set(selectors); expect(unique.size).toBe(selectors.length); }); - it('selector comparison should be consistent', () => { - const s1 = getSelector('deposit()'); - const s2 = getSelector('process(address,uint256)'); + it("selector comparison should be consistent", () => { + const s1 = getSelector("deposit()"); + const s2 = getSelector("process(address,uint256)"); // Same comparison always yields same result expect(selectorNumeric(s1) < selectorNumeric(s2)).toBe( - selectorNumeric(s1) < selectorNumeric(s2) + selectorNumeric(s1) < selectorNumeric(s2), ); }); }); - describe('gas savings from ordering', () => { - it('should save gas for earlier matches', () => { + describe("gas savings from ordering", () => { + it("should save gas for earlier matches", () => { // Each comparison costs ~3 gas (EQ + JUMPI) // If deposit is 1st vs 4th, saves 3 comparisons = ~9 gas const gasPerComparison = 3; diff --git a/test/events/EventRegistry.test.ts b/test/events/EventRegistry.test.ts index 9260613..a2355ac 100644 --- a/test/events/EventRegistry.test.ts +++ b/test/events/EventRegistry.test.ts @@ -13,16 +13,16 @@ describe("EventRegistry", () => { it("should inline constant event topic selectors matching their canonical ABI signatures", async () => { expect(await registry.EVENT_TRANSFER_TOPIC()).to.equal( - ethers.id("Transfer(address,address,uint256)") + ethers.id("Transfer(address,address,uint256)"), ); expect(await registry.EVENT_APPROVAL_TOPIC()).to.equal( - ethers.id("Approval(address,address,uint256)") + ethers.id("Approval(address,address,uint256)"), ); expect(await registry.EVENT_APPROVAL_FOR_ALL_TOPIC()).to.equal( - ethers.id("ApprovalForAll(address,address,bool)") + ethers.id("ApprovalForAll(address,address,bool)"), ); expect(await registry.EVENT_OWNERSHIP_TRANSFERRED_TOPIC()).to.equal( - ethers.id("OwnershipTransferred(address,address)") + ethers.id("OwnershipTransferred(address,address)"), ); }); @@ -39,16 +39,24 @@ describe("EventRegistry", () => { const [owner, spender] = await ethers.getSigners(); const approvalTopic = await registry.EVENT_APPROVAL_TOPIC(); - const tx = await registry.emitViaAssembly(owner.address, spender.address, 42n); + const tx = await registry.emitViaAssembly( + owner.address, + spender.address, + 42n, + ); const receipt = await tx.wait(); const log = receipt!.logs.find( - (l: { address: string }) => l.address === (await registry.getAddress()) + (l: { address: string }) => l.address === (await registry.getAddress()), ); expect(log).to.not.be.undefined; expect(log!.topics[0]).to.equal(approvalTopic); - expect(ethers.getAddress("0x" + log!.topics[1].slice(26))).to.equal(owner.address); - expect(ethers.getAddress("0x" + log!.topics[2].slice(26))).to.equal(spender.address); + expect(ethers.getAddress("0x" + log!.topics[1].slice(26))).to.equal( + owner.address, + ); + expect(ethers.getAddress("0x" + log!.topics[2].slice(26))).to.equal( + spender.address, + ); expect(BigInt(log!.data)).to.equal(42n); }); }); diff --git a/test/math/FastMath.test.ts b/test/math/FastMath.test.ts index 39c7fa6..cd44627 100644 --- a/test/math/FastMath.test.ts +++ b/test/math/FastMath.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect } from "vitest"; function mul2(x: bigint): bigint { return x << 1n; @@ -12,9 +12,9 @@ function mod8(x: bigint): bigint { return x & 7n; } -describe('FastMath', () => { - describe('mul2', () => { - it('should multiply by 2 using bitwise shift', () => { +describe("FastMath", () => { + describe("mul2", () => { + it("should multiply by 2 using bitwise shift", () => { expect(mul2(0n)).toBe(0n); expect(mul2(1n)).toBe(2n); expect(mul2(21n)).toBe(42n); @@ -22,8 +22,8 @@ describe('FastMath', () => { }); }); - describe('div4', () => { - it('should divide by 4 using bitwise shift', () => { + describe("div4", () => { + it("should divide by 4 using bitwise shift", () => { expect(div4(0n)).toBe(0n); expect(div4(4n)).toBe(1n); expect(div4(17n)).toBe(4n); @@ -31,8 +31,8 @@ describe('FastMath', () => { }); }); - describe('mod8', () => { - it('should compute modulo 8 using bitwise AND', () => { + describe("mod8", () => { + it("should compute modulo 8 using bitwise AND", () => { expect(mod8(0n)).toBe(0n); expect(mod8(7n)).toBe(7n); expect(mod8(8n)).toBe(0n); diff --git a/test/math/YulMathLib.test.ts b/test/math/YulMathLib.test.ts index 5fec230..4eb619b 100644 --- a/test/math/YulMathLib.test.ts +++ b/test/math/YulMathLib.test.ts @@ -3,19 +3,19 @@ * Issue #637 */ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect } from "vitest"; const WAD = 10n ** 18n; const RAY = 10n ** 27n; const HALF_WAD = 5n * 10n ** 17n; function mulDivDown(x: bigint, y: bigint, denominator: bigint): bigint { - if (denominator === 0n) throw new Error('DivisionByZero'); + if (denominator === 0n) throw new Error("DivisionByZero"); return (x * y) / denominator; } function mulDivUp(x: bigint, y: bigint, denominator: bigint): bigint { - if (denominator === 0n) throw new Error('DivisionByZero'); + if (denominator === 0n) throw new Error("DivisionByZero"); const result = (x * y) / denominator; return (x * y) % denominator > 0n ? result + 1n : result; } @@ -25,7 +25,7 @@ function wadMul(x: bigint, y: bigint): bigint { } function wadDiv(x: bigint, y: bigint): bigint { - if (y === 0n) throw new Error('DivisionByZero'); + if (y === 0n) throw new Error("DivisionByZero"); return (x * WAD) / y; } @@ -34,29 +34,29 @@ function rayMul(x: bigint, y: bigint): bigint { } function rayDiv(x: bigint, y: bigint): bigint { - if (y === 0n) throw new Error('DivisionByZero'); + if (y === 0n) throw new Error("DivisionByZero"); return (x * RAY) / y; } -describe('YulMathLib', () => { - describe('mulDivDown', () => { - it('should compute (x * y) / denominator correctly', () => { +describe("YulMathLib", () => { + describe("mulDivDown", () => { + it("should compute (x * y) / denominator correctly", () => { expect(mulDivDown(100n, 200n, 10n)).toBe(2000n); }); - it('should handle zero numerator', () => { + it("should handle zero numerator", () => { expect(mulDivDown(0n, 200n, 10n)).toBe(0n); }); - it('should handle zero denominator', () => { - expect(() => mulDivDown(100n, 200n, 0n)).toThrow('DivisionByZero'); + it("should handle zero denominator", () => { + expect(() => mulDivDown(100n, 200n, 0n)).toThrow("DivisionByZero"); }); - it('should truncate toward zero', () => { + it("should truncate toward zero", () => { expect(mulDivDown(10n, 3n, 7n)).toBe(4n); }); - it('should handle large numbers', () => { + it("should handle large numbers", () => { const x = 10n ** 36n; const y = 10n ** 36n; const d = 10n ** 18n; @@ -64,78 +64,78 @@ describe('YulMathLib', () => { }); }); - describe('mulDivUp', () => { - it('should round up when there is a remainder', () => { + describe("mulDivUp", () => { + it("should round up when there is a remainder", () => { expect(mulDivUp(10n, 3n, 7n)).toBe(5n); }); - it('should not round up when exact', () => { + it("should not round up when exact", () => { expect(mulDivUp(10n, 2n, 5n)).toBe(4n); }); - it('should handle zero denominator', () => { - expect(() => mulDivUp(100n, 200n, 0n)).toThrow('DivisionByZero'); + it("should handle zero denominator", () => { + expect(() => mulDivUp(100n, 200n, 0n)).toThrow("DivisionByZero"); }); }); - describe('wadMul', () => { - it('should multiply WAD-scaled numbers', () => { + describe("wadMul", () => { + it("should multiply WAD-scaled numbers", () => { const a = 2n * WAD; const b = 3n * WAD; expect(wadMul(a, b)).toBe(6n * WAD); }); - it('should handle 1.5 * 2 = 3', () => { + it("should handle 1.5 * 2 = 3", () => { const a = (3n * WAD) / 2n; const b = 2n * WAD; expect(wadMul(a, b)).toBe(3n * WAD); }); - it('should handle small fractions', () => { + it("should handle small fractions", () => { const a = WAD / 10n; // 0.1 const b = WAD / 10n; // 0.1 expect(wadMul(a, b)).toBe(WAD / 100n); // 0.01 }); }); - describe('wadDiv', () => { - it('should divide WAD-scaled numbers', () => { + describe("wadDiv", () => { + it("should divide WAD-scaled numbers", () => { const a = 6n * WAD; const b = 2n * WAD; expect(wadDiv(a, b)).toBe(3n * WAD); }); - it('should handle division by zero', () => { - expect(() => wadDiv(WAD, 0n)).toThrow('DivisionByZero'); + it("should handle division by zero", () => { + expect(() => wadDiv(WAD, 0n)).toThrow("DivisionByZero"); }); - it('should handle 1 / 2 = 0.5', () => { + it("should handle 1 / 2 = 0.5", () => { expect(wadDiv(WAD, 2n * WAD)).toBe(WAD / 2n); }); }); - describe('rayMul', () => { - it('should multiply RAY-scaled numbers', () => { + describe("rayMul", () => { + it("should multiply RAY-scaled numbers", () => { const a = 2n * RAY; const b = 3n * RAY; expect(rayMul(a, b)).toBe(6n * RAY); }); }); - describe('rayDiv', () => { - it('should divide RAY-scaled numbers', () => { + describe("rayDiv", () => { + it("should divide RAY-scaled numbers", () => { const a = 6n * RAY; const b = 2n * RAY; expect(rayDiv(a, b)).toBe(3n * RAY); }); - it('should handle division by zero', () => { - expect(() => rayDiv(RAY, 0n)).toThrow('DivisionByZero'); + it("should handle division by zero", () => { + expect(() => rayDiv(RAY, 0n)).toThrow("DivisionByZero"); }); }); - describe('precision matching', () => { - it('should match reference library for common operations', () => { + describe("precision matching", () => { + it("should match reference library for common operations", () => { const price = 1500n * WAD; const amount = 10n * WAD; @@ -143,9 +143,9 @@ describe('YulMathLib', () => { expect(total).toBe(15000n * WAD); }); - it('should handle compound interest calculation', () => { + it("should handle compound interest calculation", () => { let amount = 1000n * WAD; - const rate = 5n * WAD / 100n; // 5% + const rate = (5n * WAD) / 100n; // 5% // 3 periods of 5% compound interest amount = wadMul(amount, WAD + rate); diff --git a/test/proxy/YulProxyForwarder.test.ts b/test/proxy/YulProxyForwarder.test.ts index 05682c7..e8ef0e8 100644 --- a/test/proxy/YulProxyForwarder.test.ts +++ b/test/proxy/YulProxyForwarder.test.ts @@ -3,56 +3,62 @@ * Issue #691 */ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect } from "vitest"; -describe('YulProxyForwarder', () => { - describe('forwarding semantics', () => { - it('forwards calldata unmodified via delegatecall', () => { - const incomingCalldata = '0xa9059cbb000000000000000000000000abc'; +describe("YulProxyForwarder", () => { + describe("forwarding semantics", () => { + it("forwards calldata unmodified via delegatecall", () => { + const incomingCalldata = "0xa9059cbb000000000000000000000000abc"; // calldatacopy(0, 0, calldatasize()) copies the exact bytes received; // no ABI re-encoding occurs, so the forwarded payload must be identical. const forwardedCalldata = incomingCalldata; expect(forwardedCalldata).toBe(incomingCalldata); }); - it('preserves msg.sender via delegatecall (not a plain call)', () => { + it("preserves msg.sender via delegatecall (not a plain call)", () => { // delegatecall runs the callee's code in the caller's context, so // msg.sender/msg.value observed by `implementation` are the proxy's // original caller, not the proxy contract itself. - const callType = 'delegatecall'; - expect(callType).toBe('delegatecall'); + const callType = "delegatecall"; + expect(callType).toBe("delegatecall"); }); - it('relays successful return data unchanged', () => { - const implementationReturnData = '0x0000000000000000000000000000000000000000000000000000000000000001'; + it("relays successful return data unchanged", () => { + const implementationReturnData = + "0x0000000000000000000000000000000000000000000000000000000000000001"; const forwarderReturnData = implementationReturnData; // returndatacopy + return(0, returndatasize()) expect(forwarderReturnData).toBe(implementationReturnData); }); - it('relays revert reasons unchanged on failure', () => { - const revertReason = '0x08c379a0'; // Error(string) selector + it("relays revert reasons unchanged on failure", () => { + const revertReason = "0x08c379a0"; // Error(string) selector const forwardedRevert = revertReason; // returndatacopy + revert(0, returndatasize()) expect(forwardedRevert).toBe(revertReason); }); - it('accepts plain ETH transfers via receive()', () => { + it("accepts plain ETH transfers via receive()", () => { const hasReceive = true; const hasFallback = true; expect(hasReceive && hasFallback).toBe(true); }); }); - describe('gas overhead', () => { - it('performs only calldatacopy + delegatecall + returndatacopy + return/revert', () => { + describe("gas overhead", () => { + it("performs only calldatacopy + delegatecall + returndatacopy + return/revert", () => { // Fixed set of opcodes regardless of payload size or selector; no // dynamic dispatch table, no ABI decode/encode overhead. - const opcodes = ['CALLDATACOPY', 'DELEGATECALL', 'RETURNDATACOPY', 'RETURN_OR_REVERT']; + const opcodes = [ + "CALLDATACOPY", + "DELEGATECALL", + "RETURNDATACOPY", + "RETURN_OR_REVERT", + ]; expect(opcodes.length).toBe(4); }); - it('forwards all remaining gas to the implementation', () => { - const gasForwarded = 'gas()'; // no gas stipend truncation - expect(gasForwarded).toBe('gas()'); + it("forwards all remaining gas to the implementation", () => { + const gasForwarded = "gas()"; // no gas stipend truncation + expect(gasForwarded).toBe("gas()"); }); }); }); diff --git a/test/router/DirectIndexRouter.test.ts b/test/router/DirectIndexRouter.test.ts index f1719ca..1fed278 100644 --- a/test/router/DirectIndexRouter.test.ts +++ b/test/router/DirectIndexRouter.test.ts @@ -36,10 +36,18 @@ describe("DirectIndexRouter", function () { }); describe("index-based fallback dispatch", function () { - async function callRoute(index: number, userAddr: string, amount: bigint = 0n): Promise { + async function callRoute( + index: number, + userAddr: string, + amount: bigint = 0n, + ): Promise { const userPadded = ethers.zeroPadValue(userAddr, 32); const amountPadded = ethers.zeroPadValue(ethers.toBeHex(amount), 32); - const calldata = ethers.concat([ethers.zeroPadValue(ethers.toBeHex(index), 1), userPadded, amountPadded]); + const calldata = ethers.concat([ + ethers.zeroPadValue(ethers.toBeHex(index), 1), + userPadded, + amountPadded, + ]); return router.route({ data: calldata }); } @@ -61,16 +69,17 @@ describe("DirectIndexRouter", function () { }); it("should revert on unknown index", async function () { - await expect( - callRoute(0xFF, user.address) - ).to.be.revertedWithCustomError(router, "InvalidIndex"); + await expect(callRoute(0xff, user.address)).to.be.revertedWithCustomError( + router, + "InvalidIndex", + ); }); }); describe("gas savings vs selector dispatch", function () { it("should use fewer comparisons than 4-byte selector dispatch", function () { const selectorComparisons = 3; // 4 functions = up to 3 eq/jumpi checks - const indexComparisons = 1; // single byte comparison per candidate + const indexComparisons = 1; // single byte comparison per candidate const gasPerComparison = 3; const selectorGas = selectorComparisons * gasPerComparison; diff --git a/test/security/AdaptiveReentrancyGuard.test.ts b/test/security/AdaptiveReentrancyGuard.test.ts index 85478f9..7dd1f89 100644 --- a/test/security/AdaptiveReentrancyGuard.test.ts +++ b/test/security/AdaptiveReentrancyGuard.test.ts @@ -8,7 +8,7 @@ describe("AdaptiveReentrancyGuard", () => { before(async () => { const factory: ContractFactory = await ethers.getContractFactory( - "AdaptiveReentrancyGuardMock" + "AdaptiveReentrancyGuardMock", ); mock = await factory.deploy(true); await mock.waitForDeployment(); @@ -33,7 +33,7 @@ describe("AdaptiveReentrancyGuard", () => { before(async () => { const factory: ContractFactory = await ethers.getContractFactory( - "AdaptiveReentrancyGuardMock" + "AdaptiveReentrancyGuardMock", ); mock = await factory.deploy(false); await mock.waitForDeployment(); @@ -46,7 +46,7 @@ describe("AdaptiveReentrancyGuard", () => { it("should reject reentrant call with custom error", async () => { await expect(mock.reenter()).to.be.revertedWithCustomError( mock, - "ReentrantCall" + "ReentrantCall", ); }); @@ -59,13 +59,13 @@ describe("AdaptiveReentrancyGuard", () => { describe("gas comparison", () => { it("should be more gas efficient in transient mode than storage mode", async () => { const transientFactory: ContractFactory = await ethers.getContractFactory( - "AdaptiveReentrancyGuardMock" + "AdaptiveReentrancyGuardMock", ); const transientMock: Contract = await transientFactory.deploy(true); await transientMock.waitForDeployment(); const storageFactory: ContractFactory = await ethers.getContractFactory( - "AdaptiveReentrancyGuardMock" + "AdaptiveReentrancyGuardMock", ); const storageMock: Contract = await storageFactory.deploy(false); await storageMock.waitForDeployment(); diff --git a/test/security/TransientStateSnapshot.test.ts b/test/security/TransientStateSnapshot.test.ts index c7ca137..78a9391 100644 --- a/test/security/TransientStateSnapshot.test.ts +++ b/test/security/TransientStateSnapshot.test.ts @@ -23,13 +23,16 @@ describe("TransientStateSnapshot", () => { await mock.setSlot(SLOT_A, VALUE_A); expect(await mock.readSlot(SLOT_A)).to.equal(VALUE_A); - await mock.snapshotAndRollbackSlot(SLOT_A, ethers.id("new_value").padEnd(66, "0")); + await mock.snapshotAndRollbackSlot( + SLOT_A, + ethers.id("new_value").padEnd(66, "0"), + ); expect(await mock.readSlot(SLOT_A)).to.equal(VALUE_A); }); it("should revert SlotNotTracked when rolling back untracked slot", async () => { await expect( - mock.rollbackUntrackedSlot(SLOT_A) + mock.rollbackUntrackedSlot(SLOT_A), ).to.be.revertedWithCustomError(mock, "SlotNotTracked"); }); }); @@ -39,7 +42,10 @@ describe("TransientStateSnapshot", () => { await mock.setSlot(SLOT_A, VALUE_A); await mock.setSlot(SLOT_B, VALUE_B); - await mock.snapshotAndRollbackSlots([SLOT_A, SLOT_B], ethers.id("new_val").padEnd(66, "0")); + await mock.snapshotAndRollbackSlots( + [SLOT_A, SLOT_B], + ethers.id("new_val").padEnd(66, "0"), + ); expect(await mock.readSlot(SLOT_A)).to.equal(VALUE_A); expect(await mock.readSlot(SLOT_B)).to.equal(VALUE_B); @@ -50,7 +56,10 @@ describe("TransientStateSnapshot", () => { it("should clear transient tracking after successful execution", async () => { await mock.setSlot(SLOT_A, VALUE_A); - const tx = await mock.snapshotAndRollbackSlot(SLOT_A, ethers.id("new_value").padEnd(66, "0")); + const tx = await mock.snapshotAndRollbackSlot( + SLOT_A, + ethers.id("new_value").padEnd(66, "0"), + ); expect(await mock.readSlot(SLOT_A)).to.equal(VALUE_A); }); @@ -60,9 +69,9 @@ describe("TransientStateSnapshot", () => { it("should revert when the wrapped function reverts", async () => { await mock.setSlot(SLOT_A, VALUE_A); - await expect( - mock.executeWithRollback(SLOT_A, true) - ).to.be.revertedWith("always revert"); + await expect(mock.executeWithRollback(SLOT_A, true)).to.be.revertedWith( + "always revert", + ); expect(await mock.readSlot(SLOT_A)).to.equal(VALUE_A); }); @@ -74,7 +83,7 @@ describe("TransientStateSnapshot", () => { const txPersistent = await mock.persistentBackupAndRestore( SLOT_A, - ethers.id("tmp").padEnd(66, "0") + ethers.id("tmp").padEnd(66, "0"), ); const receiptPersistent = await txPersistent.wait(); const gasPersistent = receiptPersistent!.gasUsed; @@ -83,7 +92,7 @@ describe("TransientStateSnapshot", () => { const txTransient = await mock.snapshotAndRollbackSlot( SLOT_A, - ethers.id("tmp").padEnd(66, "0") + ethers.id("tmp").padEnd(66, "0"), ); const receiptTransient = await txTransient.wait(); const gasTransient = receiptTransient!.gasUsed; diff --git a/test/storage/UserAccountStruct.test.ts b/test/storage/UserAccountStruct.test.ts index 46da5cc..7520997 100644 --- a/test/storage/UserAccountStruct.test.ts +++ b/test/storage/UserAccountStruct.test.ts @@ -3,11 +3,11 @@ * Issue #630 */ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect } from "vitest"; -describe('UserAccountStruct Storage Packing', () => { - describe('struct field sizing', () => { - it('should calculate total bytes per slot correctly', () => { +describe("UserAccountStruct Storage Packing", () => { + describe("struct field sizing", () => { + it("should calculate total bytes per slot correctly", () => { // Slot 1: wallet (20) + lastActivity (8) + loginCount (4) = 32 bytes const walletBytes = 20; const lastActivityBytes = 8; @@ -17,7 +17,7 @@ describe('UserAccountStruct Storage Packing', () => { expect(totalSlot1).toBe(32); }); - it('should fit tier + isActive in same slot', () => { + it("should fit tier + isActive in same slot", () => { // Slot 2: tier (1) + isActive (1) = 2 bytes (30 bytes padding) const tierBytes = 1; const isActiveBytes = 1; @@ -26,7 +26,7 @@ describe('UserAccountStruct Storage Packing', () => { expect(totalSlot2).toBeLessThanOrEqual(32); }); - it('should pack admin fields into single slot', () => { + it("should pack admin fields into single slot", () => { // adminAddress (20) + adminExpiry (8) + adminNonce (4) = 32 bytes const adminAddressBytes = 20; const adminExpiryBytes = 8; @@ -37,8 +37,8 @@ describe('UserAccountStruct Storage Packing', () => { }); }); - describe('slot calculation', () => { - it('should compute correct slot offset for packed fields', () => { + describe("slot calculation", () => { + it("should compute correct slot offset for packed fields", () => { const baseSlot = 0; const walletOffset = 0; const lastActivityOffset = 20; @@ -50,15 +50,15 @@ describe('UserAccountStruct Storage Packing', () => { expect(loginCountOffset).toBe(28); }); - it('should compute tier slot as baseSlot + 1', () => { + it("should compute tier slot as baseSlot + 1", () => { const baseSlot = 0; const tierSlot = baseSlot + 1; expect(tierSlot).toBe(1); }); }); - describe('gas savings estimate', () => { - it('should save slots vs naive packing', () => { + describe("gas savings estimate", () => { + it("should save slots vs naive packing", () => { // Naive: 6 fields = 6 slots // Packed: 3 slots (balance + packed1 + packed2) const naiveSlots = 6; diff --git a/test/storage/YulArrayUtils.test.ts b/test/storage/YulArrayUtils.test.ts index 6044c3b..f9d3df1 100644 --- a/test/storage/YulArrayUtils.test.ts +++ b/test/storage/YulArrayUtils.test.ts @@ -45,7 +45,7 @@ describe("YulArrayUtils", function () { it("reverts on an out-of-bounds index", async function () { await expect(harness.removeNumberAt(99)).to.be.revertedWithCustomError( harness, - "IndexOutOfBounds" + "IndexOutOfBounds", ); }); }); diff --git a/test/utils/BatchBalanceChecker.test.ts b/test/utils/BatchBalanceChecker.test.ts index 80ee756..167fc45 100644 --- a/test/utils/BatchBalanceChecker.test.ts +++ b/test/utils/BatchBalanceChecker.test.ts @@ -3,13 +3,13 @@ * Issue #690 */ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect } from "vitest"; -describe('BatchBalanceChecker', () => { - describe('result layout', () => { - it('flattens results as tokens x accounts in row-major order', () => { - const tokens = ['tokenA', 'tokenB']; - const accounts = ['acc1', 'acc2', 'acc3']; +describe("BatchBalanceChecker", () => { + describe("result layout", () => { + it("flattens results as tokens x accounts in row-major order", () => { + const tokens = ["tokenA", "tokenB"]; + const accounts = ["acc1", "acc2", "acc3"]; // balances[i * accounts.length + j] = balanceOf(accounts[j]) in tokens[i] const flatIndexOf = (i: number, j: number) => i * accounts.length + j; @@ -24,20 +24,20 @@ describe('BatchBalanceChecker', () => { }); }); - describe('resilience to failing calls', () => { - it('defaults to 0 when a token call reverts', () => { + describe("resilience to failing calls", () => { + it("defaults to 0 when a token call reverts", () => { const staticcallSucceeded = false; const value = staticcallSucceeded ? 100n : 0n; expect(value).toBe(0n); }); - it('defaults to 0 when the target has no code (returndatasize 0)', () => { + it("defaults to 0 when the target has no code (returndatasize 0)", () => { const returnDataSize = 0; const value = returnDataSize > 31 ? 999n : 0n; expect(value).toBe(0n); }); - it('continues querying remaining pairs after one failure', () => { + it("continues querying remaining pairs after one failure", () => { const results = [0n, 500n, 0n, 200n]; // pairs 0 and 2 "failed" const successCount = results.filter((v) => v !== 0n).length; expect(successCount).toBe(2); @@ -45,17 +45,17 @@ describe('BatchBalanceChecker', () => { }); }); - describe('gas characteristics', () => { - it('builds the balanceOf selector once instead of per-call ABI encoding', () => { - const selector = '0x70a08231'; + describe("gas characteristics", () => { + it("builds the balanceOf selector once instead of per-call ABI encoding", () => { + const selector = "0x70a08231"; // Selector is written to memory once outside the loop; each iteration // only overwrites the address argument word, not the selector. - expect(selector).toBe('0x70a08231'); + expect(selector).toBe("0x70a08231"); }); - it('uses staticcall to guarantee no state mutation across arbitrary tokens', () => { - const callType = 'staticcall'; - expect(callType).toBe('staticcall'); + it("uses staticcall to guarantee no state mutation across arbitrary tokens", () => { + const callType = "staticcall"; + expect(callType).toBe("staticcall"); }); }); }); diff --git a/test/utils/MappingResolver.test.ts b/test/utils/MappingResolver.test.ts index ce4e6ca..ac8dd03 100644 --- a/test/utils/MappingResolver.test.ts +++ b/test/utils/MappingResolver.test.ts @@ -3,85 +3,111 @@ * Issue #635 */ -import { describe, it, expect } from 'vitest'; -import { keccak256, solidityPacked, zeroPadValue } from 'ethers'; +import { describe, it, expect } from "vitest"; +import { keccak256, solidityPacked, zeroPadValue } from "ethers"; -describe('MappingResolver', () => { - describe('computeSlot', () => { - it('should compute correct storage slot for single mapping', () => { - const slot = zeroPadValue('0x01', 32); - const key = zeroPadValue('0x1234', 32); +describe("MappingResolver", () => { + describe("computeSlot", () => { + it("should compute correct storage slot for single mapping", () => { + const slot = zeroPadValue("0x01", 32); + const key = zeroPadValue("0x1234", 32); // Expected: keccak256(abi.encode(key, slot)) - const expected = keccak256(solidityPacked(['bytes32', 'bytes32'], [key, slot])); + const expected = keccak256( + solidityPacked(["bytes32", "bytes32"], [key, slot]), + ); expect(expected).toMatch(/^0x[0-9a-f]{64}$/); }); - it('should be deterministic', () => { - const slot = zeroPadValue('0x01', 32); - const key = zeroPadValue('0xabcd', 32); + it("should be deterministic", () => { + const slot = zeroPadValue("0x01", 32); + const key = zeroPadValue("0xabcd", 32); - const result1 = keccak256(solidityPacked(['bytes32', 'bytes32'], [key, slot])); - const result2 = keccak256(solidityPacked(['bytes32', 'bytes32'], [key, slot])); + const result1 = keccak256( + solidityPacked(["bytes32", "bytes32"], [key, slot]), + ); + const result2 = keccak256( + solidityPacked(["bytes32", "bytes32"], [key, slot]), + ); expect(result1).toBe(result2); }); - it('should produce different slots for different keys', () => { - const slot = zeroPadValue('0x01', 32); - const key1 = zeroPadValue('0x0001', 32); - const key2 = zeroPadValue('0x0002', 32); - - const result1 = keccak256(solidityPacked(['bytes32', 'bytes32'], [key1, slot])); - const result2 = keccak256(solidityPacked(['bytes32', 'bytes32'], [key2, slot])); + it("should produce different slots for different keys", () => { + const slot = zeroPadValue("0x01", 32); + const key1 = zeroPadValue("0x0001", 32); + const key2 = zeroPadValue("0x0002", 32); + + const result1 = keccak256( + solidityPacked(["bytes32", "bytes32"], [key1, slot]), + ); + const result2 = keccak256( + solidityPacked(["bytes32", "bytes32"], [key2, slot]), + ); expect(result1).not.toBe(result2); }); }); - describe('computeNestedSlot', () => { - it('should compute correct nested mapping slot', () => { - const slot = zeroPadValue('0x01', 32); - const key1 = zeroPadValue('0x1111', 32); - const key2 = zeroPadValue('0x2222', 32); + describe("computeNestedSlot", () => { + it("should compute correct nested mapping slot", () => { + const slot = zeroPadValue("0x01", 32); + const key1 = zeroPadValue("0x1111", 32); + const key2 = zeroPadValue("0x2222", 32); - const innerSlot = keccak256(solidityPacked(['bytes32', 'bytes32'], [key1, slot])); - const outerSlot = keccak256(solidityPacked(['bytes32', 'bytes32'], [key2, innerSlot])); + const innerSlot = keccak256( + solidityPacked(["bytes32", "bytes32"], [key1, slot]), + ); + const outerSlot = keccak256( + solidityPacked(["bytes32", "bytes32"], [key2, innerSlot]), + ); expect(outerSlot).toMatch(/^0x[0-9a-f]{64}$/); }); - it('should be order-dependent', () => { - const slot = zeroPadValue('0x01', 32); - const key1 = zeroPadValue('0x1111', 32); - const key2 = zeroPadValue('0x2222', 32); + it("should be order-dependent", () => { + const slot = zeroPadValue("0x01", 32); + const key1 = zeroPadValue("0x1111", 32); + const key2 = zeroPadValue("0x2222", 32); - const innerSlot = keccak256(solidityPacked(['bytes32', 'bytes32'], [key1, slot])); - const outerSlot = keccak256(solidityPacked(['bytes32', 'bytes32'], [key2, innerSlot])); + const innerSlot = keccak256( + solidityPacked(["bytes32", "bytes32"], [key1, slot]), + ); + const outerSlot = keccak256( + solidityPacked(["bytes32", "bytes32"], [key2, innerSlot]), + ); // Swapped keys should produce different result - const innerSlot2 = keccak256(solidityPacked(['bytes32', 'bytes32'], [key2, slot])); - const outerSlot2 = keccak256(solidityPacked(['bytes32', 'bytes32'], [key1, innerSlot2])); + const innerSlot2 = keccak256( + solidityPacked(["bytes32", "bytes32"], [key2, slot]), + ); + const outerSlot2 = keccak256( + solidityPacked(["bytes32", "bytes32"], [key1, innerSlot2]), + ); expect(outerSlot).not.toBe(outerSlot2); }); }); - describe('computeAddrSlot', () => { - it('should compute correct slot for address key', () => { - const slot = zeroPadValue('0x01', 32); - const addr = '0x1234567890123456789012345678901234567890'; + describe("computeAddrSlot", () => { + it("should compute correct slot for address key", () => { + const slot = zeroPadValue("0x01", 32); + const addr = "0x1234567890123456789012345678901234567890"; const key = zeroPadValue(addr, 32); - const result = keccak256(solidityPacked(['bytes32', 'bytes32'], [key, slot])); + const result = keccak256( + solidityPacked(["bytes32", "bytes32"], [key, slot]), + ); expect(result).toMatch(/^0x[0-9a-f]{64}$/); }); }); - describe('computeUintSlot', () => { - it('should compute correct slot for uint256 key', () => { - const slot = zeroPadValue('0x01', 32); - const key = zeroPadValue('0x042', 32); + describe("computeUintSlot", () => { + it("should compute correct slot for uint256 key", () => { + const slot = zeroPadValue("0x01", 32); + const key = zeroPadValue("0x042", 32); - const result = keccak256(solidityPacked(['bytes32', 'bytes32'], [key, slot])); + const result = keccak256( + solidityPacked(["bytes32", "bytes32"], [key, slot]), + ); expect(result).toMatch(/^0x[0-9a-f]{64}$/); }); }); diff --git a/test/utils/YulBitSlice.test.ts b/test/utils/YulBitSlice.test.ts index 37a5190..c90dfe0 100644 --- a/test/utils/YulBitSlice.test.ts +++ b/test/utils/YulBitSlice.test.ts @@ -3,20 +3,24 @@ * Issue #695 */ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect } from "vitest"; /** * Reference (pure-JS) model of `YulBitSlice.extractBits` used to validate * the expected behavior of the Yul implementation against known-good * bit-level arithmetic, independent of the EVM. */ -function extractBitsReference(data: Uint8Array, bitOffset: number, bitLength: number): bigint { +function extractBitsReference( + data: Uint8Array, + bitOffset: number, + bitLength: number, +): bigint { const dataBits = data.length * 8; if (bitLength === 0 || bitLength > 256) { - throw new Error('BitLengthTooLarge'); + throw new Error("BitLengthTooLarge"); } if (bitOffset + bitLength > dataBits) { - throw new Error('OutOfBounds'); + throw new Error("OutOfBounds"); } let value = 0n; @@ -29,29 +33,29 @@ function extractBitsReference(data: Uint8Array, bitOffset: number, bitLength: nu return (value >> shiftFromRight) & mask; } -describe('YulBitSlice', () => { - describe('extractBits (reference model)', () => { - it('extracts a whole byte', () => { +describe("YulBitSlice", () => { + describe("extractBits (reference model)", () => { + it("extracts a whole byte", () => { const data = new Uint8Array([0xab]); expect(extractBitsReference(data, 0, 8)).toBe(0xabn); }); - it('extracts the high nibble of a byte', () => { + it("extracts the high nibble of a byte", () => { const data = new Uint8Array([0xab]); // 1010_1011 expect(extractBitsReference(data, 0, 4)).toBe(0xan); }); - it('extracts the low nibble of a byte', () => { + it("extracts the low nibble of a byte", () => { const data = new Uint8Array([0xab]); expect(extractBitsReference(data, 4, 4)).toBe(0xbn); }); - it('extracts bits spanning a byte boundary', () => { + it("extracts bits spanning a byte boundary", () => { const data = new Uint8Array([0xf0, 0x0f]); // 1111_0000 0000_1111 expect(extractBitsReference(data, 4, 8)).toBe(0x00n); }); - it('extracts bits spanning a 32-byte word boundary', () => { + it("extracts bits spanning a 32-byte word boundary", () => { const data = new Uint8Array(40); for (let i = 0; i < data.length; i++) data[i] = i + 1; @@ -60,7 +64,7 @@ describe('YulBitSlice', () => { expect(result).toBe(expected); }); - it('extracts a full 256-bit word', () => { + it("extracts a full 256-bit word", () => { const data = new Uint8Array(32); for (let i = 0; i < 32; i++) data[i] = i + 1; @@ -70,24 +74,28 @@ describe('YulBitSlice', () => { expect(extractBitsReference(data, 0, 256)).toBe(expected); }); - it('throws when the requested slice exceeds the buffer length', () => { + it("throws when the requested slice exceeds the buffer length", () => { const data = new Uint8Array([0xab]); - expect(() => extractBitsReference(data, 4, 8)).toThrow('OutOfBounds'); + expect(() => extractBitsReference(data, 4, 8)).toThrow("OutOfBounds"); }); - it('throws on a zero bit length', () => { + it("throws on a zero bit length", () => { const data = new Uint8Array([0xab]); - expect(() => extractBitsReference(data, 0, 0)).toThrow('BitLengthTooLarge'); + expect(() => extractBitsReference(data, 0, 0)).toThrow( + "BitLengthTooLarge", + ); }); - it('throws when bit length exceeds 256', () => { + it("throws when bit length exceeds 256", () => { const data = new Uint8Array([0xab]); - expect(() => extractBitsReference(data, 0, 257)).toThrow('BitLengthTooLarge'); + expect(() => extractBitsReference(data, 0, 257)).toThrow( + "BitLengthTooLarge", + ); }); }); - describe('gas characteristics', () => { - it('avoids per-byte memory copy loops used by high-level slicing', () => { + describe("gas characteristics", () => { + it("avoids per-byte memory copy loops used by high-level slicing", () => { // High-level `bytes` slicing in a loop costs roughly 3 gas per copied // byte plus loop overhead; the Yul implementation performs at most // two `calldataload`s regardless of slice length. diff --git a/test/utils/YulBytesUtils.test.ts b/test/utils/YulBytesUtils.test.ts index 7597a8f..48d28b2 100644 --- a/test/utils/YulBytesUtils.test.ts +++ b/test/utils/YulBytesUtils.test.ts @@ -36,4 +36,4 @@ describe("YulBytesUtils", function () { expect(result).to.equal("0x"); }); -}); \ No newline at end of file +}); diff --git a/test/utils/YulMappingSlot.test.ts b/test/utils/YulMappingSlot.test.ts index 016b187..1150000 100644 --- a/test/utils/YulMappingSlot.test.ts +++ b/test/utils/YulMappingSlot.test.ts @@ -3,36 +3,43 @@ * Issue #718 */ -import { describe, it, expect } from 'vitest'; -import { keccak256, solidityPacked, zeroPadValue } from 'ethers'; +import { describe, it, expect } from "vitest"; +import { keccak256, solidityPacked, zeroPadValue } from "ethers"; /** Mirrors Solidity's own layout for `mapping(address => uint256)`. */ function expectedSlot(key: string, baseSlot: number): string { const paddedKey = zeroPadValue(key, 32); const paddedSlot = zeroPadValue(`0x${baseSlot.toString(16)}`, 32); - return keccak256(solidityPacked(['bytes32', 'bytes32'], [paddedKey, paddedSlot])); + return keccak256( + solidityPacked(["bytes32", "bytes32"], [paddedKey, paddedSlot]), + ); } -describe('YulMappingSlot.computeSlot', () => { - it('matches standard Solidity mapping storage layout (key || baseSlot)', () => { - const key = '0x000000000000000000000000000000000000000000000000000000000000ab'; +describe("YulMappingSlot.computeSlot", () => { + it("matches standard Solidity mapping storage layout (key || baseSlot)", () => { + const key = + "0x000000000000000000000000000000000000000000000000000000000000ab"; const expected = expectedSlot(key, 0); expect(expected).toMatch(/^0x[0-9a-f]{64}$/); }); - it('is deterministic for the same key and base slot', () => { - const key = '0x00000000000000000000000000000000000000000000000000000000001234'; + it("is deterministic for the same key and base slot", () => { + const key = + "0x00000000000000000000000000000000000000000000000000000000001234"; expect(expectedSlot(key, 0)).toBe(expectedSlot(key, 0)); }); - it('produces different slots for different keys under the same mapping', () => { - const keyA = '0x0000000000000000000000000000000000000000000000000000000000000a'; - const keyB = '0x0000000000000000000000000000000000000000000000000000000000000b'; + it("produces different slots for different keys under the same mapping", () => { + const keyA = + "0x0000000000000000000000000000000000000000000000000000000000000a"; + const keyB = + "0x0000000000000000000000000000000000000000000000000000000000000b"; expect(expectedSlot(keyA, 5)).not.toBe(expectedSlot(keyB, 5)); }); - it('produces different slots for the same key under different mapping base slots', () => { - const key = '0x00000000000000000000000000000000000000000000000000000000000042'; + it("produces different slots for the same key under different mapping base slots", () => { + const key = + "0x00000000000000000000000000000000000000000000000000000000000042"; expect(expectedSlot(key, 0)).not.toBe(expectedSlot(key, 1)); }); });