Skip to content

Commit

Permalink
[Slashing] Slash locked gold (#2317)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasege authored and celo-ci-bot-user committed Jan 2, 2020
1 parent f7ca165 commit 97f2f31
Show file tree
Hide file tree
Showing 7 changed files with 330 additions and 22 deletions.
79 changes: 78 additions & 1 deletion packages/protocol/contracts/governance/LockedGold.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pragma solidity ^0.5.3;

import "openzeppelin-solidity/contracts/math/Math.sol";
import "openzeppelin-solidity/contracts/utils/ReentrancyGuard.sol";
import "openzeppelin-solidity/contracts/math/SafeMath.sol";
import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
Expand Down Expand Up @@ -31,13 +32,28 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr
}

mapping(address => Balances) private balances;
mapping(address => bool) public isSlasher;

modifier onlySlasher() {
require(isSlasher[msg.sender], "Caller must be registered slasher");
_;
}

uint256 public totalNonvoting;
uint256 public unlockingPeriod;

event UnlockingPeriodSet(uint256 period);
event GoldLocked(address indexed account, uint256 value);
event GoldUnlocked(address indexed account, uint256 value, uint256 available);
event GoldWithdrawn(address indexed account, uint256 value);
event SlasherWhitelistAdded(address indexed slasher);
event SlasherWhitelistRemoved(address indexed slasher);
event AccountSlashed(
address indexed slashed,
uint256 penalty,
address indexed reporter,
uint256 reward
);

function initialize(address registryAddress, uint256 _unlockingPeriod) external initializer {
_transferOwnership(msg.sender);
Expand Down Expand Up @@ -238,6 +254,43 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr
list.length = lastIndex;
}

/**
* @notice Adds `slasher` to whitelist of approved slashing addresses.
* @param slasher Address to whitelist.
*/
function addSlasher(address slasher) external onlyOwner {
require(slasher != address(0) && !isSlasher[slasher], "Invalid address to `addSlasher`.");
isSlasher[slasher] = true;
emit SlasherWhitelistAdded(slasher);
}

/**
* @notice Removes `slasher` from whitelist of approved slashing addresses.
* @param slasher Address to remove from whitelist.
*/
function removeSlasher(address slasher) external onlyOwner {
require(isSlasher[slasher], "Address must be valid slasher.");
isSlasher[slasher] = false;
emit SlasherWhitelistRemoved(slasher);
}

/**
* @notice Slashes `account` by reducing its nonvoting locked gold by `penalty`.
* If there is not enough nonvoting locked gold to slash, calls into
* `Election.slashVotes` to slash the remaining gold. If `account` does not have
* `penalty` worth of locked gold, slashes `account`'s total locked gold.
* Also sends `reward` gold to the reporter, and penalty-reward to the Community Fund.
* @param account Address of account being slashed.
* @param penalty Amount to slash account.
* @param reporter Address of account reporting the slasher.
* @param reward Reward to give reporter.
* @param lessers The groups receiving fewer votes than i'th group, or 0 if the i'th group has
* the fewest votes of any validator group.
* @param greaters The groups receiving more votes than the i'th group, or 0 if the i'th group
* has the most votes of any validator group.
* @param indices The indices of the i'th group in `account`'s voting list.
* @dev Fails if `reward` is greater than `account`'s total locked gold.
*/
function slash(
address account,
uint256 penalty,
Expand All @@ -246,5 +299,29 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr
address[] calldata lessers,
address[] calldata greaters,
uint256[] calldata indices
) external {}
) external onlySlasher {
uint256 maxSlash = Math.min(penalty, getAccountTotalLockedGold(account));
require(maxSlash >= reward, "reward cannot exceed penalty.");
// Local scoping is required to avoid Solc "stack too deep" error from too many locals.
{
uint256 nonvotingBalance = balances[account].nonvoting;
uint256 difference = 0;
// If not enough nonvoting, revoke the difference
if (nonvotingBalance < maxSlash) {
difference = maxSlash.sub(nonvotingBalance);
require(
getElection().forceDecrementVotes(account, difference, lessers, greaters, indices) ==
difference,
"Cannot revoke enough voting gold."
);
}
// forceDecrementVotes does not increment nonvoting account balance, so we can't double count
_decrementNonvotingAccountBalance(account, maxSlash.sub(difference));
_incrementNonvotingAccountBalance(reporter, reward);
}
address communityFund = registry.getAddressForOrDie(GOVERNANCE_REGISTRY_ID);
address payable communityFundPayable = address(uint160(communityFund));
communityFundPayable.transfer(maxSlash.sub(reward));
emit AccountSlashed(account, maxSlash, reporter, reward);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,11 @@ interface IElection {
function markGroupIneligible(address) external;
function markGroupEligible(address, address, address) external;
function electValidatorSigners() external view returns (address[] memory);
function forceDecrementVotes(
address,
uint256,
address[] calldata,
address[] calldata,
uint256[] calldata
) external returns (uint256);
}
11 changes: 11 additions & 0 deletions packages/protocol/contracts/governance/test/MockElection.sol
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,15 @@ contract MockElection is IElection {
function electValidatorSigners() external view returns (address[] memory) {
return electedValidators;
}

function forceDecrementVotes(
address,
uint256 value,
address[] calldata,
address[] calldata,
uint256[] calldata
) external returns (uint256) {
this.setActiveVotes(this.getActiveVotes() - value);
return value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import "../interfaces/IGovernance.sol";
contract MockGovernance is IGovernance {
mapping(address => bool) public isVoting;

function() external payable {} // solhint-disable no-empty-blocks

function setVoting(address voter) external {
isVoting[voter] = true;
}
Expand Down
16 changes: 13 additions & 3 deletions packages/protocol/migrations/19_governance_slasher.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { CeloContractName } from '@celo/protocol/lib/registry-utils'
import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils'
import {
deploymentForCoreContract,
getDeployedProxiedContract,
} from '@celo/protocol/lib/web3-utils'
import { config } from '@celo/protocol/migrationsConfig'
import { GovernanceSlasherInstance } from 'types'
import { GovernanceSlasherInstance, LockedGoldInstance } from 'types'

const initializeArgs = async (_: string): Promise<any[]> => {
return [config.registry.predeployedProxyAddress]
Expand All @@ -11,5 +14,12 @@ module.exports = deploymentForCoreContract<GovernanceSlasherInstance>(
web3,
artifacts,
CeloContractName.GovernanceSlasher,
initializeArgs
initializeArgs,
async (slasher: GovernanceSlasherInstance) => {
const lockedGold: LockedGoldInstance = await getDeployedProxiedContract<LockedGoldInstance>(
'LockedGold',
artifacts
)
await lockedGold.addSlasher(slasher.address)
}
)
Loading

0 comments on commit 97f2f31

Please sign in to comment.