diff --git a/packages/contracts/evm-contracts/contracts/token/IERC4906Agnostic.sol b/packages/contracts/evm-contracts/contracts/token/IERC4906Agnostic.sol new file mode 100644 index 000000000..f8693b3d0 --- /dev/null +++ b/packages/contracts/evm-contracts/contracts/token/IERC4906Agnostic.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +// Forked from OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC4906.sol) + +pragma solidity ^0.8.13; + +/// @title Agnostic Metadata Update Extension +interface IERC4906Agnostic { + /// @dev This event emits when the metadata of a token is changed. + /// So that the third-party platforms such as NFT market could + /// timely update the images and related attributes of the token. + event MetadataUpdate(uint256 _tokenId); + + /// @dev This event emits when the metadata of a range of tokens is changed. + /// So that the third-party platforms such as NFT market could + /// timely update the images and related attributes of the tokens. + event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId); +} diff --git a/packages/contracts/evm-contracts/contracts/token/IInverseAppProjected1155.sol b/packages/contracts/evm-contracts/contracts/token/IInverseAppProjected1155.sol new file mode 100644 index 000000000..59e47c0d2 --- /dev/null +++ b/packages/contracts/evm-contracts/contracts/token/IInverseAppProjected1155.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {IInverseProjected1155} from "./IInverseProjected1155.sol"; + +/// @dev A Paima Inverse Projection ERC1155 token where initialization is handled by the app-layer. +/// A standard ERC1155 that can be freely minted and stores an unique `` pair (used in `tokenURI`) when minted. +interface IInverseAppProjected1155 is IInverseProjected1155 { + /// @dev Emitted when `value` amount of globally-enforced `tokenId` in combination with an unique `` pair is minted. + event Minted( + uint256 indexed tokenId, + address indexed minter, + uint256 indexed userTokenId, + uint256 value + ); + + /// @dev Emitted when supply of globally-enforced `tokenId` in combination with an unique `` pair goes to zero. + event BurnedAll(uint256 indexed tokenId, address indexed minter, uint256 indexed userTokenId); + + /// @dev Mints `value` of a new token to the transaction sender. + /// Increases the `currentTokenId`. + /// Reverts if transaction sender is a smart contract that does not implement IERC1155Receiver-onERC1155Received. + /// Emits the `Minted` event. + /// Returns the id of the minted token. + function mint(uint256 value, bytes memory data) external returns (uint256); +} diff --git a/packages/contracts/evm-contracts/contracts/token/IInverseBaseProjected1155.sol b/packages/contracts/evm-contracts/contracts/token/IInverseBaseProjected1155.sol new file mode 100644 index 000000000..14ee93a19 --- /dev/null +++ b/packages/contracts/evm-contracts/contracts/token/IInverseBaseProjected1155.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {IInverseProjected1155} from "./IInverseProjected1155.sol"; + +/// @dev A Paima Inverse Projection ERC1155 token where initialization is handled by the base-layer. +/// A standard ERC1155 that accepts calldata in the mint function for any initialization data needed in a Paima dApp. +interface IInverseBaseProjected1155 is IInverseProjected1155 { + /// @dev Emitted when `value` amount of globally-enforced `tokenId` is minted, with `initialData` provided in the `mint` function parameters. + event Minted(uint256 indexed tokenId, string initialData); + + /// @dev Mints `value` of a new token to transaction sender, passing `initialData` to be emitted in the event. + /// Increases the `currentTokenId`. + /// Reverts if transaction sender is a smart contract that does not implement IERC1155Receiver-onERC1155Received. + /// Emits the `Minted` event. + /// Returns the id of the minted token. + function mint( + uint256 value, + bytes memory data, + string calldata initialData + ) external returns (uint256); +} diff --git a/packages/contracts/evm-contracts/contracts/token/IInverseProjected1155.sol b/packages/contracts/evm-contracts/contracts/token/IInverseProjected1155.sol new file mode 100644 index 000000000..183e327d8 --- /dev/null +++ b/packages/contracts/evm-contracts/contracts/token/IInverseProjected1155.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {IERC1155MetadataURI} from "@openzeppelin/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol"; +import {IERC4906Agnostic} from "./IERC4906Agnostic.sol"; + +/// @dev A standard ERC1155 that can be burned and has a special uri function accepting a custom base URI. +interface IInverseProjected1155 is IERC1155MetadataURI, IERC4906Agnostic { + /// @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 Burns `value` amount of token of ID `id` from transaction sender. + /// Reverts if transaction sender's balance of `id` is less than `value`. + function burn(uint256 id, uint256 value) external; + + /// @dev Sets `_URI` as the `baseURI`. + /// Callable only by the contract owner. + /// Emits the `SetBaseURI` event. + function setBaseURI(string memory _URI) external; + + /// @dev Sets `_newBaseExtension` as the `baseExtension`. + /// Callable only by the contract owner. + function setBaseExtension(string memory _newBaseExtension) external; + + /// @dev Returns the token URI of specified `id` using a custom base URI. + function uri(uint256 id, string memory customBaseUri) external view returns (string memory); +} diff --git a/packages/contracts/evm-contracts/contracts/token/InverseAppProjected1155.sol b/packages/contracts/evm-contracts/contracts/token/InverseAppProjected1155.sol new file mode 100644 index 000000000..c5989ed9f --- /dev/null +++ b/packages/contracts/evm-contracts/contracts/token/InverseAppProjected1155.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; +import {ERC1155Supply} from "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Supply.sol"; +import {IERC1155MetadataURI} from "@openzeppelin/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {IERC4906} from "@openzeppelin/contracts/interfaces/IERC4906.sol"; +import {IInverseProjected1155} from "./IInverseProjected1155.sol"; +import {IInverseAppProjected1155} from "./IInverseAppProjected1155.sol"; +import {IERC4906Agnostic} from "./IERC4906Agnostic.sol"; + +struct MintEntry { + address minter; + uint256 userTokenId; +} + +/// @dev A Paima Inverse Projection ERC1155 token where initialization is handled by the app-layer. +/// A standard ERC1155 that can be freely minted and stores an unique `` pair (used in `tokenURI`) when minted. +contract InverseAppProjected1155 is IInverseAppProjected1155, ERC1155Supply, Ownable { + using Strings for uint256; + + string public name; + string public symbol; + + mapping(uint256 id => MintEntry) public tokenToMint; + mapping(address minter => uint256) public mintCount; + mapping(uint256 id => uint256) private _initialSupply; + + /// @dev The token ID that will be minted when calling the `mint` function. + uint256 public currentTokenId; + /// @dev Base URI that is used in the `uri` function to form the start of the token URI. + string public baseURI; + /// @dev Base extension that is used in the `uri` function to form the end of the token URI. + string public baseExtension; + + /// @dev Sets the token's `_name`, `_symbol`, and transfers ownership to `_owner`. + /// Also sets `currentTokenId` to 1. + constructor( + string memory _name, + string memory _symbol, + address _owner + ) ERC1155("") Ownable(_owner) { + name = _name; + symbol = _symbol; + currentTokenId = 1; + } + + /// @dev Returns true if this contract implements the interface defined by `interfaceId`. See EIP165. + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(IERC165, ERC1155) returns (bool) { + return + interfaceId == type(IInverseProjected1155).interfaceId || + interfaceId == type(IInverseAppProjected1155).interfaceId || + super.supportsInterface(interfaceId); + } + + /// @dev Returns the amount of token with ID `id` that had been initially minted. + function initialSupply(uint256 id) public view virtual returns (uint256) { + return _initialSupply[id]; + } + + /// @dev Mints `value` of a new token to transaction sender. + /// Increases the `currentTokenId`. + /// Reverts if transaction sender is a smart contract that does not implement IERC1155Receiver-onERC1155Received. + /// Emits the `Minted` event. + /// Returns the id of the minted token. + function mint(uint256 value, bytes memory data) external virtual returns (uint256) { + uint256 tokenId = currentTokenId; + _mint(msg.sender, tokenId, value, data); + mintCount[msg.sender] += 1; + uint256 userTokenId = mintCount[msg.sender]; + tokenToMint[tokenId] = MintEntry(msg.sender, userTokenId); + _initialSupply[tokenId] = value; + + currentTokenId++; + + emit Minted(tokenId, msg.sender, userTokenId, value); + return tokenId; + } + + /// @dev Burns `value` amount of token of ID `id` from transaction sender. + /// Reverts if transaction sender's balance of `id` is less than `value`. + function burn(uint256 id, uint256 value) external virtual { + _burn(msg.sender, id, value); + if (totalSupply(id) == 0) { + MintEntry memory entry = tokenToMint[id]; + emit BurnedAll(id, entry.minter, entry.userTokenId); + } + } + + /// @dev Returns the token URI of specified `id` using the default set base URI. + function uri( + uint256 id + ) public view virtual override(ERC1155, IERC1155MetadataURI) returns (string memory) { + return uri(id, baseURI); + } + + /// @dev Returns the token URI of specified `id` using a custom base URI. + function uri( + uint256 id, + string memory customBaseUri + ) public view virtual returns (string memory) { + require(exists(id), "InverseAppProjected1155: URI query for nonexistent token"); + MintEntry memory entry = tokenToMint[id]; + string memory URI = bytes(customBaseUri).length > 0 + ? string.concat( + customBaseUri, + Strings.toHexString(uint160(entry.minter), 20), + "/", + entry.userTokenId.toString(), + "/", + initialSupply(id).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 virtual 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 virtual 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); + } +} diff --git a/packages/contracts/evm-contracts/contracts/token/InverseBaseProjected1155.sol b/packages/contracts/evm-contracts/contracts/token/InverseBaseProjected1155.sol new file mode 100644 index 000000000..21babfbc0 --- /dev/null +++ b/packages/contracts/evm-contracts/contracts/token/InverseBaseProjected1155.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; +import {ERC1155Supply} from "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Supply.sol"; +import {IERC1155MetadataURI} from "@openzeppelin/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {IERC4906} from "@openzeppelin/contracts/interfaces/IERC4906.sol"; +import {IInverseProjected1155} from "./IInverseProjected1155.sol"; +import {IInverseBaseProjected1155} from "./IInverseBaseProjected1155.sol"; +import {IERC4906Agnostic} from "./IERC4906Agnostic.sol"; + +/// @dev A Paima Inverse Projection ERC1155 token where initialization is handled by the base-layer. +/// A standard ERC1155 that accepts calldata in the mint function for any initialization data needed in a Paima dApp. +contract InverseBaseProjected1155 is IInverseBaseProjected1155, ERC1155Supply, Ownable { + using Strings for uint256; + + string public name; + string public symbol; + + /// @dev The token ID that will be minted when calling the `mint` function. + uint256 public currentTokenId; + /// @dev Base URI that is used in the `uri` function to form the start of the token URI. + string public baseURI; + /// @dev Base extension that is used in the `uri` function to form the end of the token URI. + string public baseExtension; + + /// @dev Sets the NFT's `name`, `symbol`, and transfers ownership to `owner`. + /// Also sets `currentTokenId` to 1. + constructor( + string memory _name, + string memory _symbol, + address _owner + ) ERC1155("") Ownable(_owner) { + name = _name; + symbol = _symbol; + currentTokenId = 1; + } + + /// @dev Returns true if this contract implements the interface defined by `interfaceId`. See EIP165. + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(IERC165, ERC1155) returns (bool) { + return + interfaceId == type(IInverseProjected1155).interfaceId || + interfaceId == type(IInverseBaseProjected1155).interfaceId || + super.supportsInterface(interfaceId); + } + + /// @dev Mints `value` of a new token to transaction sender, passing `initialData` to be emitted in the event. + /// Increases the `currentTokenId`. + /// Reverts if transaction sender is a smart contract that does not implement IERC1155Receiver-onERC1155Received. + /// Emits the `Minted` event. + /// Returns the id of the minted token. + function mint( + uint256 value, + bytes memory data, + string calldata initialData + ) external virtual returns (uint256) { + uint256 tokenId = currentTokenId; + _mint(msg.sender, tokenId, value, data); + + currentTokenId++; + + emit Minted(tokenId, initialData); + return tokenId; + } + + /// @dev Burns `value` amount of token of ID `id` from transaction sender. + /// Reverts if transaction sender's balance of `id` is less than `value`. + function burn(uint256 id, uint256 value) external virtual { + _burn(msg.sender, id, value); + } + + /// @dev Returns the token URI of specified `id` using the default set base URI. + function uri( + uint256 id + ) public view virtual override(ERC1155, IERC1155MetadataURI) returns (string memory) { + return uri(id, baseURI); + } + + /// @dev Returns the token URI of specified `id` using a custom base URI. + function uri( + uint256 id, + string memory customBaseUri + ) public view virtual returns (string memory) { + require(exists(id), "InverseBaseProjected1155: URI query for nonexistent token"); + string memory URI = bytes(customBaseUri).length > 0 + ? string.concat(customBaseUri, id.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 virtual 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 virtual 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); + } +}