Skip to content

Commit

Permalink
Merge pull request #1169 from keep-network/failure-to-produce-entry-s…
Browse files Browse the repository at this point in the history
…lashing

Slashing implementation - Seize stake if group fails to produce an entry

When a group fails to produce an entry, all of its members shall be
subject to seizing. The submitter of the trigger transaction shall be
treated as the tattletale, but the tattletale reward shall be limited to
min(1, 20 / group_size) of the maximum, or effectively the minimum stake
of a single member. This is to prevent actors in a lynchpin position from
profitably stealing other stakers' funds.
  • Loading branch information
pdyraga authored Jan 27, 2020
2 parents c3beb8d + d793dbb commit c0e823c
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 4 deletions.
9 changes: 6 additions & 3 deletions contracts/solidity/contracts/KeepRandomBeaconOperator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions contracts/solidity/contracts/libraries/operator/Groups.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ",
Expand Down
23 changes: 23 additions & 0 deletions contracts/solidity/test/TestKeepRandomBeaconOperatorSlashing.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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")
})
})

0 comments on commit c0e823c

Please sign in to comment.