From 88a96be3622020c993c8ffa774c30a81bddd097e Mon Sep 17 00:00:00 2001 From: purp Date: Wed, 26 Apr 2023 18:40:48 -0600 Subject: [PATCH 1/9] Remove all call-sites for purgeBytecode, before cleaning up library internals --- contracts/DependencyRegistryV0.sol | 24 +-- contracts/GenArt721CoreV3.sol | 28 +-- .../engine/V3/GenArt721CoreV3_Engine.sol | 6 +- .../engine/V3/GenArt721CoreV3_Engine_Flex.sol | 6 +- .../GenArt721CoreV3_Explorations.sol | 28 +-- contracts/mock/BytecodeTextCR_DMock.sol | 18 +- ...nArt721CoreV3_Engine_IncorrectCoreType.sol | 22 --- .../V3/GenArt721CoreV3_Integration.test.ts | 4 +- test/core/V3/GenArt721CoreV3_Views.test.ts | 4 +- ...tecodeStorage_BytecodeTextCR_DMock.test.ts | 162 ------------------ 10 files changed, 16 insertions(+), 286 deletions(-) diff --git a/contracts/DependencyRegistryV0.sol b/contracts/DependencyRegistryV0.sol index 252e213c0..72ee661c7 100644 --- a/contracts/DependencyRegistryV0.sol +++ b/contracts/DependencyRegistryV0.sol @@ -211,16 +211,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 +228,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; diff --git a/contracts/GenArt721CoreV3.sol b/contracts/GenArt721CoreV3.sol index 4c907108d..cf1e9a21b 100644 --- a/contracts/GenArt721CoreV3.sol +++ b/contracts/GenArt721CoreV3.sol @@ -242,7 +242,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 +1072,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 +1090,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; diff --git a/contracts/engine/V3/GenArt721CoreV3_Engine.sol b/contracts/engine/V3/GenArt721CoreV3_Engine.sol index c753571d5..001646e8a 100644 --- a/contracts/engine/V3/GenArt721CoreV3_Engine.sol +++ b/contracts/engine/V3/GenArt721CoreV3_Engine.sol @@ -1134,9 +1134,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 +1152,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; diff --git a/contracts/engine/V3/GenArt721CoreV3_Engine_Flex.sol b/contracts/engine/V3/GenArt721CoreV3_Engine_Flex.sol index 36d0549d2..b490eef82 100644 --- a/contracts/engine/V3/GenArt721CoreV3_Engine_Flex.sol +++ b/contracts/engine/V3/GenArt721CoreV3_Engine_Flex.sol @@ -1320,9 +1320,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 +1338,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; diff --git a/contracts/explorations/GenArt721CoreV3_Explorations.sol b/contracts/explorations/GenArt721CoreV3_Explorations.sol index 90f4b82fb..fe5f87b10 100644 --- a/contracts/explorations/GenArt721CoreV3_Explorations.sol +++ b/contracts/explorations/GenArt721CoreV3_Explorations.sol @@ -239,7 +239,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 +1078,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 +1096,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; diff --git a/contracts/mock/BytecodeTextCR_DMock.sol b/contracts/mock/BytecodeTextCR_DMock.sol index 18722bcff..b044423e9 100644 --- a/contracts/mock/BytecodeTextCR_DMock.sol +++ b/contracts/mock/BytecodeTextCR_DMock.sol @@ -89,9 +89,7 @@ contract BytecodeTextCR_DMock { * the underlying BytecodeStorage lib to throw errors where applicable. */ function deleteText(uint256 _textSlotId) external onlyDeployer { - // purge old contract bytecode contract from the blockchain state - storedTextBytecodeAddresses[_textSlotId].purgeBytecode(); - // delete reference to contract address that no longer exists + // delete reference to old storage contract address delete storedTextBytecodeAddresses[_textSlotId]; } @@ -128,20 +126,6 @@ contract BytecodeTextCR_DMock { return _bytecodeAddress.getWriterAddressForBytecode(); } - /** - * @notice Allows additional delete introspection, to delete a chunk of text, - * from chain-state that lives at a given deployed address. - * @param _bytecodeAddress address from which to delete text content. - * @dev Intentionally do not perform input validation, instead allowing - * the underlying BytecodeStorage lib to throw errors where applicable. - */ - function deleteTextAtAddress( - address _bytecodeAddress - ) external onlyDeployer { - // purge old contract bytecode contract from the blockchain state - _bytecodeAddress.purgeBytecode(); - } - /** * @notice Allows additional internal purge-logic introspection, by allowing * for the sending of arbitrary data from this contract to the diff --git a/contracts/mock/GenArt721CoreV3_Engine_IncorrectCoreType.sol b/contracts/mock/GenArt721CoreV3_Engine_IncorrectCoreType.sol index ba588a307..b92f87471 100644 --- a/contracts/mock/GenArt721CoreV3_Engine_IncorrectCoreType.sol +++ b/contracts/mock/GenArt721CoreV3_Engine_IncorrectCoreType.sol @@ -1115,16 +1115,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 +1134,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 { diff --git a/test/core/V3/GenArt721CoreV3_Integration.test.ts b/test/core/V3/GenArt721CoreV3_Integration.test.ts index 5db59ae81..89a2dc0ce 100644 --- a/test/core/V3/GenArt721CoreV3_Integration.test.ts +++ b/test/core/V3/GenArt721CoreV3_Integration.test.ts @@ -296,9 +296,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 { 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/libs/BytecodeStorage_BytecodeTextCR_DMock.test.ts b/test/libs/BytecodeStorage_BytecodeTextCR_DMock.test.ts index 3f7e236fa..eb1e438d2 100644 --- a/test/libs/BytecodeStorage_BytecodeTextCR_DMock.test.ts +++ b/test/libs/BytecodeStorage_BytecodeTextCR_DMock.test.ts @@ -275,166 +275,4 @@ describe("BytecodeStorage + BytecodeTextCR_DMock Library Tests", async function expect(textAuthorAddress).to.equal(resolvedMockAddress); }); }); - - describe("validate purgeBytecode behavior", function () { - it("writes text, and then purges it", async function () { - const config = await loadFixture(_beforeEach); - const targetText = "silly willy billy dilly dilly"; - await validateCreateAndRead( - config, - targetText, - config.bytecodeTextCR_DMock, - config.accounts.deployer - ); - - const textBytecodeAddress = getLatestTextDeploymentAddress( - config, - config.bytecodeTextCR_DMock - ); - - const deployedBytecode = await ethers.provider.getCode( - textBytecodeAddress - ); - expect(deployedBytecode).to.not.equal("0x"); - - const nextTextSlotId = await config.bytecodeTextCR_DMock.nextTextSlotId(); - // decrement from `nextTextSlotId` to get last updated slot - const textSlotId = nextTextSlotId - 1; - await config.bytecodeTextCR_DMock - .connect(config.accounts.deployer) - .deleteText(textSlotId); - - const removedBytecode = await ethers.provider.getCode( - textBytecodeAddress - ); - expect(removedBytecode).to.equal("0x"); - }); - - it("SELFDESTRUCT via direct call data possible with 0xFF", async function () { - const config = await loadFixture(_beforeEach); - const targetText = "silly willy billy dilly dilly"; - await validateCreateAndRead( - config, - targetText, - config.bytecodeTextCR_DMock, - config.accounts.deployer - ); - - const textBytecodeAddress = getLatestTextDeploymentAddress( - config, - config.bytecodeTextCR_DMock - ); - - const deployedBytecode = await ethers.provider.getCode( - textBytecodeAddress - ); - expect(deployedBytecode).to.not.equal("0x"); - - await config.bytecodeTextCR_DMock - .connect(config.accounts.deployer) - .callWithNonsenseData(textBytecodeAddress, "0xFF"); - - const removedBytecode = await ethers.provider.getCode( - textBytecodeAddress - ); - expect(removedBytecode).to.equal("0x"); - }); - - it("SELFDESTRUCT is NOT possible via call-data prodding", async function () { - const config = await loadFixture(_beforeEach); - const targetText = "silly willy billy dilly dilly"; - await validateCreateAndRead( - config, - targetText, - config.bytecodeTextCR_DMock, - config.accounts.deployer - ); - - const textBytecodeAddress = getLatestTextDeploymentAddress( - config, - config.bytecodeTextCR_DMock - ); - - const deployedBytecode = await ethers.provider.getCode( - textBytecodeAddress - ); - expect(deployedBytecode).to.not.equal("0x"); - - // Non-writer addresses should **not** be able to purge bytecode storage. - await expectRevert.unspecified( - config.accounts.deployer.call({ - to: textBytecodeAddress, - }) - ); - // And config is still the case when correct `0xFF` bytes are sent along. - await expectRevert.unspecified( - config.accounts.deployer.call({ - to: textBytecodeAddress, - data: "0xFF", - }) - ); - // 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 - .connect(config.accounts.deployer) - .callWithNonsenseData(textBytecodeAddress, "0xFFFF"); - await config.bytecodeTextCR_DMock - .connect(config.accounts.deployer) - .callWithNonsenseData(textBytecodeAddress, "0x00FF"); - await config.bytecodeTextCR_DMock - .connect(config.accounts.deployer) - .callWithNonsenseData(textBytecodeAddress, "0xFE"); - await config.bytecodeTextCR_DMock - .connect(config.accounts.deployer) - .callWithNonsenseData(textBytecodeAddress, "0x00"); - await config.bytecodeTextCR_DMock - .connect(config.accounts.deployer) - .callWithoutData(textBytecodeAddress); - - // Deployed bytes are unchanged. - const notRemovedBytecode = await ethers.provider.getCode( - textBytecodeAddress - ); - expect(notRemovedBytecode).to.equal(deployedBytecode); - expect(notRemovedBytecode).to.not.equal("0x"); - }); - - it("purgeBytecode is *not* interoperable", async function () { - const config = await loadFixture(_beforeEach); - await config.bytecodeTextCR_DMock - .connect(config.accounts.deployer) - .createText("beeeep boop bop bop bop beeeep bop"); - const textBytecodeAddress = getLatestTextDeploymentAddress( - config, - config.bytecodeTextCR_DMock - ); - - const deployedBytecode = await ethers.provider.getCode( - textBytecodeAddress - ); - expect(deployedBytecode).to.not.equal("0x"); - - // deploy a second instance of the library mock - const additionalBytecodeTextCR_DMock = await deployAndGet( - config, - "BytecodeTextCR_DMock", - [] // no deployment args - ); - - await expectRevert( - additionalBytecodeTextCR_DMock - .connect(config.accounts.deployer) - .deleteTextAtAddress(textBytecodeAddress), - "ContractAsStorage: Delete Error" - ); - - // Deployed bytes are unchanged. - const notRemovedBytecode = await ethers.provider.getCode( - textBytecodeAddress - ); - expect(notRemovedBytecode).to.equal(deployedBytecode); - expect(notRemovedBytecode).to.not.equal("0x"); - }); - }); }); From 9b6872ed21652fe11628ee1d9a0ecfdd30ffc3bd Mon Sep 17 00:00:00 2001 From: purp Date: Wed, 26 Apr 2023 19:09:49 -0600 Subject: [PATCH 2/9] Remove purge logic from library entirely --- contracts/libs/0.8.x/BytecodeStorage.sol | 156 ++++------------------- 1 file changed, 22 insertions(+), 134 deletions(-) diff --git a/contracts/libs/0.8.x/BytecodeStorage.sol b/contracts/libs/0.8.x/BytecodeStorage.sol index fb85ba86c..2182c8b7f 100644 --- a/contracts/libs/0.8.x/BytecodeStorage.sol +++ b/contracts/libs/0.8.x/BytecodeStorage.sol @@ -14,10 +14,8 @@ pragma solidity ^0.8.0; * @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 - * - exposes "delete" functionality, allowing no-longer-used storage to be purged from chain state - * - stores the "writer" address (library user) in the deployed contract bytes, which is useful for both: - * a) providing necessary information for safe deletion; and - * b) allowing this to be introspected on-chain + * - stores the "writer" address (library user) in the deployed contract bytes, which is useful for + * on-chain introspection and provenance purposes * 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 @@ -28,14 +26,14 @@ library BytecodeStorage { // Starting Index | Size | Ending Index | Description // //---------------------------------------------------------------------------------------------------------------// // 0 | N/A | 0 | // - // 0 | 72 | 72 | the bytes of the gated-cleanup-logic allowing for `selfdestruct`ion // - // 72 | 32 | 104 | the 32 bytes for storing the deploying contract's (0-padded) address // + // 0 | 1 | 1 | single byte opcode for making the storage contract non-executable // + // 0 | 1 | 33 | the 32 bytes for storing the deploying contract's (0-padded) address // //---------------------------------------------------------------------------------------------------------------// // Define the offset for where the "logic 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 internal constant DATA_OFFSET = 104; - uint256 internal constant ADDRESS_OFFSET = 72; + uint256 internal constant DATA_OFFSET = 33; + uint256 internal constant ADDRESS_OFFSET = 1; /*////////////////////////////////////////////////////////////// WRITE LOGIC @@ -44,7 +42,7 @@ library BytecodeStorage { /** * @notice Write a string to contract bytecode * @param _data string to be written to contract. No input validation is performed on this parameter. - * @return address_ address of deployed contract with bytecode containing concat(gated-cleanup-logic, address, data) + * @return address_ address of deployed contract with bytecode containing concat(deployer-address, data) */ function writeToBytecode( string memory _data @@ -54,7 +52,7 @@ library BytecodeStorage { //---------------------------------------------------------------------------------------------------------------// // Opcode | Opcode + Arguments | Description | Stack View // //---------------------------------------------------------------------------------------------------------------// - // (0) creation code returns all code in the contract except for the first 11 (0B in hex) bytes, as these 11 + // 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 // @@ -66,105 +64,24 @@ library BytecodeStorage { // 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 | // + // 0xF3 | 0xF3 | RETURN | // //---------------------------------------------------------------------------------------------------------------// // (11 bytes) hex"60_0B_59_81_38_03_80_92_59_39_F3", //---------------------------------------------------------------------------------------------------------------// - // Opcode | Opcode + Arguments | Description | Stack View // - //---------------------------------------------------------------------------------------------------------------// - // (1a) conditional logic for determing purge-gate (only the bytecode contract deployer can `selfdestruct`) - //---------------------------------------------------------------------------------------------------------------// - // 0x60 | 0x60_20 | PUSH1 32 | 32 // - // 0x60 | 0x60_48 | PUSH1 72 (*) | contractOffset 32 // - // 0x60 | 0x60_00 | PUSH1 0 | 0 contractOffset 32 // - // 0x39 | 0x39 | CODECOPY | // - // 0x60 | 0x60_00 | PUSH1 0 | 0 // - // 0x51 | 0x51 | MLOAD | byteDeployerAddress // - // 0x33 | 0x33 | CALLER | msg.sender byteDeployerAddress // - // 0x14 | 0x14 | EQ | (msg.sender == byteDeployerAddress) // - //---------------------------------------------------------------------------------------------------------------// - // (12 bytes: 0-11 in deployed contract) - hex"60_20_60_48_60_00_39_60_00_51_33_14", - //---------------------------------------------------------------------------------------------------------------// - // (1b) load up the destination jump address for `(2a) calldata length check` logic, jump or raise `invalid` op-code - //---------------------------------------------------------------------------------------------------------------// - // 0x60 | 0x60_10 | PUSH1 16 (^) | jumpDestination (msg.sender == byteDeployerAddress) // - // 0x57 | 0x57 | JUMPI | // - // 0xFE | 0xFE | INVALID | // - //---------------------------------------------------------------------------------------------------------------// - // (4 bytes: 12-15 in deployed contract) - hex"60_10_57_FE", - //---------------------------------------------------------------------------------------------------------------// - // (2a) conditional logic for determing purge-gate (only if calldata length is 1 byte) - //---------------------------------------------------------------------------------------------------------------// - // 0x5B | 0x5B | JUMPDEST (16) | // - // 0x60 | 0x60_01 | PUSH1 1 | 1 // - // 0x36 | 0x36 | CALLDATASIZE | calldataSize 1 // - // 0x14 | 0x14 | EQ | (calldataSize == 1) // - //---------------------------------------------------------------------------------------------------------------// - // (5 bytes: 16-20 in deployed contract) - hex"5B_60_01_36_14", - //---------------------------------------------------------------------------------------------------------------// - // (2b) load up the destination jump address for `(3a) calldata value check` logic, jump or raise `invalid` op-code - //---------------------------------------------------------------------------------------------------------------// - // 0x60 | 0x60_19 | PUSH1 25 (^) | jumpDestination (calldataSize == 1) // - // 0x57 | 0x57 | JUMPI | // - // 0xFE | 0xFE | INVALID | // - //---------------------------------------------------------------------------------------------------------------// - // (4 bytes: 21-24 in deployed contract) - hex"60_19_57_FE", - //---------------------------------------------------------------------------------------------------------------// - // (3a) conditional logic for determing purge-gate (only if calldata is `0xFF`) + // b.) ensure that the deployed storage contract is non-executeable (first opcode is the `invalid` opcode) //---------------------------------------------------------------------------------------------------------------// - // 0x5B | 0x5B | JUMPDEST (25) | // - // 0x60 | 0x60_00 | PUSH1 0 | 0 // - // 0x35 | 0x35 | CALLDATALOAD | calldata // - // 0x7F | 0x7F_FF_00_..._00 | PUSH32 0xFF00...00 | 0xFF0...00 calldata // - // 0x14 | 0x14 | EQ | (0xFF00...00 == calldata) // //---------------------------------------------------------------------------------------------------------------// - // (4 bytes: 25-28 in deployed contract) - hex"5B_60_00_35", - // (33 bytes: 29-61 in deployed contract) - hex"7F_FF_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00", - // (1 byte: 62 in deployed contract) - hex"14", + // 0xFE | 0xFE | INVALID | // //---------------------------------------------------------------------------------------------------------------// - // (3b) load up the destination jump address for actual purging (4), jump or raise `invalid` op-code + // (1 byte) + hex"FE", //---------------------------------------------------------------------------------------------------------------// - // 0x60 | 0x60_43 | PUSH1 67 (^) | jumpDestination (0xFF00...00 == calldata) // - // 0x57 | 0x57 | JUMPI | // - // 0xFE | 0xFE | INVALID | // + // c.) store the deploying-contract's address with 0-padding to fit a 20-byte address into a 32-byte slot //---------------------------------------------------------------------------------------------------------------// - // (4 bytes: 63-66 in deployed contract) - hex"60_43_57_FE", - //---------------------------------------------------------------------------------------------------------------// - // (4) perform actual purging - //---------------------------------------------------------------------------------------------------------------// - // 0x5B | 0x5B | JUMPDEST (67) | // - // 0x60 | 0x60_00 | PUSH1 0 | 0 // - // 0x51 | 0x51 | MLOAD | byteDeployerAddress // - // 0xFF | 0xFF | SELFDESTRUCT | // - //---------------------------------------------------------------------------------------------------------------// - // (5 bytes: 67-71 in deployed contract) - hex"5B_60_00_51_FF", - //---------------------------------------------------------------------------------------------------------------// - // (*) Note: this value must be adjusted if selfdestruct purge logic is adjusted, to refer to the correct start // - // offset for where the `msg.sender` address was stored in deployed bytecode. // - // // - // (^) Note: this value must be adjusted if portions of the selfdestruct purge logic are adjusted. // - //---------------------------------------------------------------------------------------------------------------// - // - // store the deploying-contract's address (to be used to gate and call `selfdestruct`), - // with expected 0-padding to fit a 20-byte address into a 30-byte slot. - // - // note: it is important that this address is the executing contract's address - // (the address that represents the client-application smart contract of this library) - // which means that it is the responsibility of the client-application smart contract - // to determine how deletes are gated (or if they are exposed at all) as it is only - // this contract that will be able to call `purgeBytecode` as the `CALLER` that is - // checked above (op-code 0x33). - hex"00_00_00_00_00_00_00_00_00_00_00_00", // left-pad 20-byte address with 12 0x00 bytes + // (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 @@ -186,7 +103,7 @@ library BytecodeStorage { /** * @notice Read a string from contract bytecode - * @param _address address of deployed contract with bytecode containing concat(gated-cleanup-logic, address, data) + * @param _address address of deployed contract with bytecode containing concat(deployer-address, data) * @return data string read from contract bytecode */ function readFromBytecode( @@ -203,7 +120,7 @@ library BytecodeStorage { revert("ContractAsStorage: Read Error"); } // handle case where address contains code >= DATA_OFFSET - // decrement by DATA_OFFSET to account for purge logic + // decrement by DATA_OFFSET to account for header info uint256 size; unchecked { size = bytecodeSize - DATA_OFFSET; @@ -219,14 +136,14 @@ library BytecodeStorage { 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 gated-cleanup-logic and address + // copy code to memory, excluding the deployer-address extcodecopy(_address, add(data, 0x20), DATA_OFFSET, size) } } /** * @notice Get address for deployer for given contract bytecode - * @param _address address of deployed contract with bytecode containing concat(gated-cleanup-logic, address, data) + * @param _address address of deployed contract with bytecode containing concat(deployer-address, data) * @return writerAddress address read from contract bytecode */ function getWriterAddressForBytecode( @@ -252,7 +169,7 @@ library BytecodeStorage { // 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: - // | gated-cleanup-logic | deployer-address (padded) | data | + // | deployer-address (padded) | data | extcodecopy( _address, writerAddress, @@ -266,35 +183,6 @@ library BytecodeStorage { } } - /*////////////////////////////////////////////////////////////// - DELETE LOGIC - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Purge contract bytecode for cleanup purposes - * 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 (as opposed to using a CREATE2 style opcode when creating bytecode contracts, - * which could be used in a way that may rely on the bytecode being removed from the blockchain state, - * e.g. replacing a contract at a given deployed address). - * @param _address address of deployed contract with bytecode containing concat(gated-cleanup-logic, address, data) - * @dev This contract is only callable by the address of the contract that originally deployed the bytecode - * being purged. If this method is called by any other address, it will revert with the `INVALID` op-code. - * Additionally, for security purposes, the contract must be called with calldata `0xFF` to ensure that - * the `selfdestruct` op-code is intentionally being invoked, otherwise the `INVALID` op-code will be raised. - */ - function purgeBytecode(address _address) internal { - // deployed bytecode (above) handles all logic for purging state, so no - // call data is expected to be passed along to perform data purge - (bool success /* `data` not needed */, ) = _address.call(hex"FF"); - if (!success) { - revert("ContractAsStorage: Delete Error"); - } - } - /*////////////////////////////////////////////////////////////// INTERNAL HELPER LOGIC //////////////////////////////////////////////////////////////*/ From 3000bd0226d849976f9c0af19871530b67165052 Mon Sep 17 00:00:00 2001 From: purp Date: Wed, 26 Apr 2023 19:15:38 -0600 Subject: [PATCH 3/9] fix comment --- contracts/libs/0.8.x/BytecodeStorage.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/libs/0.8.x/BytecodeStorage.sol b/contracts/libs/0.8.x/BytecodeStorage.sol index 2182c8b7f..07d1ab750 100644 --- a/contracts/libs/0.8.x/BytecodeStorage.sol +++ b/contracts/libs/0.8.x/BytecodeStorage.sol @@ -27,7 +27,7 @@ library BytecodeStorage { //---------------------------------------------------------------------------------------------------------------// // 0 | N/A | 0 | // // 0 | 1 | 1 | single byte opcode for making the storage contract non-executable // - // 0 | 1 | 33 | the 32 bytes for storing the deploying contract's (0-padded) address // + // 1 | 32 | 33 | the 32 bytes for storing the deploying contract's (0-padded) address // //---------------------------------------------------------------------------------------------------------------// // Define the offset for where the "logic 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 From 850d1eb1493f5f2d52551eeaa166ad72ffcc58ca Mon Sep 17 00:00:00 2001 From: purp Date: Wed, 26 Apr 2023 20:05:48 -0600 Subject: [PATCH 4/9] Update to reflect TWO VERSIONS (https://www.youtube.com/watch\?v\=diIFhc_Kzng) --- contracts/DependencyRegistryV0.sol | 2 +- contracts/GenArt721CoreV3.sol | 2 +- .../engine/V3/GenArt721CoreV3_Engine.sol | 2 +- .../engine/V3/GenArt721CoreV3_Engine_Flex.sol | 2 +- .../GenArt721CoreV3_Engine_Flex_PROOF.sol | 2 +- ...enArt721CoreV3_Engine_Flex_PROHIBITION.sol | 2 +- .../GenArt721CoreV3_Explorations.sol | 2 +- contracts/libs/0.8.x/BytecodeStorage.sol | 202 ------------- contracts/mock/BytecodeTextCR_DMock.sol | 168 ----------- ...nArt721CoreV3_Engine_IncorrectCoreType.sol | 2 +- ...tecodeStorage_BytecodeTextCR_DMock.test.ts | 278 ------------------ 11 files changed, 8 insertions(+), 656 deletions(-) delete mode 100644 contracts/libs/0.8.x/BytecodeStorage.sol delete mode 100644 contracts/mock/BytecodeTextCR_DMock.sol delete mode 100644 test/libs/BytecodeStorage_BytecodeTextCR_DMock.test.ts diff --git a/contracts/DependencyRegistryV0.sol b/contracts/DependencyRegistryV0.sol index 72ee661c7..d20b6fab0 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"; /** diff --git a/contracts/GenArt721CoreV3.sol b/contracts/GenArt721CoreV3.sol index cf1e9a21b..2cd43ea89 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"; /** diff --git a/contracts/engine/V3/GenArt721CoreV3_Engine.sol b/contracts/engine/V3/GenArt721CoreV3_Engine.sol index 001646e8a..20a238c0a 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"; /** diff --git a/contracts/engine/V3/GenArt721CoreV3_Engine_Flex.sol b/contracts/engine/V3/GenArt721CoreV3_Engine_Flex.sol index b490eef82..94d55ba33 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"; /** diff --git a/contracts/engine/V3/forks/GenArt721CoreV3_Engine_Flex_PROOF.sol b/contracts/engine/V3/forks/GenArt721CoreV3_Engine_Flex_PROOF.sol index b28fdb1b2..5c98a8c0a 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"; /** 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..051a632b8 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"; /** diff --git a/contracts/explorations/GenArt721CoreV3_Explorations.sol b/contracts/explorations/GenArt721CoreV3_Explorations.sol index fe5f87b10..e1b3afdc9 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"; /** diff --git a/contracts/libs/0.8.x/BytecodeStorage.sol b/contracts/libs/0.8.x/BytecodeStorage.sol deleted file mode 100644 index 07d1ab750..000000000 --- a/contracts/libs/0.8.x/BytecodeStorage.sol +++ /dev/null @@ -1,202 +0,0 @@ -// 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 persistant storage for large chunks of script string data. - * - * @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 - * 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. - */ -library BytecodeStorage { - //---------------------------------------------------------------------------------------------------------------// - // 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 bytes for storing the deploying contract's (0-padded) address // - //---------------------------------------------------------------------------------------------------------------// - // Define the offset for where the "logic 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 internal constant DATA_OFFSET = 33; - uint256 internal constant ADDRESS_OFFSET = 1; - - /*////////////////////////////////////////////////////////////// - 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. - * @return address_ address of deployed contract with bytecode containing concat(deployer-address, data) - */ - 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 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"); - } - - /*////////////////////////////////////////////////////////////// - READ LOGIC - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Read a string from contract bytecode - * @param _address address of deployed contract with bytecode containing concat(deployer-address, data) - * @return data string read from contract bytecode - */ - function readFromBytecode( - address _address - ) internal view returns (string memory data) { - // get the size of the bytecode - uint256 bytecodeSize = _bytecodeSizeAt(_address); - // handle case where address contains code < DATA_OFFSET - // note: the first check here also captures the case where - // (bytecodeSize == 0) implicitly, but we add the second check of - // (bytecodeSize == 0) as a fall-through that will never execute - // unless `DATA_OFFSET` is set to 0 at some point. - if ((bytecodeSize < DATA_OFFSET) || (bytecodeSize == 0)) { - revert("ContractAsStorage: Read Error"); - } - // handle case where address contains code >= DATA_OFFSET - // decrement by DATA_OFFSET to account for header info - uint256 size; - unchecked { - size = bytecodeSize - DATA_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), DATA_OFFSET, size) - } - } - - /** - * @notice Get address for deployer for given contract bytecode - * @param _address address of deployed contract with bytecode containing concat(deployer-address, data) - * @return writerAddress address read from contract bytecode - */ - function getWriterAddressForBytecode( - address _address - ) internal view returns (address) { - // get the size of the data - uint256 bytecodeSize = _bytecodeSizeAt(_address); - // handle case where address contains code < DATA_OFFSET - // note: the first check here also captures the case where - // (bytecodeSize == 0) implicitly, but we add the second check of - // (bytecodeSize == 0) as a fall-through that will never execute - // unless `DATA_OFFSET` is set to 0 at some point. - if ((bytecodeSize < DATA_OFFSET) || (bytecodeSize == 0)) { - 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: - // | deployer-address (padded) | data | - extcodecopy( - _address, - writerAddress, - ADDRESS_OFFSET, - 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 - ) - } - } - - /*////////////////////////////////////////////////////////////// - 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) - } - } -} diff --git a/contracts/mock/BytecodeTextCR_DMock.sol b/contracts/mock/BytecodeTextCR_DMock.sol deleted file mode 100644 index b044423e9..000000000 --- a/contracts/mock/BytecodeTextCR_DMock.sol +++ /dev/null @@ -1,168 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -pragma solidity 0.8.17; - -// Created By: Art Blocks Inc. - -import "../libs/0.8.x/BytecodeStorage.sol"; - -/** - * @title Art Blocks BytecodeTextCR_DMock. - * @author Art Blocks Inc. - * @notice This contract serves as a mock client of the BytecodeStorage 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 - * 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 BytecodeTextCR_DMock { - using BytecodeStorage for string; - using BytecodeStorage 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 _ 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 storedTextBytecodeAddresses[_textSlotId].readFromBytecode(); - } - - /** - * @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 _bytecodeAddress.readFromBytecode(); - } - - /** - * @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 _bytecodeAddress.getWriterAddressForBytecode(); - } - - /** - * @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 b92f87471..66c85d242 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"; /** diff --git a/test/libs/BytecodeStorage_BytecodeTextCR_DMock.test.ts b/test/libs/BytecodeStorage_BytecodeTextCR_DMock.test.ts deleted file mode 100644 index eb1e438d2..000000000 --- a/test/libs/BytecodeStorage_BytecodeTextCR_DMock.test.ts +++ /dev/null @@ -1,278 +0,0 @@ -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 { - T_Config, - getAccounts, - deployAndGet, - 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"; - -/** - * Tests for BytecodeStorage by way of testing the BytecodeTextCR_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 () { - // 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, - targetText: string, - bytecodeTextCR_DMock: Contract, - deployer: SignerWithAddress - ) { - const createTextTX = await bytecodeTextCR_DMock - .connect(deployer) - .createText(targetText); - const textSlotId = createTextTX.value.toNumber(); - const text = await bytecodeTextCR_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 - ) { - const nextTextSlotId = await bytecodeTextCR_DMock.nextTextSlotId(); - // decrement from `nextTextSlotId` to get last updated slot - const textSlotId = nextTextSlotId - 1; - const textBytecodeAddress = - await bytecodeTextCR_DMock.storedTextBytecodeAddresses(textSlotId); - return textBytecodeAddress; - } - - async function _beforeEach() { - let config: T_Config = { - accounts: await getAccounts(), - }; - config = await assignDefaultConstants(config); - // deploy the library mock - config.bytecodeTextCR_DMock = await deployAndGet( - config, - "BytecodeTextCR_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.bytecodeTextCR_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.bytecodeTextCR_DMock, - config.accounts.deployer - ); - }); - it("uploads and recalls chromie squiggle script", async function () { - const config = await loadFixture(_beforeEach); - await validateCreateAndRead( - config, - SQUIGGLE_SCRIPT, - config.bytecodeTextCR_DMock, - config.accounts.deployer - ); - }); - it("uploads and recalls different script", async function () { - const config = await loadFixture(_beforeEach); - await validateCreateAndRead( - config, - SKULPTUUR_SCRIPT_APPROX, - config.bytecodeTextCR_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.bytecodeTextCR_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.bytecodeTextCR_DMock, - config.accounts.deployer - ); - const textBytecodeAddress = getLatestTextDeploymentAddress( - config, - config.bytecodeTextCR_DMock - ); - const text = await config.bytecodeTextCR_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.bytecodeTextCR_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.bytecodeTextCR_DMock, - config.accounts.deployer - ); - - const textBytecodeAddress = getLatestTextDeploymentAddress( - config, - config.bytecodeTextCR_DMock - ); - - // deploy a second instance of the library mock - const additionalBytecodeTextCR_DMock = await deployAndGet( - config, - "BytecodeTextCR_DMock", - [] // no deployment args - ); - const text = await additionalBytecodeTextCR_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.bytecodeTextCR_DMock - .connect(config.accounts.deployer) - .createText(targetText, { gasLimit: GAS_LIMIT }); - const textSlotId = createTextTX.value.toNumber(); - const text = await config.bytecodeTextCR_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.bytecodeTextCR_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.bytecodeTextCR_DMock - .connect(config.accounts.deployer) - .createText("cute lil test text hehe"); - const textBytecodeAddress = getLatestTextDeploymentAddress( - config, - config.bytecodeTextCR_DMock - ); - const textAuthorAddress = - await config.bytecodeTextCR_DMock.readAuthorForTextAtAddress( - textBytecodeAddress - ); - const resolvedMockAddress = await config.bytecodeTextCR_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.bytecodeTextCR_DMock.readAuthorForTextAtAddress( - constants.ZERO_ADDRESS - ), - "ContractAsStorage: Read Error" - ); - }); - - it("getWriterAddressForBytecode is interoperable", async function () { - const config = await loadFixture(_beforeEach); - await config.bytecodeTextCR_DMock - .connect(config.accounts.deployer) - .createText("zip zipppity zoooop zop"); - const textBytecodeAddress = getLatestTextDeploymentAddress( - config, - config.bytecodeTextCR_DMock - ); - - // deploy a second instance of the library mock - const additionalBytecodeTextCR_DMock = await deployAndGet( - config, - "BytecodeTextCR_DMock", - [] // no deployment args - ); - const textAuthorAddress = - await additionalBytecodeTextCR_DMock.readAuthorForTextAtAddress( - textBytecodeAddress - ); - const resolvedMockAddress = await config.bytecodeTextCR_DMock - .resolvedAddress; - expect(textAuthorAddress).to.equal(resolvedMockAddress); - }); - }); -}); From f8930bd5880bcc4cdb13bbc1be57129a8aa7bf55 Mon Sep 17 00:00:00 2001 From: purp Date: Wed, 26 Apr 2023 20:05:52 -0600 Subject: [PATCH 5/9] Update to reflect TWO VERSIONS (https://www.youtube.com/watch\?v\=diIFhc_Kzng) --- contracts/libs/0.8.x/BytecodeStorageV0.sol | 314 +++++++++++++ contracts/libs/0.8.x/BytecodeStorageV1.sol | 206 ++++++++ contracts/mock/BytecodeV0TextCR_DMock.sol | 184 ++++++++ contracts/mock/BytecodeV1TextCR_DMock.sol | 168 +++++++ ...codeStorageV0_BytecodeTextCR_DMock.test.ts | 441 ++++++++++++++++++ ...codeStorageV1_BytecodeTextCR_DMock.test.ts | 315 +++++++++++++ 6 files changed, 1628 insertions(+) create mode 100644 contracts/libs/0.8.x/BytecodeStorageV0.sol create mode 100644 contracts/libs/0.8.x/BytecodeStorageV1.sol create mode 100644 contracts/mock/BytecodeV0TextCR_DMock.sol create mode 100644 contracts/mock/BytecodeV1TextCR_DMock.sol create mode 100644 test/libs/BytecodeStorageV0_BytecodeTextCR_DMock.test.ts create mode 100644 test/libs/BytecodeStorageV1_BytecodeTextCR_DMock.test.ts diff --git a/contracts/libs/0.8.x/BytecodeStorageV0.sol b/contracts/libs/0.8.x/BytecodeStorageV0.sol new file mode 100644 index 000000000..fb85ba86c --- /dev/null +++ b/contracts/libs/0.8.x/BytecodeStorageV0.sol @@ -0,0 +1,314 @@ +// 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 persistant storage for large chunks of script string data. + * + * @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 + * - exposes "delete" functionality, allowing no-longer-used storage to be purged from chain state + * - stores the "writer" address (library user) in the deployed contract bytes, which is useful for both: + * a) providing necessary information for safe deletion; and + * b) allowing this to be introspected on-chain + * 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. + */ +library BytecodeStorage { + //---------------------------------------------------------------------------------------------------------------// + // Starting Index | Size | Ending Index | Description // + //---------------------------------------------------------------------------------------------------------------// + // 0 | N/A | 0 | // + // 0 | 72 | 72 | the bytes of the gated-cleanup-logic allowing for `selfdestruct`ion // + // 72 | 32 | 104 | the 32 bytes for storing the deploying contract's (0-padded) address // + //---------------------------------------------------------------------------------------------------------------// + // Define the offset for where the "logic 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 internal constant DATA_OFFSET = 104; + uint256 internal constant ADDRESS_OFFSET = 72; + + /*////////////////////////////////////////////////////////////// + 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. + * @return address_ address of deployed contract with bytecode containing concat(gated-cleanup-logic, address, data) + */ + function writeToBytecode( + string memory _data + ) internal returns (address address_) { + // prefix bytecode with + bytes memory creationCode = abi.encodePacked( + //---------------------------------------------------------------------------------------------------------------// + // Opcode | Opcode + Arguments | Description | Stack View // + //---------------------------------------------------------------------------------------------------------------// + // (0) 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", + //---------------------------------------------------------------------------------------------------------------// + // Opcode | Opcode + Arguments | Description | Stack View // + //---------------------------------------------------------------------------------------------------------------// + // (1a) conditional logic for determing purge-gate (only the bytecode contract deployer can `selfdestruct`) + //---------------------------------------------------------------------------------------------------------------// + // 0x60 | 0x60_20 | PUSH1 32 | 32 // + // 0x60 | 0x60_48 | PUSH1 72 (*) | contractOffset 32 // + // 0x60 | 0x60_00 | PUSH1 0 | 0 contractOffset 32 // + // 0x39 | 0x39 | CODECOPY | // + // 0x60 | 0x60_00 | PUSH1 0 | 0 // + // 0x51 | 0x51 | MLOAD | byteDeployerAddress // + // 0x33 | 0x33 | CALLER | msg.sender byteDeployerAddress // + // 0x14 | 0x14 | EQ | (msg.sender == byteDeployerAddress) // + //---------------------------------------------------------------------------------------------------------------// + // (12 bytes: 0-11 in deployed contract) + hex"60_20_60_48_60_00_39_60_00_51_33_14", + //---------------------------------------------------------------------------------------------------------------// + // (1b) load up the destination jump address for `(2a) calldata length check` logic, jump or raise `invalid` op-code + //---------------------------------------------------------------------------------------------------------------// + // 0x60 | 0x60_10 | PUSH1 16 (^) | jumpDestination (msg.sender == byteDeployerAddress) // + // 0x57 | 0x57 | JUMPI | // + // 0xFE | 0xFE | INVALID | // + //---------------------------------------------------------------------------------------------------------------// + // (4 bytes: 12-15 in deployed contract) + hex"60_10_57_FE", + //---------------------------------------------------------------------------------------------------------------// + // (2a) conditional logic for determing purge-gate (only if calldata length is 1 byte) + //---------------------------------------------------------------------------------------------------------------// + // 0x5B | 0x5B | JUMPDEST (16) | // + // 0x60 | 0x60_01 | PUSH1 1 | 1 // + // 0x36 | 0x36 | CALLDATASIZE | calldataSize 1 // + // 0x14 | 0x14 | EQ | (calldataSize == 1) // + //---------------------------------------------------------------------------------------------------------------// + // (5 bytes: 16-20 in deployed contract) + hex"5B_60_01_36_14", + //---------------------------------------------------------------------------------------------------------------// + // (2b) load up the destination jump address for `(3a) calldata value check` logic, jump or raise `invalid` op-code + //---------------------------------------------------------------------------------------------------------------// + // 0x60 | 0x60_19 | PUSH1 25 (^) | jumpDestination (calldataSize == 1) // + // 0x57 | 0x57 | JUMPI | // + // 0xFE | 0xFE | INVALID | // + //---------------------------------------------------------------------------------------------------------------// + // (4 bytes: 21-24 in deployed contract) + hex"60_19_57_FE", + //---------------------------------------------------------------------------------------------------------------// + // (3a) conditional logic for determing purge-gate (only if calldata is `0xFF`) + //---------------------------------------------------------------------------------------------------------------// + // 0x5B | 0x5B | JUMPDEST (25) | // + // 0x60 | 0x60_00 | PUSH1 0 | 0 // + // 0x35 | 0x35 | CALLDATALOAD | calldata // + // 0x7F | 0x7F_FF_00_..._00 | PUSH32 0xFF00...00 | 0xFF0...00 calldata // + // 0x14 | 0x14 | EQ | (0xFF00...00 == calldata) // + //---------------------------------------------------------------------------------------------------------------// + // (4 bytes: 25-28 in deployed contract) + hex"5B_60_00_35", + // (33 bytes: 29-61 in deployed contract) + hex"7F_FF_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00", + // (1 byte: 62 in deployed contract) + hex"14", + //---------------------------------------------------------------------------------------------------------------// + // (3b) load up the destination jump address for actual purging (4), jump or raise `invalid` op-code + //---------------------------------------------------------------------------------------------------------------// + // 0x60 | 0x60_43 | PUSH1 67 (^) | jumpDestination (0xFF00...00 == calldata) // + // 0x57 | 0x57 | JUMPI | // + // 0xFE | 0xFE | INVALID | // + //---------------------------------------------------------------------------------------------------------------// + // (4 bytes: 63-66 in deployed contract) + hex"60_43_57_FE", + //---------------------------------------------------------------------------------------------------------------// + // (4) perform actual purging + //---------------------------------------------------------------------------------------------------------------// + // 0x5B | 0x5B | JUMPDEST (67) | // + // 0x60 | 0x60_00 | PUSH1 0 | 0 // + // 0x51 | 0x51 | MLOAD | byteDeployerAddress // + // 0xFF | 0xFF | SELFDESTRUCT | // + //---------------------------------------------------------------------------------------------------------------// + // (5 bytes: 67-71 in deployed contract) + hex"5B_60_00_51_FF", + //---------------------------------------------------------------------------------------------------------------// + // (*) Note: this value must be adjusted if selfdestruct purge logic is adjusted, to refer to the correct start // + // offset for where the `msg.sender` address was stored in deployed bytecode. // + // // + // (^) Note: this value must be adjusted if portions of the selfdestruct purge logic are adjusted. // + //---------------------------------------------------------------------------------------------------------------// + // + // store the deploying-contract's address (to be used to gate and call `selfdestruct`), + // with expected 0-padding to fit a 20-byte address into a 30-byte slot. + // + // note: it is important that this address is the executing contract's address + // (the address that represents the client-application smart contract of this library) + // which means that it is the responsibility of the client-application smart contract + // to determine how deletes are gated (or if they are exposed at all) as it is only + // this contract that will be able to call `purgeBytecode` as the `CALLER` that is + // checked above (op-code 0x33). + hex"00_00_00_00_00_00_00_00_00_00_00_00", // left-pad 20-byte address with 12 0x00 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"); + } + + /*////////////////////////////////////////////////////////////// + READ LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Read a string from contract bytecode + * @param _address address of deployed contract with bytecode containing concat(gated-cleanup-logic, address, data) + * @return data string read from contract bytecode + */ + function readFromBytecode( + address _address + ) internal view returns (string memory data) { + // get the size of the bytecode + uint256 bytecodeSize = _bytecodeSizeAt(_address); + // handle case where address contains code < DATA_OFFSET + // note: the first check here also captures the case where + // (bytecodeSize == 0) implicitly, but we add the second check of + // (bytecodeSize == 0) as a fall-through that will never execute + // unless `DATA_OFFSET` is set to 0 at some point. + if ((bytecodeSize < DATA_OFFSET) || (bytecodeSize == 0)) { + revert("ContractAsStorage: Read Error"); + } + // handle case where address contains code >= DATA_OFFSET + // decrement by DATA_OFFSET to account for purge logic + uint256 size; + unchecked { + size = bytecodeSize - DATA_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 gated-cleanup-logic and address + extcodecopy(_address, add(data, 0x20), DATA_OFFSET, size) + } + } + + /** + * @notice Get address for deployer for given contract bytecode + * @param _address address of deployed contract with bytecode containing concat(gated-cleanup-logic, address, data) + * @return writerAddress address read from contract bytecode + */ + function getWriterAddressForBytecode( + address _address + ) internal view returns (address) { + // get the size of the data + uint256 bytecodeSize = _bytecodeSizeAt(_address); + // handle case where address contains code < DATA_OFFSET + // note: the first check here also captures the case where + // (bytecodeSize == 0) implicitly, but we add the second check of + // (bytecodeSize == 0) as a fall-through that will never execute + // unless `DATA_OFFSET` is set to 0 at some point. + if ((bytecodeSize < DATA_OFFSET) || (bytecodeSize == 0)) { + 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: + // | gated-cleanup-logic | deployer-address (padded) | data | + extcodecopy( + _address, + writerAddress, + ADDRESS_OFFSET, + 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 + ) + } + } + + /*////////////////////////////////////////////////////////////// + DELETE LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Purge contract bytecode for cleanup purposes + * 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 (as opposed to using a CREATE2 style opcode when creating bytecode contracts, + * which could be used in a way that may rely on the bytecode being removed from the blockchain state, + * e.g. replacing a contract at a given deployed address). + * @param _address address of deployed contract with bytecode containing concat(gated-cleanup-logic, address, data) + * @dev This contract is only callable by the address of the contract that originally deployed the bytecode + * being purged. If this method is called by any other address, it will revert with the `INVALID` op-code. + * Additionally, for security purposes, the contract must be called with calldata `0xFF` to ensure that + * the `selfdestruct` op-code is intentionally being invoked, otherwise the `INVALID` op-code will be raised. + */ + function purgeBytecode(address _address) internal { + // deployed bytecode (above) handles all logic for purging state, so no + // call data is expected to be passed along to perform data purge + (bool success /* `data` not needed */, ) = _address.call(hex"FF"); + if (!success) { + revert("ContractAsStorage: Delete Error"); + } + } + + /*////////////////////////////////////////////////////////////// + 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) + } + } +} diff --git a/contracts/libs/0.8.x/BytecodeStorageV1.sol b/contracts/libs/0.8.x/BytecodeStorageV1.sol new file mode 100644 index 000000000..5524d2f5f --- /dev/null +++ b/contracts/libs/0.8.x/BytecodeStorageV1.sol @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// Created By: Art Blocks Inc. + +pragma solidity ^0.8.0; + +//---------------------------------------------------------------------------------------------------------------// +// NOTE: This library version is still an active work-in-progress. +//---------------------------------------------------------------------------------------------------------------// + +/** + * @title Art Blocks Script Storage Library + * @notice Utilize contract bytecode as persistant storage for large chunks of script string data. + * + * @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 + * 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. + */ +library BytecodeStorage { + //---------------------------------------------------------------------------------------------------------------// + // 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 bytes for storing the deploying contract's (0-padded) address // + //---------------------------------------------------------------------------------------------------------------// + // Define the offset for where the "logic 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 internal constant DATA_OFFSET = 33; + uint256 internal constant ADDRESS_OFFSET = 1; + + /*////////////////////////////////////////////////////////////// + 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. + * @return address_ address of deployed contract with bytecode containing concat(deployer-address, data) + */ + 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 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"); + } + + /*////////////////////////////////////////////////////////////// + READ LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Read a string from contract bytecode + * @param _address address of deployed contract with bytecode containing concat(deployer-address, data) + * @return data string read from contract bytecode + */ + function readFromBytecode( + address _address + ) internal view returns (string memory data) { + // get the size of the bytecode + uint256 bytecodeSize = _bytecodeSizeAt(_address); + // handle case where address contains code < DATA_OFFSET + // note: the first check here also captures the case where + // (bytecodeSize == 0) implicitly, but we add the second check of + // (bytecodeSize == 0) as a fall-through that will never execute + // unless `DATA_OFFSET` is set to 0 at some point. + if ((bytecodeSize < DATA_OFFSET) || (bytecodeSize == 0)) { + revert("ContractAsStorage: Read Error"); + } + // handle case where address contains code >= DATA_OFFSET + // decrement by DATA_OFFSET to account for header info + uint256 size; + unchecked { + size = bytecodeSize - DATA_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), DATA_OFFSET, size) + } + } + + /** + * @notice Get address for deployer for given contract bytecode + * @param _address address of deployed contract with bytecode containing concat(deployer-address, data) + * @return writerAddress address read from contract bytecode + */ + function getWriterAddressForBytecode( + address _address + ) internal view returns (address) { + // get the size of the data + uint256 bytecodeSize = _bytecodeSizeAt(_address); + // handle case where address contains code < DATA_OFFSET + // note: the first check here also captures the case where + // (bytecodeSize == 0) implicitly, but we add the second check of + // (bytecodeSize == 0) as a fall-through that will never execute + // unless `DATA_OFFSET` is set to 0 at some point. + if ((bytecodeSize < DATA_OFFSET) || (bytecodeSize == 0)) { + 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: + // | deployer-address (padded) | data | + extcodecopy( + _address, + writerAddress, + ADDRESS_OFFSET, + 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 + ) + } + } + + /*////////////////////////////////////////////////////////////// + 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) + } + } +} diff --git a/contracts/mock/BytecodeV0TextCR_DMock.sol b/contracts/mock/BytecodeV0TextCR_DMock.sol new file mode 100644 index 000000000..3a4745b67 --- /dev/null +++ b/contracts/mock/BytecodeV0TextCR_DMock.sol @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.17; + +// Created By: Art Blocks Inc. + +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 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 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 + * 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 BytecodeV0TextCR_DMock { + using BytecodeStorage for string; + using BytecodeStorage 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 _ 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 storedTextBytecodeAddresses[_textSlotId].readFromBytecode(); + } + + /** + * @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 { + // purge old contract bytecode contract from the blockchain state + storedTextBytecodeAddresses[_textSlotId].purgeBytecode(); + // delete reference to contract address that no longer exists + 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 _bytecodeAddress.readFromBytecode(); + } + + /** + * @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 _bytecodeAddress.getWriterAddressForBytecode(); + } + + /** + * @notice Allows additional delete introspection, to delete a chunk of text, + * from chain-state that lives at a given deployed address. + * @param _bytecodeAddress address from which to delete text content. + * @dev Intentionally do not perform input validation, instead allowing + * the underlying BytecodeStorage lib to throw errors where applicable. + */ + function deleteTextAtAddress( + address _bytecodeAddress + ) external onlyDeployer { + // purge old contract bytecode contract from the blockchain state + _bytecodeAddress.purgeBytecode(); + } + + /** + * @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/BytecodeV1TextCR_DMock.sol b/contracts/mock/BytecodeV1TextCR_DMock.sol new file mode 100644 index 000000000..409fe90dc --- /dev/null +++ b/contracts/mock/BytecodeV1TextCR_DMock.sol @@ -0,0 +1,168 @@ +// 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 BytecodeStorage for string; + using BytecodeStorage 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 _ 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 storedTextBytecodeAddresses[_textSlotId].readFromBytecode(); + } + + /** + * @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 _bytecodeAddress.readFromBytecode(); + } + + /** + * @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 _bytecodeAddress.getWriterAddressForBytecode(); + } + + /** + * @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/test/libs/BytecodeStorageV0_BytecodeTextCR_DMock.test.ts b/test/libs/BytecodeStorageV0_BytecodeTextCR_DMock.test.ts new file mode 100644 index 000000000..2a170d7bc --- /dev/null +++ b/test/libs/BytecodeStorageV0_BytecodeTextCR_DMock.test.ts @@ -0,0 +1,441 @@ +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 { + T_Config, + getAccounts, + deployAndGet, + 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"; + +/** + * Tests for BytecodeStorage 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("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, + targetText: string, + bytecodeV0TextCR_DMock: Contract, + deployer: SignerWithAddress + ) { + const createTextTX = await bytecodeV0TextCR_DMock + .connect(deployer) + .createText(targetText); + const textSlotId = createTextTX.value.toNumber(); + 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, + 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; + } + + async function _beforeEach() { + let config: T_Config = { + accounts: await getAccounts(), + }; + config = await assignDefaultConstants(config); + // deploy the library mock + config.bytecodeV0TextCR_DMock = await deployAndGet( + config, + "BytecodeV0TextCR_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.bytecodeV0TextCR_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.bytecodeV0TextCR_DMock, + config.accounts.deployer + ); + }); + it("uploads and recalls chromie squiggle script", async function () { + const config = await loadFixture(_beforeEach); + await validateCreateAndRead( + config, + SQUIGGLE_SCRIPT, + config.bytecodeV0TextCR_DMock, + config.accounts.deployer + ); + }); + it("uploads and recalls different script", async function () { + const config = await loadFixture(_beforeEach); + await validateCreateAndRead( + config, + SKULPTUUR_SCRIPT_APPROX, + config.bytecodeV0TextCR_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.bytecodeV0TextCR_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.bytecodeV0TextCR_DMock, + config.accounts.deployer + ); + const textBytecodeAddress = getLatestTextDeploymentAddress( + config, + config.bytecodeV0TextCR_DMock + ); + const text = await config.bytecodeV0TextCR_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.bytecodeV0TextCR_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.bytecodeV0TextCR_DMock, + config.accounts.deployer + ); + + const textBytecodeAddress = getLatestTextDeploymentAddress( + config, + config.bytecodeV0TextCR_DMock + ); + + // deploy a second instance of the library mock + const additionalBytecodeV0TextCR_DMock = await deployAndGet( + config, + "BytecodeV0TextCR_DMock", + [] // no deployment args + ); + const text = await additionalBytecodeV0TextCR_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.bytecodeV0TextCR_DMock + .connect(config.accounts.deployer) + .createText(targetText, { gasLimit: GAS_LIMIT }); + const textSlotId = createTextTX.value.toNumber(); + const text = await config.bytecodeV0TextCR_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.bytecodeV0TextCR_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.bytecodeV0TextCR_DMock + .connect(config.accounts.deployer) + .createText("cute lil test text hehe"); + const textBytecodeAddress = getLatestTextDeploymentAddress( + config, + config.bytecodeV0TextCR_DMock + ); + const textAuthorAddress = + await config.bytecodeV0TextCR_DMock.readAuthorForTextAtAddress( + textBytecodeAddress + ); + const resolvedMockAddress = await config.bytecodeV0TextCR_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.bytecodeV0TextCR_DMock.readAuthorForTextAtAddress( + constants.ZERO_ADDRESS + ), + "ContractAsStorage: Read Error" + ); + }); + + 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 = getLatestTextDeploymentAddress( + config, + config.bytecodeV0TextCR_DMock + ); + + // deploy a second instance of the library mock + const additionalBytecodeV0TextCR_DMock = await deployAndGet( + config, + "BytecodeV0TextCR_DMock", + [] // no deployment args + ); + const textAuthorAddress = + await additionalBytecodeV0TextCR_DMock.readAuthorForTextAtAddress( + textBytecodeAddress + ); + const resolvedMockAddress = await config.bytecodeV0TextCR_DMock + .resolvedAddress; + expect(textAuthorAddress).to.equal(resolvedMockAddress); + }); + }); + + describe("validate purgeBytecode behavior", function () { + it("writes text, and then purges it", async function () { + const config = await loadFixture(_beforeEach); + const targetText = "silly willy billy dilly dilly"; + await validateCreateAndRead( + config, + targetText, + config.bytecodeV0TextCR_DMock, + config.accounts.deployer + ); + + const textBytecodeAddress = getLatestTextDeploymentAddress( + config, + config.bytecodeV0TextCR_DMock + ); + + const deployedBytecode = await ethers.provider.getCode( + textBytecodeAddress + ); + expect(deployedBytecode).to.not.equal("0x"); + + const nextTextSlotId = + await config.bytecodeV0TextCR_DMock.nextTextSlotId(); + // decrement from `nextTextSlotId` to get last updated slot + const textSlotId = nextTextSlotId - 1; + await config.bytecodeV0TextCR_DMock + .connect(config.accounts.deployer) + .deleteText(textSlotId); + + const removedBytecode = await ethers.provider.getCode( + textBytecodeAddress + ); + expect(removedBytecode).to.equal("0x"); + }); + + it("SELFDESTRUCT via direct call data possible with 0xFF", async function () { + const config = await loadFixture(_beforeEach); + const targetText = "silly willy billy dilly dilly"; + await validateCreateAndRead( + config, + targetText, + config.bytecodeV0TextCR_DMock, + config.accounts.deployer + ); + + const textBytecodeAddress = getLatestTextDeploymentAddress( + config, + config.bytecodeV0TextCR_DMock + ); + + const deployedBytecode = await ethers.provider.getCode( + textBytecodeAddress + ); + expect(deployedBytecode).to.not.equal("0x"); + + await config.bytecodeV0TextCR_DMock + .connect(config.accounts.deployer) + .callWithNonsenseData(textBytecodeAddress, "0xFF"); + + const removedBytecode = await ethers.provider.getCode( + textBytecodeAddress + ); + expect(removedBytecode).to.equal("0x"); + }); + + it("SELFDESTRUCT is NOT possible via call-data prodding", async function () { + const config = await loadFixture(_beforeEach); + const targetText = "silly willy billy dilly dilly"; + await validateCreateAndRead( + config, + targetText, + config.bytecodeV0TextCR_DMock, + config.accounts.deployer + ); + + const textBytecodeAddress = getLatestTextDeploymentAddress( + config, + config.bytecodeV0TextCR_DMock + ); + + const deployedBytecode = await ethers.provider.getCode( + textBytecodeAddress + ); + expect(deployedBytecode).to.not.equal("0x"); + + // Non-writer addresses should **not** be able to purge bytecode storage. + await expectRevert.unspecified( + config.accounts.deployer.call({ + to: textBytecodeAddress, + }) + ); + // And config is still the case when correct `0xFF` bytes are sent along. + await expectRevert.unspecified( + config.accounts.deployer.call({ + to: textBytecodeAddress, + data: "0xFF", + }) + ); + // 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.bytecodeV0TextCR_DMock + .connect(config.accounts.deployer) + .callWithNonsenseData(textBytecodeAddress, "0xFFFF"); + await config.bytecodeV0TextCR_DMock + .connect(config.accounts.deployer) + .callWithNonsenseData(textBytecodeAddress, "0x00FF"); + await config.bytecodeV0TextCR_DMock + .connect(config.accounts.deployer) + .callWithNonsenseData(textBytecodeAddress, "0xFE"); + await config.bytecodeV0TextCR_DMock + .connect(config.accounts.deployer) + .callWithNonsenseData(textBytecodeAddress, "0x00"); + await config.bytecodeV0TextCR_DMock + .connect(config.accounts.deployer) + .callWithoutData(textBytecodeAddress); + + // Deployed bytes are unchanged. + const notRemovedBytecode = await ethers.provider.getCode( + textBytecodeAddress + ); + expect(notRemovedBytecode).to.equal(deployedBytecode); + expect(notRemovedBytecode).to.not.equal("0x"); + }); + + it("purgeBytecode is *not* interoperable", async function () { + const config = await loadFixture(_beforeEach); + await config.bytecodeV0TextCR_DMock + .connect(config.accounts.deployer) + .createText("beeeep boop bop bop bop beeeep bop"); + const textBytecodeAddress = getLatestTextDeploymentAddress( + config, + config.bytecodeV0TextCR_DMock + ); + + const deployedBytecode = await ethers.provider.getCode( + textBytecodeAddress + ); + expect(deployedBytecode).to.not.equal("0x"); + + // deploy a second instance of the library mock + const additionalBytecodeV0TextCR_DMock = await deployAndGet( + config, + "BytecodeV0TextCR_DMock", + [] // no deployment args + ); + + await expectRevert( + additionalBytecodeV0TextCR_DMock + .connect(config.accounts.deployer) + .deleteTextAtAddress(textBytecodeAddress), + "ContractAsStorage: Delete Error" + ); + + // Deployed bytes are unchanged. + const notRemovedBytecode = await ethers.provider.getCode( + textBytecodeAddress + ); + expect(notRemovedBytecode).to.equal(deployedBytecode); + expect(notRemovedBytecode).to.not.equal("0x"); + }); + }); +}); diff --git a/test/libs/BytecodeStorageV1_BytecodeTextCR_DMock.test.ts b/test/libs/BytecodeStorageV1_BytecodeTextCR_DMock.test.ts new file mode 100644 index 000000000..e29e5fd8a --- /dev/null +++ b/test/libs/BytecodeStorageV1_BytecodeTextCR_DMock.test.ts @@ -0,0 +1,315 @@ +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 { + T_Config, + getAccounts, + deployAndGet, + 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"; + +/** + * Tests for BytecodeStorage 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: T_Config, + 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: T_Config, + 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: T_Config = { + accounts: await getAccounts(), + }; + config = await assignDefaultConstants(config); + // deploy the library mock + config.bytecodeV1TextCR_DMock = await deployAndGet( + 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 deployAndGet( + 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 deployAndGet( + 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); + }); + }); +}); From 4c79417b40ab43007bacdf789be6f401851b466f Mon Sep 17 00:00:00 2001 From: Jake Rockland Date: Wed, 26 Apr 2023 23:55:21 -0600 Subject: [PATCH 6/9] Update contracts/libs/0.8.x/BytecodeStorageV1.sol Co-authored-by: ryley-o <30364988+ryley-o@users.noreply.github.com> --- contracts/libs/0.8.x/BytecodeStorageV1.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/libs/0.8.x/BytecodeStorageV1.sol b/contracts/libs/0.8.x/BytecodeStorageV1.sol index 5524d2f5f..e2e0f3e8c 100644 --- a/contracts/libs/0.8.x/BytecodeStorageV1.sol +++ b/contracts/libs/0.8.x/BytecodeStorageV1.sol @@ -46,7 +46,7 @@ library BytecodeStorage { /** * @notice Write a string to contract bytecode * @param _data string to be written to contract. No input validation is performed on this parameter. - * @return address_ address of deployed contract with bytecode containing concat(deployer-address, data) + * @return address_ address of deployed contract with bytecode containing concat(invalid opcode, deployer-address, data) */ function writeToBytecode( string memory _data From 82d6f471437f96d849bb44894de7548a0e13e0ad Mon Sep 17 00:00:00 2001 From: Jake Rockland Date: Wed, 26 Apr 2023 23:55:28 -0600 Subject: [PATCH 7/9] Update contracts/libs/0.8.x/BytecodeStorageV1.sol Co-authored-by: ryley-o <30364988+ryley-o@users.noreply.github.com> --- contracts/libs/0.8.x/BytecodeStorageV1.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/libs/0.8.x/BytecodeStorageV1.sol b/contracts/libs/0.8.x/BytecodeStorageV1.sol index e2e0f3e8c..b7f7905bf 100644 --- a/contracts/libs/0.8.x/BytecodeStorageV1.sol +++ b/contracts/libs/0.8.x/BytecodeStorageV1.sol @@ -147,7 +147,7 @@ library BytecodeStorage { /** * @notice Get address for deployer for given contract bytecode - * @param _address address of deployed contract with bytecode containing concat(deployer-address, data) + * @param _address address of deployed contract with bytecode containing concat(invalid opcode, deployer-address, data) * @return writerAddress address read from contract bytecode */ function getWriterAddressForBytecode( From 1dbcef443283d98b0c10c00d9bd3130f8c96f088 Mon Sep 17 00:00:00 2001 From: Jake Rockland Date: Wed, 26 Apr 2023 23:55:34 -0600 Subject: [PATCH 8/9] Update contracts/libs/0.8.x/BytecodeStorageV1.sol Co-authored-by: ryley-o <30364988+ryley-o@users.noreply.github.com> --- contracts/libs/0.8.x/BytecodeStorageV1.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/libs/0.8.x/BytecodeStorageV1.sol b/contracts/libs/0.8.x/BytecodeStorageV1.sol index b7f7905bf..c23c60acb 100644 --- a/contracts/libs/0.8.x/BytecodeStorageV1.sol +++ b/contracts/libs/0.8.x/BytecodeStorageV1.sol @@ -107,7 +107,7 @@ library BytecodeStorage { /** * @notice Read a string from contract bytecode - * @param _address address of deployed contract with bytecode containing concat(deployer-address, data) + * @param _address address of deployed contract with bytecode containing concat(invalid opcode, deployer-address, data) * @return data string read from contract bytecode */ function readFromBytecode( From 2ddec19448ab4aae9130fc061d79168cf07388f3 Mon Sep 17 00:00:00 2001 From: Jake Rockland Date: Fri, 5 May 2023 08:19:15 -0600 Subject: [PATCH 9/9] Add basic form of `BytecodeStorage` library versioning (#670) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Support basic semantic versioning – and ensure interop with backwards compatible reads (tests added) * update the version string * minor adjustment and a song: https://www.youtube.com/watch\?v\=vOreqez4v9Y * Update to reflect better v1/v0/unknown versioning semantics * Update documentation and code structuring * Refactor size == 0 check * clean up offset checks * `STORE2`-compatible reads in `BytecodeStorage` (#681) * Update BytecodeStorage library to provide backwards-compatible reads that are compatible with SSTORE2 as the fallback read option, in advance of plans to split off reads into a shared external public utility library, in-companion to the embedded internal library for writes. * Whoops, forgot to add all the files yo * add support for manual-offset reads * SSTORE2 / explicit bytes reads restructure * Comment fix * Filename fix * Filename fix * Better typing for tests * spelling fixes * Split `BytecodeStorage` into public/internal libraries (#684) * basic MVP of split library (with tests) * Adjusted tests for split library setup * minor modifier adjustment * Adjust optimizer runs * optimizer order * nit * get interface from deployed contract * get interface from deployed contract * ensure libraries are linked * library linkkinnnn * More linking fixes in tests * update Engine Flex as PoC (#688) * fix the rest of the test bindingsgit diff * OPTIMIZOOOOOR * remove unnecessary using for * Fixed comment * public constants * Update library naming convention * DEPLOYOOOOOR * DEPLOYOOOOOR * format * Address nits --------- Co-authored-by: ryley-o <30364988+ryley-o@users.noreply.github.com> --------- Co-authored-by: ryley-o <30364988+ryley-o@users.noreply.github.com> --- README.md | 10 + contracts/DependencyRegistryV0.sol | 15 +- contracts/GenArt721CoreV3.sol | 15 +- .../engine/V3/GenArt721CoreV3_Engine.sol | 15 +- .../engine/V3/GenArt721CoreV3_Engine_Flex.sol | 17 +- .../GenArt721CoreV3_Engine_Flex_PROOF.sol | 17 +- ...enArt721CoreV3_Engine_Flex_PROHIBITION.sol | 17 +- .../GenArt721CoreV3_Explorations.sol | 15 +- contracts/libs/0.8.x/BytecodeStorageV1.sol | 389 ++++++++++++----- contracts/libs/0.8.x/SSTORE2.sol | 109 +++++ contracts/mock/BytecodeV1TextCR_DMock.sol | 79 +++- ...nArt721CoreV3_Engine_IncorrectCoreType.sol | 15 +- contracts/mock/SSTORE2Mock.sol | 93 +++++ .../staging-v3-react-demo-2/DEPLOYMENTS.md | 42 ++ .../DEPLOYMENT_LOGS.log | 66 +++ .../deployment-config.staging.ts | 50 +++ hardhat.solidity-config.ts | 8 +- .../bytecode-storage-deployer-goerli.ts | 70 ++++ .../bytecode-storage-deployer-mainnet.ts | 70 ++++ scripts/engine/V3/generic-engine-deployer.ts | 19 +- scripts/util/constants.ts | 7 + .../GenArt721CoreV3_AdminACLRequests.test.ts | 8 +- test/core/V3/GenArt721CoreV3_Events.test.ts | 47 ++- .../V3/GenArt721CoreV3_Integration.test.ts | 9 +- .../GenArt721CoreV3_ProjectConfigure.test.ts | 27 +- ...oreV3_AdminACLRequests_PROHIBITION.test.ts | 8 +- ...GenArt721CoreV3_Events_PROHIBITION.test.ts | 47 ++- ...t721CoreV3_Integration_PROHIBITION.test.ts | 9 +- ...oreV3_ProjectConfigure_PROHIBITION.test.ts | 27 +- .../DependencyRegistryV0.test.ts | 3 +- ...codeStorageV0_BytecodeTextCR_DMock.test.ts | 15 +- ...StorageV1_BackwardsCompatibleReads.test.ts | 393 ++++++++++++++++++ ...codeStorageV1_BytecodeTextCR_DMock.test.ts | 41 +- test/minter-suite-minters/Minter.common.ts | 19 +- test/util/common.ts | 74 +++- 35 files changed, 1604 insertions(+), 261 deletions(-) create mode 100644 contracts/libs/0.8.x/SSTORE2.sol create mode 100644 contracts/mock/SSTORE2Mock.sol create mode 100644 deployments/engine/V3/internal-testing/staging-v3-react-demo-2/DEPLOYMENTS.md create mode 100644 deployments/engine/V3/internal-testing/staging-v3-react-demo-2/DEPLOYMENT_LOGS.log create mode 100644 deployments/engine/V3/internal-testing/staging-v3-react-demo-2/deployment-config.staging.ts create mode 100644 scripts/bytecode-storage/bytecode-storage-deployer-goerli.ts create mode 100644 scripts/bytecode-storage/bytecode-storage-deployer-mainnet.ts create mode 100644 test/libs/BytecodeStorageV1_BackwardsCompatibleReads.test.ts 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 d20b6fab0..9e646651b 100644 --- a/contracts/DependencyRegistryV0.sol +++ b/contracts/DependencyRegistryV0.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; @@ -735,7 +734,7 @@ contract DependencyRegistryV0 is return ""; } - return dependency.scriptBytecodeAddresses[_index].readFromBytecode(); + return _readFromBytecode(dependency.scriptBytecodeAddresses[_index]); } /** @@ -829,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 2cd43ea89..bb3e7b852 100644 --- a/contracts/GenArt721CoreV3.sol +++ b/contracts/GenArt721CoreV3.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; @@ -1493,7 +1492,7 @@ contract GenArt721CoreV3 is if (_index >= project.scriptCount) { return ""; } - return project.scriptBytecodeAddresses[_index].readFromBytecode(); + return _readFromBytecode(project.scriptBytecodeAddresses[_index]); } /** @@ -1945,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 20a238c0a..f19862cdf 100644 --- a/contracts/engine/V3/GenArt721CoreV3_Engine.sol +++ b/contracts/engine/V3/GenArt721CoreV3_Engine.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; @@ -1577,7 +1576,7 @@ contract GenArt721CoreV3_Engine is if (_index >= project.scriptCount) { return ""; } - return project.scriptBytecodeAddresses[_index].readFromBytecode(); + return _readFromBytecode(project.scriptBytecodeAddresses[_index]); } /** @@ -1996,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 94d55ba33..f88ccb319 100644 --- a/contracts/engine/V3/GenArt721CoreV3_Engine_Flex.sol +++ b/contracts/engine/V3/GenArt721CoreV3_Engine_Flex.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; @@ -1763,7 +1762,7 @@ contract GenArt721CoreV3_Engine_Flex is if (_index >= project.scriptCount) { return ""; } - return project.scriptBytecodeAddresses[_index].readFromBytecode(); + return _readFromBytecode(project.scriptBytecodeAddresses[_index]); } /** @@ -1994,7 +1993,7 @@ contract GenArt721CoreV3_Engine_Flex is bytecodeAddress: _bytecodeAddress, data: (_dependency.dependencyType == ExternalAssetDependencyType.ONCHAIN) - ? _bytecodeAddress.readFromBytecode() + ? _readFromBytecode(_bytecodeAddress) : "" }); } @@ -2217,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 5c98a8c0a..8db581aac 100644 --- a/contracts/engine/V3/forks/GenArt721CoreV3_Engine_Flex_PROOF.sol +++ b/contracts/engine/V3/forks/GenArt721CoreV3_Engine_Flex_PROOF.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 051a632b8..71244a7c9 100644 --- a/contracts/engine/V3/forks/PROHIBITION/GenArt721CoreV3_Engine_Flex_PROHIBITION.sol +++ b/contracts/engine/V3/forks/PROHIBITION/GenArt721CoreV3_Engine_Flex_PROHIBITION.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 e1b3afdc9..ac2197b34 100644 --- a/contracts/explorations/GenArt721CoreV3_Explorations.sol +++ b/contracts/explorations/GenArt721CoreV3_Explorations.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; @@ -1499,7 +1498,7 @@ contract GenArt721CoreV3_Explorations is if (_index >= project.scriptCount) { return ""; } - return project.scriptBytecodeAddresses[_index].readFromBytecode(); + return _readFromBytecode(project.scriptBytecodeAddresses[_index]); } /** @@ -1951,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/BytecodeStorageV1.sol b/contracts/libs/0.8.x/BytecodeStorageV1.sol index c23c60acb..00f1db6c1 100644 --- a/contracts/libs/0.8.x/BytecodeStorageV1.sol +++ b/contracts/libs/0.8.x/BytecodeStorageV1.sol @@ -3,13 +3,22 @@ pragma solidity ^0.8.0; -//---------------------------------------------------------------------------------------------------------------// -// NOTE: This library version is still an active work-in-progress. -//---------------------------------------------------------------------------------------------------------------// - /** * @title Art Blocks Script Storage Library - * @notice Utilize contract bytecode as persistant storage for large chunks of script string data. + * @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) @@ -20,86 +29,67 @@ pragma solidity ^0.8.0; * - 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. */ -library BytecodeStorage { + +/** + * @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 bytes for storing the deploying contract's (0-padded) address // + // 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 "logic bytes" end, and the "data bytes" begin. Note that this is a manually + // 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 internal constant DATA_OFFSET = 33; - uint256 internal constant ADDRESS_OFFSET = 1; + uint256 private constant VERSION_OFFSET = 1; + uint256 private constant ADDRESS_OFFSET = 33; + uint256 private constant DATA_OFFSET = 65; - /*////////////////////////////////////////////////////////////// - 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. - * @return address_ address of deployed contract with bytecode containing concat(invalid opcode, deployer-address, data) - */ - 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 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"); - } + // 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 @@ -107,27 +97,54 @@ library BytecodeStorage { /** * @notice Read a string from contract bytecode - * @param _address address of deployed contract with bytecode containing concat(invalid opcode, deployer-address, data) + * @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 - ) internal view returns (string memory data) { + ) 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 < DATA_OFFSET - // note: the first check here also captures the case where - // (bytecodeSize == 0) implicitly, but we add the second check of - // (bytecodeSize == 0) as a fall-through that will never execute - // unless `DATA_OFFSET` is set to 0 at some point. - if ((bytecodeSize < DATA_OFFSET) || (bytecodeSize == 0)) { + // handle case where address contains code < _offset + if (bytecodeSize < _offset) { revert("ContractAsStorage: Read Error"); } - // handle case where address contains code >= DATA_OFFSET - // decrement by DATA_OFFSET to account for header info + + // handle case where address contains code >= dataOffset + // decrement by dataOffset to account for header info uint256 size; unchecked { - size = bytecodeSize - DATA_OFFSET; + size = bytecodeSize - _offset; } assembly { @@ -141,26 +158,24 @@ library BytecodeStorage { // store length of data in first 32 bytes mstore(data, size) // copy code to memory, excluding the deployer-address - extcodecopy(_address, add(data, 0x20), DATA_OFFSET, size) + extcodecopy(_address, add(data, 0x20), _offset, size) } } /** * @notice Get address for deployer for given contract bytecode - * @param _address address of deployed contract with bytecode containing concat(invalid opcode, deployer-address, data) + * @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 - ) internal view returns (address) { + ) public view returns (address) { // get the size of the data uint256 bytecodeSize = _bytecodeSizeAt(_address); - // handle case where address contains code < DATA_OFFSET - // note: the first check here also captures the case where - // (bytecodeSize == 0) implicitly, but we add the second check of - // (bytecodeSize == 0) as a fall-through that will never execute - // unless `DATA_OFFSET` is set to 0 at some point. - if ((bytecodeSize < DATA_OFFSET) || (bytecodeSize == 0)) { + // 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"); } @@ -172,12 +187,12 @@ library BytecodeStorage { // 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: - // | deployer-address (padded) | data | + // contracts-as-storage contract looks like:: + // | invalid opcode | version-string (unless v0) | deployer-address (padded) | data | extcodecopy( _address, writerAddress, - ADDRESS_OFFSET, + addressOffset, 0x20 // full 32-bytes, as address is expected to be zero-padded ) return( @@ -187,20 +202,198 @@ library BytecodeStorage { } } + /** + * @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` - */ + * @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/BytecodeV1TextCR_DMock.sol b/contracts/mock/BytecodeV1TextCR_DMock.sol index 409fe90dc..0fa24df3e 100644 --- a/contracts/mock/BytecodeV1TextCR_DMock.sol +++ b/contracts/mock/BytecodeV1TextCR_DMock.sol @@ -22,8 +22,7 @@ import "../libs/0.8.x/BytecodeStorageV1.sol"; * supported by the underlying library. */ contract BytecodeV1TextCR_DMock { - using BytecodeStorage for string; - using BytecodeStorage for address; + using BytecodeStorageWriter for string; // monotonically increasing slot counter and associated slot-storage mapping uint256 public nextTextSlotId = 0; @@ -77,7 +76,10 @@ contract BytecodeV1TextCR_DMock { * the underlying BytecodeStorage lib to throw errors where applicable. */ function readText(uint256 _textSlotId) public view returns (string memory) { - return storedTextBytecodeAddresses[_textSlotId].readFromBytecode(); + return + BytecodeStorageReader.readFromBytecode( + storedTextBytecodeAddresses[_textSlotId] + ); } /** @@ -99,7 +101,7 @@ contract BytecodeV1TextCR_DMock { /** * @notice Allows additional read introspection, to read a chunk of text, - * from chain-state that lives at a given deployed address. + * 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 @@ -108,7 +110,51 @@ contract BytecodeV1TextCR_DMock { function readTextAtAddress( address _bytecodeAddress ) public view returns (string memory) { - return _bytecodeAddress.readFromBytecode(); + 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 + ) + ); } /** @@ -116,14 +162,33 @@ contract BytecodeV1TextCR_DMock { * 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. + * 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 _bytecodeAddress.getWriterAddressForBytecode(); + 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 + ); } /** diff --git a/contracts/mock/GenArt721CoreV3_Engine_IncorrectCoreType.sol b/contracts/mock/GenArt721CoreV3_Engine_IncorrectCoreType.sol index 66c85d242..59185823a 100644 --- a/contracts/mock/GenArt721CoreV3_Engine_IncorrectCoreType.sol +++ b/contracts/mock/GenArt721CoreV3_Engine_IncorrectCoreType.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; @@ -1558,7 +1557,7 @@ contract GenArt721CoreV3_Engine_IncorrectCoreType is if (_index >= project.scriptCount) { return ""; } - return project.scriptBytecodeAddresses[_index].readFromBytecode(); + return _readFromBytecode(project.scriptBytecodeAddresses[_index]); } /** @@ -1954,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 da7b3a1b7..e81923115 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 89a2dc0ce..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, @@ -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/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/BytecodeStorageV0_BytecodeTextCR_DMock.test.ts b/test/libs/BytecodeStorageV0_BytecodeTextCR_DMock.test.ts index 2a170d7bc..b0e6a831f 100644 --- a/test/libs/BytecodeStorageV0_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,8 +29,12 @@ 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 BytecodeV0TextCR_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. @@ -36,7 +43,7 @@ describe("BytecodeStorageV0 + BytecodeV0TextCR_DMock Library Tests", async funct // 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, bytecodeV0TextCR_DMock: Contract, deployer: SignerWithAddress @@ -52,7 +59,7 @@ describe("BytecodeStorageV0 + BytecodeV0TextCR_DMock Library Tests", async funct // Helper that retrieves the address of the most recently deployed contract // containing bytecode for storage. async function getLatestTextDeploymentAddress( - config: T_Config, + config: BytecodeStorageV0TestConfig, bytecodeV0TextCR_DMock: Contract ) { const nextTextSlotId = await bytecodeV0TextCR_DMock.nextTextSlotId(); @@ -64,7 +71,7 @@ describe("BytecodeStorageV0 + BytecodeV0TextCR_DMock Library Tests", async funct } async function _beforeEach() { - let config: T_Config = { + let config: BytecodeStorageV0TestConfig = { accounts: await getAccounts(), }; config = await assignDefaultConstants(config); 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 index e29e5fd8a..5571462fd 100644 --- a/test/libs/BytecodeStorageV1_BytecodeTextCR_DMock.test.ts +++ b/test/libs/BytecodeStorageV1_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 { BytecodeV1TextCR_DMock } from "../../scripts/contracts"; + import { T_Config, getAccounts, - deployAndGet, + deployWithStorageLibraryAndGet, assignDefaultConstants, } from "../util/common"; + import { SQUIGGLE_SCRIPT, SKULPTUUR_SCRIPT_APPROX, @@ -26,8 +29,12 @@ import { MULTI_BYTE_UTF_EIGHT_SCRIPT, } from "../util/example-scripts"; +interface BytecodeStorageV1TestConfig extends T_Config { + bytecodeV1TextCR_DMock?: BytecodeV1TextCR_DMock; +} + /** - * Tests for BytecodeStorage by way of testing the 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. @@ -36,7 +43,7 @@ describe("BytecodeStorageV1 + BytecodeV1TextCR_DMock Library Tests", async funct // 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: BytecodeStorageV1TestConfig, targetText: string, bytecodeV1TextCR_DMock: Contract, deployer: SignerWithAddress @@ -52,7 +59,7 @@ describe("BytecodeStorageV1 + BytecodeV1TextCR_DMock Library Tests", async funct // Helper that retrieves the address of the most recently deployed contract // containing bytecode for storage. async function getLatestTextDeploymentAddress( - config: T_Config, + config: BytecodeStorageV1TestConfig, bytecodeV1TextCR_DMock: Contract ) { const nextTextSlotId = await bytecodeV1TextCR_DMock.nextTextSlotId(); @@ -64,12 +71,12 @@ describe("BytecodeStorageV1 + BytecodeV1TextCR_DMock Library Tests", async funct } async function _beforeEach() { - let config: T_Config = { + let config: BytecodeStorageV1TestConfig = { accounts: await getAccounts(), }; config = await assignDefaultConstants(config); // deploy the library mock - config.bytecodeV1TextCR_DMock = await deployAndGet( + config.bytecodeV1TextCR_DMock = await deployWithStorageLibraryAndGet( config, "BytecodeV1TextCR_DMock", [] // no deployment args @@ -181,11 +188,12 @@ describe("BytecodeStorageV1 + BytecodeV1TextCR_DMock Library Tests", async funct ); // deploy a second instance of the library mock - const additionalBytecodeV1TextCR_DMock = await deployAndGet( - config, - "BytecodeV1TextCR_DMock", - [] // no deployment args - ); + const additionalBytecodeV1TextCR_DMock = + await deployWithStorageLibraryAndGet( + config, + "BytecodeV1TextCR_DMock", + [] // no deployment args + ); const text = await additionalBytecodeV1TextCR_DMock.readTextAtAddress( textBytecodeAddress ); @@ -261,11 +269,12 @@ describe("BytecodeStorageV1 + BytecodeV1TextCR_DMock Library Tests", async funct ); // deploy a second instance of the library mock - const additionalBytecodeV1TextCR_DMock = await deployAndGet( - config, - "BytecodeV1TextCR_DMock", - [] // no deployment args - ); + const additionalBytecodeV1TextCR_DMock = + await deployWithStorageLibraryAndGet( + config, + "BytecodeV1TextCR_DMock", + [] // no deployment args + ); const textAuthorAddress = await additionalBytecodeV1TextCR_DMock.readAuthorForTextAtAddress( textBytecodeAddress 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)