--- a/pallets/evm-contract-helpers/src/stubs/ContractHelpers.sol +++ b/pallets/evm-contract-helpers/src/stubs/ContractHelpers.sol @@ -27,8 +27,13 @@ } } -// Selector: 06fc42e9 +// Selector: 6073d917 contract ContractHelpers is Dummy, ERC165 { + // Get contract ovner + // + // @param Contract_address contract for which the owner is being determined. + // @return Contract owner. + // // Selector: contractOwner(address) 5152b14c function contractOwner(address contractAddress) public @@ -41,6 +46,11 @@ return 0x0000000000000000000000000000000000000000; } + // Set sponsor. + // + // @param contract_address Contract for which a sponsor is being established. + // @param sponsor User address who set as pending sponsor. + // // Selector: setSponsor(address,address) f01fba93 function setSponsor(address contractAddress, address sponsor) public { require(false, stub_error); @@ -49,6 +59,34 @@ dummy = 0; } + // Set contract as self sponsored. + // + // @param contract_address Contract for which a self sponsoring is being enabled. + // + // Selector: selfSponsoredEnable(address) 89f7d9ae + function selfSponsoredEnable(address contractAddress) public { + require(false, stub_error); + contractAddress; + dummy = 0; + } + + // Remove sponsor. + // + // @param contract_address Contract for which a sponsorship is being removed. + // + // Selector: removeSponsor(address) ef784250 + function removeSponsor(address contractAddress) public { + require(false, stub_error); + contractAddress; + dummy = 0; + } + + // Confirm sponsorship. + // + // @dev Caller must be same that set via [`set_sponsor`]. + // + // @param contract_address Сontract for which need to confirm sponsorship. + // // Selector: confirmSponsorship(address) abc00001 function confirmSponsorship(address contractAddress) public { require(false, stub_error); @@ -56,6 +94,11 @@ dummy = 0; } + // Get current sponsor. + // + // @param contract_address The contract for which a sponsor is requested. + // @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw. + // // Selector: getSponsor(address) 743fc745 function getSponsor(address contractAddress) public @@ -68,6 +111,11 @@ return Tuple0(0x0000000000000000000000000000000000000000, 0); } + // Check tat contract has confirmed sponsor. + // + // @param contract_address The contract for which the presence of a confirmed sponsor is checked. + // @return **true** if contract has confirmed sponsor. + // // Selector: hasSponsor(address) 97418603 function hasSponsor(address contractAddress) public view returns (bool) { require(false, stub_error); @@ -76,6 +124,11 @@ return false; } + // Check tat contract has pending sponsor. + // + // @param contract_address The contract for which the presence of a pending sponsor is checked. + // @return **true** if contract has pending sponsor. + // // Selector: hasPendingSponsor(address) 39b9b242 function hasPendingSponsor(address contractAddress) public --- a/tests/src/eth/api/ContractHelpers.sol +++ b/tests/src/eth/api/ContractHelpers.sol @@ -18,29 +18,74 @@ function supportsInterface(bytes4 interfaceID) external view returns (bool); } -// Selector: 06fc42e9 +// Selector: 6073d917 interface ContractHelpers is Dummy, ERC165 { + // Get contract ovner + // + // @param Contract_address contract for which the owner is being determined. + // @return Contract owner. + // // Selector: contractOwner(address) 5152b14c function contractOwner(address contractAddress) external view returns (address); + // Set sponsor. + // + // @param contract_address Contract for which a sponsor is being established. + // @param sponsor User address who set as pending sponsor. + // // Selector: setSponsor(address,address) f01fba93 function setSponsor(address contractAddress, address sponsor) external; + // Set contract as self sponsored. + // + // @param contract_address Contract for which a self sponsoring is being enabled. + // + // Selector: selfSponsoredEnable(address) 89f7d9ae + function selfSponsoredEnable(address contractAddress) external; + + // Remove sponsor. + // + // @param contract_address Contract for which a sponsorship is being removed. + // + // Selector: removeSponsor(address) ef784250 + function removeSponsor(address contractAddress) external; + + // Confirm sponsorship. + // + // @dev Caller must be same that set via [`set_sponsor`]. + // + // @param contract_address Сontract for which need to confirm sponsorship. + // // Selector: confirmSponsorship(address) abc00001 function confirmSponsorship(address contractAddress) external; + // Get current sponsor. + // + // @param contract_address The contract for which a sponsor is requested. + // @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw. + // // Selector: getSponsor(address) 743fc745 function getSponsor(address contractAddress) external view returns (Tuple0 memory); + // Check tat contract has confirmed sponsor. + // + // @param contract_address The contract for which the presence of a confirmed sponsor is checked. + // @return **true** if contract has confirmed sponsor. + // // Selector: hasSponsor(address) 97418603 function hasSponsor(address contractAddress) external view returns (bool); + // Check tat contract has pending sponsor. + // + // @param contract_address The contract for which the presence of a pending sponsor is checked. + // @return **true** if contract has pending sponsor. + // // Selector: hasPendingSponsor(address) 39b9b242 function hasPendingSponsor(address contractAddress) external --- a/tests/src/eth/contractSponsoring.test.ts +++ b/tests/src/eth/contractSponsoring.test.ts @@ -28,12 +28,31 @@ import {evmToAddress} from '@polkadot/util-crypto'; describe('Sponsoring EVM contracts', () => { + itWeb3('Self sponsored can be set by the address that deployed the contract', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const flipper = await deployFlipper(web3, owner); + const helpers = contractHelpers(web3, owner); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; + await expect(helpers.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected; + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.true; + }); + + itWeb3('Self sponsored can not be set by the address that did not deployed the contract', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const notOwner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const flipper = await deployFlipper(web3, owner); + const helpers = contractHelpers(web3, owner); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; + await expect(helpers.methods.selfSponsoredEnable(flipper.options.address).call({from: notOwner})).to.be.rejectedWith('NoPermission'); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; + }); + itWeb3('Sponsoring can be set by the address that has deployed the contract', async ({api, web3, privateKeyWrapper}) => { const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const flipper = await deployFlipper(web3, owner); const helpers = contractHelpers(web3, owner); expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.false; - await helpers.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Allowlisted).send({from: owner}); + await expect(helpers.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Allowlisted).send({from: owner})).to.be.not.rejected; expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.true; }); @@ -43,7 +62,7 @@ const flipper = await deployFlipper(web3, owner); const helpers = contractHelpers(web3, owner); expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.false; - await expect(helpers.methods.setSponsoringMode(notOwner, SponsoringMode.Allowlisted).send({from: notOwner})).to.rejected; + await expect(helpers.methods.setSponsoringMode(notOwner, SponsoringMode.Allowlisted).call({from: notOwner})).to.be.rejectedWith('NoPermission'); expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.false; }); @@ -91,6 +110,19 @@ expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; }); + itWeb3('Get self sponsored sponsor', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const flipper = await deployFlipper(web3, owner); + const helpers = contractHelpers(web3, owner); + await helpers.methods.selfSponsoredEnable(flipper.options.address).send(); + + const result = await helpers.methods.getSponsor(flipper.options.address).call(); + + expect(result[0]).to.be.eq(flipper.options.address); + const sponsorSub = api.registry.createType('AccountId', '0x' + BigInt(result[1]).toString(16).padStart(64, '0')).toJSON(); + expect(sponsorSub).to.be.eq(evmToAddress(flipper.options.address)); + }); + itWeb3('Get confirmed sponsor', async ({api, web3, privateKeyWrapper}) => { const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); @@ -106,6 +138,37 @@ expect(sponsorSub).to.be.eq(evmToAddress(sponsor)); }); + itWeb3('Sponsor can be removed by the address that deployed the contract', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const flipper = await deployFlipper(web3, owner); + const helpers = contractHelpers(web3, owner); + + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; + await helpers.methods.setSponsor(flipper.options.address, sponsor).send(); + await helpers.methods.confirmSponsorship(flipper.options.address).send({from: sponsor}); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.true; + + await helpers.methods.removeSponsor(flipper.options.address).send(); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; + }); + + itWeb3('Sponsor can not be removed by the address that did not deployed the contract', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const notOwner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const flipper = await deployFlipper(web3, owner); + const helpers = contractHelpers(web3, owner); + + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; + await helpers.methods.setSponsor(flipper.options.address, sponsor).send(); + await helpers.methods.confirmSponsorship(flipper.options.address).send({from: sponsor}); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.true; + + await expect(helpers.methods.removeSponsor(flipper.options.address).call({from: notOwner})).to.be.rejectedWith('NoPermission'); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.true; + }); + itWeb3('In generous mode, non-allowlisted user transaction will be sponsored', async ({api, web3, privateKeyWrapper}) => { const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); @@ -115,10 +178,8 @@ const helpers = contractHelpers(web3, owner); - expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; await helpers.methods.setSponsor(flipper.options.address, sponsor).send(); await helpers.methods.confirmSponsorship(flipper.options.address).send({from: sponsor}); - expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.true; await helpers.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: owner}); await helpers.methods.setSponsoringRateLimit(flipper.options.address, 0).send({from: owner}); @@ -136,6 +197,36 @@ expect(callerBalanceAfter).to.be.eq(callerBalanceBefore); }); + itWeb3('In generous mode, non-allowlisted user transaction will be self sponsored', async ({api, web3, privateKeyWrapper}) => { + const alice = privateKeyWrapper('//Alice'); + + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + + const flipper = await deployFlipper(web3, owner); + + const helpers = contractHelpers(web3, owner); + + await helpers.methods.selfSponsoredEnable(flipper.options.address).send(); + + await helpers.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: owner}); + await helpers.methods.setSponsoringRateLimit(flipper.options.address, 0).send({from: owner}); + + await transferBalanceToEth(api, alice, flipper.options.address); + + const contractBalanceBefore = await ethBalanceViaSub(api, flipper.options.address); + const callerBalanceBefore = await ethBalanceViaSub(api, caller); + + await flipper.methods.flip().send({from: caller}); + expect(await flipper.methods.getValue().call()).to.be.true; + + // Balance should be taken from sponsor instead of caller + const contractBalanceAfter = await ethBalanceViaSub(api, flipper.options.address); + const callerBalanceAfter = await ethBalanceViaSub(api, caller); + expect(contractBalanceAfter < contractBalanceBefore).to.be.true; + expect(callerBalanceAfter).to.be.eq(callerBalanceBefore); + }); + itWeb3('Sponsoring is set, an address that has no UNQ can send a transaction and it works. Sponsor balance should decrease (allowlisted)', async ({api, web3, privateKeyWrapper}) => { const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); --- a/tests/src/eth/util/contractHelpersAbi.json +++ b/tests/src/eth/util/contractHelpersAbi.json @@ -120,6 +120,32 @@ "internalType": "address", "name": "contractAddress", "type": "address" + } + ], + "name": "removeSponsor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "selfSponsoredEnable", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" }, { "internalType": "address", "name": "sponsor", "type": "address" } ],