Skip to content

Commit b5fabfa

Browse files
authored
epochs: implement an epoch manager contract (#170)
* epochs: implement an epoch manager contract * build: do not use deploy script if we are doing tests * epochs: add tests and rename functions * epochs: fix epoch init and add event when changing length
1 parent ae0b892 commit b5fabfa

File tree

5 files changed

+346
-9
lines changed

5 files changed

+346
-9
lines changed

contracts/EpochManager.sol

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
pragma solidity ^0.6.4;
2+
pragma experimental ABIEncoderV2;
3+
4+
/*
5+
* @title EpochManager contract
6+
* @notice Tracks epochs based on its block duration to sync contracts in the protocol.
7+
*/
8+
9+
import "./Governed.sol";
10+
import "@openzeppelin/contracts/math/SafeMath.sol";
11+
12+
13+
contract EpochManager is Governed {
14+
using SafeMath for uint256;
15+
16+
// -- State --
17+
18+
// Epoch length in blocks
19+
uint256 public epochLength;
20+
21+
// Epoch that was last run
22+
uint256 public lastRunEpoch;
23+
24+
// Block and epoch when epoch length was last updated
25+
uint256 public lastLengthUpdateEpoch;
26+
uint256 public lastLengthUpdateBlock;
27+
28+
// -- Events --
29+
30+
event NewEpoch(uint256 indexed epoch, uint256 blockNumber, address caller);
31+
event EpochLengthUpdate(uint256 indexed epoch, uint256 epochLength);
32+
33+
/**
34+
* @dev Contract Constructor
35+
* @param _governor Owner address of this contract
36+
* @param _epochLength Epoch length in blocks
37+
*/
38+
constructor(address _governor, uint256 _epochLength)
39+
public
40+
Governed(_governor)
41+
{
42+
require(_epochLength > 0, "Epoch length cannot be 0");
43+
44+
lastLengthUpdateEpoch = 0;
45+
lastLengthUpdateBlock = blockNum();
46+
epochLength = _epochLength;
47+
48+
emit EpochLengthUpdate(lastLengthUpdateEpoch, epochLength);
49+
}
50+
51+
/**
52+
* @dev Set the epoch length
53+
* @notice Set epoch length to `_epochLength` blocks
54+
* @param _epochLength Epoch length in blocks
55+
*/
56+
function setEpochLength(uint256 _epochLength) external onlyGovernance {
57+
require(_epochLength > 0, "Epoch length cannot be 0");
58+
require(
59+
_epochLength != epochLength,
60+
"Epoch length must be different to current"
61+
);
62+
63+
lastLengthUpdateEpoch = currentEpoch();
64+
lastLengthUpdateBlock = currentEpochBlock();
65+
epochLength = _epochLength;
66+
67+
emit EpochLengthUpdate(lastLengthUpdateEpoch, epochLength);
68+
}
69+
70+
/**
71+
* @dev Run a new epoch, should be called once at the start of any epoch
72+
* @notice Perform state changes for the current epoch
73+
*/
74+
function runEpoch() external {
75+
// Check if already called for the current epoch
76+
require(!isCurrentEpochRun(), "Current epoch already run");
77+
78+
lastRunEpoch = currentEpoch();
79+
80+
// Hook for protocol general state updates
81+
82+
emit NewEpoch(lastRunEpoch, blockNum(), msg.sender);
83+
}
84+
85+
/**
86+
* @dev Return true if the current epoch has already run
87+
* @return Return true if epoch has run
88+
*/
89+
function isCurrentEpochRun() public view returns (bool) {
90+
return lastRunEpoch == currentEpoch();
91+
}
92+
93+
/**
94+
* @dev Return current block number
95+
* @return Block number
96+
*/
97+
function blockNum() public view returns (uint256) {
98+
return block.number;
99+
}
100+
101+
/**
102+
* @dev Return blockhash for a block
103+
* @return BlockHash for `_block` number
104+
*/
105+
function blockHash(uint256 _block) public view returns (bytes32) {
106+
uint256 currentBlock = blockNum();
107+
108+
require(_block < currentBlock, "Can only retrieve past block hashes");
109+
require(
110+
currentBlock < 256 || _block >= currentBlock - 256,
111+
"Can only retrieve hashes for last 256 blocks"
112+
);
113+
114+
return blockhash(_block);
115+
}
116+
117+
/**
118+
* @dev Return the current epoch, it may have not been run yet
119+
* @return The current epoch based on epoch length
120+
*/
121+
function currentEpoch() public view returns (uint256) {
122+
return lastLengthUpdateEpoch.add(epochsSinceUpdate());
123+
}
124+
125+
/**
126+
* @dev Return block where the current epoch started
127+
* @return The block number when the current epoch started
128+
*/
129+
function currentEpochBlock() public view returns (uint256) {
130+
return lastLengthUpdateBlock.add(epochsSinceUpdate().mul(epochLength));
131+
}
132+
133+
/**
134+
* @dev Return number of epochs passed since last epoch length update
135+
* @return The number of epoch that passed since last epoch length update
136+
*/
137+
function epochsSinceUpdate() private view returns (uint256) {
138+
return blockNum().sub(lastLengthUpdateBlock).div(epochLength);
139+
}
140+
}

migrations/2_deploy_contracts.js

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const Curation = artifacts.require('Curation')
2+
const EpochManager = artifacts.require('EpochManager')
23
const GNS = artifacts.require('GNS')
34
const GraphToken = artifacts.require('GraphToken')
45
const RewardsManager = artifacts.require('RewardsManager')
@@ -25,11 +26,15 @@ const reserveRatio = new BN('500000')
2526
// const slashingPercentage = 10
2627
// amount of seconds to wait until indexer can finish stake logout
2728
const thawingPeriod = 60 * 60 * 24 * 7
29+
// Epoch length
30+
const epochLength = new BN((24 * 60 * 60) / 15) // One day in blocks
2831

2932
const deployed = {} // store deployed contracts in a JSON object
3033
let simpleGraphTokenGovernorAddress
3134

3235
module.exports = (deployer, network, accounts) => {
36+
if (network === 'development') return
37+
3338
// governor NOTE - Governor of GraphToken is accounts[1], NOT accounts[0],
3439
// because of a require statement in GraphToken.sol
3540
simpleGraphTokenGovernorAddress = accounts[1]
@@ -41,9 +46,18 @@ module.exports = (deployer, network, accounts) => {
4146
initialSupply, // initial supply
4247
)
4348

44-
// Deploy Staking contract using deployed GraphToken address + constants defined above
4549
.then(deployedGraphToken => {
4650
deployed.GraphToken = deployedGraphToken
51+
return deployer.deploy(
52+
EpochManager,
53+
deployAddress, // <address> governor
54+
epochLength, // <uint256> epoch duration in blocks
55+
)
56+
})
57+
58+
// Deploy Staking contract using deployed GraphToken address + constants defined above
59+
.then(deployedEpochManager => {
60+
deployed.EpochManager = deployedEpochManager
4761
return deployer.deploy(
4862
Staking,
4963
deployAddress, // <address> governor
@@ -113,15 +127,20 @@ module.exports = (deployer, network, accounts) => {
113127
// All contracts have been deployed and we log the total
114128
.then(deployedGNS => {
115129
deployed.GNS = deployedGNS
130+
131+
console.log('\n')
132+
console.log('> GOVERNOR:', simpleGraphTokenGovernorAddress)
133+
console.log('> GRAPH TOKEN:', deployed.GraphToken.address)
134+
console.log('> EPOCH MANAGER:', deployed.EpochManager.address)
135+
console.log('[Incentives]')
136+
console.log('> STAKING:', deployed.Staking.address)
137+
console.log('> CURATION:', deployed.Curation.address)
138+
console.log('> REWARDS MANAGER:', deployed.RewardsManager.address)
139+
console.log('[Discovery]')
140+
console.log('> SERVICE REGISTRY:', deployed.ServiceRegistry.address)
141+
console.log('> GNS:', deployed.GNS.address)
116142
console.log('\n')
117-
console.log('GOVERNOR: ', simpleGraphTokenGovernorAddress)
118-
console.log('GRAPH TOKEN: ', deployed.GraphToken.address)
119-
console.log('STAKING: ', deployed.Staking.address)
120-
console.log('CURATION', deployed.Curation.address)
121-
console.log('REWARDS MANAGER: ', deployed.RewardsManager.address)
122-
console.log('SERVICE REGISTRY: ', deployed.ServiceRegistry.address)
123-
console.log('GNS: ', deployed.GNS.address)
124-
console.log(`Deployed ${Object.entries(deployed).length} contracts.`)
143+
console.log(`>> Deployed ${Object.entries(deployed).length} contracts`)
125144
})
126145
.catch(err => {
127146
console.log('There was an error with deploy: ', err)

test/epochs.test.js

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
const { expect } = require('chai')
2+
const {
3+
expectEvent,
4+
expectRevert,
5+
time,
6+
} = require('@openzeppelin/test-helpers')
7+
const BN = web3.utils.BN
8+
9+
// helpers
10+
const deployment = require('./lib/deployment')
11+
const { defaults } = require('./lib/testHelpers')
12+
13+
contract('EpochManager', ([me, other, governor]) => {
14+
beforeEach(async function() {
15+
// Deploy epoch manager contract
16+
this.epochManager = await deployment.deployEpochManagerContract(governor, {
17+
from: me,
18+
})
19+
})
20+
21+
describe('state variables functions', () => {
22+
it('should set `governor`', async function() {
23+
// Set right in the constructor
24+
expect(await this.epochManager.governor()).to.equal(governor)
25+
26+
// Can set if allowed
27+
await this.epochManager.transferGovernance(other, { from: governor })
28+
expect(await this.epochManager.governor()).to.equal(other)
29+
})
30+
31+
it('should set `epochLength', async function() {
32+
// Set right in the constructor
33+
expect(await this.epochManager.epochLength()).to.be.bignumber.equal(
34+
defaults.epochs.lengthInBlocks,
35+
)
36+
37+
// Update and check new value
38+
const newEpochLength = new BN(4)
39+
const currentEpoch = await this.epochManager.currentEpoch()
40+
const { logs } = await this.epochManager.setEpochLength(newEpochLength, {
41+
from: governor,
42+
})
43+
expect(await this.epochManager.epochLength()).to.be.bignumber.equal(
44+
newEpochLength,
45+
)
46+
47+
// Event emitted
48+
expectEvent.inLogs(logs, 'EpochLengthUpdate', {
49+
epoch: currentEpoch,
50+
epochLength: newEpochLength,
51+
})
52+
})
53+
54+
it('reject set `epochLength` if zero', async function() {
55+
// Update and check new value
56+
const newEpochLength = new BN(0)
57+
await expectRevert(
58+
this.epochManager.setEpochLength(newEpochLength, { from: governor }),
59+
'Epoch length cannot be 0',
60+
)
61+
})
62+
})
63+
64+
describe('epoch lifecycle', function() {
65+
// Use epochs every three blocks
66+
// Blocks -> (1,2,3)(4,5,6)(7,8,9)
67+
// Epochs -> 1 2 3
68+
beforeEach(async function() {
69+
this.epochLength = new BN(3)
70+
await this.epochManager.setEpochLength(this.epochLength, {
71+
from: governor,
72+
})
73+
})
74+
75+
describe('calculations', () => {
76+
it('should return correct block number', async function() {
77+
const currentBlock = await time.latestBlock()
78+
expect(await this.epochManager.blockNum()).to.be.bignumber.equal(
79+
currentBlock,
80+
)
81+
})
82+
83+
it('should return same starting block if we stay on the same epoch', async function() {
84+
const currentEpochBlockBefore = await this.epochManager.currentEpochBlock()
85+
86+
// Advance blocks to stay on the same epoch
87+
await time.advanceBlock()
88+
89+
const currentEpochBlockAfter = await this.epochManager.currentEpochBlock()
90+
expect(currentEpochBlockAfter).to.be.bignumber.equal(
91+
currentEpochBlockBefore,
92+
)
93+
})
94+
95+
it('should return next starting block if we move to the next epoch', async function() {
96+
const currentEpochBlockBefore = await this.epochManager.currentEpochBlock()
97+
98+
// Advance blocks to move to the next epoch
99+
await time.advanceBlockTo(currentEpochBlockBefore.add(this.epochLength))
100+
101+
const currentEpochBlockAfter = await this.epochManager.currentEpochBlock()
102+
expect(currentEpochBlockAfter).to.be.bignumber.not.equal(
103+
currentEpochBlockBefore,
104+
)
105+
})
106+
107+
it('should return next epoch if advance > epochLength', async function() {
108+
const nextEpoch = (await this.epochManager.currentEpoch()).add(
109+
new BN(1),
110+
)
111+
112+
// Advance blocks and move to the next epoch
113+
const currentEpochBlock = await this.epochManager.currentEpochBlock()
114+
await time.advanceBlockTo(currentEpochBlock.add(this.epochLength))
115+
116+
const currentEpochAfter = await this.epochManager.currentEpoch()
117+
expect(currentEpochAfter).to.be.bignumber.equal(nextEpoch)
118+
})
119+
})
120+
121+
describe('progression', () => {
122+
beforeEach(async function() {
123+
const currentEpochBlock = await this.epochManager.currentEpochBlock()
124+
await time.advanceBlockTo(currentEpochBlock.add(this.epochLength))
125+
})
126+
127+
context('epoch not run', function() {
128+
it('should return that current epoch is not run', async function() {
129+
expect(await this.epochManager.isCurrentEpochRun(), false)
130+
})
131+
132+
it('should run new epoch', async function() {
133+
// Run epoch
134+
const currentEpoch = await this.epochManager.currentEpoch()
135+
const { logs } = await this.epochManager.runEpoch({ from: me })
136+
const currentBlock = await time.latestBlock()
137+
138+
// State
139+
const lastRunEpoch = await this.epochManager.lastRunEpoch()
140+
expect(lastRunEpoch).to.be.bignumber.equal(currentEpoch)
141+
142+
// Event emitted
143+
expectEvent.inLogs(logs, 'NewEpoch', {
144+
epoch: currentEpoch,
145+
blockNumber: currentBlock,
146+
caller: me,
147+
})
148+
})
149+
})
150+
151+
context('epoch run', function() {
152+
beforeEach(async function() {
153+
await this.epochManager.runEpoch()
154+
})
155+
156+
it('should return current epoch is already run', async function() {
157+
expect(await this.epochManager.isCurrentEpochRun(), true)
158+
})
159+
160+
it('reject run new epoch', async function() {
161+
await expectRevert(
162+
this.epochManager.runEpoch(),
163+
'Current epoch already run',
164+
)
165+
})
166+
})
167+
})
168+
})
169+
})

0 commit comments

Comments
 (0)