diff --git a/.gas-snapshot b/.gas-snapshot index 7b93267..e69de29 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,11 +0,0 @@ -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 diff --git a/src/lib/LibDataContract.sol b/src/lib/LibDataContract.sol index 08aed85..74df161 100644 --- a/src/lib/LibDataContract.sol +++ b/src/lib/LibDataContract.sol @@ -75,110 +75,53 @@ 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 { - /// 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) + /// 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); - // 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 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) { + // 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); } - } - - /// 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; + uint256 prefixBytesLength = PREFIX_BYTES_LENGTH; + uint256 basePrefix = BASE_PREFIX; 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) + // 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 as asserted above. + add(mload(data), 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) + 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(creationCodeDataPointer, dataPointer, mload(data)) } - if (deployedAddress == address(0) || !success) revert WriteError(); } /// Reads data back from a previously deployed container. diff --git a/test/lib/LibDataContract.t.sol b/test/lib/LibDataContract.t.sol index c657682..5950256 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"; @@ -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), 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); } @@ -31,28 +48,35 @@ 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 sliceLength) + external + { + bytes32 dataHash = keccak256(data); + vm.assume(uint256(start) + uint256(sliceLength) <= data.length); + + bytes memory expectedSlice = new bytes(sliceLength); + LibMemCpy.unsafeCopyBytesTo(data.dataPointer().unsafeAddBytes(start), expectedSlice.dataPointer(), sliceLength); - /// 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); + 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); + + // 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); } /// Reading from a contract that isn't a valid data contract should throw @@ -74,46 +98,34 @@ 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)) + } 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); @@ -121,92 +133,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); - } }