diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index a464dcc97f1..32db0489a83 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -216,6 +216,74 @@ describe('governance tests', () => { assertAlmostEqual(currentBalance.minus(previousBalance), expected) } + const waitForBlock = async (blockNumber: number) => { + // const epoch = new BigNumber(await validators.methods.getEpochSize().call()).toNumber() + let currentBlock: number + do { + currentBlock = await web3.eth.getBlockNumber() + await sleep(0.1) + } while (currentBlock < blockNumber) + } + + const waitForEpochTransition = async (epoch: number) => { + // const epoch = new BigNumber(await validators.methods.getEpochSize().call()).toNumber() + let blockNumber: number + do { + blockNumber = await web3.eth.getBlockNumber() + await sleep(0.1) + } while (blockNumber % epoch !== 1) + } + + const assertTargetVotingYieldChanged = async (blockNumber: number, expected: BigNumber) => { + const currentTarget = new BigNumber( + (await epochRewards.methods.getTargetVotingYieldParameters().call({}, blockNumber))[0] + ) + const previousTarget = new BigNumber( + (await epochRewards.methods.getTargetVotingYieldParameters().call({}, blockNumber - 1))[0] + ) + const max = new BigNumber( + (await epochRewards.methods.getTargetVotingYieldParameters().call({}, blockNumber))[1] + ) + const expectedTarget = previousTarget.plus(expected) + if (expectedTarget.isGreaterThanOrEqualTo(max)) { + assert.equal(currentTarget.toFixed(), max.toFixed()) + } else if (expectedTarget.isLessThanOrEqualTo(0)) { + assert.isTrue(currentTarget.isZero()) + } else { + const difference = currentTarget.minus(previousTarget) + // Assert equal to 9 decimal places due to rounding errors. + assert.equal( + fromFixed(difference) + .dp(9) + .toFixed(), + fromFixed(expected) + .dp(9) + .toFixed() + ) + } + } + + const assertTargetVotingYieldUnchanged = async (blockNumber: number) => { + await assertTargetVotingYieldChanged(blockNumber, new BigNumber(0)) + } + + const getLastEpochBlock = (blockNumber: number, epoch: number) => { + const epochNumber = Math.floor((blockNumber - 1) / epoch) + return epochNumber * epoch + } + + const assertGoldTokenTotalSupplyUnchanged = async (blockNumber: number) => { + await assertGoldTokenTotalSupplyChanged(blockNumber, new BigNumber(0)) + } + + const assertGoldTokenTotalSupplyChanged = async (blockNumber: number, expected: BigNumber) => { + const currentSupply = new BigNumber(await goldToken.methods.totalSupply().call({}, blockNumber)) + const previousSupply = new BigNumber( + await goldToken.methods.totalSupply().call({}, blockNumber - 1) + ) + assertAlmostEqual(currentSupply.minus(previousSupply), expected) + } + describe('when the validator set is changing', () => { let epoch: number const blockNumbers: number[] = [] @@ -281,16 +349,9 @@ describe('governance tests', () => { assert.equal(epoch, 10) // Wait for an epoch transition so we can activate our vote. - let blockNumber: number - do { - blockNumber = await web3.eth.getBlockNumber() - await sleep(0.1) - } while (blockNumber % epoch !== 1) + await waitForEpochTransition(epoch) // Wait for an extra epoch transition to ensure everyone is connected to one another. - do { - blockNumber = await web3.eth.getBlockNumber() - await sleep(0.1) - } while (blockNumber % epoch !== 1) + await waitForEpochTransition(epoch) // Prepare for member swapping. const groupWeb3 = new Web3('ws://localhost:8555') @@ -357,14 +418,9 @@ describe('governance tests', () => { ) } - const getLastEpochBlock = (blockNumber: number) => { - const epochNumber = Math.floor((blockNumber - 1) / epoch) - return epochNumber * epoch - } - it('should always return a validator set size equal to the number of group members at the end of the last epoch', async () => { for (const blockNumber of blockNumbers) { - const lastEpochBlock = getLastEpochBlock(blockNumber) + const lastEpochBlock = getLastEpochBlock(blockNumber, epoch) const validatorSetSize = await election.methods .numberValidatorsInCurrentSet() .call({}, blockNumber) @@ -376,7 +432,7 @@ describe('governance tests', () => { it('should always return a validator set equal to the signing keys of the group members at the end of the last epoch', async function(this: any) { this.timeout(0) for (const blockNumber of blockNumbers) { - const lastEpochBlock = getLastEpochBlock(blockNumber) + const lastEpochBlock = getLastEpochBlock(blockNumber, epoch) const memberAccounts = await getValidatorGroupMembers(lastEpochBlock) const memberSigners = await Promise.all( memberAccounts.map((v: string) => getValidatorSigner(v, lastEpochBlock)) @@ -391,7 +447,7 @@ describe('governance tests', () => { it('should block propose in a round robin fashion', async () => { let roundRobinOrder: string[] = [] for (const blockNumber of blockNumbers) { - const lastEpochBlock = getLastEpochBlock(blockNumber) + const lastEpochBlock = getLastEpochBlock(blockNumber, epoch) // Fetch the round robin order if it hasn't already been set for this epoch. if (roundRobinOrder.length === 0 || blockNumber === lastEpochBlock + 1) { const validatorSet = await getValidatorSetSignersAtBlock(blockNumber) @@ -548,19 +604,6 @@ describe('governance tests', () => { return new BigNumber(gpm).times(new BigNumber(gas)) } - const assertGoldTokenTotalSupplyChanged = async ( - blockNumber: number, - expected: BigNumber - ) => { - const currentSupply = new BigNumber( - await goldToken.methods.totalSupply().call({}, blockNumber) - ) - const previousSupply = new BigNumber( - await goldToken.methods.totalSupply().call({}, blockNumber - 1) - ) - assertAlmostEqual(currentSupply.minus(previousSupply), expected) - } - const assertLockedGoldBalanceChanged = async (blockNumber: number, expected: BigNumber) => { await assertBalanceChanged(lockedGold.options.address, blockNumber, expected, goldToken) } @@ -573,10 +616,6 @@ describe('governance tests', () => { await assertVotesChanged(blockNumber, new BigNumber(0)) } - const assertGoldTokenTotalSupplyUnchanged = async (blockNumber: number) => { - await assertGoldTokenTotalSupplyChanged(blockNumber, new BigNumber(0)) - } - const assertLockedGoldBalanceUnchanged = async (blockNumber: number) => { await assertLockedGoldBalanceChanged(blockNumber, new BigNumber(0)) } @@ -641,39 +680,6 @@ describe('governance tests', () => { }) it('should update the target voting yield', async () => { - const assertTargetVotingYieldChanged = async (blockNumber: number, expected: BigNumber) => { - const currentTarget = new BigNumber( - (await epochRewards.methods.getTargetVotingYieldParameters().call({}, blockNumber))[0] - ) - const previousTarget = new BigNumber( - (await epochRewards.methods.getTargetVotingYieldParameters().call({}, blockNumber - 1))[0] - ) - const max = new BigNumber( - (await epochRewards.methods.getTargetVotingYieldParameters().call({}, blockNumber))[1] - ) - const expectedTarget = previousTarget.plus(expected) - if (expectedTarget.isGreaterThanOrEqualTo(max)) { - assert.equal(currentTarget.toFixed(), max.toFixed()) - } else if (expectedTarget.isLessThanOrEqualTo(0)) { - assert.isTrue(currentTarget.isZero()) - } else { - const difference = currentTarget.minus(previousTarget) - // Assert equal to 9 decimal places due to rounding errors. - assert.equal( - fromFixed(difference) - .dp(9) - .toFixed(), - fromFixed(expected) - .dp(9) - .toFixed() - ) - } - } - - const assertTargetVotingYieldUnchanged = async (blockNumber: number) => { - await assertTargetVotingYieldChanged(blockNumber, new BigNumber(0)) - } - for (const blockNumber of blockNumbers) { if (isLastBlockOfEpoch(blockNumber, epoch)) { // We use the voting gold fraction from before the rewards are granted. @@ -698,6 +704,37 @@ describe('governance tests', () => { }) }) + describe('when rewards distribution is frozen', () => { + before(restart) + + let epoch: number + let blockFrozen: number + let latestBlock: number + + before(async function(this: any) { + this.timeout(0) + const validator = (await kit.web3.eth.getAccounts())[0] + await kit.web3.eth.personal.unlockAccount(validator, '', 1000000) + await epochRewards.methods.freeze().send({ from: validator }) + blockFrozen = await web3.eth.getBlockNumber() + epoch = new BigNumber(await validators.methods.getEpochSize().call()).toNumber() + await waitForBlock(blockFrozen + epoch * 2) + latestBlock = await web3.eth.getBlockNumber() + }) + + it('should not update the target voing yield', async () => { + for (let blockNumber = blockFrozen; blockNumber < latestBlock; blockNumber++) { + await assertTargetVotingYieldUnchanged(blockNumber) + } + }) + + it('should not mint new Celo Gold', async () => { + for (let blockNumber = blockFrozen; blockNumber < latestBlock; blockNumber++) { + await assertGoldTokenTotalSupplyUnchanged(blockNumber) + } + }) + }) + describe('after the gold token smart contract is registered', () => { let goldGenesisSupply = new BigNumber(0) beforeEach(async function(this: any) { diff --git a/packages/protocol/contracts/governance/EpochRewards.sol b/packages/protocol/contracts/governance/EpochRewards.sol index 2bf40d6e2b7..994887df029 100644 --- a/packages/protocol/contracts/governance/EpochRewards.sol +++ b/packages/protocol/contracts/governance/EpochRewards.sol @@ -3,6 +3,7 @@ pragma solidity ^0.5.3; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; +import "../baklava/Freezable.sol"; import "../common/FixidityLib.sol"; import "../common/Initializable.sol"; import "../common/UsingRegistry.sol"; @@ -11,7 +12,7 @@ import "../common/UsingPrecompiles.sol"; /** * @title Contract for calculating epoch rewards. */ -contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry { +contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry, Freezable { using FixidityLib for FixidityLib.Fraction; using SafeMath for uint256; @@ -82,6 +83,7 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry */ function initialize( address registryAddress, + address _freezer, uint256 targetVotingYieldInitial, uint256 targetVotingYieldMax, uint256 targetVotingYieldAdjustmentFactor, @@ -92,6 +94,7 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry uint256 _targetValidatorEpochPayment ) external initializer { _transferOwnership(msg.sender); + setFreezer(_freezer); setRegistry(registryAddress); setTargetVotingYieldParameters(targetVotingYieldMax, targetVotingYieldAdjustmentFactor); setRewardsMultiplierParameters( @@ -127,6 +130,10 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry ); } + function setFreezer(address freezer) public onlyOwner { + _setFreezer(freezer); + } + /** * @notice Sets the target voting Gold fraction. * @param value The percentage of floating Gold voting to target. @@ -362,7 +369,7 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry * voting Gold fraction. * @dev Only called directly by the protocol. */ - function updateTargetVotingYield() external { + function updateTargetVotingYield() external onlyWhenNotFrozen { require(msg.sender == address(0)); _updateTargetVotingYield(); } @@ -372,6 +379,10 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry * @return The per validator epoch payment and the total rewards to voters. */ function calculateTargetEpochPaymentAndRewards() external view returns (uint256, uint256) { + if (frozen) { + return (0, 0); + } + uint256 targetEpochRewards = getTargetEpochRewards(); uint256 targetTotalEpochPaymentsInGold = getTargetTotalEpochPaymentsInGold(); uint256 targetGoldSupplyIncrease = targetEpochRewards.add(targetTotalEpochPaymentsInGold); diff --git a/packages/protocol/migrations/14_epoch_rewards.ts b/packages/protocol/migrations/14_epoch_rewards.ts index 43dfa88fc49..7c8756cead1 100644 --- a/packages/protocol/migrations/14_epoch_rewards.ts +++ b/packages/protocol/migrations/14_epoch_rewards.ts @@ -3,10 +3,13 @@ import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils' import { config } from '@celo/protocol/migrationsConfig' import { toFixed } from '@celo/utils/lib/fixidity' import { EpochRewardsInstance } from 'types' +const truffle = require('@celo/protocol/truffle-config.js') -const initializeArgs = async (): Promise => { +const initializeArgs = async (networkName: string): Promise => { + const network: any = truffle.networks[networkName] return [ config.registry.predeployedProxyAddress, + network.from, toFixed(config.epochRewards.targetVotingYieldParameters.initial).toFixed(), toFixed(config.epochRewards.targetVotingYieldParameters.max).toFixed(), toFixed(config.epochRewards.targetVotingYieldParameters.adjustmentFactor).toFixed(), diff --git a/packages/protocol/test/governance/epochrewards.ts b/packages/protocol/test/governance/epochrewards.ts index 39fb8670fdc..3106f052aab 100644 --- a/packages/protocol/test/governance/epochrewards.ts +++ b/packages/protocol/test/governance/epochrewards.ts @@ -91,6 +91,7 @@ contract('EpochRewards', (accounts: string[]) => { await epochRewards.initialize( registry.address, + accounts[0], targetVotingYieldParams.initial, targetVotingYieldParams.max, targetVotingYieldParams.adjustmentFactor, @@ -130,6 +131,7 @@ contract('EpochRewards', (accounts: string[]) => { await assertRevert( epochRewards.initialize( registry.address, + accounts[0], targetVotingYieldParams.initial, targetVotingYieldParams.max, targetVotingYieldParams.adjustmentFactor, @@ -578,4 +580,19 @@ contract('EpochRewards', (accounts: string[]) => { }) }) }) + + describe('when the contract is frozen', () => { + beforeEach(async () => { + await epochRewards.freeze() + }) + + it('should make calculateTargetEpochPaymentAndRewards return zeroes', async () => { + const [ + validatorPayment, + voterRewards, + ] = await epochRewards.calculateTargetEpochPaymentAndRewards() + assertEqualBN(validatorPayment, 0) + assertEqualBN(voterRewards, 0) + }) + }) })