-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[WIP] test/bonding: Test GovernorCountingOverridable
- Loading branch information
Showing
4 changed files
with
353 additions
and
2 deletions.
There are no files selected for viewing
71 changes: 71 additions & 0 deletions
71
contracts/test/mocks/GovernorCountingOverridableTestable.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity 0.8.9; | ||
|
||
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; | ||
import "@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol"; | ||
import "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesUpgradeable.sol"; | ||
import "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesQuorumFractionUpgradeable.sol"; | ||
import "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorSettingsUpgradeable.sol"; | ||
|
||
import "../../treasury/GovernorCountingOverridable.sol"; | ||
|
||
/** | ||
* @dev This is a concrete contract to test the GovernorCountingOverridable extension. It implements the minimum | ||
* necessary to get a working Governor to test the extension. | ||
*/ | ||
contract GovernorCountingOverridableTestable is | ||
Initializable, | ||
GovernorUpgradeable, | ||
GovernorSettingsUpgradeable, | ||
GovernorVotesUpgradeable, | ||
GovernorCountingOverridable | ||
{ | ||
// use non-standard values for these to test if it's really used | ||
uint256 constant QUOTA = 420000; // 42% | ||
uint256 constant QUORUM = 370000; // 37% | ||
|
||
IVotes internal iVotes; // 🍎 | ||
|
||
function initialize(IVotes _votes) public initializer { | ||
iVotes = _votes; | ||
|
||
__Governor_init("GovernorCountingOverridableConcrete"); | ||
__GovernorSettings_init( | ||
0, /* no voting delay */ | ||
100, /* 100 blocks voting period */ | ||
0 /* no minimum proposal threshold */ | ||
); | ||
|
||
__GovernorVotes_init(iVotes); | ||
__GovernorCountingOverridable_init(); | ||
} | ||
|
||
function votes() public view override returns (IVotes) { | ||
return iVotes; | ||
} | ||
|
||
function quorum(uint256 timepoint) public view virtual override returns (uint256) { | ||
return MathUtils.percOf(iVotes.getPastTotalSupply(timepoint), QUORUM); | ||
} | ||
|
||
function quota() public pure override returns (uint256) { | ||
return QUOTA; | ||
} | ||
|
||
/** | ||
* @dev Expose internal function for testing. | ||
*/ | ||
function quorumReached(uint256 proposalId) public view returns (bool) { | ||
uint256 timepoint = proposalSnapshot(proposalId); | ||
return super._quorumReached(timepoint); | ||
} | ||
|
||
function proposalThreshold() | ||
public | ||
view | ||
override(GovernorUpgradeable, GovernorSettingsUpgradeable) | ||
returns (uint256) | ||
{ | ||
return super.proposalThreshold(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.9; | ||
|
||
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; | ||
import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol"; | ||
import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20SnapshotUpgradeable.sol"; | ||
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; | ||
import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20VotesUpgradeable.sol"; | ||
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; | ||
|
||
import { IVotes } from "../../treasury/GovernorCountingOverridable.sol"; | ||
import "../../bonding/libraries/SortedArrays.sol"; | ||
|
||
/** | ||
* @dev Minimum implementation of an IVotes interface to test the GovernorCountingOverridable extension. | ||
*/ | ||
contract VotesMock is | ||
Initializable, | ||
ERC20Upgradeable, | ||
ERC20BurnableUpgradeable, | ||
OwnableUpgradeable, | ||
ERC20VotesUpgradeable, | ||
IVotes | ||
{ | ||
mapping(address => uint256[]) private _delegateChangingTimes; | ||
mapping(address => mapping(uint256 => address)) private _delegatedAtTime; | ||
|
||
function initialize() public initializer { | ||
__ERC20_init("VotesMock", "VTCK"); | ||
__ERC20Burnable_init(); | ||
__Ownable_init(); | ||
__ERC20Votes_init(); | ||
} | ||
|
||
function delegatedAt(address _account, uint256 _timepoint) external view returns (address) { | ||
uint256[] storage rounds = _delegateChangingTimes[_account]; | ||
if (rounds.length == 0 || _timepoint < rounds[0]) { | ||
return address(0); | ||
} | ||
|
||
uint256 prevRound = SortedArrays.findLowerBound(rounds, _timepoint); | ||
return _delegatedAtTime[_account][prevRound]; | ||
} | ||
|
||
function _delegate(address _delegator, address _to) internal override { | ||
super._delegate(_delegator, _to); | ||
|
||
uint256 currTime = clock(); | ||
|
||
uint256[] storage rounds = _delegateChangingTimes[_delegator]; | ||
SortedArrays.pushSorted(rounds, currTime); | ||
_delegatedAtTime[_delegator][currTime] = _to; | ||
} | ||
|
||
function mint(address to, uint256 amount) public onlyOwner { | ||
_mint(to, amount); | ||
} | ||
|
||
// The following functions are overrides required by Solidity. | ||
|
||
function _afterTokenTransfer( | ||
address from, | ||
address to, | ||
uint256 amount | ||
) internal override(ERC20Upgradeable, ERC20VotesUpgradeable) { | ||
super._afterTokenTransfer(from, to, amount); | ||
} | ||
|
||
function _mint(address to, uint256 amount) internal override(ERC20Upgradeable, ERC20VotesUpgradeable) { | ||
super._mint(to, amount); | ||
} | ||
|
||
function _burn(address account, uint256 amount) internal override(ERC20Upgradeable, ERC20VotesUpgradeable) { | ||
super._burn(account, amount); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
import Fixture from "./helpers/Fixture" | ||
// import {functionSig} from "../../utils/helpers" | ||
import {assert} from "chai" | ||
import {ethers, web3} from "hardhat" | ||
import chai from "chai" | ||
import {solidity} from "ethereum-waffle" | ||
// import {BigNumber} from "ethers" | ||
|
||
chai.use(solidity) | ||
// const {expect} = chai | ||
|
||
const ProposalState = { | ||
Pending: 0, | ||
Active: 1, | ||
Canceled: 2, | ||
Defeated: 3, | ||
Succeeded: 4, | ||
Queued: 5, | ||
Expired: 6, | ||
Executed: 7 | ||
} | ||
|
||
describe.only("BondingCheckpointsVotes", () => { | ||
let signers | ||
let fixture | ||
|
||
let votes | ||
let governor | ||
|
||
let proposer | ||
let proposalId | ||
let voters | ||
|
||
const initVoter = async ({ | ||
signer, | ||
amount = ethers.utils.parseEther("1"), | ||
delegateAddress = signer.address | ||
}) => { | ||
await votes.mint(signer.address, amount) | ||
await votes.connect(signer).delegate(delegateAddress) | ||
return signer | ||
} | ||
|
||
before(async () => { | ||
signers = await ethers.getSigners() | ||
proposer = signers[0] | ||
voters = signers.slice(1, 11) | ||
|
||
fixture = new Fixture(web3) | ||
await fixture.deploy() | ||
|
||
// setup votes token | ||
|
||
const votesFac = await ethers.getContractFactory("VotesMock") | ||
votes = await votesFac.deploy() | ||
await votes.initialize() | ||
|
||
await initVoter({signer: proposer}) | ||
for (const i = 1; i <= voters.length; i++) { | ||
await initVoter({ | ||
signer: voters[i - 1], | ||
amount: ethers.utils.parseEther("1").mul(i) | ||
}) | ||
} | ||
|
||
// setup governor | ||
|
||
const governorFac = await ethers.getContractFactory( | ||
"GovernorCountingOverridableTestable" | ||
) | ||
governor = await governorFac.deploy() | ||
await governor.initialize(votes.address) | ||
|
||
await votes.mint(governor.address, ethers.utils.parseEther("100")) | ||
|
||
// setup proposal | ||
|
||
const tx = await governor.propose( | ||
[votes.address], | ||
[0], | ||
[ | ||
votes.interface.encodeFunctionData("transfer", [ | ||
proposer.address, | ||
ethers.utils.parseEther("100") | ||
]) | ||
], | ||
"Steal all the money" | ||
) | ||
|
||
const filter = governor.filters.ProposalCreated() | ||
const events = await governor.queryFilter( | ||
filter, | ||
tx.blockNumber, | ||
tx.blockNumber | ||
) | ||
proposalId = events[0].args[0] | ||
}) | ||
|
||
beforeEach(async () => { | ||
await fixture.setUp() | ||
}) | ||
|
||
afterEach(async () => { | ||
await fixture.tearDown() | ||
}) | ||
|
||
it("it should use the block number as clock", async () => { | ||
assert.equal( | ||
await governor.clock(), | ||
await ethers.provider.getBlockNumber() | ||
) | ||
}) | ||
|
||
describe("COUNTING_MODE", () => { | ||
it("should include bravo support and all vote types on quorum count", async () => { | ||
assert.equal( | ||
await governor.COUNTING_MODE(), | ||
"support=bravo&quorum=for,abstain,against" | ||
) | ||
}) | ||
}) | ||
|
||
describe("hasVoted", () => { | ||
it("should return false for users that haven't voted", async () => { | ||
for (let i = 0; i < 10; i++) { | ||
assert.isFalse( | ||
await governor.hasVoted(proposalId, signers[i].address) | ||
) | ||
} | ||
}) | ||
|
||
it("should return true after voting", async () => { | ||
await governor.connect(voters[0]).castVote(proposalId, 1) | ||
|
||
assert.isTrue( | ||
await governor.hasVoted(proposalId, voters[0].address) | ||
) | ||
}) | ||
}) | ||
|
||
describe("proposalVotes", () => { | ||
it("should return the sum of all votes made of each type", async () => { | ||
// against, for abstain, as per bravo ordering | ||
const tally = [0, 0, 0] | ||
|
||
const checkTally = async () => { | ||
const ether = ethers.utils.parseEther("1") | ||
const expected = tally.map(c => ether.mul(c).toString()) | ||
|
||
const votes = await governor | ||
.proposalVotes(proposalId) | ||
.then(v => v.map(v => v.toString())) | ||
|
||
assert.deepEqual(votes, expected) | ||
} | ||
|
||
for (let i = 1; i <= 10; i++) { | ||
await checkTally() | ||
|
||
// Each voter has a voting power of {i} VTCK. This renders a voting power sum of: | ||
// - 25 against (1 + 3 + 5 + 7 + 9) | ||
// - 24 for (2 + 4 + 8 + 10) | ||
// - 6 abstain (6) | ||
const voteType = i % 2 ? 0 : i % 3 ? 1 : 2 | ||
|
||
await governor | ||
.connect(voters[i - 1]) | ||
.castVote(proposalId, voteType) | ||
tally[voteType] += i | ||
} | ||
|
||
// sanity check | ||
assert.deepEqual(tally, [25, 24, 6]) | ||
await checkTally() | ||
|
||
await fixture.rpc.wait(100) | ||
|
||
assert.equal( | ||
await governor.state(proposalId), | ||
ProposalState.Succeeded // funds were stolen! | ||
) | ||
}) | ||
}) | ||
|
||
describe("_quorumReached", () => { | ||
it("should return false if less than the quorum has voted", async () => { | ||
// this results in a 41.8% participation, just below the quorum of 42% | ||
const voterIdxs = [1, 2, 3, 4, 5, 8] | ||
|
||
for (const i of voterIdxs) { | ||
assert.isFalse(await governor.quorumReached(proposalId)) | ||
|
||
await governor.connect(voters[i - 1]).castVote(proposalId, 0) | ||
} | ||
assert.isFalse(await governor.quorumReached(proposalId)) | ||
|
||
await fixture.rpc.wait(100) | ||
|
||
assert.equal( | ||
await governor.state(proposalId), // calls _quorumReached internally | ||
ProposalState.Expired // not enough participation for the coup! | ||
) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters