From b7eb5ea2c41f42e43aac5998cbba0ec3dbea687f Mon Sep 17 00:00:00 2001 From: Ian He <39037239+ianhe8x@users.noreply.github.com> Date: Fri, 1 Nov 2024 23:43:43 +1300 Subject: [PATCH 1/2] support terminate state channel with current state --- contracts/StateChannel.sol | 59 ++++++++++++++++++++++++++--------- publish/ABI/StateChannel.json | 13 ++++++++ test/StateChannel.test.ts | 41 ++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 14 deletions(-) diff --git a/contracts/StateChannel.sol b/contracts/StateChannel.sol index f919dfc4..08bedcd9 100644 --- a/contracts/StateChannel.sol +++ b/contracts/StateChannel.sol @@ -375,7 +375,7 @@ contract StateChannel is Initializable, OwnableUpgradeable, SQParameter { emit ChannelCheckpoint(query.channelId, query.spent, query.isFinal); // update channel state - _settlement(query, false); + _settlement(query.channelId, query.spent, query.isFinal); } /** @@ -421,7 +421,37 @@ contract StateChannel is Initializable, OwnableUpgradeable, SQParameter { emit ChannelTerminate(query.channelId, query.spent, expiration, isIndexer); // update channel state. - _settlement(query, false); + _settlement(query.channelId, query.spent, query.isFinal); + } + + function terminateWithCurrentState(uint256 channelId) external { + ChannelState storage state = channels[channelId]; + + // check sender + bool isIndexer = msg.sender == state.indexer; + bool isConsumer = msg.sender == state.consumer; + if (!isIndexer && !isConsumer) { + address controller = IIndexerRegistry( + settings.getContractAddress(SQContracts.IndexerRegistry) + ).getController(state.indexer); + isIndexer = msg.sender == controller; + } + if (_isContract(state.consumer)) { + isConsumer = IConsumer(state.consumer).checkSender(channelId, msg.sender); + } + require(isIndexer || isConsumer, 'G008'); + + // set state to terminate + state.status = ChannelStatus.Terminating; + uint256 expiration = block.timestamp + terminateExpiration; + state.terminatedAt = expiration; + state.terminateByIndexer = isIndexer; + + emit ChannelTerminate(channelId, state.spent, expiration, isIndexer); + + // update channel state. + QueryState memory query = QueryState(channelId, state.spent, false, '', ''); + _settlement(query.channelId, query.spent, query.isFinal); } /** @@ -451,7 +481,7 @@ contract StateChannel is Initializable, OwnableUpgradeable, SQParameter { _checkStateSign(query.channelId, payload, query.indexerSign, query.consumerSign); // update channel state - _settlement(query, true); + _settlement(query.channelId, query.spent, true); } /** @@ -519,21 +549,22 @@ contract StateChannel is Initializable, OwnableUpgradeable, SQParameter { } /// @notice Settlement the new state - function _settlement(QueryState calldata query, bool finalize) private { + function _settlement(uint256 channelId, uint256 spent, bool isFinal) private { // update channel state - uint256 amount = query.spent - channels[query.channelId].spent; + ChannelState storage state = channels[channelId]; + uint256 amount = spent - state.spent; - if (channels[query.channelId].total > query.spent) { - channels[query.channelId].spent = query.spent; + if (state.total > spent) { + state.spent = spent; } else { - amount = channels[query.channelId].total - channels[query.channelId].spent; - channels[query.channelId].spent = channels[query.channelId].total; + amount = state.total - state.spent; + state.spent = state.total; } // reward pool if (amount > 0) { - address indexer = channels[query.channelId].indexer; - bytes32 deploymentId = channels[query.channelId].deploymentId; + address indexer = state.indexer; + bytes32 deploymentId = state.deploymentId; // rewards pool is deprecated // address rewardPoolAddress = settings.getContractAddress(SQContracts.RewardsPool); // IERC20(settings.getContractAddress(SQContracts.SQToken)).approve( @@ -559,12 +590,12 @@ contract StateChannel is Initializable, OwnableUpgradeable, SQParameter { amount, eraManager.safeUpdateAndGetEra() ); - emit ChannelLabor2(query.channelId, deploymentId, indexer, amount); + emit ChannelLabor2(channelId, deploymentId, indexer, amount); } // finalise channel if meet the requirements - if (finalize || query.isFinal) { - _finalize(query.channelId); + if (isFinal) { + _finalize(channelId); } } diff --git a/publish/ABI/StateChannel.json b/publish/ABI/StateChannel.json index b9e752ff..26434512 100644 --- a/publish/ABI/StateChannel.json +++ b/publish/ABI/StateChannel.json @@ -729,6 +729,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "channelId", + "type": "uint256" + } + ], + "name": "terminateWithCurrentState", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/test/StateChannel.test.ts b/test/StateChannel.test.ts index 167ea7a8..bd08cc9f 100644 --- a/test/StateChannel.test.ts +++ b/test/StateChannel.test.ts @@ -609,6 +609,47 @@ describe('StateChannel Contract', () => { expect(state.status).to.equal(0); }); + it('terminate State Channel with current onchain state', async () => { + await stateChannel.setTerminateExpiration(5); // 5s + + const channelId = ethers.utils.randomBytes(32); + await openChannel( + stateChannel, + channelId, + deploymentId, + runner, + consumer, + etherParse('1'), + etherParse('0.1'), + 60 + ); + + const query1 = await buildQueryState(channelId, runner, consumer, etherParse('0.1'), false); + await stateChannel.connect(runner).checkpoint(query1); + let state1 = await stateChannel.channel(channelId); + expect(state1.spent).to.equal(etherParse('0.1')); + + await expect(stateChannel.connect(runner).terminateWithCurrentState(channelId)).to.emit( + stateChannel, + 'ChannelTerminate' + ); + state1 = await stateChannel.channel(channelId); + expect(state1.status).to.equal(2); // Terminate + + await expect(stateChannel.claim(channelId)).to.be.revertedWith('SC008'); + + await delay(6); + await stateChannel.claim(channelId); + + const balance2 = await token.balanceOf(consumer.address); + expect(balance2).to.equal(etherParse('4.9')); + + await startNewEra(eraManager); + await rewardsHelper.connect(runner).indexerCatchup(runner.address); + const indexerReward = await rewardsDistributor.userRewards(runner.address, runner.address); + + expect(indexerReward).to.eq(etherParse('0.1')); + }); /** * when only one indexer in the pool and that indexer unregistered, * channel can still be terminated, consumer can claim the channel token From 460c52f1499dd22430e084b3b30b23eda2dcf5fb Mon Sep 17 00:00:00 2001 From: Ian He <39037239+ianhe8x@users.noreply.github.com> Date: Fri, 1 Nov 2024 23:45:08 +1300 Subject: [PATCH 2/2] update --- contracts/StateChannel.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/StateChannel.sol b/contracts/StateChannel.sol index 08bedcd9..9566dc59 100644 --- a/contracts/StateChannel.sol +++ b/contracts/StateChannel.sol @@ -450,8 +450,7 @@ contract StateChannel is Initializable, OwnableUpgradeable, SQParameter { emit ChannelTerminate(channelId, state.spent, expiration, isIndexer); // update channel state. - QueryState memory query = QueryState(channelId, state.spent, false, '', ''); - _settlement(query.channelId, query.spent, query.isFinal); + _settlement(channelId, state.spent, false); } /**