From 52e9ab2eea23d990e135d5508f4abd16eb58956a Mon Sep 17 00:00:00 2001 From: Ben Yoshiwara Date: Thu, 18 Jan 2024 14:19:46 -0800 Subject: [PATCH] Make generator contract upgradable and add write functions for external contracts addresses. Use self deployed gunzip contract instead of ethfs. --- packages/contracts/.openzeppelin/sepolia.json | 78 +++++++ .../contracts/BytecodeStorageV1Writer.sol | 29 +++ .../generator/GenArt721GeneratorV0.sol | 76 ++++++- .../scripty-support/ETHFSFileStorage.sol | 39 ---- .../generator/scripty-support/ethfs/File.sol | 136 ------------ .../scripty-support/ethfs/IFileStore.sol | 205 ------------------ .../scripts/one-off/deploy-dev-generator.ts | 98 ++++++--- .../generator/GenArt721GeneratorV0.test.ts | 145 ++++++++++++- 8 files changed, 375 insertions(+), 431 deletions(-) create mode 100644 packages/contracts/contracts/BytecodeStorageV1Writer.sol delete mode 100644 packages/contracts/contracts/generator/scripty-support/ETHFSFileStorage.sol delete mode 100644 packages/contracts/contracts/generator/scripty-support/ethfs/File.sol delete mode 100644 packages/contracts/contracts/generator/scripty-support/ethfs/IFileStore.sol diff --git a/packages/contracts/.openzeppelin/sepolia.json b/packages/contracts/.openzeppelin/sepolia.json index 5234a206b..ed2d82e0d 100644 --- a/packages/contracts/.openzeppelin/sepolia.json +++ b/packages/contracts/.openzeppelin/sepolia.json @@ -14,6 +14,11 @@ "address": "0xEFA7Ef074A6E90a99fba8bAd4dCf337ef298387f", "txHash": "0xee1ad6b97bf1819837293f000948d2802b325faa261f9d7d8dd277f999150658", "kind": "transparent" + }, + { + "address": "0xdC862938cA0a2D8dcabe5733C23e54ac7aAFFF27", + "txHash": "0x51a4238afeca59f44439f0303bb99bbc728d07993ad7bcfefb688585e4243770", + "kind": "transparent" } ], "impls": { @@ -92,6 +97,79 @@ } } } + }, + "1b6033ccc3bed545b5ceb14d868b4e11f3b6ec5a05a6b7628923f382bec23202": { + "address": "0x34a5F30D34EF69102DcAFeCb062605F3E3f57D93", + "txHash": "0x7bb2f060c373ed2af9f7733870de8fbf781e87bb97556e7277c0ce5d0369e18a", + "layout": { + "solcVersion": "0.8.19", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin-4.8/contracts-upgradeable/proxy/utils/Initializable.sol:62", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin-4.8/contracts-upgradeable/proxy/utils/Initializable.sol:67" + }, + { + "label": "dependencyRegistry", + "offset": 2, + "slot": "0", + "type": "t_contract(DependencyRegistryV0)4223", + "contract": "GenArt721GeneratorV0", + "src": "contracts/generator/GenArt721GeneratorV0.sol:31" + }, + { + "label": "scriptyBuilder", + "offset": 0, + "slot": "1", + "type": "t_contract(IScriptyBuilderV2)6462", + "contract": "GenArt721GeneratorV0", + "src": "contracts/generator/GenArt721GeneratorV0.sol:32" + }, + { + "label": "gunzipScriptAddress", + "offset": 0, + "slot": "2", + "type": "t_address", + "contract": "GenArt721GeneratorV0", + "src": "contracts/generator/GenArt721GeneratorV0.sol:33" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(DependencyRegistryV0)4223": { + "label": "contract DependencyRegistryV0", + "numberOfBytes": "20" + }, + "t_contract(IScriptyBuilderV2)6462": { + "label": "contract IScriptyBuilderV2", + "numberOfBytes": "20" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": {} + } } } } diff --git a/packages/contracts/contracts/BytecodeStorageV1Writer.sol b/packages/contracts/contracts/BytecodeStorageV1Writer.sol new file mode 100644 index 000000000..4a8411928 --- /dev/null +++ b/packages/contracts/contracts/BytecodeStorageV1Writer.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.19; + +// Created By: Art Blocks Inc. +import "./libs/v0.8.x/BytecodeStorageV1.sol"; + +/** + * @title Art Blocks Storage Writer + * @author Art Blocks Inc. + * @notice A simple contract that with a single function that uses + * the BytecodeStorageV1 library to write a string to bytecode storage. + */ +contract BytecodeStorageV1Writer { + using BytecodeStorageWriter for string; + + event StorageContractCreated(address indexed storageContract); + + /** + * @notice Write a string to bytecode storage. + * @param _string The string to write to bytecode storage. + * @dev This function emits an event with the address of the newly created + * storage contract. + */ + function writeStringToBytecodeStorage(string memory _string) public { + address storageContract = _string.writeToBytecode(); + + emit StorageContractCreated(storageContract); + } +} diff --git a/packages/contracts/contracts/generator/GenArt721GeneratorV0.sol b/packages/contracts/contracts/generator/GenArt721GeneratorV0.sol index 7717d6dd7..8b03e8063 100644 --- a/packages/contracts/contracts/generator/GenArt721GeneratorV0.sol +++ b/packages/contracts/contracts/generator/GenArt721GeneratorV0.sol @@ -12,6 +12,7 @@ import {ABHelpers} from "../libs/v0.8.x/ABHelpers.sol"; import {AddressChunks} from "./AddressChunks.sol"; import "@openzeppelin-4.7/contracts/utils/Strings.sol"; +import "@openzeppelin-4.8/contracts-upgradeable/proxy/utils/Initializable.sol"; import {IScriptyBuilderV2, HTMLRequest, HTMLTagType, HTMLTag} from "scripty.sol/contracts/scripty/interfaces/IScriptyBuilderV2.sol"; @@ -22,13 +23,14 @@ import {IScriptyBuilderV2, HTMLRequest, HTMLTagType, HTMLTag} from "scripty.sol/ * by combining the dependency script, project script, token data. It utilizes * the ScriptyBuilder contract to generate the HTML. */ -contract GenArt721GeneratorV0 { +contract GenArt721GeneratorV0 is Initializable { using Bytes32Strings for bytes32; using Bytes32Strings for string; + using BytecodeStorageWriter for string; DependencyRegistryV0 public dependencyRegistry; IScriptyBuilderV2 public scriptyBuilder; - address public ethFS; + address public gunzipScriptAddress; function _onlySupportedCoreContract( address coreContractAddress @@ -39,24 +41,74 @@ contract GenArt721GeneratorV0 { ); } + function _onlyDependencyRegistryAdminACL(bytes4 selector) internal { + require( + dependencyRegistry.adminACLAllowed( + msg.sender, + address(this), + selector + ), + "Only DependencyRegistry AdminACL" + ); + } + /** - * @notice Constructor for the GenArt721GeneratorV0 contract. + * @notice Initializer for the GenArt721GeneratorV0 contract. * @param _dependencyRegistry The address of the DependencyRegistry * contract to be used for retrieving dependency scripts. * @param _scriptyBuilder The address of the ScriptyBuilderV2 contract * to be used for generating the HTML for tokens. - * @param _ethFS The address of the EthFSFileStorage contract to retrieve - * the gunzipScripts-0.0.1.js script from used to gunzip the dependency - * scripts. + * @param _gunzipScriptAddress The address of the gunzip script bytecode + * storage contract used to gunzip the dependency scripts in the browser. */ - constructor( + function initialize( address _dependencyRegistry, address _scriptyBuilder, - address _ethFS - ) { + address _gunzipScriptAddress + ) public initializer { dependencyRegistry = DependencyRegistryV0(_dependencyRegistry); scriptyBuilder = IScriptyBuilderV2(_scriptyBuilder); - ethFS = _ethFS; + gunzipScriptAddress = _gunzipScriptAddress; + } + + /** + * @notice Set the DependencyRegistry contract address. + * @param _dependencyRegistry The address of the DependencyRegistry + * contract to be used for retrieving dependency scripts. + * @dev This function is gated to only the DependencyRegistry AdminACL. + * If an address is passed that does not implement the adminACLAllowed + * function, we will lose access to the write functions on this contract. + */ + function updateDependencyRegistry(address _dependencyRegistry) external { + _onlyDependencyRegistryAdminACL(this.updateDependencyRegistry.selector); + + dependencyRegistry = DependencyRegistryV0(_dependencyRegistry); + } + + /** + * @notice Set the ScriptyBuilder contract address. + * @param _scriptyBuilder The address of the ScriptyBuilderV2 contract + * to be used for generating the HTML for tokens. + * @dev This function is gated to only the DependencyRegistry AdminACL. + */ + function updateScriptyBuilder(address _scriptyBuilder) external { + _onlyDependencyRegistryAdminACL(this.updateScriptyBuilder.selector); + + scriptyBuilder = IScriptyBuilderV2(_scriptyBuilder); + } + + /** + * @notice Set the gunzip script address. + * @param _gunzipScriptAddress The address of the gunzip script bytecode + * storage contract used to gunzip the dependency scripts in the browser. + * @dev This function is gated to only the DependencyRegistry AdminACL. + */ + function updateGunzipScriptAddress(address _gunzipScriptAddress) external { + _onlyDependencyRegistryAdminACL( + this.updateGunzipScriptAddress.selector + ); + + gunzipScriptAddress = _gunzipScriptAddress; } /** @@ -325,7 +377,9 @@ contract GenArt721GeneratorV0 { // so we need to include this so we can gunzip them in the browser. bodyTags[1].name = "gunzipScripts-0.0.1.js"; bodyTags[1].tagType = HTMLTagType.scriptBase64DataURI; // - bodyTags[1].contractAddress = ethFS; + bodyTags[1].tagContent = bytes( + BytecodeStorageReader.readFromBytecode(gunzipScriptAddress) + ); bytes memory projectScript = getProjectScriptBytes( coreContractAddress, diff --git a/packages/contracts/contracts/generator/scripty-support/ETHFSFileStorage.sol b/packages/contracts/contracts/generator/scripty-support/ETHFSFileStorage.sol deleted file mode 100644 index 63ddb2651..000000000 --- a/packages/contracts/contracts/generator/scripty-support/ETHFSFileStorage.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -/////////////////////////////////////////////////////////// -// ░██████╗░█████╗░██████╗░██╗██████╗░████████╗██╗░░░██╗ // -// ██╔════╝██╔══██╗██╔══██╗██║██╔══██╗╚══██╔══╝╚██╗░██╔╝ // -// ╚█████╗░██║░░╚═╝██████╔╝██║██████╔╝░░░██║░░░░╚████╔╝░ // -// ░╚═══██╗██║░░██╗██╔══██╗██║██╔═══╝░░░░██║░░░░░╚██╔╝░░ // -// ██████╔╝╚█████╔╝██║░░██║██║██║░░░░░░░░██║░░░░░░██║░░░ // -// ╚═════╝░░╚════╝░╚═╝░░╚═╝╚═╝╚═╝░░░░░░░░╚═╝░░░░░░╚═╝░░░ // -/////////////////////////////////////////////////////////// - -import {IFileStore} from "./ethfs/IFileStore.sol"; -import {IContractScript} from "scripty.sol/contracts/scripty/interfaces/IContractScript.sol"; - -contract ETHFSFileStorage is IContractScript { - IFileStore public immutable fileStore; - - constructor(address _fileStoreAddress) { - fileStore = IFileStore(_fileStoreAddress); - } - - // ============================================================= - // GETTERS - // ============================================================= - - /** - * @notice Get the full script from ethfs's FileStore contract - * @param name - Name given to the script. Eg: threejs.min.js_r148 - * @param - Arbitrary data. Not used by this contract. - * @return script - Full script from merged chunks - */ - function getScript( - string calldata name, - bytes memory /*data*/ - ) external view returns (bytes memory script) { - return bytes(fileStore.getFile(name).read()); - } -} diff --git a/packages/contracts/contracts/generator/scripty-support/ethfs/File.sol b/packages/contracts/contracts/generator/scripty-support/ethfs/File.sol deleted file mode 100644 index a164bf514..000000000 --- a/packages/contracts/contracts/generator/scripty-support/ethfs/File.sol +++ /dev/null @@ -1,136 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.22; - -/** - * @title EthFS File - * @notice A representation of an onchain file, composed of slices of contract bytecode and utilities to construct the file contents from those slices. - * @dev For best gas efficiency, it's recommended using `File.read()` as close to the output returned by the contract call as possible. Lots of gas is consumed every time a large data blob is passed between functions. - */ - -/** - * @dev Represents a reference to a slice of bytecode in a contract - */ -struct BytecodeSlice { - address pointer; - uint32 start; - uint32 end; -} - -/** - * @dev Represents a file composed of one or more bytecode slices - */ -struct File { - // Total length of file contents (sum of all slice sizes). Useful when you want to use DynamicBuffer to build the file contents from the slices. - uint256 size; - BytecodeSlice[] slices; -} -// extend File struct with read functions -using {read} for File global; -using {readUnchecked} for File global; - -/** - * @dev Error thrown when a slice is out of the bounds of the contract's bytecode - */ -error SliceOutOfBounds( - address pointer, - uint32 codeSize, - uint32 sliceStart, - uint32 sliceEnd -); - -/** - * @notice Reads the contents of a file by concatenating its slices - * @param file The file to read - * @return contents The concatenated contents of the file - */ -function read(File memory file) view returns (string memory contents) { - BytecodeSlice[] memory slices = file.slices; - bytes4 sliceOutOfBoundsSelector = SliceOutOfBounds.selector; - - assembly { - let len := mload(slices) - let size := 0x20 - contents := mload(0x40) - let slice - let pointer - let start - let end - let codeSize - - for { - let i := 0 - } lt(i, len) { - i := add(i, 1) - } { - slice := mload(add(slices, add(0x20, mul(i, 0x20)))) - pointer := mload(slice) - start := mload(add(slice, 0x20)) - end := mload(add(slice, 0x40)) - - codeSize := extcodesize(pointer) - if gt(end, codeSize) { - mstore(0x00, sliceOutOfBoundsSelector) - mstore(0x04, pointer) - mstore(0x24, codeSize) - mstore(0x44, start) - mstore(0x64, end) - revert(0x00, 0x84) - } - - extcodecopy(pointer, add(contents, size), start, sub(end, start)) - size := add(size, sub(end, start)) - } - - // update contents size - mstore(contents, sub(size, 0x20)) - // store contents - mstore(0x40, add(contents, and(add(size, 0x1f), not(0x1f)))) - } -} - -/** - * @notice Reads the contents of a file without reverting on unreadable/invalid slices. Skips any slices that are out of bounds or invalid. Useful if you are composing contract bytecode where a contract can still selfdestruct (which would result in an invalid slice) and want to avoid reverts but still output potentially "corrupted" file contents (due to missing data). - * @param file The file to read - * @return contents The concatenated contents of the file, skipping invalid slices - */ -function readUnchecked(File memory file) view returns (string memory contents) { - BytecodeSlice[] memory slices = file.slices; - - assembly { - let len := mload(slices) - let size := 0x20 - contents := mload(0x40) - let slice - let pointer - let start - let end - let codeSize - - for { - let i := 0 - } lt(i, len) { - i := add(i, 1) - } { - slice := mload(add(slices, add(0x20, mul(i, 0x20)))) - pointer := mload(slice) - start := mload(add(slice, 0x20)) - end := mload(add(slice, 0x40)) - - codeSize := extcodesize(pointer) - if lt(end, codeSize) { - extcodecopy( - pointer, - add(contents, size), - start, - sub(end, start) - ) - size := add(size, sub(end, start)) - } - } - - // update contents size - mstore(contents, sub(size, 0x20)) - // store contents - mstore(0x40, add(contents, and(add(size, 0x1f), not(0x1f)))) - } -} diff --git a/packages/contracts/contracts/generator/scripty-support/ethfs/IFileStore.sol b/packages/contracts/contracts/generator/scripty-support/ethfs/IFileStore.sol deleted file mode 100644 index 79967f590..000000000 --- a/packages/contracts/contracts/generator/scripty-support/ethfs/IFileStore.sol +++ /dev/null @@ -1,205 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.22; - -import {File, BytecodeSlice} from "./File.sol"; - -/// @title EthFS FileStore interface -/// @notice Specifies a content-addressable onchain file store -interface IFileStore { - event Deployed(); - - /** - * @dev Emitted when a new file is created - * @param indexedFilename The indexed filename for easier finding by filename in logs - * @param pointer The pointer address of the file - * @param filename The name of the file - * @param size The total size of the file - * @param metadata Additional metadata of the file, only emitted for use in offchain indexers - */ - event FileCreated( - string indexed indexedFilename, - address indexed pointer, - string filename, - uint256 size, - bytes metadata - ); - - /** - * @dev Error thrown when a requested file is not found - * @param filename The name of the file requested - */ - error FileNotFound(string filename); - - /** - * @dev Error thrown when a filename already exists - * @param filename The name of the file attempted to be created - */ - error FilenameExists(string filename); - - /** - * @dev Error thrown when attempting to create an empty file - */ - error FileEmpty(); - - /** - * @dev Error thrown when a provided slice for a file is empty - * @param pointer The contract address where the bytecode lives - * @param start The byte offset to start the slice (inclusive) - * @param end The byte offset to end the slice (exclusive) - */ - error SliceEmpty(address pointer, uint32 start, uint32 end); - - /** - * @dev Error thrown when the provided pointer's bytecode does not have the expected STOP opcode prefix from SSTORE2 - * @param pointer The SSTORE2 pointer address - */ - error InvalidPointer(address pointer); - - /** - * @notice Returns the address of the CREATE2 deterministic deployer used by this FileStore - * @return The address of the CREATE2 deterministic deployer - */ - function deployer() external view returns (address); - - /** - * @notice Retrieves the pointer address of a file by its filename - * @param filename The name of the file - * @return pointer The pointer address of the file - */ - function files( - string memory filename - ) external view returns (address pointer); - - /** - * @notice Checks if a file exists for a given filename - * @param filename The name of the file to check - * @return True if the file exists, false otherwise - */ - function fileExists(string memory filename) external view returns (bool); - - /** - * @notice Retrieves the pointer address for a given filename - * @param filename The name of the file - * @return pointer The pointer address of the file - */ - function getPointer( - string memory filename - ) external view returns (address pointer); - - /** - * @notice Retrieves a file by its filename - * @param filename The name of the file - * @return file The file associated with the filename - */ - function getFile( - string memory filename - ) external view returns (File memory file); - - /** - * @notice Creates a new file with the provided file contents - * @dev This is a convenience method to simplify small file uploads. It's recommended to use `createFileFromPointers` or `createFileFromSlices` for larger files. This particular method splits `contents` into 24575-byte chunks before storing them via SSTORE2. - * @param filename The name of the new file - * @param contents The contents of the file - * @return pointer The pointer address of the new file - * @return file The newly created file - */ - function createFile( - string memory filename, - string memory contents - ) external returns (address pointer, File memory file); - - /** - * @notice Creates a new file with the provided file contents and file metadata - * @dev This is a convenience method to simplify small file uploads. It's recommended to use `createFileFromPointers` or `createFileFromSlices` for larger files. This particular method splits `contents` into 24575-byte chunks before storing them via SSTORE2. - * @param filename The name of the new file - * @param contents The contents of the file - * @param metadata Additional file metadata, usually a JSON-encoded string, for offchain indexers - * @return pointer The pointer address of the new file - * @return file The newly created file - */ - function createFile( - string memory filename, - string memory contents, - bytes memory metadata - ) external returns (address pointer, File memory file); - - /** - * @notice Creates a new file where its content is composed of the provided string chunks - * @dev This is a convenience method to simplify small and nuanced file uploads. It's recommended to use `createFileFromPointers` or `createFileFromSlices` for larger files. This particular will store each chunk separately via SSTORE2. For best gas efficiency, each chunk should be as large as possible (up to the contract size limit) and at least 32 bytes. - * @param filename The name of the new file - * @param chunks The string chunks composing the file - * @return pointer The pointer address of the new file - * @return file The newly created file - */ - function createFileFromChunks( - string memory filename, - string[] memory chunks - ) external returns (address pointer, File memory file); - - /** - * @notice Creates a new file with the provided string chunks and file metadata - * @dev This is a convenience method to simplify small and nuanced file uploads. It's recommended to use `createFileFromPointers` or `createFileFromSlices` for larger files. This particular will store each chunk separately via SSTORE2. For best gas efficiency, each chunk should be as large as possible (up to the contract size limit) and at least 32 bytes. - * @param filename The name of the new file - * @param chunks The string chunks composing the file - * @param metadata Additional file metadata, usually a JSON-encoded string, for offchain indexers - * @return pointer The pointer address of the new file - * @return file The newly created file - */ - function createFileFromChunks( - string memory filename, - string[] memory chunks, - bytes memory metadata - ) external returns (address pointer, File memory file); - - /** - * @notice Creates a new file where its content is composed of the provided SSTORE2 pointers - * @param filename The name of the new file - * @param pointers The SSTORE2 pointers composing the file - * @return pointer The pointer address of the new file - * @return file The newly created file - */ - function createFileFromPointers( - string memory filename, - address[] memory pointers - ) external returns (address pointer, File memory file); - - /** - * @notice Creates a new file with the provided SSTORE2 pointers and file metadata - * @param filename The name of the new file - * @param pointers The SSTORE2 pointers composing the file - * @param metadata Additional file metadata, usually a JSON-encoded string, for offchain indexers - * @return pointer The pointer address of the new file - * @return file The newly created file - */ - function createFileFromPointers( - string memory filename, - address[] memory pointers, - bytes memory metadata - ) external returns (address pointer, File memory file); - - /** - * @notice Creates a new file where its content is composed of the provided bytecode slices - * @param filename The name of the new file - * @param slices The bytecode slices composing the file - * @return pointer The pointer address of the new file - * @return file The newly created file - */ - function createFileFromSlices( - string memory filename, - BytecodeSlice[] memory slices - ) external returns (address pointer, File memory file); - - /** - * @notice Creates a new file with the provided bytecode slices and file metadata - * @param filename The name of the new file - * @param slices The bytecode slices composing the file - * @param metadata Additional file metadata, usually a JSON-encoded string, for offchain indexers - * @return pointer The pointer address of the new file - * @return file The newly created file - */ - function createFileFromSlices( - string memory filename, - BytecodeSlice[] memory slices, - bytes memory metadata - ) external returns (address pointer, File memory file); -} diff --git a/packages/contracts/scripts/one-off/deploy-dev-generator.ts b/packages/contracts/scripts/one-off/deploy-dev-generator.ts index 8f68c9783..b1c850773 100644 --- a/packages/contracts/scripts/one-off/deploy-dev-generator.ts +++ b/packages/contracts/scripts/one-off/deploy-dev-generator.ts @@ -1,15 +1,20 @@ // SPDX-License-Identifier: LGPL-3.0-only // Created By: Art Blocks Inc. -import hre, { ethers } from "hardhat"; -import { ETHFSFileStorage__factory, GenArt721GeneratorV0 } from "../contracts"; -import { GenArt721GeneratorV0__factory } from "../contracts/factories/GenArt721GeneratorV0__factory"; +import hre, { ethers, upgrades } from "hardhat"; +import { + BytecodeStorageV1Writer__factory, + GenArt721GeneratorV0, +} from "../contracts"; +import { GenArt721GeneratorV0__factory } from "../contracts/factories/generator/GenArt721GeneratorV0__factory"; import { getNetworkName } from "../util/utils"; import { BYTECODE_STORAGE_READER_LIBRARY_ADDRESSES } from "../util/constants"; +import { StorageContractCreatedEvent } from "../contracts/BytecodeStorageV1Writer"; const dependencyRegistryAddress = "0x5Fcc415BCFb164C5F826B5305274749BeB684e9b"; -const ethFSAddress = "0xFe1411d6864592549AdE050215482e4385dFa0FB"; const scriptyBuilderV2Address = "0xb205DFfE32259E2F1c3C0cba855250134147C083"; +const GUNZIP_SCRIPT_BASE64 = + "InVzZSBzdHJpY3QiOygoKT0+e3ZhciB2PVVpbnQ4QXJyYXksQT1VaW50MTZBcnJheSxfPVVpbnQzMkFycmF5LHJyPW5ldyB2KFswLDAsMCwwLDAsMCwwLDAsMSwxLDEsMSwyLDIsMiwyLDMsMywzLDMsNCw0LDQsNCw1LDUsNSw1LDAsMCwwLDBdKSxucj1uZXcgdihbMCwwLDAsMCwxLDEsMiwyLDMsMyw0LDQsNSw1LDYsNiw3LDcsOCw4LDksOSwxMCwxMCwxMSwxMSwxMiwxMiwxMywxMywwLDBdKSxscj1uZXcgdihbMTYsMTcsMTgsMCw4LDcsOSw2LDEwLDUsMTEsNCwxMiwzLDEzLDIsMTQsMSwxNV0pLHRyPWZ1bmN0aW9uKHIsbil7Zm9yKHZhciB0PW5ldyBBKDMxKSxlPTA7ZTwzMTsrK2UpdFtlXT1uKz0xPDxyW2UtMV07Zm9yKHZhciBhPW5ldyBfKHRbMzBdKSxlPTE7ZTwzMDsrK2UpZm9yKHZhciB1PXRbZV07dTx0W2UrMV07Kyt1KWFbdV09dS10W2VdPDw1fGU7cmV0dXJuW3QsYV19LGVyPXRyKHJyLDIpLGlyPWVyWzBdLGNyPWVyWzFdO2lyWzI4XT0yNTgsY3JbMjU4XT0yODt2YXIgYXI9dHIobnIsMCkscHI9YXJbMF0sVXI9YXJbMV0scT1uZXcgQSgzMjc2OCk7Zm9yKG89MDtvPDMyNzY4OysrbyltPShvJjQzNjkwKT4+PjF8KG8mMjE4NDUpPDwxLG09KG0mNTI0MjgpPj4+MnwobSYxMzEwNyk8PDIsbT0obSY2MTY4MCk+Pj40fChtJjM4NTUpPDw0LHFbb109KChtJjY1MjgwKT4+Pjh8KG0mMjU1KTw8OCk+Pj4xO3ZhciBtLG8sRD1mdW5jdGlvbihyLG4sdCl7Zm9yKHZhciBlPXIubGVuZ3RoLGE9MCx1PW5ldyBBKG4pO2E8ZTsrK2EpclthXSYmKyt1W3JbYV0tMV07dmFyIGc9bmV3IEEobik7Zm9yKGE9MDthPG47KythKWdbYV09Z1thLTFdK3VbYS0xXTw8MTt2YXIgcztpZih0KXtzPW5ldyBBKDE8PG4pO3ZhciBpPTE1LW47Zm9yKGE9MDthPGU7KythKWlmKHJbYV0pZm9yKHZhciBmPWE8PDR8clthXSxoPW4tclthXSxsPWdbclthXS0xXSsrPDxoLHc9bHwoMTw8aCktMTtsPD13OysrbClzW3FbbF0+Pj5pXT1mfWVsc2UgZm9yKHM9bmV3IEEoZSksYT0wO2E8ZTsrK2EpclthXSYmKHNbYV09cVtnW3JbYV0tMV0rK10+Pj4xNS1yW2FdKTtyZXR1cm4gc30sRT1uZXcgdigyODgpO2ZvcihvPTA7bzwxNDQ7KytvKUVbb109ODt2YXIgbztmb3Iobz0xNDQ7bzwyNTY7KytvKUVbb109OTt2YXIgbztmb3Iobz0yNTY7bzwyODA7KytvKUVbb109Nzt2YXIgbztmb3Iobz0yODA7bzwyODg7KytvKUVbb109ODt2YXIgbyxvcj1uZXcgdigzMik7Zm9yKG89MDtvPDMyOysrbylvcltvXT01O3ZhciBvO3ZhciBncj1EKEUsOSwxKTt2YXIgeXI9RChvciw1LDEpLFI9ZnVuY3Rpb24ocil7Zm9yKHZhciBuPXJbMF0sdD0xO3Q8ci5sZW5ndGg7Kyt0KXJbdF0+biYmKG49clt0XSk7cmV0dXJuIG59LHA9ZnVuY3Rpb24ocixuLHQpe3ZhciBlPW4vOHwwO3JldHVybihyW2VdfHJbZSsxXTw8OCk+PihuJjcpJnR9LCQ9ZnVuY3Rpb24ocixuKXt2YXIgdD1uLzh8MDtyZXR1cm4oclt0XXxyW3QrMV08PDh8clt0KzJdPDwxNik+PihuJjcpfSx3cj1mdW5jdGlvbihyKXtyZXR1cm4ocis3KS84fDB9LG1yPWZ1bmN0aW9uKHIsbix0KXsobj09bnVsbHx8bjwwKSYmKG49MCksKHQ9PW51bGx8fHQ+ci5sZW5ndGgpJiYodD1yLmxlbmd0aCk7dmFyIGU9bmV3KHIuQllURVNfUEVSX0VMRU1FTlQ9PTI/QTpyLkJZVEVTX1BFUl9FTEVNRU5UPT00P186dikodC1uKTtyZXR1cm4gZS5zZXQoci5zdWJhcnJheShuLHQpKSxlfTt2YXIgeHI9WyJ1bmV4cGVjdGVkIEVPRiIsImludmFsaWQgYmxvY2sgdHlwZSIsImludmFsaWQgbGVuZ3RoL2xpdGVyYWwiLCJpbnZhbGlkIGRpc3RhbmNlIiwic3RyZWFtIGZpbmlzaGVkIiwibm8gc3RyZWFtIGhhbmRsZXIiLCwibm8gY2FsbGJhY2siLCJpbnZhbGlkIFVURi04IGRhdGEiLCJleHRyYSBmaWVsZCB0b28gbG9uZyIsImRhdGUgbm90IGluIHJhbmdlIDE5ODAtMjA5OSIsImZpbGVuYW1lIHRvbyBsb25nIiwic3RyZWFtIGZpbmlzaGluZyIsImludmFsaWQgemlwIGRhdGEiXSx4PWZ1bmN0aW9uKHIsbix0KXt2YXIgZT1uZXcgRXJyb3Iobnx8eHJbcl0pO2lmKGUuY29kZT1yLEVycm9yLmNhcHR1cmVTdGFja1RyYWNlJiZFcnJvci5jYXB0dXJlU3RhY2tUcmFjZShlLHgpLCF0KXRocm93IGU7cmV0dXJuIGV9LHpyPWZ1bmN0aW9uKHIsbix0KXt2YXIgZT1yLmxlbmd0aDtpZighZXx8dCYmdC5mJiYhdC5sKXJldHVybiBufHxuZXcgdigwKTt2YXIgYT0hbnx8dCx1PSF0fHx0Lmk7dHx8KHQ9e30pLG58fChuPW5ldyB2KGUqMykpO3ZhciBnPWZ1bmN0aW9uKFYpe3ZhciBYPW4ubGVuZ3RoO2lmKFY+WCl7dmFyIGI9bmV3IHYoTWF0aC5tYXgoWCoyLFYpKTtiLnNldChuKSxuPWJ9fSxzPXQuZnx8MCxpPXQucHx8MCxmPXQuYnx8MCxoPXQubCxsPXQuZCx3PXQubSxUPXQubixJPWUqODtkb3tpZighaCl7cz1wKHIsaSwxKTt2YXIgQj1wKHIsaSsxLDMpO2lmKGkrPTMsQilpZihCPT0xKWg9Z3IsbD15cix3PTksVD01O2Vsc2UgaWYoQj09Mil7dmFyIEc9cChyLGksMzEpKzI1NyxZPXAocixpKzEwLDE1KSs0LFc9RytwKHIsaSs1LDMxKSsxO2krPTE0O2Zvcih2YXIgQz1uZXcgdihXKSxPPW5ldyB2KDE5KSxjPTA7YzxZOysrYylPW2xyW2NdXT1wKHIsaStjKjMsNyk7aSs9WSozO2Zvcih2YXIgaj1SKE8pLHNyPSgxPDxqKS0xLHVyPUQoTyxqLDEpLGM9MDtjPFc7KXt2YXIgZD11cltwKHIsaSxzcildO2krPWQmMTU7dmFyIHk9ZD4+PjQ7aWYoeTwxNilDW2MrK109eTtlbHNle3ZhciBTPTAsRj0wO2Zvcih5PT0xNj8oRj0zK3AocixpLDMpLGkrPTIsUz1DW2MtMV0pOnk9PTE3PyhGPTMrcChyLGksNyksaSs9Myk6eT09MTgmJihGPTExK3AocixpLDEyNyksaSs9Nyk7Ri0tOylDW2MrK109U319dmFyIEo9Qy5zdWJhcnJheSgwLEcpLHo9Qy5zdWJhcnJheShHKTt3PVIoSiksVD1SKHopLGg9RChKLHcsMSksbD1EKHosVCwxKX1lbHNlIHgoMSk7ZWxzZXt2YXIgeT13cihpKSs0LFo9clt5LTRdfHJbeS0zXTw8OCxrPXkrWjtpZihrPmUpe3UmJngoMCk7YnJlYWt9YSYmZyhmK1opLG4uc2V0KHIuc3ViYXJyYXkoeSxrKSxmKSx0LmI9Zis9Wix0LnA9aT1rKjgsdC5mPXM7Y29udGludWV9aWYoaT5JKXt1JiZ4KDApO2JyZWFrfX1hJiZnKGYrMTMxMDcyKTtmb3IodmFyIHZyPSgxPDx3KS0xLGhyPSgxPDxUKS0xLEw9aTs7TD1pKXt2YXIgUz1oWyQocixpKSZ2cl0sTT1TPj4+NDtpZihpKz1TJjE1LGk+SSl7dSYmeCgwKTticmVha31pZihTfHx4KDIpLE08MjU2KW5bZisrXT1NO2Vsc2UgaWYoTT09MjU2KXtMPWksaD1udWxsO2JyZWFrfWVsc2V7dmFyIEs9TS0yNTQ7aWYoTT4yNjQpe3ZhciBjPU0tMjU3LFU9cnJbY107Sz1wKHIsaSwoMTw8VSktMSkraXJbY10saSs9VX12YXIgUD1sWyQocixpKSZocl0sTj1QPj4+NDtQfHx4KDMpLGkrPVAmMTU7dmFyIHo9cHJbTl07aWYoTj4zKXt2YXIgVT1ucltOXTt6Kz0kKHIsaSkmKDE8PFUpLTEsaSs9VX1pZihpPkkpe3UmJngoMCk7YnJlYWt9YSYmZyhmKzEzMTA3Mik7Zm9yKHZhciBRPWYrSztmPFE7Zis9NCluW2ZdPW5bZi16XSxuW2YrMV09bltmKzEtel0sbltmKzJdPW5bZisyLXpdLG5bZiszXT1uW2YrMy16XTtmPVF9fXQubD1oLHQucD1MLHQuYj1mLHQuZj1zLGgmJihzPTEsdC5tPXcsdC5kPWwsdC5uPVQpfXdoaWxlKCFzKTtyZXR1cm4gZj09bi5sZW5ndGg/bjptcihuLDAsZil9O3ZhciBBcj1uZXcgdigwKTt2YXIgU3I9ZnVuY3Rpb24ocil7KHJbMF0hPTMxfHxyWzFdIT0xMzl8fHJbMl0hPTgpJiZ4KDYsImludmFsaWQgZ3ppcCBkYXRhIik7dmFyIG49clszXSx0PTEwO24mNCYmKHQrPXJbMTBdfChyWzExXTw8OCkrMik7Zm9yKHZhciBlPShuPj4zJjEpKyhuPj40JjEpO2U+MDtlLT0hclt0KytdKTtyZXR1cm4gdCsobiYyKX0sTXI9ZnVuY3Rpb24ocil7dmFyIG49ci5sZW5ndGg7cmV0dXJuKHJbbi00XXxyW24tM108PDh8cltuLTJdPDwxNnxyW24tMV08PDI0KT4+PjB9O2Z1bmN0aW9uIEgocixuKXtyZXR1cm4genIoci5zdWJhcnJheShTcihyKSwtOCksbnx8bmV3IHYoTXIocikpKX12YXIgVHI9dHlwZW9mIFRleHREZWNvZGVyPCJ1IiYmbmV3IFRleHREZWNvZGVyLENyPTA7dHJ5e1RyLmRlY29kZShBcix7c3RyZWFtOiEwfSksQ3I9MX1jYXRjaHt9dmFyIGZyPSgpPT57dmFyIG47bGV0IHI9ZG9jdW1lbnQucXVlcnlTZWxlY3RvckFsbCgnc2NyaXB0W3R5cGU9InRleHQvamF2YXNjcmlwdCtnemlwIl1bc3JjXScpO2ZvcihsZXQgdCBvZiByKXRyeXtsZXQgZT10LnNyYy5tYXRjaCgvXmRhdGE6KC4qPykoPzo7KGJhc2U2NCkpPywoLiopJC8pO2lmKCFlKWNvbnRpbnVlO2xldFthLHUsZyxzXT1lLGk9VWludDhBcnJheS5mcm9tKGc/YXRvYihzKTpkZWNvZGVVUklDb21wb25lbnQocyksdz0+dy5jaGFyQ29kZUF0KDApKSxoPW5ldyBUZXh0RGVjb2RlcigpLmRlY29kZShIKGkpKSxsPWRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoInNjcmlwdCIpO2wudGV4dENvbnRlbnQ9aCwobj10LnBhcmVudE5vZGUpPT1udWxsfHxuLnJlcGxhY2VDaGlsZChsLHQpfWNhdGNoKGUpe2NvbnNvbGUuZXJyb3IoIkNvdWxkIG5vdCBndW56aXAgc2NyaXB0Iix0LGUpfX07ZnIoKTt3aW5kb3cuZ3VuemlwU3luYz1IO3dpbmRvdy5ndW56aXBTY3JpcHRzPWZyO30pKCk7"; async function main() { const [deployer] = await ethers.getSigners(); @@ -20,33 +25,68 @@ async function main() { ////////////////////////////////////////////////////////////////////////////// // DEPLOYMENT BEGINS HERE ////////////////////////////////////////////////////////////////////////////// - // Deploy scripty builder compatible ethFS wrapper. This is only really necessary - // because scripty/ethFS wasn't already deployed on sepolia. - const ethFSFileStorageFactory = new ETHFSFileStorage__factory(deployer); - const ethFSFileStorage = await ethFSFileStorageFactory.deploy(ethFSAddress); - await ethFSFileStorage.deployed(); - console.log(`ETHFSFileStorage deployed at ${ethFSFileStorage.address}`); + // Deploy BytecodeStorageV1Writer contract + const bytecodeStorageV1WriterFactory = new BytecodeStorageV1Writer__factory( + deployer + ); + const bytecodeStorageV1Writer = await bytecodeStorageV1WriterFactory.deploy(); + console.log( + `BytecodeStorageV1Writer deployed at ${bytecodeStorageV1Writer.address}` + ); - // // Deploy generator contract + // Use BytecodeStorageV1Writer to upload gunzip script + const gunzipUploadTransaction = await bytecodeStorageV1Writer + .connect(deployer) + .writeStringToBytecodeStorage(GUNZIP_SCRIPT_BASE64); + + // Get address of gunzip storage contract from StorageContractCreated event + const gunzipUploadReceipt = await gunzipUploadTransaction.wait(); + const storageContractCreatedEvent = gunzipUploadReceipt.events?.find( + (event) => { + if (event.event === "StorageContractCreated") { + return true; + } + } + ); + if (!storageContractCreatedEvent) { + throw new Error("Failed to find StorageContractCreated event"); + } + const gunzipStorageContractAddress = ( + storageContractCreatedEvent as StorageContractCreatedEvent + ).args.storageContract; + + // Deploy generator contract const bytecodeStorageLibraryAddress = BYTECODE_STORAGE_READER_LIBRARY_ADDRESSES[networkName]; - const genArt721GeneratorFactory = (await ethers.getContractFactory( - "GenArt721GeneratorV0", + const genArt721GeneratorFactory = new GenArt721GeneratorV0__factory( { - libraries: { - BytecodeStorageReader: bytecodeStorageLibraryAddress, - }, - } - )) as GenArt721GeneratorV0__factory; - const genArt721Generator: GenArt721GeneratorV0 = - await genArt721GeneratorFactory.deploy( + "contracts/libs/v0.8.x/BytecodeStorageV1.sol:BytecodeStorageReader": + bytecodeStorageLibraryAddress, + }, + deployer + ); + + const genArt721Generator: GenArt721GeneratorV0 = (await upgrades.deployProxy( + genArt721GeneratorFactory, + [ dependencyRegistryAddress, scriptyBuilderV2Address, - ethFSFileStorage.address - ); - + gunzipStorageContractAddress, + ], + { + unsafeAllow: ["external-library-linking"], + } + )) as GenArt721GeneratorV0; await genArt721Generator.deployed(); - console.log(`GenArt721GeneratorV0 deployed at ${genArt721Generator.address}`); + + const genArt721GeneratorAddress = genArt721Generator.address; + const implementationAddress = await upgrades.erc1967.getImplementationAddress( + genArt721GeneratorAddress + ); + console.log( + `GenArt721GeneratorV0 implementation deployed at ${implementationAddress}` + ); + console.log(`GenArt721GeneratorV0 deployed at ${genArt721GeneratorAddress}`); // Wait for 10 seconds to make sure etherscan has indexed the contracts await new Promise((resolve) => setTimeout(resolve, 10000)); @@ -61,8 +101,7 @@ async function main() { try { await hre.run("verify:verify", { - address: ethFSFileStorage.address, - constructorArguments: [ethFSAddress], + address: bytecodeStorageV1Writer.address, }); } catch (e) { console.error("Failed to verify ETHFSFileStorage programatically", e); @@ -70,12 +109,7 @@ async function main() { try { await hre.run("verify:verify", { - address: genArt721Generator.address, - constructorArguments: [ - dependencyRegistryAddress, - scriptyBuilderV2Address, - ethFSFileStorage.address, - ], + address: implementationAddress, }); } catch (e) { console.error("Failed to verify GenArt721GeneratorV0 programatically", e); diff --git a/packages/contracts/test/generator/GenArt721GeneratorV0.test.ts b/packages/contracts/test/generator/GenArt721GeneratorV0.test.ts index 60189583f..2038ede38 100644 --- a/packages/contracts/test/generator/GenArt721GeneratorV0.test.ts +++ b/packages/contracts/test/generator/GenArt721GeneratorV0.test.ts @@ -3,6 +3,7 @@ import { ethers } from "hardhat"; import { Contract } from "ethers"; import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; import zlib from "zlib"; +import { expectRevert } from "@openzeppelin/test-helpers"; import { AdminACLV0, @@ -10,6 +11,7 @@ import { MinterSetPriceV2, GenArt721GeneratorV0, GenArt721, + BytecodeStorageV1Writer, } from "../../scripts/contracts"; import { @@ -21,9 +23,15 @@ import { deployCoreWithMinterFilter, deployAndGetPBAB, } from "../util/common"; +import { StorageContractCreatedEvent } from "../../scripts/contracts/BytecodeStorageV1Writer"; const NO_OVERRIDE_ERROR = "Contract does not implement projectScriptDetails and has no override set."; +const ONLY_DEPENDENCY_REGISTRY_ADMIN_ACL_ERROR = + "Only DependencyRegistry AdminACL"; +const INVALID_DEPENDENCY_REGISTRY_ERROR = + "Contract at the provided address is not a valid DependencyRegistry"; + const ONE_MILLION = 1000000; // Default styles injected by genArt721Generator @@ -114,8 +122,33 @@ describe(`GenArt721GeneratorV0`, async function () { compressedDepScript.slice(Math.floor(compressedDepScript.length / 2)) ); - // Deploy mock file store which contains the gunzip script - const mockFs = await deployAndGet(config, "MockFileStore"); + // Deploy BytecodeStorageV1Writer contract + const bytecodeStorageV1Writer = (await deployAndGet( + config, + "BytecodeStorageV1Writer" + )) as BytecodeStorageV1Writer; + + // Use BytecodeStorageV1Writer to upload gunzip script + const gunzipUploadTransaction = + await bytecodeStorageV1Writer.writeStringToBytecodeStorage( + GUNZIP_SCRIPT_BASE64 + ); + + // Get address of gunzip storage contract from StorageContractCreated event + const gunzipUploadReceipt = await gunzipUploadTransaction.wait(); + const storageContractCreatedEvent = gunzipUploadReceipt.events?.find( + (event) => { + if (event.event === "StorageContractCreated") { + return true; + } + } + ); + if (!storageContractCreatedEvent) { + throw new Error("Failed to find StorageContractCreated event"); + } + const gunzipStorageContractAddress = ( + storageContractCreatedEvent as StorageContractCreatedEvent + ).args.storageContract; // Deploy scripty builder config.scriptyBuilder = await deployAndGet(config, "ScriptyBuilderV2"); @@ -123,13 +156,13 @@ describe(`GenArt721GeneratorV0`, async function () { // Deploy GenArt721GeneratorV0 config.genArt721Generator = (await deployWithStorageLibraryAndGet( config, - "GenArt721GeneratorV0", - [ - config.dependencyRegistry.address, - config.scriptyBuilder.address, - mockFs.address, - ] + "GenArt721GeneratorV0" )) as GenArt721GeneratorV0; + await config.genArt721Generator!.initialize( + config.dependencyRegistry.address, + config.scriptyBuilder.address, + gunzipStorageContractAddress + ); return config as GenArt721GeneratorV0TestConfig; } @@ -553,4 +586,100 @@ describe(`GenArt721GeneratorV0`, async function () { `data:text/html;base64,${Buffer.from(tokenHtml).toString("base64")}` ); }); + + describe("updateDependencyRegistry", function () { + it("updates dependencyRegistry", async function () { + const config = await loadFixture(_beforeEach); + + const newDependencyRegistry = (await deployWithStorageLibraryAndGet( + config, + "DependencyRegistryV0" + )) as DependencyRegistryV0; + + await newDependencyRegistry + .connect(config.accounts.deployer) + .initialize(config.adminACL!.address); + + await config.genArt721Generator + .connect(config.accounts.deployer) + .updateDependencyRegistry(newDependencyRegistry.address); + + expect(await config.genArt721Generator.dependencyRegistry()).to.equal( + newDependencyRegistry.address + ); + }); + it("reverts if not called by admin", async function () { + const config = await loadFixture(_beforeEach); + + const newDependencyRegistry = (await deployWithStorageLibraryAndGet( + config, + "DependencyRegistryV0" + )) as DependencyRegistryV0; + + await newDependencyRegistry + .connect(config.accounts.deployer) + .initialize(config.adminACL!.address); + + await expectRevert( + config.genArt721Generator + .connect(config.accounts.artist) + .updateDependencyRegistry(newDependencyRegistry.address), + ONLY_DEPENDENCY_REGISTRY_ADMIN_ACL_ERROR + ); + }); + }); + describe("updateScriptyBuilder", function () { + it("updates scriptyBuilder", async function () { + const config = await loadFixture(_beforeEach); + // Arbitrary address for testing + const newScriptyBuilderAddress = config.accounts.artist.address; + + await config.genArt721Generator + .connect(config.accounts.deployer) + .updateScriptyBuilder(newScriptyBuilderAddress); + + expect(await config.genArt721Generator.scriptyBuilder()).to.equal( + newScriptyBuilderAddress + ); + }); + it("reverts if not called by admin", async function () { + const config = await loadFixture(_beforeEach); + // Arbitrary address for testing + const newScriptyBuilderAddress = config.accounts.artist.address; + + await expectRevert( + config.genArt721Generator + .connect(config.accounts.artist) + .updateScriptyBuilder(newScriptyBuilderAddress), + ONLY_DEPENDENCY_REGISTRY_ADMIN_ACL_ERROR + ); + }); + }); + describe("updateGunzipStorageContract", function () { + it("updates gunzipStorageContract", async function () { + const config = await loadFixture(_beforeEach); + // Arbitrary address for testing + const newGunzipStorageContractAddress = config.accounts.artist.address; + + await config.genArt721Generator + .connect(config.accounts.deployer) + .updateGunzipScriptAddress(newGunzipStorageContractAddress); + + expect(await config.genArt721Generator.gunzipScriptAddress()).to.equal( + newGunzipStorageContractAddress + ); + }); + it("reverts if not called by admin", async function () { + const config = await loadFixture(_beforeEach); + // Arbitrary address for testing + const newGunzipStorageContractAddress = config.accounts.artist.address; + + await expectRevert( + config.genArt721Generator + .connect(config.accounts.artist) + .updateGunzipScriptAddress(newGunzipStorageContractAddress), + ONLY_DEPENDENCY_REGISTRY_ADMIN_ACL_ERROR + ); + }); + }); });