Skip to content

Commit

Permalink
Merge pull request #38 from aragon/feature/rd-984-add-metadata-to-the…
Browse files Browse the repository at this point in the history
…-vetokens-nfts

Add NFT metadata
  • Loading branch information
jordaniza authored Dec 13, 2024
2 parents dbdda9f + b095eb0 commit e8ab43d
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 4 deletions.
51 changes: 48 additions & 3 deletions src/escrow/increasing/Lock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,23 @@
pragma solidity ^0.8.17;

import {ILock} from "@escrow-interfaces/ILock.sol";
import {ERC721Upgradeable as ERC721} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import {ERC721EnumerableUpgradeable as ERC721Enumerable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol";
import {ERC721URIStorageUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol";
import {ReentrancyGuardUpgradeable as ReentrancyGuard} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {DaoAuthorizableUpgradeable as DaoAuthorizable} from "@aragon/osx/core/plugin/dao-authorizable/DaoAuthorizableUpgradeable.sol";
import {IDAO} from "@aragon/osx/core/dao/IDAO.sol";

/// @title NFT representation of an escrow locking mechanism
contract Lock is ILock, ERC721Enumerable, UUPSUpgradeable, DaoAuthorizable, ReentrancyGuard {
contract Lock is
ILock,
ERC721Enumerable,
UUPSUpgradeable,
DaoAuthorizable,
ReentrancyGuard,
ERC721URIStorageUpgradeable
{
/// @dev enables transfers without whitelisting
address public constant WHITELIST_ANY_ADDRESS =
address(uint160(uint256(keccak256("WHITELIST_ANY_ADDRESS"))));
Expand All @@ -23,6 +32,8 @@ contract Lock is ILock, ERC721Enumerable, UUPSUpgradeable, DaoAuthorizable, Reen
/// @notice Whitelisted contracts that are allowed to transfer
mapping(address => bool) public whitelisted;

string public baseTokenURI;

/*//////////////////////////////////////////////////////////////
Modifiers
//////////////////////////////////////////////////////////////*/
Expand All @@ -38,7 +49,7 @@ contract Lock is ILock, ERC721Enumerable, UUPSUpgradeable, DaoAuthorizable, Reen

function supportsInterface(
bytes4 _interfaceId
) public view override(ERC721Enumerable) returns (bool) {
) public view override(ERC721Enumerable, ERC721URIStorageUpgradeable) returns (bool) {
return super.supportsInterface(_interfaceId) || _interfaceId == type(ILock).interfaceId;
}

Expand Down Expand Up @@ -83,6 +94,16 @@ contract Lock is ILock, ERC721Enumerable, UUPSUpgradeable, DaoAuthorizable, Reen
emit WhitelistSet(WHITELIST_ANY_ADDRESS, true);
}

/// @notice Override the beforeTokenTransfer as required by ERC721Enumerable
function _beforeTokenTransfer(
address _from,
address _to,
uint256 _tokenId,
uint256 batchSize
) internal override(ERC721, ERC721Enumerable) {
super._beforeTokenTransfer(_from, _to, _tokenId, batchSize);
}

/// @dev Override the transfer to check if the recipient is whitelisted
/// This avoids needing to check for mint/burn but is less idomatic than beforeTokenTransfer
function _transfer(address _from, address _to, uint256 _tokenId) internal override {
Expand Down Expand Up @@ -110,6 +131,30 @@ contract Lock is ILock, ERC721Enumerable, UUPSUpgradeable, DaoAuthorizable, Reen
_burn(_tokenId);
}

function _burn(uint256 _tokenId) internal override(ERC721, ERC721URIStorageUpgradeable) {
super._burn(_tokenId);
}

function tokenURI(
uint256 _tokenId
) public view override(ERC721, ERC721URIStorageUpgradeable) returns (string memory) {
return super.tokenURI(_tokenId);
}

function _baseURI() internal view override returns (string memory) {
return baseTokenURI;
}

function setBaseURI(string memory _baseTokenURI) external auth(LOCK_ADMIN_ROLE) {
baseTokenURI = _baseTokenURI;

emit BaseURISet(baseTokenURI);
}

function setTokenURI(uint256 _tokenId, string memory _tokenURI) external auth(LOCK_ADMIN_ROLE) {
_setTokenURI(_tokenId, _tokenURI);
}

/*//////////////////////////////////////////////////////////////
UUPS Upgrade
//////////////////////////////////////////////////////////////*/
Expand All @@ -123,5 +168,5 @@ contract Lock is ILock, ERC721Enumerable, UUPSUpgradeable, DaoAuthorizable, Reen
/// @notice Internal method authorizing the upgrade of the contract via the [upgradeability mechanism for UUPS proxies](https://docs.openzeppelin.com/contracts/4.x/api/proxy#UUPSUpgradeable) (see [ERC-1822](https://eips.ethereum.org/EIPS/eip-1822)).
function _authorizeUpgrade(address) internal virtual override auth(LOCK_ADMIN_ROLE) {}

uint256[48] private __gap;
uint256[47] private __gap;
}
14 changes: 13 additions & 1 deletion src/escrow/increasing/interfaces/ILock.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/*///////////////////////////////////////////////////////////////
METADATA
//////////////////////////////////////////////////////////////*/

interface IMetadataEvents {
/// @notice Event emmited when the base metadata URI is updated
/// @param uri New base URI
event BaseURISet(string uri);
}

/*///////////////////////////////////////////////////////////////
WHITELIST
//////////////////////////////////////////////////////////////*/
Expand All @@ -21,7 +31,9 @@ interface IWhitelist is IWhitelistEvents, IWhitelistErrors {
function whitelisted(address addr) external view returns (bool);
}

interface ILock is IWhitelist {
interface IMetadata is IMetadataEvents {}

interface ILock is IWhitelist, IMetadata {
error OnlyEscrow();

/// @notice Address of the escrow contract that holds underyling assets
Expand Down
55 changes: 55 additions & 0 deletions test/escrow/escrow/Lock.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ import {EscrowBase} from "./EscrowBase.sol";
import {console2 as console} from "forge-std/console2.sol";
import {IDAO} from "@aragon/osx/core/dao/IDAO.sol";
import {DAO} from "@aragon/osx/core/dao/DAO.sol";
import {DaoUnauthorized} from "@aragon/osx/core/utils/auth.sol";
import {Multisig, MultisigSetup} from "@aragon/multisig/MultisigSetup.sol";

import {IERC721EnumerableUpgradeable as IERC721Enumerable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/IERC721EnumerableUpgradeable.sol";
import {IERC721MetadataUpgradeable as IERC721Metadata} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/IERC721MetadataUpgradeable.sol";

import {ProxyLib} from "@libs/ProxyLib.sol";

import {IEscrowCurveTokenStorage} from "@escrow-interfaces/IEscrowCurveIncreasing.sol";
Expand All @@ -31,12 +35,15 @@ contract TestLockMintBurn is EscrowBase, IEscrowCurveTokenStorage, IGaugeVote {
assertEq(_nftLock.name(), _name);
assertEq(_nftLock.symbol(), _symbol);
assertEq(_nftLock.escrow(), _escrow);
assertEq(_nftLock.baseTokenURI(), "");
assertEq(address(_nftLock.dao()), _dao);
}

function testSupportsInterface() public view {
assertTrue(nftLock.supportsInterface(type(ILock).interfaceId));
assertFalse(nftLock.supportsInterface(0xffffffff));
assertTrue(nftLock.supportsInterface(type(IERC721Metadata).interfaceId));
assertTrue(nftLock.supportsInterface(type(IERC721Enumerable).interfaceId));
}

function testFuzz_OnlyEscrowCanMint(address _notEscrow) public {
Expand Down Expand Up @@ -95,6 +102,54 @@ contract TestLockMintBurn is EscrowBase, IEscrowCurveTokenStorage, IGaugeVote {
vm.expectRevert("revert");
newLock.mint(address(reentrant), 1);
}

function testSetNFTMetadata() public {
vm.prank(address(escrow));
nftLock.mint(address(123), 1);

assertEq(nftLock.tokenURI(1), "");

vm.prank(address(this));
nftLock.setBaseURI("https://example.com/");
assertEq(nftLock.baseTokenURI(), "https://example.com/");
assertEq(nftLock.tokenURI(1), "https://example.com/1");

vm.prank(address(this));
nftLock.setTokenURI(1, "?tokenId=1");
assertEq(nftLock.tokenURI(1), "https://example.com/?tokenId=1");

vm.prank(address(escrow));
nftLock.mint(address(123), 2);

assertEq(nftLock.tokenURI(2), "https://example.com/2");
}

function testOnlyOwnerCanSetNFTMetadata(address _notEscrow) public {
vm.assume(_notEscrow != address(this));

bytes memory data = abi.encodeWithSelector(
DaoUnauthorized.selector,
address(dao),
address(nftLock),
address(_notEscrow),
nftLock.LOCK_ADMIN_ROLE()
);

vm.prank(address(escrow));
nftLock.mint(address(123), 1);

vm.prank(_notEscrow);
vm.expectRevert(data);
nftLock.setBaseURI("https://example.com/");

assertEq(nftLock.baseTokenURI(), "");
assertEq(nftLock.tokenURI(1), "");

vm.prank(_notEscrow);
vm.expectRevert(data);
nftLock.setTokenURI(1, "?tokenId=1");
assertEq(nftLock.tokenURI(1), "");
}
}

contract NFTReentrant {
Expand Down

0 comments on commit e8ab43d

Please sign in to comment.