Skip to content

Commit

Permalink
Inverse Projected NFT (#306)
Browse files Browse the repository at this point in the history
* Run prettier

* Add InverseProjectedNft

* Implement ERC4906 - MetadataUpdate event

* Add tokenURI to interface and move IERC4096 there too
  • Loading branch information
matejos authored Mar 2, 2024
1 parent e5df905 commit b9841fd
Show file tree
Hide file tree
Showing 8 changed files with 1,771 additions and 1,391 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import {IERC4906} from "@openzeppelin/contracts/interfaces/IERC4906.sol";

/// @dev A standard ERC721 that accepts calldata in the mint function for any initialization data needed in a Paima dApp.
interface IInverseProjectedNft is IERC4906 {
/// @dev Emitted when `baseExtension` is updated from `oldBaseExtension` to `newBaseExtension`.
event SetBaseExtension(string oldBaseExtension, string newBaseExtension);

/// @dev Emitted when `baseUri` is updated from `oldUri` to `newUri`.
event SetBaseURI(string oldUri, string newUri);

/// @dev Emitted when a new token with ID `tokenId` is minted, with `initialData` provided in the `mint` function parameters.
event Minted(uint256 indexed tokenId, string initialData);

/// @dev Mints a new token to address `_to`, passing `initialData` to be emitted in the event.
/// Increases the `totalSupply` and `currentTokenId`.
/// Reverts if `totalSupply` is not less than `maxSupply` or if `_to` is a zero address.
/// Emits the `Minted` event.
function mint(address _to, string calldata initialData) external returns (uint256);

/// @dev Burns token of ID `_tokenId`. Callable only by the owner of the specified token.
/// Reverts if `_tokenId` is not existing.
function burn(uint256 _tokenId) external;

/// @dev Sets `_URI` as the `baseURI` of the NFT.
/// Callable only by the contract owner.
/// Emits the `SetBaseURI` event.
function setBaseURI(string memory _URI) external;

/// @dev Sets `_newBaseExtension` as the `baseExtension` of the NFT.
/// Callable only by the contract owner.
function setBaseExtension(string memory _newBaseExtension) external;

/// @dev Returns the token URI of specified `tokenId` using a custom base URI.
function tokenURI(
uint256 tokenId,
string memory customBaseUri
) external view returns (string memory);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import {IERC4906} from "@openzeppelin/contracts/interfaces/IERC4906.sol";
import {IInverseProjectedNft} from "./IInverseProjectedNft.sol";

/// @dev A standard ERC721 that accepts calldata in the mint function for any initialization data needed in a Paima dApp.
contract InverseProjectedNft is IInverseProjectedNft, ERC721, Ownable {
using Strings for uint256;

/// @dev The token ID that will be minted when calling the `mint` function.
uint256 public currentTokenId;
/// @dev Base URI that is used in the `tokenURI` function to form the start of the token URI.
string public baseURI;
/// @dev Total token supply, increased by minting and decreased by burning.
uint256 public totalSupply;
/// @dev Base extension that is used in the `tokenURI` function to form the end of the token URI.
string public baseExtension;

/// @dev Reverts if `msg.sender` is not the specified token's owner.
modifier onlyTokenOwner(uint256 tokenId) {
require(msg.sender == ownerOf(tokenId), "InverseProjectedNft: not owner");
_;
}

/// @dev Sets the NFT's `name`, `symbol`, and transfers ownership to `owner`.
/// Also sets `currentTokenId` to 1 and `baseExtension` to `".json"`.
constructor(
string memory name,
string memory symbol,
address owner
) ERC721(name, symbol) Ownable(owner) {
currentTokenId = 1;
baseExtension = ".json";
}

/// @dev Returns true if this contract implements the interface defined by `interfaceId`. See EIP165.
function supportsInterface(
bytes4 interfaceId
) public view override(IERC165, ERC721) returns (bool) {
return
interfaceId == type(IInverseProjectedNft).interfaceId ||
interfaceId == bytes4(0x49064906) ||
super.supportsInterface(interfaceId);
}

/// @dev Mints a new token to address `_to`, passing `initialData` to be emitted in the event.
/// Increases the `totalSupply` and `currentTokenId`.
/// Reverts if `_to` is a zero address or if it refers to smart contract but does not implement IERC721Receiver-onERC721Received.
/// Emits the `Minted` event.
function mint(address _to, string calldata initialData) external returns (uint256) {
require(_to != address(0), "InverseProjectedNft: zero receiver address");

uint256 tokenId = currentTokenId;
_safeMint(_to, tokenId);

totalSupply++;
currentTokenId++;

emit Minted(tokenId, initialData);
return tokenId;
}

/// @dev Burns token of ID `_tokenId`. Callable only by the owner of the specified token.
/// Reverts if `_tokenId` does not exist.
function burn(uint256 _tokenId) external onlyTokenOwner(_tokenId) {
totalSupply--;
_burn(_tokenId);
}

/// @dev Returns the `baseURI` of this NFT.
function _baseURI() internal view override returns (string memory) {
return baseURI;
}

/// @dev Returns the token URI of specified `tokenId` using the default set base URI.
function tokenURI(uint256 tokenId) public view override returns (string memory) {
return tokenURI(tokenId, _baseURI());
}

/// @dev Returns the token URI of specified `tokenId` using a custom base URI.
function tokenURI(
uint256 tokenId,
string memory customBaseUri
) public view returns (string memory) {
_requireOwned(tokenId);
string memory URI = bytes(customBaseUri).length > 0
? string.concat(customBaseUri, tokenId.toString())
: "";
return string(abi.encodePacked(URI, baseExtension));
}

/// @dev Sets `_URI` as the `baseURI` of the NFT.
/// Callable only by the contract owner.
/// Emits the `SetBaseURI` event.
function setBaseURI(string memory _URI) external onlyOwner {
string memory oldURI = baseURI;
baseURI = _URI;
emit SetBaseURI(oldURI, _URI);
}

/// @dev Sets `_newBaseExtension` as the `baseExtension` of the NFT.
/// Callable only by the contract owner.
function setBaseExtension(string memory _newBaseExtension) public onlyOwner {
string memory oldBaseExtension = baseExtension;
baseExtension = _newBaseExtension;
emit SetBaseURI(oldBaseExtension, _newBaseExtension);
}

/// @dev Function that emits an event to notify third-parties (e.g. NFT marketplaces) about
/// an update to consecutive range of tokens. Can be overriden in inheriting contract.
function updateMetadataBatch(uint256 _fromTokenId, uint256 _toTokenId) public virtual {
emit BatchMetadataUpdate(_fromTokenId, _toTokenId);
}

/// @dev Function that emits an event to notify third-parties (e.g. NFT marketplaces) about
/// an update to a single token. Can be overriden in inheriting contract.
function updateMetadata(uint256 _tokenId) public virtual {
emit MetadataUpdate(_tokenId);
}
}
14 changes: 9 additions & 5 deletions packages/contracts/evm-contracts/docs/templates/helpers.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const { version } = require('../../package.json');

module.exports['escapeChars'] = (input) => {
if (input == null) return "No description given";
module.exports['escapeChars'] = input => {
if (input == null) return 'No description given';
return input.replace(/\{([a-zA-Z0-9-]+)\}/g, '\\{$1\\}');
};

Expand All @@ -14,7 +14,9 @@ module.exports['readme-path'] = opts => {
module.exports.names = params => params?.map(p => p.name).join(', ');

module.exports['typed-params'] = params => {
return params?.map(p => `${p.type}${p.indexed ? ' indexed' : ''}${p.name ? ' ' + p.name : ''}`).join(', ');
return params
?.map(p => `${p.type}${p.indexed ? ' indexed' : ''}${p.name ? ' ' + p.name : ''}`)
.join(', ');
};

const slug = (module.exports.slug = str => {
Expand All @@ -34,7 +36,9 @@ function getAllLinks(items) {
linksCache.set(items, res);
for (const item of items) {
res[`xref-${item.anchor}`] = `xref:${item.__item_context.page}#${item.anchor}`;
res[slug(item.fullName)] = `pass:normal[xref:${item.__item_context.page}#${item.anchor}[\`${item.fullName}\`]]`;
res[
slug(item.fullName)
] = `pass:normal[xref:${item.__item_context.page}#${item.anchor}[\`${item.fullName}\`]]`;
}
return res;
}
Expand All @@ -50,4 +54,4 @@ module.exports['with-prelude'] = opts => {
// const prelude = neededLinks.map(k => `- [${k}](#${k})`).join('\n');
return contents;
// return prelude + '\n' + contents;
};
};
14 changes: 10 additions & 4 deletions packages/contracts/evm-contracts/docs/templates/properties.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ module.exports.anchor = function anchor({ item, contract }) {
}
res += item.name;
if ('parameters' in item) {
const signature = item.parameters.parameters.map(v => v.typeName.typeDescriptions.typeString).join(',');
const signature = item.parameters.parameters
.map(v => v.typeName.typeDescriptions.typeString)
.join(',');
res += slug('(' + signature + ')');
}
if (isNodeType('VariableDeclaration', item)) {
Expand Down Expand Up @@ -56,9 +58,13 @@ module.exports.returns2 = function ({ item }) {

module.exports['inherited-functions'] = function ({ item }) {
const { inheritance } = item;
const baseFunctions = new Set(inheritance.flatMap(c => c.functions.flatMap(f => f.baseFunctions ?? [])));
const baseFunctions = new Set(
inheritance.flatMap(c => c.functions.flatMap(f => f.baseFunctions ?? []))
);
return inheritance.map((contract, i) => ({
contract,
functions: contract.functions.filter(f => !baseFunctions.has(f.id) && (f.name !== 'constructor' || i === 0)),
functions: contract.functions.filter(
f => !baseFunctions.has(f.id) && (f.name !== 'constructor' || i === 0)
),
}));
};
};
110 changes: 55 additions & 55 deletions packages/contracts/evm-contracts/test-lib/cheatcodes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,83 +2,83 @@
pragma solidity ^0.8.7;

interface CheatCodes {
// Set block.timestamp (newTimestamp)
function warp(uint256) external;
// Set block.timestamp (newTimestamp)
function warp(uint256) external;

// Set block.height (newHeight)
function roll(uint256) external;
// Set block.height (newHeight)
function roll(uint256) external;

// Set block.basefee (newBasefee)
function fee(uint256) external;
// Set block.basefee (newBasefee)
function fee(uint256) external;

// Loads a storage slot from an address (who, slot)
function load(address, bytes32) external returns (bytes32);
// Loads a storage slot from an address (who, slot)
function load(address, bytes32) external returns (bytes32);

// Stores a value to an address' storage slot, (who, slot, value)
function store(address, bytes32, bytes32) external;
// Stores a value to an address' storage slot, (who, slot, value)
function store(address, bytes32, bytes32) external;

// Signs data, (privateKey, digest) => (v, r, s)
function sign(uint256, bytes32) external returns (uint8, bytes32, bytes32);
// Signs data, (privateKey, digest) => (v, r, s)
function sign(uint256, bytes32) external returns (uint8, bytes32, bytes32);

// Gets address for a given private key, (privateKey) => (address)
function addr(uint256) external returns (address);
// Gets address for a given private key, (privateKey) => (address)
function addr(uint256) external returns (address);

// Performs a foreign function call via terminal, (stringInputs) => (result)
function ffi(string[] calldata) external returns (bytes memory);
// Performs a foreign function call via terminal, (stringInputs) => (result)
function ffi(string[] calldata) external returns (bytes memory);

// Sets the *next* call's msg.sender to be the input address
function prank(address) external;
// Sets the *next* call's msg.sender to be the input address
function prank(address) external;

// Sets all subsequent calls' msg.sender to be the input address until `stopPrank` is called
function startPrank(address) external;
// Sets all subsequent calls' msg.sender to be the input address until `stopPrank` is called
function startPrank(address) external;

// Sets the *next* call's msg.sender to be the input address, and the tx.origin to be the second input
function prank(address, address) external;
// Sets the *next* call's msg.sender to be the input address, and the tx.origin to be the second input
function prank(address, address) external;

// Sets all subsequent calls' msg.sender to be the input address until `stopPrank` is called, and the tx.origin to be the second input
function startPrank(address, address) external;
// Sets all subsequent calls' msg.sender to be the input address until `stopPrank` is called, and the tx.origin to be the second input
function startPrank(address, address) external;

// Resets subsequent calls' msg.sender to be `address(this)`
function stopPrank() external;
// Resets subsequent calls' msg.sender to be `address(this)`
function stopPrank() external;

// Sets an address' balance, (who, newBalance)
function deal(address, uint256) external;
// Sets an address' balance, (who, newBalance)
function deal(address, uint256) external;

// Sets an address' code, (who, newCode)
function etch(address, bytes calldata) external;
// Sets an address' code, (who, newCode)
function etch(address, bytes calldata) external;

// Expects an error on next call
function expectRevert(bytes calldata) external;
// Expects an error on next call
function expectRevert(bytes calldata) external;

function expectRevert(bytes4) external;
function expectRevert(bytes4) external;

// Record all storage reads and writes
function record() external;
// Record all storage reads and writes
function record() external;

// Gets all accessed reads and write slot from a recording session, for a given address
function accesses(address) external returns (bytes32[] memory reads, bytes32[] memory writes);
// Gets all accessed reads and write slot from a recording session, for a given address
function accesses(address) external returns (bytes32[] memory reads, bytes32[] memory writes);

// Prepare an expected log with (bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData).
// Call this function, then emit an event, then call a function. Internally after the call, we check if
// logs were emitted in the expected order with the expected topics and data (as specified by the booleans)
function expectEmit(bool, bool, bool, bool) external;
// Prepare an expected log with (bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData).
// Call this function, then emit an event, then call a function. Internally after the call, we check if
// logs were emitted in the expected order with the expected topics and data (as specified by the booleans)
function expectEmit(bool, bool, bool, bool) external;

// Mocks a call to an address, returning specified data.
// Calldata can either be strict or a partial match, e.g. if you only
// pass a Solidity selector to the expected calldata, then the entire Solidity
// function will be mocked.
function mockCall(address, bytes calldata, bytes calldata) external;
// Mocks a call to an address, returning specified data.
// Calldata can either be strict or a partial match, e.g. if you only
// pass a Solidity selector to the expected calldata, then the entire Solidity
// function will be mocked.
function mockCall(address, bytes calldata, bytes calldata) external;

// Clears all mocked calls
function clearMockedCalls() external;
// Clears all mocked calls
function clearMockedCalls() external;

// Expect a call to an address with the specified calldata.
// Calldata can either be strict or a partial match
function expectCall(address, bytes calldata) external;
// Expect a call to an address with the specified calldata.
// Calldata can either be strict or a partial match
function expectCall(address, bytes calldata) external;

// Gets the code from an artifact file. Takes in the relative path to the json file
function getCode(string calldata) external returns (bytes memory);
// Gets the code from an artifact file. Takes in the relative path to the json file
function getCode(string calldata) external returns (bytes memory);

// Labels an address in call traces
function label(address, string calldata) external;
// Labels an address in call traces
function label(address, string calldata) external;
}
Loading

0 comments on commit b9841fd

Please sign in to comment.