diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md index 299b686d0..5b15e04f7 100644 --- a/contracts/CHANGELOG.md +++ b/contracts/CHANGELOG.md @@ -54,6 +54,8 @@ The format is based on [Common Changelog](https://common-changelog.org/). ### Fixed - Do not pass to Voting period if all the commits are cast because it breaks the current Shutter auto-reveal process. ([#2085](https://github.com/kleros/kleros-v2/issues/2085)) +- Do not emit the `KlerosCore.TokenAndETHShift` event if the both the PNK and fee amounts are zero ([#2135](https://github.com/kleros/kleros-v2/issues/2135)) +- Do not make PNK or ETH transfers if the amounts are zero ([#2135](https://github.com/kleros/kleros-v2/issues/2135)) ## [0.12.0] - 2025-08-05 diff --git a/contracts/src/arbitration/KlerosCore.sol b/contracts/src/arbitration/KlerosCore.sol index fab1f2882..589da7c48 100644 --- a/contracts/src/arbitration/KlerosCore.sol +++ b/contracts/src/arbitration/KlerosCore.sol @@ -925,17 +925,19 @@ contract KlerosCore is IArbitratorV2, Initializable, UUPSProxiable { penalizedInCourtID, penalty ); - _params.pnkPenaltiesInRound += availablePenalty; - emit TokenAndETHShift( - account, - _params.disputeID, - _params.round, - coherence, - 0, - -int256(availablePenalty), - 0, - round.feeToken - ); + if (availablePenalty != 0) { + _params.pnkPenaltiesInRound += availablePenalty; + emit TokenAndETHShift( + account, + _params.disputeID, + _params.round, + coherence, + 0, + -int256(availablePenalty), + 0, + round.feeToken + ); + } if (pnkBalance == 0 || !disputeKit.isVoteActive(_params.disputeID, _params.round, _params.repartition)) { // The juror is inactive or their balance is can't cover penalties anymore, unstake them from all courts. @@ -996,24 +998,28 @@ contract KlerosCore is IArbitratorV2, Initializable, UUPSProxiable { uint256 feeReward = _applyCoherence(round.totalFeesForJurors / _params.coherentCount, feeCoherence); round.sumFeeRewardPaid += feeReward; - // Transfer the fee reward - _transferFeeToken(round.feeToken, payable(account), feeReward); - - // Stake the PNK reward if possible, bypasses delayed stakes and other checks done by validateStake() - if (!sortitionModule.setStakeReward(account, dispute.courtID, pnkReward)) { - pinakion.safeTransfer(account, pnkReward); + if (feeReward != 0) { + // Transfer the fee reward + _transferFeeToken(round.feeToken, payable(account), feeReward); + } + if (pnkReward != 0) { + // Stake the PNK reward if possible, bypasses delayed stakes and other checks done by validateStake() + if (!sortitionModule.setStakeReward(account, dispute.courtID, pnkReward)) { + pinakion.safeTransfer(account, pnkReward); + } + } + if (pnkReward != 0 || feeReward != 0) { + emit TokenAndETHShift( + account, + _params.disputeID, + _params.round, + pnkCoherence, + feeCoherence, + int256(pnkReward), + int256(feeReward), + round.feeToken + ); } - - emit TokenAndETHShift( - account, - _params.disputeID, - _params.round, - pnkCoherence, - feeCoherence, - int256(pnkReward), - int256(feeReward), - round.feeToken - ); // Transfer any residual rewards to the owner. It may happen due to partial coherence of the jurors. if (_params.repartition == _params.numberOfVotesInRound * 2 - 1) { diff --git a/contracts/test/foundry/KlerosCore_Execution.t.sol b/contracts/test/foundry/KlerosCore_Execution.t.sol index 7681eff29..e0677b81a 100644 --- a/contracts/test/foundry/KlerosCore_Execution.t.sol +++ b/contracts/test/foundry/KlerosCore_Execution.t.sol @@ -103,12 +103,6 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { // Check iterations for the winning staker to see the shifts vm.expectEmit(true, true, true, true); emit SortitionModule.StakeLocked(staker2, 0, true); - vm.expectEmit(true, true, true, true); - emit KlerosCore.TokenAndETHShift(staker2, disputeID, 0, 10000, 0, 0, 0, IERC20(address(0))); // penalties but amounts are 0 - vm.expectEmit(true, true, true, true); - emit SortitionModule.StakeLocked(staker2, 0, true); - vm.expectEmit(true, true, true, true); - emit KlerosCore.TokenAndETHShift(staker2, disputeID, 0, 10000, 0, 0, 0, IERC20(address(0))); // penalties but amounts are 0 core.execute(disputeID, 0, 3); // Do 3 iterations to check penalties first (uint256 totalStaked, uint256 totalLocked, , ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); @@ -123,8 +117,6 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { vm.expectEmit(true, true, true, true); emit SortitionModule.StakeLocked(staker1, 0, true); - vm.expectEmit(true, true, true, true); - emit KlerosCore.TokenAndETHShift(staker1, disputeID, 0, 0, 0, 0, 0, IERC20(address(0))); // rewards but amounts are 0 // Check iterations for the winning staker to see the shifts vm.expectEmit(true, true, true, true); emit SortitionModule.StakeLocked(staker2, 1000, true); @@ -538,8 +530,6 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { // Check only once per penalty and per reward vm.expectEmit(true, true, true, true); - emit KlerosCore.TokenAndETHShift(staker1, disputeID, 0, 10000, 0, 0, 0, feeToken); // penalties but amounts are 0 - vm.expectEmit(true, true, true, true); emit KlerosCore.TokenAndETHShift(staker1, disputeID, 0, 10000, 10000, 0, 0.06 ether, feeToken); // rewards core.execute(disputeID, 0, 6); diff --git a/contracts/test/integration/index.ts b/contracts/test/integration/index.ts index c8a656357..3884c9712 100644 --- a/contracts/test/integration/index.ts +++ b/contracts/test/integration/index.ts @@ -131,23 +131,23 @@ describe("Integration tests", async () => { throw new Error("Block hash is null - cannot calculate dispute hash"); } // Relayer tx - const tx2 = await homeGateway - .connect(relayer) - ["relayCreateDispute((bytes32,uint256,address,uint256,uint256,uint256,uint256,bytes))"]( - { - foreignBlockHash: ethers.toBeHex(lastBlock.hash), - foreignChainID: 31337, - foreignArbitrable: arbitrable.target, - foreignDisputeID: disputeId, - externalDisputeID: ethers.keccak256(ethers.toUtf8Bytes("future of france")), - templateId: 0, - choices: 2, - extraData: "0x00", - }, - { value: arbitrationCost } - ); - expect(tx2).to.emit(homeGateway, "DisputeRequest"); - await tx2.wait(); + await expect( + homeGateway + .connect(relayer) + ["relayCreateDispute((bytes32,uint256,address,uint256,uint256,uint256,uint256,bytes))"]( + { + foreignBlockHash: ethers.toBeHex(lastBlock.hash), + foreignChainID: 31337, + foreignArbitrable: arbitrable.target, + foreignDisputeID: disputeId, + externalDisputeID: ethers.keccak256(ethers.toUtf8Bytes("future of france")), + templateId: 0, + choices: 2, + extraData: "0x00", + }, + { value: arbitrationCost } + ) + ).to.emit(homeGateway, "DisputeRequest"); await network.provider.send("evm_increaseTime", [2000]); // Wait for minStakingTime await network.provider.send("evm_mine"); @@ -186,12 +186,16 @@ describe("Integration tests", async () => { await core.passPeriod(0); expect((await core.disputes(0)).period).to.equal(Period.execution); - expect(await core.execute(0, 0, 1000)).to.emit(core, "TokenAndETHShift"); - - const tx4 = await core.executeRuling(0, { gasLimit: 10000000, gasPrice: 5000000000 }); + await expect(core.execute(0, 0, 1000)) + .to.emit(core, "TokenAndETHShift") + .withArgs(deployer, 0, 0, 10000, 10000, 0, arbitrationCost / 3n, ethers.ZeroAddress); + + await expect(core.executeRuling(0, { gasLimit: 10000000, gasPrice: 5000000000 })) + .to.emit(core, "Ruling") + .withArgs(homeGateway.target, 0, 0) + .and.to.emit(arbitrable, "Ruling") + .withArgs(foreignGateway.target, 1, 0); // The ForeignGateway starts counting disputeID from 1. console.log("Ruling executed on KlerosCore"); - expect(tx4).to.emit(core, "Ruling").withArgs(homeGateway.target, 0, 0); - expect(tx4).to.emit(arbitrable, "Ruling").withArgs(foreignGateway.target, 1, 0); // The ForeignGateway starts counting disputeID from 1. }); const mineBlocks = async (n: number) => {