From c55d2304a95e62e4de97503006085725287ec11f Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Sat, 7 Feb 2026 17:37:14 +0400 Subject: [PATCH 1/9] contract creation code --- src/lib/LibDataContract.sol | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/lib/LibDataContract.sol b/src/lib/LibDataContract.sol index 08aed85..7bb5523 100644 --- a/src/lib/LibDataContract.sol +++ b/src/lib/LibDataContract.sol @@ -75,6 +75,39 @@ type DataContractMemoryContainer is uint256; /// Solidity but instead requires the caller to copy memory directy by pointer. /// https://github.com/rainprotocol/sol.lib.bytes can help with that. library LibDataContract { + function contractCreationCode(bytes memory data) internal pure returns (bytes memory creationCode) { + uint256 prefixBytesLength = PREFIX_BYTES_LENGTH; + uint256 basePrefix = BASE_PREFIX; + assembly ("memory-safe") { + // allocate output byte array + creationCode := mload(0x40) + // new "memory end" including padding + let dataLength := add(prefixBytesLength, mload(data)) + let paddedDataLength := and(add(dataLength, 0x1f), not(0x1f)) + let totalLength := add(paddedDataLength, 0x20) + mstore(0x40, add(creationCode, totalLength)) + mstore(creationCode, dataLength) + let prefix := + or( + basePrefix, + shl( + // Length sits 29 bytes from the right + 232, + // Length fits in 2 bytes for all valid inputs of type + // `bytes` that can possibly deploy as a contract (max 24kb). + // Add 1 to length to include the 0x00 prefix byte to be + // deployed along with the main contract data. + add(mload(data), 1) + ) + ) + mstore(add(creationCode, 0x20), prefix) + // copy data to end of prefix in creation code + let dataPointer := add(data, 0x20) + let creationCodeDataPointer := add(creationCode, add(0x20, prefixBytesLength)) + mcopy(dataPointer, creationCodeDataPointer, mload(data)) + } + } + /// Prepares a container ready to write exactly `length` bytes at the /// returned `pointer_`. The caller MUST write exactly the number of bytes /// that it asks for at the pointer otherwise memory WILL be corrupted. From 8f8026f0f29c09d8977783485934a49bea70aa65 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Sat, 7 Feb 2026 17:54:10 +0400 Subject: [PATCH 2/9] test round trip creation code fuzz --- src/lib/LibDataContract.sol | 10 ++++++++++ test/lib/LibDataContract.t.sol | 15 +++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/lib/LibDataContract.sol b/src/lib/LibDataContract.sol index 7bb5523..733014d 100644 --- a/src/lib/LibDataContract.sol +++ b/src/lib/LibDataContract.sol @@ -75,6 +75,16 @@ type DataContractMemoryContainer is uint256; /// Solidity but instead requires the caller to copy memory directy by pointer. /// https://github.com/rainprotocol/sol.lib.bytes can help with that. library LibDataContract { + /// Given some data in memory, prepares the creation code for a contract that + /// will contain that data when deployed. The caller is responsible for + /// actually deploying the creation code, which should be compatible with any + /// normal method that works for `type(Foo).creationCode` such as `create` or + /// a deterministic deployment proxy. Usual considerations such as checking + /// the success of contract creation after deployment all apply. + /// @param data The data to be included in the deployed contract. This can be + /// any data that fits in the EVM code size limit for contracts (24kb). + /// @return creationCode The creation code that can be deployed to create a + /// contract containing the data. function contractCreationCode(bytes memory data) internal pure returns (bytes memory creationCode) { uint256 prefixBytesLength = PREFIX_BYTES_LENGTH; uint256 basePrefix = BASE_PREFIX; diff --git a/test/lib/LibDataContract.t.sol b/test/lib/LibDataContract.t.sol index c657682..0fe11cf 100644 --- a/test/lib/LibDataContract.t.sol +++ b/test/lib/LibDataContract.t.sol @@ -37,6 +37,21 @@ contract DataContractTest is Test { return LibDataContract.writeZoltu(container); } + function testRoundCreationCodeFuzz(bytes memory data, bytes memory garbage) external { + // Put some garbage in unallocated memory. + LibMemCpy.unsafeCopyBytesTo(garbage.dataPointer(), LibPointer.allocatedMemoryPointer(), garbage.length); + + bytes memory creationCode = LibDataContract.contractCreationCode(data); + address dataContract; + assembly ("memory-safe") { + dataContract := create(0, add(creationCode, 0x20), mload(creationCode)) + } + bytes memory round = LibDataContract.read(dataContract); + + assertEq(round.length, data.length); + assertEq(round, data); + } + /// Writing any data to a contract then reading it back without corrupting /// memory or the data itself. function testRoundFuzz(bytes memory data, bytes memory garbage) public { From d449490b5fe8724bba9f2505b0cfddec54f723b7 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Sat, 7 Feb 2026 18:08:03 +0400 Subject: [PATCH 3/9] remove write functions --- src/lib/LibDataContract.sol | 106 ---------------------- test/lib/LibDataContract.t.sol | 157 +++++++-------------------------- 2 files changed, 31 insertions(+), 232 deletions(-) diff --git a/src/lib/LibDataContract.sol b/src/lib/LibDataContract.sol index 733014d..61a54ef 100644 --- a/src/lib/LibDataContract.sol +++ b/src/lib/LibDataContract.sol @@ -118,112 +118,6 @@ library LibDataContract { } } - /// Prepares a container ready to write exactly `length` bytes at the - /// returned `pointer_`. The caller MUST write exactly the number of bytes - /// that it asks for at the pointer otherwise memory WILL be corrupted. - /// @param length Caller specifies the number of bytes to allocate for the - /// data it wants to write. The actual size of the container in memory will - /// be larger than this due to the contract creation prefix and the padding - /// potentially required to align the memory allocation. - /// @return container The pointer to the start of the container that can be - /// deployed as an onchain contract. Caller can pass this back to `write` to - /// have the data contract deployed - /// (after it copies its data to the pointer). - /// @return pointer The caller can copy its data at the pointer without any - /// additional allocations or Solidity type wrangling. - function newContainer(uint256 length) - internal - pure - returns (DataContractMemoryContainer container, Pointer pointer) - { - unchecked { - uint256 prefixBytesLength = PREFIX_BYTES_LENGTH; - uint256 basePrefix = BASE_PREFIX; - assembly ("memory-safe") { - // allocate output byte array - this could also be done without assembly - // by using container = new bytes(size) - container := mload(0x40) - // new "memory end" including padding - mstore(0x40, add(container, and(add(add(length, prefixBytesLength), 0x1f), not(0x1f)))) - // pointer is where the caller will write data to - pointer := add(container, prefixBytesLength) - - // copy length into the 2 bytes gap in the base prefix - let prefix := - or( - basePrefix, - shl( - // length sits 29 bytes from the right - 232, - and( - // mask the length to 2 bytes - 0xFFFF, - add(length, 1) - ) - ) - ) - mstore(container, prefix) - } - } - } - - /// Given a container prepared by `newContainer` and populated with bytes by - /// the caller, deploy to a new onchain contract and return the contract - /// address. - /// @param container The container full of data to deploy as an onchain data - /// contract. - /// @return The newly deployed contract containing the data in the container. - function write(DataContractMemoryContainer container) internal returns (address) { - address pointer; - uint256 prefixLength = PREFIX_BYTES_LENGTH; - assembly ("memory-safe") { - pointer := create( - 0, - container, - add( - prefixLength, - // Read length out of prefix. - // Sub 1 as length stored is +1 to include the 0x00 prefix - // byte. - sub(and(0xFFFF, shr(232, mload(container))), 1) - ) - ) - } - // Zero address means create failed. - if (pointer == address(0)) revert WriteError(); - return pointer; - } - - /// Same as `write` but deploys to a deterministic address that does not - /// rely on the address nor nonce of the caller. This means that the address - /// will be the same on all networks and for all callers for the same data. - /// https://github.com/Zoltu/deterministic-deployment-proxy - function writeZoltu(DataContractMemoryContainer container) internal returns (address deployedAddress) { - address zoltu = ZOLTU_PROXY_ADDRESS; - uint256 prefixLength = PREFIX_BYTES_LENGTH; - bool success; - assembly ("memory-safe") { - mstore(0, 0) - success := call( - gas(), - zoltu, - 0, - container, - add( - prefixLength, - // Read length out of prefix. - // Sub 1 as length stored is +1 to include the 0x00 prefix - // byte. - sub(and(0xFFFF, shr(232, mload(container))), 1) - ), - 12, - 20 - ) - deployedAddress := mload(0) - } - if (deployedAddress == address(0) || !success) revert WriteError(); - } - /// Reads data back from a previously deployed container. /// Almost verbatim Solidity docs. /// https://docs.soliditylang.org/en/v0.8.17/assembly.html#example diff --git a/test/lib/LibDataContract.t.sol b/test/lib/LibDataContract.t.sol index 0fe11cf..79c3f0d 100644 --- a/test/lib/LibDataContract.t.sol +++ b/test/lib/LibDataContract.t.sol @@ -31,13 +31,12 @@ contract DataContractTest is Test { return LibDataContract.readSlice(datacontract, start, length); } - function writeZoltuExternal(bytes memory data) external returns (address) { - (DataContractMemoryContainer container, Pointer pointer) = LibDataContract.newContainer(data.length); - LibMemCpy.unsafeCopyBytesTo(data.dataPointer(), pointer, data.length); - return LibDataContract.writeZoltu(container); - } + function testRoundCreationCodeFuzz(bytes memory data, bytes memory garbage, uint16 start, uint16 length) external { + vm.assume(uint256(start) + uint256(length) <= data.length); + + bytes memory expectedSlice = new bytes(length); + LibMemCpy.unsafeCopyBytesTo(data.dataPointer().unsafeAddBytes(start), expectedSlice.dataPointer(), length); - function testRoundCreationCodeFuzz(bytes memory data, bytes memory garbage) external { // Put some garbage in unallocated memory. LibMemCpy.unsafeCopyBytesTo(garbage.dataPointer(), LibPointer.allocatedMemoryPointer(), garbage.length); @@ -50,24 +49,9 @@ contract DataContractTest is Test { assertEq(round.length, data.length); assertEq(round, data); - } - - /// Writing any data to a contract then reading it back without corrupting - /// memory or the data itself. - function testRoundFuzz(bytes memory data, bytes memory garbage) public { - // Put some garbage in unallocated memory. - LibMemCpy.unsafeCopyBytesTo(garbage.dataPointer(), LibPointer.allocatedMemoryPointer(), garbage.length); - - (DataContractMemoryContainer container, Pointer pointer) = LibDataContract.newContainer(data.length); - LibMemCpy.unsafeCopyBytesTo(data.dataPointer(), pointer, data.length); - - address datacontract = LibDataContract.write(container); - - bytes memory round = LibDataContract.read(datacontract); - - assertEq(round.length, data.length); - assertEq(round, data); + bytes memory roundSlice = LibDataContract.readSlice(dataContract, start, length); + assertEq(roundSlice, expectedSlice); } /// Reading from a contract that isn't a valid data contract should throw @@ -89,46 +73,38 @@ contract DataContractTest is Test { (read); } - /// Should be possible to read only a slice of the data. - function testRoundSlice(bytes memory data, uint16 start, uint16 length) public { - vm.assume(uint256(start) + uint256(length) <= data.length); - - bytes memory expected = new bytes(length); - LibMemCpy.unsafeCopyBytesTo(data.dataPointer().unsafeAddBytes(start), expected.dataPointer(), length); - - (DataContractMemoryContainer container, Pointer pointer) = LibDataContract.newContainer(data.length); - LibMemCpy.unsafeCopyBytesTo(data.dataPointer(), pointer, data.length); - address datacontract = LibDataContract.write(container); - - bytes memory slice = LibDataContract.readSlice(datacontract, start, length); - - assertEq(expected, slice); - } - /// Reading a slice that is out of bounds should throw a ReadError. function testRoundSliceError(bytes memory data, uint16 start, uint16 length) public { vm.assume(uint256(start) + uint256(length) > data.length); - (DataContractMemoryContainer container, Pointer pointer) = LibDataContract.newContainer(data.length); - LibMemCpy.unsafeCopyBytesTo(data.dataPointer(), pointer, data.length); - address datacontract = LibDataContract.write(container); + bytes memory creationCode = LibDataContract.contractCreationCode(data); + address dataContract; + assembly ("memory-safe") { + dataContract := create(0, add(creationCode, 0x20), mload(creationCode)) + } vm.expectRevert(ReadError.selector); - (bytes memory slice) = this.readSliceExternal(datacontract, start, length); + (bytes memory slice) = this.readSliceExternal(dataContract, start, length); (slice); } /// Reading a slice over the whole contract gives the same result as reading /// the whole contract. function testSameReads(bytes memory data) public { - (DataContractMemoryContainer container, Pointer pointer) = LibDataContract.newContainer(data.length); - LibMemCpy.unsafeCopyBytesTo(data.dataPointer(), pointer, data.length); - address datacontract = LibDataContract.write(container); + bytes memory creationCode = LibDataContract.contractCreationCode(data); + address dataContract; + assembly ("memory-safe") { + dataContract := create(0, add(creationCode, 0x20), mload(creationCode)) + } + + // (DataContractMemoryContainer container, Pointer pointer) = LibDataContract.newContainer(data.length); + // LibMemCpy.unsafeCopyBytesTo(data.dataPointer(), pointer, data.length); + // address datacontract = LibDataContract.write(container); uint256 a = gasleft(); - bytes memory read = LibDataContract.read(datacontract); + bytes memory read = LibDataContract.read(dataContract); uint256 b = gasleft(); - bytes memory readSlice = LibDataContract.readSlice(datacontract, 0, uint16(data.length)); + bytes memory readSlice = LibDataContract.readSlice(dataContract, 0, uint16(data.length)); uint256 c = gasleft(); assertEq(read, readSlice); @@ -136,92 +112,21 @@ contract DataContractTest is Test { assertGt(b - c, a - b); } - /// Writing data twice yields two different addresses even if the data is - /// the same. - function testNewAddressFuzzData(bytes memory data) public { - (DataContractMemoryContainer container, Pointer pointer) = LibDataContract.newContainer(data.length); - LibMemCpy.unsafeCopyBytesTo(data.dataPointer(), pointer, data.length); - - address datacontractAlpha = LibDataContract.write(container); - address datacontractBeta = LibDataContract.write(container); - - assertTrue(datacontractAlpha != datacontractBeta); - assertEq(LibDataContract.read(datacontractAlpha), LibDataContract.read(datacontractBeta)); - } - - /// Writing data twice yields two different addresses if the data is - /// different. - function testNewAddressFuzzDataDifferent(bytes memory alpha, bytes memory beta) public { - vm.assume(keccak256(alpha) != keccak256(beta)); - (DataContractMemoryContainer containerAlpha, Pointer pointerAlpha) = LibDataContract.newContainer(alpha.length); - LibMemCpy.unsafeCopyBytesTo(alpha.dataPointer(), pointerAlpha, alpha.length); - (DataContractMemoryContainer containerBeta, Pointer pointerBeta) = LibDataContract.newContainer(beta.length); - LibMemCpy.unsafeCopyBytesTo(beta.dataPointer(), pointerBeta, beta.length); - - address datacontractAlpha = LibDataContract.write(containerAlpha); - address datacontractBeta = LibDataContract.write(containerBeta); - - assertTrue(datacontractAlpha != datacontractBeta); - assertTrue( - keccak256(LibDataContract.read(datacontractAlpha)) != keccak256(LibDataContract.read(datacontractBeta)) - ); - } - /// Check there is always a 0 byte prefix on the underlying data contract. function testZeroPrefix(bytes memory data) public { - (DataContractMemoryContainer container, Pointer pointer) = LibDataContract.newContainer(data.length); - LibMemCpy.unsafeCopyBytesTo(data.dataPointer(), pointer, data.length); - address datacontract_ = LibDataContract.write(container); + bytes memory creationCode = LibDataContract.contractCreationCode(data); + address dataContract; + assembly ("memory-safe") { + dataContract := create(0, add(creationCode, 0x20), mload(creationCode)) + } + uint256 firstByte; assembly ("memory-safe") { mstore(0, 0) // copy to scratch. - extcodecopy(datacontract_, 0, 0, 1) + extcodecopy(dataContract, 0, 0, 1) firstByte := mload(0) } assertEq(firstByte, 0); } - - /// Check that if we deploy with zoltu we get the same address on different - /// networks. - function testZoltu() public { - bytes memory data = bytes("zoltu"); - - (DataContractMemoryContainer container, Pointer pointer) = LibDataContract.newContainer(data.length); - LibMemCpy.unsafeCopyBytesTo(data.dataPointer(), pointer, data.length); - - vm.createSelectFork(vm.envString("CI_FORK_ETH_RPC_URL")); - - address datacontractAlpha = LibDataContract.writeZoltu(container); - - assertEq(datacontractAlpha, 0x1Cf89F16784b780E549105B04e80D5196E13C4Af); - assertEq(keccak256(data), keccak256(LibDataContract.read(datacontractAlpha))); - - vm.createSelectFork(vm.envString("CI_FORK_AVALANCHE_RPC_URL")); - - address datacontractBeta = LibDataContract.writeZoltu(container); - - assertEq(datacontractBeta, 0x1Cf89F16784b780E549105B04e80D5196E13C4Af); - assertEq(keccak256(data), keccak256(LibDataContract.read(datacontractBeta))); - } - - /// Check that if we use zoltu without the zoltu proxy existing that we - /// revert. - function testZoltuNoZoltu(bytes memory data) external { - vm.assume(ZOLTU_PROXY_ADDRESS.code.length == 0); - vm.expectRevert(abi.encodeWithSelector(WriteError.selector)); - this.writeZoltuExternal(data); - } - - /// Check that if zoltu exists but returns not success we revert. - function testZoltuBadZoltu(bytes memory data) external { - vm.assume(ZOLTU_PROXY_ADDRESS.code.length == 0); - vm.etch( - ZOLTU_PROXY_ADDRESS, - // revert opcode. - hex"fd" - ); - vm.expectRevert(abi.encodeWithSelector(WriteError.selector)); - this.writeZoltuExternal(data); - } } From 4e5e9fc8ce57fe678f5ffbbb3fc98d6927b3c682 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Sat, 7 Feb 2026 18:20:31 +0400 Subject: [PATCH 4/9] fix tests --- .gas-snapshot | 14 ++++---------- src/lib/LibDataContract.sol | 2 +- test/lib/LibDataContract.t.sol | 20 ++++++++++++++------ 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index 7b93267..f4580d2 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,11 +1,5 @@ DataContractTest:testErrorBadAddressRead(address) (runs: 1024, μ: 8771, ~: 8771) -DataContractTest:testNewAddressFuzzData(bytes) (runs: 1024, μ: 85287, ~: 82448) -DataContractTest:testNewAddressFuzzDataDifferent(bytes,bytes) (runs: 1024, μ: 86318, ~: 82982) -DataContractTest:testRoundFuzz(bytes,bytes) (runs: 1024, μ: 45487, ~: 43907) -DataContractTest:testRoundSlice(bytes,uint16,uint16) (runs: 1024, μ: 46636, ~: 44212) -DataContractTest:testRoundSliceError(bytes,uint16,uint16) (runs: 1024, μ: 47523, ~: 46303) -DataContractTest:testSameReads(bytes) (runs: 1024, μ: 45381, ~: 43952) -DataContractTest:testZeroPrefix(bytes) (runs: 1024, μ: 41309, ~: 39893) -DataContractTest:testZoltu() (gas: 78935) -DataContractTest:testZoltuBadZoltu(bytes) (runs: 1024, μ: 1040428879, ~: 1040429085) -DataContractTest:testZoltuNoZoltu(bytes) (runs: 1024, μ: 8598, ~: 8587) \ No newline at end of file +DataContractTest:testRoundCreationCodeFuzz(bytes,bytes,uint16,uint16) (runs: 1024, μ: 48470, ~: 46014) +DataContractTest:testRoundSliceError(bytes,uint16,uint16) (runs: 1024, μ: 47973, ~: 46191) +DataContractTest:testSameReads(bytes) (runs: 1024, μ: 45864, ~: 43845) +DataContractTest:testZeroPrefix(bytes) (runs: 1024, μ: 41769, ~: 39764) \ No newline at end of file diff --git a/src/lib/LibDataContract.sol b/src/lib/LibDataContract.sol index 61a54ef..441e6e9 100644 --- a/src/lib/LibDataContract.sol +++ b/src/lib/LibDataContract.sol @@ -114,7 +114,7 @@ library LibDataContract { // copy data to end of prefix in creation code let dataPointer := add(data, 0x20) let creationCodeDataPointer := add(creationCode, add(0x20, prefixBytesLength)) - mcopy(dataPointer, creationCodeDataPointer, mload(data)) + mcopy(creationCodeDataPointer, dataPointer, mload(data)) } } diff --git a/test/lib/LibDataContract.t.sol b/test/lib/LibDataContract.t.sol index 79c3f0d..0ad7f40 100644 --- a/test/lib/LibDataContract.t.sol +++ b/test/lib/LibDataContract.t.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {Test} from "forge-std/Test.sol"; +import {Test, console2} from "forge-std/Test.sol"; import {LibMemCpy} from "rain.solmem/lib/LibMemCpy.sol"; import {LibBytes} from "rain.solmem/lib/LibBytes.sol"; @@ -31,11 +31,14 @@ contract DataContractTest is Test { return LibDataContract.readSlice(datacontract, start, length); } - function testRoundCreationCodeFuzz(bytes memory data, bytes memory garbage, uint16 start, uint16 length) external { - vm.assume(uint256(start) + uint256(length) <= data.length); + function testRoundCreationCodeFuzz(bytes memory data, bytes memory garbage, uint16 start, uint16 sliceLength) + external + { + bytes32 dataHash = keccak256(data); + vm.assume(uint256(start) + uint256(sliceLength) <= data.length); - bytes memory expectedSlice = new bytes(length); - LibMemCpy.unsafeCopyBytesTo(data.dataPointer().unsafeAddBytes(start), expectedSlice.dataPointer(), length); + bytes memory expectedSlice = new bytes(sliceLength); + LibMemCpy.unsafeCopyBytesTo(data.dataPointer().unsafeAddBytes(start), expectedSlice.dataPointer(), sliceLength); // Put some garbage in unallocated memory. LibMemCpy.unsafeCopyBytesTo(garbage.dataPointer(), LibPointer.allocatedMemoryPointer(), garbage.length); @@ -50,7 +53,12 @@ contract DataContractTest is Test { assertEq(round.length, data.length); assertEq(round, data); - bytes memory roundSlice = LibDataContract.readSlice(dataContract, start, length); + // Check before/after hashes against datas to ensure bad mutations didn't + // occur somewhere in the process. + assertEq(keccak256(data), dataHash); + assertEq(keccak256(round), dataHash); + + bytes memory roundSlice = LibDataContract.readSlice(dataContract, start, sliceLength); assertEq(roundSlice, expectedSlice); } From 6c1b78f837d331ec366e5d5cd63d88549a65de77 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Sat, 7 Feb 2026 18:25:01 +0400 Subject: [PATCH 5/9] revert when data too large for contract creation --- src/lib/LibDataContract.sol | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/lib/LibDataContract.sol b/src/lib/LibDataContract.sol index 441e6e9..8b2b792 100644 --- a/src/lib/LibDataContract.sol +++ b/src/lib/LibDataContract.sol @@ -75,6 +75,11 @@ type DataContractMemoryContainer is uint256; /// Solidity but instead requires the caller to copy memory directy by pointer. /// https://github.com/rainprotocol/sol.lib.bytes can help with that. library LibDataContract { + /// Thrown when trying to write data that is too large to fit in uint16. + /// @param dataLength The length of the data that was attempted to create a + /// contract with. + error DataTooLarge(uint256 dataLength); + /// Given some data in memory, prepares the creation code for a contract that /// will contain that data when deployed. The caller is responsible for /// actually deploying the creation code, which should be compatible with any @@ -86,6 +91,9 @@ library LibDataContract { /// @return creationCode The creation code that can be deployed to create a /// contract containing the data. function contractCreationCode(bytes memory data) internal pure returns (bytes memory creationCode) { + if (data.length > uint256(type(uint16).max)) { + revert DataTooLarge(data.length); + } uint256 prefixBytesLength = PREFIX_BYTES_LENGTH; uint256 basePrefix = BASE_PREFIX; assembly ("memory-safe") { From c7f7e1006ae76b6f02c952b9f3f344f49ee43640 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Sat, 7 Feb 2026 18:25:22 +0400 Subject: [PATCH 6/9] lint --- src/lib/LibDataContract.sol | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/lib/LibDataContract.sol b/src/lib/LibDataContract.sol index 8b2b792..d5a84e2 100644 --- a/src/lib/LibDataContract.sol +++ b/src/lib/LibDataContract.sol @@ -111,10 +111,7 @@ library LibDataContract { shl( // Length sits 29 bytes from the right 232, - // Length fits in 2 bytes for all valid inputs of type - // `bytes` that can possibly deploy as a contract (max 24kb). - // Add 1 to length to include the 0x00 prefix byte to be - // deployed along with the main contract data. + // Length fits in 2 bytes as asserted above. add(mload(data), 1) ) ) From 05468aaa9efa99d4eea398c050e0d38b65096b98 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Sat, 7 Feb 2026 18:29:20 +0400 Subject: [PATCH 7/9] test data too large revert --- test/lib/LibDataContract.t.sol | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/lib/LibDataContract.t.sol b/test/lib/LibDataContract.t.sol index 0ad7f40..fb75273 100644 --- a/test/lib/LibDataContract.t.sol +++ b/test/lib/LibDataContract.t.sol @@ -23,6 +23,23 @@ contract DataContractTest is Test { using LibBytes for bytes; using LibPointer for Pointer; + function contractCreationCodeVeryLargeData(uint256 length) external pure { + bytes memory data; + // Point data after allocated memory and just extend it virtually out + // to the desired length without doing an explicit memory expansion. + assembly ("memory-safe") { + data := mload(0x40) + mstore(data, length) + } + LibDataContract.contractCreationCode(data); + } + + function testContractCreationCodeDataTooLargeRevert(uint256 length) external { + length = bound(length, uint256(type(uint16).max) + 1, type(uint256).max); + vm.expectRevert(abi.encodeWithSelector(LibDataContract.DataTooLarge.selector, length)); + this.contractCreationCodeVeryLargeData(length); + } + function readExternal(address datacontract) external view returns (bytes memory) { return LibDataContract.read(datacontract); } From 5c86a2cafff09da2bb310ee8cce20e783392b882 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Sat, 7 Feb 2026 18:30:51 +0400 Subject: [PATCH 8/9] lint --- test/lib/LibDataContract.t.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/lib/LibDataContract.t.sol b/test/lib/LibDataContract.t.sol index fb75273..0c07a3f 100644 --- a/test/lib/LibDataContract.t.sol +++ b/test/lib/LibDataContract.t.sol @@ -122,10 +122,6 @@ contract DataContractTest is Test { dataContract := create(0, add(creationCode, 0x20), mload(creationCode)) } - // (DataContractMemoryContainer container, Pointer pointer) = LibDataContract.newContainer(data.length); - // LibMemCpy.unsafeCopyBytesTo(data.dataPointer(), pointer, data.length); - // address datacontract = LibDataContract.write(container); - uint256 a = gasleft(); bytes memory read = LibDataContract.read(dataContract); uint256 b = gasleft(); From 5c165eb2eae92e79df2cfaa51f1dc7a0238cebcb Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Sat, 7 Feb 2026 18:58:16 +0400 Subject: [PATCH 9/9] fix edge condition --- .gas-snapshot | 5 ----- src/lib/LibDataContract.sol | 3 ++- test/lib/LibDataContract.t.sol | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index f4580d2..e69de29 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,5 +0,0 @@ -DataContractTest:testErrorBadAddressRead(address) (runs: 1024, μ: 8771, ~: 8771) -DataContractTest:testRoundCreationCodeFuzz(bytes,bytes,uint16,uint16) (runs: 1024, μ: 48470, ~: 46014) -DataContractTest:testRoundSliceError(bytes,uint16,uint16) (runs: 1024, μ: 47973, ~: 46191) -DataContractTest:testSameReads(bytes) (runs: 1024, μ: 45864, ~: 43845) -DataContractTest:testZeroPrefix(bytes) (runs: 1024, μ: 41769, ~: 39764) \ No newline at end of file diff --git a/src/lib/LibDataContract.sol b/src/lib/LibDataContract.sol index d5a84e2..74df161 100644 --- a/src/lib/LibDataContract.sol +++ b/src/lib/LibDataContract.sol @@ -91,7 +91,8 @@ library LibDataContract { /// @return creationCode The creation code that can be deployed to create a /// contract containing the data. function contractCreationCode(bytes memory data) internal pure returns (bytes memory creationCode) { - if (data.length > uint256(type(uint16).max)) { + // GTE here because of the extra 0 byte that needs to be accounted for. + if (data.length >= uint256(type(uint16).max)) { revert DataTooLarge(data.length); } uint256 prefixBytesLength = PREFIX_BYTES_LENGTH; diff --git a/test/lib/LibDataContract.t.sol b/test/lib/LibDataContract.t.sol index 0c07a3f..5950256 100644 --- a/test/lib/LibDataContract.t.sol +++ b/test/lib/LibDataContract.t.sol @@ -35,7 +35,7 @@ contract DataContractTest is Test { } function testContractCreationCodeDataTooLargeRevert(uint256 length) external { - length = bound(length, uint256(type(uint16).max) + 1, type(uint256).max); + length = bound(length, uint256(type(uint16).max), type(uint256).max); vm.expectRevert(abi.encodeWithSelector(LibDataContract.DataTooLarge.selector, length)); this.contractCreationCodeVeryLargeData(length); }