diff --git a/packages/nouns-contracts/.env.example b/packages/nouns-contracts/.env.example index 15c08e2f55..d4290ac0f8 100644 --- a/packages/nouns-contracts/.env.example +++ b/packages/nouns-contracts/.env.example @@ -8,3 +8,4 @@ WALLET_PUBLIC_KEY= WALLET_PRIVATE_KEY= FOUNDRY_PROFILE=lite +RPC_MAINNET= diff --git a/packages/nouns-contracts/contracts/NounsArt.sol b/packages/nouns-contracts/contracts/NounsArt.sol index c29711f5df..ac33c051a7 100644 --- a/packages/nouns-contracts/contracts/NounsArt.sol +++ b/packages/nouns-contracts/contracts/NounsArt.sol @@ -327,6 +327,154 @@ contract NounsArt is INounsArt { emit GlassesAdded(imageCount); } + /** + * @notice Replace all pages of body images with new ones. + * @param encodedCompressed bytes created by taking a string array of RLE-encoded images, abi encoding it as a bytes array, + * and finally compressing it using deflate. + * @param decompressedLength the size in bytes the images bytes were prior to compression; required input for Inflate. + * @param imageCount the number of images in this batch; used when searching for images among batches. + * @dev This function can only be called by the descriptor. + */ + function updateBodies( + bytes calldata encodedCompressed, + uint80 decompressedLength, + uint16 imageCount + ) external onlyDescriptor { + replaceTraitData(bodiesTrait, encodedCompressed, decompressedLength, imageCount); + + emit BodiesUpdated(imageCount); + } + + /** + * @notice Replace all pages of accessory images with new ones. + * @param encodedCompressed bytes created by taking a string array of RLE-encoded images, abi encoding it as a bytes array, + * and finally compressing it using deflate. + * @param decompressedLength the size in bytes the images bytes were prior to compression; required input for Inflate. + * @param imageCount the number of images in this batch; used when searching for images among batches. + * @dev This function can only be called by the descriptor. + */ + function updateAccessories( + bytes calldata encodedCompressed, + uint80 decompressedLength, + uint16 imageCount + ) external onlyDescriptor { + replaceTraitData(accessoriesTrait, encodedCompressed, decompressedLength, imageCount); + + emit AccessoriesUpdated(imageCount); + } + + /** + * @notice Replace all pages of head images with new ones. + * @param encodedCompressed bytes created by taking a string array of RLE-encoded images, abi encoding it as a bytes array, + * and finally compressing it using deflate. + * @param decompressedLength the size in bytes the images bytes were prior to compression; required input for Inflate. + * @param imageCount the number of images in this batch; used when searching for images among batches. + * @dev This function can only be called by the descriptor. + */ + function updateHeads( + bytes calldata encodedCompressed, + uint80 decompressedLength, + uint16 imageCount + ) external override onlyDescriptor { + replaceTraitData(headsTrait, encodedCompressed, decompressedLength, imageCount); + + emit HeadsUpdated(imageCount); + } + + /** + * @notice Replace all pages of glasses images with new ones. + * @param encodedCompressed bytes created by taking a string array of RLE-encoded images, abi encoding it as a bytes array, + * and finally compressing it using deflate. + * @param decompressedLength the size in bytes the images bytes were prior to compression; required input for Inflate. + * @param imageCount the number of images in this batch; used when searching for images among batches. + * @dev This function can only be called by the descriptor. + */ + function updateGlasses( + bytes calldata encodedCompressed, + uint80 decompressedLength, + uint16 imageCount + ) external onlyDescriptor { + replaceTraitData(glassesTrait, encodedCompressed, decompressedLength, imageCount); + + emit GlassesUpdated(imageCount); + } + + /** + * @notice Replace all pages of body images with new ones from an existing storage contract. + * @param pointer the address of a contract where the image batch was stored using SSTORE2. The data + * format is expected to be like {encodedCompressed}: bytes created by taking a string array of + * RLE-encoded images, abi encoding it as a bytes array, and finally compressing it using deflate. + * @param decompressedLength the size in bytes the images bytes were prior to compression; required input for Inflate. + * @param imageCount the number of images in this batch; used when searching for images among batches. + * @dev This function can only be called by the descriptor. + */ + function updateBodiesFromPointer( + address pointer, + uint80 decompressedLength, + uint16 imageCount + ) external onlyDescriptor { + replaceTraitData(bodiesTrait, pointer, decompressedLength, imageCount); + + emit BodiesUpdated(imageCount); + } + + /** + * @notice Replace all pages of accessory images with new ones from an existing storage contract. + * @param pointer the address of a contract where the image batch was stored using SSTORE2. The data + * format is expected to be like {encodedCompressed}: bytes created by taking a string array of + * RLE-encoded images, abi encoding it as a bytes array, and finally compressing it using deflate. + * @param decompressedLength the size in bytes the images bytes were prior to compression; required input for Inflate. + * @param imageCount the number of images in this batch; used when searching for images among batches. + * @dev This function can only be called by the descriptor. + */ + function updateAccessoriesFromPointer( + address pointer, + uint80 decompressedLength, + uint16 imageCount + ) external onlyDescriptor { + replaceTraitData(accessoriesTrait, pointer, decompressedLength, imageCount); + + emit AccessoriesUpdated(imageCount); + } + + /** + * @notice Replace all pages of head images with new ones from an existing storage contract. + * @param pointer the address of a contract where the image batch was stored using SSTORE2. The data + * format is expected to be like {encodedCompressed}: bytes created by taking a string array of + * RLE-encoded images, abi encoding it as a bytes array, and finally compressing it using deflate. + * @param decompressedLength the size in bytes the images bytes were prior to compression; required input for Inflate. + * @param imageCount the number of images in this batch; used when searching for images among batches. + * @dev This function can only be called by the descriptor. + */ + function updateHeadsFromPointer( + address pointer, + uint80 decompressedLength, + uint16 imageCount + ) external override onlyDescriptor { + replaceTraitData(headsTrait, pointer, decompressedLength, imageCount); + + emit HeadsUpdated(imageCount); + } + + /** + * @notice Replace all pages of glasses images with new ones from an existing storage contract. + * @param pointer the address of a contract where the image batch was stored using SSTORE2. The data + * format is expected to be like {encodedCompressed}: bytes created by taking a string array of + * RLE-encoded images, abi encoding it as a bytes array, and finally compressing it using deflate. + * @param decompressedLength the size in bytes the images bytes were prior to compression; required input for Inflate. + * @param imageCount the number of images in this batch; used when searching for images among batches. + * @dev This function can only be called by the descriptor. + */ + function updateGlassesFromPointer( + address pointer, + uint80 decompressedLength, + uint16 imageCount + ) external onlyDescriptor { + replaceTraitData(glassesTrait, pointer, decompressedLength, imageCount); + + emit GlassesUpdated(imageCount); + } + /** * @notice Get the number of available Noun `backgrounds`. */ @@ -405,6 +553,39 @@ contract NounsArt is INounsArt { backgrounds.push(_background); } + function replaceTraitData( + Trait storage trait, + bytes calldata encodedCompressed, + uint80 decompressedLength, + uint16 imageCount + ) internal { + if (encodedCompressed.length == 0) { + revert EmptyBytes(); + } + delete trait.storagePages; + delete trait.storedImagesCount; + + addPage(trait, encodedCompressed, decompressedLength, imageCount); + } + + function replaceTraitData( + Trait storage trait, + address pointer, + uint80 decompressedLength, + uint16 imageCount + ) internal { + if (decompressedLength == 0) { + revert BadDecompressedLength(); + } + if (imageCount == 0) { + revert BadImageCount(); + } + delete trait.storagePages; + delete trait.storedImagesCount; + + addPage(trait, pointer, decompressedLength, imageCount); + } + function addPage( Trait storage trait, bytes calldata encodedCompressed, diff --git a/packages/nouns-contracts/contracts/NounsDescriptorV3.sol b/packages/nouns-contracts/contracts/NounsDescriptorV3.sol new file mode 100644 index 0000000000..53736473d7 --- /dev/null +++ b/packages/nouns-contracts/contracts/NounsDescriptorV3.sol @@ -0,0 +1,618 @@ +// SPDX-License-Identifier: GPL-3.0 + +/// @title The Nouns NFT descriptor + +/********************************* + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░██░░░████░░██░░░████░░░ * + * ░░██████░░░████████░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + *********************************/ + +pragma solidity ^0.8.6; + +import { Ownable } from '@openzeppelin/contracts/access/Ownable.sol'; +import { Strings } from '@openzeppelin/contracts/utils/Strings.sol'; +import { INounsDescriptorV3 } from './interfaces/INounsDescriptorV3.sol'; +import { INounsSeeder } from './interfaces/INounsSeeder.sol'; +import { NFTDescriptorV2 } from './libs/NFTDescriptorV2.sol'; +import { ISVGRenderer } from './interfaces/ISVGRenderer.sol'; +import { INounsArt } from './interfaces/INounsArt.sol'; +import { IInflator } from './interfaces/IInflator.sol'; + +contract NounsDescriptorV3 is INounsDescriptorV3, Ownable { + using Strings for uint256; + + // prettier-ignore + // https://creativecommons.org/publicdomain/zero/1.0/legalcode.txt + bytes32 constant COPYRIGHT_CC0_1_0_UNIVERSAL_LICENSE = 0xa2010f343487d3f7618affe54f789f5487602331c0a8d03f49e9a7c547cf0499; + + /// @notice The contract responsible for holding compressed Noun art + INounsArt public art; + + /// @notice The contract responsible for constructing SVGs + ISVGRenderer public renderer; + + /// @notice Whether or not new Noun parts can be added + bool public override arePartsLocked; + + /// @notice Whether or not `tokenURI` should be returned as a data URI (Default: true) + bool public override isDataURIEnabled = true; + + /// @notice Base URI, used when isDataURIEnabled is false + string public override baseURI; + + /** + * @notice Require that the parts have not been locked. + */ + modifier whenPartsNotLocked() { + require(!arePartsLocked, 'Parts are locked'); + _; + } + + constructor(INounsArt _art, ISVGRenderer _renderer) { + art = _art; + renderer = _renderer; + } + + /** + * @notice Set the Noun's art contract. + * @dev Only callable by the owner when not locked. + */ + function setArt(INounsArt _art) external onlyOwner whenPartsNotLocked { + art = _art; + + emit ArtUpdated(_art); + } + + /** + * @notice Set the SVG renderer. + * @dev Only callable by the owner. + */ + function setRenderer(ISVGRenderer _renderer) external onlyOwner { + renderer = _renderer; + + emit RendererUpdated(_renderer); + } + + /** + * @notice Set the art contract's `descriptor`. + * @param descriptor the address to set. + * @dev Only callable by the owner. + */ + function setArtDescriptor(address descriptor) external onlyOwner { + art.setDescriptor(descriptor); + } + + /** + * @notice Set the art contract's `inflator`. + * @param inflator the address to set. + * @dev Only callable by the owner. + */ + function setArtInflator(IInflator inflator) external onlyOwner { + art.setInflator(inflator); + } + + /** + * @notice Get the number of available Noun `backgrounds`. + */ + function backgroundCount() public view override returns (uint256) { + return art.backgroundCount(); + } + + /** + * @notice Get the number of available Noun `bodies`. + */ + function bodyCount() public view override returns (uint256) { + return art.bodyCount(); + } + + /** + * @notice Get the number of available Noun `accessories`. + */ + function accessoryCount() public view override returns (uint256) { + return art.accessoryCount(); + } + + /** + * @notice Get the number of available Noun `heads`. + */ + function headCount() public view override returns (uint256) { + return art.headCount(); + } + + /** + * @notice Get the number of available Noun `glasses`. + */ + function glassesCount() public view override returns (uint256) { + return art.glassesCount(); + } + + /** + * @notice Batch add Noun backgrounds. + * @dev This function can only be called by the owner when not locked. + */ + function addManyBackgrounds(string[] calldata _backgrounds) external override onlyOwner whenPartsNotLocked { + art.addManyBackgrounds(_backgrounds); + } + + /** + * @notice Add a Noun background. + * @dev This function can only be called by the owner when not locked. + */ + function addBackground(string calldata _background) external override onlyOwner whenPartsNotLocked { + art.addBackground(_background); + } + + /** + * @notice Update a single color palette. This function can be used to + * add a new color palette or update an existing palette. + * @param paletteIndex the identifier of this palette + * @param palette byte array of colors. every 3 bytes represent an RGB color. max length: 256 * 3 = 768 + * @dev This function can only be called by the owner when not locked. + */ + function setPalette(uint8 paletteIndex, bytes calldata palette) external override onlyOwner whenPartsNotLocked { + art.setPalette(paletteIndex, palette); + } + + /** + * @notice Add a batch of body images. + * @param encodedCompressed bytes created by taking a string array of RLE-encoded images, abi encoding it as a bytes array, + * and finally compressing it using deflate. + * @param decompressedLength the size in bytes the images bytes were prior to compression; required input for Inflate. + * @param imageCount the number of images in this batch; used when searching for images among batches. + * @dev This function can only be called by the owner when not locked. + */ + function addBodies( + bytes calldata encodedCompressed, + uint80 decompressedLength, + uint16 imageCount + ) external override onlyOwner whenPartsNotLocked { + art.addBodies(encodedCompressed, decompressedLength, imageCount); + } + + /** + * @notice Add a batch of accessory images. + * @param encodedCompressed bytes created by taking a string array of RLE-encoded images, abi encoding it as a bytes array, + * and finally compressing it using deflate. + * @param decompressedLength the size in bytes the images bytes were prior to compression; required input for Inflate. + * @param imageCount the number of images in this batch; used when searching for images among batches. + * @dev This function can only be called by the owner when not locked. + */ + function addAccessories( + bytes calldata encodedCompressed, + uint80 decompressedLength, + uint16 imageCount + ) external override onlyOwner whenPartsNotLocked { + art.addAccessories(encodedCompressed, decompressedLength, imageCount); + } + + /** + * @notice Add a batch of head images. + * @param encodedCompressed bytes created by taking a string array of RLE-encoded images, abi encoding it as a bytes array, + * and finally compressing it using deflate. + * @param decompressedLength the size in bytes the images bytes were prior to compression; required input for Inflate. + * @param imageCount the number of images in this batch; used when searching for images among batches. + * @dev This function can only be called by the owner when not locked. + */ + function addHeads( + bytes calldata encodedCompressed, + uint80 decompressedLength, + uint16 imageCount + ) external override onlyOwner whenPartsNotLocked { + art.addHeads(encodedCompressed, decompressedLength, imageCount); + } + + /** + * @notice Add a batch of glasses images. + * @param encodedCompressed bytes created by taking a string array of RLE-encoded images, abi encoding it as a bytes array, + * and finally compressing it using deflate. + * @param decompressedLength the size in bytes the images bytes were prior to compression; required input for Inflate. + * @param imageCount the number of images in this batch; used when searching for images among batches. + * @dev This function can only be called by the owner when not locked. + */ + function addGlasses( + bytes calldata encodedCompressed, + uint80 decompressedLength, + uint16 imageCount + ) external override onlyOwner whenPartsNotLocked { + art.addGlasses(encodedCompressed, decompressedLength, imageCount); + } + + /** + * @notice Update a single color palette. This function can be used to + * add a new color palette or update an existing palette. This function does not check for data length validity + * (len <= 768, len % 3 == 0). + * @param paletteIndex the identifier of this palette + * @param pointer the address of the contract holding the palette bytes. every 3 bytes represent an RGB color. + * max length: 256 * 3 = 768. + * @dev This function can only be called by the owner when not locked. + */ + function setPalettePointer(uint8 paletteIndex, address pointer) external override onlyOwner whenPartsNotLocked { + art.setPalettePointer(paletteIndex, pointer); + } + + /** + * @notice Add a batch of body images from an existing storage contract. + * @param pointer the address of a contract where the image batch was stored using SSTORE2. The data + * format is expected to be like {encodedCompressed}: bytes created by taking a string array of + * RLE-encoded images, abi encoding it as a bytes array, and finally compressing it using deflate. + * @param decompressedLength the size in bytes the images bytes were prior to compression; required input for Inflate. + * @param imageCount the number of images in this batch; used when searching for images among batches. + * @dev This function can only be called by the owner when not locked. + */ + function addBodiesFromPointer( + address pointer, + uint80 decompressedLength, + uint16 imageCount + ) external override onlyOwner whenPartsNotLocked { + art.addBodiesFromPointer(pointer, decompressedLength, imageCount); + } + + /** + * @notice Add a batch of accessory images from an existing storage contract. + * @param pointer the address of a contract where the image batch was stored using SSTORE2. The data + * format is expected to be like {encodedCompressed}: bytes created by taking a string array of + * RLE-encoded images, abi encoding it as a bytes array, and finally compressing it using deflate. + * @param decompressedLength the size in bytes the images bytes were prior to compression; required input for Inflate. + * @param imageCount the number of images in this batch; used when searching for images among batches. + * @dev This function can only be called by the owner when not locked. + */ + function addAccessoriesFromPointer( + address pointer, + uint80 decompressedLength, + uint16 imageCount + ) external override onlyOwner whenPartsNotLocked { + art.addAccessoriesFromPointer(pointer, decompressedLength, imageCount); + } + + /** + * @notice Add a batch of head images from an existing storage contract. + * @param pointer the address of a contract where the image batch was stored using SSTORE2. The data + * format is expected to be like {encodedCompressed}: bytes created by taking a string array of + * RLE-encoded images, abi encoding it as a bytes array, and finally compressing it using deflate. + * @param decompressedLength the size in bytes the images bytes were prior to compression; required input for Inflate. + * @param imageCount the number of images in this batch; used when searching for images among batches. + * @dev This function can only be called by the owner when not locked. + */ + function addHeadsFromPointer( + address pointer, + uint80 decompressedLength, + uint16 imageCount + ) external override onlyOwner whenPartsNotLocked { + art.addHeadsFromPointer(pointer, decompressedLength, imageCount); + } + + /** + * @notice Add a batch of glasses images from an existing storage contract. + * @param pointer the address of a contract where the image batch was stored using SSTORE2. The data + * format is expected to be like {encodedCompressed}: bytes created by taking a string array of + * RLE-encoded images, abi encoding it as a bytes array, and finally compressing it using deflate. + * @param decompressedLength the size in bytes the images bytes were prior to compression; required input for Inflate. + * @param imageCount the number of images in this batch; used when searching for images among batches. + * @dev This function can only be called by the owner when not locked. + */ + function addGlassesFromPointer( + address pointer, + uint80 decompressedLength, + uint16 imageCount + ) external override onlyOwner whenPartsNotLocked { + art.addGlassesFromPointer(pointer, decompressedLength, imageCount); + } + + /** + * @notice Get a background color by ID. + * @param index the index of the background. + * @return string the RGB hex value of the background. + */ + function backgrounds(uint256 index) public view override returns (string memory) { + return art.backgrounds(index); + } + + /** + * @notice Get a head image by ID. + * @param index the index of the head. + * @return bytes the RLE-encoded bytes of the image. + */ + function heads(uint256 index) public view override returns (bytes memory) { + return art.heads(index); + } + + /** + * @notice Get a body image by ID. + * @param index the index of the body. + * @return bytes the RLE-encoded bytes of the image. + */ + function bodies(uint256 index) public view override returns (bytes memory) { + return art.bodies(index); + } + + /** + * @notice Get an accessory image by ID. + * @param index the index of the accessory. + * @return bytes the RLE-encoded bytes of the image. + */ + function accessories(uint256 index) public view override returns (bytes memory) { + return art.accessories(index); + } + + /** + * @notice Get a glasses image by ID. + * @param index the index of the glasses. + * @return bytes the RLE-encoded bytes of the image. + */ + function glasses(uint256 index) public view override returns (bytes memory) { + return art.glasses(index); + } + + /** + * @notice Get a color palette by ID. + * @param index the index of the palette. + * @return bytes the palette bytes, where every 3 consecutive bytes represent a color in RGB format. + */ + function palettes(uint8 index) public view override returns (bytes memory) { + return art.palettes(index); + } + + /** + * @notice Lock all Noun parts. + * @dev This cannot be reversed and can only be called by the owner when not locked. + */ + function lockParts() external override onlyOwner whenPartsNotLocked { + arePartsLocked = true; + + emit PartsLocked(); + } + + /** + * @notice Toggle a boolean value which determines if `tokenURI` returns a data URI + * or an HTTP URL. + * @dev This can only be called by the owner. + */ + function toggleDataURIEnabled() external override onlyOwner { + bool enabled = !isDataURIEnabled; + + isDataURIEnabled = enabled; + emit DataURIToggled(enabled); + } + + /** + * @notice Set the base URI for all token IDs. It is automatically + * added as a prefix to the value returned in {tokenURI}, or to the + * token ID if {tokenURI} is empty. + * @dev This can only be called by the owner. + */ + function setBaseURI(string calldata _baseURI) external override onlyOwner { + baseURI = _baseURI; + + emit BaseURIUpdated(_baseURI); + } + + /** + * @notice Given a token ID and seed, construct a token URI for an official Nouns DAO noun. + * @dev The returned value may be a base64 encoded data URI or an API URL. + */ + function tokenURI(uint256 tokenId, INounsSeeder.Seed memory seed) external view override returns (string memory) { + if (isDataURIEnabled) { + return dataURI(tokenId, seed); + } + return string(abi.encodePacked(baseURI, tokenId.toString())); + } + + /** + * @notice Given a token ID and seed, construct a base64 encoded data URI for an official Nouns DAO noun. + */ + function dataURI(uint256 tokenId, INounsSeeder.Seed memory seed) public view override returns (string memory) { + string memory nounId = tokenId.toString(); + string memory name = string(abi.encodePacked('Noun ', nounId)); + string memory description = string(abi.encodePacked('Noun ', nounId, ' is a member of the Nouns DAO')); + + return genericDataURI(name, description, seed); + } + + /** + * @notice Given a name, description, and seed, construct a base64 encoded data URI. + */ + function genericDataURI( + string memory name, + string memory description, + INounsSeeder.Seed memory seed + ) public view override returns (string memory) { + NFTDescriptorV2.TokenURIParams memory params = NFTDescriptorV2.TokenURIParams({ + name: name, + description: description, + parts: getPartsForSeed(seed), + background: art.backgrounds(seed.background) + }); + return NFTDescriptorV2.constructTokenURI(renderer, params); + } + + /** + * @notice Given a seed, construct a base64 encoded SVG image. + */ + function generateSVGImage(INounsSeeder.Seed memory seed) external view override returns (string memory) { + ISVGRenderer.SVGParams memory params = ISVGRenderer.SVGParams({ + parts: getPartsForSeed(seed), + background: art.backgrounds(seed.background) + }); + return NFTDescriptorV2.generateSVGImage(renderer, params); + } + + /** + * @notice Get all Noun parts for the passed `seed`. + */ + function getPartsForSeed(INounsSeeder.Seed memory seed) public view returns (ISVGRenderer.Part[] memory) { + bytes memory body = art.bodies(seed.body); + bytes memory accessory = art.accessories(seed.accessory); + bytes memory head = art.heads(seed.head); + bytes memory glasses_ = art.glasses(seed.glasses); + + ISVGRenderer.Part[] memory parts = new ISVGRenderer.Part[](4); + parts[0] = ISVGRenderer.Part({ image: body, palette: _getPalette(body) }); + parts[1] = ISVGRenderer.Part({ image: accessory, palette: _getPalette(accessory) }); + parts[2] = ISVGRenderer.Part({ image: head, palette: _getPalette(head) }); + parts[3] = ISVGRenderer.Part({ image: glasses_, palette: _getPalette(glasses_) }); + return parts; + } + + /** + * @notice Get the color palette pointer for the passed part. + */ + function _getPalette(bytes memory part) private view returns (bytes memory) { + return art.palettes(uint8(part[0])); + } + + /** + * @notice Replace all pages of accessories images with new ones. + * @param encodedCompressed bytes created by taking a string array of RLE-encoded images, abi encoding it as a bytes array, + * and finally compressing it using deflate. + * @param decompressedLength the size in bytes the images bytes were prior to compression; required input for Inflate. + * @param imageCount the number of images in this batch; used when searching for images among batches. + * @dev This function can only be called by the owner when not locked. + */ + function updateAccessories( + bytes calldata encodedCompressed, + uint80 decompressedLength, + uint16 imageCount + ) external override onlyOwner whenPartsNotLocked { + uint256 originalCount = accessoryCount(); + art.updateAccessories(encodedCompressed, decompressedLength, imageCount); + require(originalCount == accessoryCount(), 'Image count must remain the same'); + } + + /** + * @notice Replace all pages of body images with new ones. + * @param encodedCompressed bytes created by taking a string array of RLE-encoded images, abi encoding it as a bytes array, + * and finally compressing it using deflate. + * @param decompressedLength the size in bytes the images bytes were prior to compression; required input for Inflate. + * @param imageCount the number of images in this batch; used when searching for images among batches. + * @dev This function can only be called by the owner when not locked. + */ + function updateBodies( + bytes calldata encodedCompressed, + uint80 decompressedLength, + uint16 imageCount + ) external override onlyOwner whenPartsNotLocked { + uint256 originalCount = bodyCount(); + art.updateBodies(encodedCompressed, decompressedLength, imageCount); + require(originalCount == bodyCount(), 'Image count must remain the same'); + } + + /** + * @notice Replace current batch of head images with new ones. + * @param encodedCompressed bytes created by taking a string array of RLE-encoded images, abi encoding it as a bytes array, + * and finally compressing it using deflate. + * @param decompressedLength the size in bytes the images bytes were prior to compression; required input for Inflate. + * @param imageCount the number of images in this batch; used when searching for images among batches. + * @dev This function can only be called by the owner when not locked. + */ + function updateHeads( + bytes calldata encodedCompressed, + uint80 decompressedLength, + uint16 imageCount + ) external override onlyOwner whenPartsNotLocked { + uint256 originalCount = headCount(); + art.updateHeads(encodedCompressed, decompressedLength, imageCount); + require(originalCount == headCount(), 'Image count must remain the same'); + } + + /** + * @notice Replace all pages of glasses images with new ones. + * @param encodedCompressed bytes created by taking a string array of RLE-encoded images, abi encoding it as a bytes array, + * and finally compressing it using deflate. + * @param decompressedLength the size in bytes the images bytes were prior to compression; required input for Inflate. + * @param imageCount the number of images in this batch; used when searching for images among batches. + * @dev This function can only be called by the owner when not locked. + */ + function updateGlasses( + bytes calldata encodedCompressed, + uint80 decompressedLength, + uint16 imageCount + ) external override onlyOwner whenPartsNotLocked { + uint256 originalCount = glassesCount(); + art.updateGlasses(encodedCompressed, decompressedLength, imageCount); + require(originalCount == glassesCount(), 'Image count must remain the same'); + } + + /** + * @notice Replace current batch of accessories images with new ones from an existing storage contract. + * @param pointer the address of a contract where the image batch was stored using SSTORE2. The data + * format is expected to be like {encodedCompressed}: bytes created by taking a string array of + * RLE-encoded images, abi encoding it as a bytes array, and finally compressing it using deflate. + * @param decompressedLength the size in bytes the images bytes were prior to compression; required input for Inflate. + * @param imageCount the number of images in this batch; used when searching for images among batches. + * @dev This function can only be called by the owner when not locked. + */ + function updateAccessoriesFromPointer( + address pointer, + uint80 decompressedLength, + uint16 imageCount + ) external override onlyOwner whenPartsNotLocked { + uint256 originalCount = accessoryCount(); + art.updateAccessoriesFromPointer(pointer, decompressedLength, imageCount); + require(originalCount == accessoryCount(), 'Image count must remain the same'); + } + + /** + * @notice Replace all pages of body images with new ones from an existing storage contract. + * @param pointer the address of a contract where the image batch was stored using SSTORE2. The data + * format is expected to be like {encodedCompressed}: bytes created by taking a string array of + * RLE-encoded images, abi encoding it as a bytes array, and finally compressing it using deflate. + * @param decompressedLength the size in bytes the images bytes were prior to compression; required input for Inflate. + * @param imageCount the number of images in this batch; used when searching for images among batches. + * @dev This function can only be called by the owner when not locked. + */ + function updateBodiesFromPointer( + address pointer, + uint80 decompressedLength, + uint16 imageCount + ) external override onlyOwner whenPartsNotLocked { + uint256 originalCount = bodyCount(); + art.updateBodiesFromPointer(pointer, decompressedLength, imageCount); + require(originalCount == bodyCount(), 'Image count must remain the same'); + } + + /** + * @notice Replace all pages of head images with new ones from an existing storage contract. + * @param pointer the address of a contract where the image batch was stored using SSTORE2. The data + * format is expected to be like {encodedCompressed}: bytes created by taking a string array of + * RLE-encoded images, abi encoding it as a bytes array, and finally compressing it using deflate. + * @param decompressedLength the size in bytes the images bytes were prior to compression; required input for Inflate. + * @param imageCount the number of images in this batch; used when searching for images among batches. + * @dev This function can only be called by the owner when not locked. + */ + function updateHeadsFromPointer( + address pointer, + uint80 decompressedLength, + uint16 imageCount + ) external override onlyOwner whenPartsNotLocked { + uint256 originalCount = headCount(); + art.updateHeadsFromPointer(pointer, decompressedLength, imageCount); + require(originalCount == headCount(), 'Image count must remain the same'); + } + + /** + * @notice Replace all pages of glasses images with new ones from an existing storage contract. + * @param pointer the address of a contract where the image batch was stored using SSTORE2. The data + * format is expected to be like {encodedCompressed}: bytes created by taking a string array of + * RLE-encoded images, abi encoding it as a bytes array, and finally compressing it using deflate. + * @param decompressedLength the size in bytes the images bytes were prior to compression; required input for Inflate. + * @param imageCount the number of images in this batch; used when searching for images among batches. + * @dev This function can only be called by the owner when not locked. + */ + function updateGlassesFromPointer( + address pointer, + uint80 decompressedLength, + uint16 imageCount + ) external override onlyOwner whenPartsNotLocked { + uint256 originalCount = glassesCount(); + art.updateGlassesFromPointer(pointer, decompressedLength, imageCount); + require(originalCount == glassesCount(), 'Image count must remain the same'); + } +} \ No newline at end of file diff --git a/packages/nouns-contracts/contracts/interfaces/INounsArt.sol b/packages/nouns-contracts/contracts/interfaces/INounsArt.sol index 92ba778b51..254b9dd5e8 100644 --- a/packages/nouns-contracts/contracts/interfaces/INounsArt.sol +++ b/packages/nouns-contracts/contracts/interfaces/INounsArt.sol @@ -52,6 +52,14 @@ interface INounsArt { event GlassesAdded(uint16 count); + event BodiesUpdated(uint16 count); + + event AccessoriesUpdated(uint16 count); + + event HeadsUpdated(uint16 count); + + event GlassesUpdated(uint16 count); + struct NounArtStoragePage { uint16 imageCount; uint80 decompressedLength; @@ -156,4 +164,52 @@ interface INounsArt { function getHeadsTrait() external view returns (Trait memory); function getGlassesTrait() external view returns (Trait memory); -} + + function updateBodies( + bytes calldata encodedCompressed, + uint80 decompressedLength, + uint16 imageCount + ) external; + + function updateAccessories( + bytes calldata encodedCompressed, + uint80 decompressedLength, + uint16 imageCount + ) external; + + function updateHeads( + bytes calldata encodedCompressed, + uint80 decompressedLength, + uint16 imageCount + ) external; + + function updateGlasses( + bytes calldata encodedCompressed, + uint80 decompressedLength, + uint16 imageCount + ) external; + + function updateBodiesFromPointer( + address pointer, + uint80 decompressedLength, + uint16 imageCount + ) external; + + function updateAccessoriesFromPointer( + address pointer, + uint80 decompressedLength, + uint16 imageCount + ) external; + + function updateHeadsFromPointer( + address pointer, + uint80 decompressedLength, + uint16 imageCount + ) external; + + function updateGlassesFromPointer( + address pointer, + uint80 decompressedLength, + uint16 imageCount + ) external; +} \ No newline at end of file diff --git a/packages/nouns-contracts/contracts/interfaces/INounsDescriptorV3.sol b/packages/nouns-contracts/contracts/interfaces/INounsDescriptorV3.sol new file mode 100644 index 0000000000..909f4adb3f --- /dev/null +++ b/packages/nouns-contracts/contracts/interfaces/INounsDescriptorV3.sol @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: GPL-3.0 + +/// @title Interface for NounsDescriptorV3 + +/********************************* + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░██░░░████░░██░░░████░░░ * + * ░░██████░░░████████░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + *********************************/ + +pragma solidity ^0.8.6; + +import { INounsSeeder } from './INounsSeeder.sol'; +import { ISVGRenderer } from './ISVGRenderer.sol'; +import { INounsArt } from './INounsArt.sol'; +import { INounsDescriptorMinimal } from './INounsDescriptorMinimal.sol'; + +interface INounsDescriptorV3 is INounsDescriptorMinimal { + event PartsLocked(); + + event DataURIToggled(bool enabled); + + event BaseURIUpdated(string baseURI); + + event ArtUpdated(INounsArt art); + + event RendererUpdated(ISVGRenderer renderer); + + error EmptyPalette(); + error BadPaletteLength(); + error IndexNotFound(); + + function arePartsLocked() external returns (bool); + + function isDataURIEnabled() external returns (bool); + + function baseURI() external returns (string memory); + + function palettes(uint8 paletteIndex) external view returns (bytes memory); + + function backgrounds(uint256 index) external view returns (string memory); + + function bodies(uint256 index) external view returns (bytes memory); + + function accessories(uint256 index) external view returns (bytes memory); + + function heads(uint256 index) external view returns (bytes memory); + + function glasses(uint256 index) external view returns (bytes memory); + + function backgroundCount() external view override returns (uint256); + + function bodyCount() external view override returns (uint256); + + function accessoryCount() external view override returns (uint256); + + function headCount() external view override returns (uint256); + + function glassesCount() external view override returns (uint256); + + function addManyBackgrounds(string[] calldata backgrounds) external; + + function addBackground(string calldata background) external; + + function setPalette(uint8 paletteIndex, bytes calldata palette) external; + + function addBodies( + bytes calldata encodedCompressed, + uint80 decompressedLength, + uint16 imageCount + ) external; + + function addAccessories( + bytes calldata encodedCompressed, + uint80 decompressedLength, + uint16 imageCount + ) external; + + function addHeads( + bytes calldata encodedCompressed, + uint80 decompressedLength, + uint16 imageCount + ) external; + + function addGlasses( + bytes calldata encodedCompressed, + uint80 decompressedLength, + uint16 imageCount + ) external; + + function setPalettePointer(uint8 paletteIndex, address pointer) external; + + function addBodiesFromPointer( + address pointer, + uint80 decompressedLength, + uint16 imageCount + ) external; + + function addAccessoriesFromPointer( + address pointer, + uint80 decompressedLength, + uint16 imageCount + ) external; + + function addHeadsFromPointer( + address pointer, + uint80 decompressedLength, + uint16 imageCount + ) external; + + function addGlassesFromPointer( + address pointer, + uint80 decompressedLength, + uint16 imageCount + ) external; + + function lockParts() external; + + function toggleDataURIEnabled() external; + + function setBaseURI(string calldata baseURI) external; + + function tokenURI(uint256 tokenId, INounsSeeder.Seed memory seed) external view override returns (string memory); + + function dataURI(uint256 tokenId, INounsSeeder.Seed memory seed) external view override returns (string memory); + + function genericDataURI( + string calldata name, + string calldata description, + INounsSeeder.Seed memory seed + ) external view returns (string memory); + + function generateSVGImage(INounsSeeder.Seed memory seed) external view returns (string memory); + + function updateAccessories( + bytes calldata encodedCompressed, + uint80 decompressedLength, + uint16 imageCount + ) external; + + function updateBodies( + bytes calldata encodedCompressed, + uint80 decompressedLength, + uint16 imageCount + ) external; + + function updateHeads( + bytes calldata encodedCompressed, + uint80 decompressedLength, + uint16 imageCount + ) external; + + function updateGlasses( + bytes calldata encodedCompressed, + uint80 decompressedLength, + uint16 imageCount + ) external; + + function updateAccessoriesFromPointer( + address pointer, + uint80 decompressedLength, + uint16 imageCount + ) external; + + function updateBodiesFromPointer( + address pointer, + uint80 decompressedLength, + uint16 imageCount + ) external; + + function updateHeadsFromPointer( + address pointer, + uint80 decompressedLength, + uint16 imageCount + ) external; + + function updateGlassesFromPointer( + address pointer, + uint80 decompressedLength, + uint16 imageCount + ) external; +} \ No newline at end of file diff --git a/packages/nouns-contracts/files/proposal-test-traits.json b/packages/nouns-contracts/files/proposal-test-traits.json new file mode 100644 index 0000000000..fbb29e842a --- /dev/null +++ b/packages/nouns-contracts/files/proposal-test-traits.json @@ -0,0 +1,9 @@ +[ + { + "glasses": { + "encodedCompressed": "0xcd55416e1a4110dcee61bcded8899002f209c9488b2c7ec0253772e4e80b51a4f890e40b1cf3007ec025fc8423af40794aca356a9230a36d6f7209d2d6767575570debb5a8aacecf7db75cdd75cbfaa35b0f9f1cddd91f38fb03673f3afbd1d9bf72f6af0edd7aedecd74efeb5b37fedec37ce7ee3ecbffad6addf74cbd58db37febecdf3afbafef1dfd7bb7fec6d9af1eab9bbb611daaaba9e00a954c55b515b0544562c86a9d6a362b53f37142ffa3fcf796df62af0d5568e5339c787fdb22a3952fe01f703fd7483fcfa7fbcbf32e3fab8b7c695503b3546b5c11d780e7f8d5bfe4e11fcef1d1f297d85f227fc9270996aa480c445d6aa6977ab234bf1ef935e671979a0e60a98ac440d45a33bdd493b35f8ffc03e60fc83fd0012c559118887ad04c2ff5e4607e7efebb8bbfffa0157d7ed79eefd7b8d0577bdfeabf78ef9ecc7f88f92dbedf90270493ed7335d85a47c981d94ca9275bf3f4cfd0f5393fff23bc8e38df91ee60a98ac440d4a3667aa92747f3eb91bfc0fc02f90b3a80a52a120351179ae9a59e2cccaf47fe09f327e49fe80096aa480c443d69a6977a7232bf1ef90de61be43774004b552406a2369ae9a59e34e6d7237f8cf931f2c774004b552406a28e35d34b3d199b9f9ffff4fbefcfee8fdf9f1dff3f76d65172603653eac9ce3cfd33bce8f9cce035c3f96674074b552406a2ce34d34b3d99995f8ffc35e6d7c85feb573880a52a120351d79ae9a59eaccdaf47fe04f313e44ff80dc052158981a813cdf4524f26e6d7237f8ff93df2f774004b552406a2ee35d34b3dd99b9f9f7f7e3f1f303f42fe031dc064c4f773641d25076633a59e8cccd33fc38b9ecf1c5e739c6f4e77b054456220ea5c33bdd493b9f9f5c8df607e83fc0d1dc052158981a81bcdf4524f36e6d7237f85f915f25774004b552406a2ae34d34b3d59991f027e02", + "originalLength": 3808, + "itemCount": 21 + } + } + ] \ No newline at end of file diff --git a/packages/nouns-contracts/tasks/deploy-and-configure-short-times-dao-v3.ts b/packages/nouns-contracts/tasks/deploy-and-configure-short-times-dao-v3.ts index 0afe5121f6..6524093ab3 100644 --- a/packages/nouns-contracts/tasks/deploy-and-configure-short-times-dao-v3.ts +++ b/packages/nouns-contracts/tasks/deploy-and-configure-short-times-dao-v3.ts @@ -85,13 +85,13 @@ task( // Populate the on-chain art await run('populate-descriptor', { nftDescriptor: contracts.NFTDescriptorV2.address, - nounsDescriptor: contracts.NounsDescriptorV2.address, + nounsDescriptor: contracts.NounsDescriptorV3.address, }); // Transfer ownership of all contract except for the auction house. // We must maintain ownership of the auction house to kick off the first auction. const executorAddress = contracts.NounsDAOExecutorProxy.instance.address; - await contracts.NounsDescriptorV2.instance.transferOwnership(executorAddress); + await contracts.NounsDescriptorV3.instance.transferOwnership(executorAddress); await contracts.NounsToken.instance.transferOwnership(executorAddress); await contracts.NounsAuctionHouseProxyAdmin.instance.transferOwnership(executorAddress); console.log( diff --git a/packages/nouns-contracts/tasks/deploy-descriptor-v2.ts b/packages/nouns-contracts/tasks/deploy-descriptor-v3.ts similarity index 93% rename from packages/nouns-contracts/tasks/deploy-descriptor-v2.ts rename to packages/nouns-contracts/tasks/deploy-descriptor-v3.ts index b93bfb2313..c6c75e1ebf 100644 --- a/packages/nouns-contracts/tasks/deploy-descriptor-v2.ts +++ b/packages/nouns-contracts/tasks/deploy-descriptor-v3.ts @@ -6,10 +6,10 @@ async function delay(seconds: number) { return new Promise(resolve => setTimeout(resolve, 1000 * seconds)); } -task('deploy-descriptor-v2', 'Deploy NounsDescriptorV2 & populate it with art') +task('deploy-descriptor-v3', 'Deploy NounsDescriptorV3 & populate it with art') .addParam( 'daoExecutor', - 'The address of the NounsDAOExecutor that should be the owner of the descriptor.', + 'The address of the NounsDAOExecutor that should be the owner of the descriptor.' ) .setAction(async ({ daoExecutor }, { ethers, run, network }) => { const contracts: Record = {} as Record< @@ -44,7 +44,7 @@ task('deploy-descriptor-v2', 'Deploy NounsDescriptorV2 & populate it with art') libraries: {}, }; - const nounsDescriptorFactory = await ethers.getContractFactory('NounsDescriptorV2', { + const nounsDescriptorFactory = await ethers.getContractFactory('NounsDescriptorV3', { libraries: { NFTDescriptorV2: library.address, }, @@ -53,8 +53,8 @@ task('deploy-descriptor-v2', 'Deploy NounsDescriptorV2 & populate it with art') expectedNounsArtAddress, renderer.address, ); - contracts.NounsDescriptorV2 = { - name: 'NounsDescriptorV2', + contracts.NounsDescriptorV3 = { + name: 'NounsDescriptorV3', address: nounsDescriptor.address, constructorArguments: [expectedNounsArtAddress, renderer.address], instance: nounsDescriptor, @@ -96,7 +96,7 @@ task('deploy-descriptor-v2', 'Deploy NounsDescriptorV2 & populate it with art') console.log('Populating Descriptor...'); await run('populate-descriptor', { nftDescriptor: contracts.NFTDescriptorV2.address, - nounsDescriptor: contracts.NounsDescriptorV2.address, + nounsDescriptor: contracts.NounsDescriptorV3.address, }); console.log('Population complete.'); @@ -114,4 +114,4 @@ task('deploy-descriptor-v2', 'Deploy NounsDescriptorV2 & populate it with art') }); console.log('Verify complete.'); } - }); + }); \ No newline at end of file diff --git a/packages/nouns-contracts/tasks/deploy-local-dao-v3.ts b/packages/nouns-contracts/tasks/deploy-local-dao-v3.ts index 90f5843e94..a0e4818db4 100644 --- a/packages/nouns-contracts/tasks/deploy-local-dao-v3.ts +++ b/packages/nouns-contracts/tasks/deploy-local-dao-v3.ts @@ -92,7 +92,7 @@ task('deploy-local-dao-v3', 'Deploy contracts to hardhat') WETH: {}, NFTDescriptorV2: {}, SVGRenderer: {}, - NounsDescriptorV2: { + NounsDescriptorV3: { args: [expectedNounsArtAddress, () => contracts.SVGRenderer.instance?.address], libraries: () => ({ NFTDescriptorV2: contracts.NFTDescriptorV2.instance?.address as string, @@ -101,7 +101,7 @@ task('deploy-local-dao-v3', 'Deploy contracts to hardhat') Inflator: {}, NounsArt: { args: [ - () => contracts.NounsDescriptorV2.instance?.address, + () => contracts.NounsDescriptorV3.instance?.address, () => contracts.Inflator.instance?.address, ], }, @@ -110,7 +110,7 @@ task('deploy-local-dao-v3', 'Deploy contracts to hardhat') args: [ args.noundersdao || deployer.address, expectedAuctionHouseProxyAddress, - () => contracts.NounsDescriptorV2.instance?.address, + () => contracts.NounsDescriptorV3.instance?.address, () => contracts.NounsSeeder.instance?.address, proxyRegistryAddress, ], diff --git a/packages/nouns-contracts/tasks/deploy-short-times-dao-v3.ts b/packages/nouns-contracts/tasks/deploy-short-times-dao-v3.ts index 93db50839f..26d345a3fe 100644 --- a/packages/nouns-contracts/tasks/deploy-short-times-dao-v3.ts +++ b/packages/nouns-contracts/tasks/deploy-short-times-dao-v3.ts @@ -138,7 +138,7 @@ task('deploy-short-times-dao-v3', 'Deploy all Nouns contracts with short gov tim const contracts: Record = { NFTDescriptorV2: {}, SVGRenderer: {}, - NounsDescriptorV2: { + NounsDescriptorV3: { args: [expectedNounsArtAddress, () => deployment.SVGRenderer.address], libraries: () => ({ NFTDescriptorV2: deployment.NFTDescriptorV2.address, @@ -146,14 +146,14 @@ task('deploy-short-times-dao-v3', 'Deploy all Nouns contracts with short gov tim }, Inflator: {}, NounsArt: { - args: [() => deployment.NounsDescriptorV2.address, () => deployment.Inflator.address], + args: [() => deployment.NounsDescriptorV3.address, () => deployment.Inflator.address], }, NounsSeeder: {}, NounsToken: { args: [ args.noundersdao, expectedAuctionHouseProxyAddress, - () => deployment.NounsDescriptorV2.address, + () => deployment.NounsDescriptorV3.address, () => deployment.NounsSeeder.address, proxyRegistryAddress, ], diff --git a/packages/nouns-contracts/tasks/index.ts b/packages/nouns-contracts/tasks/index.ts index 9ce6237ea9..550eb900b2 100644 --- a/packages/nouns-contracts/tasks/index.ts +++ b/packages/nouns-contracts/tasks/index.ts @@ -2,7 +2,7 @@ export * from './accounts'; export * from './create-proposal'; export * from './populate-descriptor'; export * from './descriptor-art-to-console'; -export * from './deploy-descriptor-v2'; +export * from './deploy-descriptor-v3'; export * from './populate-descriptor-via-proposal'; export * from './deploy-test-token'; export * from './upgrade-descriptor-via-proposal'; diff --git a/packages/nouns-contracts/tasks/populate-descriptor-v3.ts b/packages/nouns-contracts/tasks/populate-descriptor-v3.ts new file mode 100644 index 0000000000..e779a7864a --- /dev/null +++ b/packages/nouns-contracts/tasks/populate-descriptor-v3.ts @@ -0,0 +1,65 @@ +import { task, types } from 'hardhat/config'; +import ImageData from '../files/image-data-v2.json'; +import { dataToDescriptorInput } from './utils'; + +task('populate-descriptor-v3.', 'Populates the descriptor with color palettes and Noun parts') + .addOptionalParam( + 'nftDescriptor', + 'The `NFTDescriptorV2` contract address', + undefined, + types.string, + ) + .addOptionalParam( + 'nounsDescriptor', + 'The `NounsDescriptorV3` contract address', + undefined, + types.string, + ) + .setAction(async ({ nftDescriptor, nounsDescriptor }, { ethers, network }) => { + const options = { gasLimit: network.name === 'hardhat' ? 30000000 : undefined }; + + const descriptorFactory = await ethers.getContractFactory('NounsDescriptorV3', { + libraries: { + NFTDescriptorV2: nftDescriptor, + }, + }); + const descriptorContract = descriptorFactory.attach(nounsDescriptor); + + const { bgcolors, palette, images } = ImageData; + const { bodies, accessories, heads, glasses } = images; + + const bodiesPage = dataToDescriptorInput(bodies.map(({ data }) => data)); + const headsPage = dataToDescriptorInput(heads.map(({ data }) => data)); + const glassesPage = dataToDescriptorInput(glasses.map(({ data }) => data)); + const accessoriesPage = dataToDescriptorInput(accessories.map(({ data }) => data)); + + await descriptorContract.addManyBackgrounds(bgcolors); + await descriptorContract.setPalette(0, `0x000000${palette.join('')}`); + + await descriptorContract.addBodies( + bodiesPage.encodedCompressed, + bodiesPage.originalLength, + bodiesPage.itemCount, + options, + ); + await descriptorContract.addHeads( + headsPage.encodedCompressed, + headsPage.originalLength, + headsPage.itemCount, + options, + ); + await descriptorContract.addGlasses( + glassesPage.encodedCompressed, + glassesPage.originalLength, + glassesPage.itemCount, + options, + ); + await descriptorContract.addAccessories( + accessoriesPage.encodedCompressed, + accessoriesPage.originalLength, + accessoriesPage.itemCount, + options, + ); + + console.log('Descriptor populated with palettes and parts.'); + }); \ No newline at end of file diff --git a/packages/nouns-contracts/tasks/populate-descriptor.ts b/packages/nouns-contracts/tasks/populate-descriptor.ts index f98dc57d86..0dfee51478 100644 --- a/packages/nouns-contracts/tasks/populate-descriptor.ts +++ b/packages/nouns-contracts/tasks/populate-descriptor.ts @@ -11,14 +11,14 @@ task('populate-descriptor', 'Populates the descriptor with color palettes and No ) .addOptionalParam( 'nounsDescriptor', - 'The `NounsDescriptorV2` contract address', + 'The `NounsDescriptorV3` contract address', '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512', types.string, ) .setAction(async ({ nftDescriptor, nounsDescriptor }, { ethers, network }) => { const options = { gasLimit: network.name === 'hardhat' ? 30000000 : undefined }; - const descriptorFactory = await ethers.getContractFactory('NounsDescriptorV2', { + const descriptorFactory = await ethers.getContractFactory('NounsDescriptorV3', { libraries: { NFTDescriptorV2: nftDescriptor, }, diff --git a/packages/nouns-contracts/tasks/run-local-dao-v3.ts b/packages/nouns-contracts/tasks/run-local-dao-v3.ts index f2dde422fb..ac582e264a 100644 --- a/packages/nouns-contracts/tasks/run-local-dao-v3.ts +++ b/packages/nouns-contracts/tasks/run-local-dao-v3.ts @@ -19,7 +19,7 @@ task( await run('populate-descriptor', { nftDescriptor: contracts.NFTDescriptorV2.instance.address, - nounsDescriptor: contracts.NounsDescriptorV2.instance.address, + nounsDescriptor: contracts.NounsDescriptorV3.instance.address, }); await contracts.NounsAuctionHouse.instance @@ -30,7 +30,7 @@ task( // Transfer ownership const executorAddress = contracts.NounsDAOExecutorProxy.instance.address; - await contracts.NounsDescriptorV2.instance.transferOwnership(executorAddress); + await contracts.NounsDescriptorV3.instance.transferOwnership(executorAddress); await contracts.NounsToken.instance.transferOwnership(executorAddress); await contracts.NounsAuctionHouseProxyAdmin.instance.transferOwnership(executorAddress); await contracts.NounsAuctionHouse.instance diff --git a/packages/nouns-contracts/tasks/types/index.ts b/packages/nouns-contracts/tasks/types/index.ts index 7f9a6953a2..6af2201c3f 100644 --- a/packages/nouns-contracts/tasks/types/index.ts +++ b/packages/nouns-contracts/tasks/types/index.ts @@ -11,7 +11,7 @@ export enum ChainId { export type ContractNamesDAOV3 = | 'NFTDescriptorV2' - | 'NounsDescriptorV2' + | 'NounsDescriptorV3' | 'SVGRenderer' | 'NounsArt' | 'Inflator' diff --git a/packages/nouns-contracts/tasks/update-configs-dao-v3.ts b/packages/nouns-contracts/tasks/update-configs-dao-v3.ts index 29e8e52b5a..033f64cd4b 100644 --- a/packages/nouns-contracts/tasks/update-configs-dao-v3.ts +++ b/packages/nouns-contracts/tasks/update-configs-dao-v3.ts @@ -20,7 +20,7 @@ task('update-configs-dao-v3', 'Write the deployed addresses to the SDK and subgr addresses[chainId] = { nounsToken: contracts.NounsToken.address, nounsSeeder: contracts.NounsSeeder.address, - nounsDescriptor: contracts.NounsDescriptorV2.address, + nounsDescriptor: contracts.NounsDescriptorV3.address, nftDescriptor: contracts.NFTDescriptorV2.address, nounsAuctionHouse: contracts.NounsAuctionHouse.address, nounsAuctionHouseProxy: contracts.NounsAuctionHouseProxy.address, diff --git a/packages/nouns-contracts/tasks/upgrade-descriptor-via-proposal.ts b/packages/nouns-contracts/tasks/upgrade-descriptor-via-proposal.ts index cf660de49b..d006ca0ddc 100644 --- a/packages/nouns-contracts/tasks/upgrade-descriptor-via-proposal.ts +++ b/packages/nouns-contracts/tasks/upgrade-descriptor-via-proposal.ts @@ -1,7 +1,7 @@ import { task } from 'hardhat/config'; -task('upgrade-descriptor-via-proposal', 'Upgrade NounsToken to use Descriptor V2.') - .addParam('descriptor', 'The `NounsDescriptorV2` contract address') +task('upgrade-descriptor-via-proposal', 'Upgrade NounsToken to use Descriptor V3.') + .addParam('descriptor', 'The `NounsDescriptorV3` contract address') .addParam('dao', 'The `NounsDAOProxy` contract address') .addParam('token', 'The `NounsToken` contract address') .setAction(async ({ descriptor, dao, token }, { ethers }) => { @@ -16,7 +16,7 @@ task('upgrade-descriptor-via-proposal', 'Upgrade NounsToken to use Descriptor V2 values, signatures, calldatas, - `# Upgrade NounsToken descriptor to V2\nThis proposal calls a function on NounsToken to set its descriptor to V2.`, + `# Upgrade NounsToken descriptor to V3\nThis proposal calls a function on NounsToken to set its descriptor to V3.`, ); await propTx.wait(); diff --git a/packages/nouns-contracts/test/auction.test.ts b/packages/nouns-contracts/test/auction.test.ts index 39af599567..139106b30f 100644 --- a/packages/nouns-contracts/test/auction.test.ts +++ b/packages/nouns-contracts/test/auction.test.ts @@ -6,7 +6,7 @@ import { ethers, upgrades } from 'hardhat'; import { MaliciousBidder__factory as MaliciousBidderFactory, NounsAuctionHouse, - NounsDescriptorV2__factory as NounsDescriptorV2Factory, + NounsDescriptorV3__factory as NounsDescriptorV3Factory, NounsToken, WETH, } from '../typechain'; @@ -51,7 +51,7 @@ describe('NounsAuctionHouse', () => { const descriptor = await nounsToken.descriptor(); - await populateDescriptorV2(NounsDescriptorV2Factory.connect(descriptor, deployer)); + await populateDescriptorV2(NounsDescriptorV3Factory.connect(descriptor, deployer)); await nounsToken.setMinter(nounsAuctionHouse.address); }); diff --git a/packages/nouns-contracts/test/descriptorV2.test.ts b/packages/nouns-contracts/test/descriptorV2.test.ts deleted file mode 100644 index 670cfec24a..0000000000 --- a/packages/nouns-contracts/test/descriptorV2.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import chai from 'chai'; -import { solidity } from 'ethereum-waffle'; -import { NounsDescriptorV2 } from '../typechain'; -import ImageData from '../files/image-data-v2.json'; -import { LongestPart } from './types'; -import { deployNounsDescriptorV2, populateDescriptorV2 } from './utils'; -import { ethers } from 'hardhat'; -import { appendFileSync } from 'fs'; - -chai.use(solidity); -const { expect } = chai; - -describe('NounsDescriptorV2', () => { - let nounsDescriptor: NounsDescriptorV2; - let snapshotId: number; - - const part: LongestPart = { - length: 0, - index: 0, - }; - const longest: Record = { - bodies: part, - accessories: part, - heads: part, - glasses: part, - }; - - before(async () => { - nounsDescriptor = await deployNounsDescriptorV2(); - - for (const [l, layer] of Object.entries(ImageData.images)) { - for (const [i, item] of layer.entries()) { - if (item.data.length > longest[l].length) { - longest[l] = { - length: item.data.length, - index: i, - }; - } - } - } - - await populateDescriptorV2(nounsDescriptor); - }); - - beforeEach(async () => { - snapshotId = await ethers.provider.send('evm_snapshot', []); - }); - - afterEach(async () => { - await ethers.provider.send('evm_revert', [snapshotId]); - }); - - // Unskip this test to validate the encoding of all parts. It ensures that no parts revert when building the token URI. - // This test also outputs a parts.html file, which can be visually inspected. - // Note that this test takes a long time to run. You must increase the mocha timeout to a large number. - it.skip('should generate valid token uri metadata for all supported parts when data uris are enabled', async () => { - console.log('Running... this may take a little while...'); - - const { bgcolors, images } = ImageData; - const { bodies, accessories, heads, glasses } = images; - const max = Math.max(bodies.length, accessories.length, heads.length, glasses.length); - for (let i = 0; i < max; i++) { - const tokenUri = await nounsDescriptor.tokenURI(i, { - background: Math.min(i, bgcolors.length - 1), - body: Math.min(i, bodies.length - 1), - accessory: Math.min(i, accessories.length - 1), - head: Math.min(i, heads.length - 1), - glasses: Math.min(i, glasses.length - 1), - }); - const { name, description, image } = JSON.parse( - Buffer.from(tokenUri.replace('data:application/json;base64,', ''), 'base64').toString( - 'ascii', - ), - ); - expect(name).to.equal(`Noun ${i}`); - expect(description).to.equal(`Noun ${i} is a member of the Nouns DAO`); - expect(image).to.not.be.undefined; - - appendFileSync( - 'parts.html', - Buffer.from(image.split(';base64,').pop(), 'base64').toString('ascii'), - ); - - if (i && i % Math.round(max / 10) === 0) { - console.log(`${Math.round((i / max) * 100)}% complete`); - } - } - }); -}); diff --git a/packages/nouns-contracts/test/foundry/NounsDescriptorV3.t.sol b/packages/nouns-contracts/test/foundry/NounsDescriptorV3.t.sol new file mode 100644 index 0000000000..0dbdf8918f --- /dev/null +++ b/packages/nouns-contracts/test/foundry/NounsDescriptorV3.t.sol @@ -0,0 +1,742 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import 'forge-std/Test.sol'; +import 'forge-std/StdJson.sol'; +import { NounsDescriptorV3 } from '../../contracts/NounsDescriptorV3.sol'; +import { SVGRenderer } from '../../contracts/SVGRenderer.sol'; +import { ISVGRenderer } from '../../contracts/interfaces/ISVGRenderer.sol'; +import { INounsSeeder } from '../../contracts/interfaces/INounsSeeder.sol'; +import { NounsArt } from '../../contracts/NounsArt.sol'; +import { INounsArt } from '../../contracts/interfaces/INounsArt.sol'; +import { Base64 } from 'base64-sol/base64.sol'; +import { Inflator } from '../../contracts/Inflator.sol'; +import { IInflator } from '../../contracts/interfaces/IInflator.sol'; +import { DeployUtils } from './helpers/DeployUtils.sol'; +import { strings } from './lib/strings.sol'; +import { console } from 'forge-std/console.sol'; + +contract NounsDescriptorV3Test is Test { + NounsDescriptorV3 descriptor; + NounsArt art; + SVGRenderer renderer; + + function setUp() public { + renderer = new SVGRenderer(); + descriptor = new NounsDescriptorV3(INounsArt(address(0)), renderer); + art = new NounsArt(address(descriptor), new Inflator()); + descriptor.setArt(art); + } + + function testCannotSetArtIfNotOwner() public { + vm.prank(address(1)); + vm.expectRevert(bytes('Ownable: caller is not the owner')); + descriptor.setArt(INounsArt(address(2))); + } + + function testCannotSetArtIfPartsAreLocked() public { + descriptor.lockParts(); + vm.expectRevert(bytes('Parts are locked')); + descriptor.setArt(INounsArt(address(2))); + } + + function testSetArtWorks() public { + descriptor.setArt(INounsArt(address(2))); + assertEq(address(descriptor.art()), address(2)); + } + + function testCannotSetRendererIfNotOwner() public { + vm.prank(address(1)); + vm.expectRevert(bytes('Ownable: caller is not the owner')); + descriptor.setRenderer(ISVGRenderer(address(2))); + } + + function testSetRendererWorksIfPartsAreLocked() public { + descriptor.lockParts(); + descriptor.setRenderer(ISVGRenderer(address(2))); + assertEq(address(descriptor.renderer()), address(2)); + } + + function testSetRendererWorks() public { + descriptor.setRenderer(ISVGRenderer(address(2))); + assertEq(address(descriptor.renderer()), address(2)); + } + + function testCannotSetArtDescriptorIfNotOwner() public { + vm.prank(address(1)); + vm.expectRevert(bytes('Ownable: caller is not the owner')); + descriptor.setArtDescriptor(address(2)); + } + + function testSetArtDescriptorUsesArt() public { + vm.expectCall(address(art), abi.encodeCall(art.setDescriptor, address(42))); + descriptor.setArtDescriptor(address(42)); + } + + function testCannotSetArtInflatorIfNotOwner() public { + vm.prank(address(1)); + vm.expectRevert(bytes('Ownable: caller is not the owner')); + descriptor.setArtInflator(IInflator(address(2))); + } + + function testSetArtInflatorUsesArt() public { + vm.expectCall(address(art), abi.encodeCall(art.setInflator, IInflator(address(42)))); + descriptor.setArtInflator(IInflator(address(42))); + } + + function testDataURIEnabledByDefault() public { + assertEq(descriptor.isDataURIEnabled(), true); + } + + function testCannotToggleDataURIIfNotOwner() public { + vm.prank(address(1)); + vm.expectRevert(bytes('Ownable: caller is not the owner')); + descriptor.toggleDataURIEnabled(); + } + + function testToggleDataURIWorks() public { + descriptor.setBaseURI('https://nouns.wtf/'); + _makeArtGettersNotRevert(); + vm.mockCall( + address(renderer), + abi.encodeWithSelector(SVGRenderer.generateSVG.selector), + abi.encode('mock svg') + ); + + descriptor.toggleDataURIEnabled(); + assertEq(descriptor.tokenURI(42, INounsSeeder.Seed(0, 0, 0, 0, 0)), 'https://nouns.wtf/42'); + + descriptor.toggleDataURIEnabled(); + assertEq( + descriptor.tokenURI(42, INounsSeeder.Seed(0, 0, 0, 0, 0)), + string( + abi.encodePacked( + 'data:application/json;base64,', + Base64.encode( + bytes( + abi.encodePacked( + '{"name":"', + 'Noun 42', + '", "description":"', + 'Noun 42 is a member of the Nouns DAO', + '", "image": "', + 'data:image/svg+xml;base64,', + Base64.encode(bytes('mock svg')), + '"}' + ) + ) + ) + ) + ) + ); + + vm.clearMockedCalls(); + } + + function testBackgroundCountUsesArt() public { + vm.mockCall(address(art), abi.encodeWithSelector(NounsArt.backgroundCount.selector), abi.encode(42)); + assertEq(descriptor.backgroundCount(), 42); + vm.clearMockedCalls(); + } + + function testBodyCountUsesArt() public { + vm.prank(address(descriptor)); + art.addBodiesFromPointer(address(0), 1, 42); + assertEq(descriptor.bodyCount(), 42); + } + + function testAccessoryCountUsesArt() public { + vm.prank(address(descriptor)); + art.addAccessoriesFromPointer(address(0), 1, 42); + assertEq(descriptor.accessoryCount(), 42); + } + + function testHeadCountUsesArt() public { + vm.prank(address(descriptor)); + art.addHeadsFromPointer(address(0), 1, 42); + assertEq(descriptor.headCount(), 42); + } + + function testGlassesCountUsesArt() public { + vm.prank(address(descriptor)); + art.addGlassesFromPointer(address(0), 1, 42); + assertEq(descriptor.glassesCount(), 42); + } + + function testAddManyBackgroundsUsesArt() public { + string[] memory params = new string[](2); + params[0] = 'ff00ff'; + params[1] = '00ff00'; + + vm.expectCall(address(art), abi.encodeCall(art.addManyBackgrounds, (params))); + descriptor.addManyBackgrounds(params); + } + + function testCannotAddManyBackgroundsWhenPartsLocked() public { + descriptor.lockParts(); + vm.expectRevert(bytes('Parts are locked')); + descriptor.addManyBackgrounds(new string[](0)); + } + + function testAddBackgroundUsesArt() public { + vm.expectCall(address(art), abi.encodeCall(art.addBackground, ('fff000'))); + descriptor.addBackground('fff000'); + } + + function testCannotAddBackgroundWhenPartsLocked() public { + descriptor.lockParts(); + vm.expectRevert(bytes('Parts are locked')); + descriptor.addBackground(''); + } + + function testSetPaletteUsesArt() public { + vm.expectCall(address(art), abi.encodeCall(art.setPalette, (0, '123456'))); + descriptor.setPalette(0, '123456'); + + vm.expectCall(address(art), abi.encodeCall(art.setPalette, (1, '654321'))); + descriptor.setPalette(1, '654321'); + } + + function testSetPalettePointerUsesArt() public { + vm.expectCall(address(art), abi.encodeCall(art.setPalettePointer, (0, address(42)))); + descriptor.setPalettePointer(0, address(42)); + + vm.expectCall(address(art), abi.encodeCall(art.setPalettePointer, (1, address(1337)))); + descriptor.setPalettePointer(1, address(1337)); + } + + function testCannotSetPaletteWhenPartsLocked() public { + descriptor.lockParts(); + vm.expectRevert(bytes('Parts are locked')); + descriptor.setPalette(0, '000000'); + } + + function testCannotSetPalettePointerWhenPartsLocked() public { + descriptor.lockParts(); + vm.expectRevert(bytes('Parts are locked')); + descriptor.setPalettePointer(0, address(42)); + } + + function testCannotSetPalettePointerIfNotOwner() public { + vm.prank(address(1)); + vm.expectRevert(bytes('Ownable: caller is not the owner')); + descriptor.setPalettePointer(0, address(42)); + } + + function testAddBodiesUsesArt() public { + bytes memory someBytes = 'some bytes'; + uint80 decompressedLen = 123; + uint16 imageCount = 456; + vm.expectCall(address(art), abi.encodeCall(art.addBodies, (someBytes, decompressedLen, imageCount))); + descriptor.addBodies(someBytes, decompressedLen, imageCount); + } + + function testCannotAddBodiesWhenPartsLocked() public { + descriptor.lockParts(); + vm.expectRevert(bytes('Parts are locked')); + descriptor.addBodies('00', 1, 1); + } + + function testCannotAddBodiesIfNotOwner() public { + vm.prank(address(1)); + vm.expectRevert(bytes('Ownable: caller is not the owner')); + descriptor.addBodies('00', 1, 1); + } + + function testAddAccessoriesUsesArt() public { + bytes memory someBytes = 'some bytes'; + uint80 decompressedLen = 123; + uint16 imageCount = 456; + vm.expectCall(address(art), abi.encodeCall(art.addAccessories, (someBytes, decompressedLen, imageCount))); + descriptor.addAccessories(someBytes, decompressedLen, imageCount); + } + + function testCannotAddAccessoriesWhenPartsLocked() public { + descriptor.lockParts(); + vm.expectRevert(bytes('Parts are locked')); + descriptor.addAccessories('00', 1, 1); + } + + function testCannotAddAccessoriesIfNotOwner() public { + vm.prank(address(1)); + vm.expectRevert(bytes('Ownable: caller is not the owner')); + descriptor.addAccessories('00', 1, 1); + } + + function testAddHeadsUsesArt() public { + bytes memory someBytes = 'some bytes'; + uint80 decompressedLen = 123; + uint16 imageCount = 456; + vm.expectCall(address(art), abi.encodeCall(art.addHeads, (someBytes, decompressedLen, imageCount))); + descriptor.addHeads(someBytes, decompressedLen, imageCount); + } + + function testCannotAddHeadsWhenPartsLocked() public { + descriptor.lockParts(); + vm.expectRevert(bytes('Parts are locked')); + descriptor.addHeads('00', 1, 1); + } + + function testCannotAddHeadsIfNotOwner() public { + vm.prank(address(1)); + vm.expectRevert(bytes('Ownable: caller is not the owner')); + descriptor.addHeads('00', 1, 1); + } + + function testAddGlassesUsesArt() public { + bytes memory someBytes = 'some bytes'; + uint80 decompressedLen = 123; + uint16 imageCount = 456; + vm.expectCall(address(art), abi.encodeCall(art.addGlasses, (someBytes, decompressedLen, imageCount))); + descriptor.addGlasses(someBytes, decompressedLen, imageCount); + } + + function testCannotAddGlassesWhenPartsLocked() public { + descriptor.lockParts(); + vm.expectRevert(bytes('Parts are locked')); + descriptor.addGlasses('00', 1, 1); + } + + function testCannotAddGlassesIfNotOwner() public { + vm.prank(address(1)); + vm.expectRevert(bytes('Ownable: caller is not the owner')); + descriptor.addGlasses('00', 1, 1); + } + + function testAddBodiesFromPointerUsesArt() public { + address somePointer = address(1337); + uint80 decompressedLen = 123; + uint16 imageCount = 456; + vm.expectCall( + address(art), + abi.encodeCall(art.addBodiesFromPointer, (somePointer, decompressedLen, imageCount)) + ); + descriptor.addBodiesFromPointer(somePointer, decompressedLen, imageCount); + } + + function testCannotAddBodiesFromPointerWhenPartsLocked() public { + descriptor.lockParts(); + vm.expectRevert(bytes('Parts are locked')); + descriptor.addBodiesFromPointer(address(1337), 1, 1); + } + + function testCannotAddBodiesFromPointerIfNotOwner() public { + vm.prank(address(1)); + vm.expectRevert(bytes('Ownable: caller is not the owner')); + descriptor.addBodiesFromPointer(address(1337), 1, 1); + } + + function testAddAccessoriesFromPointerUsesArt() public { + address somePointer = address(1337); + uint80 decompressedLen = 123; + uint16 imageCount = 456; + vm.expectCall( + address(art), + abi.encodeCall(art.addAccessoriesFromPointer, (somePointer, decompressedLen, imageCount)) + ); + descriptor.addAccessoriesFromPointer(somePointer, decompressedLen, imageCount); + } + + function testCannotAddAccessoriesFromPointerWhenPartsLocked() public { + descriptor.lockParts(); + vm.expectRevert(bytes('Parts are locked')); + descriptor.addAccessoriesFromPointer(address(1337), 1, 1); + } + + function testCannotAddAccessoriesFromPointerIfNotOwner() public { + vm.prank(address(1)); + vm.expectRevert(bytes('Ownable: caller is not the owner')); + descriptor.addAccessoriesFromPointer(address(1337), 1, 1); + } + + function testAddHeadsFromPointerUsesArt() public { + address somePointer = address(1337); + uint80 decompressedLen = 123; + uint16 imageCount = 456; + vm.expectCall( + address(art), + abi.encodeCall(art.addHeadsFromPointer, (somePointer, decompressedLen, imageCount)) + ); + descriptor.addHeadsFromPointer(somePointer, decompressedLen, imageCount); + } + + function testCannotAddHeadsFromPointerWhenPartsLocked() public { + descriptor.lockParts(); + vm.expectRevert(bytes('Parts are locked')); + descriptor.addHeadsFromPointer(address(1337), 1, 1); + } + + function testCannotAddHeadsFromPointerIfNotOwner() public { + vm.prank(address(1)); + vm.expectRevert(bytes('Ownable: caller is not the owner')); + descriptor.addHeadsFromPointer(address(1337), 1, 1); + } + + function testAddGlassesFromPointerUsesArt() public { + address somePointer = address(1337); + uint80 decompressedLen = 123; + uint16 imageCount = 456; + vm.expectCall( + address(art), + abi.encodeCall(art.addGlassesFromPointer, (somePointer, decompressedLen, imageCount)) + ); + descriptor.addGlassesFromPointer(somePointer, decompressedLen, imageCount); + } + + function testCannotAddGlassesFromPointerWhenPartsLocked() public { + descriptor.lockParts(); + vm.expectRevert(bytes('Parts are locked')); + descriptor.addGlassesFromPointer(address(1337), 1, 1); + } + + function testCannotAddGlassesFromPointerIfNotOwner() public { + vm.prank(address(1)); + vm.expectRevert(bytes('Ownable: caller is not the owner')); + descriptor.addGlassesFromPointer(address(1337), 1, 1); + } + + function testUpdateBodiesUsesArt() public { + bytes memory someBytes = 'some bytes'; + uint80 decompressedLen = 123; + uint16 imageCount = 456; + + testAddBodiesUsesArt(); + + vm.expectCall(address(art), abi.encodeCall(art.updateBodies, (someBytes, decompressedLen, imageCount))); + descriptor.updateBodies(someBytes, decompressedLen, imageCount); + } + + function testCannotUpdateBodiesWhenPartsLocked() public { + descriptor.lockParts(); + vm.expectRevert(bytes('Parts are locked')); + descriptor.updateBodies('00', 1, 1); + } + + function testCannotUpdateBodiesIfNotOwner() public { + vm.prank(address(1)); + vm.expectRevert(bytes('Ownable: caller is not the owner')); + descriptor.updateBodies('00', 1, 1); + } + + function testUpdateAccessoriesUsesArt() public { + bytes memory someBytes = 'some bytes'; + uint80 decompressedLen = 123; + uint16 imageCount = 456; + + testAddAccessoriesUsesArt(); + + vm.expectCall(address(art), abi.encodeCall(art.updateAccessories, (someBytes, decompressedLen, imageCount))); + descriptor.updateAccessories(someBytes, decompressedLen, imageCount); + } + + function testCannotUpdateAccessoriesWhenPartsLocked() public { + descriptor.lockParts(); + vm.expectRevert(bytes('Parts are locked')); + descriptor.updateAccessories('00', 1, 1); + } + + function testCannotUpdateAccessoriesIfNotOwner() public { + vm.prank(address(1)); + vm.expectRevert(bytes('Ownable: caller is not the owner')); + descriptor.updateAccessories('00', 1, 1); + } + + function testUpdateHeadsUsesArt() public { + bytes memory someBytes = 'some bytes'; + uint80 decompressedLen = 123; + uint16 imageCount = 456; + + testAddHeadsUsesArt(); + + vm.expectCall(address(art), abi.encodeCall(art.updateHeads, (someBytes, decompressedLen, imageCount))); + descriptor.updateHeads(someBytes, decompressedLen, imageCount); + } + + function testCannotUpdateHeadsWhenPartsLocked() public { + descriptor.lockParts(); + vm.expectRevert(bytes('Parts are locked')); + descriptor.updateHeads('00', 1, 1); + } + + function testCannotUpdateHeadsIfNotOwner() public { + vm.prank(address(1)); + vm.expectRevert(bytes('Ownable: caller is not the owner')); + descriptor.updateHeads('00', 1, 1); + } + + function testUpdateGlassesUsesArt() public { + bytes memory someBytes = 'some bytes'; + uint80 decompressedLen = 123; + uint16 imageCount = 456; + + testAddGlassesUsesArt(); + + vm.expectCall(address(art), abi.encodeCall(art.updateGlasses, (someBytes, decompressedLen, imageCount))); + descriptor.updateGlasses(someBytes, decompressedLen, imageCount); + } + + function testCannotUpdateGlassesWhenPartsLocked() public { + descriptor.lockParts(); + vm.expectRevert(bytes('Parts are locked')); + descriptor.updateGlasses('00', 1, 1); + } + + function testCannotUpdateGlassesIfNotOwner() public { + vm.prank(address(1)); + vm.expectRevert(bytes('Ownable: caller is not the owner')); + descriptor.updateGlasses('00', 1, 1); + } + + function testUpdateBodiesFromPointerUsesArt() public { + address somePointer = address(1337); + uint80 decompressedLen = 123; + uint16 imageCount = 456; + + testAddBodiesFromPointerUsesArt(); + + vm.expectCall( + address(art), + abi.encodeCall(art.updateBodiesFromPointer, (somePointer, decompressedLen, imageCount)) + ); + descriptor.updateBodiesFromPointer(somePointer, decompressedLen, imageCount); + } + + function testCannotUpdateBodiesFromPointerWhenPartsLocked() public { + descriptor.lockParts(); + vm.expectRevert(bytes('Parts are locked')); + descriptor.updateBodiesFromPointer(address(1337), 1, 1); + } + + function testCannotUpdateBodiesFromPointerIfNotOwner() public { + vm.prank(address(1)); + vm.expectRevert(bytes('Ownable: caller is not the owner')); + descriptor.updateBodiesFromPointer(address(1337), 1, 1); + } + + function testUpdateAccessoriesFromPointerUsesArt() public { + address somePointer = address(1337); + uint80 decompressedLen = 123; + uint16 imageCount = 456; + + testAddAccessoriesFromPointerUsesArt(); + + vm.expectCall( + address(art), + abi.encodeCall(art.updateAccessoriesFromPointer, (somePointer, decompressedLen, imageCount)) + ); + descriptor.updateAccessoriesFromPointer(somePointer, decompressedLen, imageCount); + } + + function testCannotUpdateAccessoriesFromPointerWhenPartsLocked() public { + descriptor.lockParts(); + vm.expectRevert(bytes('Parts are locked')); + descriptor.updateAccessoriesFromPointer(address(1337), 1, 1); + } + + function testCannotUpdateAccessoriesFromPointerIfNotOwner() public { + vm.prank(address(1)); + vm.expectRevert(bytes('Ownable: caller is not the owner')); + descriptor.updateAccessoriesFromPointer(address(1337), 1, 1); + } + + function testUpdateHeadsFromPointerUsesArt() public { + address somePointer = address(1337); + uint80 decompressedLen = 123; + uint16 imageCount = 456; + + testAddHeadsFromPointerUsesArt(); + + vm.expectCall( + address(art), + abi.encodeCall(art.updateHeadsFromPointer, (somePointer, decompressedLen, imageCount)) + ); + descriptor.updateHeadsFromPointer(somePointer, decompressedLen, imageCount); + } + + function testCannotUpdateHeadsFromPointerWhenPartsLocked() public { + descriptor.lockParts(); + vm.expectRevert(bytes('Parts are locked')); + descriptor.updateHeadsFromPointer(address(1337), 1, 1); + } + + function testCannotUpdateHeadsFromPointerIfNotOwner() public { + vm.prank(address(1)); + vm.expectRevert(bytes('Ownable: caller is not the owner')); + descriptor.updateHeadsFromPointer(address(1337), 1, 1); + } + + function testUpdateGlassesFromPointerUsesArt() public { + address somePointer = address(1337); + uint80 decompressedLen = 123; + uint16 imageCount = 456; + + testAddGlassesFromPointerUsesArt(); + + vm.expectCall( + address(art), + abi.encodeCall(art.updateGlassesFromPointer, (somePointer, decompressedLen, imageCount)) + ); + descriptor.updateGlassesFromPointer(somePointer, decompressedLen, imageCount); + } + + function testCannotUpdateGlassesFromPointerWhenPartsLocked() public { + descriptor.lockParts(); + vm.expectRevert(bytes('Parts are locked')); + descriptor.updateGlassesFromPointer(address(1337), 1, 1); + } + + function testCannotUpdateGlassesFromPointerIfNotOwner() public { + vm.prank(address(1)); + vm.expectRevert(bytes('Ownable: caller is not the owner')); + descriptor.updateGlassesFromPointer(address(1337), 1, 1); + } + + function testBackgroundsUsesArt() public { + vm.mockCall( + address(art), + abi.encodeWithSelector(INounsArt.backgrounds.selector, 17), + abi.encode('return value') + ); + assertEq(descriptor.backgrounds(17), 'return value'); + vm.clearMockedCalls(); + } + + function testHeadsUsesArt() public { + vm.mockCall(address(art), abi.encodeWithSelector(INounsArt.heads.selector, 17), abi.encode('return value')); + assertEq(descriptor.heads(17), 'return value'); + vm.clearMockedCalls(); + } + + function testBodiesUsesArt() public { + vm.mockCall(address(art), abi.encodeWithSelector(INounsArt.bodies.selector, 17), abi.encode('return value')); + assertEq(descriptor.bodies(17), 'return value'); + vm.clearMockedCalls(); + } + + function testAccessoriesUsesArt() public { + vm.mockCall( + address(art), + abi.encodeWithSelector(INounsArt.accessories.selector, 17), + abi.encode('return value') + ); + assertEq(descriptor.accessories(17), 'return value'); + vm.clearMockedCalls(); + } + + function testGlassesUsesArt() public { + vm.mockCall(address(art), abi.encodeWithSelector(INounsArt.glasses.selector, 17), abi.encode('return value')); + assertEq(descriptor.glasses(17), 'return value'); + vm.clearMockedCalls(); + } + + function testPalettesUsesArt() public { + vm.mockCall(address(art), abi.encodeWithSelector(INounsArt.palettes.selector, 17), abi.encode('return value')); + assertEq(descriptor.palettes(17), 'return value'); + vm.clearMockedCalls(); + } + + function testGetPartsForSeedWorks() public { + vm.mockCall(address(art), abi.encodeWithSelector(INounsArt.bodies.selector), abi.encode('the body')); + vm.mockCall(address(art), abi.encodeWithSelector(INounsArt.accessories.selector), abi.encode('the accessory')); + vm.mockCall(address(art), abi.encodeWithSelector(INounsArt.heads.selector), abi.encode('the head')); + vm.mockCall(address(art), abi.encodeWithSelector(INounsArt.glasses.selector), abi.encode('the glasses')); + vm.mockCall(address(art), abi.encodeWithSelector(INounsArt.palettes.selector), abi.encode('the palette')); + + ISVGRenderer.Part[] memory parts = descriptor.getPartsForSeed(INounsSeeder.Seed(0, 0, 0, 0, 0)); + + assertEq(parts[0].image, 'the body'); + assertEq(parts[0].palette, 'the palette'); + + assertEq(parts[1].image, 'the accessory'); + assertEq(parts[1].palette, 'the palette'); + + assertEq(parts[2].image, 'the head'); + assertEq(parts[2].palette, 'the palette'); + + assertEq(parts[3].image, 'the glasses'); + assertEq(parts[3].palette, 'the palette'); + } + + function _makeArtGettersNotRevert() internal { + vm.mockCall(address(art), abi.encodeWithSelector(INounsArt.backgroundCount.selector), abi.encode(123)); + vm.mockCall(address(art), abi.encodeWithSelector(INounsArt.backgrounds.selector), abi.encode('return value')); + vm.mockCall(address(art), abi.encodeWithSelector(INounsArt.bodies.selector), abi.encode('return value')); + vm.mockCall(address(art), abi.encodeWithSelector(INounsArt.accessories.selector), abi.encode('return value')); + vm.mockCall(address(art), abi.encodeWithSelector(INounsArt.heads.selector), abi.encode('return value')); + vm.mockCall(address(art), abi.encodeWithSelector(INounsArt.glasses.selector), abi.encode('return value')); + vm.mockCall(address(art), abi.encodeWithSelector(INounsArt.palettes.selector), abi.encode('return value')); + } +} + +contract NounsDescriptorV3WithRealArtTest is DeployUtils { + using strings for *; + using stdJson for string; + using Base64 for string; + + NounsDescriptorV3 descriptor; + + uint256 glassesIndex = 1; + + function setUp() public { + descriptor = _deployAndPopulateV3(); + } + + function testGeneratesValidTokenURI() public { + string memory uri = descriptor.tokenURI( + 0, + INounsSeeder.Seed({ background: 0, body: 0, accessory: 0, head: 0, glasses: 0 }) + ); + + string memory json = string(removeDataTypePrefix(uri).decode()); + string memory imageDecoded = string(removeDataTypePrefix(json.readString('.image')).decode()); + strings.slice memory imageSlice = imageDecoded.toSlice(); + + assertEq(json.readString('.name'), 'Noun 0'); + assertEq(json.readString('.description'), 'Noun 0 is a member of the Nouns DAO'); + assertEq(bytes(imageDecoded).length, 6849); + assertTrue( + imageSlice.startsWith( + '' + .toSlice() + ) + ); + assertTrue( + imageSlice.endsWith( + '' + .toSlice() + ) + ); + } + + function testUpdateGlasssWorks() public { + // Expected 'hiprose' glasses data after update + bytes + memory expectedGlasses = hex'000b1710070300062101000621030001210202022401210100012102020224052102020224032102020224052102020224032102020224022102000121020202240121010001210202022401210300062101000621'; + + // Store the initial glasses data at index 1 + bytes memory initialGlasses = descriptor.glasses(glassesIndex); + + // Prepare the new glasses trait data + bytes + memory newEncodedCompressedTrait = hex'cd55416e1a4110dcee61bcded8899002f209c9488b2c7ec0253772e4e80b51a4f890e40b1cf3007ec025fc8423af40794aca356a9230a36d6f7209d2d6767575570debb5a8aacecf7db75cdd75cbfaa35b0f9f1cddd91f38fb03673f3afbd1d9bf72f6af0edd7aedecd74efeb5b37fedec37ce7ee3ecbffad6addf74cbd58db37febecdf3afbafef1dfd7bb7fec6d9af1eab9bbb611daaaba9e00a954c55b515b0544562c86a9d6a362b53f37142ffa3fcf796df62af0d5568e5339c787fdb22a3952fe01f703fd7483fcfa7fbcbf32e3fab8b7c695503b3546b5c11d780e7f8d5bfe4e11fcef1d1f297d85f227fc9270996aa480c445d6aa6977ab234bf1ef935e671979a0e60a98ac440d45a33bdd493b35f8ffc03e60fc83fd0012c559118887ad04c2ff5e4607e7efebb8bbfffa0157d7ed79eefd7b8d0577bdfeabf78ef9ecc7f88f92dbedf90270493ed7335d85a47c981d94ca9275bf3f4cfd0f5393fff23bc8e38df91ee60a98ac440d4a3667aa92747f3eb91bfc0fc02f90b3a80a52a120351179ae9a59e2cccaf47fe09f327e49fe80096aa480c443d69a6977a7232bf1ef90de61be43774004b552406a2369ae9a59e34e6d7237f8cf931f2c774004b552406a28e35d34b3d199b9f9ffff4fbefcfee8fdf9f1dff3f76d65172603653eac9ce3cfd33bce8f9cce035c3f96674074b552406a2ce34d34b3d99995f8ffc35e6d7c85feb573880a52a120351d79ae9a59eaccdaf47fe04f313e44ff80dc052158981a813cdf4524f26e6d7237f8ff93df2f774004b552406a2ee35d34b3dd99b9f9f7f7e3f1f303f42fe031dc064c4f773641d25076633a59e8cccd33fc38b9ecf1c5e739c6f4e77b054456220ea5c33bdd493b9f9f5c8df607e83fc0d1dc052158981a81bcdf4524f36e6d7237f85f915f25774004b552406a2ae34d34b3d59991f027e02'; + uint80 decompressedLength = 3808; + uint16 itemCount = 21; + + // Update glasses at index 1 + descriptor.updateGlasses(newEncodedCompressedTrait, decompressedLength, itemCount); + + // Check if glasses count remains the same after update + uint256 glassesCount = descriptor.glassesCount(); + assertEq(glassesCount, 21, 'Glasses count should remain unchanged after update'); + + // Verify that glasses at index 1 have been updated correctly: to expectedGlasses 'hiprose' + assertEq(descriptor.glasses(glassesIndex), expectedGlasses, 'Glasses at index 1 should match expected data'); + + // Verify that glasses at index 1 have been updated correctly: to not be equal to initialGlasses + assertFalse( + keccak256(abi.encodePacked(descriptor.glasses(glassesIndex))) == + keccak256(abi.encodePacked(initialGlasses)), + 'Updated glasses should differ from initial glasses' + ); + } +} diff --git a/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol b/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol index 1544da9773..b13c75b75f 100644 --- a/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol +++ b/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol @@ -5,6 +5,7 @@ import 'forge-std/Test.sol'; import { INounsDAOLogic } from '../../../contracts/interfaces/INounsDAOLogic.sol'; import { DescriptorHelpers } from './DescriptorHelpers.sol'; import { NounsDescriptorV2 } from '../../../contracts/NounsDescriptorV2.sol'; +import { NounsDescriptorV3 } from '../../../contracts/NounsDescriptorV3.sol'; import { SVGRenderer } from '../../../contracts/SVGRenderer.sol'; import { NounsArt } from '../../../contracts/NounsArt.sol'; import { NounsDAOExecutor } from '../../../contracts/governance/NounsDAOExecutor.sol'; @@ -90,9 +91,24 @@ abstract contract DeployUtils is Test, DescriptorHelpers { return descriptorV2; } + function _deployAndPopulateV3() internal returns (NounsDescriptorV3) { + NounsDescriptorV3 descriptorV3 = _deployDescriptorV3(); + _populateDescriptorV3(descriptorV3); + return descriptorV3; + } + + function _deployDescriptorV3() internal returns (NounsDescriptorV3) { + SVGRenderer renderer = new SVGRenderer(); + Inflator inflator = new Inflator(); + NounsDescriptorV3 descriptorV3 = new NounsDescriptorV3(NounsArt(address(0)), renderer); + NounsArt art = new NounsArt(address(descriptorV3), inflator); + descriptorV3.setArt(art); + return descriptorV3; + } + function deployToken(address noundersDAO, address minter) internal returns (NounsToken nounsToken) { IProxyRegistry proxyRegistry = IProxyRegistry(address(3)); - NounsDescriptorV2 descriptor = _deployAndPopulateV2(); + NounsDescriptorV3 descriptor = _deployAndPopulateV3(); nounsToken = new NounsToken(noundersDAO, minter, descriptor, new NounsSeeder(), proxyRegistry); } @@ -101,4 +117,4 @@ abstract contract DeployUtils is Test, DescriptorHelpers { bytes32 slot = bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1); return address(uint160(uint256(vm.load(proxy, slot)))); } -} +} \ No newline at end of file diff --git a/packages/nouns-contracts/test/foundry/helpers/DescriptorHelpers.sol b/packages/nouns-contracts/test/foundry/helpers/DescriptorHelpers.sol index fe645ad4eb..815ded2f4d 100644 --- a/packages/nouns-contracts/test/foundry/helpers/DescriptorHelpers.sol +++ b/packages/nouns-contracts/test/foundry/helpers/DescriptorHelpers.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.19; import 'forge-std/Test.sol'; import { NounsDescriptor } from '../../../contracts/NounsDescriptor.sol'; import { NounsDescriptorV2 } from '../../../contracts/NounsDescriptorV2.sol'; +import { NounsDescriptorV3 } from '../../../contracts/NounsDescriptorV3.sol'; import { Constants } from './Constants.sol'; import { strings } from '../lib/strings.sol'; @@ -66,6 +67,40 @@ abstract contract DescriptorHelpers is Test, Constants { descriptor.addGlasses(glasses, glassesLength, glassesCount); } + function _populateDescriptorV3(NounsDescriptorV3 descriptor) internal { + // created with `npx hardhat descriptor-art-to-console` + (bytes memory palette, string[] memory backgrounds) = abi.decode( + readFile('./test/foundry/files/descriptor_v2/paletteAndBackgrounds.abi'), + (bytes, string[]) + ); + descriptor.setPalette(0, palette); + descriptor.addManyBackgrounds(backgrounds); + + (bytes memory bodies, uint80 bodiesLength, uint16 bodiesCount) = abi.decode( + readFile('./test/foundry/files/descriptor_v2/bodiesPage.abi'), + (bytes, uint80, uint16) + ); + descriptor.addBodies(bodies, bodiesLength, bodiesCount); + + (bytes memory heads, uint80 headsLength, uint16 headsCount) = abi.decode( + readFile('./test/foundry/files/descriptor_v2/headsPage.abi'), + (bytes, uint80, uint16) + ); + descriptor.addHeads(heads, headsLength, headsCount); + + (bytes memory accessories, uint80 accessoriesLength, uint16 accessoriesCount) = abi.decode( + readFile('./test/foundry/files/descriptor_v2/accessoriesPage.abi'), + (bytes, uint80, uint16) + ); + descriptor.addAccessories(accessories, accessoriesLength, accessoriesCount); + + (bytes memory glasses, uint80 glassesLength, uint16 glassesCount) = abi.decode( + readFile('./test/foundry/files/descriptor_v2/glassesPage.abi'), + (bytes, uint80, uint16) + ); + descriptor.addGlasses(glasses, glassesLength, glassesCount); + } + function readFile(string memory filepath) internal returns (bytes memory output) { string[] memory inputs = new string[](2); inputs[0] = 'cat'; @@ -92,4 +127,4 @@ abstract contract DescriptorHelpers is Test, Constants { strSlice.split(string(',').toSlice()); return strSlice.toString(); } -} +} \ No newline at end of file diff --git a/packages/nouns-contracts/test/foundry/helpers/NounsDAOLogicSharedBase.t.sol b/packages/nouns-contracts/test/foundry/helpers/NounsDAOLogicSharedBase.t.sol index c5e672fbd9..48e638fbd6 100644 --- a/packages/nouns-contracts/test/foundry/helpers/NounsDAOLogicSharedBase.t.sol +++ b/packages/nouns-contracts/test/foundry/helpers/NounsDAOLogicSharedBase.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.15; import 'forge-std/Test.sol'; import { INounsDAOLogic } from '../../../contracts/interfaces/INounsDAOLogic.sol'; -import { NounsDescriptorV2 } from '../../../contracts/NounsDescriptorV2.sol'; +import { NounsDescriptorV3 } from '../../../contracts/NounsDescriptorV3.sol'; import { DeployUtilsFork } from './DeployUtilsFork.sol'; import { NounsToken } from '../../../contracts/NounsToken.sol'; import { NounsSeeder } from '../../../contracts/NounsSeeder.sol'; @@ -31,7 +31,7 @@ abstract contract NounsDAOLogicSharedBaseTest is Test, DeployUtilsFork { Utils utils; function setUp() public virtual { - NounsDescriptorV2 descriptor = _deployAndPopulateV2(); + NounsDescriptorV3 descriptor = _deployAndPopulateV3(); nounsToken = new NounsToken(noundersDAO, minter, descriptor, new NounsSeeder(), IProxyRegistry(address(0))); daoProxy = deployDAOProxy(address(timelock), address(nounsToken), vetoer); @@ -124,4 +124,4 @@ abstract contract NounsDAOLogicSharedBaseTest is Test, DeployUtilsFork { return INounsDAOLogic(daoAddress); } -} +} \ No newline at end of file diff --git a/packages/nouns-contracts/test/governance/castVote.test.ts b/packages/nouns-contracts/test/governance/castVote.test.ts index 31af03af66..fa908c71de 100644 --- a/packages/nouns-contracts/test/governance/castVote.test.ts +++ b/packages/nouns-contracts/test/governance/castVote.test.ts @@ -20,7 +20,7 @@ import { mineBlock } from '../utils'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { NounsToken, - NounsDescriptorV2__factory as NounsDescriptorV2Factory, + NounsDescriptorV3__factory as NounsDescriptorV3Factory, NounsDAOLogicV4, } from '../../typechain'; @@ -48,7 +48,7 @@ async function reset() { token = await deployNounsToken(signers.deployer); await populateDescriptorV2( - NounsDescriptorV2Factory.connect(await token.descriptor(), signers.deployer), + NounsDescriptorV3Factory.connect(await token.descriptor(), signers.deployer), ); await setTotalSupply(token, 10); diff --git a/packages/nouns-contracts/test/governance/nounsGovernance.test.ts b/packages/nouns-contracts/test/governance/nounsGovernance.test.ts index d571c76724..47af56d86c 100644 --- a/packages/nouns-contracts/test/governance/nounsGovernance.test.ts +++ b/packages/nouns-contracts/test/governance/nounsGovernance.test.ts @@ -3,7 +3,7 @@ import { solidity } from 'ethereum-waffle'; import { ethers } from 'hardhat'; import { NounsToken, - NounsDescriptorV2__factory as NounsDescriptorV2Factory, + NounsDescriptorV3__factory as NounsDescriptorV3Factory, } from '../../typechain'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { @@ -63,7 +63,7 @@ describe('Nouns Governance', () => { token = await deployNounsToken(signers.deployer); await populateDescriptorV2( - NounsDescriptorV2Factory.connect(await token.descriptor(), signers.deployer), + NounsDescriptorV3Factory.connect(await token.descriptor(), signers.deployer), ); domain = Domain('Nouns', token.address, await chainId()); diff --git a/packages/nouns-contracts/test/governance/proxy.test.ts b/packages/nouns-contracts/test/governance/proxy.test.ts index 0be87d3b92..1c28f46f24 100644 --- a/packages/nouns-contracts/test/governance/proxy.test.ts +++ b/packages/nouns-contracts/test/governance/proxy.test.ts @@ -13,7 +13,7 @@ import { import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { NounsToken, - NounsDescriptorV2__factory as NounsDescriptorV2Factory, + NounsDescriptorV3__factory as NounsDescriptorV3Factory, NounsDAOLogicV4, } from '../../typechain'; import { MAX_QUORUM_VOTES_BPS, MIN_QUORUM_VOTES_BPS } from '../constants'; @@ -30,7 +30,7 @@ async function setup() { token = await deployNounsToken(signers.deployer); await populateDescriptorV2( - NounsDescriptorV2Factory.connect(await token.descriptor(), signers.deployer), + NounsDescriptorV3Factory.connect(await token.descriptor(), signers.deployer), ); await setTotalSupply(token, 100); @@ -71,4 +71,4 @@ describe('NounsDAOProxyV3', () => { const params = await gov.getDynamicQuorumParamsAt(await blockNumber()); expect(params.quorumCoefficient).to.equal(3); }); -}); +}); \ No newline at end of file diff --git a/packages/nouns-contracts/test/governance/quorumConfig.test.ts b/packages/nouns-contracts/test/governance/quorumConfig.test.ts index 5ab7f05e1f..eb3a5b28e2 100644 --- a/packages/nouns-contracts/test/governance/quorumConfig.test.ts +++ b/packages/nouns-contracts/test/governance/quorumConfig.test.ts @@ -14,7 +14,7 @@ import { import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { NounsToken, - NounsDescriptorV2__factory as NounsDescriptorV2Factory, + NounsDescriptorV2__factory as NounsDescriptorV3Factory, INounsDAOLogic, NounsDAOLogicV4__factory, } from '../../typechain'; @@ -38,7 +38,7 @@ async function setup() { token = await deployNounsToken(signers.deployer); await populateDescriptorV2( - NounsDescriptorV2Factory.connect(await token.descriptor(), signers.deployer), + NounsDescriptorV3Factory.connect(await token.descriptor(), signers.deployer), ); await setTotalSupply(token, 100); diff --git a/packages/nouns-contracts/test/governance/voteRefund.test.ts b/packages/nouns-contracts/test/governance/voteRefund.test.ts index 72eee72538..30ebc5af86 100644 --- a/packages/nouns-contracts/test/governance/voteRefund.test.ts +++ b/packages/nouns-contracts/test/governance/voteRefund.test.ts @@ -6,7 +6,7 @@ import { ethers } from 'hardhat'; import { NounsDAOLogicV4__factory, NounsDAOLogicV4, - NounsDescriptorV2__factory, + NounsDescriptorV3__factory, NounsToken, Voter__factory, INounsDAOLogic, @@ -52,7 +52,7 @@ describe('V3 Vote Refund', () => { user2 = signers.account1; token = await deployNounsToken(deployer); - const descriptor = NounsDescriptorV2__factory.connect(await token.descriptor(), deployer); + const descriptor = NounsDescriptorV3__factory.connect(await token.descriptor(), deployer); await populateDescriptorV2(descriptor); await token.connect(deployer).mint(); diff --git a/packages/nouns-contracts/test/nouns.test.ts b/packages/nouns-contracts/test/nouns.test.ts index 07424572e2..bd197e2a46 100644 --- a/packages/nouns-contracts/test/nouns.test.ts +++ b/packages/nouns-contracts/test/nouns.test.ts @@ -2,7 +2,7 @@ import chai from 'chai'; import { ethers } from 'hardhat'; import { BigNumber as EthersBN, constants } from 'ethers'; import { solidity } from 'ethereum-waffle'; -import { NounsDescriptorV2__factory as NounsDescriptorV2Factory, NounsToken } from '../typechain'; +import { NounsDescriptorV3__factory as NounsDescriptorV3Factory, NounsToken } from '../typechain'; import { deployNounsToken, populateDescriptorV2 } from './utils'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; @@ -21,7 +21,7 @@ describe('NounsToken', () => { const descriptor = await nounsToken.descriptor(); - await populateDescriptorV2(NounsDescriptorV2Factory.connect(descriptor, deployer)); + await populateDescriptorV2(NounsDescriptorV3Factory.connect(descriptor, deployer)); }); beforeEach(async () => { diff --git a/packages/nouns-contracts/test/utils.ts b/packages/nouns-contracts/test/utils.ts index 06cd632e76..3df1f134ca 100644 --- a/packages/nouns-contracts/test/utils.ts +++ b/packages/nouns-contracts/test/utils.ts @@ -5,6 +5,8 @@ import { NounsDescriptor__factory as NounsDescriptorFactory, NounsDescriptorV2, NounsDescriptorV2__factory as NounsDescriptorV2Factory, + NounsDescriptorV3, + NounsDescriptorV3__factory as NounsDescriptorV3Factory, NounsToken, NounsToken__factory as NounsTokenFactory, NounsSeeder, @@ -101,6 +103,33 @@ export const deployNounsDescriptorV2 = async ( return descriptor; }; +export const deployNounsDescriptorV3 = async ( + deployer?: SignerWithAddress, +): Promise => { + const signer = deployer || (await getSigners()).deployer; + const nftDescriptorLibraryFactory = await ethers.getContractFactory('NFTDescriptorV2', signer); + const nftDescriptorLibrary = await nftDescriptorLibraryFactory.deploy(); + const nounsDescriptorFactory = new NounsDescriptorV3Factory( + { + 'contracts/libs/NFTDescriptorV2.sol:NFTDescriptorV2': nftDescriptorLibrary.address, + }, + signer, + ); + + const renderer = await new SVGRendererFactory(signer).deploy(); + const descriptor = await nounsDescriptorFactory.deploy( + ethers.constants.AddressZero, + renderer.address, + ); + + const inflator = await new Inflator__factory(signer).deploy(); + + const art = await new NounsArtFactory(signer).deploy(descriptor.address, inflator.address); + await descriptor.setArt(art.address); + + return descriptor; +}; + export const deployNounsSeeder = async (deployer?: SignerWithAddress): Promise => { const factory = new NounsSeederFactory(deployer || (await getSigners()).deployer); @@ -150,7 +179,7 @@ export const populateDescriptor = async (nounsDescriptor: NounsDescriptor): Prom ]); }; -export const populateDescriptorV2 = async (nounsDescriptor: NounsDescriptorV2): Promise => { +export const populateDescriptorV2 = async (nounsDescriptor: NounsDescriptorV2 | NounsDescriptorV3): Promise => { const { bgcolors, palette, images } = ImageDataV2; const { bodies, accessories, heads, glasses } = images; @@ -622,4 +651,4 @@ export const deployGovernorV3WithV3Proxy = async ( ); return INounsDAOLogic__factory.connect(proxy.address, deployer); -}; +}; \ No newline at end of file diff --git a/packages/nouns-sdk/src/contract/addresses.json b/packages/nouns-sdk/src/contract/addresses.json index 72acf59950..6c39e884ff 100644 --- a/packages/nouns-sdk/src/contract/addresses.json +++ b/packages/nouns-sdk/src/contract/addresses.json @@ -2,8 +2,8 @@ "1": { "nounsToken": "0x9C8fF314C9Bc7F6e59A9d9225Fb22946427eDC03", "nounsSeeder": "0xCC8a0FB5ab3C7132c1b2A0109142Fb112c4Ce515", - "nounsDescriptor": "0x0Cfdb3Ba1694c2bb2CFACB0339ad7b1Ae5932B63", - "nftDescriptor": "0x0BBAd8c947210ab6284699605ce2a61780958264", + "nounsDescriptor": "0x33A9c445fb4FB21f2c030A6b2d3e2F12D017BFAC", + "nftDescriptor": "0xdEdd7Ec3F440B19C627AE909D020ff037F618336", "nounsAuctionHouse": "0xF15a943787014461d94da08aD4040f79Cd7c124e", "nounsAuctionHouseProxy": "0x830BD73E4184ceF73443C15111a1DF14e495C706", "nounsAuctionHouseProxyAdmin": "0xC1C119932d78aB9080862C5fcb964029f086401e", @@ -56,8 +56,8 @@ "11155111": { "nounsToken": "0x4C4674bb72a096855496a7204962297bd7e12b85", "nounsSeeder": "0xe99b8Ee07B28C587B755f348649f3Ee45aDA5E7D", - "nounsDescriptor": "0x5319dbcb313738aD70a3D945E61ceB8b84691928", - "nftDescriptor": "0xF5A7A2f948b6b2B1BD6E25C6ddE4dA892301caB5", + "nounsDescriptor": "0xE5e3Debf38AdceC0316f89eFe88a27A7d99a4477", + "nftDescriptor": "0x29273D2d87F6408f5ADde02Cd93be4dbdAe59015", "nounsAuctionHouse": "0x44FeBD884Abf796d2d198974A768CBD882a959a8", "nounsAuctionHouseProxy": "0x488609b7113FCf3B761A05956300d605E8f6BcAf", "nounsAuctionHouseProxyAdmin": "0x9A19E520d9cd6c40eCc79623f16390a68962b7E9",