From 56b64d5a73f052b605fbd1cd34c676e6642f0410 Mon Sep 17 00:00:00 2001 From: Aitor <1726644+aaitor@users.noreply.github.com> Date: Tue, 30 Apr 2024 08:37:21 +0200 Subject: [PATCH] feat: susbcription 1155 without blocks --- .../NFT1155SubscriptionUpgradeable.sol | 23 +-- .../NFT1155SubscriptionWithoutBlocks.sol | 131 +++++++++++++ .../NFT1155SubscriptionWithoutBlocks.Test.js | 181 ++++++++++++++++++ 3 files changed, 324 insertions(+), 11 deletions(-) create mode 100644 contracts/token/erc1155/NFT1155SubscriptionWithoutBlocks.sol create mode 100644 test/unit/token/NFT1155SubscriptionWithoutBlocks.Test.js diff --git a/contracts/token/erc1155/NFT1155SubscriptionUpgradeable.sol b/contracts/token/erc1155/NFT1155SubscriptionUpgradeable.sol index 1ca98fd8..e6b04566 100644 --- a/contracts/token/erc1155/NFT1155SubscriptionUpgradeable.sol +++ b/contracts/token/erc1155/NFT1155SubscriptionUpgradeable.sol @@ -10,7 +10,7 @@ contract NFT1155SubscriptionUpgradeable is NFT1155Upgradeable { struct MintedTokens { uint256 amountMinted; // uint64 - uint256 expirationBlock; // uint64 + uint256 expirationBlock; uint256 mintBlock; bool isMintOps; // true means mint, false means burn } @@ -64,12 +64,13 @@ contract NFT1155SubscriptionUpgradeable is NFT1155Upgradeable { // solhint-disable-next-line function burn(address to, uint256 id, uint256 amount, uint256 _seed) override public { + address _sender = _msgSender(); require(balanceOf(to, id) >= amount, 'ERC1155: burn amount exceeds balance'); require( - isOperator(_msgSender()) || // Or the DIDRegistry is burning the NFT - to == _msgSender() || // Or the NFT owner is _msgSender() - nftRegistry.isDIDProvider(bytes32(id), _msgSender()) || // Or the DID Provider (Node) is burning the NFT - isApprovedForAll(to, _msgSender()), // Or the _msgSender() is approved + isOperator(_sender) || // Or the DIDRegistry is burning the NFT + to == _sender || // Or the NFT owner is _msgSender() + nftRegistry.isDIDProvider(bytes32(id), _sender) || // Or the DID Provider (Node) is burning the NFT + isApprovedForAll(to, _sender), // Or the _msgSender() is approved 'ERC1155: caller is not owner nor approved' ); @@ -83,15 +84,15 @@ contract NFT1155SubscriptionUpgradeable is NFT1155Upgradeable { bytes32 _key = _getTokenKey(to, id); uint256 _pendingToBurn = amount; - for (uint index = 0; index < _tokens[_key].length; index++) { - if (_tokens[_key][index].expirationBlock == 0 || _tokens[_key][index].expirationBlock > block.number) { - if (_pendingToBurn <= _tokens[_key][index].amountMinted) { - _tokens[_key].push( MintedTokens(_pendingToBurn, _tokens[_key][index].expirationBlock, block.number, false)); + MintedTokens memory entry = _tokens[_key][index]; + if (entry.expirationBlock == 0 || entry.expirationBlock > block.number) { + if (_pendingToBurn <= entry.amountMinted) { + _tokens[_key].push( MintedTokens(_pendingToBurn, entry.expirationBlock, block.number, false)); break; } else { - _pendingToBurn -= _tokens[_key][index].amountMinted; - _tokens[_key].push( MintedTokens(_tokens[_key][index].amountMinted, _tokens[_key][index].expirationBlock, block.number, false)); + _pendingToBurn -= entry.amountMinted; + _tokens[_key].push( MintedTokens(entry.amountMinted, entry.expirationBlock, block.number, false)); } } } diff --git a/contracts/token/erc1155/NFT1155SubscriptionWithoutBlocks.sol b/contracts/token/erc1155/NFT1155SubscriptionWithoutBlocks.sol new file mode 100644 index 00000000..fee1b1bc --- /dev/null +++ b/contracts/token/erc1155/NFT1155SubscriptionWithoutBlocks.sol @@ -0,0 +1,131 @@ +pragma solidity ^0.8.0; + +import '@openzeppelin/contracts-upgradeable/utils/introspection/ERC165StorageUpgradeable.sol'; +import './NFT1155Upgradeable.sol'; +// Copyright 2022 Nevermined AG. +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +contract NFT1155SubscriptionWithoutBlocks is NFT1155Upgradeable { + +// struct MintedTokens { +// uint256 amountMinted; // uint64 +// uint256 expirationBlock; +// uint256 mintBlock; +// bool isMintOps; // true means mint, false means burn +// } +// +// mapping(bytes32 => MintedTokens[]) internal _tokens; + + // It represents the NFT type. It is used to identify the NFT type in the Nevermined ecosystem + // solhint-disable-next-line + bytes32 public constant override nftType = keccak256('nft1155-subscription'); + + function initialize( + address owner, + address didRegistryAddress, + string memory name_, + string memory symbol_, + string memory uri_, + address nvmConfig_ + ) + public + override + virtual + initializer + { + __NFT1155Upgradeable_init(owner, didRegistryAddress, name_, symbol_, uri_, nvmConfig_); + } + + + function mint(address to, uint256 tokenId, uint256 amount, bytes memory data) virtual override public { + super.mint(to, tokenId, amount, data); + } + + function burn(uint256 id, uint256 amount) override public { + burn(_msgSender(), id, amount); + } + + // solhint-disable-next-line + function burn(address to, uint256 id, uint256 amount) override public { + address _sender = _msgSender(); + require(balanceOf(to, id) >= amount, 'ERC1155: burn amount exceeds balance'); + require( + isOperator(_sender) || // Or the DIDRegistry is burning the NFT + to == _sender || // Or the NFT owner is _msgSender() + nftRegistry.isDIDProvider(bytes32(id), _sender) || // Or the DID Provider (Node) is burning the NFT + isApprovedForAll(to, _sender), // Or the _msgSender() is approved + 'ERC1155: caller is not owner nor approved' + ); + + // Update nftSupply + _nftAttributes[id].nftSupply -= amount; + // Register provenance event + + _burn(to, id, amount); + } + + /** + * @dev See {NFT1155Upgradeableable-balanceOf}. + */ + function balanceOf(address account, uint256 tokenId) public view virtual override returns (uint256) { + return super.balanceOf(account, tokenId); +// bytes32 _key = _getTokenKey(account, tokenId); +// uint256 _amountBurned; +// uint256 _amountMinted; +// for (uint index = 0; index < _tokens[_key].length; index++) { +// if (_tokens[_key][index].mintBlock > 0 && +// (_tokens[_key][index].expirationBlock == 0 || _tokens[_key][index].expirationBlock > block.number)) { +// if (_tokens[_key][index].isMintOps) +// _amountMinted += _tokens[_key][index].amountMinted; +// else +// _amountBurned += _tokens[_key][index].amountMinted; +// } +// } +// +// if (_amountBurned >= _amountMinted) +// return 0; +// else +// return _amountMinted - _amountBurned; + } + +// function whenWasMinted(address owner, uint256 tokenId) public view returns (uint256[] memory) { +// bytes32 _key = _getTokenKey(owner, tokenId); +// uint256[] memory _whenMinted = new uint256[](_tokens[_key].length); +// for (uint index = 0; index < _tokens[_key].length; index++) { +// _whenMinted[index] = _tokens[_key][index].mintBlock; +// } +// return _whenMinted; +// } +// +// function getMintedEntries(address owner, uint256 tokenId) public view returns (MintedTokens[] memory) { +// return _tokens[_getTokenKey(owner, tokenId)]; +// } +// +// function _getTokenKey(address account, uint256 tokenId) internal pure returns (bytes32) { +// return keccak256(abi.encode(account, tokenId)); +// } + + function mintBatch( + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) external { + require(ids.length == amounts.length, 'mintBatch: lengths do not match'); + for (uint i = 0; i < ids.length; i++) { + mint(to, ids[i], amounts[i], data); + } + } + + function burnBatch( + address from, + uint256[] memory ids, + uint256[] memory amounts + ) external { + require(ids.length == amounts.length, 'burnBatch: lengths do not match'); + for (uint i = 0; i < ids.length; i++) { + burn(from, ids[i], amounts[i]); + } + } +} diff --git a/test/unit/token/NFT1155SubscriptionWithoutBlocks.Test.js b/test/unit/token/NFT1155SubscriptionWithoutBlocks.Test.js new file mode 100644 index 00000000..2c086194 --- /dev/null +++ b/test/unit/token/NFT1155SubscriptionWithoutBlocks.Test.js @@ -0,0 +1,181 @@ +/* eslint-env mocha */ +/* eslint-disable no-console */ +/* global artifacts, contract, describe, it */ + +const chai = require('chai') +const { assert } = chai +const chaiAsPromised = require('chai-as-promised') +chai.use(chaiAsPromised) +const { ethers } = require('hardhat') + +const DIDRegistry = artifacts.require('DIDRegistry') +const TestERC1155 = artifacts.require('NFT1155SubscriptionWithoutBlocks') + +const testUtils = require('../../helpers/utils.js') +const constants = require('../../helpers/constants.js') +const increaseTime = require('../../helpers/increaseTime.js') +const BigNumber = require('bignumber.js') + +contract('NFT1155 Subscription', (accounts) => { + const web3 = global.web3 + + const didSeedExpiring = testUtils.generateId() + const didSeedNonExpiring = testUtils.generateId() + + let tokenIdExpiring + let tokenIdNonExpiring + + const amount = 1 + const blocksExpiring = 10 + const blocksNonExpiring = 0 + const data = '0x' + + const checksum = testUtils.generateId() + const url = 'https://raw.githubusercontent.com/nevermined-io/assets/main/images/logo/banner_logo.png' + + const [ + owner, + deployer, + minter, + account1, + account2 + ] = accounts + + let nft + let didRegistry + + async function setupTest() { + const config = await artifacts.require('NeverminedConfig').new() + await config.initialize(owner, owner, true) + didRegistry = await DIDRegistry.new() + await didRegistry.initialize(owner, constants.address.zero, constants.address.zero, config.address, constants.address.zero) + + nft = await TestERC1155.new({ from: deployer }) + await nft.initialize(owner, didRegistry.address, 'TestERC1155', 'TEST', '', config.address, { from: owner }) + + await nft.setNvmConfigAddress(config.address, { from: owner }) + await config.grantNVMOperatorRole(didRegistry.address, { from: owner }) + await config.grantNVMOperatorRole(owner, { from: owner }) + await config.grantNVMOperatorRole(minter, { from: owner }) + } + + describe('Providers can burn', () => { + const initialAmount = 10 + let tokenId + + it('As a minter can register a DID without providers', async () => { + await setupTest() + const didSeed = testUtils.generateId() + + tokenId = await didRegistry.hashDID(didSeed, minter) + await didRegistry.methods[ + 'registerMintableDID(bytes32,address,bytes32,address[],string,uint256,uint256,bool,bytes32,string,string)' + ](didSeed, nft.address, checksum, [], url, 0, 0, false, constants.activities.GENERATED, '', '', { from: minter }) + + await nft.methods[ + 'mint(address,uint256,uint256,bytes)' + ](account1, tokenId, initialAmount, data, { from: minter }) + + const balance = new BigNumber(await nft.balanceOf(account1, tokenId)) + assert.strictEqual(balance.toNumber(), initialAmount) + }) + + it('NFT holder can burn', async () => { + await nft.methods[ + 'burn(address,uint256,uint256)' + ](account1, tokenId, 1, { from: account1 }) + + const balance = new BigNumber(await nft.balanceOf(account1, tokenId)) + assert.strictEqual(balance.toNumber(), initialAmount - 1) + }) + + it('Account can not burn unless is a provider', async () => { + await assert.isRejected( + nft.methods[ + 'burn(address,uint256,uint256)' + ](account1, tokenId, 1, { from: account2 }), + 'ERC1155: caller is not owner nor approved' + ) + + let balance = new BigNumber(await nft.balanceOf(account1, tokenId)) + assert.strictEqual(balance.toNumber(), initialAmount - 1) + + await didRegistry.addDIDProvider(tokenId, account2, { from: minter }) + + await nft.methods[ + 'burn(address,uint256,uint256)' + ](account1, tokenId, 1, { from: account2 }) + + balance = new BigNumber(await nft.balanceOf(account1, tokenId)) + assert.strictEqual(balance.toNumber(), initialAmount - 2) + }) + }) + + + describe('Mint and burn', () => { + it('New tokens can be minted and burned', async () => { + await setupTest() + + let balance + const didSeed3 = testUtils.generateId() + const tokenId3 = await didRegistry.hashDID(didSeed3, minter) + await didRegistry.methods[ + 'registerMintableDID(bytes32,address,bytes32,address[],string,uint256,uint256,bool,bytes32,string,string)' + ](didSeed3, nft.address, checksum, [], url, 0, 0, false, constants.activities.GENERATED, '', '', { from: minter }) + + const currentBlockNumber = await ethers.provider.getBlockNumber() + + // MINT 7 tokens + await nft.methods[ + 'mint(address,uint256,uint256,bytes)' + ](account2, tokenId3, 7, data, { from: minter }) + + // Balance is 7 + balance = new BigNumber(await nft.balanceOf(account2, tokenId3)) + assert.strictEqual(balance.toNumber(), 7) + + // MINT 10 tokens + await nft.methods[ + 'mint(address,uint256,uint256,bytes)' + ](account2, tokenId3, 10, data, { from: minter }) + + // Balance is 17 + balance = new BigNumber(await nft.balanceOf(account2, tokenId3)) + assert.strictEqual(balance.toNumber(), 17) + + // BURN 4 tokens + await nft.methods[ + 'burn(address,uint256,uint256)' + ](account2, tokenId3, 4, { from: minter }) + + // Balance is 13 + balance = new BigNumber(await nft.balanceOf(account2, tokenId3)) + assert.strictEqual(balance.toNumber(), 13) + + // Batch mint + const didSeed4 = testUtils.generateId() + const tokenId4 = await didRegistry.hashDID(didSeed4, minter) + await didRegistry.methods[ + 'registerMintableDID(bytes32,address,bytes32,address[],string,uint256,uint256,bool,bytes32,string,string)' + ](didSeed4, nft.address, checksum, [], url, 0, 0, false, constants.activities.GENERATED, '', '', { from: minter }) + + // Also test balance batch + await nft.mintBatch(account2, [tokenId3, tokenId4], [10, 15], data, { from: minter }) + balance = new BigNumber(await nft.balanceOf(account2, tokenId3)) + let balance2 = new BigNumber(await nft.balanceOf(account2, tokenId4)) + assert.strictEqual(balance.toNumber(), 23) + assert.strictEqual(balance2.toNumber(), 15) + + const balances = (await nft.balanceOfBatch([account2, account2], [tokenId3, tokenId4])).map(a => new BigNumber(a).toNumber()) + assert.strictEqual(balances[0], 23) + assert.strictEqual(balances[1], 15) + + await nft.burnBatch(account2, [tokenId3, tokenId4], [22, 14], { from: minter }) + balance = new BigNumber(await nft.balanceOf(account2, tokenId3)) + balance2 = new BigNumber(await nft.balanceOf(account2, tokenId4)) + assert.strictEqual(balance.toNumber(), 1) + assert.strictEqual(balance2.toNumber(), 1) + }) + + }) +})