diff --git a/README.md b/README.md index f350969b4..c6758d4b3 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ erDiagram ADMIN_ACL ||--o{ CORE_V3_V3_FLEX : manages ADMIN_ACL ||--o{ MINTERFILTER : manages CORE_V3_V3_FLEX ||--|| MINTERFILTER : uses + CORE_V3_V3_FLEX }o--|| BYTECODESTORAGEREADER : uses MINTERFILTER ||--o{ MINTER_1 : uses MINTERFILTER ||--o{ MINTER_2 : uses MINTERFILTER ||--o{ MINTER_N : uses @@ -141,6 +142,15 @@ The following represents the current set of flagship core contracts deployed on For deployed core contracts, see the deployment details in the `/deployments/engine/[V2|V3]//` directories. For V2 core contracts, archived source code is available in the `/posterity/engine/` directory. +## BytecodeStorageReader + +BytecodeStorageReader (currently on V1 version) is public library for reading from storage contracts. This library is intended to be deployed as a standalone contract, and provides all _read_ functionality by being used as an externally linked library within the Art Blocks ecosystem contracts that use contract storage for writes. + +Given that it is an externally linked library with a shared public deployment, the deployment addresses for these shared deployments are referenced in our shared deployments `constants.ts` util (in the `BYTECODE_STORAGE_READER_LIBRARY_ADDRESSES` constant) so that they may be linked at time of deployment and are also linked here below for shared reference: + +- V1 `BytecodeStorageReader` (goerli): https://goerli.etherscan.io/address/0xB8B806A10d16cc80dB788552B54B3ECb4A2A3C3D#code +- V1 `BytecodeStorageReader` (mainnet): https://etherscan.io/address/0xf0585dF582A0ad119F1616FB82f3b449a98EeCd5#code + ## Minter Suite For details on the Flagship and Engine Minter Suite, see the [minter suite documenation](./MINTER_SUITE.md). diff --git a/contracts/DependencyRegistryV0.sol b/contracts/DependencyRegistryV0.sol index 252e213c0..9e646651b 100644 --- a/contracts/DependencyRegistryV0.sol +++ b/contracts/DependencyRegistryV0.sol @@ -13,7 +13,7 @@ import "@openzeppelin-4.8/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "@openzeppelin-4.8/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin-4.5/contracts/utils/math/SafeCast.sol"; -import "./libs/0.8.x/BytecodeStorage.sol"; +import "./libs/0.8.x/BytecodeStorageV1.sol"; import "./libs/0.8.x/Bytes32Strings.sol"; /** @@ -33,8 +33,7 @@ contract DependencyRegistryV0 is OwnableUpgradeable, IDependencyRegistryV0 { - using BytecodeStorage for string; - using BytecodeStorage for address; + using BytecodeStorageWriter for string; using Bytes32Strings for bytes32; using Strings for uint256; using EnumerableSet for EnumerableSet.Bytes32Set; @@ -211,16 +210,6 @@ contract DependencyRegistryV0 is _scriptId < dependencyType.scriptCount, "scriptId out of range" ); - // purge old contract bytecode contract from the blockchain state - // note: Although this does reduce usage of Ethereum state, it does not - // reduce the gas costs of removal transactions. We believe this is the - // best behavior at the time of writing, and do not expect this to - // result in any breaking changes in the future. All current proposals - // to change the self-destruct opcode are backwards compatible, but may - // result in not removing the bytecode from the blockchain state. This - // implementation is compatible with that architecture, as it does not - // rely on the bytecode being removed from the blockchain state. - dependencyType.scriptBytecodeAddresses[_scriptId].purgeBytecode(); // store script in contract bytecode, replacing reference address from // the contract that no longer exists with the newly created one dependencyType.scriptBytecodeAddresses[_scriptId] = _script @@ -238,19 +227,7 @@ contract DependencyRegistryV0 is _onlyExistingDependencyType(_dependencyType); Dependency storage dependency = dependencyDetails[_dependencyType]; require(dependency.scriptCount > 0, "there are no scripts to remove"); - // purge old contract bytecode contract from the blockchain state - // note: Although this does reduce usage of Ethereum state, it does not - // reduce the gas costs of removal transactions. We believe this is the - // best behavior at the time of writing, and do not expect this to - // result in any breaking changes in the future. All current proposals - // to change the self-destruct opcode are backwards compatible, but may - // result in not removing the bytecode from the blockchain state. This - // implementation is compatible with that architecture, as it does not - // rely on the bytecode being removed from the blockchain state. - dependency - .scriptBytecodeAddresses[dependency.scriptCount - 1] - .purgeBytecode(); - // delete reference to contract address that no longer exists + // delete reference to old storage contract address delete dependency.scriptBytecodeAddresses[dependency.scriptCount - 1]; unchecked { dependency.scriptCount = dependency.scriptCount - 1; @@ -757,7 +734,7 @@ contract DependencyRegistryV0 is return ""; } - return dependency.scriptBytecodeAddresses[_index].readFromBytecode(); + return _readFromBytecode(dependency.scriptBytecodeAddresses[_index]); } /** @@ -851,4 +828,14 @@ contract DependencyRegistryV0 is OwnableUpgradeable._transferOwnership(newOwner); adminACLContract = IAdminACLV0(newOwner); } + + /** + * Helper for calling `BytecodeStorageReader` external library reader method, + * added for bytecode size reduction purposes. + */ + function _readFromBytecode( + address _address + ) internal view returns (string memory) { + return BytecodeStorageReader.readFromBytecode(_address); + } } diff --git a/contracts/GenArt721CoreV3.sol b/contracts/GenArt721CoreV3.sol index 4c907108d..bb3e7b852 100644 --- a/contracts/GenArt721CoreV3.sol +++ b/contracts/GenArt721CoreV3.sol @@ -12,7 +12,7 @@ import "./interfaces/0.8.x/IManifold.sol"; import "@openzeppelin-4.7/contracts/utils/Strings.sol"; import "@openzeppelin-4.7/contracts/access/Ownable.sol"; import "./libs/0.8.x/ERC721_PackedHashSeed.sol"; -import "./libs/0.8.x/BytecodeStorage.sol"; +import "./libs/0.8.x/BytecodeStorageV1.sol"; import "./libs/0.8.x/Bytes32Strings.sol"; /** @@ -93,8 +93,7 @@ contract GenArt721CoreV3 is IGenArt721CoreContractV3, IGenArt721CoreContractExposesHashSeed { - using BytecodeStorage for string; - using BytecodeStorage for address; + using BytecodeStorageWriter for string; using Bytes32Strings for bytes32; using Strings for uint256; uint256 constant ONE_HUNDRED = 100; @@ -242,7 +241,7 @@ contract GenArt721CoreV3 is bool public newProjectsForbidden; /// version & type of this core contract - string public constant coreVersion = "v3.1.0"; + string public constant coreVersion = "v3.2.0"; string public constant coreType = "GenArt721CoreV3"; /// default base URI to initialize all new project projectBaseURI values to @@ -1072,18 +1071,8 @@ contract GenArt721CoreV3 is _onlyNonEmptyString(_script); Project storage project = projects[_projectId]; require(_scriptId < project.scriptCount, "scriptId out of range"); - // purge old contract bytecode contract from the blockchain state - // note: Although this does reduce usage of Ethereum state, it does not - // reduce the gas costs of removal transactions. We believe this is the - // best behavior at the time of writing, and do not expect this to - // result in any breaking changes in the future. All current proposals - // to change the self-destruct opcode are backwards compatible, but may - // result in not removing the bytecode from the blockchain state. This - // implementation is compatible with that architecture, as it does not - // rely on the bytecode being removed from the blockchain state. - project.scriptBytecodeAddresses[_scriptId].purgeBytecode(); // store script in contract bytecode, replacing reference address from - // the contract that no longer exists with the newly created one + // the old storage contract with the newly created one project.scriptBytecodeAddresses[_scriptId] = _script.writeToBytecode(); emit ProjectUpdated(_projectId, FIELD_PROJECT_SCRIPT); } @@ -1100,19 +1089,7 @@ contract GenArt721CoreV3 is ); Project storage project = projects[_projectId]; require(project.scriptCount > 0, "there are no scripts to remove"); - // purge old contract bytecode contract from the blockchain state - // note: Although this does reduce usage of Ethereum state, it does not - // reduce the gas costs of removal transactions. We believe this is the - // best behavior at the time of writing, and do not expect this to - // result in any breaking changes in the future. All current proposals - // to change the self-destruct opcode are backwards compatible, but may - // result in not removing the bytecode from the blockchain state. This - // implementation is compatible with that architecture, as it does not - // rely on the bytecode being removed from the blockchain state. - project - .scriptBytecodeAddresses[project.scriptCount - 1] - .purgeBytecode(); - // delete reference to contract address that no longer exists + // delete reference to old storage contract address delete project.scriptBytecodeAddresses[project.scriptCount - 1]; unchecked { project.scriptCount = project.scriptCount - 1; @@ -1515,7 +1492,7 @@ contract GenArt721CoreV3 is if (_index >= project.scriptCount) { return ""; } - return project.scriptBytecodeAddresses[_index].readFromBytecode(); + return _readFromBytecode(project.scriptBytecodeAddresses[_index]); } /** @@ -1967,4 +1944,14 @@ contract GenArt721CoreV3 is (block.timestamp - projectCompletedTimestamp < FOUR_WEEKS_IN_SECONDS); } + + /** + * Helper for calling `BytecodeStorageReader` external library reader method, + * added for bytecode size reduction purposes. + */ + function _readFromBytecode( + address _address + ) internal view returns (string memory) { + return BytecodeStorageReader.readFromBytecode(_address); + } } diff --git a/contracts/engine/V3/GenArt721CoreV3_Engine.sol b/contracts/engine/V3/GenArt721CoreV3_Engine.sol index c753571d5..f19862cdf 100644 --- a/contracts/engine/V3/GenArt721CoreV3_Engine.sol +++ b/contracts/engine/V3/GenArt721CoreV3_Engine.sol @@ -14,7 +14,7 @@ import "../../interfaces/0.8.x/IManifold.sol"; import "@openzeppelin-4.7/contracts/utils/Strings.sol"; import "@openzeppelin-4.7/contracts/access/Ownable.sol"; import "../../libs/0.8.x/ERC721_PackedHashSeed.sol"; -import "../../libs/0.8.x/BytecodeStorage.sol"; +import "../../libs/0.8.x/BytecodeStorageV1.sol"; import "../../libs/0.8.x/Bytes32Strings.sol"; /** @@ -100,8 +100,7 @@ contract GenArt721CoreV3_Engine is IGenArt721CoreContractV3_Engine, IGenArt721CoreContractExposesHashSeed { - using BytecodeStorage for string; - using BytecodeStorage for address; + using BytecodeStorageWriter for string; using Bytes32Strings for bytes32; using Strings for uint256; using Strings for address; @@ -1134,9 +1133,8 @@ contract GenArt721CoreV3_Engine is _onlyNonEmptyString(_script); Project storage project = projects[_projectId]; require(_scriptId < project.scriptCount, "scriptId out of range"); - // store script in contract bytecode, replacing reference address from - // the contract that no longer exists with the newly created one + // the old storage contract with the newly created one project.scriptBytecodeAddresses[_scriptId] = _script.writeToBytecode(); emit ProjectUpdated(_projectId, FIELD_PROJECT_SCRIPT); } @@ -1153,8 +1151,7 @@ contract GenArt721CoreV3_Engine is ); Project storage project = projects[_projectId]; require(project.scriptCount > 0, "No scripts to remove"); - - // delete reference to contract address that no longer exists + // delete reference to old storage contract address delete project.scriptBytecodeAddresses[project.scriptCount - 1]; unchecked { project.scriptCount = project.scriptCount - 1; @@ -1579,7 +1576,7 @@ contract GenArt721CoreV3_Engine is if (_index >= project.scriptCount) { return ""; } - return project.scriptBytecodeAddresses[_index].readFromBytecode(); + return _readFromBytecode(project.scriptBytecodeAddresses[_index]); } /** @@ -1998,4 +1995,14 @@ contract GenArt721CoreV3_Engine is (block.timestamp - projectCompletedTimestamp < FOUR_WEEKS_IN_SECONDS); } + + /** + * Helper for calling `BytecodeStorageReader` external library reader method, + * added for bytecode size reduction purposes. + */ + function _readFromBytecode( + address _address + ) internal view returns (string memory) { + return BytecodeStorageReader.readFromBytecode(_address); + } } diff --git a/contracts/engine/V3/GenArt721CoreV3_Engine_Flex.sol b/contracts/engine/V3/GenArt721CoreV3_Engine_Flex.sol index 36d0549d2..f88ccb319 100644 --- a/contracts/engine/V3/GenArt721CoreV3_Engine_Flex.sol +++ b/contracts/engine/V3/GenArt721CoreV3_Engine_Flex.sol @@ -13,7 +13,7 @@ import "../../interfaces/0.8.x/IManifold.sol"; import "@openzeppelin-4.7/contracts/access/Ownable.sol"; import "../../libs/0.8.x/ERC721_PackedHashSeed.sol"; -import "../../libs/0.8.x/BytecodeStorage.sol"; +import "../../libs/0.8.x/BytecodeStorageV1.sol"; import "../../libs/0.8.x/Bytes32Strings.sol"; /** @@ -108,8 +108,7 @@ contract GenArt721CoreV3_Engine_Flex is IGenArt721CoreContractV3_Engine_Flex, IGenArt721CoreContractExposesHashSeed { - using BytecodeStorage for string; - using BytecodeStorage for address; + using BytecodeStorageWriter for string; using Bytes32Strings for bytes32; uint256 constant ONE_HUNDRED = 100; uint256 constant ONE_MILLION = 1_000_000; @@ -1320,9 +1319,8 @@ contract GenArt721CoreV3_Engine_Flex is _onlyNonEmptyString(_script); Project storage project = projects[_projectId]; require(_scriptId < project.scriptCount, "scriptId out of range"); - // store script in contract bytecode, replacing reference address from - // the contract that no longer exists with the newly created one + // the old storage contract with the newly created one project.scriptBytecodeAddresses[_scriptId] = _script.writeToBytecode(); emit ProjectUpdated(_projectId, FIELD_PROJECT_SCRIPT); } @@ -1339,8 +1337,7 @@ contract GenArt721CoreV3_Engine_Flex is ); Project storage project = projects[_projectId]; require(project.scriptCount > 0, "No scripts to remove"); - - // delete reference to contract address that no longer exists + // delete reference to old storage contract address delete project.scriptBytecodeAddresses[project.scriptCount - 1]; unchecked { project.scriptCount = project.scriptCount - 1; @@ -1765,7 +1762,7 @@ contract GenArt721CoreV3_Engine_Flex is if (_index >= project.scriptCount) { return ""; } - return project.scriptBytecodeAddresses[_index].readFromBytecode(); + return _readFromBytecode(project.scriptBytecodeAddresses[_index]); } /** @@ -1996,7 +1993,7 @@ contract GenArt721CoreV3_Engine_Flex is bytecodeAddress: _bytecodeAddress, data: (_dependency.dependencyType == ExternalAssetDependencyType.ONCHAIN) - ? _bytecodeAddress.readFromBytecode() + ? _readFromBytecode(_bytecodeAddress) : "" }); } @@ -2219,6 +2216,16 @@ contract GenArt721CoreV3_Engine_Flex is FOUR_WEEKS_IN_SECONDS); } + /** + * Helper for calling `BytecodeStorageReader` external library reader method, + * added for bytecode size reduction purposes. + */ + function _readFromBytecode( + address _address + ) internal view returns (string memory) { + return BytecodeStorageReader.readFromBytecode(_address); + } + // strings library from OpenZeppelin, modified for no constants bytes16 private _HEX_SYMBOLS = "0123456789abcdef"; diff --git a/contracts/engine/V3/forks/GenArt721CoreV3_Engine_Flex_PROOF.sol b/contracts/engine/V3/forks/GenArt721CoreV3_Engine_Flex_PROOF.sol index b28fdb1b2..8db581aac 100644 --- a/contracts/engine/V3/forks/GenArt721CoreV3_Engine_Flex_PROOF.sol +++ b/contracts/engine/V3/forks/GenArt721CoreV3_Engine_Flex_PROOF.sol @@ -12,7 +12,7 @@ import "../../../interfaces/0.8.x/IManifold.sol"; import "@openzeppelin-4.7/contracts/access/Ownable.sol"; import "../../../libs/0.8.x/ERC721_PackedHashSeed.sol"; -import "../../../libs/0.8.x/BytecodeStorage.sol"; +import "../../../libs/0.8.x/BytecodeStorageV1.sol"; import "../../../libs/0.8.x/Bytes32Strings.sol"; /** @@ -107,8 +107,7 @@ contract GenArt721CoreV3_Engine_Flex_PROOF is IManifold, IGenArt721CoreContractV3_Engine_Flex { - using BytecodeStorage for string; - using BytecodeStorage for address; + using BytecodeStorageWriter for string; using Bytes32Strings for bytes32; uint256 constant ONE_HUNDRED = 100; @@ -1773,7 +1772,7 @@ contract GenArt721CoreV3_Engine_Flex_PROOF is if (_index >= project.scriptCount) { return ""; } - return project.scriptBytecodeAddresses[_index].readFromBytecode(); + return _readFromBytecode(project.scriptBytecodeAddresses[_index]); } /** @@ -2004,7 +2003,7 @@ contract GenArt721CoreV3_Engine_Flex_PROOF is bytecodeAddress: _bytecodeAddress, data: (_dependency.dependencyType == ExternalAssetDependencyType.ONCHAIN) - ? _bytecodeAddress.readFromBytecode() + ? _readFromBytecode(_bytecodeAddress) : "" }); } @@ -2227,6 +2226,16 @@ contract GenArt721CoreV3_Engine_Flex_PROOF is FOUR_WEEKS_IN_SECONDS); } + /** + * Helper for calling `BytecodeStorageReader` external library reader method, + * added for bytecode size reduction purposes. + */ + function _readFromBytecode( + address _address + ) internal view returns (string memory) { + return BytecodeStorageReader.readFromBytecode(_address); + } + // strings library from OpenZeppelin, modified for no constants bytes16 private _HEX_SYMBOLS = "0123456789abcdef"; diff --git a/contracts/engine/V3/forks/PROHIBITION/GenArt721CoreV3_Engine_Flex_PROHIBITION.sol b/contracts/engine/V3/forks/PROHIBITION/GenArt721CoreV3_Engine_Flex_PROHIBITION.sol index 7904b4bf6..71244a7c9 100644 --- a/contracts/engine/V3/forks/PROHIBITION/GenArt721CoreV3_Engine_Flex_PROHIBITION.sol +++ b/contracts/engine/V3/forks/PROHIBITION/GenArt721CoreV3_Engine_Flex_PROHIBITION.sol @@ -13,7 +13,7 @@ import "../../../../interfaces/0.8.x/IManifold.sol"; import "@openzeppelin-4.7/contracts/access/Ownable.sol"; import "../../../../libs/0.8.x/ERC721_PackedHashSeed.sol"; -import "../../../../libs/0.8.x/BytecodeStorage.sol"; +import "../../../../libs/0.8.x/BytecodeStorageV1.sol"; import "../../../../libs/0.8.x/Bytes32Strings.sol"; /** @@ -108,8 +108,7 @@ contract GenArt721CoreV3_Engine_Flex_PROHIBITION is IManifold, IGenArt721CoreContractV3_Engine_Flex_PROHIBITION { - using BytecodeStorage for string; - using BytecodeStorage for address; + using BytecodeStorageWriter for string; using Bytes32Strings for bytes32; uint256 constant ONE_HUNDRED = 100; uint256 constant ONE_MILLION = 1_000_000; @@ -1781,7 +1780,7 @@ contract GenArt721CoreV3_Engine_Flex_PROHIBITION is if (_index >= project.scriptCount) { return ""; } - return project.scriptBytecodeAddresses[_index].readFromBytecode(); + return _readFromBytecode(project.scriptBytecodeAddresses[_index]); } /** @@ -2012,7 +2011,7 @@ contract GenArt721CoreV3_Engine_Flex_PROHIBITION is bytecodeAddress: _bytecodeAddress, data: (_dependency.dependencyType == ExternalAssetDependencyType.ONCHAIN) - ? _bytecodeAddress.readFromBytecode() + ? _readFromBytecode(_bytecodeAddress) : "" }); } @@ -2276,6 +2275,16 @@ contract GenArt721CoreV3_Engine_Flex_PROHIBITION is FOUR_WEEKS_IN_SECONDS); } + /** + * Helper for calling `BytecodeStorageReader` external library reader method, + * added for bytecode size reduction purposes. + */ + function _readFromBytecode( + address _address + ) internal view returns (string memory) { + return BytecodeStorageReader.readFromBytecode(_address); + } + // strings library from OpenZeppelin, modified for no constants bytes16 private _HEX_SYMBOLS = "0123456789abcdef"; diff --git a/contracts/explorations/GenArt721CoreV3_Explorations.sol b/contracts/explorations/GenArt721CoreV3_Explorations.sol index 90f4b82fb..ac2197b34 100644 --- a/contracts/explorations/GenArt721CoreV3_Explorations.sol +++ b/contracts/explorations/GenArt721CoreV3_Explorations.sol @@ -12,7 +12,7 @@ import "../interfaces/0.8.x/IManifold.sol"; import "@openzeppelin-4.7/contracts/utils/Strings.sol"; import "@openzeppelin-4.7/contracts/access/Ownable.sol"; import "../libs/0.8.x/ERC721_PackedHashSeed.sol"; -import "../libs/0.8.x/BytecodeStorage.sol"; +import "../libs/0.8.x/BytecodeStorageV1.sol"; import "../libs/0.8.x/Bytes32Strings.sol"; /** @@ -92,8 +92,7 @@ contract GenArt721CoreV3_Explorations is IGenArt721CoreContractV3, IGenArt721CoreContractExposesHashSeed { - using BytecodeStorage for string; - using BytecodeStorage for address; + using BytecodeStorageWriter for string; using Bytes32Strings for bytes32; using Strings for uint256; using Strings for address; @@ -239,7 +238,7 @@ contract GenArt721CoreV3_Explorations is /// version & type of this core contract /// coreVersion is updated from Flagship V3 core due to minor changes /// implemented in the Explorations version of the contract. - string public constant coreVersion = "v3.1.1"; + string public constant coreVersion = "v3.2.1"; /// coreType remains consistent with flagship V3 core because external & /// public functions used for indexing are unchanged. string public constant coreType = "GenArt721CoreV3"; @@ -1078,18 +1077,8 @@ contract GenArt721CoreV3_Explorations is _onlyNonEmptyString(_script); Project storage project = projects[_projectId]; require(_scriptId < project.scriptCount, "scriptId out of range"); - // purge old contract bytecode contract from the blockchain state - // note: Although this does reduce usage of Ethereum state, it does not - // reduce the gas costs of removal transactions. We believe this is the - // best behavior at the time of writing, and do not expect this to - // result in any breaking changes in the future. All current proposals - // to change the self-destruct opcode are backwards compatible, but may - // result in not removing the bytecode from the blockchain state. This - // implementation is compatible with that architecture, as it does not - // rely on the bytecode being removed from the blockchain state. - project.scriptBytecodeAddresses[_scriptId].purgeBytecode(); // store script in contract bytecode, replacing reference address from - // the contract that no longer exists with the newly created one + // the old storage contract with the newly created one project.scriptBytecodeAddresses[_scriptId] = _script.writeToBytecode(); emit ProjectUpdated(_projectId, FIELD_PROJECT_SCRIPT); } @@ -1106,19 +1095,7 @@ contract GenArt721CoreV3_Explorations is ); Project storage project = projects[_projectId]; require(project.scriptCount > 0, "there are no scripts to remove"); - // purge old contract bytecode contract from the blockchain state - // note: Although this does reduce usage of Ethereum state, it does not - // reduce the gas costs of removal transactions. We believe this is the - // best behavior at the time of writing, and do not expect this to - // result in any breaking changes in the future. All current proposals - // to change the self-destruct opcode are backwards compatible, but may - // result in not removing the bytecode from the blockchain state. This - // implementation is compatible with that architecture, as it does not - // rely on the bytecode being removed from the blockchain state. - project - .scriptBytecodeAddresses[project.scriptCount - 1] - .purgeBytecode(); - // delete reference to contract address that no longer exists + // delete reference to old storage contract address delete project.scriptBytecodeAddresses[project.scriptCount - 1]; unchecked { project.scriptCount = project.scriptCount - 1; @@ -1521,7 +1498,7 @@ contract GenArt721CoreV3_Explorations is if (_index >= project.scriptCount) { return ""; } - return project.scriptBytecodeAddresses[_index].readFromBytecode(); + return _readFromBytecode(project.scriptBytecodeAddresses[_index]); } /** @@ -1973,4 +1950,14 @@ contract GenArt721CoreV3_Explorations is (block.timestamp - projectCompletedTimestamp < FOUR_WEEKS_IN_SECONDS); } + + /** + * Helper for calling `BytecodeStorageReader` external library reader method, + * added for bytecode size reduction purposes. + */ + function _readFromBytecode( + address _address + ) internal view returns (string memory) { + return BytecodeStorageReader.readFromBytecode(_address); + } } diff --git a/contracts/libs/0.8.x/BytecodeStorage.sol b/contracts/libs/0.8.x/BytecodeStorageV0.sol similarity index 100% rename from contracts/libs/0.8.x/BytecodeStorage.sol rename to contracts/libs/0.8.x/BytecodeStorageV0.sol diff --git a/contracts/libs/0.8.x/BytecodeStorageV1.sol b/contracts/libs/0.8.x/BytecodeStorageV1.sol new file mode 100644 index 000000000..00f1db6c1 --- /dev/null +++ b/contracts/libs/0.8.x/BytecodeStorageV1.sol @@ -0,0 +1,399 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// Created By: Art Blocks Inc. + +pragma solidity ^0.8.0; + +/** + * @title Art Blocks Script Storage Library + * @notice Utilize contract bytecode as persistent storage for large chunks of script string data. + * This library is intended to have an external deployed copy that is released in the future, + * and, as such, has been designed to support both updated V1 (versioned, with purging removed) + * reads as well as backwards-compatible reads for both a) the unversioned "V0" storage contracts + * which were deployed by the original version of this libary and b) contracts that were deployed + * using one of the SSTORE2 implementations referenced below. + * For these pre-V1 storage contracts (which themselves did not have any explicit versioning semantics) + * backwards-compatible reads are optimistic, and only expected to work for contracts actually + * deployed by the original version of this library – and may fail ungracefully if attempted to be + * used to read from other contracts. + * This library is split into two components, intended to be updated in tandem, and thus included + * here in the same source file. One component is an internal library that is intended to be embedded + * directly into other contracts and provides all _write_ functionality. The other is a public library + * that is intended to be deployed as a standalone contract and provides all _read_ functionality. + * + * @author Art Blocks Inc. + * @author Modified from 0xSequence (https://github.com/0xsequence/sstore2/blob/master/contracts/SSTORE2.sol) + * @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/SSTORE2.sol) + * + * @dev Compared to the above two rerferenced libraries, this contracts-as-storage implementation makes a few + * notably different design decisions: + * - uses the `string` data type for input/output on reads, rather than speaking in bytes directly + * - stores the "writer" address (library user) in the deployed contract bytes, which is useful for + * on-chain introspection and provenance purposes + * - stores a very simple versioning string in the deployed contract bytes, which captures the version + * of the library that was used to deploy the storage contract and useful for supporting future + * compatibility management as this library evolves (e.g. in response to EOF v1 migration plans) + * Also, given that much of this library is written in assembly, this library makes use of a slightly + * different convention (when compared to the rest of the Art Blocks smart contract repo) around + * pre-defining return values in some cases in order to simplify need to directly memory manage these + * return values. + */ + +/** + * @title Art Blocks Script Storage Library (Public, Reads) + * @author Art Blocks Inc. + * @notice The public library for reading from storage contracts. This library is intended to be deployed as a + * standalone contract, and provides all _read_ functionality. + */ +library BytecodeStorageReader { + // Define the set of known valid version strings that may be stored in the deployed storage contract bytecode + // note: These are all intentionally exactly 32-bytes and are null-terminated. Null-termination is used due + // to this being the standard expected formatting in common web3 tooling such as ethers.js. Please see + // the following for additional context: https://docs.ethers.org/v5/api/utils/strings/#Bytes32String + // Used for storage contracts that were deployed by an unknown source + bytes32 public constant UNKNOWN_VERSION_STRING = + "UNKNOWN_VERSION_STRING_________ "; + // Pre-dates versioning string, so this doesn't actually exist in any deployed contracts, + // but is useful for backwards-compatible semantics with original version of this library + bytes32 public constant V0_VERSION_STRING = + "BytecodeStorage_V0.0.0_________ "; + // The first versioned storage contract, deployed by an updated version of this library + bytes32 public constant V1_VERSION_STRING = + "BytecodeStorage_V1.0.0_________ "; + // The current version of this library. + bytes32 public constant CURRENT_VERSION = V1_VERSION_STRING; + + //---------------------------------------------------------------------------------------------------------------// + // Starting Index | Size | Ending Index | Description // + //---------------------------------------------------------------------------------------------------------------// + // 0 | N/A | 0 | // + // 0 | 1 | 1 | single byte opcode for making the storage contract non-executable // + // 1 | 32 | 33 | the 32 byte slot used for storing a basic versioning string // + // 33 | 32 | 65 | the 32 bytes for storing the deploying contract's (0-padded) address // + //---------------------------------------------------------------------------------------------------------------// + // Define the offset for where the "meta bytes" end, and the "data bytes" begin. Note that this is a manually + // calculated value, and must be updated if the above table is changed. It is expected that tests will fail + // loudly if these values are not updated in-step with eachother. + uint256 private constant VERSION_OFFSET = 1; + uint256 private constant ADDRESS_OFFSET = 33; + uint256 private constant DATA_OFFSET = 65; + + // Define the set of known *historic* offset values for where the "meta bytes" end, and the "data bytes" begin. + // SSTORE2 deployed storage contracts take the general format of: + // concat(0x00, data) + // note: this is true for both variants of the SSTORE2 library + uint256 private constant SSTORE2_DATA_OFFSET = 1; + // V0 deployed storage contracts take the general format of: + // concat(gated-cleanup-logic, deployer-address, data) + uint256 private constant V0_ADDRESS_OFFSET = 72; + uint256 private constant V0_DATA_OFFSET = 104; + // V1 deployed storage contracts take the general format of: + // concat(invalid opcode, version, deployer-address, data) + uint256 private constant V1_ADDRESS_OFFSET = ADDRESS_OFFSET; + uint256 private constant V1_DATA_OFFSET = DATA_OFFSET; + + /*////////////////////////////////////////////////////////////// + READ LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Read a string from contract bytecode + * @param _address address of deployed contract with bytecode stored in the V0 or V1 format + * @return data string read from contract bytecode + * @dev This function performs input validation that the contract to read is in an expected format + */ + function readFromBytecode( + address _address + ) public view returns (string memory data) { + uint256 dataOffset = _bytecodeDataOffsetAt(_address); + return string(readBytesFromBytecode(_address, dataOffset)); + } + + /** + * @notice Read the bytes from contract bytecode that was written to the EVM using SSTORE2 + * @param _address address of deployed contract with bytecode stored in the SSTORE2 format + * @return data bytes read from contract bytecode + * @dev This function performs no input validation on the provided contract, + * other than that there is content to read (but not that its a "storage contract") + */ + function readBytesFromSSTORE2Bytecode( + address _address + ) public view returns (bytes memory data) { + return readBytesFromBytecode(_address, SSTORE2_DATA_OFFSET); + } + + /** + * @notice Read the bytes from contract bytecode, with an explicitly provided starting offset + * @param _address address of deployed contract with bytecode stored in the V0 or V1 format + * @param _offset offset to read from in contract bytecode, explicitly provided (not calculated) + * @return data bytes read from contract bytecode + * @dev This function performs no input validation on the provided contract, + * other than that there is content to read (but not that its a "storage contract") + */ + function readBytesFromBytecode( + address _address, + uint256 _offset + ) public view returns (bytes memory data) { + // get the size of the bytecode + uint256 bytecodeSize = _bytecodeSizeAt(_address); + // handle case where address contains code < _offset + if (bytecodeSize < _offset) { + revert("ContractAsStorage: Read Error"); + } + + // handle case where address contains code >= dataOffset + // decrement by dataOffset to account for header info + uint256 size; + unchecked { + size = bytecodeSize - _offset; + } + + assembly { + // allocate free memory + data := mload(0x40) + // update free memory pointer + // use and(x, not(0x1f) as cheaper equivalent to sub(x, mod(x, 0x20)). + // adding 0x1f to size + logic above ensures the free memory pointer + // remains word-aligned, following the Solidity convention. + mstore(0x40, add(data, and(add(add(size, 0x20), 0x1f), not(0x1f)))) + // store length of data in first 32 bytes + mstore(data, size) + // copy code to memory, excluding the deployer-address + extcodecopy(_address, add(data, 0x20), _offset, size) + } + } + + /** + * @notice Get address for deployer for given contract bytecode + * @param _address address of deployed contract with bytecode stored in the V0 or V1 format + * @return writerAddress address read from contract bytecode + */ + function getWriterAddressForBytecode( + address _address + ) public view returns (address) { + // get the size of the data + uint256 bytecodeSize = _bytecodeSizeAt(_address); + // the dataOffset for the bytecode + uint256 addressOffset = _bytecodeAddressOffsetAt(_address); + // handle case where address contains code < addressOffset + 32 (address takes a whole slot) + if (bytecodeSize < (addressOffset + 32)) { + revert("ContractAsStorage: Read Error"); + } + + assembly { + // allocate free memory + let writerAddress := mload(0x40) + // shift free memory pointer by one slot + mstore(0x40, add(mload(0x40), 0x20)) + // copy the 32-byte address of the data contract writer to memory + // note: this relies on the assumption noted at the top-level of + // this file that the storage layout for the deployed + // contracts-as-storage contract looks like:: + // | invalid opcode | version-string (unless v0) | deployer-address (padded) | data | + extcodecopy( + _address, + writerAddress, + addressOffset, + 0x20 // full 32-bytes, as address is expected to be zero-padded + ) + return( + writerAddress, + 0x20 // return size is entire slot, as it is zero-padded + ) + } + } + + /** + * @notice Get version for given contract bytecode + * @param _address address of deployed contract with bytecode stored in the V0 or V1 format + * @return version version read from contract bytecode + */ + function getLibraryVersionForBytecode( + address _address + ) public view returns (bytes32) { + return _bytecodeVersionAt(_address); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL HELPER LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Returns the size of the bytecode at address `_address` + * @param _address address that may or may not contain bytecode + * @return size size of the bytecode code at `_address` + */ + function _bytecodeSizeAt( + address _address + ) private view returns (uint256 size) { + assembly { + size := extcodesize(_address) + } + if (size == 0) { + revert("ContractAsStorage: Read Error"); + } + } + + /** + * @notice Returns the offset of the data in the bytecode at address `_address` + * @param _address address that may or may not contain bytecode + * @return dataOffset offset of data in bytecode if a known version, otherwise 0 + */ + function _bytecodeDataOffsetAt( + address _address + ) private view returns (uint256 dataOffset) { + bytes32 version = _bytecodeVersionAt(_address); + if (version == V1_VERSION_STRING) { + dataOffset = V1_DATA_OFFSET; + } else if (version == V0_VERSION_STRING) { + dataOffset = V0_DATA_OFFSET; + } else { + // unknown version, revert + revert("ContractAsStorage: Unsupported Version"); + } + } + + /** + * @notice Returns the offset of the address in the bytecode at address `_address` + * @param _address address that may or may not contain bytecode + * @return addressOffset offset of address in bytecode if a known version, otherwise 0 + */ + function _bytecodeAddressOffsetAt( + address _address + ) private view returns (uint256 addressOffset) { + bytes32 version = _bytecodeVersionAt(_address); + if (version == V1_VERSION_STRING) { + addressOffset = V1_ADDRESS_OFFSET; + } else if (version == V0_VERSION_STRING) { + addressOffset = V0_ADDRESS_OFFSET; + } else { + // unknown version, revert + revert("ContractAsStorage: Unsupported Version"); + } + } + + /** + * @notice Get version string for given contract bytecode + * @param _address address of deployed contract with bytecode stored in the V0 or V1 format + * @return version version string read from contract bytecode + */ + function _bytecodeVersionAt( + address _address + ) private view returns (bytes32 version) { + // get the size of the data + uint256 bytecodeSize = _bytecodeSizeAt(_address); + // handle case where address contains code < minimum expected version string size, + // by returning early with the unknown version string + if (bytecodeSize < (VERSION_OFFSET + 32)) { + return UNKNOWN_VERSION_STRING; + } + + assembly { + // allocate free memory + let versionString := mload(0x40) + // shift free memory pointer by one slot + mstore(0x40, add(mload(0x40), 0x20)) + // copy the 32-byte version string of the bytecode library to memory + // note: this relies on the assumption noted at the top-level of + // this file that the storage layout for the deployed + // contracts-as-storage contract looks like: + // | invalid opcode | version-string (unless v0) | deployer-address (padded) | data | + extcodecopy( + _address, + versionString, + VERSION_OFFSET, + 0x20 // 32-byte version string + ) + // note: must check against literal strings, as Yul does not allow for + // dynamic strings in switch statements. + switch mload(versionString) + case "BytecodeStorage_V1.0.0_________ " { + version := V1_VERSION_STRING + } + case 0x2060486000396000513314601057fe5b60013614601957fe5b6000357fff0000 { + // the v0 variant of this library pre-dates formal versioning w/ version strings, + // so we check the first 32 bytes of the execution bytecode itself which + // is static and known across all storage contracts deployed with the first version + // of this library. + version := V0_VERSION_STRING + } + default { + version := UNKNOWN_VERSION_STRING + } + } + } +} + +/** + * @title Art Blocks Script Storage Library (Internal, Writes) + * @author Art Blocks Inc. + * @notice The internal library for writing to storage contracts. This library is intended to be deployed + * within library client contracts that use this library to perform _write_ operations on storage. + */ +library BytecodeStorageWriter { + /*////////////////////////////////////////////////////////////// + WRITE LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Write a string to contract bytecode + * @param _data string to be written to contract. No input validation is performed on this parameter. + * @param address_ address of deployed contract with bytecode stored in the V0 or V1 format + */ + function writeToBytecode( + string memory _data + ) internal returns (address address_) { + // prefix bytecode with + bytes memory creationCode = abi.encodePacked( + //---------------------------------------------------------------------------------------------------------------// + // Opcode | Opcode + Arguments | Description | Stack View // + //---------------------------------------------------------------------------------------------------------------// + // a.) creation code returns all code in the contract except for the first 11 (0B in hex) bytes, as these 11 + // bytes are the creation code itself which we do not want to store in the deployed storage contract result + //---------------------------------------------------------------------------------------------------------------// + // 0x60 | 0x60_0B | PUSH1 11 | codeOffset // + // 0x59 | 0x59 | MSIZE | 0 codeOffset // + // 0x81 | 0x81 | DUP2 | codeOffset 0 codeOffset // + // 0x38 | 0x38 | CODESIZE | codeSize codeOffset 0 codeOffset // + // 0x03 | 0x03 | SUB | (codeSize - codeOffset) 0 codeOffset // + // 0x80 | 0x80 | DUP | (codeSize - codeOffset) (codeSize - codeOffset) 0 codeOffset // + // 0x92 | 0x92 | SWAP3 | codeOffset (codeSize - codeOffset) 0 (codeSize - codeOffset) // + // 0x59 | 0x59 | MSIZE | 0 codeOffset (codeSize - codeOffset) 0 (codeSize - codeOffset) // + // 0x39 | 0x39 | CODECOPY | 0 (codeSize - codeOffset) // + // 0xF3 | 0xF3 | RETURN | // + //---------------------------------------------------------------------------------------------------------------// + // (11 bytes) + hex"60_0B_59_81_38_03_80_92_59_39_F3", + //---------------------------------------------------------------------------------------------------------------// + // b.) ensure that the deployed storage contract is non-executeable (first opcode is the `invalid` opcode) + //---------------------------------------------------------------------------------------------------------------// + //---------------------------------------------------------------------------------------------------------------// + // 0xFE | 0xFE | INVALID | // + //---------------------------------------------------------------------------------------------------------------// + // (1 byte) + hex"FE", + //---------------------------------------------------------------------------------------------------------------// + // c.) store the version string, which is already represented as a 32-byte value + //---------------------------------------------------------------------------------------------------------------// + // (32 bytes) + BytecodeStorageReader.CURRENT_VERSION, + //---------------------------------------------------------------------------------------------------------------// + // d.) store the deploying-contract's address with 0-padding to fit a 20-byte address into a 32-byte slot + //---------------------------------------------------------------------------------------------------------------// + // (12 bytes) + hex"00_00_00_00_00_00_00_00_00_00_00_00", + // (20 bytes) + address(this), + // uploaded data (stored as bytecode) comes last + _data + ); + + assembly { + // deploy a new contract with the generated creation code. + // start 32 bytes into creationCode to avoid copying the byte length. + address_ := create(0, add(creationCode, 0x20), mload(creationCode)) + } + + // address must be non-zero if contract was deployed successfully + require(address_ != address(0), "ContractAsStorage: Write Error"); + } +} diff --git a/contracts/libs/0.8.x/SSTORE2.sol b/contracts/libs/0.8.x/SSTORE2.sol new file mode 100644 index 000000000..e51dc7169 --- /dev/null +++ b/contracts/libs/0.8.x/SSTORE2.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +/// @notice Read and write to persistent storage at a fraction of the cost. +/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/SSTORE2.sol) +/// @author Modified from 0xSequence (https://github.com/0xSequence/sstore2/blob/master/contracts/SSTORE2.sol) +library SSTORE2 { + uint256 internal constant DATA_OFFSET = 1; // We skip the first byte as it's a STOP opcode to ensure the contract can't be called. + + /*////////////////////////////////////////////////////////////// + WRITE LOGIC + //////////////////////////////////////////////////////////////*/ + + function write(bytes memory data) internal returns (address pointer) { + // Prefix the bytecode with a STOP opcode to ensure it cannot be called. + bytes memory runtimeCode = abi.encodePacked(hex"00", data); + + bytes memory creationCode = abi.encodePacked( + //---------------------------------------------------------------------------------------------------------------// + // Opcode | Opcode + Arguments | Description | Stack View // + //---------------------------------------------------------------------------------------------------------------// + // 0x60 | 0x600B | PUSH1 11 | codeOffset // + // 0x59 | 0x59 | MSIZE | 0 codeOffset // + // 0x81 | 0x81 | DUP2 | codeOffset 0 codeOffset // + // 0x38 | 0x38 | CODESIZE | codeSize codeOffset 0 codeOffset // + // 0x03 | 0x03 | SUB | (codeSize - codeOffset) 0 codeOffset // + // 0x80 | 0x80 | DUP | (codeSize - codeOffset) (codeSize - codeOffset) 0 codeOffset // + // 0x92 | 0x92 | SWAP3 | codeOffset (codeSize - codeOffset) 0 (codeSize - codeOffset) // + // 0x59 | 0x59 | MSIZE | 0 codeOffset (codeSize - codeOffset) 0 (codeSize - codeOffset) // + // 0x39 | 0x39 | CODECOPY | 0 (codeSize - codeOffset) // + // 0xf3 | 0xf3 | RETURN | // + //---------------------------------------------------------------------------------------------------------------// + hex"60_0B_59_81_38_03_80_92_59_39_F3", // Returns all code in the contract except for the first 11 (0B in hex) bytes. + runtimeCode // The bytecode we want the contract to have after deployment. Capped at 1 byte less than the code size limit. + ); + + /// @solidity memory-safe-assembly + assembly { + // Deploy a new contract with the generated creation code. + // We start 32 bytes into the code to avoid copying the byte length. + pointer := create(0, add(creationCode, 32), mload(creationCode)) + } + + require(pointer != address(0), "DEPLOYMENT_FAILED"); + } + + /*////////////////////////////////////////////////////////////// + READ LOGIC + //////////////////////////////////////////////////////////////*/ + + function read(address pointer) internal view returns (bytes memory) { + return + readBytecode( + pointer, + DATA_OFFSET, + pointer.code.length - DATA_OFFSET + ); + } + + function read( + address pointer, + uint256 start + ) internal view returns (bytes memory) { + start += DATA_OFFSET; + + return readBytecode(pointer, start, pointer.code.length - start); + } + + function read( + address pointer, + uint256 start, + uint256 end + ) internal view returns (bytes memory) { + start += DATA_OFFSET; + end += DATA_OFFSET; + + require(pointer.code.length >= end, "OUT_OF_BOUNDS"); + + return readBytecode(pointer, start, end - start); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL HELPER LOGIC + //////////////////////////////////////////////////////////////*/ + + function readBytecode( + address pointer, + uint256 start, + uint256 size + ) private view returns (bytes memory data) { + /// @solidity memory-safe-assembly + assembly { + // Get a pointer to some free memory. + data := mload(0x40) + + // Update the free memory pointer to prevent overriding our data. + // We use and(x, not(31)) as a cheaper equivalent to sub(x, mod(x, 32)). + // Adding 31 to size and running the result through the logic above ensures + // the memory pointer remains word-aligned, following the Solidity convention. + mstore(0x40, add(data, and(add(add(size, 32), 31), not(31)))) + + // Store the size of the data in the first 32 byte chunk of free memory. + mstore(data, size) + + // Copy the code into memory right after the 32 bytes we used to store the size. + extcodecopy(pointer, add(data, 32), start, size) + } + } +} diff --git a/contracts/mock/BytecodeTextCR_DMock.sol b/contracts/mock/BytecodeV0TextCR_DMock.sol similarity index 97% rename from contracts/mock/BytecodeTextCR_DMock.sol rename to contracts/mock/BytecodeV0TextCR_DMock.sol index 18722bcff..3a4745b67 100644 --- a/contracts/mock/BytecodeTextCR_DMock.sol +++ b/contracts/mock/BytecodeV0TextCR_DMock.sol @@ -3,17 +3,17 @@ pragma solidity 0.8.17; // Created By: Art Blocks Inc. -import "../libs/0.8.x/BytecodeStorage.sol"; +import "../libs/0.8.x/BytecodeStorageV0.sol"; /** * @title Art Blocks BytecodeTextCR_DMock. * @author Art Blocks Inc. - * @notice This contract serves as a mock client of the BytecodeStorage library + * @notice This contract serves as a mock client of the BytecodeStorageV0 library * to allow for more granular testing of this library than is supported * by the usage of the library directly by specific current Art Blocks * client smart contracts, such as the `GenArt721CoreV3` contract. This * mock exposes the CR_D (create, read, _not updates_, delete) - * operations that the underlying BytecodeStorage library allows clients + * operations that the underlying BytecodeStorageV0 library allows clients * to support. * Note that because updates can only functionally be performed by * combining a "delete" and "create" operation, given that deployed @@ -21,7 +21,7 @@ import "../libs/0.8.x/BytecodeStorage.sol"; * not expose updates (the usual "U" in "CRUD") as they are not * supported by the underlying library. */ -contract BytecodeTextCR_DMock { +contract BytecodeV0TextCR_DMock { using BytecodeStorage for string; using BytecodeStorage for address; diff --git a/contracts/mock/BytecodeV1TextCR_DMock.sol b/contracts/mock/BytecodeV1TextCR_DMock.sol new file mode 100644 index 000000000..0fa24df3e --- /dev/null +++ b/contracts/mock/BytecodeV1TextCR_DMock.sol @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.17; + +// Created By: Art Blocks Inc. + +import "../libs/0.8.x/BytecodeStorageV1.sol"; + +/** + * @title Art Blocks BytecodeTextCR_DMock. + * @author Art Blocks Inc. + * @notice This contract serves as a mock client of the BytecodeStorageV1 library + * to allow for more granular testing of this library than is supported + * by the usage of the library directly by specific current Art Blocks + * client smart contracts, such as the `GenArt721CoreV3` contract. This + * mock exposes the CR_D (create, read, _not updates_, delete) + * operations that the underlying BytecodeStorageV1 library allows clients + * to support. + * Note that because updates can only functionally be performed by + * combining a "delete" and "create" operation, given that deployed + * contracts cannot be _updated_ directly in chain-state, this mock does + * not expose updates (the usual "U" in "CRUD") as they are not + * supported by the underlying library. + */ +contract BytecodeV1TextCR_DMock { + using BytecodeStorageWriter for string; + + // monotonically increasing slot counter and associated slot-storage mapping + uint256 public nextTextSlotId = 0; + mapping(uint256 => address) public storedTextBytecodeAddresses; + + // save deployer address to support basic ACL checks for non-read operations + address public deployerAddress; + + modifier onlyDeployer() { + require(msg.sender == deployerAddress, "Only deployer"); + _; + } + + /** + * @notice Initializes contract. + */ + constructor() { + deployerAddress = msg.sender; + } + + /*////////////////////////////////////////////////////////////// + Create Read _ Delete + //////////////////////////////////////////////////////////////*/ + + /** + * @notice "Create": Adds a chunk of text to be stored to chain-state. + * @param _text Text to be created in chain-state. + * @return uint256 Slot that the written bytecode contract address was + * stored in. + * @dev Intentionally do not perform input validation, instead allowing + * the underlying BytecodeStorage lib to throw errors where applicable. + */ + function createText( + string memory _text + ) external onlyDeployer returns (uint256) { + // store text in contract bytecode + storedTextBytecodeAddresses[nextTextSlotId] = _text.writeToBytecode(); + // record written slot before incrementing + uint256 textSlotId = nextTextSlotId; + nextTextSlotId++; + return textSlotId; + } + + /** + * @notice "Read": Reads chunk of text currently in the provided slot, from + * chain-state. + * @param _textSlotId Slot (associated with this contract) for which to + * read text content. + * @return string Content read from contract bytecode in the given slot. + * @dev Intentionally do not perform input validation, instead allowing + * the underlying BytecodeStorage lib to throw errors where applicable. + */ + function readText(uint256 _textSlotId) public view returns (string memory) { + return + BytecodeStorageReader.readFromBytecode( + storedTextBytecodeAddresses[_textSlotId] + ); + } + + /** + * @notice "Delete": Deletes chunk of text currently in the provided slot, + * from chain-state. + * @param _textSlotId Slot (associated with this contract) for which to + * delete text content. + * @dev Intentionally do not perform input validation, instead allowing + * the underlying BytecodeStorage lib to throw errors where applicable. + */ + function deleteText(uint256 _textSlotId) external onlyDeployer { + // delete reference to old storage contract address + delete storedTextBytecodeAddresses[_textSlotId]; + } + + /*////////////////////////////////////////////////////////////// + Additional Introspection + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Allows additional read introspection, to read a chunk of text, + * from chain-state that lives at a given deployed address. + * @param _bytecodeAddress address from which to read text content. + * @return string Content read from contract bytecode at the given address. + * @dev Intentionally do not perform input validation, instead allowing + * the underlying BytecodeStorage lib to throw errors where applicable. + */ + function readTextAtAddress( + address _bytecodeAddress + ) public view returns (string memory) { + return BytecodeStorageReader.readFromBytecode(_bytecodeAddress); + } + + /** + * @notice Allows additional read introspection, to read a chunk of text, + * from chain-state that lives at a given deployed address with an + * explicitly provided `_offset`. + * @param _bytecodeAddress address from which to read text content. + * @param _offset Offset to read from in contract bytecode, + * explicitly provided (not calculated) + * @return string Content read from contract bytecode at the given address. + * @dev Intentionally do not perform input validation, instead allowing + * the underlying BytecodeStorage lib to throw errors where applicable. + */ + function forceReadTextAtAddress( + address _bytecodeAddress, + uint256 _offset + ) public view returns (string memory) { + return + string( + BytecodeStorageReader.readBytesFromBytecode( + _bytecodeAddress, + _offset + ) + ); + } + + /** + * @notice Allows additional read introspection, to read a chunk of text, + * from chain-state that lives at a given deployed address that + * was written with SSTORE2. + * @param _bytecodeAddress address from which to read text content. + * @return string Content read from contract bytecode at the given address. + * @dev Intentionally do not perform input validation, instead allowing + * the underlying BytecodeStorage lib to throw errors where applicable. + */ + function readSSTORE2TextAtAddress( + address _bytecodeAddress + ) public view returns (string memory) { + return + string( + BytecodeStorageReader.readBytesFromSSTORE2Bytecode( + _bytecodeAddress + ) + ); + } + + /** + * @notice Allows introspection of who deployed a given contracts-as-storage + * contract, based on a provided `_bytecodeAddress`. + * @param _bytecodeAddress address for which to read the author address. + * @return address of the author who wrote the data contained in the + * given `_bytecodeAddress` contract. + * @dev Intentionally do not perform input validation, instead allowing + * the underlying BytecodeStorage lib to throw errors where applicable. + */ + function readAuthorForTextAtAddress( + address _bytecodeAddress + ) public view returns (address) { + return + BytecodeStorageReader.getWriterAddressForBytecode(_bytecodeAddress); + } + + /** + * @notice Allows introspection of the version of a given contracts-as-storage + * contract, based on a provided `_bytecodeAddress`. + * @param _bytecodeAddress address for which to read the version. + * @return bytes32 version of the version string contained in the given `_bytecodeAddress` + * contract. + * @dev Intentionally do not perform input validation, instead allowing + * the underlying BytecodeStorage lib to throw errors where applicable. + */ + function readLibraryVersionForTextAtAddress( + address _bytecodeAddress + ) public view returns (bytes32) { + return + BytecodeStorageReader.getLibraryVersionForBytecode( + _bytecodeAddress + ); + } + + /** + * @notice Allows additional internal purge-logic introspection, by allowing + * for the sending of arbitrary data from this contract to the + * provided existing `_bytecodeAddress`. + * @param _bytecodeAddress address for which to send call data. + * @param _data aribtrary data to send as call-data for raw `.call`. + * @dev WARNING - THIS IS NOT SECURE AND SHOULD NOT BE USED IN PRODUCTION. + */ + function callWithNonsenseData( + address _bytecodeAddress, + bytes memory _data + ) external onlyDeployer { + (bool success /* `data` not needed */, ) = _bytecodeAddress.call(_data); + if (success) { + // WARNING - This implementation does not make use of the low-level + // call return result indicating success/failure. This is + // contrary to best practice but is OK in this instance as + // this method IS NOT SECURE AND SHOULD NOT BE USED IN + // PRODUCTION under any normal circumstances. + } + } + + /** + * @notice Allows additional internal purge-logic introspection, by allowing + * for the sending of raw calls (with no data) from this contract to + * the provided existing `_bytecodeAddress`. + * @param _bytecodeAddress address for which to send call data. + * @dev WARNING - THIS IS NOT SECURE AND SHOULD NOT BE USED IN PRODUCTION. + */ + function callWithoutData(address _bytecodeAddress) external onlyDeployer { + (bool success /* `data` not needed */, ) = _bytecodeAddress.call(""); + if (success) { + // WARNING - This implementation does not make use of the low-level + // call return result indicating success/failure. This is + // contrary to best practice but is OK in this instance as + // this method IS NOT SECURE AND SHOULD NOT BE USED IN + // PRODUCTION under any normal circumstances. + } + } +} diff --git a/contracts/mock/GenArt721CoreV3_Engine_IncorrectCoreType.sol b/contracts/mock/GenArt721CoreV3_Engine_IncorrectCoreType.sol index ba588a307..59185823a 100644 --- a/contracts/mock/GenArt721CoreV3_Engine_IncorrectCoreType.sol +++ b/contracts/mock/GenArt721CoreV3_Engine_IncorrectCoreType.sol @@ -13,7 +13,7 @@ import "../interfaces/0.8.x/IManifold.sol"; import "@openzeppelin-4.7/contracts/utils/Strings.sol"; import "@openzeppelin-4.7/contracts/access/Ownable.sol"; import "../libs/0.8.x/ERC721_PackedHashSeed.sol"; -import "../libs/0.8.x/BytecodeStorage.sol"; +import "../libs/0.8.x/BytecodeStorageV1.sol"; import "../libs/0.8.x/Bytes32Strings.sol"; /** @@ -30,8 +30,7 @@ contract GenArt721CoreV3_Engine_IncorrectCoreType is IManifold // INTERFACE CONFORMANCE INTENTIONALLY OMITTED TO ENABLE MOCKING BUGGED CONTRACT { - using BytecodeStorage for string; - using BytecodeStorage for address; + using BytecodeStorageWriter for string; using Bytes32Strings for bytes32; using Strings for uint256; using Strings for address; @@ -1115,16 +1114,6 @@ contract GenArt721CoreV3_Engine_IncorrectCoreType is { Project storage project = projects[_projectId]; require(_scriptId < project.scriptCount, "scriptId out of range"); - // purge old contract bytecode contract from the blockchain state - // note: Although this does reduce usage of Ethereum state, it does not - // reduce the gas costs of removal transactions. We believe this is the - // best behavior at the time of writing, and do not expect this to - // result in any breaking changes in the future. All current proposals - // to change the self-destruct opcode are backwards compatible, but may - // result in not removing the bytecode from the blockchain state. This - // implementation is compatible with that architecture, as it does not - // rely on the bytecode being removed from the blockchain state. - project.scriptBytecodeAddresses[_scriptId].purgeBytecode(); // store script in contract bytecode, replacing reference address from // the contract that no longer exists with the newly created one project.scriptBytecodeAddresses[_scriptId] = _script.writeToBytecode(); @@ -1144,18 +1133,6 @@ contract GenArt721CoreV3_Engine_IncorrectCoreType is { Project storage project = projects[_projectId]; require(project.scriptCount > 0, "there are no scripts to remove"); - // purge old contract bytecode contract from the blockchain state - // note: Although this does reduce usage of Ethereum state, it does not - // reduce the gas costs of removal transactions. We believe this is the - // best behavior at the time of writing, and do not expect this to - // result in any breaking changes in the future. All current proposals - // to change the self-destruct opcode are backwards compatible, but may - // result in not removing the bytecode from the blockchain state. This - // implementation is compatible with that architecture, as it does not - // rely on the bytecode being removed from the blockchain state. - project - .scriptBytecodeAddresses[project.scriptCount - 1] - .purgeBytecode(); // delete reference to contract address that no longer exists delete project.scriptBytecodeAddresses[project.scriptCount - 1]; unchecked { @@ -1580,7 +1557,7 @@ contract GenArt721CoreV3_Engine_IncorrectCoreType is if (_index >= project.scriptCount) { return ""; } - return project.scriptBytecodeAddresses[_index].readFromBytecode(); + return _readFromBytecode(project.scriptBytecodeAddresses[_index]); } /** @@ -1976,4 +1953,14 @@ contract GenArt721CoreV3_Engine_IncorrectCoreType is (block.timestamp - projectCompletedTimestamp < FOUR_WEEKS_IN_SECONDS); } + + /** + * Helper for calling `BytecodeStorageReader` external library reader method, + * added for bytecode size reduction purposes. + */ + function _readFromBytecode( + address _address + ) internal view returns (string memory) { + return BytecodeStorageReader.readFromBytecode(_address); + } } diff --git a/contracts/mock/SSTORE2Mock.sol b/contracts/mock/SSTORE2Mock.sol new file mode 100644 index 000000000..3192912d8 --- /dev/null +++ b/contracts/mock/SSTORE2Mock.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.17; + +// Created By: Art Blocks Inc. + +import "../libs/0.8.x/SSTORE2.sol"; + +/** + * @title Art Blocks SSTORE2Mock. + * @author Art Blocks Inc. + * @notice This contract serves as a mock client of the SSTORE2 library + * to allow for more testing of this library in the context of + * backwards-compatible reads with the Art Blocks BytecodeStorage library + * @dev For the purposes of our backwards-compatibility testing, the two different + * variations of the SSTORE2 library are functionally equivalent, so we just test + * against the one that is more widely tracked on Github for simplicity, but the + * same tests could be run against the other variation of the library as well + * and would be expected to "just work" given both use the same data offset of + * a single 0x00 "stop byte". + */ +contract SSTORE2Mock { + using SSTORE2 for bytes; + using SSTORE2 for address; + + // monotonically increasing slot counter and associated slot-storage mapping + uint256 public nextTextSlotId = 0; + mapping(uint256 => address) public storedTextBytecodeAddresses; + + // save deployer address to support basic ACL checks for non-read operations + address public deployerAddress; + + modifier onlyDeployer() { + require(msg.sender == deployerAddress, "Only deployer"); + _; + } + + /** + * @notice Initializes contract. + */ + constructor() { + deployerAddress = msg.sender; + } + + /*////////////////////////////////////////////////////////////// + Create + Read + //////////////////////////////////////////////////////////////*/ + + /** + * @notice "Create": Adds a chunk of text to be stored to chain-state. + * @param _text Text to be created in chain-state. + * @return uint256 Slot that the written bytecode contract address was + * stored in. + * @dev Intentionally do not perform input validation, instead allowing + * the underlying BytecodeStorage lib to throw errors where applicable. + */ + function createText( + string memory _text + ) external onlyDeployer returns (uint256) { + // store text in contract bytecode + storedTextBytecodeAddresses[nextTextSlotId] = bytes(_text).write(); + // record written slot before incrementing + uint256 textSlotId = nextTextSlotId; + nextTextSlotId++; + return textSlotId; + } + + /** + * @notice "Read": Reads chunk of text currently in the provided slot, from + * chain-state. + * @param _textSlotId Slot (associated with this contract) for which to + * read text content. + * @return string Content read from contract bytecode in the given slot. + * @dev Intentionally do not perform input validation, instead allowing + * the underlying BytecodeStorage lib to throw errors where applicable. + */ + function readText(uint256 _textSlotId) public view returns (string memory) { + return string(storedTextBytecodeAddresses[_textSlotId].read()); + } + + /** + * @notice Allows additional read introspection, to read a chunk of text, + * from chain-state that lives at a given deployed address. + * @param _bytecodeAddress address from which to read text content. + * @return string Content read from contract bytecode at the given address. + * @dev Intentionally do not perform input validation, instead allowing + * the underlying BytecodeStorage lib to throw errors where applicable. + */ + function readTextAtAddress( + address _bytecodeAddress + ) public view returns (string memory) { + return string(_bytecodeAddress.read()); + } +} diff --git a/deployments/engine/V3/internal-testing/staging-v3-react-demo-2/DEPLOYMENTS.md b/deployments/engine/V3/internal-testing/staging-v3-react-demo-2/DEPLOYMENTS.md new file mode 100644 index 000000000..36c60b980 --- /dev/null +++ b/deployments/engine/V3/internal-testing/staging-v3-react-demo-2/DEPLOYMENTS.md @@ -0,0 +1,42 @@ + +# Deployment + +Date: 2023-05-04T19:39:28.024Z + +## **Network:** goerli + +## **Environment:** staging + +**Deployment Input File:** `deployments/engine/V3/internal-testing/staging-v3-react-demo-2/deployment-config.staging.ts` + +**GenArt721CoreV3_Engine:** https://goerli.etherscan.io/address/0xFb2480C9c02F3222d1299330976DACEB98009800#code + +**AdminACLV1:** https://goerli.etherscan.io/address/0xeA28d75dd6e3108000E4d0E1411A58b872248024#code + +**Engine Registry:** https://goerli.etherscan.io/address/0xEa698596b6009A622C3eD00dD5a8b5d1CAE4fC36#code + +**MinterFilterV1:** https://goerli.etherscan.io/address/0x682A5aB4d364a5CB5Ee372F6F5727C91431B8F10#code + +**Minters:** + +**MinterSetPriceV4:** https://goerli.etherscan.io/address/0xE4aF88B3E9a3045537339Fc05fd5F211fBFD92Df#code + + + +**Metadata** + +- **Starting Project Id:** 0 +- **Token Name:** V3 Engine Demo External Reader Lib +- **Token Ticker:** DEMO +- **Auto Approve Artist Split Proposals:** true +- **Render Provider Address, Primary Sales:** deployer +- **Platform Provider Address, Primary Sales:** deployer + +**Other** + +- **Add initial project?:** true +- **Add initial token?:** true +- **Image Bucket:** v3-engine-demo-external-reader-lib-goerli + +--- + diff --git a/deployments/engine/V3/internal-testing/staging-v3-react-demo-2/DEPLOYMENT_LOGS.log b/deployments/engine/V3/internal-testing/staging-v3-react-demo-2/DEPLOYMENT_LOGS.log new file mode 100644 index 000000000..9718543c2 --- /dev/null +++ b/deployments/engine/V3/internal-testing/staging-v3-react-demo-2/DEPLOYMENT_LOGS.log @@ -0,0 +1,66 @@ +---------------------------------------- +[INFO] Datetime of deployment: 2023-05-04T19:33:15.886Z +[INFO] Deployment configuration file: /Users/jakerockland/Code/artblocks-contracts/deployments/engine/V3/internal-testing/staging-v3-react-demo-2/deployment-config.staging.ts +[INFO] Deploying to network: goerli +[INFO] Deploying to environment: staging +[INFO] New Admin ACL AdminACLV1 deployed at address: 0xeA28d75dd6e3108000E4d0E1411A58b872248024 +[INFO] Randomizer BasicRandomizerV2 deployed at g0x399fa387324E4588047BBe1598e3691AB94c7e5C +[INFO] Core GenArt721CoreV3_Engine deployed at 0xFb2480C9c02F3222d1299330976DACEB98009800 +[INFO] Minter Filter MinterFilterV1 deployed at 0x682A5aB4d364a5CB5Ee372F6F5727C91431B8F10 +[INFO] MinterSetPriceV4 deployed at 0xE4aF88B3E9a3045537339Fc05fd5F211fBFD92Df +[INFO] Assigned randomizer to core and renounced ownership of randomizer +[INFO] Updated the Minter Filter on the Core contract to 0x682A5aB4d364a5CB5Ee372F6F5727C91431B8F10. +[INFO] Allowlisted minter MinterSetPriceV4 at 0xE4aF88B3E9a3045537339Fc05fd5F211fBFD92Df on minter filter. +[INFO] Added V3 Engine Demo External Reader Lib project 0 placeholder on V3 Engine Demo External Reader Lib contract, artist is 0xB8559AF91377e5BaB052A4E9a5088cB65a9a4d63. +[INFO] Configured set price minter (0xE4aF88B3E9a3045537339Fc05fd5F211fBFD92Df) for project 0. +[INFO] Configured minter price project 0. +[INFO] Minted token 0 for project 0. +[INFO] Skipping transfer of superAdmin role on adminACL. +[INFO] Verifying core contract contract deployment... +Nothing to compile +Successfully submitted source code for contract +contracts/engine/V3/GenArt721CoreV3_Engine.sol:GenArt721CoreV3_Engine at 0xFb2480C9c02F3222d1299330976DACEB98009800 +for verification on the block explorer. Waiting for verification result... + +Successfully verified contract GenArt721CoreV3_Engine on Etherscan. +https://goerli.etherscan.io/address/0xFb2480C9c02F3222d1299330976DACEB98009800#code +[INFO] Core contract verified on Etherscan at 0xFb2480C9c02F3222d1299330976DACEB98009800} +[INFO] Verifying AdminACL contract deployment... +Compiled 209 Solidity files successfully +Successfully submitted source code for contract +contracts/AdminACLV1.sol:AdminACLV1 at 0xeA28d75dd6e3108000E4d0E1411A58b872248024 +for verification on the block explorer. Waiting for verification result... + +Successfully verified contract AdminACLV1 on Etherscan. +https://goerli.etherscan.io/address/0xeA28d75dd6e3108000E4d0E1411A58b872248024#code +[INFO] AdminACL contract verified on Etherscan at 0xeA28d75dd6e3108000E4d0E1411A58b872248024} +[INFO] Verifying MinterFilter contract deployment... +The contract 0x682A5aB4d364a5CB5Ee372F6F5727C91431B8F10 has already been verified +[INFO] MinterFilter contract verified on Etherscan at 0x682A5aB4d364a5CB5Ee372F6F5727C91431B8F10} +[INFO] Verifying MinterSetPriceV4 contract deployment... +The contract 0xE4aF88B3E9a3045537339Fc05fd5F211fBFD92Df has already been verified +[INFO] MinterSetPriceV4 contract verified on Etherscan at 0xE4aF88B3E9a3045537339Fc05fd5F211fBFD92Df} +Created s3 bucket for https://v3-engine-demo-external-reader-lib-goerli.s3.amazonaws.com +[INFO] Created image bucket v3-engine-demo-external-reader-lib-goerli +[INFO] Deployment details written to /Users/jakerockland/Code/artblocks-contracts/deployments/engine/V3/internal-testing/staging-v3-react-demo-2/DEPLOYMENTS.md +Upserting 1 contract... +Contracts metadata upsert input: +{ + "address": "0xfb2480c9c02f3222d1299330976daceb98009800", + "name": "V3 Engine Demo External Reader Lib", + "bucket_name": "v3-engine-demo-external-reader-lib-goerli", + "default_vertical_name": "fullyonchain" +} +Successfully upserted 1 contract +Upserting 1 project... +Projects metadata upsert input: +{ + "id": "0xfb2480c9c02f3222d1299330976daceb98009800-0", + "contract_address": "0xfb2480c9c02f3222d1299330976daceb98009800", + "project_id": "0", + "artist_address": "0xb8559af91377e5bab052a4e9a5088cb65a9a4d63", + "vertical_name": "fullyonchain" +} +Successfully upserted 1 project +[ACTION] provider primary and secondary sales payment addresses remain as deployer addresses: 0xB8559AF91377e5BaB052A4E9a5088cB65a9a4d63. Update later as needed. +[ACTION] AdminACL's superAdmin address is 0xB8559AF91377e5BaB052A4E9a5088cB65a9a4d63, don't forget to update if requred. diff --git a/deployments/engine/V3/internal-testing/staging-v3-react-demo-2/deployment-config.staging.ts b/deployments/engine/V3/internal-testing/staging-v3-react-demo-2/deployment-config.staging.ts new file mode 100644 index 000000000..7e1085a74 --- /dev/null +++ b/deployments/engine/V3/internal-testing/staging-v3-react-demo-2/deployment-config.staging.ts @@ -0,0 +1,50 @@ +// This file is used to configure the deployment of the Engine Partner contracts +// It is intended to be imported by the generic deployer by running `deploy:mainnet:v3-engine`, `deploy:staging:v3-engine` or `deploy:dev:v3-engine`. +export const deployDetailsArray = [ + { + network: "goerli", + // environment is only used for metadata purposes, and is not used in the deployment process + // Please set to "dev", "staging", or "mainnet", as appropriate + environment: "staging", + // if you want to use an existing admin ACL, set the address here (otherwise set as undefined to deploy a new one) + existingAdminACL: undefined, + // the following can be undefined if you are using an existing admin ACL, otherwise define the Admin ACL contract name + // if deploying a new AdminACL + adminACLContractName: "AdminACLV1", + // See the `KNOWN_ENGINE_REGISTRIES` object in `/scripts/engine/V3/constants.ts` for the correct registry address for + // the intended network and the corresponding deployer wallet addresses + // @dev if you neeed a new engine registry, use the `/scripts/engine/V3/engine-registry-deployer.ts` script + engineRegistryAddress: "0xEa698596b6009A622C3eD00dD5a8b5d1CAE4fC36", + randomizerContractName: "BasicRandomizerV2", + genArt721CoreContractName: "GenArt721CoreV3_Engine", + tokenName: "V3 Engine Demo External Reader Lib", + tokenTicker: "DEMO", + startingProjectId: 0, + autoApproveArtistSplitProposals: true, + renderProviderAddress: "deployer", // use either "0x..." or special "deployer" which sets the render provider to the deployer + platformProviderAddress: "deployer", // use either "0x..." or special "deployer" which sets the render provider to the deployer + // minter suite + minterFilterContractName: "MinterFilterV1", + minters: [ + // include any of the most recent minter contracts the engine partner wishes to use + // @dev ensure the minter contracts here are the latest versions + "MinterSetPriceV4", + ], + // set to true if you want to add an initial project to the core contract + addInitialProject: true, + // set to true if you want to add an initial token to the initial project + // (this will only work if you have set addInitialProject to true, and requires a MinterSetPriceV[4-9]) + addInitialToken: true, + // set to true if you want to transfer the superAdmin role to a different address + doTransferSuperAdmin: false, + // set to the address you want to transfer the superAdmin role to + // (this will only work if you have set doTransferSuperAdmin to true, can be undefined if you are not transferring) + newSuperAdminAddress: undefined, // use either "0x..." or undefined if not transferring + // optionally define this to set default vertical name for the contract after deployment. + // if not defined, the default vertical name will be "unassigned". + // common values include `fullyonchain`, `flex`, or partnerships like `artblocksxpace`. + // also note that if you desire to create a new veritcal, you will need to add the vertical name to the + // `project_verticals` table in the database before running this deploy script. + defaultVerticalName: "fullyonchain", + }, +]; diff --git a/hardhat.solidity-config.ts b/hardhat.solidity-config.ts index 8c9bac369..9c70ced82 100644 --- a/hardhat.solidity-config.ts +++ b/hardhat.solidity-config.ts @@ -2,11 +2,11 @@ export const solidityConfig = { compilers: [ { - version: "0.5.17", + version: "0.8.17", settings: { optimizer: { enabled: true, - runs: 100, + runs: 25, }, }, }, @@ -20,11 +20,11 @@ export const solidityConfig = { }, }, { - version: "0.8.17", + version: "0.5.17", settings: { optimizer: { enabled: true, - runs: 25, + runs: 100, }, }, }, diff --git a/scripts/bytecode-storage/bytecode-storage-deployer-goerli.ts b/scripts/bytecode-storage/bytecode-storage-deployer-goerli.ts new file mode 100644 index 000000000..f564b5b09 --- /dev/null +++ b/scripts/bytecode-storage/bytecode-storage-deployer-goerli.ts @@ -0,0 +1,70 @@ +// This file can be used to deploy new copies of the shared +// public BytecodeStorageReader library, which is intended to +// be used as an externally linked library. +// SPDX-License-Identifier: LGPL-3.0-only +// Created By: Art Blocks Inc. +import hre from "hardhat"; +import { ethers } from "hardhat"; +import { tryVerify } from "../util/verification"; + +/** + * This file can be used to deploy new copies of the shared + * public BytecodeStorageReader library, which is intended to + * be used as an externally linked library. + */ +////////////////////////////////////////////////////////////////////////////// +// CONFIG BEGINS HERE +////////////////////////////////////////////////////////////////////////////// +const intendedNetwork = "goerli"; // "goerli" or "mainnet" +const libraryContractName = "BytecodeStorageReader"; +////////////////////////////////////////////////////////////////////////////// +// CONFIG ENDS HERE +////////////////////////////////////////////////////////////////////////////// +async function main() { + const [deployer] = await ethers.getSigners(); + const network = await ethers.provider.getNetwork(); + const networkName = network.name == "homestead" ? "mainnet" : network.name; + if (networkName != intendedNetwork) { + throw new Error( + `[ERROR] This script is intended to be run on ${intendedNetwork} only` + ); + } + ////////////////////////////////////////////////////////////////////////////// + // DEPLOYMENT BEGINS HERE + ////////////////////////////////////////////////////////////////////////////// + + // Deploy library + const libraryFactory = await ethers.getContractFactory(libraryContractName); + const library = await libraryFactory.deploy(); + await library.deployed(); + const libraryAddress = library.address; + console.log( + `[INFO] ${intendedNetwork} ${libraryContractName} deployed at: ${libraryAddress}` + ); + + ////////////////////////////////////////////////////////////////////////////// + // DEPLOYMENT ENDS HERE + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + // VERIFICATION BEGINS HERE + ////////////////////////////////////////////////////////////////////////////// + + // Output instructions for manual Etherscan verification. + await tryVerify(libraryContractName, libraryAddress, [], networkName); + + console.log( + `[INFO] Deployment complete! Please record deployment details in the top-level README of this repo.` + ); + + ////////////////////////////////////////////////////////////////////////////// + // VERIFICATION ENDS HERE + ////////////////////////////////////////////////////////////////////////////// +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/bytecode-storage/bytecode-storage-deployer-mainnet.ts b/scripts/bytecode-storage/bytecode-storage-deployer-mainnet.ts new file mode 100644 index 000000000..2f58de66e --- /dev/null +++ b/scripts/bytecode-storage/bytecode-storage-deployer-mainnet.ts @@ -0,0 +1,70 @@ +// This file can be used to deploy new copies of the shared +// public BytecodeStorageReader library, which is intended to +// be used as an externally linked library. +// SPDX-License-Identifier: LGPL-3.0-only +// Created By: Art Blocks Inc. +import hre from "hardhat"; +import { ethers } from "hardhat"; +import { tryVerify } from "../util/verification"; + +/** + * This file can be used to deploy new copies of the shared + * public BytecodeStorageReader library, which is intended to + * be used as an externally linked library. + */ +////////////////////////////////////////////////////////////////////////////// +// CONFIG BEGINS HERE +////////////////////////////////////////////////////////////////////////////// +const intendedNetwork = "mainnet"; // "goerli" or "mainnet" +const libraryContractName = "BytecodeStorageReader"; +////////////////////////////////////////////////////////////////////////////// +// CONFIG ENDS HERE +////////////////////////////////////////////////////////////////////////////// +async function main() { + const [deployer] = await ethers.getSigners(); + const network = await ethers.provider.getNetwork(); + const networkName = network.name == "homestead" ? "mainnet" : network.name; + if (networkName != intendedNetwork) { + throw new Error( + `[ERROR] This script is intended to be run on ${intendedNetwork} only` + ); + } + ////////////////////////////////////////////////////////////////////////////// + // DEPLOYMENT BEGINS HERE + ////////////////////////////////////////////////////////////////////////////// + + // Deploy library + const libraryFactory = await ethers.getContractFactory(libraryContractName); + const library = await libraryFactory.deploy(); + await library.deployed(); + const libraryAddress = library.address; + console.log( + `[INFO] ${intendedNetwork} ${libraryContractName} deployed at: ${libraryAddress}` + ); + + ////////////////////////////////////////////////////////////////////////////// + // DEPLOYMENT ENDS HERE + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + // VERIFICATION BEGINS HERE + ////////////////////////////////////////////////////////////////////////////// + + // Output instructions for manual Etherscan verification. + await tryVerify(libraryContractName, libraryAddress, [], networkName); + + console.log( + `[INFO] Deployment complete! Please record deployment details in the top-level README of this repo.` + ); + + ////////////////////////////////////////////////////////////////////////////// + // VERIFICATION ENDS HERE + ////////////////////////////////////////////////////////////////////////////// +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/engine/V3/generic-engine-deployer.ts b/scripts/engine/V3/generic-engine-deployer.ts index 40f794f89..77f73f5d7 100644 --- a/scripts/engine/V3/generic-engine-deployer.ts +++ b/scripts/engine/V3/generic-engine-deployer.ts @@ -18,6 +18,7 @@ import { import { DELEGATION_REGISTRY_ADDRESSES, + BYTECODE_STORAGE_READER_LIBRARY_ADDRESSES, KNOWN_ENGINE_REGISTRIES, EXTRA_DELAY_BETWEEN_TX, } from "../../util/constants"; @@ -158,6 +159,15 @@ async function main() { `[ERROR] The default vertical cannot be flex if not using a flex engine` ); } + + // verify that there is a valid bytecode storage reader library address for the network + const bytecodeStorageLibraryAddress = + BYTECODE_STORAGE_READER_LIBRARY_ADDRESSES[networkName]; + if (!bytecodeStorageLibraryAddress) { + throw new Error( + `[ERROR] No bytecode storage reader library address configured for network ${networkName}` + ); + } ////////////////////////////////////////////////////////////////////////////// // INPUT VALIDATION ENDS HERE ////////////////////////////////////////////////////////////////////////////// @@ -212,8 +222,14 @@ async function main() { await delay(EXTRA_DELAY_BETWEEN_TX); // Deploy Core contract + // Ensure that BytecodeStorageReader library is linked in the process const genArt721CoreFactory = await ethers.getContractFactory( - deployDetails.genArt721CoreContractName + deployDetails.genArt721CoreContractName, + { + libraries: { + BytecodeStorageReader: bytecodeStorageLibraryAddress, + }, + } ); const tokenName = deployDetails.tokenName; const tokenTicker = deployDetails.tokenTicker; @@ -566,6 +582,7 @@ ${deployedMinterNames - **Platform Provider Address, Primary Sales:** ${ deployDetails.platformProviderAddress } +- **BytecodeStorageReader Library:** ${bytecodeStorageLibraryAddress} **Other** diff --git a/scripts/util/constants.ts b/scripts/util/constants.ts index 00a5080b1..0d8205747 100644 --- a/scripts/util/constants.ts +++ b/scripts/util/constants.ts @@ -9,6 +9,13 @@ export const DELEGATION_REGISTRY_ADDRESSES = { mainnet: "0x00000000000076A84feF008CDAbe6409d2FE638B", }; +// BytecodeStorageReader library addresses on supported networks +export const BYTECODE_STORAGE_READER_LIBRARY_ADDRESSES = { + // note: _different_ address for goerli and mainnet + goerli: "0xB8B806A10d16cc80dB788552B54B3ECb4A2A3C3D", + mainnet: "0xf0585dF582A0ad119F1616FB82f3b449a98EeCd5", +}; + // known V3 engine registry contracts, and their deployers // format is [network]: { [registry address]: [deployer address] } export const KNOWN_ENGINE_REGISTRIES = { diff --git a/test/core/V3/GenArt721CoreV3_AdminACLRequests.test.ts b/test/core/V3/GenArt721CoreV3_AdminACLRequests.test.ts index eefd73686..b7ff6496b 100644 --- a/test/core/V3/GenArt721CoreV3_AdminACLRequests.test.ts +++ b/test/core/V3/GenArt721CoreV3_AdminACLRequests.test.ts @@ -75,12 +75,6 @@ for (const coreContractName of coreContractsToTest) { }; config = await assignDefaultConstants(config); - // get core contract interface for signature hash retrieval - const artblocksFactory = await ethers.getContractFactory( - coreContractName - ); - config.coreInterface = artblocksFactory.interface; - // deploy and configure minter filter and minter ({ genArt721Core: config.genArt721Core, @@ -94,6 +88,8 @@ for (const coreContractName of coreContractsToTest) { true )); + // get core contract interface for signature hash retrieval + config.coreInterface = config.genArt721Core.interface; config.minter = await deployAndGet(config, "MinterSetPriceV2", [ config.genArt721Core.address, config.minterFilter.address, diff --git a/test/core/V3/GenArt721CoreV3_Events.test.ts b/test/core/V3/GenArt721CoreV3_Events.test.ts index fdac8c775..adc43436e 100644 --- a/test/core/V3/GenArt721CoreV3_Events.test.ts +++ b/test/core/V3/GenArt721CoreV3_Events.test.ts @@ -98,9 +98,24 @@ for (const coreContractName of coreContractsToTest) { describe("PlatformUpdated", function () { it("deployment events (nextProjectId, etc.)", async function () { const config = await loadFixture(_beforeEach); - // typical expect event helper doesn't work for deploy event - const contractFactory = await ethers.getContractFactory( - coreContractName + + // Note that for testing purposes, we deploy a new version of the library, + // but in production we would use the same library deployment for all contracts + const libraryFactory = await ethers.getContractFactory( + "BytecodeStorageReader" + ); + const library = await libraryFactory + .connect(config.accounts.deployer) + .deploy(/* no args for library ever */); + + // Deploy actual contract (with library linked) + const coreContractFactory = await ethers.getContractFactory( + coreContractName, + { + libraries: { + BytecodeStorageReader: library.address, + }, + } ); // it is OK that config construction addresses aren't particularly valid // addresses for the purposes of config test @@ -112,17 +127,19 @@ for (const coreContractName of coreContractsToTest) { const engineRegistry = await engineRegistryFactory .connect(config.accounts.deployer) .deploy(); - tx = await contractFactory.connect(config.accounts.deployer).deploy( - "name", - "symbol", - config.accounts.additional.address, - config.accounts.additional.address, - config.accounts.additional.address, - config.accounts.additional.address, - 365, - false, - engineRegistry.address // Note: important to use a real engine registry - ); + tx = await coreContractFactory + .connect(config.accounts.deployer) + .deploy( + "name", + "symbol", + config.accounts.additional.address, + config.accounts.additional.address, + config.accounts.additional.address, + config.accounts.additional.address, + 365, + false, + engineRegistry.address // Note: important to use a real engine registry + ); const receipt = await tx.deployTransaction.wait(); const registrationLog = receipt.logs[receipt.logs.length - 1]; // expect "ContractRegistered" event as log 0 @@ -152,7 +169,7 @@ for (const coreContractName of coreContractsToTest) { ethers.utils.formatBytes32String("nextProjectId") ); } else { - tx = await contractFactory + tx = await coreContractFactory .connect(config.accounts.deployer) .deploy( "name", diff --git a/test/core/V3/GenArt721CoreV3_Integration.test.ts b/test/core/V3/GenArt721CoreV3_Integration.test.ts index 5db59ae81..9f3108cfe 100644 --- a/test/core/V3/GenArt721CoreV3_Integration.test.ts +++ b/test/core/V3/GenArt721CoreV3_Integration.test.ts @@ -16,6 +16,7 @@ import { getAccounts, assignDefaultConstants, deployAndGet, + deployWithStorageLibraryAndGet, deployCoreWithMinterFilter, mintProjectUntilRemaining, advanceEVMByTime, @@ -296,9 +297,9 @@ for (const coreContractName of coreContractsToTest) { const config = await loadFixture(_beforeEach); let targetCoreVersion; if (coreContractName === "GenArt721CoreV3") { - targetCoreVersion = "v3.1.0"; + targetCoreVersion = "v3.2.0"; } else if (coreContractName === "GenArt721CoreV3_Explorations") { - targetCoreVersion = "v3.1.1"; + targetCoreVersion = "v3.2.1"; } else if (coreContractName.includes("GenArt721CoreV3_Engine")) { targetCoreVersion = "v3.1.2"; } else { @@ -389,7 +390,7 @@ for (const coreContractName of coreContractsToTest) { const engineRegistry = await engineRegistryFactory .connect(config.accounts.deployer) .deploy(); - differentGenArt721Core = await deployAndGet( + differentGenArt721Core = await deployWithStorageLibraryAndGet( config, coreContractName, [ @@ -405,7 +406,7 @@ for (const coreContractName of coreContractsToTest) { ] ); } else { - differentGenArt721Core = await deployAndGet( + differentGenArt721Core = await deployWithStorageLibraryAndGet( config, coreContractName, [ @@ -439,7 +440,7 @@ for (const coreContractName of coreContractsToTest) { const engineRegistry = await engineRegistryFactory .connect(config.accounts.deployer) .deploy(); - differentGenArt721Core = await deployAndGet( + differentGenArt721Core = await deployWithStorageLibraryAndGet( config, coreContractName, [ @@ -455,7 +456,7 @@ for (const coreContractName of coreContractsToTest) { ] ); } else { - differentGenArt721Core = await deployAndGet( + differentGenArt721Core = await deployWithStorageLibraryAndGet( config, coreContractName, [ diff --git a/test/core/V3/GenArt721CoreV3_ProjectConfigure.test.ts b/test/core/V3/GenArt721CoreV3_ProjectConfigure.test.ts index 8c0774246..b25255635 100644 --- a/test/core/V3/GenArt721CoreV3_ProjectConfigure.test.ts +++ b/test/core/V3/GenArt721CoreV3_ProjectConfigure.test.ts @@ -18,6 +18,7 @@ import { deployCoreWithMinterFilter, mintProjectUntilRemaining, advanceEVMByTime, + deployWithStorageLibraryAndGet, } from "../../util/common"; import { FOUR_WEEKS } from "../../util/constants"; import { @@ -1423,17 +1424,21 @@ for (const coreContractName of coreContractsToTest) { [] ); // set `autoApproveArtistSplitProposals` to true - config.genArt721Core = await deployAndGet(config, coreContractName, [ - config.name, // _tokenName - config.symbol, // _tokenSymbol - config.accounts.deployer.address, // _renderProviderAddress - config.accounts.additional.address, // _platformProviderAddress - config.randomizer.address, // _randomizerContract - config.adminACL.address, // _adminACLContract - 0, // _startingProjectId - true, // _autoApproveArtistSplitProposals - config.engineRegistry.address, // _engineRegistryContract - ]); + config.genArt721Core = await deployWithStorageLibraryAndGet( + config, + coreContractName, + [ + config.name, // _tokenName + config.symbol, // _tokenSymbol + config.accounts.deployer.address, // _renderProviderAddress + config.accounts.additional.address, // _platformProviderAddress + config.randomizer.address, // _randomizerContract + config.adminACL.address, // _adminACLContract + 0, // _startingProjectId + true, // _autoApproveArtistSplitProposals + config.engineRegistry.address, // _engineRegistryContract + ] + ); // assign core contract for randomizer to use config.randomizer .connect(config.accounts.deployer) diff --git a/test/core/V3/GenArt721CoreV3_Views.test.ts b/test/core/V3/GenArt721CoreV3_Views.test.ts index 1a0b75a97..2ec9b7d7e 100644 --- a/test/core/V3/GenArt721CoreV3_Views.test.ts +++ b/test/core/V3/GenArt721CoreV3_Views.test.ts @@ -117,9 +117,9 @@ for (const coreContractName of coreContractsToTest) { describe("coreVersion", function () { it("returns expected value", async function () { const config = await loadFixture(_beforeEach); - let targetCoreVersion = "v3.1.0"; + let targetCoreVersion = "v3.2.0"; if (coreContractName === "GenArt721CoreV3_Explorations") { - targetCoreVersion = "v3.1.1"; + targetCoreVersion = "v3.2.1"; } const coreVersion = await config.genArt721Core .connect(config.accounts.deployer) diff --git a/test/core/V3/prohibition/GenArt721CoreV3_AdminACLRequests_PROHIBITION.test.ts b/test/core/V3/prohibition/GenArt721CoreV3_AdminACLRequests_PROHIBITION.test.ts index dc82e43ab..aa5457815 100644 --- a/test/core/V3/prohibition/GenArt721CoreV3_AdminACLRequests_PROHIBITION.test.ts +++ b/test/core/V3/prohibition/GenArt721CoreV3_AdminACLRequests_PROHIBITION.test.ts @@ -72,12 +72,6 @@ for (const coreContractName of coreContractsToTest) { }; config = await assignDefaultConstants(config); - // get core contract interface for signature hash retrieval - const artblocksFactory = await ethers.getContractFactory( - coreContractName - ); - config.coreInterface = artblocksFactory.interface; - // deploy and configure minter filter and minter ({ genArt721Core: config.genArt721Core, @@ -90,6 +84,8 @@ for (const coreContractName of coreContractsToTest) { "MinterFilterV1", true )); + // get core contract interface for signature hash retrieval + config.coreInterface = config.genArt721Core.interface; config.minter = await deployAndGet(config, "MinterSetPriceV2", [ config.genArt721Core.address, diff --git a/test/core/V3/prohibition/GenArt721CoreV3_Events_PROHIBITION.test.ts b/test/core/V3/prohibition/GenArt721CoreV3_Events_PROHIBITION.test.ts index 7834ea888..682d78383 100644 --- a/test/core/V3/prohibition/GenArt721CoreV3_Events_PROHIBITION.test.ts +++ b/test/core/V3/prohibition/GenArt721CoreV3_Events_PROHIBITION.test.ts @@ -95,9 +95,24 @@ for (const coreContractName of coreContractsToTest) { describe("PlatformUpdated", function () { it("deployment events (nextProjectId, etc.)", async function () { const config = await loadFixture(_beforeEach); - // typical expect event helper doesn't work for deploy event - const contractFactory = await ethers.getContractFactory( - coreContractName + + // Note that for testing purposes, we deploy a new version of the library, + // but in production we would use the same library deployment for all contracts + const libraryFactory = await ethers.getContractFactory( + "BytecodeStorageReader" + ); + const library = await libraryFactory + .connect(config.accounts.deployer) + .deploy(/* no args for library ever */); + + // Deploy actual contract (with library linked) + const coreContractFactory = await ethers.getContractFactory( + coreContractName, + { + libraries: { + BytecodeStorageReader: library.address, + }, + } ); // it is OK that config construction addresses aren't particularly valid // addresses for the purposes of config test @@ -109,17 +124,19 @@ for (const coreContractName of coreContractsToTest) { const engineRegistry = await engineRegistryFactory .connect(config.accounts.deployer) .deploy(); - tx = await contractFactory.connect(config.accounts.deployer).deploy( - "name", - "symbol", - config.accounts.additional.address, - config.accounts.additional.address, - config.accounts.additional.address, - config.accounts.additional.address, - 365, - false, - engineRegistry.address // Note: important to use a real engine registry - ); + tx = await coreContractFactory + .connect(config.accounts.deployer) + .deploy( + "name", + "symbol", + config.accounts.additional.address, + config.accounts.additional.address, + config.accounts.additional.address, + config.accounts.additional.address, + 365, + false, + engineRegistry.address // Note: important to use a real engine registry + ); const receipt = await tx.deployTransaction.wait(); const registrationLog = receipt.logs[receipt.logs.length - 1]; // expect "ContractRegistered" event as log 0 @@ -149,7 +166,7 @@ for (const coreContractName of coreContractsToTest) { ethers.utils.formatBytes32String("nextProjectId") ); } else { - tx = await contractFactory + tx = await coreContractFactory .connect(config.accounts.deployer) .deploy( "name", diff --git a/test/core/V3/prohibition/GenArt721CoreV3_Integration_PROHIBITION.test.ts b/test/core/V3/prohibition/GenArt721CoreV3_Integration_PROHIBITION.test.ts index 9a5914968..92f010463 100644 --- a/test/core/V3/prohibition/GenArt721CoreV3_Integration_PROHIBITION.test.ts +++ b/test/core/V3/prohibition/GenArt721CoreV3_Integration_PROHIBITION.test.ts @@ -16,6 +16,7 @@ import { getAccounts, assignDefaultConstants, deployAndGet, + deployWithStorageLibraryAndGet, deployCoreWithMinterFilter, mintProjectUntilRemaining, advanceEVMByTime, @@ -441,7 +442,7 @@ for (const coreContractName of coreContractsToTest) { const engineRegistry = await engineRegistryFactory .connect(config.accounts.deployer) .deploy(); - differentGenArt721Core = await deployAndGet( + differentGenArt721Core = await deployWithStorageLibraryAndGet( config, coreContractName, [ @@ -457,7 +458,7 @@ for (const coreContractName of coreContractsToTest) { ] ); } else { - differentGenArt721Core = await deployAndGet( + differentGenArt721Core = await deployWithStorageLibraryAndGet( config, coreContractName, [ @@ -491,7 +492,7 @@ for (const coreContractName of coreContractsToTest) { const engineRegistry = await engineRegistryFactory .connect(config.accounts.deployer) .deploy(); - differentGenArt721Core = await deployAndGet( + differentGenArt721Core = await deployWithStorageLibraryAndGet( config, coreContractName, [ @@ -507,7 +508,7 @@ for (const coreContractName of coreContractsToTest) { ] ); } else { - differentGenArt721Core = await deployAndGet( + differentGenArt721Core = await deployWithStorageLibraryAndGet( config, coreContractName, [ diff --git a/test/core/V3/prohibition/GenArt721CoreV3_ProjectConfigure_PROHIBITION.test.ts b/test/core/V3/prohibition/GenArt721CoreV3_ProjectConfigure_PROHIBITION.test.ts index f2a6a3294..fb87b4739 100644 --- a/test/core/V3/prohibition/GenArt721CoreV3_ProjectConfigure_PROHIBITION.test.ts +++ b/test/core/V3/prohibition/GenArt721CoreV3_ProjectConfigure_PROHIBITION.test.ts @@ -18,6 +18,7 @@ import { deployCoreWithMinterFilter, mintProjectUntilRemaining, advanceEVMByTime, + deployWithStorageLibraryAndGet, } from "../../../util/common"; import { FOUR_WEEKS } from "../../../util/constants"; import { @@ -1420,17 +1421,21 @@ for (const coreContractName of coreContractsToTest) { [] ); // set `autoApproveArtistSplitProposals` to true - config.genArt721Core = await deployAndGet(config, coreContractName, [ - config.name, // _tokenName - config.symbol, // _tokenSymbol - config.accounts.deployer.address, // _renderProviderAddress - config.accounts.additional.address, // _platformProviderAddress - config.randomizer.address, // _randomizerContract - config.adminACL.address, // _adminACLContract - 0, // _startingProjectId - true, // _autoApproveArtistSplitProposals - config.engineRegistry.address, // _engineRegistryContract - ]); + config.genArt721Core = await deployWithStorageLibraryAndGet( + config, + coreContractName, + [ + config.name, // _tokenName + config.symbol, // _tokenSymbol + config.accounts.deployer.address, // _renderProviderAddress + config.accounts.additional.address, // _platformProviderAddress + config.randomizer.address, // _randomizerContract + config.adminACL.address, // _adminACLContract + 0, // _startingProjectId + true, // _autoApproveArtistSplitProposals + config.engineRegistry.address, // _engineRegistryContract + ] + ); // assign core contract for randomizer to use config.randomizer .connect(config.accounts.deployer) diff --git a/test/dependency-registry/DependencyRegistryV0.test.ts b/test/dependency-registry/DependencyRegistryV0.test.ts index aa581bd67..78d6d3f4f 100644 --- a/test/dependency-registry/DependencyRegistryV0.test.ts +++ b/test/dependency-registry/DependencyRegistryV0.test.ts @@ -15,6 +15,7 @@ import { getAccounts, assignDefaultConstants, deployAndGet, + deployWithStorageLibraryAndGet, deployCoreWithMinterFilter, } from "../util/common"; @@ -68,7 +69,7 @@ describe(`DependencyRegistryV0`, async function () { config.minterFilter.address, ]); - config.dependencyRegistry = await deployAndGet( + config.dependencyRegistry = await deployWithStorageLibraryAndGet( config, "DependencyRegistryV0" ); diff --git a/test/libs/BytecodeStorage_BytecodeTextCR_DMock.test.ts b/test/libs/BytecodeStorageV0_BytecodeTextCR_DMock.test.ts similarity index 79% rename from test/libs/BytecodeStorage_BytecodeTextCR_DMock.test.ts rename to test/libs/BytecodeStorageV0_BytecodeTextCR_DMock.test.ts index 3f7e236fa..b0e6a831f 100644 --- a/test/libs/BytecodeStorage_BytecodeTextCR_DMock.test.ts +++ b/test/libs/BytecodeStorageV0_BytecodeTextCR_DMock.test.ts @@ -12,12 +12,15 @@ import { Contract } from "ethers"; import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import { BytecodeV0TextCR_DMock } from "../../scripts/contracts"; + import { T_Config, getAccounts, deployAndGet, assignDefaultConstants, } from "../util/common"; + import { SQUIGGLE_SCRIPT, SKULPTUUR_SCRIPT_APPROX, @@ -26,52 +29,56 @@ import { MULTI_BYTE_UTF_EIGHT_SCRIPT, } from "../util/example-scripts"; +interface BytecodeStorageV0TestConfig extends T_Config { + bytecodeV0TextCR_DMock?: BytecodeV0TextCR_DMock; +} + /** - * Tests for BytecodeStorage by way of testing the BytecodeTextCR_DMock. + * Tests for BytecodeStorageV0 by way of testing the BytecodeV0TextCR_DMock. * Note: it is not the intention of these tests to comprehensively test the mock * itself, but rather to achieve full test coverage of the underlying * library under test here, BytecodeStorage. */ -describe("BytecodeStorage + BytecodeTextCR_DMock Library Tests", async function () { +describe("BytecodeStorageV0 + BytecodeV0TextCR_DMock Library Tests", async function () { // Helper that validates a Create and subsequent Read operation, ensuring // that bytes-in == bytes-out for a given input string. async function validateCreateAndRead( - config: T_Config, + config: BytecodeStorageV0TestConfig, targetText: string, - bytecodeTextCR_DMock: Contract, + bytecodeV0TextCR_DMock: Contract, deployer: SignerWithAddress ) { - const createTextTX = await bytecodeTextCR_DMock + const createTextTX = await bytecodeV0TextCR_DMock .connect(deployer) .createText(targetText); const textSlotId = createTextTX.value.toNumber(); - const text = await bytecodeTextCR_DMock.readText(textSlotId); + const text = await bytecodeV0TextCR_DMock.readText(textSlotId); expect(text).to.equal(targetText); } // Helper that retrieves the address of the most recently deployed contract // containing bytecode for storage. async function getLatestTextDeploymentAddress( - config: T_Config, - bytecodeTextCR_DMock: Contract + config: BytecodeStorageV0TestConfig, + bytecodeV0TextCR_DMock: Contract ) { - const nextTextSlotId = await bytecodeTextCR_DMock.nextTextSlotId(); + const nextTextSlotId = await bytecodeV0TextCR_DMock.nextTextSlotId(); // decrement from `nextTextSlotId` to get last updated slot const textSlotId = nextTextSlotId - 1; const textBytecodeAddress = - await bytecodeTextCR_DMock.storedTextBytecodeAddresses(textSlotId); + await bytecodeV0TextCR_DMock.storedTextBytecodeAddresses(textSlotId); return textBytecodeAddress; } async function _beforeEach() { - let config: T_Config = { + let config: BytecodeStorageV0TestConfig = { accounts: await getAccounts(), }; config = await assignDefaultConstants(config); // deploy the library mock - config.bytecodeTextCR_DMock = await deployAndGet( + config.bytecodeV0TextCR_DMock = await deployAndGet( config, - "BytecodeTextCR_DMock", + "BytecodeV0TextCR_DMock", [] // no deployment args ); return config; @@ -97,7 +104,7 @@ describe("BytecodeStorage + BytecodeTextCR_DMock Library Tests", async function await validateCreateAndRead( config, "0", - config.bytecodeTextCR_DMock, + config.bytecodeV0TextCR_DMock, config.accounts.deployer ); }); @@ -106,7 +113,7 @@ describe("BytecodeStorage + BytecodeTextCR_DMock Library Tests", async function await validateCreateAndRead( config, "console.log(hello world)", - config.bytecodeTextCR_DMock, + config.bytecodeV0TextCR_DMock, config.accounts.deployer ); }); @@ -115,7 +122,7 @@ describe("BytecodeStorage + BytecodeTextCR_DMock Library Tests", async function await validateCreateAndRead( config, SQUIGGLE_SCRIPT, - config.bytecodeTextCR_DMock, + config.bytecodeV0TextCR_DMock, config.accounts.deployer ); }); @@ -124,7 +131,7 @@ describe("BytecodeStorage + BytecodeTextCR_DMock Library Tests", async function await validateCreateAndRead( config, SKULPTUUR_SCRIPT_APPROX, - config.bytecodeTextCR_DMock, + config.bytecodeV0TextCR_DMock, config.accounts.deployer ); }); @@ -133,7 +140,7 @@ describe("BytecodeStorage + BytecodeTextCR_DMock Library Tests", async function await validateCreateAndRead( config, MULTI_BYTE_UTF_EIGHT_SCRIPT, - config.bytecodeTextCR_DMock, + config.bytecodeV0TextCR_DMock, config.accounts.deployer ); }); @@ -144,14 +151,14 @@ describe("BytecodeStorage + BytecodeTextCR_DMock Library Tests", async function await validateCreateAndRead( config, targetText, - config.bytecodeTextCR_DMock, + config.bytecodeV0TextCR_DMock, config.accounts.deployer ); const textBytecodeAddress = getLatestTextDeploymentAddress( config, - config.bytecodeTextCR_DMock + config.bytecodeV0TextCR_DMock ); - const text = await config.bytecodeTextCR_DMock.readTextAtAddress( + const text = await config.bytecodeV0TextCR_DMock.readTextAtAddress( textBytecodeAddress ); expect(text).to.equal(targetText); @@ -160,7 +167,7 @@ describe("BytecodeStorage + BytecodeTextCR_DMock Library Tests", async function it("readFromBytecode fails to read from invalid address", async function () { const config = await loadFixture(_beforeEach); await expectRevert( - config.bytecodeTextCR_DMock.readTextAtAddress(constants.ZERO_ADDRESS), + config.bytecodeV0TextCR_DMock.readTextAtAddress(constants.ZERO_ADDRESS), "ContractAsStorage: Read Error" ); }); @@ -171,22 +178,22 @@ describe("BytecodeStorage + BytecodeTextCR_DMock Library Tests", async function await validateCreateAndRead( config, targetText, - config.bytecodeTextCR_DMock, + config.bytecodeV0TextCR_DMock, config.accounts.deployer ); const textBytecodeAddress = getLatestTextDeploymentAddress( config, - config.bytecodeTextCR_DMock + config.bytecodeV0TextCR_DMock ); // deploy a second instance of the library mock - const additionalBytecodeTextCR_DMock = await deployAndGet( + const additionalBytecodeV0TextCR_DMock = await deployAndGet( config, - "BytecodeTextCR_DMock", + "BytecodeV0TextCR_DMock", [] // no deployment args ); - const text = await additionalBytecodeTextCR_DMock.readTextAtAddress( + const text = await additionalBytecodeV0TextCR_DMock.readTextAtAddress( textBytecodeAddress ); expect(text).to.equal(targetText); @@ -199,11 +206,11 @@ describe("BytecodeStorage + BytecodeTextCR_DMock Library Tests", async function it("uploads and recalls 23.95 KB script", async function () { const config = await loadFixture(_beforeEach); const targetText = CONTRACT_SIZE_LIMIT_SCRIPT; - const createTextTX = await config.bytecodeTextCR_DMock + const createTextTX = await config.bytecodeV0TextCR_DMock .connect(config.accounts.deployer) .createText(targetText, { gasLimit: GAS_LIMIT }); const textSlotId = createTextTX.value.toNumber(); - const text = await config.bytecodeTextCR_DMock.readText(textSlotId); + const text = await config.bytecodeV0TextCR_DMock.readText(textSlotId); expect(text).to.equal(targetText); }); @@ -211,7 +218,7 @@ describe("BytecodeStorage + BytecodeTextCR_DMock Library Tests", async function it("fails to upload 26 KB script [ @skip-on-coverage ]", async function () { const config = await loadFixture(_beforeEach); await expectRevert( - config.bytecodeTextCR_DMock + config.bytecodeV0TextCR_DMock .connect(config.accounts.deployer) .createText(GREATER_THAN_CONTRACT_SIZE_LIMIT_SCRIPT, { gasLimit: GAS_LIMIT, @@ -224,18 +231,18 @@ describe("BytecodeStorage + BytecodeTextCR_DMock Library Tests", async function describe("validate getWriterAddressForBytecode behavior", function () { it("author is the mock for valid bytecode contract", async function () { const config = await loadFixture(_beforeEach); - await config.bytecodeTextCR_DMock + await config.bytecodeV0TextCR_DMock .connect(config.accounts.deployer) .createText("cute lil test text hehe"); const textBytecodeAddress = getLatestTextDeploymentAddress( config, - config.bytecodeTextCR_DMock + config.bytecodeV0TextCR_DMock ); const textAuthorAddress = - await config.bytecodeTextCR_DMock.readAuthorForTextAtAddress( + await config.bytecodeV0TextCR_DMock.readAuthorForTextAtAddress( textBytecodeAddress ); - const resolvedMockAddress = await config.bytecodeTextCR_DMock + const resolvedMockAddress = await config.bytecodeV0TextCR_DMock .resolvedAddress; expect(textAuthorAddress).to.equal(resolvedMockAddress); }); @@ -243,7 +250,7 @@ describe("BytecodeStorage + BytecodeTextCR_DMock Library Tests", async function it("getWriterAddressForBytecode fails to read from invalid address", async function () { const config = await loadFixture(_beforeEach); await expectRevert( - config.bytecodeTextCR_DMock.readAuthorForTextAtAddress( + config.bytecodeV0TextCR_DMock.readAuthorForTextAtAddress( constants.ZERO_ADDRESS ), "ContractAsStorage: Read Error" @@ -252,25 +259,25 @@ describe("BytecodeStorage + BytecodeTextCR_DMock Library Tests", async function it("getWriterAddressForBytecode is interoperable", async function () { const config = await loadFixture(_beforeEach); - await config.bytecodeTextCR_DMock + await config.bytecodeV0TextCR_DMock .connect(config.accounts.deployer) .createText("zip zipppity zoooop zop"); const textBytecodeAddress = getLatestTextDeploymentAddress( config, - config.bytecodeTextCR_DMock + config.bytecodeV0TextCR_DMock ); // deploy a second instance of the library mock - const additionalBytecodeTextCR_DMock = await deployAndGet( + const additionalBytecodeV0TextCR_DMock = await deployAndGet( config, - "BytecodeTextCR_DMock", + "BytecodeV0TextCR_DMock", [] // no deployment args ); const textAuthorAddress = - await additionalBytecodeTextCR_DMock.readAuthorForTextAtAddress( + await additionalBytecodeV0TextCR_DMock.readAuthorForTextAtAddress( textBytecodeAddress ); - const resolvedMockAddress = await config.bytecodeTextCR_DMock + const resolvedMockAddress = await config.bytecodeV0TextCR_DMock .resolvedAddress; expect(textAuthorAddress).to.equal(resolvedMockAddress); }); @@ -283,13 +290,13 @@ describe("BytecodeStorage + BytecodeTextCR_DMock Library Tests", async function await validateCreateAndRead( config, targetText, - config.bytecodeTextCR_DMock, + config.bytecodeV0TextCR_DMock, config.accounts.deployer ); const textBytecodeAddress = getLatestTextDeploymentAddress( config, - config.bytecodeTextCR_DMock + config.bytecodeV0TextCR_DMock ); const deployedBytecode = await ethers.provider.getCode( @@ -297,10 +304,11 @@ describe("BytecodeStorage + BytecodeTextCR_DMock Library Tests", async function ); expect(deployedBytecode).to.not.equal("0x"); - const nextTextSlotId = await config.bytecodeTextCR_DMock.nextTextSlotId(); + const nextTextSlotId = + await config.bytecodeV0TextCR_DMock.nextTextSlotId(); // decrement from `nextTextSlotId` to get last updated slot const textSlotId = nextTextSlotId - 1; - await config.bytecodeTextCR_DMock + await config.bytecodeV0TextCR_DMock .connect(config.accounts.deployer) .deleteText(textSlotId); @@ -316,13 +324,13 @@ describe("BytecodeStorage + BytecodeTextCR_DMock Library Tests", async function await validateCreateAndRead( config, targetText, - config.bytecodeTextCR_DMock, + config.bytecodeV0TextCR_DMock, config.accounts.deployer ); const textBytecodeAddress = getLatestTextDeploymentAddress( config, - config.bytecodeTextCR_DMock + config.bytecodeV0TextCR_DMock ); const deployedBytecode = await ethers.provider.getCode( @@ -330,7 +338,7 @@ describe("BytecodeStorage + BytecodeTextCR_DMock Library Tests", async function ); expect(deployedBytecode).to.not.equal("0x"); - await config.bytecodeTextCR_DMock + await config.bytecodeV0TextCR_DMock .connect(config.accounts.deployer) .callWithNonsenseData(textBytecodeAddress, "0xFF"); @@ -346,13 +354,13 @@ describe("BytecodeStorage + BytecodeTextCR_DMock Library Tests", async function await validateCreateAndRead( config, targetText, - config.bytecodeTextCR_DMock, + config.bytecodeV0TextCR_DMock, config.accounts.deployer ); const textBytecodeAddress = getLatestTextDeploymentAddress( config, - config.bytecodeTextCR_DMock + config.bytecodeV0TextCR_DMock ); const deployedBytecode = await ethers.provider.getCode( @@ -376,19 +384,19 @@ describe("BytecodeStorage + BytecodeTextCR_DMock Library Tests", async function // The following prodding attempts will not revert in a way caught by // hardhat, as the INVALID call is wrapped by the silent failures in // `callWithNonsenseData` and `callWithoutData`. - await config.bytecodeTextCR_DMock + await config.bytecodeV0TextCR_DMock .connect(config.accounts.deployer) .callWithNonsenseData(textBytecodeAddress, "0xFFFF"); - await config.bytecodeTextCR_DMock + await config.bytecodeV0TextCR_DMock .connect(config.accounts.deployer) .callWithNonsenseData(textBytecodeAddress, "0x00FF"); - await config.bytecodeTextCR_DMock + await config.bytecodeV0TextCR_DMock .connect(config.accounts.deployer) .callWithNonsenseData(textBytecodeAddress, "0xFE"); - await config.bytecodeTextCR_DMock + await config.bytecodeV0TextCR_DMock .connect(config.accounts.deployer) .callWithNonsenseData(textBytecodeAddress, "0x00"); - await config.bytecodeTextCR_DMock + await config.bytecodeV0TextCR_DMock .connect(config.accounts.deployer) .callWithoutData(textBytecodeAddress); @@ -402,12 +410,12 @@ describe("BytecodeStorage + BytecodeTextCR_DMock Library Tests", async function it("purgeBytecode is *not* interoperable", async function () { const config = await loadFixture(_beforeEach); - await config.bytecodeTextCR_DMock + await config.bytecodeV0TextCR_DMock .connect(config.accounts.deployer) .createText("beeeep boop bop bop bop beeeep bop"); const textBytecodeAddress = getLatestTextDeploymentAddress( config, - config.bytecodeTextCR_DMock + config.bytecodeV0TextCR_DMock ); const deployedBytecode = await ethers.provider.getCode( @@ -416,14 +424,14 @@ describe("BytecodeStorage + BytecodeTextCR_DMock Library Tests", async function expect(deployedBytecode).to.not.equal("0x"); // deploy a second instance of the library mock - const additionalBytecodeTextCR_DMock = await deployAndGet( + const additionalBytecodeV0TextCR_DMock = await deployAndGet( config, - "BytecodeTextCR_DMock", + "BytecodeV0TextCR_DMock", [] // no deployment args ); await expectRevert( - additionalBytecodeTextCR_DMock + additionalBytecodeV0TextCR_DMock .connect(config.accounts.deployer) .deleteTextAtAddress(textBytecodeAddress), "ContractAsStorage: Delete Error" diff --git a/test/libs/BytecodeStorageV1_BackwardsCompatibleReads.test.ts b/test/libs/BytecodeStorageV1_BackwardsCompatibleReads.test.ts new file mode 100644 index 000000000..ef0299cdc --- /dev/null +++ b/test/libs/BytecodeStorageV1_BackwardsCompatibleReads.test.ts @@ -0,0 +1,393 @@ +import { + BN, + constants, + expectEvent, + expectRevert, + balance, + ether, +} from "@openzeppelin/test-helpers"; + +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { Contract } from "ethers"; +import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; + +import { + BytecodeV1TextCR_DMock, + BytecodeV0TextCR_DMock, + SSTORE2Mock, +} from "../../scripts/contracts"; + +import { + T_Config, + getAccounts, + deployAndGet, + deployWithStorageLibraryAndGet, + assignDefaultConstants, +} from "../util/common"; + +import { + SQUIGGLE_SCRIPT, + SKULPTUUR_SCRIPT_APPROX, + MULTI_BYTE_UTF_EIGHT_SCRIPT, +} from "../util/example-scripts"; + +interface BytecodeStorageBackwardsCompatibleTestConfig extends T_Config { + bytecodeV1TextCR_DMock?: BytecodeV1TextCR_DMock; + bytecodeV0TextCR_DMock?: BytecodeV0TextCR_DMock; + sstore2Mock?: SSTORE2Mock; +} + +/** + * Tests for BytecodeStorageV1 by way of testing the BytecodeV1TextCR_DMock. + * Note: it is not the intention of these tests to comprehensively test the mock + * itself, but rather to achieve full test coverage of the underlying + * library under test here, BytecodeStorage. + */ +describe("BytecodeStorageV1 Backwards Compatible Reads Tests", async function () { + // Helper that validates a write from the SSTORE2 library is readable + // from the V1 library, for a given string. + async function validateReadInterop_SSTORE2_V1( + config: BytecodeStorageBackwardsCompatibleTestConfig, + targetText: string, + sstore2Mock: Contract, + bytecodeV1TextCR_DMock: Contract, + deployer: SignerWithAddress + ) { + // Upload the target text via the SSTORE2 library. + const createTextTX = await sstore2Mock + .connect(deployer) + .createText(targetText); + + // Retrieve the address of the written target text from the SSTORE2 library. + const textBytecodeAddress = getLatestTextDeploymentAddressSSTORE2( + config, + config.sstore2Mock + ); + + // Validate that V1 read of SSTORE2 written text is same as original text. + const text = await bytecodeV1TextCR_DMock.readSSTORE2TextAtAddress( + textBytecodeAddress + ); + expect(text).to.equal(targetText); + // Validate that read is the same when using manually provided read-offsets. + const textManualOffset = + await bytecodeV1TextCR_DMock.forceReadTextAtAddress( + textBytecodeAddress, + 1 // for SSTORE2, expected data offset is `1` + ); + expect(textManualOffset).to.equal(targetText); + } + + // Helper that validates a write from the V0 library is readable + // from the V1 library, for a given string. + async function validateReadInterop_V0_V1( + config: BytecodeStorageBackwardsCompatibleTestConfig, + targetText: string, + bytecodeV0TextCR_DMock: Contract, + bytecodeV1TextCR_DMock: Contract, + deployer: SignerWithAddress + ) { + // Upload the target text via the V0 library. + const createTextTX = await bytecodeV0TextCR_DMock + .connect(deployer) + .createText(targetText); + + // Retrieve the address of the written target text from the V0 library. + const textBytecodeAddress = getLatestTextDeploymentAddressV0( + config, + config.bytecodeV0TextCR_DMock + ); + + // Validate that V1 read of V0 written text is same as original text. + const text = await bytecodeV1TextCR_DMock.readTextAtAddress( + textBytecodeAddress + ); + expect(text).to.equal(targetText); + // Validate that read is the same when using manually provided read-offsets. + const textManualOffset = + await bytecodeV1TextCR_DMock.forceReadTextAtAddress( + textBytecodeAddress, + 104 // for V0, expected data offset is `104` + ); + expect(textManualOffset).to.equal(targetText); + } + + // Helper that retrieves the address of the most recently deployed contract + // containing bytecode for storage, from the SSTORE2 library. + async function getLatestTextDeploymentAddressSSTORE2( + config: BytecodeStorageBackwardsCompatibleTestConfig, + sstore2Mock: Contract + ) { + const nextTextSlotId = await sstore2Mock.nextTextSlotId(); + // decrement from `nextTextSlotId` to get last updated slot + const textSlotId = nextTextSlotId - 1; + const textBytecodeAddress = await sstore2Mock.storedTextBytecodeAddresses( + textSlotId + ); + return textBytecodeAddress; + } + + // Helper that retrieves the address of the most recently deployed contract + // containing bytecode for storage, from the V0 ByteCode storage library. + async function getLatestTextDeploymentAddressV0( + config: BytecodeStorageBackwardsCompatibleTestConfig, + bytecodeV0TextCR_DMock: Contract + ) { + const nextTextSlotId = await bytecodeV0TextCR_DMock.nextTextSlotId(); + // decrement from `nextTextSlotId` to get last updated slot + const textSlotId = nextTextSlotId - 1; + const textBytecodeAddress = + await bytecodeV0TextCR_DMock.storedTextBytecodeAddresses(textSlotId); + return textBytecodeAddress; + } + + // Helper that retrieves the address of the most recently deployed contract + // containing bytecode for storage, from the V0 ByteCode storage library. + async function getLatestTextDeploymentAddressV1( + config: BytecodeStorageBackwardsCompatibleTestConfig, + bytecodeV1TextCR_DMock: Contract + ) { + const nextTextSlotId = await bytecodeV1TextCR_DMock.nextTextSlotId(); + // decrement from `nextTextSlotId` to get last updated slot + const textSlotId = nextTextSlotId - 1; + const textBytecodeAddress = + await bytecodeV1TextCR_DMock.storedTextBytecodeAddresses(textSlotId); + return textBytecodeAddress; + } + + async function _beforeEach() { + let config: BytecodeStorageBackwardsCompatibleTestConfig = { + accounts: await getAccounts(), + }; + config = await assignDefaultConstants(config); + // deploy the V1 library mock + config.bytecodeV1TextCR_DMock = await deployWithStorageLibraryAndGet( + config, + "BytecodeV1TextCR_DMock", + [] // no deployment args + ); + // deploy the V0 library mock + config.bytecodeV0TextCR_DMock = await deployAndGet( + config, + "BytecodeV0TextCR_DMock", + [] // no deployment args + ); + // deploy the SSTORE2 library mock + config.sstore2Mock = await deployAndGet( + config, + "SSTORE2Mock", + [] // no deployment args + ); + return config; + } + + describe("validate readFromBytecode backwards-compatible interoperability", function () { + it("validates interop for a single-byte script", async function () { + const config = await loadFixture(_beforeEach); + let testString = "0"; + await validateReadInterop_V0_V1( + config, + testString, + config.bytecodeV0TextCR_DMock, + config.bytecodeV1TextCR_DMock, + config.accounts.deployer + ); + await validateReadInterop_SSTORE2_V1( + config, + testString, + config.sstore2Mock, + config.bytecodeV1TextCR_DMock, + config.accounts.deployer + ); + }); + it("validates interop for an short script < 32 bytes", async function () { + const config = await loadFixture(_beforeEach); + let testString = "console.log(hello world)"; + await validateReadInterop_V0_V1( + config, + testString, + config.bytecodeV0TextCR_DMock, + config.bytecodeV1TextCR_DMock, + config.accounts.deployer + ); + await validateReadInterop_SSTORE2_V1( + config, + testString, + config.sstore2Mock, + config.bytecodeV1TextCR_DMock, + config.accounts.deployer + ); + }); + it("validates interop for chromie squiggle script", async function () { + const config = await loadFixture(_beforeEach); + await validateReadInterop_V0_V1( + config, + SQUIGGLE_SCRIPT, + config.bytecodeV0TextCR_DMock, + config.bytecodeV1TextCR_DMock, + config.accounts.deployer + ); + await validateReadInterop_SSTORE2_V1( + config, + SQUIGGLE_SCRIPT, + config.sstore2Mock, + config.bytecodeV1TextCR_DMock, + config.accounts.deployer + ); + }); + it("validates interop for different script", async function () { + const config = await loadFixture(_beforeEach); + await validateReadInterop_V0_V1( + config, + SKULPTUUR_SCRIPT_APPROX, + config.bytecodeV0TextCR_DMock, + config.bytecodeV1TextCR_DMock, + config.accounts.deployer + ); + await validateReadInterop_SSTORE2_V1( + config, + SKULPTUUR_SCRIPT_APPROX, + config.sstore2Mock, + config.bytecodeV1TextCR_DMock, + config.accounts.deployer + ); + }); + it("validates interop for misc. UTF-8 script", async function () { + const config = await loadFixture(_beforeEach); + await validateReadInterop_V0_V1( + config, + MULTI_BYTE_UTF_EIGHT_SCRIPT, + config.bytecodeV0TextCR_DMock, + config.bytecodeV1TextCR_DMock, + config.accounts.deployer + ); + await validateReadInterop_SSTORE2_V1( + config, + MULTI_BYTE_UTF_EIGHT_SCRIPT, + config.sstore2Mock, + config.bytecodeV1TextCR_DMock, + config.accounts.deployer + ); + }); + }); + + describe("validate getWriterAddressForBytecode backwards-compatible interoperability", function () { + it("getWriterAddressForBytecode is interoperable", async function () { + const config = await loadFixture(_beforeEach); + await config.bytecodeV0TextCR_DMock + .connect(config.accounts.deployer) + .createText("zip zipppity zoooop zop"); + const textBytecodeAddress = getLatestTextDeploymentAddressV0( + config, + config.bytecodeV0TextCR_DMock + ); + + // validate read with V1 library + const textAuthorAddressV1 = + await config.bytecodeV1TextCR_DMock.readAuthorForTextAtAddress( + textBytecodeAddress + ); + const textAuthorAddressV0 = + await config.bytecodeV0TextCR_DMock.readAuthorForTextAtAddress( + textBytecodeAddress + ); + const resolvedMockAddress = await config.bytecodeV0TextCR_DMock + .resolvedAddress; + expect(textAuthorAddressV1).to.equal(resolvedMockAddress); + expect(textAuthorAddressV1).to.equal(textAuthorAddressV0); + }); + + it("getWriterAddressForBytecode is not supported for SSTORE2", async function () { + const config = await loadFixture(_beforeEach); + await config.sstore2Mock + .connect(config.accounts.deployer) + .createText("zip zipppity zoooop zop"); + const textBytecodeAddress = getLatestTextDeploymentAddressSSTORE2( + config, + config.sstore2Mock + ); + + // validate read with V1 library + await expectRevert( + config.bytecodeV1TextCR_DMock.readAuthorForTextAtAddress( + textBytecodeAddress + ), + "ContractAsStorage: Unsupported Version" + ); + }); + }); + + describe("validate getLibraryVersionForBytecode works across versions", function () { + it("read unknown contract from V1 library", async function () { + const config = await loadFixture(_beforeEach); + await config.sstore2Mock + .connect(config.accounts.deployer) + .createText("zip zipppity zoooop zop"); + const textBytecodeAddress = getLatestTextDeploymentAddressSSTORE2( + config, + config.sstore2Mock + ); + + // read SSTORE2 version from V1 library + const textLibraryVersionV1 = + await config.bytecodeV1TextCR_DMock.readLibraryVersionForTextAtAddress( + textBytecodeAddress + ); + + // hard-coded expected value + let textLibraryVersionV1UTF8 = + ethers.utils.toUtf8String(textLibraryVersionV1); + expect(textLibraryVersionV1UTF8).to.equal( + "UNKNOWN_VERSION_STRING_________ " + ); + }); + + it("read V0 version from V1 library", async function () { + const config = await loadFixture(_beforeEach); + await config.bytecodeV0TextCR_DMock + .connect(config.accounts.deployer) + .createText("zip zipppity zoooop zop"); + const textBytecodeAddress = getLatestTextDeploymentAddressV0( + config, + config.bytecodeV0TextCR_DMock + ); + + // read V0 version from V1 library + const textLibraryVersionV1 = + await config.bytecodeV1TextCR_DMock.readLibraryVersionForTextAtAddress( + textBytecodeAddress + ); + // hard-coded expected value + let textLibraryVersionV1UTF8 = + ethers.utils.toUtf8String(textLibraryVersionV1); + expect(textLibraryVersionV1UTF8).to.equal( + "BytecodeStorage_V0.0.0_________ " + ); + }); + + it("read V1 version from V1 library", async function () { + const config = await loadFixture(_beforeEach); + await config.bytecodeV1TextCR_DMock + .connect(config.accounts.deployer) + .createText("zip zipppity zoooop zop"); + const textBytecodeAddress = getLatestTextDeploymentAddressV1( + config, + config.bytecodeV1TextCR_DMock + ); + + // read V1 version from V1 library + const textLibraryVersionV1 = + await config.bytecodeV1TextCR_DMock.readLibraryVersionForTextAtAddress( + textBytecodeAddress + ); + // hard-coded expected value + let textLibraryVersionV1UTF8 = + ethers.utils.toUtf8String(textLibraryVersionV1); + expect(textLibraryVersionV1UTF8).to.equal( + "BytecodeStorage_V1.0.0_________ " + ); + }); + }); +}); diff --git a/test/libs/BytecodeStorageV1_BytecodeTextCR_DMock.test.ts b/test/libs/BytecodeStorageV1_BytecodeTextCR_DMock.test.ts new file mode 100644 index 000000000..5571462fd --- /dev/null +++ b/test/libs/BytecodeStorageV1_BytecodeTextCR_DMock.test.ts @@ -0,0 +1,324 @@ +import { + BN, + constants, + expectEvent, + expectRevert, + balance, + ether, +} from "@openzeppelin/test-helpers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { Contract } from "ethers"; +import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; + +import { BytecodeV1TextCR_DMock } from "../../scripts/contracts"; + +import { + T_Config, + getAccounts, + deployWithStorageLibraryAndGet, + assignDefaultConstants, +} from "../util/common"; + +import { + SQUIGGLE_SCRIPT, + SKULPTUUR_SCRIPT_APPROX, + CONTRACT_SIZE_LIMIT_SCRIPT, + GREATER_THAN_CONTRACT_SIZE_LIMIT_SCRIPT, + MULTI_BYTE_UTF_EIGHT_SCRIPT, +} from "../util/example-scripts"; + +interface BytecodeStorageV1TestConfig extends T_Config { + bytecodeV1TextCR_DMock?: BytecodeV1TextCR_DMock; +} + +/** + * Tests for BytecodeStorageV1 by way of testing the BytecodeV1TextCR_DMock. + * Note: it is not the intention of these tests to comprehensively test the mock + * itself, but rather to achieve full test coverage of the underlying + * library under test here, BytecodeStorage. + */ +describe("BytecodeStorageV1 + BytecodeV1TextCR_DMock Library Tests", async function () { + // Helper that validates a Create and subsequent Read operation, ensuring + // that bytes-in == bytes-out for a given input string. + async function validateCreateAndRead( + config: BytecodeStorageV1TestConfig, + targetText: string, + bytecodeV1TextCR_DMock: Contract, + deployer: SignerWithAddress + ) { + const createTextTX = await bytecodeV1TextCR_DMock + .connect(deployer) + .createText(targetText); + const textSlotId = createTextTX.value.toNumber(); + const text = await bytecodeV1TextCR_DMock.readText(textSlotId); + expect(text).to.equal(targetText); + } + + // Helper that retrieves the address of the most recently deployed contract + // containing bytecode for storage. + async function getLatestTextDeploymentAddress( + config: BytecodeStorageV1TestConfig, + bytecodeV1TextCR_DMock: Contract + ) { + const nextTextSlotId = await bytecodeV1TextCR_DMock.nextTextSlotId(); + // decrement from `nextTextSlotId` to get last updated slot + const textSlotId = nextTextSlotId - 1; + const textBytecodeAddress = + await bytecodeV1TextCR_DMock.storedTextBytecodeAddresses(textSlotId); + return textBytecodeAddress; + } + + async function _beforeEach() { + let config: BytecodeStorageV1TestConfig = { + accounts: await getAccounts(), + }; + config = await assignDefaultConstants(config); + // deploy the library mock + config.bytecodeV1TextCR_DMock = await deployWithStorageLibraryAndGet( + config, + "BytecodeV1TextCR_DMock", + [] // no deployment args + ); + return config; + } + + describe("imported scripts are non-empty", function () { + it("ensure diffs are captured if project scripts are deleted", async function () { + const config = await loadFixture(_beforeEach); + expect(SQUIGGLE_SCRIPT.length).to.be.gt(0); + expect(SKULPTUUR_SCRIPT_APPROX.length).to.be.gt(0); + expect(CONTRACT_SIZE_LIMIT_SCRIPT.length).to.be.gt(0); + expect(GREATER_THAN_CONTRACT_SIZE_LIMIT_SCRIPT.length).to.be.gt(0); + expect(GREATER_THAN_CONTRACT_SIZE_LIMIT_SCRIPT.length).to.be.gt( + CONTRACT_SIZE_LIMIT_SCRIPT.length + ); + expect(MULTI_BYTE_UTF_EIGHT_SCRIPT.length).to.be.gt(0); + }); + }); + + describe("validate writeToBytecode + readFromBytecode write-and-recall", function () { + it("uploads and recalls a single-byte script", async function () { + const config = await loadFixture(_beforeEach); + await validateCreateAndRead( + config, + "0", + config.bytecodeV1TextCR_DMock, + config.accounts.deployer + ); + }); + it("uploads and recalls an short script < 32 bytes", async function () { + const config = await loadFixture(_beforeEach); + await validateCreateAndRead( + config, + "console.log(hello world)", + config.bytecodeV1TextCR_DMock, + config.accounts.deployer + ); + }); + it("uploads and recalls chromie squiggle script", async function () { + const config = await loadFixture(_beforeEach); + await validateCreateAndRead( + config, + SQUIGGLE_SCRIPT, + config.bytecodeV1TextCR_DMock, + config.accounts.deployer + ); + }); + it("uploads and recalls different script", async function () { + const config = await loadFixture(_beforeEach); + await validateCreateAndRead( + config, + SKULPTUUR_SCRIPT_APPROX, + config.bytecodeV1TextCR_DMock, + config.accounts.deployer + ); + }); + it("uploads and recalls misc. UTF-8 script", async function () { + const config = await loadFixture(_beforeEach); + await validateCreateAndRead( + config, + MULTI_BYTE_UTF_EIGHT_SCRIPT, + config.bytecodeV1TextCR_DMock, + config.accounts.deployer + ); + }); + + it("readFromBytecode works in normal conditions", async function () { + const config = await loadFixture(_beforeEach); + const targetText = "0"; + await validateCreateAndRead( + config, + targetText, + config.bytecodeV1TextCR_DMock, + config.accounts.deployer + ); + const textBytecodeAddress = getLatestTextDeploymentAddress( + config, + config.bytecodeV1TextCR_DMock + ); + const text = await config.bytecodeV1TextCR_DMock.readTextAtAddress( + textBytecodeAddress + ); + expect(text).to.equal(targetText); + }); + + it("readFromBytecode fails to read from invalid address", async function () { + const config = await loadFixture(_beforeEach); + await expectRevert( + config.bytecodeV1TextCR_DMock.readTextAtAddress(constants.ZERO_ADDRESS), + "ContractAsStorage: Read Error" + ); + }); + + it("readFromBytecode is interoperable", async function () { + const config = await loadFixture(_beforeEach); + const targetText = "hip hip hippity hop"; + await validateCreateAndRead( + config, + targetText, + config.bytecodeV1TextCR_DMock, + config.accounts.deployer + ); + + const textBytecodeAddress = getLatestTextDeploymentAddress( + config, + config.bytecodeV1TextCR_DMock + ); + + // deploy a second instance of the library mock + const additionalBytecodeV1TextCR_DMock = + await deployWithStorageLibraryAndGet( + config, + "BytecodeV1TextCR_DMock", + [] // no deployment args + ); + const text = await additionalBytecodeV1TextCR_DMock.readTextAtAddress( + textBytecodeAddress + ); + expect(text).to.equal(targetText); + }); + }); + + describe("validate writeToBytecode behavior at size-limit boundaries", function () { + // hard-code gas limit because ethers sometimes estimates too high + const GAS_LIMIT = 30000000; + it("uploads and recalls 23.95 KB script", async function () { + const config = await loadFixture(_beforeEach); + const targetText = CONTRACT_SIZE_LIMIT_SCRIPT; + const createTextTX = await config.bytecodeV1TextCR_DMock + .connect(config.accounts.deployer) + .createText(targetText, { gasLimit: GAS_LIMIT }); + const textSlotId = createTextTX.value.toNumber(); + const text = await config.bytecodeV1TextCR_DMock.readText(textSlotId); + expect(text).to.equal(targetText); + }); + + // skip on coverage because contract max sizes are ignored + it("fails to upload 26 KB script [ @skip-on-coverage ]", async function () { + const config = await loadFixture(_beforeEach); + await expectRevert( + config.bytecodeV1TextCR_DMock + .connect(config.accounts.deployer) + .createText(GREATER_THAN_CONTRACT_SIZE_LIMIT_SCRIPT, { + gasLimit: GAS_LIMIT, + }), + "ContractAsStorage: Write Error" + ); + }); + }); + + describe("validate getWriterAddressForBytecode behavior", function () { + it("author is the mock for valid bytecode contract", async function () { + const config = await loadFixture(_beforeEach); + await config.bytecodeV1TextCR_DMock + .connect(config.accounts.deployer) + .createText("cute lil test text hehe"); + const textBytecodeAddress = getLatestTextDeploymentAddress( + config, + config.bytecodeV1TextCR_DMock + ); + const textAuthorAddress = + await config.bytecodeV1TextCR_DMock.readAuthorForTextAtAddress( + textBytecodeAddress + ); + const resolvedMockAddress = await config.bytecodeV1TextCR_DMock + .resolvedAddress; + expect(textAuthorAddress).to.equal(resolvedMockAddress); + }); + + it("getWriterAddressForBytecode fails to read from invalid address", async function () { + const config = await loadFixture(_beforeEach); + await expectRevert( + config.bytecodeV1TextCR_DMock.readAuthorForTextAtAddress( + constants.ZERO_ADDRESS + ), + "ContractAsStorage: Read Error" + ); + }); + + it("getWriterAddressForBytecode is interoperable", async function () { + const config = await loadFixture(_beforeEach); + await config.bytecodeV1TextCR_DMock + .connect(config.accounts.deployer) + .createText("zip zipppity zoooop zop"); + const textBytecodeAddress = getLatestTextDeploymentAddress( + config, + config.bytecodeV1TextCR_DMock + ); + + // deploy a second instance of the library mock + const additionalBytecodeV1TextCR_DMock = + await deployWithStorageLibraryAndGet( + config, + "BytecodeV1TextCR_DMock", + [] // no deployment args + ); + const textAuthorAddress = + await additionalBytecodeV1TextCR_DMock.readAuthorForTextAtAddress( + textBytecodeAddress + ); + const resolvedMockAddress = await config.bytecodeV1TextCR_DMock + .resolvedAddress; + expect(textAuthorAddress).to.equal(resolvedMockAddress); + }); + }); + + describe("validate delete behavior (no purges)", function () { + it("writes text, and then deletes it", async function () { + const config = await loadFixture(_beforeEach); + const targetText = "silly willy billy dilly dilly"; + await validateCreateAndRead( + config, + targetText, + config.bytecodeV1TextCR_DMock, + config.accounts.deployer + ); + + const textBytecodeAddress = getLatestTextDeploymentAddress( + config, + config.bytecodeV1TextCR_DMock + ); + + const deployedBytecode = await ethers.provider.getCode( + textBytecodeAddress + ); + expect(deployedBytecode).to.not.equal("0x"); + + const nextTextSlotId = + await config.bytecodeV1TextCR_DMock.nextTextSlotId(); + // decrement from `nextTextSlotId` to get last updated slot + const textSlotId = nextTextSlotId - 1; + await config.bytecodeV1TextCR_DMock + .connect(config.accounts.deployer) + .deleteText(textSlotId); + + const deletedBytecode = await ethers.provider.getCode( + textBytecodeAddress + ); + // no-purge! bytecode is still there + expect(deletedBytecode).to.equal(deployedBytecode); + }); + }); +}); diff --git a/test/minter-suite-minters/Minter.common.ts b/test/minter-suite-minters/Minter.common.ts index a6bf1aa91..e7beaea06 100644 --- a/test/minter-suite-minters/Minter.common.ts +++ b/test/minter-suite-minters/Minter.common.ts @@ -3,7 +3,7 @@ import { constants, expectRevert } from "@openzeppelin/test-helpers"; import { expect } from "chai"; import { ethers } from "hardhat"; import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; -import { T_Config } from "../util/common"; +import { T_Config, deployWithStorageLibraryAndGet } from "../util/common"; /** * These tests are intended to check common Minter functionality @@ -20,19 +20,12 @@ export const Minter_Common = async (_beforeEach: () => Promise) => { it("reverts when given incorrect minter filter and core addresses", async function () { const config = await loadFixture(_beforeEach); - const artblocksFactory = await ethers.getContractFactory( - "GenArt721CoreV3" - ); const adminACL = await config.genArt721Core.owner(); - const token2 = await artblocksFactory - .connect(config.accounts.deployer) - .deploy( - config.name, - config.symbol, - config.randomizer.address, - adminACL, - 0 - ); + const token2 = await deployWithStorageLibraryAndGet( + config, + "GenArt721CoreV3", + [config.name, config.symbol, config.randomizer.address, adminACL, 0] + ); const minterFilterFactory = await ethers.getContractFactory( "MinterFilterV1" diff --git a/test/util/common.ts b/test/util/common.ts index 72603cb69..cbd4f9297 100644 --- a/test/util/common.ts +++ b/test/util/common.ts @@ -137,6 +137,36 @@ export async function deployAndGet( .deploy(...deployArgs); } +// utility function to simplify code when deploying any contract from factory +// that requires the bytecode storage library +export async function deployWithStorageLibraryAndGet( + config: T_Config, + coreContractName: string, + deployArgs?: any[] +): Promise { + // Note that for testing purposes, we deploy a new version of the library, + // but in production we would use the same library deployment for all contracts + const libraryFactory = await ethers.getContractFactory( + "BytecodeStorageReader" + ); + const library = await libraryFactory + .connect(config.accounts.deployer) + .deploy(/* no args for library ever */); + + // Deploy actual contract (with library linked) + const coreContractFactory = await ethers.getContractFactory( + coreContractName, + { + libraries: { + BytecodeStorageReader: library.address, + }, + } + ); + return await coreContractFactory + .connect(config.accounts.deployer) + .deploy(...deployArgs); +} + // utility function to deploy basic randomizer, core, and MinterFilter // works for core versions V0, V1, V2_PRTNR, V3 export async function deployCoreWithMinterFilter( @@ -203,13 +233,17 @@ export async function deployCoreWithMinterFilter( ? _adminACLContractName : adminACLContractName; adminACL = await deployAndGet(config, adminACLContractName, []); - genArt721Core = await deployAndGet(config, coreContractName, [ - config.name, - config.symbol, - randomizer.address, - adminACL.address, - 0, // _startingProjectId - ]); + genArt721Core = await deployWithStorageLibraryAndGet( + config, + coreContractName, + [ + config.name, + config.symbol, + randomizer.address, + adminACL.address, + 0, // _startingProjectId + ] + ); // assign core contract for randomizer to use randomizer .connect(config.accounts.deployer) @@ -247,17 +281,21 @@ export async function deployCoreWithMinterFilter( engineRegistry = await deployAndGet(config, "EngineRegistryV0", []); // Note: in the common tests, set `autoApproveArtistSplitProposals` to false, which // mirrors the approval-flow behavior of the other (non-Engine) V3 contracts - genArt721Core = await deployAndGet(config, coreContractName, [ - config.name, // _tokenName - config.symbol, // _tokenSymbol - config.accounts.deployer.address, // _renderProviderAddress - config.accounts.additional.address, // _platformProviderAddress - randomizer.address, // _randomizerContract - adminACL.address, // _adminACLContract - 0, // _startingProjectId - false, // _autoApproveArtistSplitProposals - engineRegistry.address, // _engineRegistryContract - ]); + genArt721Core = await deployWithStorageLibraryAndGet( + config, + coreContractName, + [ + config.name, // _tokenName + config.symbol, // _tokenSymbol + config.accounts.deployer.address, // _renderProviderAddress + config.accounts.additional.address, // _platformProviderAddress + randomizer.address, // _randomizerContract + adminACL.address, // _adminACLContract + 0, // _startingProjectId + false, // _autoApproveArtistSplitProposals + engineRegistry.address, // _engineRegistryContract + ] + ); // assign core contract for randomizer to use randomizer .connect(config.accounts.deployer)