From dafe0cc4b62bc8ac163d14acc6d0e780a4fa2305 Mon Sep 17 00:00:00 2001 From: Marko Kostic Date: Mon, 17 Mar 2025 10:30:23 +0100 Subject: [PATCH 1/9] fixed the contract address whitelist issue --- contracts/KnowledgeCollection.sol | 8 ++++---- contracts/Paymaster.sol | 18 ++++++++++++------ contracts/interfaces/IPaymaster.sol | 2 +- contracts/storage/PaymasterManager.sol | 2 +- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/contracts/KnowledgeCollection.sol b/contracts/KnowledgeCollection.sol index a6546b51..a0714823 100644 --- a/contracts/KnowledgeCollection.sol +++ b/contracts/KnowledgeCollection.sol @@ -118,7 +118,7 @@ contract KnowledgeCollection is INamed, IVersioned, ContractStatus, IInitializab es.addTokensToEpochRange(1, currentEpoch, currentEpoch + epochs + 1, tokenAmount); es.addEpochProducedKnowledgeValue(publisherNodeIdentityId, currentEpoch, tokenAmount); - _addTokens(tokenAmount, paymaster); + _addTokens(tokenAmount, paymaster, msg.sender); return id; } @@ -223,7 +223,7 @@ contract KnowledgeCollection is INamed, IVersioned, ContractStatus, IInitializab epochStorage.addTokensToEpochRange(1, endEpoch, endEpoch + epochs, tokenAmount); - _addTokens(tokenAmount, paymaster); + _addTokens(tokenAmount, paymaster, msg.sender); ParanetKnowledgeCollectionsRegistry pkar = paranetKnowledgeCollectionsRegistry; @@ -300,11 +300,11 @@ contract KnowledgeCollection is INamed, IVersioned, ContractStatus, IInitializab } } - function _addTokens(uint96 tokenAmount, address paymaster) internal { + function _addTokens(uint96 tokenAmount, address paymaster, address originalSender) internal { IERC20 token = tokenContract; if (paymasterManager.validPaymasters(paymaster)) { - IPaymaster(paymaster).coverCost(tokenAmount); + IPaymaster(paymaster).coverCost(tokenAmount, originalSender); } else { if (token.allowance(msg.sender, address(this)) < tokenAmount) { revert TokenLib.TooLowAllowance( diff --git a/contracts/Paymaster.sol b/contracts/Paymaster.sol index f213e315..57577c5c 100644 --- a/contracts/Paymaster.sol +++ b/contracts/Paymaster.sol @@ -7,7 +7,7 @@ import {TokenLib} from "./libraries/TokenLib.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -contract Paymaster is Ownable(msg.sender) { +contract Paymaster is Ownable { error NotAllowed(); Hub public hub; @@ -15,14 +15,14 @@ contract Paymaster is Ownable(msg.sender) { mapping(address => bool) public allowedAddresses; - modifier onlyAllowed() { - if (!allowedAddresses[msg.sender]) { + modifier onlyAllowed(address _originalSender) { + if (!allowedAddresses[_originalSender]) { revert NotAllowed(); } _; } - constructor(address hubAddress) { + constructor(address hubAddress, address initialOwner) Ownable(initialOwner) { hub = Hub(hubAddress); tokenContract = IERC20(hub.getContractAddress("Token")); } @@ -59,8 +59,14 @@ contract Paymaster is Ownable(msg.sender) { _transferTokens(recipient, amount); } - function coverCost(uint256 amount) external onlyAllowed { - _transferTokens(hub.getContractAddress("KnowledgeCollection"), amount); + function coverCost(uint256 amount, address _originalSender) external onlyAllowed(_originalSender) { + address knowledgeCollectionAddress = hub.getContractAddress("KnowledgeCollection"); + + if (msg.sender != knowledgeCollectionAddress) { + revert("Sender is not the KnowledgeCollection contract"); + } + + _transferTokens(knowledgeCollectionAddress, amount); } function _transferTokens(address to, uint256 amount) internal { diff --git a/contracts/interfaces/IPaymaster.sol b/contracts/interfaces/IPaymaster.sol index 48aa2929..3f404157 100644 --- a/contracts/interfaces/IPaymaster.sol +++ b/contracts/interfaces/IPaymaster.sol @@ -9,5 +9,5 @@ interface IPaymaster { function fundPaymaster(uint256 amount) external; - function coverCost(uint256 amount) external; + function coverCost(uint256 amount, address originalSender) external; } diff --git a/contracts/storage/PaymasterManager.sol b/contracts/storage/PaymasterManager.sol index 369505df..33fa05a0 100644 --- a/contracts/storage/PaymasterManager.sol +++ b/contracts/storage/PaymasterManager.sol @@ -27,7 +27,7 @@ contract PaymasterManager is INamed, IVersioned, ContractStatus { } function deployPaymaster() external { - address paymasterAddress = address(new Paymaster(address(hub))); + address paymasterAddress = address(new Paymaster(address(hub), msg.sender)); validPaymasters[paymasterAddress] = true; deployedPaymasters[msg.sender].push(paymasterAddress); From c6e753a89bfbeb8b80eb2864c00c64e079105b39 Mon Sep 17 00:00:00 2001 From: Marko Kostic Date: Mon, 17 Mar 2025 10:40:59 +0100 Subject: [PATCH 2/9] provided optional KC creation arguments --- test/helpers/kc-helpers.ts | 58 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/test/helpers/kc-helpers.ts b/test/helpers/kc-helpers.ts index 55804ee4..b7a263cd 100644 --- a/test/helpers/kc-helpers.ts +++ b/test/helpers/kc-helpers.ts @@ -114,3 +114,61 @@ export async function createKnowledgeCollection( return { tx, receipt, collectionId }; } + +export async function createProfilesAndKC( + kcCreator: SignerWithAddress, + publishingNode: NodeAccounts, + receivingNodes: NodeAccounts[], + contracts: { + Profile: Profile; + KnowledgeCollection: KnowledgeCollection; + Token: Token; + }, + kcOptions?: { + publishOperationId?: string; + knowledgeAssetsAmount?: number; + byteSize?: number; + epochs?: number; + tokenAmount?: bigint; + isImmutable?: boolean; + paymaster?: string; + } +) { + const { identityId: publishingNodeIdentityId } = await createProfile( + contracts.Profile, + publishingNode, + ); + const receivingNodesIdentityIds = ( + await createProfiles(contracts.Profile, receivingNodes) + ).map((p) => p.identityId); + + // Create knowledge collection + const signaturesData = await getKCSignaturesData( + publishingNode, + publishingNodeIdentityId, + receivingNodes, + ); + const { collectionId } = await createKnowledgeCollection( + kcCreator, + publishingNodeIdentityId, + receivingNodesIdentityIds, + signaturesData, + contracts, + kcOptions?.publishOperationId, + kcOptions?.knowledgeAssetsAmount, + kcOptions?.byteSize, + kcOptions?.epochs, + kcOptions?.tokenAmount, + kcOptions?.isImmutable, + kcOptions?.paymaster + ); + + return { + publishingNode, + publishingNodeIdentityId, + receivingNodes, + receivingNodesIdentityIds, + kcCreator, + collectionId, + }; +} From 2a1128e6d4b9b7b788cb603fabd0dafe55993cab Mon Sep 17 00:00:00 2001 From: Marko Kostic Date: Mon, 17 Mar 2025 10:41:34 +0100 Subject: [PATCH 3/9] added a helper for creating a Paymaster contract instance --- test/helpers/paymaster-helpers.ts | 32 +++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 test/helpers/paymaster-helpers.ts diff --git a/test/helpers/paymaster-helpers.ts b/test/helpers/paymaster-helpers.ts new file mode 100644 index 00000000..00807aaf --- /dev/null +++ b/test/helpers/paymaster-helpers.ts @@ -0,0 +1,32 @@ +import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; +import { PaymasterManager } from '../../typechain'; + +export async function createPaymaster( + paymasterCreator: SignerWithAddress, + paymasterManager: PaymasterManager +) { + const tx = await paymasterManager.connect(paymasterCreator).deployPaymaster(); + const receipt = await tx.wait(); + + const paymasterDeployedEvent = receipt!.logs.find( + log => log.topics[0] === paymasterManager.interface.getEvent('PaymasterDeployed').topicHash + ); + + if (!paymasterDeployedEvent) { + throw new Error('PaymasterDeployed event not found in transaction logs'); + } + + const parsedEvent = paymasterManager.interface.parseLog({ + topics: paymasterDeployedEvent.topics as string[], + data: paymasterDeployedEvent.data + }); + + if (!parsedEvent) { + throw new Error('Failed to parse PaymasterDeployed event'); + } + + const deployer = parsedEvent.args.deployer; + const paymasterAddress = parsedEvent.args.paymasterAddress; + + return {deployer, paymasterAddress} +}; From 5af3554a9710209e3443307b0f9cd3052d81e4c8 Mon Sep 17 00:00:00 2001 From: Marko Kostic Date: Mon, 17 Mar 2025 10:53:25 +0100 Subject: [PATCH 4/9] added unit tests and fixed failing ones --- test/unit/Paymaster.test.ts | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/test/unit/Paymaster.test.ts b/test/unit/Paymaster.test.ts index 547fd0bf..f5d33a8d 100644 --- a/test/unit/Paymaster.test.ts +++ b/test/unit/Paymaster.test.ts @@ -13,7 +13,7 @@ describe('@unit Paymaster', () => { let Paymaster: Paymaster; let owner: SignerWithAddress; let user: SignerWithAddress; - let knowledgeCollectionAddress: string; + let knowledgeCollection: SignerWithAddress; async function deployPaymasterFixture() { await hre.deployments.fixture(['Hub', 'Token']); @@ -26,13 +26,13 @@ describe('@unit Paymaster', () => { // Deploy Paymaster const PaymasterFactory = await hre.ethers.getContractFactory('Paymaster'); - Paymaster = await PaymasterFactory.deploy(Hub.getAddress()); + Paymaster = await PaymasterFactory.deploy(Hub.getAddress(), owner); // Set mock KnowledgeCollection address in Hub - knowledgeCollectionAddress = accounts[3].address; + knowledgeCollection = accounts[3]; await Hub.setContractAddress( 'KnowledgeCollection', - knowledgeCollectionAddress, + knowledgeCollection.address, ); // Reset user's balance to zero first @@ -179,32 +179,39 @@ describe('@unit Paymaster', () => { }); it('Should allow allowed address to cover cost', async () => { - await expect(Paymaster.connect(user).coverCost(coverAmount)) + await expect(Paymaster.connect(knowledgeCollection).coverCost(coverAmount, user.address)) .to.emit(Token, 'Transfer') .withArgs( await Paymaster.getAddress(), - knowledgeCollectionAddress, + knowledgeCollection.address, coverAmount, ); }); it('Should revert when non-allowed address tries to cover cost', async () => { - const nonAllowed = accounts[4]; + const notAllowed = accounts[4]; await expect( - Paymaster.connect(nonAllowed).coverCost(coverAmount), + Paymaster.connect(knowledgeCollection).coverCost(coverAmount, notAllowed), ).to.be.revertedWithCustomError(Paymaster, 'NotAllowed'); }); + it('Should revert when non-KnowledgeCollection contract address tries to cover cost', async () => { + const notKnowledgeCollection = accounts[4]; + await expect( + Paymaster.connect(notKnowledgeCollection).coverCost(coverAmount, user.address), + ).to.be.revertedWith('Sender is not the KnowledgeCollection contract'); + }); + it('Should revert with zero amount', async () => { await expect( - Paymaster.connect(user).coverCost(0), + Paymaster.connect(knowledgeCollection).coverCost(0, user.address), ).to.be.revertedWithCustomError(Paymaster, 'ZeroTokenAmount'); }); it('Should revert with insufficient balance', async () => { const tooMuch = parseEther('200'); await expect( - Paymaster.connect(user).coverCost(tooMuch), + Paymaster.connect(knowledgeCollection).coverCost(tooMuch, user.address), ).to.be.revertedWithCustomError(Paymaster, 'TooLowBalance'); }); }); From b6580ffb1b15e8e983bc90b7b6ee854edbe5f90f Mon Sep 17 00:00:00 2001 From: Marko Kostic Date: Mon, 17 Mar 2025 12:01:21 +0100 Subject: [PATCH 5/9] added Paymaster integration tests --- test/integration/Paymaster.test.ts | 351 +++++++++++++++++++++++++++++ 1 file changed, 351 insertions(+) create mode 100644 test/integration/Paymaster.test.ts diff --git a/test/integration/Paymaster.test.ts b/test/integration/Paymaster.test.ts new file mode 100644 index 00000000..a4e6707d --- /dev/null +++ b/test/integration/Paymaster.test.ts @@ -0,0 +1,351 @@ +import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { expect } from 'chai'; +import { ethers } from 'ethers'; +import hre from 'hardhat'; + +import { + Paranet, + ParanetsRegistry, + ParanetServicesRegistry, + ParanetKnowledgeMinersRegistry, + ParanetKnowledgeCollectionsRegistry, + ParanetIncentivesPoolFactory, + KnowledgeCollection, + KnowledgeCollectionStorage, + Profile, + Token, + Hub, + EpochStorage, + ParanetIncentivesPoolFactoryHelper, + ParanetStagingRegistry, + IdentityStorage, + HubLib, + ParanetLib, + PaymasterManager, +} from '../../typechain'; +import { + createPaymaster, +} from '../helpers/paymaster-helpers'; +import { + createProfilesAndKC, +} from '../helpers/kc-helpers'; +import { + getDefaultPublishingNode, + getDefaultReceivingNodes, + getDefaultKCCreator, +} from '../helpers/setup-helpers'; + +// Fixture containing all contracts and accounts needed to test Paranet +type PaymasterFixture = { + accounts: SignerWithAddress[]; + Paranet: Paranet; + ParanetsRegistry: ParanetsRegistry; + ParanetServicesRegistry: ParanetServicesRegistry; + ParanetKnowledgeMinersRegistry: ParanetKnowledgeMinersRegistry; + ParanetKnowledgeCollectionsRegistry: ParanetKnowledgeCollectionsRegistry; + ParanetIncentivesPoolFactoryHelper: ParanetIncentivesPoolFactoryHelper; + ParanetIncentivesPoolFactory: ParanetIncentivesPoolFactory; + KnowledgeCollection: KnowledgeCollection; + KnowledgeCollectionStorage: KnowledgeCollectionStorage; + Profile: Profile; + Token: Token; + EpochStorage: EpochStorage; + ParanetStagingRegistry: ParanetStagingRegistry; + IdentityStorage: IdentityStorage; + HubLib: HubLib; + ParanetLib: ParanetLib; + PaymasterManager: PaymasterManager, +}; + +describe('@integration Paymaster', () => { + let accounts: SignerWithAddress[]; + let Paranet: Paranet; + let ParanetsRegistry: ParanetsRegistry; + let ParanetServicesRegistry: ParanetServicesRegistry; + let ParanetKnowledgeMinersRegistry: ParanetKnowledgeMinersRegistry; + let ParanetKnowledgeCollectionsRegistry: ParanetKnowledgeCollectionsRegistry; + let ParanetIncentivesPoolFactoryHelper: ParanetIncentivesPoolFactoryHelper; + let ParanetIncentivesPoolFactory: ParanetIncentivesPoolFactory; + let KnowledgeCollection: KnowledgeCollection; + let KnowledgeCollectionStorage: KnowledgeCollectionStorage; + let Profile: Profile; + let Token: Token; + let EpochStorage: EpochStorage; + let ParanetStagingRegistry: ParanetStagingRegistry; + let IdentityStorage: IdentityStorage; + let HubLib: HubLib; + let ParanetLib: ParanetLib; + let PaymasterManager: PaymasterManager; + + // Deploy all contracts, set the HubOwner and necessary accounts. Returns the PaymasterFixture + async function deployPaymasterFixture(): Promise { + await hre.deployments.fixture([ + 'Paranet', + 'ParanetsRegistry', + 'ParanetServicesRegistry', + 'ParanetKnowledgeMinersRegistry', + 'ParanetKnowledgeCollectionsRegistry', + 'ParanetIncentivesPoolFactoryHelper', + 'ParanetIncentivesPoolFactory', + 'KnowledgeCollection', + 'Profile', + 'Token', + 'EpochStorage', + 'ParanetStagingRegistry', + 'IdentityStorage', + 'PaymasterManager', + ]); + + accounts = await hre.ethers.getSigners(); + const Hub = await hre.ethers.getContract('Hub'); + await Hub.setContractAddress('HubOwner', accounts[0].address); + + EpochStorage = await hre.ethers.getContract('EpochStorageV8'); + Paranet = await hre.ethers.getContract('Paranet'); + ParanetsRegistry = + await hre.ethers.getContract('ParanetsRegistry'); + ParanetServicesRegistry = + await hre.ethers.getContract( + 'ParanetServicesRegistry', + ); + ParanetKnowledgeMinersRegistry = + await hre.ethers.getContract( + 'ParanetKnowledgeMinersRegistry', + ); + ParanetKnowledgeCollectionsRegistry = + await hre.ethers.getContract( + 'ParanetKnowledgeCollectionsRegistry', + ); + ParanetIncentivesPoolFactoryHelper = + await hre.ethers.getContract( + 'ParanetIncentivesPoolFactoryHelper', + ); + ParanetIncentivesPoolFactory = + await hre.ethers.getContract( + 'ParanetIncentivesPoolFactory', + ); + KnowledgeCollection = await hre.ethers.getContract( + 'KnowledgeCollection', + ); + KnowledgeCollectionStorage = + await hre.ethers.getContract( + 'KnowledgeCollectionStorage', + ); + Profile = await hre.ethers.getContract('Profile'); + // await hre.deployments.deploy('Token', { + // from: accounts[0].address, + // args: ['Neuro', 'NEURO'], + // log: true, + // }); + Token = await hre.ethers.getContract('Token'); + ParanetStagingRegistry = + await hre.ethers.getContract( + 'ParanetStagingRegistry', + ); + IdentityStorage = + await hre.ethers.getContract('IdentityStorage'); + + const hubLibDeployment = await hre.deployments.deploy('HubLib', { + from: accounts[0].address, + log: true, + }); + HubLib = await hre.ethers.getContract( + 'HubLib', + hubLibDeployment.address, + ); + + const paranetLibDeployment = await hre.deployments.deploy('ParanetLib', { + from: accounts[0].address, + log: true, + }); + ParanetLib = await hre.ethers.getContract( + 'ParanetLib', + paranetLibDeployment.address, + ); + PaymasterManager = await hre.ethers.getContract( + 'PaymasterManager', + ); + + return { + accounts, + Paranet, + ParanetsRegistry, + ParanetServicesRegistry, + ParanetKnowledgeMinersRegistry, + ParanetKnowledgeCollectionsRegistry, + ParanetIncentivesPoolFactoryHelper, + ParanetIncentivesPoolFactory, + KnowledgeCollection, + KnowledgeCollectionStorage, + Profile, + Token, + EpochStorage, + ParanetStagingRegistry, + IdentityStorage, + HubLib, + ParanetLib, + PaymasterManager, + }; + } + + // Before each test, deploy all contracts and necessary accounts. These variables can be used in the tests + beforeEach(async () => { + ({ + accounts, + Paranet, + ParanetsRegistry, + ParanetServicesRegistry, + ParanetKnowledgeMinersRegistry, + ParanetKnowledgeCollectionsRegistry, + ParanetIncentivesPoolFactoryHelper, + ParanetIncentivesPoolFactory, + KnowledgeCollection, + KnowledgeCollectionStorage, + Profile, + Token, + ParanetStagingRegistry, + HubLib, + ParanetLib, + } = await loadFixture(deployPaymasterFixture)); + }); + + it('Should deploy a KC with Paymaster passed', async () => { + const kcCreator = getDefaultKCCreator(accounts); + const paymasterCreator = kcCreator; + + const publishingNode = getDefaultPublishingNode(accounts); + const receivingNodes = getDefaultReceivingNodes(accounts); + + const { deployer, paymasterAddress } = await createPaymaster(paymasterCreator, PaymasterManager); + + const paymaster = await hre.ethers.getContractAt('Paymaster', paymasterAddress); + + await paymaster.connect(kcCreator).addAllowedAddress(kcCreator.address); + expect(await paymaster.allowedAddresses(kcCreator.address)).to.be.true; + + const tokenAmount = ethers.parseEther('100'); + await Token.connect(kcCreator).approve(paymasterAddress, tokenAmount); + await paymaster.connect(kcCreator).fundPaymaster(tokenAmount); + + expect(await Token.balanceOf(paymasterAddress)).to.equal(tokenAmount); + + const initialPaymasterBalance = await Token.balanceOf(paymasterAddress); + + const paymasterOwner = await paymaster.owner(); + + expect(deployer).to.equal(paymasterOwner); + expect(kcCreator.address).to.equal(paymasterOwner); + + const { collectionId } = await createProfilesAndKC( + kcCreator, + publishingNode, + receivingNodes, + { Profile, KnowledgeCollection, Token }, + { paymaster: paymasterAddress, + tokenAmount: tokenAmount, + } + ); + + // Verify that the paymaster's token balance has decreased by the token amount + // This confirms that tokens were transferred from the paymaster to the KC contract + const finalPaymasterBalance = await Token.balanceOf(paymasterAddress); + expect(finalPaymasterBalance).to.equal(initialPaymasterBalance - tokenAmount); + + // Verify that the KC storage has the correct token amount for the collection + const storedTokenAmount = await KnowledgeCollectionStorage.getTokenAmount(collectionId); + expect(storedTokenAmount).to.equal(tokenAmount); + + // Verify that the KC was created with the correct token amount + const collectionData = await KnowledgeCollectionStorage.getKnowledgeCollection(collectionId); + expect(collectionData.tokenAmount).to.equal(tokenAmount); + + // Verify that the KC creator's balance should not have changed since the paymaster covered the cost + const kcCreatorBalance = await Token.balanceOf(kcCreator.address); + // The KC creator's balance should not have changed since the paymaster covered the cost + expect(kcCreatorBalance).to.be.gt(0); // Just verify the creator has tokens + }); + + it('Whitelisted users can publish and pay', async () => { + const kcCreator = getDefaultKCCreator(accounts); + const paymasterCreator = kcCreator; + const publishingNode = getDefaultPublishingNode(accounts); + const receivingNodes = getDefaultReceivingNodes(accounts); + + const whitelistedUser = accounts[8]; + + const { paymasterAddress } = await createPaymaster(paymasterCreator, PaymasterManager); + const paymaster = await hre.ethers.getContractAt('Paymaster', paymasterAddress); + + const tokenAmount = ethers.parseEther('100'); + await Token.connect(kcCreator).approve(paymasterAddress, tokenAmount); + await paymaster.connect(kcCreator).fundPaymaster(tokenAmount); + + expect(await paymaster.allowedAddresses(whitelistedUser.address)).to.be.false; + + await paymaster.connect(kcCreator).addAllowedAddress(whitelistedUser.address); + + expect(await paymaster.allowedAddresses(whitelistedUser.address)).to.be.true; + + await Token.connect(kcCreator).transfer(whitelistedUser.address, ethers.parseEther('10')); + + const { collectionId } = await createProfilesAndKC( + whitelistedUser, + publishingNode, + receivingNodes, + { Profile, KnowledgeCollection, Token }, + { paymaster: paymasterAddress, + tokenAmount: tokenAmount, + } + ); + + // Check that we can retrieve the collection and it has valid data + const collectionData = await KnowledgeCollectionStorage.getKnowledgeCollection(collectionId); + expect(collectionData.tokenAmount).to.equal(tokenAmount); + + // Check that token amount is properly set + const storedTokenAmount = await KnowledgeCollectionStorage.getTokenAmount(collectionId); + expect(storedTokenAmount).to.equal(tokenAmount); + + // Make sure paymaster was used correctly + const finalPaymasterBalance = await Token.balanceOf(paymasterAddress); + expect(finalPaymasterBalance).to.equal(0); // All tokens should have been spent + }); + + it('Non-whitelisted accounts cannot use the paymaster', async () => { + const kcCreator = getDefaultKCCreator(accounts); + const publishingNode = getDefaultPublishingNode(accounts); + const receivingNodes = getDefaultReceivingNodes(accounts); + const nonWhitelistedUser = accounts[7]; // This user will not be whitelisted + + // Deploy the paymaster owned by kcCreator + const { paymasterAddress } = await createPaymaster(kcCreator, PaymasterManager); + const paymaster = await hre.ethers.getContractAt('Paymaster', paymasterAddress); + + // Fund the paymaster with tokens + const tokenAmount = ethers.parseEther('100'); + await Token.connect(kcCreator).approve(paymasterAddress, tokenAmount); + await paymaster.connect(kcCreator).fundPaymaster(tokenAmount); + + // Verify the non-whitelisted user is indeed not whitelisted + expect(await paymaster.allowedAddresses(nonWhitelistedUser.address)).to.be.false; + + // Give the non-whitelisted user some tokens to create profiles + await Token.connect(kcCreator).transfer(nonWhitelistedUser.address, ethers.parseEther('10')); + + // When a non-whitelisted user tries to use the paymaster, the transaction should revert + await expect( + createProfilesAndKC( + nonWhitelistedUser, + publishingNode, + receivingNodes, + { Profile, KnowledgeCollection, Token }, + { paymaster: paymasterAddress, tokenAmount: tokenAmount } + ) + ).to.be.revertedWithCustomError(paymaster, 'NotAllowed'); + + // Verify paymaster's balance hasn't changed (no tokens were spent) + const finalPaymasterBalance = await Token.balanceOf(paymasterAddress); + expect(finalPaymasterBalance).to.equal(tokenAmount); + }); +}); From edbf1eb86832c768d1a6853ea18bae89d6da89e8 Mon Sep 17 00:00:00 2001 From: Marko Kostic Date: Tue, 18 Mar 2025 11:47:56 +0100 Subject: [PATCH 6/9] added 'Only KnowledgeCollection can call coverCost case --- test/integration/Paymaster.test.ts | 35 +++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/test/integration/Paymaster.test.ts b/test/integration/Paymaster.test.ts index a4e6707d..b38a9342 100644 --- a/test/integration/Paymaster.test.ts +++ b/test/integration/Paymaster.test.ts @@ -317,7 +317,6 @@ describe('@integration Paymaster', () => { const publishingNode = getDefaultPublishingNode(accounts); const receivingNodes = getDefaultReceivingNodes(accounts); const nonWhitelistedUser = accounts[7]; // This user will not be whitelisted - // Deploy the paymaster owned by kcCreator const { paymasterAddress } = await createPaymaster(kcCreator, PaymasterManager); const paymaster = await hre.ethers.getContractAt('Paymaster', paymasterAddress); @@ -348,4 +347,38 @@ describe('@integration Paymaster', () => { const finalPaymasterBalance = await Token.balanceOf(paymasterAddress); expect(finalPaymasterBalance).to.equal(tokenAmount); }); + + it('Non KnowledgeCollection address cant call coverCost', async () => { + const kcCreator = getDefaultKCCreator(accounts); + const nonKC = accounts[6]; // This will be our non-KnowledgeCollection address + + // Deploy the paymaster owned by kcCreator + const { paymasterAddress } = await createPaymaster(kcCreator, PaymasterManager); + const paymaster = await hre.ethers.getContractAt('Paymaster', paymasterAddress); + + // Fund the paymaster with tokens + const tokenAmount = ethers.parseEther('100'); + await Token.connect(kcCreator).approve(paymasterAddress, tokenAmount); + await paymaster.connect(kcCreator).fundPaymaster(tokenAmount); + + // Get the actual KnowledgeCollection address from the Hub + const hub = await hre.ethers.getContract('Hub'); + const knowledgeCollectionAddress = await hub.getContractAddress('KnowledgeCollection'); + + // Verify that nonKC is not the KnowledgeCollection address + expect(nonKC.address).to.not.equal(knowledgeCollectionAddress); + + await paymaster.connect(kcCreator).addAllowedAddress(nonKC.address); + + expect(await paymaster.allowedAddresses(nonKC.address)).to.be.true; + + // Try to call coverCost from nonKC address + await expect( + paymaster.connect(nonKC).coverCost(ethers.parseEther('10'), nonKC.address) + ).to.be.revertedWith('Sender is not the KnowledgeCollection contract'); + + // Verify paymaster's balance hasn't changed (no tokens were spent) + const finalPaymasterBalance = await Token.balanceOf(paymasterAddress); + expect(finalPaymasterBalance).to.equal(tokenAmount); + }); }); From ce661ed3cfa5aac9f9165e450fdb398a26b6faa9 Mon Sep 17 00:00:00 2001 From: Marko Kostic Date: Wed, 7 May 2025 10:51:55 +0200 Subject: [PATCH 7/9] removed deprecated helper function --- test/helpers/kc-helpers.ts | 58 -------------------- test/integration/Paymaster.test.ts | 86 +++++++++++++++++++++++------- 2 files changed, 67 insertions(+), 77 deletions(-) diff --git a/test/helpers/kc-helpers.ts b/test/helpers/kc-helpers.ts index b7a263cd..55804ee4 100644 --- a/test/helpers/kc-helpers.ts +++ b/test/helpers/kc-helpers.ts @@ -114,61 +114,3 @@ export async function createKnowledgeCollection( return { tx, receipt, collectionId }; } - -export async function createProfilesAndKC( - kcCreator: SignerWithAddress, - publishingNode: NodeAccounts, - receivingNodes: NodeAccounts[], - contracts: { - Profile: Profile; - KnowledgeCollection: KnowledgeCollection; - Token: Token; - }, - kcOptions?: { - publishOperationId?: string; - knowledgeAssetsAmount?: number; - byteSize?: number; - epochs?: number; - tokenAmount?: bigint; - isImmutable?: boolean; - paymaster?: string; - } -) { - const { identityId: publishingNodeIdentityId } = await createProfile( - contracts.Profile, - publishingNode, - ); - const receivingNodesIdentityIds = ( - await createProfiles(contracts.Profile, receivingNodes) - ).map((p) => p.identityId); - - // Create knowledge collection - const signaturesData = await getKCSignaturesData( - publishingNode, - publishingNodeIdentityId, - receivingNodes, - ); - const { collectionId } = await createKnowledgeCollection( - kcCreator, - publishingNodeIdentityId, - receivingNodesIdentityIds, - signaturesData, - contracts, - kcOptions?.publishOperationId, - kcOptions?.knowledgeAssetsAmount, - kcOptions?.byteSize, - kcOptions?.epochs, - kcOptions?.tokenAmount, - kcOptions?.isImmutable, - kcOptions?.paymaster - ); - - return { - publishingNode, - publishingNodeIdentityId, - receivingNodes, - receivingNodesIdentityIds, - kcCreator, - collectionId, - }; -} diff --git a/test/integration/Paymaster.test.ts b/test/integration/Paymaster.test.ts index b38a9342..27ba6c51 100644 --- a/test/integration/Paymaster.test.ts +++ b/test/integration/Paymaster.test.ts @@ -27,14 +27,18 @@ import { import { createPaymaster, } from '../helpers/paymaster-helpers'; -import { - createProfilesAndKC, -} from '../helpers/kc-helpers'; import { getDefaultPublishingNode, getDefaultReceivingNodes, getDefaultKCCreator, } from '../helpers/setup-helpers'; +import { + createProfile, createProfiles, +} from '../helpers/profile-helpers'; +import { + createKnowledgeCollection, +} from '../helpers/kc-helpers'; + // Fixture containing all contracts and accounts needed to test Paranet type PaymasterFixture = { @@ -237,16 +241,30 @@ describe('@integration Paymaster', () => { expect(deployer).to.equal(paymasterOwner); expect(kcCreator.address).to.equal(paymasterOwner); - const { collectionId } = await createProfilesAndKC( + const { identityId: publishingNodeIdentityId } = await createProfile( + Profile, + publishingNode, + ); + const receivingNodesIdentityIds = ( + await createProfiles(Profile, receivingNodes) + ).map((p) => p.identityId); + + const { collectionId } = await createKnowledgeCollection( kcCreator, publishingNode, + publishingNodeIdentityId, receivingNodes, - { Profile, KnowledgeCollection, Token }, - { paymaster: paymasterAddress, - tokenAmount: tokenAmount, - } + receivingNodesIdentityIds, + { KnowledgeCollection, Token }, + undefined, + undefined, + undefined, + undefined, + undefined, + tokenAmount, + undefined, + paymasterAddress ); - // Verify that the paymaster's token balance has decreased by the token amount // This confirms that tokens were transferred from the paymaster to the KC contract const finalPaymasterBalance = await Token.balanceOf(paymasterAddress); @@ -289,16 +307,30 @@ describe('@integration Paymaster', () => { await Token.connect(kcCreator).transfer(whitelistedUser.address, ethers.parseEther('10')); - const { collectionId } = await createProfilesAndKC( + const { identityId: publishingNodeIdentityId } = await createProfile( + Profile, + publishingNode, + ); + const receivingNodesIdentityIds = ( + await createProfiles(Profile, receivingNodes) + ).map((p) => p.identityId); + + const { collectionId } = await createKnowledgeCollection( whitelistedUser, publishingNode, + publishingNodeIdentityId, receivingNodes, - { Profile, KnowledgeCollection, Token }, - { paymaster: paymasterAddress, - tokenAmount: tokenAmount, - } + receivingNodesIdentityIds, + { KnowledgeCollection, Token }, + undefined, + undefined, + undefined, + undefined, + undefined, + tokenAmount, + undefined, + paymasterAddress ); - // Check that we can retrieve the collection and it has valid data const collectionData = await KnowledgeCollectionStorage.getKnowledgeCollection(collectionId); expect(collectionData.tokenAmount).to.equal(tokenAmount); @@ -332,17 +364,33 @@ describe('@integration Paymaster', () => { // Give the non-whitelisted user some tokens to create profiles await Token.connect(kcCreator).transfer(nonWhitelistedUser.address, ethers.parseEther('10')); + const { identityId: publishingNodeIdentityId } = await createProfile( + Profile, + publishingNode, + ); + const receivingNodesIdentityIds = ( + await createProfiles(Profile, receivingNodes) + ).map((p) => p.identityId); + // When a non-whitelisted user tries to use the paymaster, the transaction should revert await expect( - createProfilesAndKC( + createKnowledgeCollection( nonWhitelistedUser, publishingNode, + publishingNodeIdentityId, receivingNodes, - { Profile, KnowledgeCollection, Token }, - { paymaster: paymasterAddress, tokenAmount: tokenAmount } + receivingNodesIdentityIds, + { KnowledgeCollection, Token }, + undefined, + undefined, + undefined, + undefined, + undefined, + tokenAmount, + undefined, + paymasterAddress ) ).to.be.revertedWithCustomError(paymaster, 'NotAllowed'); - // Verify paymaster's balance hasn't changed (no tokens were spent) const finalPaymasterBalance = await Token.balanceOf(paymasterAddress); expect(finalPaymasterBalance).to.equal(tokenAmount); From d8b8e4cf79fa003402c88077eb05670d54648980 Mon Sep 17 00:00:00 2001 From: Marko Kostic Date: Wed, 7 May 2025 11:04:41 +0200 Subject: [PATCH 8/9] updated Paymaster contract ABI --- abi/IPaymaster.json | 5 +++++ abi/Paymaster.json | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/abi/IPaymaster.json b/abi/IPaymaster.json index 3506c7dc..81897d7d 100644 --- a/abi/IPaymaster.json +++ b/abi/IPaymaster.json @@ -18,6 +18,11 @@ "internalType": "uint256", "name": "amount", "type": "uint256" + }, + { + "internalType": "address", + "name": "originalSender", + "type": "address" } ], "name": "coverCost", diff --git a/abi/Paymaster.json b/abi/Paymaster.json index ab43392b..ad136726 100644 --- a/abi/Paymaster.json +++ b/abi/Paymaster.json @@ -5,6 +5,11 @@ "internalType": "address", "name": "hubAddress", "type": "address" + }, + { + "internalType": "address", + "name": "initialOwner", + "type": "address" } ], "stateMutability": "nonpayable", @@ -146,6 +151,11 @@ "internalType": "uint256", "name": "amount", "type": "uint256" + }, + { + "internalType": "address", + "name": "_originalSender", + "type": "address" } ], "name": "coverCost", From 0a0cc46341f42ea9043f4da63debfbe278a9dcb8 Mon Sep 17 00:00:00 2001 From: Marko Kostic Date: Thu, 19 Jun 2025 16:06:29 +0200 Subject: [PATCH 9/9] added events --- contracts/Paymaster.sol | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/contracts/Paymaster.sol b/contracts/Paymaster.sol index 57577c5c..7419651d 100644 --- a/contracts/Paymaster.sol +++ b/contracts/Paymaster.sol @@ -15,6 +15,11 @@ contract Paymaster is Ownable { mapping(address => bool) public allowedAddresses; + event AllowedAddressAdded(address _address); + event AllowedAddressRemoved(address _address); + event FundsAdded(address sender, uint256 amount); + event WhitdrawalMade(address recipient, uint256 amount); + modifier onlyAllowed(address _originalSender) { if (!allowedAddresses[_originalSender]) { revert NotAllowed(); @@ -29,10 +34,12 @@ contract Paymaster is Ownable { function addAllowedAddress(address _address) external onlyOwner { allowedAddresses[_address] = true; + emit AllowedAddressAdded(_address); } function removeAllowedAddress(address _address) external onlyOwner { allowedAddresses[_address] = false; + emit AllowedAddressRemoved(_address); } function fundPaymaster(uint256 amount) external { @@ -53,10 +60,13 @@ contract Paymaster is Ownable { if (!tokenContract.transferFrom(msg.sender, address(this), amount)) { revert TokenLib.TransferFailed(); } + + emit FundsAdded(msg.sender, amount); } function withdraw(address recipient, uint256 amount) external onlyOwner { _transferTokens(recipient, amount); + emit WhitdrawalMade(recipient, amount); } function coverCost(uint256 amount, address _originalSender) external onlyAllowed(_originalSender) {