diff --git a/contracts/solidity/contracts/KeepRandomBeaconOperator.sol b/contracts/solidity/contracts/KeepRandomBeaconOperator.sol index 1f6f0f7501..f275eb7631 100644 --- a/contracts/solidity/contracts/KeepRandomBeaconOperator.sol +++ b/contracts/solidity/contracts/KeepRandomBeaconOperator.sol @@ -583,13 +583,16 @@ contract KeepRandomBeaconOperator is ReentrancyGuard { /** * @dev Function used to inform about the fact the currently ongoing * new relay entry generation operation timed out. As a result, the group - * which was supposed to produce a new relay entry is immediatelly + * which was supposed to produce a new relay entry is immediately * terminated and a new group is selected to produce a new relay entry. + * All members of the group are punished by seizing minimum stake of + * their tokens. The submitter of the transaction is rewarded with a + * tattletale reward which is limited to min(1, 20 / group_size) of the + * maximum tattletale reward. */ function reportRelayEntryTimeout() public { require(hasEntryTimedOut(), "Entry did not time out"); - - groups.terminateGroup(signingRequest.groupIndex); + groups.reportRelayEntryTimeout(signingRequest.groupIndex, groupSize, minimumStake); // We could terminate the last active group. If that's the case, // do not try to execute signing again because there is no group diff --git a/contracts/solidity/contracts/libraries/operator/Groups.sol b/contracts/solidity/contracts/libraries/operator/Groups.sol index 803e7114ec..3f01ce728b 100644 --- a/contracts/solidity/contracts/libraries/operator/Groups.sol +++ b/contracts/solidity/contracts/libraries/operator/Groups.sol @@ -367,6 +367,17 @@ library Groups { return self.groupMembers[groupPubKey]; } + /** + * @dev Returns addresses of all the members in the provided group. + */ + function membersOf( + Storage storage self, + uint256 groupIndex + ) public view returns (address[] memory members) { + bytes memory groupPubKey = self.groups[groupIndex].groupPubKey; + return self.groupMembers[groupPubKey]; + } + /** * @dev Reports unauthorized signing for the provided group. Must provide * a valid signature of the group address as a message. Successful signature @@ -401,6 +412,19 @@ library Groups { } } + function reportRelayEntryTimeout( + Storage storage self, + uint256 groupIndex, + uint256 groupSize, + uint256 minimumStake + ) public { + terminateGroup(self, groupIndex); + // Reward is limited to min(1, 20 / group_size) of the maximum tattletale reward, see the Yellow Paper for more details. + uint256 rewardAdjustment = uint256(20 * 100).div(groupSize); // Reward adjustment in percentage + rewardAdjustment = rewardAdjustment > 100 ? 100:rewardAdjustment; // Reward adjustment can be 100% max + self.stakingContract.seize(minimumStake, rewardAdjustment, msg.sender, membersOf(self, groupIndex)); + } + /** * @dev Returns members of the given group by group public key. * diff --git a/contracts/solidity/test/TestKeepRandomBeaconOperatorSize.js b/contracts/solidity/test/TestKeepRandomBeaconOperatorSize.js index 5eb69b7d80..6dc71ead68 100644 --- a/contracts/solidity/test/TestKeepRandomBeaconOperatorSize.js +++ b/contracts/solidity/test/TestKeepRandomBeaconOperatorSize.js @@ -16,7 +16,7 @@ contract('KeepRandomBeaconOperator', (_) => { let bytecodeSize = bytecode.length / 2; // size in bytes let deployedBytecodeSize = deployedBytecode.length / 2; // size in bytes - const maxSafeBytecodeSize = 22482 + const maxSafeBytecodeSize = 22546 console.log( "KeepRandomBeaconOperator size of bytecode in bytes = ", diff --git a/contracts/solidity/test/TestKeepRandomBeaconOperatorSlashing.js b/contracts/solidity/test/TestKeepRandomBeaconOperatorSlashing.js index 9fad7198d4..457b0ca696 100644 --- a/contracts/solidity/test/TestKeepRandomBeaconOperatorSlashing.js +++ b/contracts/solidity/test/TestKeepRandomBeaconOperatorSlashing.js @@ -3,6 +3,7 @@ import stakeDelegate from './helpers/stakeDelegate' import {createSnapshot, restoreSnapshot} from "./helpers/snapshot" import {bls} from './helpers/data' import expectThrowWithMessage from './helpers/expectThrowWithMessage' +import mineBlocks from './helpers/mineBlocks' contract('KeepRandomBeaconOperator', function(accounts) { let token, stakingContract, serviceContract, operatorContract, minimumStake, largeStake, entryFeeEstimate, groupIndex, @@ -89,4 +90,26 @@ contract('KeepRandomBeaconOperator', function(accounts) { assert.isTrue((await token.balanceOf(tattletale)).isZero(), "Unexpected tattletale balance") }) + + it("should be able to report failure to produce entry after relay entry timeout", async () => { + let operator1balance = await stakingContract.balanceOf(operator1) + let operator2balance = await stakingContract.balanceOf(operator2) + let operator3balance = await stakingContract.balanceOf(operator3) + + await expectThrowWithMessage( + operatorContract.reportRelayEntryTimeout({from: tattletale}), + "Entry did not time out." + ) + + mineBlocks(20) + await operatorContract.reportRelayEntryTimeout({from: tattletale}) + + assert.isTrue((await stakingContract.balanceOf(operator1)).eq(operator1balance.sub(minimumStake)), "Unexpected operator 1 balance") + assert.isTrue((await stakingContract.balanceOf(operator2)).eq(operator2balance.sub(minimumStake)), "Unexpected operator 2 balance") + assert.isTrue((await stakingContract.balanceOf(operator3)).eq(operator3balance.sub(minimumStake)), "Unexpected operator 3 balance") + + // Expecting 5% of all the seized tokens with reward adjustment of (20 / 64) = 31% + let expectedTattletaleReward = minimumStake.muln(3).muln(5).divn(100).muln(31).divn(100) + assert.isTrue((await token.balanceOf(tattletale)).eq(expectedTattletaleReward), "Unexpected tattletale balance") + }) })