From 8c54d758a325a39aedd14b209564376ea1949dc2 Mon Sep 17 00:00:00 2001 From: Ian He <39037239+ianhe8x@users.noreply.github.com> Date: Mon, 7 Apr 2025 18:17:13 +1200 Subject: [PATCH 1/8] allow boost rewards to be used in channel openning --- contracts/StateChannel.sol | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/contracts/StateChannel.sol b/contracts/StateChannel.sol index 86c780d0..938c18db 100644 --- a/contracts/StateChannel.sol +++ b/contracts/StateChannel.sol @@ -213,13 +213,28 @@ contract StateChannel is Initializable, OwnableUpgradeable, SQParameter { _checkSign(payload, indexerSign, indexer, true); - // transfer the balance to contract - IERC20(settings.getContractAddress(SQContracts.SQToken)).safeTransferFrom( - consumer, - address(this), - amount + // transfer the rewards to channel + address rbAddress = settings.getContractAddress(SQContracts.RewardsBooster); + uint256 rewardsAmount = IRewardsBooster(rbAddress).spendQueryRewards( + channels[channelId].deploymentId, + realConsumer, + amount, + abi.encode(channelId) ); + if (rewardsAmount < amount) { + // transfer the balance to contract + uint256 realAmount = amount - rewardsAmount; + if (_isContract(consumer)) { + IConsumer(consumer).paid(channelId, msg.sender, realAmount, callback); + } + IERC20(settings.getContractAddress(SQContracts.SQToken)).safeTransferFrom( + consumer, + address(this), + realAmount + ); + } + // initial the channel ChannelState storage state = channels[channelId]; state.status = ChannelStatus.Open; From 1c7613b3454f1b04215444dd945d18dd6e20e3ec Mon Sep 17 00:00:00 2001 From: Ian He <39037239+ianhe8x@users.noreply.github.com> Date: Mon, 7 Apr 2025 18:18:59 +1200 Subject: [PATCH 2/8] fix --- contracts/StateChannel.sol | 3 --- 1 file changed, 3 deletions(-) diff --git a/contracts/StateChannel.sol b/contracts/StateChannel.sol index 938c18db..8dda7399 100644 --- a/contracts/StateChannel.sol +++ b/contracts/StateChannel.sol @@ -225,9 +225,6 @@ contract StateChannel is Initializable, OwnableUpgradeable, SQParameter { if (rewardsAmount < amount) { // transfer the balance to contract uint256 realAmount = amount - rewardsAmount; - if (_isContract(consumer)) { - IConsumer(consumer).paid(channelId, msg.sender, realAmount, callback); - } IERC20(settings.getContractAddress(SQContracts.SQToken)).safeTransferFrom( consumer, address(this), From 760723f0e307c46a33e80aaaf60fb4ef323cc98b Mon Sep 17 00:00:00 2001 From: Ian He <39037239+ianhe8x@users.noreply.github.com> Date: Mon, 7 Apr 2025 18:19:40 +1200 Subject: [PATCH 3/8] clean up --- contracts/StateChannel.sol | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/contracts/StateChannel.sol b/contracts/StateChannel.sol index 8dda7399..24f81aea 100644 --- a/contracts/StateChannel.sol +++ b/contracts/StateChannel.sol @@ -215,20 +215,18 @@ contract StateChannel is Initializable, OwnableUpgradeable, SQParameter { // transfer the rewards to channel address rbAddress = settings.getContractAddress(SQContracts.RewardsBooster); - uint256 rewardsAmount = IRewardsBooster(rbAddress).spendQueryRewards( + uint256 fundByReward = IRewardsBooster(rbAddress).spendQueryRewards( channels[channelId].deploymentId, realConsumer, amount, abi.encode(channelId) ); - if (rewardsAmount < amount) { - // transfer the balance to contract - uint256 realAmount = amount - rewardsAmount; + if (fundByReward < amount) { IERC20(settings.getContractAddress(SQContracts.SQToken)).safeTransferFrom( consumer, address(this), - realAmount + amount - fundByReward ); } From 89ada515a22bf60fddf5877ff68a008bff4c8643 Mon Sep 17 00:00:00 2001 From: Ian He <39037239+ianhe8x@users.noreply.github.com> Date: Mon, 7 Apr 2025 18:56:26 +1200 Subject: [PATCH 4/8] fix error --- contracts/StateChannel.sol | 103 ++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 53 deletions(-) diff --git a/contracts/StateChannel.sol b/contracts/StateChannel.sol index 24f81aea..a1ac6fb0 100644 --- a/contracts/StateChannel.sol +++ b/contracts/StateChannel.sol @@ -183,10 +183,12 @@ contract StateChannel is Initializable, OwnableUpgradeable, SQParameter { require(channels[channelId].status == ChannelStatus.Finalized, 'SC001'); // check indexer registered - IIndexerRegistry indexerRegistry = IIndexerRegistry( - settings.getContractAddress(SQContracts.IndexerRegistry) + require( + IIndexerRegistry(settings.getContractAddress(SQContracts.IndexerRegistry)).isIndexer( + indexer + ), + 'G002' ); - require(indexerRegistry.isIndexer(indexer), 'G002'); // check sign bytes32 payload = keccak256( @@ -203,9 +205,7 @@ contract StateChannel is Initializable, OwnableUpgradeable, SQParameter { ); if (_isContract(consumer)) { require(consumer.supportsInterface(type(IConsumer).interfaceId), 'G018'); - IConsumer cConsumer = IConsumer(consumer); - require(cConsumer.checkSign(channelId, payload, consumerSign), 'C006'); - cConsumer.paid(channelId, msg.sender, amount, callback); + require(IConsumer(consumer).checkSign(channelId, payload, consumerSign), 'C006'); } else { _checkSign(payload, consumerSign, consumer, false); require(msg.sender == consumer, 'SC111'); @@ -213,35 +213,23 @@ contract StateChannel is Initializable, OwnableUpgradeable, SQParameter { _checkSign(payload, indexerSign, indexer, true); - // transfer the rewards to channel - address rbAddress = settings.getContractAddress(SQContracts.RewardsBooster); - uint256 fundByReward = IRewardsBooster(rbAddress).spendQueryRewards( - channels[channelId].deploymentId, - realConsumer, - amount, - abi.encode(channelId) - ); - - if (fundByReward < amount) { - IERC20(settings.getContractAddress(SQContracts.SQToken)).safeTransferFrom( - consumer, - address(this), - amount - fundByReward - ); - } - // initial the channel ChannelState storage state = channels[channelId]; state.status = ChannelStatus.Open; state.indexer = indexer; state.consumer = consumer; state.expiredAt = block.timestamp + expiration; - state.realTotal = amount; - state.total = amount; + // commented by @ian, now they will be set in _fundChannel() + // state.realTotal = real; + // state.total = amount; state.spent = 0; state.terminatedAt = 0; state.deploymentId = deploymentId; state.terminateByIndexer = false; + + // transfer the rewards to channel + _fundChannel(channelId, deploymentId, consumer, amount, callback); + // set channel price channelPrice[channelId] = price; @@ -318,42 +306,16 @@ contract StateChannel is Initializable, OwnableUpgradeable, SQParameter { bytes32 payload = keccak256( abi.encode(channelId, indexer, consumer, preTotal, amount, callback) ); - address realConsumer = consumer; // check sign if (_isContract(consumer)) { - IConsumer cConsumer = IConsumer(consumer); - require(cConsumer.checkSign(channelId, payload, sign), 'C006'); - realConsumer = cConsumer.channelConsumer(channelId); + require(IConsumer(consumer).checkSign(channelId, payload, sign), 'C006'); } else { _checkSign(payload, sign, consumer, false); } // transfer the rewards to channel - address rbAddress = settings.getContractAddress(SQContracts.RewardsBooster); - uint256 rewardsAmount = IRewardsBooster(rbAddress).spendQueryRewards( - channels[channelId].deploymentId, - realConsumer, - amount, - abi.encode(channelId) - ); - - if (rewardsAmount < amount) { - // transfer the balance to contract - uint256 realAmount = amount - rewardsAmount; - if (_isContract(consumer)) { - IConsumer(consumer).paid(channelId, msg.sender, realAmount, callback); - } - IERC20(settings.getContractAddress(SQContracts.SQToken)).safeTransferFrom( - consumer, - address(this), - realAmount - ); - - channels[channelId].realTotal += realAmount; - } - - channels[channelId].total += amount; + _fundChannel(channelId, channels[channelId].deploymentId, consumer, amount, callback); emit ChannelFund(channelId, channels[channelId].realTotal, channels[channelId].total); } @@ -672,4 +634,39 @@ contract StateChannel is Initializable, OwnableUpgradeable, SQParameter { } return (size > 0); } + + function _fundChannel( + uint256 channelId, + bytes32 deploymentId, + address consumer, + uint256 amount, + bytes memory callback + ) internal { + address realConsumer = consumer; + if (_isContract(consumer)) { + IConsumer cConsumer = IConsumer(consumer); + realConsumer = cConsumer.channelConsumer(channelId); + } + // transfer the rewards to channel + uint256 fundByReward = IRewardsBooster( + settings.getContractAddress(SQContracts.RewardsBooster) + ).spendQueryRewards(deploymentId, realConsumer, amount, abi.encode(channelId)); + uint256 realAmount = 0; + if (fundByReward < amount) { + realAmount = amount - fundByReward; + if (_isContract(consumer)) { + IConsumer(consumer).paid(channelId, msg.sender, realAmount, callback); + } + IERC20(settings.getContractAddress(SQContracts.SQToken)).safeTransferFrom( + consumer, + address(this), + realAmount + ); + // return realAmount; + } + + channels[channelId].realTotal += realAmount; + channels[channelId].total += amount; + // return 0; + } } From 3c681fd4ce1a29d52ea08df12a81238bcc25a3ac Mon Sep 17 00:00:00 2001 From: Ian He <39037239+ianhe8x@users.noreply.github.com> Date: Mon, 7 Apr 2025 19:05:59 +1200 Subject: [PATCH 5/8] clean up --- contracts/StateChannel.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/StateChannel.sol b/contracts/StateChannel.sol index a1ac6fb0..34aad9e1 100644 --- a/contracts/StateChannel.sol +++ b/contracts/StateChannel.sol @@ -662,11 +662,9 @@ contract StateChannel is Initializable, OwnableUpgradeable, SQParameter { address(this), realAmount ); - // return realAmount; } channels[channelId].realTotal += realAmount; channels[channelId].total += amount; - // return 0; } } From 6a0ba51b03bf9066110d08b8445052baaef6a1a9 Mon Sep 17 00:00:00 2001 From: Jacob <3282625+icezohu@users.noreply.github.com> Date: Thu, 10 Apr 2025 17:36:19 +0800 Subject: [PATCH 6/8] add tests for opening state channels with booster rewards --- test/StateChannel.test.ts | 74 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/test/StateChannel.test.ts b/test/StateChannel.test.ts index 3bcc375f..3c912aef 100644 --- a/test/StateChannel.test.ts +++ b/test/StateChannel.test.ts @@ -299,6 +299,80 @@ describe('StateChannel Contract', () => { }); }); + describe('State Channel Rewards Open', () => { + const consumerInit = etherParse('10005'); + beforeEach(async () => { + await registerRunner(token, indexerRegistry, staking, wallet_0, runner, etherParse('2000')); + await token.connect(wallet_0).transfer(treasury.address, etherParse('100000')); + await token.connect(wallet_0).transfer(consumer.address, consumerInit); + await token.connect(consumer).increaseAllowance(stateChannel.address, etherParse('5')); + await token.connect(wallet_0).transfer(rewardsBooster.address, etherParse('5')); + + await boosterDeployment(token, rewardsBooster, consumer, deploymentId, etherParse('10000')); + }); + + it('open State Channel with booster rewards more than channel amount should work', async () => { + // 1000 blocks passed + await blockTravel(1000); + const queryRewardsBeforeCreating = await rewardsBooster.getQueryRewards(deploymentId, consumer.address); + // one block passed + await blockTravel(1); + const oneBlockRewards = (await rewardsBooster.getQueryRewards(deploymentId, consumer.address)).sub( + queryRewardsBeforeCreating + ); + // one block passed + await openChannel( + stateChannel, + defaultChannelId, + deploymentId, + runner, + consumer, + etherParse('1'), + etherParse('1'), + time.duration.days(1).toString() + ); + + const queryRewardsAfterCreating = await rewardsBooster.getQueryRewards(deploymentId, consumer.address); + + expect((await stateChannel.channel(defaultChannelId)).realTotal).to.equal(etherParse('0')); + expect((await stateChannel.channel(defaultChannelId)).total).to.equal(etherParse('1')); + + expect(await token.balanceOf(consumer.address)).to.equal(etherParse('5')); + expect(queryRewardsAfterCreating as BigNumber).to.equal( + queryRewardsBeforeCreating.sub(etherParse('1')).add(oneBlockRewards.mul(2)) + ); + }); + + it('open State Channel with booster rewards less than channel amount should work', async () => { + // one block passed + await blockTravel(1); + const oneBlockRewards = await rewardsBooster.getQueryRewards(deploymentId, consumer.address); + // one block passed + await openChannel( + stateChannel, + defaultChannelId, + deploymentId, + runner, + consumer, + etherParse('1'), + etherParse('1'), + time.duration.days(1).toString() + ); + + const queryRewardsAfterCreating = await rewardsBooster.getQueryRewards(deploymentId, consumer.address); + + expect((await stateChannel.channel(defaultChannelId)).realTotal).to.equal( + etherParse('1').sub(oneBlockRewards.mul(2)) + ); + expect((await stateChannel.channel(defaultChannelId)).total).to.equal(etherParse('1')); + + expect(await token.balanceOf(consumer.address)).to.equal( + etherParse('5').sub(etherParse('1').sub(oneBlockRewards.mul(2))) + ); + expect(queryRewardsAfterCreating as BigNumber).to.equal(etherParse('0')); + }); + }); + describe('State Channel Rewards Fund', () => { const consumerInit = etherParse('10005'); beforeEach(async () => { From 6da4782f47359c9496b24cdc772f2b6fdec8ca63 Mon Sep 17 00:00:00 2001 From: Jacob <3282625+icezohu@users.noreply.github.com> Date: Mon, 14 Apr 2025 12:11:59 +0800 Subject: [PATCH 7/8] fix test case, open channel will consume query rewards now --- test/RewardsBooster.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/RewardsBooster.test.ts b/test/RewardsBooster.test.ts index 5e57f892..aee2eac3 100644 --- a/test/RewardsBooster.test.ts +++ b/test/RewardsBooster.test.ts @@ -342,6 +342,7 @@ describe('RewardsBooster Contract', () => { // spend query rewards await token.connect(consumer0).increaseAllowance(stateChannel.address, etherParse('5')); + // open channel will consume query rewards now await openChannel( stateChannel, defaultChannelId, @@ -352,6 +353,9 @@ describe('RewardsBooster Contract', () => { etherParse('1'), 60 ); + const channel = await stateChannel.channel(defaultChannelId); + const consumedQueryReward = channel.total.sub(channel.realTotal); + const abi = ethers.utils.defaultAbiCoder; const msg = abi.encode( ['uint256', 'address', 'address', 'uint256', 'uint256', 'bytes'], @@ -366,7 +370,9 @@ describe('RewardsBooster Contract', () => { const queryReward1 = await rewardsBooster.getQueryRewards(deploymentId3, consumer0.address); const reward1 = await rewardsBooster.getAccRewardsForDeployment(deploymentId3); // has at least 1 block's reward, not zero - expect(queryReward1).to.eq(getQueryReward(reward1.sub(reward0), queryRewardRatePerMill)); + expect(queryReward1.add(consumedQueryReward)).to.eq( + getQueryReward(reward1.sub(reward0), queryRewardRatePerMill) + ); }); it('can add/remove booster - 1 booster account', async () => { From e73083c43d86beaee6666706337e6415021c3566 Mon Sep 17 00:00:00 2001 From: Jacob <3282625+icezohu@users.noreply.github.com> Date: Mon, 14 Apr 2025 12:13:14 +0800 Subject: [PATCH 8/8] fix test case, open channel may validate deployment project type now --- test/StateChannelFlow.test.ts | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/test/StateChannelFlow.test.ts b/test/StateChannelFlow.test.ts index 384636e1..1598fde4 100644 --- a/test/StateChannelFlow.test.ts +++ b/test/StateChannelFlow.test.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { ethers, waffle } from 'hardhat'; import { deployContracts } from './setup'; -import { deploymentIds } from './constants'; +import { deploymentIds, deploymentMetadatas, projectMetadatas } from './constants'; import { IndexerRegistry, RewardsPool, @@ -12,8 +12,10 @@ import { StateChannel, StakingManager, RewardsHelper, + ProjectRegistry, + ProjectType, } from '../src'; -import { registerRunner, startNewEra, time, etherParse } from './helper'; +import { registerRunner, startNewEra, time, etherParse, createProject } from './helper'; import { Wallet, BigNumber } from 'ethers'; describe('StateChannel Workflow Tests', () => { @@ -23,6 +25,7 @@ describe('StateChannel Workflow Tests', () => { let token: ERC20; let staking: Staking; let indexerRegistry: IndexerRegistry; + let projectRegistry: ProjectRegistry; let eraManager: EraManager; let rewardsDistributor: RewardsDistributor; let rewardsPool: RewardsPool; @@ -105,6 +108,7 @@ describe('StateChannel Workflow Tests', () => { beforeEach(async () => { const deployment = await waffle.loadFixture(deployer); indexerRegistry = deployment.indexerRegistry; + projectRegistry = deployment.projectRegistry; staking = deployment.staking; token = deployment.token; rewardsDistributor = deployment.rewardsDistributor; @@ -126,6 +130,24 @@ describe('StateChannel Workflow Tests', () => { await eraManager.connect(wallet_0).updateEraPeriod(time.duration.days(1).toString()); await startNewEra(eraManager); + // create projects + await createProject( + projectRegistry, + wallet_0, + projectMetadatas[0], + deploymentMetadatas[0], + deploymentIds[0], + ProjectType.SUBQUERY + ); + await createProject( + projectRegistry, + wallet_0, + projectMetadatas[0], + deploymentMetadatas[0], + deploymentIds[1], + ProjectType.SUBQUERY + ); + //create statechannels channelId = ethers.utils.randomBytes(32); channelId2 = ethers.utils.randomBytes(32);