Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions contracts/utils/YulByteSwapper.sol
Original file line number Diff line number Diff line change
@@ -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)));
}
}
95 changes: 95 additions & 0 deletions contracts/utils/YulByteSwapper.test.ts
Original file line number Diff line number Diff line change
@@ -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);
}
});
});
14 changes: 14 additions & 0 deletions contracts/utils/YulByteSwapperHarness.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
44 changes: 24 additions & 20 deletions test/cache/TransientCache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,56 +3,60 @@
* 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<string, string>();
storage.set(slot, value);
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<string, string>();
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<string, string>();
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<string, bigint>();
const user = '0x1234567890123456789012345678901234567890';
const user = "0x1234567890123456789012345678901234567890";
const slot = BigInt(user);

// First call - compute and cache
Expand All @@ -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);
});
Expand Down
34 changes: 22 additions & 12 deletions test/calldata/CalldataUnpacker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,19 @@ 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],
),
),
);
}

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])],
);
}

Expand Down Expand Up @@ -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,
);
}
});

Expand All @@ -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",
);
});
});
Expand All @@ -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");
Expand All @@ -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}`);
Expand Down
Loading
Loading