Skip to content

Commit

Permalink
Add EpochRewards smart contract to calculate epoch rewards and paymen…
Browse files Browse the repository at this point in the history
…ts (#1558)
  • Loading branch information
Asa Oines authored and celo-ci-bot-user committed Nov 12, 2019
1 parent 7e3d507 commit 0452093
Show file tree
Hide file tree
Showing 38 changed files with 1,325 additions and 216 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,7 @@ jobs:
command: |
set -e
cd packages/celotool
./ci_test_governance.sh checkout master
./ci_test_governance.sh checkout asaj/pos-4
end-to-end-geth-sync-test:
<<: *e2e-defaults
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe('Blockchain parameters tests', function(this: any) {
let parameters: BlockchainParametersWrapper

const gethConfig: GethTestConfig = {
migrateTo: 17,
migrateTo: 18,
instances: [
{ name: 'validator', validating: true, syncmode: 'full', port: 30303, rpcport: 8545 },
],
Expand Down
146 changes: 128 additions & 18 deletions packages/celotool/src/e2e-tests/governance_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ import { fromFixed, toFixed } from '@celo/utils/lib/fixidity'
import BigNumber from 'bignumber.js'
import { assert } from 'chai'
import Web3 from 'web3'
import { getContext, getEnode, importGenesis, initAndStartGeth, sleep } from './utils'
import {
assertAlmostEqual,
getContext,
getEnode,
importGenesis,
initAndStartGeth,
sleep,
} from './utils'

describe('governance tests', () => {
const gethConfig = {
Expand All @@ -21,9 +28,12 @@ describe('governance tests', () => {
const context: any = getContext(gethConfig)
let web3: any
let election: any
let validators: any
let stableToken: any
let sortedOracles: any
let epochRewards: any
let goldToken: any
let registry: any
let validators: any
let accounts: AccountsWrapper
let kit: ContractKit

Expand All @@ -39,9 +49,12 @@ describe('governance tests', () => {
web3 = new Web3('http://localhost:8545')
kit = newKitFromWeb3(web3)
goldToken = await kit._web3Contracts.getGoldToken()
stableToken = await kit._web3Contracts.getStableToken()
sortedOracles = await kit._web3Contracts.getSortedOracles()
validators = await kit._web3Contracts.getValidators()
registry = await kit._web3Contracts.getRegistry()
election = await kit._web3Contracts.getElection()
epochRewards = await kit._web3Contracts.getEpochRewards()
accounts = await kit.contracts.getAccounts()
}

Expand Down Expand Up @@ -143,8 +156,13 @@ describe('governance tests', () => {
epoch = new BigNumber(await validators.methods.getEpochSize().call()).toNumber()
assert.equal(epoch, 10)

// Give the node time to sync, and time for an epoch transition so we can activate our vote.
await sleep(20)
// Give the nodes time to sync, and time 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 activate(allValidators[0])
const groupWeb3 = new Web3('ws://localhost:8567')
const groupKit = newKitFromWeb3(groupWeb3)
Expand Down Expand Up @@ -278,10 +296,9 @@ describe('governance tests', () => {
})

it('should distribute epoch payments at the end of each epoch', async () => {
const stableToken = await kit._web3Contracts.getStableToken()
const commission = 0.1
const validatorEpochPayment = new BigNumber(
await validators.methods.validatorEpochPayment().call()
const targetValidatorEpochPayment = new BigNumber(
await epochRewards.methods.targetValidatorEpochPayment().call()
)
const [group] = await validators.methods.getRegisteredValidatorGroups().call()

Expand All @@ -298,7 +315,7 @@ describe('governance tests', () => {
)
assert.isNotNaN(currentBalance)
assert.isNotNaN(previousBalance)
assert.equal(expected.toFixed(), currentBalance.minus(previousBalance).toFixed())
assertAlmostEqual(currentBalance.minus(previousBalance), expected)
}

const assertBalanceUnchanged = async (validator: string, blockNumber: number) => {
Expand All @@ -310,7 +327,14 @@ describe('governance tests', () => {
(await validators.methods.getValidator(validator).call({}, blockNumber))[2]
)
assert.isNotNaN(score)
return validatorEpochPayment.times(fromFixed(score))
// We need to calculate the rewards multiplier for the previous block, before
// the rewards actually are awarded.
const rewardsMultiplier = new BigNumber(
await epochRewards.methods.getRewardsMultiplier().call({}, blockNumber - 1)
)
return targetValidatorEpochPayment
.times(fromFixed(score))
.times(fromFixed(rewardsMultiplier))
}

for (const blockNumber of blockNumbers) {
Expand Down Expand Up @@ -346,8 +370,6 @@ describe('governance tests', () => {
it('should distribute epoch rewards at the end of each epoch', async () => {
const lockedGold = await kit._web3Contracts.getLockedGold()
const governance = await kit._web3Contracts.getGovernance()
const epochReward = new BigNumber(10).pow(18)
const infraReward = new BigNumber(10).pow(18)
const [group] = await validators.methods.getRegisteredValidatorGroups().call()

const assertVotesChanged = async (blockNumber: number, expected: BigNumber) => {
Expand All @@ -357,7 +379,7 @@ describe('governance tests', () => {
const previousVotes = new BigNumber(
await election.methods.getTotalVotesForGroup(group).call({}, blockNumber - 1)
)
assert.equal(expected.toFixed(), currentVotes.minus(previousVotes).toFixed())
assertAlmostEqual(currentVotes.minus(previousVotes), expected)
}

const assertGoldTokenTotalSupplyChanged = async (
Expand All @@ -370,7 +392,7 @@ describe('governance tests', () => {
const previousSupply = new BigNumber(
await goldToken.methods.totalSupply().call({}, blockNumber - 1)
)
assert.equal(expected.toFixed(), currentSupply.minus(previousSupply).toFixed())
assertAlmostEqual(currentSupply.minus(previousSupply), expected)
}

const assertBalanceChanged = async (
Expand All @@ -384,7 +406,7 @@ describe('governance tests', () => {
const previousBalance = new BigNumber(
await goldToken.methods.balanceOf(address).call({}, blockNumber - 1)
)
assert.equal(expected.toFixed(), currentBalance.minus(previousBalance).toFixed())
assertAlmostEqual(currentBalance.minus(previousBalance), expected)
}

const assertLockedGoldBalanceChanged = async (blockNumber: number, expected: BigNumber) => {
Expand All @@ -411,12 +433,52 @@ describe('governance tests', () => {
await assertGovernanceBalanceChanged(blockNumber, new BigNumber(0))
}

const getStableTokenSupplyChange = async (blockNumber: number) => {
const currentSupply = new BigNumber(
await stableToken.methods.totalSupply().call({}, blockNumber)
)
const previousSupply = new BigNumber(
await stableToken.methods.totalSupply().call({}, blockNumber - 1)
)
return currentSupply.minus(previousSupply)
}

const getStableTokenExchangeRate = async (blockNumber: number) => {
const rate = await sortedOracles.methods
.medianRate(stableToken.options.address)
.call({}, blockNumber)
return new BigNumber(rate[0]).div(rate[1])
}

for (const blockNumber of blockNumbers) {
if (isLastBlockOfEpoch(blockNumber, epoch)) {
await assertVotesChanged(blockNumber, epochReward)
await assertGoldTokenTotalSupplyChanged(blockNumber, epochReward.plus(infraReward))
await assertLockedGoldBalanceChanged(blockNumber, epochReward)
await assertGovernanceBalanceChanged(blockNumber, infraReward)
// We use the number of active votes from the previous block to calculate the expected
// epoch reward as the number of active votes for the current block will include the
// epoch reward.
const activeVotes = new BigNumber(
await election.methods.getActiveVotes().call({}, blockNumber - 1)
)
const targetVotingYield = new BigNumber(
(await epochRewards.methods.getTargetVotingYieldParameters().call({}, blockNumber))[0]
)
// We need to calculate the rewards multiplier for the previous block, before
// the rewards actually are awarded.
const rewardsMultiplier = new BigNumber(
await epochRewards.methods.getRewardsMultiplier().call({}, blockNumber - 1)
)
const expectedEpochReward = activeVotes
.times(fromFixed(targetVotingYield))
.times(fromFixed(rewardsMultiplier))
const expectedInfraReward = new BigNumber(10).pow(18)
const stableTokenSupplyChange = await getStableTokenSupplyChange(blockNumber)
const exchangeRate = await getStableTokenExchangeRate(blockNumber)
const expectedGoldTotalSupplyChange = expectedInfraReward
.plus(expectedEpochReward)
.plus(stableTokenSupplyChange.div(exchangeRate))
await assertVotesChanged(blockNumber, expectedEpochReward)
await assertLockedGoldBalanceChanged(blockNumber, expectedEpochReward)
await assertGovernanceBalanceChanged(blockNumber, expectedInfraReward)
await assertGoldTokenTotalSupplyChanged(blockNumber, expectedGoldTotalSupplyChange)
} else {
await assertVotesUnchanged(blockNumber)
await assertGoldTokenTotalSupplyUnchanged(blockNumber)
Expand All @@ -425,6 +487,54 @@ 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 difference = currentTarget.minus(previousTarget)

// Assert equal to 10 decimal places due to rounding errors.
assert.equal(
fromFixed(difference)
.dp(10)
.toFixed(),
fromFixed(expected)
.dp(10)
.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.
const votingGoldFraction = new BigNumber(
await epochRewards.methods.getVotingGoldFraction().call({}, blockNumber - 1)
)
const targetVotingGoldFraction = new BigNumber(
await epochRewards.methods.getTargetVotingGoldFraction().call({}, blockNumber)
)
const difference = targetVotingGoldFraction.minus(votingGoldFraction)
const adjustmentFactor = fromFixed(
new BigNumber(
(await epochRewards.methods.getTargetVotingYieldParameters().call({}, blockNumber))[2]
)
)
const delta = difference.times(adjustmentFactor)
await assertTargetVotingYieldChanged(blockNumber, delta)
} else {
await assertTargetVotingYieldUnchanged(blockNumber)
}
}
})
})

describe('after the gold token smart contract is registered', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/celotool/src/e2e-tests/transfer_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ describe('Transfer tests', function(this: any) {

const syncModes = ['full', 'fast', 'light', 'ultralight']
const gethConfig: GethTestConfig = {
migrateTo: 17,
migrateTo: 18,
instances: [
{ name: 'validator', validating: true, syncmode: 'full', port: 30303, rpcport: 8545 },
],
Expand Down
17 changes: 17 additions & 0 deletions packages/celotool/src/e2e-tests/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import BigNumber from 'bignumber.js'
import { assert } from 'chai'
import { spawn, SpawnOptions } from 'child_process'
import fs from 'fs'
Expand Down Expand Up @@ -40,6 +41,22 @@ const GENESIS_PATH = `${TEST_DIR}/genesis.json`
const NetworkId = 1101
const MonorepoRoot = resolvePath(joinPath(__dirname, '../..', '../..'))

export function assertAlmostEqual(
actual: BigNumber,
expected: BigNumber,
delta: BigNumber = new BigNumber(10).pow(12).times(5)
) {
if (expected.isZero()) {
assert.equal(actual.toFixed(), expected.toFixed())
} else {
const isCloseTo = actual.plus(delta).gte(expected) || actual.minus(delta).lte(expected)
assert(
isCloseTo,
`expected ${actual.toString()} to almost equal ${expected.toString()} +/- ${delta.toString()}`
)
}
}

export function spawnWithLog(cmd: string, args: string[], logsFilepath: string) {
try {
fs.unlinkSync(logsFilepath)
Expand Down
2 changes: 1 addition & 1 deletion packages/celotool/src/e2e-tests/validator_order_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const BLOCK_COUNT = EPOCH * EPOCHS_TO_WAIT

describe('governance tests', () => {
const gethConfig: GethTestConfig = {
migrateTo: 14,
migrateTo: 15,
instances: _.range(VALIDATORS).map((i) => ({
name: `validator${i}`,
validating: true,
Expand Down
1 change: 1 addition & 0 deletions packages/contractkit/src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export enum CeloContract {
Attestations = 'Attestations',
BlockchainParameters = 'BlockchainParameters',
Election = 'Election',
EpochRewards = 'EpochRewards',
Escrow = 'Escrow',
Exchange = 'Exchange',
GasCurrencyWhitelist = 'GasCurrencyWhitelist',
Expand Down
6 changes: 6 additions & 0 deletions packages/contractkit/src/contract-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { AccountsWrapper } from './wrappers/Accounts'
import { AttestationsWrapper } from './wrappers/Attestations'
import { BlockchainParametersWrapper } from './wrappers/BlockchainParameters'
import { ElectionWrapper } from './wrappers/Election'
// import { EpochRewardsWrapper } from './wrappers/EpochRewards'
import { EscrowWrapper } from './wrappers/Escrow'
import { ExchangeWrapper } from './wrappers/Exchange'
import { GasPriceMinimumWrapper } from './wrappers/GasPriceMinimum'
Expand All @@ -20,6 +21,7 @@ const WrapperFactories = {
[CeloContract.Attestations]: AttestationsWrapper,
[CeloContract.BlockchainParameters]: BlockchainParametersWrapper,
[CeloContract.Election]: ElectionWrapper,
// [CeloContract.EpochRewards]?: EpochRewardsWrapper,
[CeloContract.Escrow]: EscrowWrapper,
[CeloContract.Exchange]: ExchangeWrapper,
// [CeloContract.GasCurrencyWhitelist]: GasCurrencyWhitelistWrapper,
Expand All @@ -44,6 +46,7 @@ interface WrapperCacheMap {
[CeloContract.Attestations]?: AttestationsWrapper
[CeloContract.BlockchainParameters]?: BlockchainParametersWrapper
[CeloContract.Election]?: ElectionWrapper
// [CeloContract.EpochRewards]?: EpochRewardsWrapper
[CeloContract.Escrow]?: EscrowWrapper
[CeloContract.Exchange]?: ExchangeWrapper
// [CeloContract.GasCurrencyWhitelist]?: GasCurrencyWhitelistWrapper,
Expand Down Expand Up @@ -83,6 +86,9 @@ export class WrapperCache {
getElection() {
return this.getContract(CeloContract.Election)
}
// getEpochRewards() {
// return this.getContract(CeloContract.EpochRewards)
// }
getEscrow() {
return this.getContract(CeloContract.Escrow)
}
Expand Down
5 changes: 5 additions & 0 deletions packages/contractkit/src/web3-contract-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { newAccounts } from './generated/Accounts'
import { newAttestations } from './generated/Attestations'
import { newBlockchainParameters } from './generated/BlockchainParameters'
import { newElection } from './generated/Election'
import { newEpochRewards } from './generated/EpochRewards'
import { newEscrow } from './generated/Escrow'
import { newExchange } from './generated/Exchange'
import { newGasCurrencyWhitelist } from './generated/GasCurrencyWhitelist'
Expand All @@ -26,6 +27,7 @@ const ContractFactories = {
[CeloContract.Attestations]: newAttestations,
[CeloContract.BlockchainParameters]: newBlockchainParameters,
[CeloContract.Election]: newElection,
[CeloContract.EpochRewards]: newEpochRewards,
[CeloContract.Escrow]: newEscrow,
[CeloContract.Exchange]: newExchange,
[CeloContract.GasCurrencyWhitelist]: newGasCurrencyWhitelist,
Expand Down Expand Up @@ -68,6 +70,9 @@ export class Web3ContractCache {
getElection() {
return this.getContract(CeloContract.Election)
}
getEpochRewards() {
return this.getContract(CeloContract.EpochRewards)
}
getEscrow() {
return this.getContract(CeloContract.Escrow)
}
Expand Down
5 changes: 5 additions & 0 deletions packages/protocol/contracts/common/UsingRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import "../governance/interfaces/IValidators.sol";

import "../identity/interfaces/IRandom.sol";

import "../stability/interfaces/ISortedOracles.sol";
import "../stability/interfaces/IStableToken.sol";

// Ideally, UsingRegistry should inherit from Initializable and implement initialize() which calls
Expand Down Expand Up @@ -81,6 +82,10 @@ contract UsingRegistry is Ownable {
return IRandom(registry.getAddressForOrDie(RANDOM_REGISTRY_ID));
}

function getSortedOracles() internal view returns (ISortedOracles) {
return ISortedOracles(registry.getAddressForOrDie(SORTED_ORACLES_REGISTRY_ID));
}

function getStableToken() internal view returns (IStableToken) {
return IStableToken(registry.getAddressForOrDie(STABLE_TOKEN_REGISTRY_ID));
}
Expand Down
Loading

0 comments on commit 0452093

Please sign in to comment.