--- a/crates/evm-coder/procedural/src/solidity_interface.rs +++ b/crates/evm-coder/procedural/src/solidity_interface.rs @@ -560,6 +560,7 @@ selector: u32, args: Vec, has_normal_args: bool, + has_value_args: bool, mutability: Mutability, result: Type, weight: Option, @@ -647,14 +648,20 @@ .unwrap_or_else(|| cases::camelcase::to_camel_case(&ident.to_string())); let mut selector_str = camel_name.clone(); selector_str.push('('); - let mut has_normal_args = false; - for (i, arg) in args.iter().filter(|arg| !arg.is_special()).enumerate() { - if i != 0 { - selector_str.push(','); + let mut normal_args_count = 0u32; + let mut has_value_args = false; + for arg in args.iter() { + if arg.is_value() { + has_value_args = true; + } else if !arg.is_special() { + if normal_args_count != 0 { + selector_str.push(','); + } + write!(selector_str, "{}", arg.selector_ty()).unwrap(); + normal_args_count = normal_args_count.saturating_add(1); } - write!(selector_str, "{}", arg.selector_ty()).unwrap(); - has_normal_args = true; } + let has_normal_args = normal_args_count > 0; selector_str.push(')'); let selector = fn_selector_str(&selector_str); @@ -667,6 +674,7 @@ selector, args, has_normal_args, + has_value_args, mutability, result: result.clone(), weight, @@ -823,7 +831,7 @@ let docs = &self.docs; let selector_str = &self.selector_str; let selector = self.selector; - + let is_payable = self.has_value_args; quote! { SolidityFunction { docs: &[#(#docs),*], @@ -831,6 +839,7 @@ selector: #selector, name: #camel_name, mutability: #mutability, + is_payable: #is_payable, args: ( #( #args, --- a/crates/evm-coder/src/solidity.rs +++ b/crates/evm-coder/src/solidity.rs @@ -422,6 +422,7 @@ pub args: A, pub result: R, pub mutability: SolidityMutability, + pub is_payable: bool, } impl SolidityFunctions for SolidityFunction { fn solidity_name( @@ -452,6 +453,9 @@ SolidityMutability::View => write!(writer, " view")?, SolidityMutability::Mutable => {} } + if self.is_payable { + write!(writer, " payable")?; + } if !self.result.is_empty() { write!(writer, " returns (")?; self.result.solidity_name(writer, tc)?; --- a/pallets/common/src/dispatch.rs +++ b/pallets/common/src/dispatch.rs @@ -78,6 +78,7 @@ /// * `data` - Description of the created collection. fn create( sender: T::CrossAccountId, + payer: T::CrossAccountId, data: CreateCollectionData, ) -> Result; --- a/pallets/common/src/lib.rs +++ b/pallets/common/src/lib.rs @@ -866,6 +866,7 @@ /// * `flags` - Extra flags to store. pub fn init_collection( owner: T::CrossAccountId, + payer: T::CrossAccountId, data: CreateCollectionData, flags: CollectionFlags, ) -> Result { @@ -939,7 +940,7 @@ ), ); ::Currency::settle( - owner.as_sub(), + payer.as_sub(), imbalance, WithdrawReasons::TRANSFER, ExistenceRequirement::KeepAlive, --- a/pallets/foreign-assets/src/lib.rs +++ b/pallets/foreign-assets/src/lib.rs @@ -309,9 +309,10 @@ mode: CollectionMode::Fungible(md.decimals), ..Default::default() }; - + let owner = T::CrossAccountId::from_sub(owner); let bounded_collection_id = >::init_foreign_collection( - CrossAccountId::from_sub(owner), + owner.clone(), + owner, data, )?; let foreign_asset_id = --- a/pallets/fungible/src/lib.rs +++ b/pallets/fungible/src/lib.rs @@ -210,18 +210,21 @@ /// Initializes the collection. Returns [CollectionId] on success, [DispatchError] otherwise. pub fn init_collection( owner: T::CrossAccountId, + payer: T::CrossAccountId, data: CreateCollectionData, ) -> Result { - >::init_collection(owner, data, CollectionFlags::default()) + >::init_collection(owner, payer, data, CollectionFlags::default()) } /// Initializes the collection with ForeignCollection flag. Returns [CollectionId] on success, [DispatchError] otherwise. pub fn init_foreign_collection( owner: T::CrossAccountId, + payer: T::CrossAccountId, data: CreateCollectionData, ) -> Result { let id = >::init_collection( owner, + payer, data, CollectionFlags { foreign: true, --- a/pallets/nonfungible/src/lib.rs +++ b/pallets/nonfungible/src/lib.rs @@ -405,11 +405,13 @@ /// - `data`: Contains settings for collection limits and permissions. pub fn init_collection( owner: T::CrossAccountId, + payer: T::CrossAccountId, data: CreateCollectionData, is_external: bool, ) -> Result { >::init_collection( owner, + payer, data, CollectionFlags { external: is_external, --- a/pallets/proxy-rmrk-core/src/lib.rs +++ b/pallets/proxy-rmrk-core/src/lib.rs @@ -1448,7 +1448,7 @@ data: CreateCollectionData, properties: impl Iterator, ) -> Result { - let collection_id = >::init_collection(sender, data, true); + let collection_id = >::init_collection(sender.clone(), sender, data, true); if let Err(DispatchError::Arithmetic(_)) = &collection_id { return Err(>::NoAvailableCollectionId.into()); --- a/pallets/proxy-rmrk-equip/src/lib.rs +++ b/pallets/proxy-rmrk-equip/src/lib.rs @@ -251,7 +251,7 @@ }; let collection_id_res = - >::init_collection(cross_sender.clone(), data, true); + >::init_collection(cross_sender.clone(), cross_sender.clone(), data, true); if let Err(DispatchError::Arithmetic(_)) = &collection_id_res { return Err(>::NoAvailableBaseId.into()); --- a/pallets/refungible/src/lib.rs +++ b/pallets/refungible/src/lib.rs @@ -373,9 +373,10 @@ /// - `data`: Contains settings for collection limits and permissions. pub fn init_collection( owner: T::CrossAccountId, + payer: T::CrossAccountId, data: CreateCollectionData, ) -> Result { - >::init_collection(owner, data, CollectionFlags::default()) + >::init_collection(owner, payer, data, CollectionFlags::default()) } /// Destroy RFT collection --- a/pallets/unique/src/eth/mod.rs +++ b/pallets/unique/src/eth/mod.rs @@ -28,7 +28,7 @@ static_property::{key, value as property_value}, }, }; -use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder}; +use pallet_evm_coder_substrate::{dispatch_to_evm, SubstrateRecorder, WithRecorder}; use pallet_evm::{account::CrossAccountId, OnMethodCall, PrecompileHandle, PrecompileResult}; use up_data_structs::{ CollectionName, CollectionDescription, CollectionTokenPrefix, CreateCollectionData, @@ -156,6 +156,7 @@ T: Config + pallet_nonfungible::Config + pallet_refungible::Config, >( caller: caller, + value: value, name: string, description: string, token_prefix: string, @@ -172,8 +173,16 @@ base_uri_value, add_properties, )?; + let value = value.as_u128(); + let creation_price: Result = T::CollectionCreationPrice::get() + .try_into() + .map_err(|_| "collection creation price should be convertible to u128".into()); + if value != creation_price? { + return Err("Sent amount not equals to collection creation price".into()); + } + let collection_helpers_address = T::CrossAccountId::from_eth(::ContractAddress::get()); - let collection_id = T::CollectionDispatch::create(caller.clone(), data) + let collection_id = T::CollectionDispatch::create(caller.clone(), collection_helpers_address, data) .map_err(pallet_evm_coder_substrate::dispatch_to_evm::)?; let address = pallet_common::eth::collection_id_to_address(collection_id); Ok(address) @@ -183,7 +192,7 @@ #[solidity_interface(name = CollectionHelpers, events(CollectionHelpersEvents))] impl EvmCollectionHelpers where - T: Config + pallet_nonfungible::Config + pallet_refungible::Config, + T: Config + pallet_common::Config + pallet_nonfungible::Config + pallet_refungible::Config, { /// Create an NFT collection /// @param name Name of the collection @@ -209,8 +218,17 @@ Default::default(), false, )?; - let collection_id = T::CollectionDispatch::create(caller, data) - .map_err(pallet_evm_coder_substrate::dispatch_to_evm::)?; + let value = value.as_u128(); + let creation_price: Result = T::CollectionCreationPrice::get() + .try_into() + .map_err(|_| "collection creation price should be convertible to u128".into()); + let creation_price = creation_price?; + if value != creation_price { + return Err(format!("Sent amount not equals to collection creation price ({0})", creation_price).into()); + } + let collection_helpers_address = T::CrossAccountId::from_eth(::ContractAddress::get()); + let collection_id = + T::CollectionDispatch::create(caller, collection_helpers_address, data).map_err(dispatch_to_evm::)?; let address = pallet_common::eth::collection_id_to_address(collection_id); Ok(address) @@ -221,6 +239,7 @@ fn create_nonfungible_collection_with_properties( &mut self, caller: caller, + value: value, name: string, description: string, token_prefix: string, @@ -236,7 +255,15 @@ base_uri_value, true, )?; - let collection_id = T::CollectionDispatch::create(caller, data) + let value = value.as_u128(); + let creation_price: Result = T::CollectionCreationPrice::get() + .try_into() + .map_err(|_| "collection creation price should be convertible to u128".into()); + if value != creation_price? { + return Err("Sent amount not equals to collection creation price".into()); + } + let collection_helpers_address = T::CrossAccountId::from_eth(::ContractAddress::get()); + let collection_id = T::CollectionDispatch::create(caller, collection_helpers_address, data) .map_err(pallet_evm_coder_substrate::dispatch_to_evm::)?; let address = pallet_common::eth::collection_id_to_address(collection_id); @@ -248,12 +275,14 @@ fn create_refungible_collection( &mut self, caller: caller, + value: value, name: string, description: string, token_prefix: string, ) -> Result
{ create_refungible_collection_internal::( caller, + value, name, description, token_prefix, @@ -267,6 +296,7 @@ fn create_refungible_collection_with_properties( &mut self, caller: caller, + value: value, name: string, description: string, token_prefix: string, @@ -274,6 +304,7 @@ ) -> Result
{ create_refungible_collection_internal::( caller, + value, name, description, token_prefix, --- a/pallets/unique/src/eth/stubs/CollectionHelpers.sol +++ b/pallets/unique/src/eth/stubs/CollectionHelpers.sol @@ -36,7 +36,7 @@ string memory name, string memory description, string memory tokenPrefix - ) public returns (address) { + ) public payable returns (address) { require(false, stub_error); name; description; @@ -52,7 +52,7 @@ string memory description, string memory tokenPrefix, string memory baseUri - ) public returns (address) { + ) public payable returns (address) { require(false, stub_error); name; description; @@ -68,7 +68,7 @@ string memory name, string memory description, string memory tokenPrefix - ) public returns (address) { + ) public payable returns (address) { require(false, stub_error); name; description; @@ -84,7 +84,7 @@ string memory description, string memory tokenPrefix, string memory baseUri - ) public returns (address) { + ) public payable returns (address) { require(false, stub_error); name; description; --- a/pallets/unique/src/lib.rs +++ b/pallets/unique/src/lib.rs @@ -344,8 +344,8 @@ let sender = ensure_signed(origin)?; // ========= - - let _id = T::CollectionDispatch::create(T::CrossAccountId::from_sub(sender), data)?; + let sender = T::CrossAccountId::from_sub(sender); + let _id = T::CollectionDispatch::create(sender.clone(), sender, data)?; Ok(()) } --- a/runtime/common/dispatch.rs +++ b/runtime/common/dispatch.rs @@ -55,21 +55,22 @@ { fn create( sender: T::CrossAccountId, + payer: T::CrossAccountId, data: CreateCollectionData, ) -> Result { let id = match data.mode { - CollectionMode::NFT => >::init_collection(sender, data, false)?, + CollectionMode::NFT => >::init_collection(sender, payer, data, false)?, CollectionMode::Fungible(decimal_points) => { // check params ensure!( decimal_points <= MAX_DECIMAL_POINTS, pallet_unique::Error::::CollectionDecimalPointLimitExceeded ); - >::init_collection(sender, data)? + >::init_collection(sender, payer, data)? } #[cfg(feature = "refungible")] - CollectionMode::ReFungible => >::init_collection(sender, data)?, + CollectionMode::ReFungible => >::init_collection(sender, payer, data)?, #[cfg(not(feature = "refungible"))] CollectionMode::ReFungible => return unsupported!(T), --- a/tests/src/eth/api/CollectionHelpers.sol +++ b/tests/src/eth/api/CollectionHelpers.sol @@ -31,7 +31,7 @@ string memory name, string memory description, string memory tokenPrefix - ) external returns (address); + ) external payable returns (address); /// @dev EVM selector for this function is: 0xa634a5f9, /// or in textual repr: createERC721MetadataCompatibleCollection(string,string,string,string) @@ -40,7 +40,7 @@ string memory description, string memory tokenPrefix, string memory baseUri - ) external returns (address); + ) external payable returns (address); /// @dev EVM selector for this function is: 0xab173450, /// or in textual repr: createRFTCollection(string,string,string) @@ -48,7 +48,7 @@ string memory name, string memory description, string memory tokenPrefix - ) external returns (address); + ) external payable returns (address); /// @dev EVM selector for this function is: 0xa5596388, /// or in textual repr: createERC721MetadataCompatibleRFTCollection(string,string,string,string) @@ -57,7 +57,7 @@ string memory description, string memory tokenPrefix, string memory baseUri - ) external returns (address); + ) external payable returns (address); /// Check if a collection exists /// @param collectionAddress Address of the collection in question --- a/tests/src/eth/collectionHelpersAbi.json +++ b/tests/src/eth/collectionHelpersAbi.json @@ -27,7 +27,7 @@ ], "name": "createERC721MetadataCompatibleCollection", "outputs": [{ "internalType": "address", "name": "", "type": "address" }], - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "function" }, { @@ -39,7 +39,7 @@ ], "name": "createERC721MetadataCompatibleRFTCollection", "outputs": [{ "internalType": "address", "name": "", "type": "address" }], - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "function" }, { @@ -50,7 +50,7 @@ ], "name": "createNonfungibleCollection", "outputs": [{ "internalType": "address", "name": "", "type": "address" }], - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "function" }, { @@ -61,7 +61,7 @@ ], "name": "createRFTCollection", "outputs": [{ "internalType": "address", "name": "", "type": "address" }], - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "function" }, { --- a/tests/src/eth/fractionalizer/Fractionalizer.sol +++ b/tests/src/eth/fractionalizer/Fractionalizer.sol @@ -18,7 +18,8 @@ mapping(address => bool) nftCollectionAllowList; mapping(address => mapping(uint256 => uint256)) public nft2rftMapping; mapping(address => Token) public rft2nftMapping; - bytes32 refungibleCollectionType = keccak256(bytes("ReFungible")); + //use constant to reduce gas cost + bytes32 constant refungibleCollectionType = keccak256(bytes("ReFungible")); receive() external payable onlyOwner {} @@ -51,11 +52,12 @@ /// Throws if `msg.sender` is not owner or admin of provided RFT collection. /// Can only be called by contract owner. /// @param _collection address of RFT collection. - function setRFTCollection(address _collection) public onlyOwner { + function setRFTCollection(address _collection) external onlyOwner { require(rftCollection == address(0), "RFT collection is already set"); UniqueRefungible refungibleContract = UniqueRefungible(_collection); string memory collectionType = refungibleContract.uniqueCollectionType(); + // compare hashed to reduce gas cost require( keccak256(bytes(collectionType)) == refungibleCollectionType, "Wrong collection type. Collection is not refungible." @@ -79,7 +81,7 @@ string calldata _name, string calldata _description, string calldata _tokenPrefix - ) public onlyOwner { + ) external onlyOwner { require(rftCollection == address(0), "RFT collection is already set"); address collectionHelpers = 0x6C4E9fE1AE37a41E93CEE429e8E1881aBdcbb54F; rftCollection = CollectionHelpers(collectionHelpers).createRFTCollection(_name, _description, _tokenPrefix); @@ -90,7 +92,7 @@ /// @dev Can only be called by contract owner. /// @param collection NFT token address. /// @param status `true` to allow and `false` to disallow NFT token. - function setNftCollectionIsAllowed(address collection, bool status) public onlyOwner { + function setNftCollectionIsAllowed(address collection, bool status) external onlyOwner { nftCollectionAllowList[collection] = status; emit AllowListSet(collection, status); } @@ -109,7 +111,7 @@ address _collection, uint256 _token, uint128 _pieces - ) public { + ) external { require(rftCollection != address(0), "RFT collection is not set"); UniqueRefungible rftCollectionContract = UniqueRefungible(rftCollection); require( @@ -148,7 +150,7 @@ /// Throws if `msg.sender` isn't owner of all RFT token pieces. /// @param _collection RFT collection address /// @param _token id of RFT token - function rft2nft(address _collection, uint256 _token) public { + function rft2nft(address _collection, uint256 _token) external { require(rftCollection != address(0), "RFT collection is not set"); require(rftCollection == _collection, "Wrong RFT collection"); UniqueRefungible rftCollectionContract = UniqueRefungible(rftCollection); --- a/tests/src/eth/payable.test.ts +++ b/tests/src/eth/payable.test.ts @@ -140,16 +140,13 @@ }); itEth('Fee for nested calls to native methods is withdrawn from the user', async({helper}) => { - const CONTRACT_BALANCE = 3n * helper.balance.getOneTokenNominal(); + const CONTRACT_BALANCE = 2n * helper.balance.getOneTokenNominal(); const deployer = await helper.eth.createAccountWithBalance(donor); const caller = await helper.eth.createAccountWithBalance(donor); const contract = await deployProxyContract(helper, deployer); - - const web3 = helper.getWeb3(); - await web3.eth.sendTransaction({from: deployer, to: contract.options.address, value: CONTRACT_BALANCE.toString(), gas: helper.eth.DEFAULT_GAS}); - const collectionAddress = (await contract.methods.createNonfungibleCollection().send({from: caller})).events.CollectionCreated.returnValues.collection; + const collectionAddress = (await contract.methods.createNonfungibleCollection().send({from: caller, value: Number(CONTRACT_BALANCE)})).events.CollectionCreated.returnValues.collection; const initialCallerBalance = await helper.balance.getEthereum(caller); const initialContractBalance = await helper.balance.getEthereum(contract.options.address); await contract.methods.mintNftToken(collectionAddress).send({from: caller}); @@ -160,22 +157,18 @@ }); itEth('Fee for nested calls to create*Collection methods is withdrawn from the user and from the contract', async({helper}) => { - const CONTRACT_BALANCE = 3n * helper.balance.getOneTokenNominal(); - + const CONTRACT_BALANCE = 2n * helper.balance.getOneTokenNominal(); const deployer = await helper.eth.createAccountWithBalance(donor); const caller = await helper.eth.createAccountWithBalance(donor); const contract = await deployProxyContract(helper, deployer); - - const web3 = helper.getWeb3(); - await web3.eth.sendTransaction({from: deployer, to: contract.options.address, value: CONTRACT_BALANCE.toString(), gas: helper.eth.DEFAULT_GAS}); const initialCallerBalance = await helper.balance.getEthereum(caller); const initialContractBalance = await helper.balance.getEthereum(contract.options.address); - await contract.methods.createNonfungibleCollection().send({from: caller}); + await contract.methods.createNonfungibleCollection().send({from: caller, value: Number(CONTRACT_BALANCE)}); const finalCallerBalance = await helper.balance.getEthereum(caller); const finalContractBalance = await helper.balance.getEthereum(contract.options.address); expect(finalCallerBalance < initialCallerBalance).to.be.true; - expect(finalContractBalance < initialContractBalance).to.be.true; + expect(finalContractBalance == initialContractBalance).to.be.true; }); async function deployProxyContract(helper: EthUniqueHelper, deployer: string) { @@ -189,6 +182,8 @@ import {CollectionHelpers} from "../api/CollectionHelpers.sol"; import {UniqueNFT} from "../api/UniqueNFT.sol"; + error Value(uint256 value); + contract ProxyContract { bool value = false; address flipper; @@ -207,30 +202,30 @@ Flipper(flipper).flip(); } - function createNonfungibleCollection() public { + function createNonfungibleCollection() external payable { address collectionHelpers = 0x6C4E9fE1AE37a41E93CEE429e8E1881aBdcbb54F; - address nftCollection = CollectionHelpers(collectionHelpers).createNonfungibleCollection("A", "B", "C"); + address nftCollection = CollectionHelpers(collectionHelpers).createNonfungibleCollection{value: msg.value}("A", "B", "C"); emit CollectionCreated(nftCollection); } - function mintNftToken(address collectionAddress) public { + function mintNftToken(address collectionAddress) external { UniqueNFT collection = UniqueNFT(collectionAddress); uint256 tokenId = collection.nextTokenId(); collection.mint(msg.sender, tokenId); emit TokenMinted(tokenId); } - function getValue() public view returns (bool) { + function getValue() external view returns (bool) { return Flipper(flipper).getValue(); } } contract Flipper { bool value = false; - function flip() public { + function flip() external { value = !value; } - function getValue() public view returns (bool) { + function getValue() external view returns (bool) { return value; } }