--- a/pallets/refungible/src/erc.rs +++ b/pallets/refungible/src/erc.rs @@ -34,9 +34,8 @@ CommonEvmHandler, CollectionCall, static_property::{key, value as property_value}, }, - eth::collection_id_to_address, }; -use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm, PrecompileHandle}; +use pallet_evm::{account::CrossAccountId, PrecompileHandle}; use pallet_evm_coder_substrate::{call, dispatch_to_evm}; use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _}; use sp_core::H160; @@ -51,6 +50,8 @@ TokenProperties, TokensMinted, TotalSupply, weights::WeightInfo, }; +pub const ADDRESS_FOR_PARTIALLY_OWNED_TOKENS: H160 = H160::repeat_byte(0xff); + /// @title A contract that allows to set and delete token properties and change token property permissions. #[solidity_interface(name = "TokenProperties")] impl RefungibleHandle { @@ -298,13 +299,20 @@ Ok(balance.into()) } + /// @notice Find the owner of an RFT + /// @dev RFTs assigned to zero address are considered invalid, and queries + /// about them do throw. + /// Returns special 0xffffffffffffffffffffffffffffffffffffffff address for + /// the tokens that are partially owned. + /// @param tokenId The identifier for an RFT + /// @return The address of the owner of the RFT fn owner_of(&self, token_id: uint256) -> Result
{ self.consume_store_reads(2)?; let token = token_id.try_into()?; let owner = >::token_owner(self.id, token); Ok(owner .map(|address| *address.as_eth()) - .unwrap_or_else(|| H160::default())) + .unwrap_or_else(|| ADDRESS_FOR_PARTIALLY_OWNED_TOKENS)) } /// @dev Not implemented @@ -366,14 +374,6 @@ >::transfer_from(self, &caller, &from, &to, token, balance, &budget) .map_err(dispatch_to_evm::)?; - >::deposit_log( - ERC721Events::Transfer { - from: *from.as_eth(), - to: *to.as_eth(), - token_id: token_id.into(), - } - .to_log(collection_id_to_address(self.id)), - ); Ok(()) } @@ -652,14 +652,6 @@ >::transfer(self, &caller, &to, token, balance, &budget) .map_err(dispatch_to_evm::)?; - >::deposit_log( - ERC721Events::Transfer { - from: *caller.as_eth(), - to: *to.as_eth(), - token_id: token_id.into(), - } - .to_log(collection_id_to_address(self.id)), - ); Ok(()) } --- a/pallets/refungible/src/lib.rs +++ b/pallets/refungible/src/lib.rs @@ -707,12 +707,13 @@ } >::ensure_correct_receiver(to)?; - let balance_from = >::get((collection.id, token, from)) + let initial_balance_from = >::get((collection.id, token, from)); + let updated_balance_from = initial_balance_from .checked_sub(amount) .ok_or(>::TokenValueTooLow)?; let mut create_target = false; let from_to_differ = from != to; - let balance_to = if from != to { + let updated_balance_to = if from != to { let old_balance = >::get((collection.id, token, to)); if old_balance == 0 { create_target = true; @@ -726,7 +727,7 @@ None }; - let account_balance_from = if balance_from == 0 { + let account_balance_from = if updated_balance_from == 0 { Some( >::get((collection.id, from)) .checked_sub(1) @@ -762,15 +763,15 @@ nesting_budget, )?; - if let Some(balance_to) = balance_to { + if let Some(updated_balance_to) = updated_balance_to { // from != to - if balance_from == 0 { + if updated_balance_from == 0 { >::remove((collection.id, token, from)); >::unnest_if_nested(from, collection.id, token); } else { - >::insert((collection.id, token, from), balance_from); + >::insert((collection.id, token, from), updated_balance_from); } - >::insert((collection.id, token, to), balance_to); + >::insert((collection.id, token, to), updated_balance_to); if let Some(account_balance_from) = account_balance_from { >::insert((collection.id, from), account_balance_from); >::remove((collection.id, from, token)); @@ -800,6 +801,46 @@ to.clone(), amount, )); + + let total_supply = >::get((collection.id, token)); + + if amount == total_supply { + // if token was fully owned by `from` and will be fully owned by `to` after transfer + >::deposit_log( + ERC721Events::Transfer { + from: *from.as_eth(), + to: *to.as_eth(), + token_id: token.into(), + } + .to_log(collection_id_to_address(collection.id)), + ); + } else if let Some(updated_balance_to) = updated_balance_to { + // if `from` not equals `to`. This condition is needed to avoid sending event + // when `from` fully owns token and sends part of token pieces to itself. + if initial_balance_from == total_supply { + // if token was fully owned by `from` and will be only partially owned by `to` + // and `from` after transfer + >::deposit_log( + ERC721Events::Transfer { + from: *from.as_eth(), + to: erc::ADDRESS_FOR_PARTIALLY_OWNED_TOKENS, + token_id: token.into(), + } + .to_log(collection_id_to_address(collection.id)), + ); + } else if updated_balance_to == total_supply { + // if token was partially owned by `from` and will be fully owned by `to` after transfer + >::deposit_log( + ERC721Events::Transfer { + from: erc::ADDRESS_FOR_PARTIALLY_OWNED_TOKENS, + to: *to.as_eth(), + token_id: token.into(), + } + .to_log(collection_id_to_address(collection.id)), + ); + } + } + Ok(()) } --- a/tests/src/eth/reFungible.test.ts +++ b/tests/src/eth/reFungible.test.ts @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . -import {createCollectionExpectSuccess, UNIQUE} from '../util/helpers'; +import {createCollectionExpectSuccess, transfer, UNIQUE} from '../util/helpers'; import {collectionIdToAddress, createEthAccount, createEthAccountWithBalance, evmCollection, evmCollectionHelpers, GAS_ARGS, getCollectionAddressFromResult, itWeb3, normalizeEvents, recordEthFee, tokenIdToAddress} from './util/helpers'; import reFungibleAbi from './reFungibleAbi.json'; import reFungibleTokenAbi from './reFungibleTokenAbi.json'; @@ -96,6 +96,28 @@ expect(owner).to.equal(receiver); }); + + itWeb3('ownerOf for partial ownership', async ({api, web3, privateKeyWrapper}) => { + const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const receiver = createEthAccount(web3); + const helper = evmCollectionHelpers(web3, caller); + const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send(); + const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); + const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'}); + + const tokenId = await contract.methods.nextTokenId().call(); + await contract.methods.mint(caller, tokenId).send(); + + const tokenAddress = tokenIdToAddress(collectionId, tokenId); + const tokenContract = new web3.eth.Contract(reFungibleTokenAbi as any, tokenAddress, {from: caller, ...GAS_ARGS}); + + await tokenContract.methods.repartition(2).send(); + await tokenContract.methods.transfer(receiver, 1).send(); + + const owner = await contract.methods.ownerOf(tokenId).call(); + + expect(owner).to.equal('0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF'); + }); }); describe('Refungible: Plain calls', () => { @@ -293,6 +315,74 @@ expect(+balance).to.equal(1); } }); + + itWeb3('transfer event on transfer from partial ownership to full ownership', async ({api, web3, privateKeyWrapper}) => { + const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const receiver = createEthAccount(web3); + const helper = evmCollectionHelpers(web3, caller); + const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send(); + const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); + const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'}); + + const tokenId = await contract.methods.nextTokenId().call(); + await contract.methods.mint(caller, tokenId).send(); + + const tokenAddress = tokenIdToAddress(collectionId, tokenId); + const tokenContract = new web3.eth.Contract(reFungibleTokenAbi as any, tokenAddress, {from: caller, ...GAS_ARGS}); + + await tokenContract.methods.repartition(2).send(); + await tokenContract.methods.transfer(receiver, 1).send(); + + let transfer; + contract.events.Transfer({}, function(_error: any, event: any){ transfer = event;}); + await tokenContract.methods.transfer(receiver, 1).send(); + const events = normalizeEvents([transfer]); + expect(events).to.deep.equal([ + { + address: collectionIdAddress, + event: 'Transfer', + args: { + from: '0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF', + to: receiver, + tokenId: tokenId.toString(), + }, + }, + ]); + }); + + itWeb3('transfer event on transfer from full ownership to partial ownership', async ({api, web3, privateKeyWrapper}) => { + const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const receiver = createEthAccount(web3); + const helper = evmCollectionHelpers(web3, caller); + const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send(); + const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); + const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'}); + + const tokenId = await contract.methods.nextTokenId().call(); + await contract.methods.mint(caller, tokenId).send(); + + const tokenAddress = tokenIdToAddress(collectionId, tokenId); + const tokenContract = new web3.eth.Contract(reFungibleTokenAbi as any, tokenAddress, {from: caller, ...GAS_ARGS}); + + await tokenContract.methods.repartition(2).send(); + + let transfer; + contract.events.Transfer({}, function(_error: any, event: any){ transfer = event;}); + await tokenContract.methods.transfer(receiver, 1).send(); + + const events = normalizeEvents([transfer]); + expect(events).to.deep.equal([ + { + address: collectionIdAddress, + event: 'Transfer', + args: { + from: caller, + to: '0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF', + tokenId: tokenId.toString(), + }, + }, + ]); + }); }); describe('RFT: Fees', () => {