difftreelog
feat(refungible-pallet) fractionalizer contract
in: master
7 files changed
tests/package.jsondiffbeforeafterboth--- a/tests/package.json
+++ b/tests/package.json
@@ -8,6 +8,7 @@
"@polkadot/typegen": "8.7.2-15",
"@types/chai": "^4.3.1",
"@types/chai-as-promised": "^7.1.5",
+ "@types/chai-like": "^1.1.1",
"@types/mocha": "^9.1.1",
"@types/node": "^17.0.35",
"@typescript-eslint/eslint-plugin": "^5.26.0",
@@ -96,6 +97,7 @@
"@polkadot/util-crypto": "9.4.1",
"bignumber.js": "^9.0.2",
"chai-as-promised": "^7.1.1",
+ "chai-like": "^1.1.1",
"find-process": "^1.4.7",
"solc": "0.8.14-fixed",
"web3": "^1.7.3"
tests/src/eth/fractionalizer/Fractionalizer.bindiffbeforeafterbothno changes
tests/src/eth/fractionalizer/Fractionalizer.soldiffbeforeafterboth--- /dev/null
+++ b/tests/src/eth/fractionalizer/Fractionalizer.sol
@@ -0,0 +1,144 @@
+// SPDX-License-Identifier: Apache License
+pragma solidity >=0.8.0;
+import {CollectionHelpers} from "../api/CollectionHelpers.sol";
+import {ContractHelpers} from "../api/ContractHelpers.sol";
+import {UniqueRefungibleToken} from "../api/UniqueRefungibleToken.sol";
+import {UniqueRefungible} from "../api/UniqueRefungible.sol";
+import {UniqueNFT} from "../api/UniqueNFT.sol";
+
+contract Fractionalizer {
+ struct Token {
+ address _collection;
+ uint256 _tokenId;
+ }
+ address rftCollection;
+ mapping(address => bool) nftCollectionAllowList;
+ mapping(address => mapping(uint256 => uint256)) nft2rftMapping;
+ mapping(address => Token) rft2nftMapping;
+ address owner;
+ bytes32 refungibleCollectionType = keccak256(bytes("ReFungible"));
+
+ constructor() {
+ owner = msg.sender;
+ }
+
+ modifier onlyOwner() {
+ require(msg.sender == owner, "Only owner can");
+ _;
+ }
+
+ event RFTCollectionSet(address _collection);
+ event AllowListSet(address _collection, bool _status);
+ event Fractionalized(address _collection, uint256 _tokenId, address _rftToken, uint128 _amount);
+ event DeFractionalized(address _rftToken, address _nftCollection, uint256 _nftTokenId);
+
+ function setRFTCollection(address _collection) public onlyOwner {
+ require(
+ rftCollection == address(0),
+ "RFT collection is already set"
+ );
+ UniqueRefungible refungibleContract = UniqueRefungible(_collection);
+ string memory collectionType = refungibleContract.uniqueCollectionType();
+
+ require(
+ keccak256(bytes(collectionType)) == refungibleCollectionType,
+ "Fractionalizer contract should be an admin of the collection"
+ );
+ require(
+ refungibleContract.verifyOwnerOrAdmin(),
+ "Fractionalizer contract should be an admin of the collection"
+ );
+ rftCollection = _collection;
+ emit RFTCollectionSet(rftCollection);
+ }
+
+ function mintRFTCollection(string calldata _name, string calldata _description, string calldata _tokenPrefix) public onlyOwner {
+ require(
+ rftCollection == address(0),
+ "RFT collection is already set"
+ );
+ address collectionHelpers = 0x6C4E9fE1AE37a41E93CEE429e8E1881aBdcbb54F;
+ rftCollection = CollectionHelpers(collectionHelpers).createRefungibleCollection(_name, _description, _tokenPrefix);
+ emit RFTCollectionSet(rftCollection);
+ }
+
+ function setAllowlist(address collection, bool status) public onlyOwner {
+ nftCollectionAllowList[collection] = status;
+ emit AllowListSet(collection, status);
+ }
+
+ function nft2rft(address _collection, uint256 _token, uint128 _pieces) public {
+ require(
+ rftCollection != address(0),
+ "RFT collection is not set"
+ );
+ UniqueRefungible rftCollectionContract = UniqueRefungible(rftCollection);
+ require(
+ UniqueNFT(_collection).ownerOf(_token) == msg.sender,
+ "Only token owner could fractionalize it"
+ );
+ require(
+ nftCollectionAllowList[_collection] == true,
+ "Fractionalization of this collection is not allowed by admin"
+ );
+ require(
+ UniqueNFT(_collection).ownerOf(_token) == msg.sender,
+ "Only token owner could fractionalize it"
+ );
+ UniqueNFT(_collection).transferFrom(
+ msg.sender,
+ address(this),
+ _token
+ );
+ uint256 rftTokenId;
+ address rftTokenAddress;
+ UniqueRefungibleToken rftTokenContract;
+ if (nft2rftMapping[_collection][_token] == 0) {
+ rftTokenId = rftCollectionContract.nextTokenId();
+ rftCollectionContract.mint(address(this), rftTokenId);
+ rftTokenAddress = rftCollectionContract.tokenContractAddress(rftTokenId);
+ nft2rftMapping[_collection][_token] = rftTokenId;
+ rft2nftMapping[rftTokenAddress] = Token(_collection, _token);
+
+ rftTokenContract = UniqueRefungibleToken(rftTokenAddress);
+ rftTokenContract.setParentNFT(_collection, _token);
+ } else {
+ rftTokenId = nft2rftMapping[_collection][_token];
+ rftTokenAddress = rftCollectionContract.tokenContractAddress(rftTokenId);
+ rftTokenContract = UniqueRefungibleToken(rftTokenAddress);
+ }
+ rftTokenContract.repartition(_pieces);
+ rftTokenContract.transfer(msg.sender, _pieces);
+ emit Fractionalized(_collection, _token, rftTokenAddress, _pieces);
+ }
+
+ function rft2nft(address _collection, uint256 _token) public {
+ require(
+ rftCollection != address(0),
+ "RFT collection is not set"
+ );
+ require(
+ rftCollection == _collection,
+ "Wrong RFT collection"
+ );
+ UniqueRefungible rftCollectionContract = UniqueRefungible(rftCollection);
+ address rftTokenAddress = rftCollectionContract.tokenContractAddress(_token);
+ Token memory nftToken = rft2nftMapping[rftTokenAddress];
+ require(
+ nftToken._collection != address(0),
+ "No corresponding NFT token found"
+ );
+ UniqueRefungibleToken rftTokenContract = UniqueRefungibleToken(rftTokenAddress);
+ require(
+ rftTokenContract.balanceOf(msg.sender) == rftTokenContract.totalSupply(),
+ "Not all pieces are owned by the caller"
+ );
+ rftCollectionContract.transferFrom(msg.sender, address(this), _token);
+ UniqueNFT(nftToken._collection).transferFrom(
+ address(this),
+ msg.sender,
+ nftToken._tokenId
+ );
+ emit DeFractionalized(rftTokenAddress, nftToken._collection, nftToken._tokenId);
+ }
+}
\ No newline at end of file
tests/src/eth/fractionalizer/FractionalizerAbi.jsondiffbeforeafterboth--- /dev/null
+++ b/tests/src/eth/fractionalizer/FractionalizerAbi.json
@@ -0,0 +1 @@
+[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"_collection","type":"address"},{"indexed":false,"internalType":"bool","name":"_status","type":"bool"}],"name":"AllowListSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"_rftToken","type":"address"},{"indexed":false,"internalType":"address","name":"_nftCollection","type":"address"},{"indexed":false,"internalType":"uint256","name":"_nftTokenId","type":"uint256"}],"name":"DeFractionalized","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"_collection","type":"address"},{"indexed":false,"internalType":"uint256","name":"_tokenId","type":"uint256"},{"indexed":false,"internalType":"address","name":"_rftToken","type":"address"},{"indexed":false,"internalType":"uint128","name":"_amount","type":"uint128"}],"name":"Fractionalized","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"_collection","type":"address"}],"name":"RFTCollectionSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"msg","type":"string"}],"name":"Test","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes","name":"msg","type":"bytes"}],"name":"TestB","type":"event"},{"inputs":[{"internalType":"string","name":"_name","type":"string"},{"internalType":"string","name":"_description","type":"string"},{"internalType":"string","name":"_tokenPrefix","type":"string"}],"name":"mintRFTCollection","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_collection","type":"address"},{"internalType":"uint256","name":"_token","type":"uint256"},{"internalType":"uint128","name":"_pieces","type":"uint128"}],"name":"nft2rft","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_collection","type":"address"},{"internalType":"uint256","name":"_token","type":"uint256"}],"name":"rft2nft","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"collection","type":"address"},{"internalType":"bool","name":"status","type":"bool"}],"name":"setAllowlist","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_collection","type":"address"}],"name":"setRFTCollection","outputs":[],"stateMutability":"nonpayable","type":"function"}]
\ No newline at end of file
tests/src/eth/fractionalizer/fractionalizer.test.tsdiffbeforeafterboth--- /dev/null
+++ b/tests/src/eth/fractionalizer/fractionalizer.test.ts
@@ -0,0 +1,176 @@
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// This file is part of Unique Network.
+
+// Unique Network is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Unique Network is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
+
+
+import Web3 from 'web3';
+import {ApiPromise} from '@polkadot/api';
+import {evmToAddress} from '@polkadot/util-crypto';
+import {readFile} from 'fs/promises';
+import fractionalizerAbi from './FractionalizerAbi.json';
+import {submitTransactionAsync} from '../../substrate/substrate-api';
+import {UNIQUE} from '../../util/helpers';
+import {collectionIdToAddress, createEthAccountWithBalance, createNonfungibleCollection, createRefungibleCollection, GAS_ARGS, itWeb3, tokenIdFromAddress, uniqueNFT, uniqueRefungible, uniqueRefungibleToken} from '../util/helpers';
+import {Contract} from 'web3-eth-contract';
+
+import chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
+import chaiLike from 'chai-like';
+import {IKeyringPair} from '@polkadot/types/types';
+chai.use(chaiAsPromised);
+chai.use(chaiLike);
+const expect = chai.expect;
+
+async function deployFractionalizer(api: ApiPromise, web3: Web3, owner: string) {
+ const fractionalizerContract = new web3.eth.Contract(fractionalizerAbi as any, undefined, {
+ from: owner,
+ ...GAS_ARGS,
+ });
+ return await fractionalizerContract.deploy({data: (await readFile(`${__dirname}/Fractionalizer.bin`)).toString()}).send({from: owner});
+}
+
+async function initFractionalizer(api: ApiPromise, web3: Web3, privateKeyWrapper: (account: string) => IKeyringPair, owner: string) {
+ const fractionalizer = await deployFractionalizer(api, web3, owner);
+ const tx = api.tx.balances.transfer(evmToAddress(fractionalizer.options.address), 10n * UNIQUE);
+ const alice = privateKeyWrapper('//Alice');
+ await submitTransactionAsync(alice, tx);
+ const result = await fractionalizer.methods.mintRFTCollection('A', 'B', 'C').send();
+ const rftCollectionAddress = result.events.RFTCollectionSet.returnValues._collection;
+ return {fractionalizer, rftCollectionAddress};
+}
+
+async function createRFTToken(api: ApiPromise, web3: Web3, owner: string, fractionalizer: Contract, amount: bigint) {
+ const {collectionIdAddress: nftCollectionAddress} = await createNonfungibleCollection(api, web3, owner);
+ const nftContract = uniqueNFT(web3, nftCollectionAddress, owner);
+ const nftTokenId = await nftContract.methods.nextTokenId().call();
+ await nftContract.methods.mint(owner, nftTokenId).send();
+
+ await fractionalizer.methods.setAllowlist(nftCollectionAddress, true).send();
+ await nftContract.methods.approve(fractionalizer.options.address, nftTokenId).send();
+ const result = await fractionalizer.methods.nft2rft(nftCollectionAddress, nftTokenId, amount).send();
+ const {_collection, _tokenId, _rftToken} = result.events.Fractionalized.returnValues;
+ return {
+ nftCollectionAddress: _collection,
+ nftTokenId: _tokenId,
+ rftTokenAddress: _rftToken,
+ };
+}
+
+describe('Fractionalizer contract usage', () => {
+ itWeb3('Set RFT collection', async ({api, web3, privateKeyWrapper}) => {
+ const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+ const fractionalizer = await deployFractionalizer(api, web3, owner);
+ const {collectionIdAddress} = await createRefungibleCollection(api, web3, owner);
+ const refungibleContract = uniqueRefungible(web3, collectionIdAddress, owner);
+ await refungibleContract.methods.addCollectionAdmin(fractionalizer.options.address).send();
+ const result = await fractionalizer.methods.setRFTCollection(collectionIdAddress).send();
+ expect(result.events).to.be.like({
+ RFTCollectionSet: {
+ returnValues: {
+ _collection: collectionIdAddress,
+ },
+ },
+ });
+ });
+
+ itWeb3('Mint RFT collection', async ({api, web3, privateKeyWrapper}) => {
+ const alice = privateKeyWrapper('//Alice');
+ const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+ const fractionalizer = await deployFractionalizer(api, web3, owner);
+ const tx = api.tx.balances.transfer(evmToAddress(fractionalizer.options.address), 10n * UNIQUE);
+ await submitTransactionAsync(alice, tx);
+
+ const result = await fractionalizer.methods.mintRFTCollection('A', 'B', 'C').send({from: owner});
+ expect(result.events).to.be.like({
+ RFTCollectionSet: {},
+ });
+ expect(result.events.RFTCollectionSet.returnValues._collection).to.be.ok;
+ });
+
+ itWeb3('Set Allowlist', async ({api, web3, privateKeyWrapper}) => {
+ const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+ const {fractionalizer} = await initFractionalizer(api, web3, privateKeyWrapper, owner);
+
+ const {collectionIdAddress: nftCollectionAddress} = await createNonfungibleCollection(api, web3, owner);
+ const result1 = await fractionalizer.methods.setAllowlist(nftCollectionAddress, true).send({from: owner});
+ expect(result1.events).to.be.like({
+ AllowListSet: {
+ returnValues: {
+ _collection: nftCollectionAddress,
+ _status: true,
+ },
+ },
+ });
+ const result2 = await fractionalizer.methods.setAllowlist(nftCollectionAddress, false).send({from: owner});
+ expect(result2.events).to.be.like({
+ AllowListSet: {
+ returnValues: {
+ _collection: nftCollectionAddress,
+ _status: false,
+ },
+ },
+ });
+ });
+
+ itWeb3('NFT to RFT', async ({api, web3, privateKeyWrapper}) => {
+ const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+
+ const {collectionIdAddress: nftCollectionAddress} = await createNonfungibleCollection(api, web3, owner);
+ const nftContract = uniqueNFT(web3, nftCollectionAddress, owner);
+ const nftTokenId = await nftContract.methods.nextTokenId().call();
+ await nftContract.methods.mint(owner, nftTokenId).send();
+
+ const {fractionalizer} = await initFractionalizer(api, web3, privateKeyWrapper, owner);
+
+ await fractionalizer.methods.setAllowlist(nftCollectionAddress, true).send();
+ await nftContract.methods.approve(fractionalizer.options.address, nftTokenId).send();
+ const result = await fractionalizer.methods.nft2rft(nftCollectionAddress, nftTokenId, 100).send();
+ expect(result.events).to.be.like({
+ Fractionalized: {
+ returnValues: {
+ _collection: nftCollectionAddress,
+ _tokenId: nftTokenId,
+ _amount: '100',
+ },
+ },
+ });
+ const rftTokenAddress = result.events.Fractionalized.returnValues._rftToken;
+ const rftTokenContract = uniqueRefungibleToken(web3, rftTokenAddress, owner);
+ expect(await rftTokenContract.methods.balanceOf(owner).call()).to.equal('100');
+ });
+
+ itWeb3('RFT to NFT', async ({api, web3, privateKeyWrapper}) => {
+ const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+
+ const {fractionalizer, rftCollectionAddress} = await initFractionalizer(api, web3, privateKeyWrapper, owner);
+ const {rftTokenAddress, nftCollectionAddress, nftTokenId} = await createRFTToken(api, web3, owner, fractionalizer, 100n);
+
+ const {collectionId, tokenId} = tokenIdFromAddress(rftTokenAddress);
+ const refungibleAddress = collectionIdToAddress(collectionId);
+ expect(rftCollectionAddress).to.be.equal(refungibleAddress);
+ const refungibleTokenContract = uniqueRefungibleToken(web3, rftTokenAddress, owner);
+ await refungibleTokenContract.methods.approve(fractionalizer.options.address, 100).send();
+ const result = await fractionalizer.methods.rft2nft(refungibleAddress, tokenId).send();
+ expect(result.events).to.be.like({
+ DeFractionalized: {
+ returnValues: {
+ _rftToken: rftTokenAddress,
+ _nftCollection: nftCollectionAddress,
+ _nftTokenId: nftTokenId,
+ },
+ },
+ });
+ });
+});
\ No newline at end of file
tests/src/eth/util/helpers.tsdiffbeforeafterboth--- a/tests/src/eth/util/helpers.ts
+++ b/tests/src/eth/util/helpers.ts
@@ -32,6 +32,7 @@
import fungibleAbi from '../fungibleAbi.json';
import nonFungibleAbi from '../nonFungibleAbi.json';
import refungibleAbi from '../reFungibleAbi.json';
+import refungibleTokenAbi from '../reFungibleTokenAbi.json';
import contractHelpersAbi from './contractHelpersAbi.json';
export const GAS_ARGS = {gas: 2500000};
@@ -101,6 +102,18 @@
]);
return Web3.utils.toChecksumAddress('0x' + buf.toString('hex'));
}
+
+export function tokenIdFromAddress(address: string) {
+ if (!address.startsWith('0x'))
+ throw 'address not starts with "0x"';
+ if (address.length > 42)
+ throw 'address length is more than 20 bytes';
+ return {
+ collectionId: Number('0x' + address.substring(address.length - 16, address.length - 8)),
+ tokenId: Number('0x' + address.substring(address.length - 8)),
+ };
+}
+
export function tokenIdToCross(collection: number, token: number): CrossAccountId {
return {
Ethereum: tokenIdToAddress(collection, token),
@@ -128,6 +141,44 @@
expect(result.success).to.be.true;
}
+export async function createRefungibleCollection(api: ApiPromise, web3: Web3, owner: string) {
+ const collectionHelper = evmCollectionHelpers(web3, owner);
+ const result = await collectionHelper.methods
+ .createRefungibleCollection('A', 'B', 'C')
+ .send();
+ return await getCollectionAddressFromResult(api, result);
+}
+
+
+export async function createNonfungibleCollection(api: ApiPromise, web3: Web3, owner: string) {
+ const collectionHelper = evmCollectionHelpers(web3, owner);
+ const result = await collectionHelper.methods
+ .createNonfungibleCollection('A', 'B', 'C')
+ .send();
+ return await getCollectionAddressFromResult(api, result);
+}
+
+export function uniqueNFT(web3: Web3, address: string, owner: string) {
+ return new web3.eth.Contract(nonFungibleAbi as any, address, {
+ from: owner,
+ ...GAS_ARGS,
+ });
+}
+
+export function uniqueRefungible(web3: Web3, collectionAddress: string, owner: string) {
+ return new web3.eth.Contract(refungibleAbi as any, collectionAddress, {
+ from: owner,
+ ...GAS_ARGS,
+ });
+}
+
+export function uniqueRefungibleToken(web3: Web3, tokenAddress: string, owner: string) {
+ return new web3.eth.Contract(refungibleTokenAbi as any, tokenAddress, {
+ from: owner,
+ ...GAS_ARGS,
+ });
+}
+
export async function itWeb3(name: string, cb: (apis: { web3: Web3, api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair }) => any, opts: { only?: boolean, skip?: boolean } = {}) {
let i: any = it;
if (opts.only) i = i.only;
tests/yarn.lockdiffbeforeafterboth--- a/tests/yarn.lock
+++ b/tests/yarn.lock
@@ -963,6 +963,20 @@
dependencies:
"@types/chai" "*"
+"@types/chai-like@^1.1.1":
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@types/chai-like/-/chai-like-1.1.1.tgz#c454039b0a2f92664fb5b7b7a2a66c3358783ae7"
+ integrity sha512-s46EZsupBuVhLn66DbRee5B0SELLmL4nFXVrBiV29BxLGm9Sh7Bful623j3AfiQRu2zAP4cnlZ3ETWB3eWc4bA==
+ dependencies:
+ "@types/chai" "*"
+
+"@types/chai-things@^0.0.35":
+ version "0.0.35"
+ resolved "https://registry.yarnpkg.com/@types/chai-things/-/chai-things-0.0.35.tgz#4b5d9ec032067faa62b3bf7bb40dc0bec941945f"
+ integrity sha512-BC8FwMf9FHj87XT4dgTwbdb8dNRilGqYWGmwLPdJ54YNk6K2PlcFTt68NGHjgPDnms8zIYcOtmPePd0mPNTo/Q==
+ dependencies:
+ "@types/chai" "*"
+
"@types/chai@*", "@types/chai@^4.3.1":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.1.tgz#e2c6e73e0bdeb2521d00756d099218e9f5d90a04"
@@ -1545,6 +1559,16 @@
dependencies:
check-error "^1.0.2"
+chai-like@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/chai-like/-/chai-like-1.1.1.tgz#8c558a414c34514e814d497c772547ceb7958f64"
+ integrity sha512-VKa9z/SnhXhkT1zIjtPACFWSoWsqVoaz1Vg+ecrKo5DCKVlgL30F/pEyEvXPBOVwCgLZcWUleCM/C1okaKdTTA==
+
+chai-things@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/chai-things/-/chai-things-0.2.0.tgz#c55128378f9bb399e994f00052151984ed6ebe70"
+ integrity sha512-6ns0SU21xdRCoEXVKH3HGbwnsgfVMXQ+sU5V8PI9rfxaITos8lss1vUxbF1FAcJKjfqmmmLVlr/z3sLes00w+A==
+
chai@^4.3.6:
version "4.3.6"
resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.6.tgz#ffe4ba2d9fa9d6680cc0b370adae709ec9011e9c"