From 3fbb0176f907e6c7510ed0c98d81f29fe3611e46 Mon Sep 17 00:00:00 2001 From: lucasege Date: Tue, 17 Dec 2019 13:06:47 -0600 Subject: [PATCH] Adding slashing multiplier to validators.sol and reward calculation (#2239) --- packages/dev-utils/src/ganache-setup.ts | 2 +- .../contracts/governance/Election.sol | 16 ++- .../contracts/governance/Validators.sol | 97 ++++++++++++++++--- .../governance/interfaces/IValidators.sol | 1 + .../governance/test/MockValidators.sol | 4 + packages/protocol/migrations/12_validators.ts | 1 + packages/protocol/migrationsConfig.js | 1 + packages/protocol/runTests.js | 2 +- packages/protocol/scripts/devchain.ts | 2 +- .../protocol/test/governance/validators.ts | 85 ++++++++++++++++ packages/protocol/truffle-config.js | 2 +- 11 files changed, 192 insertions(+), 21 deletions(-) diff --git a/packages/dev-utils/src/ganache-setup.ts b/packages/dev-utils/src/ganache-setup.ts index 0e18fcd5bc5..3c45a52211f 100644 --- a/packages/dev-utils/src/ganache-setup.ts +++ b/packages/dev-utils/src/ganache-setup.ts @@ -43,7 +43,7 @@ export async function startGanache(datadir: string, opts: { verbose?: boolean } network_id: 1101, db_path: datadir, mnemonic: MNEMONIC, - gasLimit: 10000000, + gasLimit: 15000000, allowUnlimitedContractSize: true, }) diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 8f48c587d23..3f45de85f58 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -6,6 +6,7 @@ import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "openzeppelin-solidity/contracts/utils/ReentrancyGuard.sol"; import "./interfaces/IElection.sol"; +import "./interfaces/IValidators.sol"; import "../common/Initializable.sol"; import "../common/FixidityLib.sol"; import "../common/linkedlists/AddressSortedLinkedList.sol"; @@ -430,8 +431,9 @@ contract Election is uint256 totalEpochRewards, uint256[] calldata uptimes ) external view returns (uint256) { + IValidators validators = getValidators(); // The group must meet the balance requirements for their voters to receive epoch rewards. - if (!getValidators().meetsAccountLockedGoldRequirements(group) || votes.active.total <= 0) { + if (!validators.meetsAccountLockedGoldRequirements(group) || votes.active.total <= 0) { return 0; } @@ -440,10 +442,18 @@ contract Election is votes.active.total ); FixidityLib.Fraction memory score = FixidityLib.wrap( - getValidators().calculateGroupEpochScore(uptimes) + validators.calculateGroupEpochScore(uptimes) + ); + FixidityLib.Fraction memory slashingMultiplier = FixidityLib.wrap( + validators.getValidatorGroupSlashingMultiplier(group) ); return - FixidityLib.newFixed(totalEpochRewards).multiply(votePortion).multiply(score).fromFixed(); + FixidityLib + .newFixed(totalEpochRewards) + .multiply(votePortion) + .multiply(score) + .multiply(slashingMultiplier) + .fromFixed(); } /** diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index ab062c2c7c4..9ec6e24d540 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -60,6 +60,7 @@ contract Validators is FixidityLib.Fraction commission; // sizeHistory[i] contains the last time the group contained i members. uint256[] sizeHistory; + SlashingInfo slashInfo; } // Stores the epoch number at which a validator joined a particular group. @@ -81,6 +82,11 @@ contract Validators is uint256 lastRemovedFromGroupTimestamp; } + struct SlashingInfo { + FixidityLib.Fraction multiplier; + uint256 lastSlashed; + } + struct PublicKeys { bytes ecdsa; bytes bls; @@ -108,6 +114,7 @@ contract Validators is ValidatorScoreParameters private validatorScoreParameters; uint256 public membershipHistoryLength; uint256 public maxGroupSize; + uint256 public slashingMultiplierResetPeriod; event MaxGroupSizeSet(uint256 size); event ValidatorEpochPaymentSet(uint256 value); @@ -162,6 +169,7 @@ contract Validators is uint256 validatorScoreExponent, uint256 validatorScoreAdjustmentSpeed, uint256 _membershipHistoryLength, + uint256 _slashingMultiplierResetPeriod, uint256 _maxGroupSize ) external initializer { _transferOwnership(msg.sender); @@ -171,6 +179,7 @@ contract Validators is setValidatorScoreParameters(validatorScoreExponent, validatorScoreAdjustmentSpeed); setMaxGroupSize(_maxGroupSize); setMembershipHistoryLength(_membershipHistoryLength); + setSlashingMultiplierResetPeriod(_slashingMultiplierResetPeriod); } /** @@ -282,7 +291,7 @@ contract Validators is bytes calldata blsPublicKey, bytes calldata blsPop ) external nonReentrant returns (bool) { - address account = getAccounts().signerToAccount(msg.sender); + address account = getAccounts().validatorSignerToAccount(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); uint256 lockedGoldBalance = getLockedGold().getAccountTotalLockedGold(account); require(lockedGoldBalance >= validatorLockedGoldRequirements.value); @@ -454,7 +463,7 @@ contract Validators is * @dev Fails if the validator has been a member of a group too recently. */ function deregisterValidator(uint256 index) external nonReentrant returns (bool) { - address account = getAccounts().signerToAccount(msg.sender); + address account = getAccounts().validatorSignerToAccount(msg.sender); require(isValidator(account)); // Require that the validator has not been a member of a validator group for @@ -482,7 +491,7 @@ contract Validators is * @dev De-affiliates with the previously affiliated group if present. */ function affiliate(address group) external nonReentrant returns (bool) { - address account = getAccounts().signerToAccount(msg.sender); + address account = getAccounts().validatorSignerToAccount(msg.sender); require(isValidator(account) && isValidatorGroup(group)); require(meetsAccountLockedGoldRequirements(account)); require(meetsAccountLockedGoldRequirements(group)); @@ -501,7 +510,7 @@ contract Validators is * @dev Fails if the account is not a validator with non-zero affiliation. */ function deaffiliate() external nonReentrant returns (bool) { - address account = getAccounts().signerToAccount(msg.sender); + address account = getAccounts().validatorSignerToAccount(msg.sender); require(isValidator(account)); Validator storage validator = validators[account]; require(validator.affiliation != address(0)); @@ -521,7 +530,7 @@ contract Validators is external returns (bool) { - address account = getAccounts().signerToAccount(msg.sender); + address account = getAccounts().validatorSignerToAccount(msg.sender); require(isValidator(account)); Validator storage validator = validators[account]; _updateBlsPublicKey(validator, account, blsPublicKey, blsPop); @@ -603,13 +612,14 @@ contract Validators is */ function registerValidatorGroup(uint256 commission) external nonReentrant returns (bool) { require(commission <= FixidityLib.fixed1().unwrap(), "Commission can't be greater than 100%"); - address account = getAccounts().signerToAccount(msg.sender); + address account = getAccounts().validatorSignerToAccount(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); uint256 lockedGoldBalance = getLockedGold().getAccountTotalLockedGold(account); require(lockedGoldBalance >= groupLockedGoldRequirements.value); ValidatorGroup storage group = groups[account]; group.exists = true; group.commission = FixidityLib.wrap(commission); + group.slashInfo = SlashingInfo(FixidityLib.fixed1(), 0); registeredGroups.push(account); emit ValidatorGroupRegistered(account, commission); return true; @@ -623,7 +633,7 @@ contract Validators is * @dev Fails if the group has had members too recently. */ function deregisterValidatorGroup(uint256 index) external nonReentrant returns (bool) { - address account = getAccounts().signerToAccount(msg.sender); + address account = getAccounts().validatorSignerToAccount(msg.sender); // Only Validator Groups that have never had members or have been empty for at least // `groupLockedGoldRequirements.duration` seconds can be deregistered. require(isValidatorGroup(account) && groups[account].members.numElements == 0); @@ -645,7 +655,7 @@ contract Validators is * @dev Fails if the group has zero members. */ function addMember(address validator) external nonReentrant returns (bool) { - address account = getAccounts().signerToAccount(msg.sender); + address account = getAccounts().validatorSignerToAccount(msg.sender); require(groups[account].members.numElements > 0); return _addMember(account, validator, address(0), address(0)); } @@ -664,7 +674,7 @@ contract Validators is nonReentrant returns (bool) { - address account = getAccounts().signerToAccount(msg.sender); + address account = getAccounts().validatorSignerToAccount(msg.sender); require(groups[account].members.numElements == 0); return _addMember(account, validator, lesser, greater); } @@ -707,7 +717,7 @@ contract Validators is * @dev Fails if `validator` is not a member of the account's group. */ function removeMember(address validator) external nonReentrant returns (bool) { - address account = getAccounts().signerToAccount(msg.sender); + address account = getAccounts().validatorSignerToAccount(msg.sender); require(isValidatorGroup(account) && isValidator(validator), "is not group and validator"); return _removeMember(account, validator); } @@ -727,7 +737,7 @@ contract Validators is nonReentrant returns (bool) { - address account = getAccounts().signerToAccount(msg.sender); + address account = getAccounts().validatorSignerToAccount(msg.sender); require(isValidatorGroup(account) && isValidator(validator)); ValidatorGroup storage group = groups[account]; require(group.members.contains(validator)); @@ -743,7 +753,7 @@ contract Validators is * @return True upon success. */ function updateCommission(uint256 commission) external returns (bool) { - address account = getAccounts().signerToAccount(msg.sender); + address account = getAccounts().validatorSignerToAccount(msg.sender); require(isValidatorGroup(account)); ValidatorGroup storage group = groups[account]; require(commission <= FixidityLib.fixed1().unwrap(), "Commission can't be greater than 100%"); @@ -837,11 +847,17 @@ contract Validators is function getValidatorGroup(address account) external view - returns (address[] memory, uint256, uint256[] memory) + returns (address[] memory, uint256, uint256[] memory, uint256, uint256) { require(isValidatorGroup(account)); ValidatorGroup storage group = groups[account]; - return (group.members.getKeys(), group.commission.unwrap(), group.sizeHistory); + return ( + group.members.getKeys(), + group.commission.unwrap(), + group.sizeHistory, + group.slashInfo.multiplier.unwrap(), + group.slashInfo.lastSlashed + ); } /** @@ -1131,4 +1147,57 @@ contract Validators is } } } + + /** + * @notice Sets the slashingMultiplierRestPeriod property if called by owner. + * @param value New reset period for slashing multiplier. + */ + function setSlashingMultiplierResetPeriod(uint256 value) public nonReentrant onlyOwner { + slashingMultiplierResetPeriod = value; + } + + /** + * @notice Resets a group's slashing multiplier if it has been >= the reset period since + * the last time the group was slashed. + */ + function resetSlashingMultiplier() external nonReentrant { + address account = getAccounts().validatorSignerToAccount(msg.sender); + require(isValidatorGroup(account)); + ValidatorGroup storage group = groups[account]; + require( + now >= group.slashInfo.lastSlashed.add(slashingMultiplierResetPeriod), + "`resetSlashingMultiplier` called before resetPeriod expired" + ); + group.slashInfo.multiplier = FixidityLib.fixed1(); + } + + bytes32[] canHalveSlashingMultiplier = [ + DOWNTIME_SLASHER_REGISTRY_ID, + DOUBLE_SIGNING_SLASHER_REGISTRY_ID + ]; + + /** + * @notice Halves the group's slashing multiplier. + * @param account The group being slashed. + */ + function halveSlashingMultiplier(address account) + external + nonReentrant + onlyRegisteredContracts(canHalveSlashingMultiplier) + { + require(isValidatorGroup(account)); + ValidatorGroup storage group = groups[account]; + group.slashInfo.multiplier = FixidityLib.wrap(group.slashInfo.multiplier.unwrap().div(2)); + group.slashInfo.lastSlashed = now; + } + + /** + * @notice Getter for a group's slashing multiplier. + * @param account The group to fetch slashing multiplier for. + */ + function getValidatorGroupSlashingMultiplier(address account) external view returns (uint256) { + require(isValidatorGroup(account)); + ValidatorGroup storage group = groups[account]; + return group.slashInfo.multiplier.unwrap(); + } } diff --git a/packages/protocol/contracts/governance/interfaces/IValidators.sol b/packages/protocol/contracts/governance/interfaces/IValidators.sol index 58a19f4a3aa..9c1dd1c73e2 100644 --- a/packages/protocol/contracts/governance/interfaces/IValidators.sol +++ b/packages/protocol/contracts/governance/interfaces/IValidators.sol @@ -10,4 +10,5 @@ interface IValidators { function updateEcdsaPublicKey(address, address, bytes calldata) external returns (bool); function isValidator(address) external view returns (bool); function calculateGroupEpochScore(uint256[] calldata uptimes) external view returns (uint256); + function getValidatorGroupSlashingMultiplier(address) external view returns (uint256); } diff --git a/packages/protocol/contracts/governance/test/MockValidators.sol b/packages/protocol/contracts/governance/test/MockValidators.sol index c216d1ebf44..e4a3af2b7fd 100644 --- a/packages/protocol/contracts/governance/test/MockValidators.sol +++ b/packages/protocol/contracts/governance/test/MockValidators.sol @@ -79,4 +79,8 @@ contract MockValidators is IValidators { } return numMembers; } + + function getValidatorGroupSlashingMultiplier(address) external view returns (uint256) { + return FIXED1_UINT; + } } diff --git a/packages/protocol/migrations/12_validators.ts b/packages/protocol/migrations/12_validators.ts index 138187900de..5a7b0a8daee 100644 --- a/packages/protocol/migrations/12_validators.ts +++ b/packages/protocol/migrations/12_validators.ts @@ -14,6 +14,7 @@ const initializeArgs = async (): Promise => { config.validators.validatorScoreParameters.exponent, toFixed(config.validators.validatorScoreParameters.adjustmentSpeed).toFixed(), config.validators.membershipHistoryLength, + config.validators.slashingPenaltyResetPeriod, config.validators.maxGroupSize, ] } diff --git a/packages/protocol/migrationsConfig.js b/packages/protocol/migrationsConfig.js index 695766d2ce9..608ffcefb75 100644 --- a/packages/protocol/migrationsConfig.js +++ b/packages/protocol/migrationsConfig.js @@ -111,6 +111,7 @@ const DefaultConfig = { }, membershipHistoryLength: 60, maxGroupSize: '5', + slashingPenaltyResetPeriod: 60 * 60 * 24 * 30, // 30 Days // We register a number of C-Labs groups to contain an initial set of validators to run the network. validatorKeys: [], diff --git a/packages/protocol/runTests.js b/packages/protocol/runTests.js index c06403f47df..4676eafd96c 100644 --- a/packages/protocol/runTests.js +++ b/packages/protocol/runTests.js @@ -17,7 +17,7 @@ async function startGanache() { network_id: network.network_id, mnemonic: network.mnemonic, gasPrice: network.gasPrice, - gasLimit: 10000000, + gasLimit: 15000000, allowUnlimitedContractSize: true, }) diff --git a/packages/protocol/scripts/devchain.ts b/packages/protocol/scripts/devchain.ts index 192d9438be1..a23aa638a52 100644 --- a/packages/protocol/scripts/devchain.ts +++ b/packages/protocol/scripts/devchain.ts @@ -7,7 +7,7 @@ import * as yargs from 'yargs' const MNEMONIC = 'concert load couple harbor equip island argue ramp clarify fence smart topic' -const gasLimit = 10000000 +const gasLimit = 15000000 const ProtocolRoot = path.normalize(path.join(__dirname, '../')) diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index cac17ea4208..378cc66734b 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -55,6 +55,8 @@ const parseValidatorGroupParams = (groupParams: any) => { members: groupParams[0], commission: groupParams[1], sizeHistory: groupParams[2], + slashingMultiplier: groupParams[3], + lastSlashed: groupParams[4], } } @@ -91,6 +93,7 @@ contract('Validators', (accounts: string[]) => { exponent: new BigNumber(5), adjustmentSpeed: toFixed(0.25), } + const slashingMultiplierResetPeriod = 30 * DAY const membershipHistoryLength = new BigNumber(5) const maxGroupSize = new BigNumber(5) @@ -124,6 +127,7 @@ contract('Validators', (accounts: string[]) => { validatorScoreParameters.exponent, validatorScoreParameters.adjustmentSpeed, membershipHistoryLength, + slashingMultiplierResetPeriod, maxGroupSize ) }) @@ -208,6 +212,7 @@ contract('Validators', (accounts: string[]) => { validatorScoreParameters.exponent, validatorScoreParameters.adjustmentSpeed, membershipHistoryLength, + slashingMultiplierResetPeriod, maxGroupSize ) ) @@ -2206,4 +2211,84 @@ contract('Validators', (accounts: string[]) => { }) }) }) + + describe('#halveSlashingMultiplier', async () => { + const group = accounts[1] + + beforeEach(async () => { + await registerValidatorGroup(group) + }) + + describe('when run from an approved address', async () => { + beforeEach(async () => { + await registry.setAddressFor(CeloContractName.DowntimeSlasher, accounts[2]) + }) + + it('should halve the slashing multiplier of a group', async () => { + let multiplier = 1.0 + for (let i = 0; i < 10; i++) { + await validators.halveSlashingMultiplier(group, { from: accounts[2] }) + multiplier /= 2 + const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) + assertEqualBN(parsedGroup.slashingMultiplier, toFixed(multiplier)) + } + }) + + it('should update `lastSlashed timestamp', async () => { + let parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) + const initialTimestamp = parsedGroup.lastSlashed + await validators.halveSlashingMultiplier(group, { from: accounts[2] }) + parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) + assert(parsedGroup.lastSlashed > initialTimestamp) + }) + }) + + describe('when called from an unapproved address', async () => { + it('should revert', async () => { + await assertRevert(validators.halveSlashingMultiplier(group)) + }) + }) + }) + + describe('#resetSlashingMultiplier', async () => { + const validator = accounts[0] + const group = accounts[1] + + beforeEach(async () => { + await registerValidator(validator) + await registerValidatorGroup(group) + await validators.affiliate(group) + await registry.setAddressFor(CeloContractName.DowntimeSlasher, accounts[2]) + await validators.halveSlashingMultiplier(group, { from: accounts[2] }) + const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) + assertEqualBN(parsedGroup.slashingMultiplier, toFixed(0.5)) + }) + + describe('when the slashing multiplier is reset after reset period', async () => { + it('should return to default 1.0', async () => { + await timeTravel(slashingMultiplierResetPeriod, web3) + await validators.resetSlashingMultiplier({ from: group }) + const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) + assertEqualBN(parsedGroup.slashingMultiplier, toFixed(1)) + }) + }) + + describe('when the slashing multiplier is reset before reset period', async () => { + it('should revert', async () => { + await timeTravel(slashingMultiplierResetPeriod - 1, web3) + await assertRevert(validators.resetSlashingMultiplier({ from: group })) + }) + }) + + describe('when the slashing reset period is changed', async () => { + it('should be read properly', async () => { + const newPeriod = 60 * 60 * 24 * 10 // 10 days + await validators.setSlashingMultiplierResetPeriod(newPeriod) + await timeTravel(newPeriod, web3) + await validators.resetSlashingMultiplier({ from: group }) + const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) + assertEqualBN(parsedGroup.slashingMultiplier, toFixed(1)) + }) + }) + }) }) diff --git a/packages/protocol/truffle-config.js b/packages/protocol/truffle-config.js index c5d4bc9f673..9fb84d3fe7c 100644 --- a/packages/protocol/truffle-config.js +++ b/packages/protocol/truffle-config.js @@ -21,7 +21,7 @@ const ALFAJORES_FROM = '0x456f41406B32c45D59E539e4BBA3D7898c3584dA' const PILOT_FROM = '0x387bCb16Bfcd37AccEcF5c9eB2938E30d3aB8BF2' const PILOTSTAGING_FROM = '0x545DEBe3030B570731EDab192640804AC8Cf65CA' -const gasLimit = 10000000 +const gasLimit = 15000000 const defaultConfig = { host: '127.0.0.1',