Skip to content

Commit 9a07096

Browse files
committed
Allow indexing by timestamp, allow to freeze minting and allowing to exclude the supply of certain addresses
1 parent e162898 commit 9a07096

File tree

10 files changed

+408
-136
lines changed

10 files changed

+408
-136
lines changed

script/Deploy.s.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,10 @@ contract DeployScript is Script {
8484
function deployPluginRepo() public {
8585
// Dependency implementations
8686
GovernanceERC20 governanceERC20 = new GovernanceERC20(
87-
IDAO(address(0)), "", "", GovernanceERC20.MintSettings(new address[](0), new uint256[](0))
87+
IDAO(address(0)), "", "", GovernanceERC20.MintSettings(new address[](0), new uint256[](0)), new address[](0)
8888
);
8989
GovernanceWrappedERC20 governanceWrappedERC20 =
90-
new GovernanceWrappedERC20(IERC20Upgradeable(address(0)), "", "");
90+
new GovernanceWrappedERC20(IERC20Upgradeable(address(0)), "", "", new address[](0));
9191

9292
// Plugin setup (the installer)
9393
pluginSetup = new TokenVotingSetup(governanceERC20, governanceWrappedERC20);

src/TokenVoting.sol

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
pragma solidity ^0.8.8;
44

5+
import {DAO, IDAO, Action} from "@aragon/osx/core/dao/DAO.sol";
6+
import {PluginUUPSUpgradeable} from "@aragon/osx/framework/plugin/setup/PluginSetupProcessor.sol";
57
import {IVotesUpgradeable} from "@openzeppelin/contracts-upgradeable/governance/utils/IVotesUpgradeable.sol";
68
import {SafeCastUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol";
79
import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
10+
import {IERC6372Upgradeable} from "@openzeppelin/contracts-upgradeable/interfaces/IERC6372Upgradeable.sol";
811

912
import {IMembership} from "@aragon/osx-commons-contracts/src/plugin/extensions/membership/IMembership.sol";
1013
import {_applyRatioCeiled} from "@aragon/osx-commons-contracts/src/utils/math/Ratio.sol";
@@ -20,7 +23,7 @@ import {MajorityVotingBase} from "./base/MajorityVotingBase.sol";
2023
/// @notice The majority voting implementation using an
2124
/// [OpenZeppelin `Votes`](https://docs.openzeppelin.com/contracts/4.x/api/governance#Votes)
2225
/// compatible governance token.
23-
/// @dev v1.3 (Release 1, Build 3). For each upgrade, if the reinitialization step is required,
26+
/// @dev v1.4 (Release 1, Build 4). For each upgrade, if the reinitialization step is required,
2427
/// increment the version numbers in the modifier for both the initialize and initializeFrom functions.
2528
/// @custom:security-contact sirt@aragon.org
2629
contract TokenVoting is IMembership, MajorityVotingBase {
@@ -33,6 +36,9 @@ contract TokenVoting is IMembership, MajorityVotingBase {
3336
/// compatible contract referencing the token being used for voting.
3437
IVotesUpgradeable private votingToken;
3538

39+
/// @notice Wether the token contract indexes past voting power by timestamp.
40+
bool private tokenIndexedByTimestamp;
41+
3642
/// @notice Thrown if the voting power is zero
3743
error NoVotingPower();
3844

@@ -54,11 +60,28 @@ contract TokenVoting is IMembership, MajorityVotingBase {
5460
TargetConfig calldata _targetConfig,
5561
uint256 _minApprovals,
5662
bytes calldata _pluginMetadata
57-
) external onlyCallAtInitialization reinitializer(2) {
63+
) external onlyCallAtInitialization reinitializer(3) {
5864
__MajorityVotingBase_init(_dao, _votingSettings, _targetConfig, _minApprovals, _pluginMetadata);
5965

6066
votingToken = _token;
6167

68+
// Check if the given token indexes past voting power by blocks or by timestamp
69+
try IERC6372Upgradeable(address(_token)).CLOCK_MODE() returns (string memory ms) {
70+
if (keccak256(bytes(ms)) == keccak256(bytes("mode=timestamp&version=1"))) {
71+
tokenIndexedByTimestamp = true;
72+
}
73+
} catch {
74+
// CLOCK_MODE() not found, reverted, or other issue.
75+
try IERC6372Upgradeable(address(_token)).clock() returns (uint48 cv) {
76+
if (cv == block.timestamp) {
77+
tokenIndexedByTimestamp = true;
78+
}
79+
} catch {
80+
// clock() not found, reverted, or other issue.
81+
// Assuming that the token indexes by block number
82+
}
83+
}
84+
6285
emit MembershipContractAnnounced({definingContract: address(_token)});
6386
}
6487

@@ -70,7 +93,7 @@ contract TokenVoting is IMembership, MajorityVotingBase {
7093
/// @param _fromBuild Build version number of previous implementation contract this upgrade is transitioning from.
7194
/// @param _initData The initialization data to be passed to via `upgradeToAndCall`
7295
/// (see [ERC-1967](https://docs.openzeppelin.com/contracts/4.x/api/proxy#ERC1967Upgrade)).
73-
function initializeFrom(uint16 _fromBuild, bytes calldata _initData) external reinitializer(2) {
96+
function initializeFrom(uint16 _fromBuild, bytes calldata _initData) external reinitializer(3) {
7497
if (_fromBuild < 3) {
7598
(uint256 minApprovals, TargetConfig memory targetConfig, bytes memory pluginMetadata) =
7699
abi.decode(_initData, (uint256, TargetConfig, bytes));
@@ -115,14 +138,18 @@ contract TokenVoting is IMembership, MajorityVotingBase {
115138
VoteOption _voteOption,
116139
bool _tryEarlyExecution
117140
) public override auth(CREATE_PROPOSAL_PERMISSION_ID) returns (uint256 proposalId) {
118-
uint256 snapshotBlock;
141+
uint256 snapshotTimepoint;
119142
unchecked {
120-
// The snapshot block must be mined already to
121-
// protect the transaction against backrunning transactions causing census changes.
122-
snapshotBlock = block.number - 1;
143+
// The time point must be already mined (block) or in the past (timestamp) to
144+
// protect against backrunning transactions causing census changes.
145+
if (tokenIndexedByTimestamp) {
146+
snapshotTimepoint = block.timestamp - 1;
147+
} else {
148+
snapshotTimepoint = block.number - 1;
149+
}
123150
}
124151

125-
uint256 totalVotingPower_ = totalVotingPower(snapshotBlock);
152+
uint256 totalVotingPower_ = totalVotingPower(snapshotTimepoint);
126153

127154
if (totalVotingPower_ == 0) {
128155
revert NoVotingPower();
@@ -135,13 +162,13 @@ contract TokenVoting is IMembership, MajorityVotingBase {
135162
// Store proposal related information
136163
Proposal storage proposal_ = proposals[proposalId];
137164

138-
if (proposal_.parameters.snapshotBlock != 0) {
165+
if (proposal_.parameters.snapshotTimepoint != 0) {
139166
revert ProposalAlreadyExists(proposalId);
140167
}
141168

142169
proposal_.parameters.startDate = _startDate;
143170
proposal_.parameters.endDate = _endDate;
144-
proposal_.parameters.snapshotBlock = snapshotBlock.toUint64();
171+
proposal_.parameters.snapshotTimepoint = snapshotTimepoint.toUint64();
145172
proposal_.parameters.votingMode = votingMode();
146173
proposal_.parameters.supportThreshold = supportThreshold();
147174
proposal_.parameters.minVotingPower = _applyRatioCeiled(totalVotingPower_, minParticipation());
@@ -209,7 +236,7 @@ contract TokenVoting is IMembership, MajorityVotingBase {
209236
Proposal storage proposal_ = proposals[_proposalId];
210237

211238
// This could re-enter, though we can assume the governance token is not malicious
212-
uint256 votingPower = votingToken.getPastVotes(_voter, proposal_.parameters.snapshotBlock);
239+
uint256 votingPower = votingToken.getPastVotes(_voter, proposal_.parameters.snapshotTimepoint);
213240
VoteOption state = proposal_.voters[_voter];
214241

215242
// If voter had previously voted, decrease count
@@ -266,7 +293,7 @@ contract TokenVoting is IMembership, MajorityVotingBase {
266293
}
267294

268295
// The voter has no voting power.
269-
if (votingToken.getPastVotes(_account, proposal_.parameters.snapshotBlock) == 0) {
296+
if (votingToken.getPastVotes(_account, proposal_.parameters.snapshotTimepoint) == 0) {
270297
return false;
271298
}
272299

@@ -296,5 +323,5 @@ contract TokenVoting is IMembership, MajorityVotingBase {
296323
/// @dev This empty reserved space is put in place to allow future versions to add new
297324
/// variables without shifting down storage in the inheritance chain.
298325
/// https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
299-
uint256[49] private __gap;
326+
uint256[48] private __gap;
300327
}

src/TokenVotingSetup.sol

Lines changed: 109 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -102,65 +102,75 @@ contract TokenVotingSetup is PluginUpgradeableSetup {
102102
external
103103
returns (address plugin, PreparedSetupData memory preparedSetupData)
104104
{
105-
// Decode `_data` to extract the params needed for deploying and initializing `TokenVoting` plugin,
106-
// and the required helpers
107-
(
108-
MajorityVotingBase.VotingSettings memory votingSettings,
109-
TokenSettings memory tokenSettings,
110-
// only used for GovernanceERC20(token is not passed)
111-
GovernanceERC20.MintSettings memory mintSettings,
112-
IPlugin.TargetConfig memory targetConfig,
113-
uint256 minApprovals,
114-
bytes memory pluginMetadata
115-
) = abi.decode(
116-
_data,
105+
TokenSettings memory tokenSettings;
106+
address token;
107+
108+
{
109+
MajorityVotingBase.VotingSettings memory votingSettings;
110+
GovernanceERC20.MintSettings memory mintSettings;
111+
IPlugin.TargetConfig memory targetConfig;
112+
uint256 minApprovals;
113+
bytes memory pluginMetadata;
114+
address[] memory excludedAccounts;
115+
116+
// Decode `_data` to extract the params needed for deploying and initializing `TokenVoting` plugin,
117+
// and the required helpers
117118
(
118-
MajorityVotingBase.VotingSettings,
119-
TokenSettings,
120-
GovernanceERC20.MintSettings,
121-
IPlugin.TargetConfig,
122-
uint256,
123-
bytes
124-
)
125-
);
126-
127-
address token = tokenSettings.addr;
128-
129-
if (tokenSettings.addr != address(0)) {
130-
if (!token.isContract()) {
131-
revert TokenNotContract(token);
132-
}
133-
134-
if (!_isERC20(token)) {
135-
revert TokenNotERC20(token);
136-
}
137-
138-
if (!supportsIVotesInterface(token)) {
139-
token = governanceWrappedERC20Base.clone();
140-
// User already has a token. We need to wrap it in
141-
// GovernanceWrappedERC20 in order to make the token
142-
// include governance functionality.
143-
GovernanceWrappedERC20(token).initialize(
144-
IERC20Upgradeable(tokenSettings.addr), tokenSettings.name, tokenSettings.symbol
119+
votingSettings,
120+
tokenSettings,
121+
// only used for GovernanceERC20(token is not passed)
122+
mintSettings,
123+
targetConfig,
124+
minApprovals,
125+
pluginMetadata,
126+
excludedAccounts
127+
) = decodeInstallationParameters(_data);
128+
129+
token = tokenSettings.addr;
130+
131+
// Use the given token
132+
if (token != address(0)) {
133+
if (!token.isContract()) {
134+
revert TokenNotContract(token);
135+
}
136+
137+
if (!_isERC20(token)) {
138+
revert TokenNotERC20(token);
139+
}
140+
141+
if (!supportsIVotesInterface(token)) {
142+
token = governanceWrappedERC20Base.clone();
143+
144+
// User already has a token. We need to wrap it in
145+
// GovernanceWrappedERC20 in order to make the token
146+
// include governance functionality.
147+
GovernanceWrappedERC20(token).initialize(
148+
IERC20Upgradeable(tokenSettings.addr),
149+
tokenSettings.name,
150+
tokenSettings.symbol,
151+
excludedAccounts
152+
);
153+
}
154+
} else {
155+
// Create a new token: Clone a `GovernanceERC20`.
156+
token = governanceERC20Base.clone();
157+
GovernanceERC20(token).initialize(
158+
IDAO(_dao), tokenSettings.name, tokenSettings.symbol, mintSettings, excludedAccounts
145159
);
146160
}
147-
} else {
148-
// Clone a `GovernanceERC20`.
149-
token = governanceERC20Base.clone();
150-
GovernanceERC20(token).initialize(IDAO(_dao), tokenSettings.name, tokenSettings.symbol, mintSettings);
151-
}
152161

153-
// Prepare and deploy plugin proxy.
154-
plugin = address(tokenVotingBase).deployUUPSProxy(
155-
abi.encodeCall(
156-
TokenVoting.initialize,
157-
(IDAO(_dao), votingSettings, IVotesUpgradeable(token), targetConfig, minApprovals, pluginMetadata)
158-
)
159-
);
162+
// Prepare and deploy plugin proxy.
163+
plugin = address(tokenVotingBase).deployUUPSProxy(
164+
abi.encodeCall(
165+
TokenVoting.initialize,
166+
(IDAO(_dao), votingSettings, IVotesUpgradeable(token), targetConfig, minApprovals, pluginMetadata)
167+
)
168+
);
160169

161-
preparedSetupData.helpers = new address[](2);
162-
preparedSetupData.helpers[0] = address(new VotingPowerCondition(plugin));
163-
preparedSetupData.helpers[1] = token;
170+
preparedSetupData.helpers = new address[](2);
171+
preparedSetupData.helpers[0] = address(new VotingPowerCondition(plugin));
172+
preparedSetupData.helpers[1] = token;
173+
}
164174

165175
// Prepare permissions
166176
PermissionLib.MultiTargetPermission[] memory permissions =
@@ -365,6 +375,51 @@ contract TokenVotingSetup is PluginUpgradeableSetup {
365375
(success1 && data1.length == 0x20 && success2 && data2.length == 0x20 && success3 && data3.length == 0x20);
366376
}
367377

378+
/// @notice Encodes the given installation parameters into a byte array
379+
function encodeInstallationParameters(
380+
MajorityVotingBase.VotingSettings memory votingSettings,
381+
TokenSettings memory tokenSettings,
382+
// only used for GovernanceERC20(token is not passed)
383+
GovernanceERC20.MintSettings memory mintSettings,
384+
IPlugin.TargetConfig memory targetConfig,
385+
uint256 minApprovals,
386+
bytes memory pluginMetadata,
387+
address[] memory excludedAccounts
388+
) external pure returns (bytes memory) {
389+
return abi.encode(
390+
votingSettings, tokenSettings, mintSettings, targetConfig, minApprovals, pluginMetadata, excludedAccounts
391+
);
392+
}
393+
394+
/// @notice Decodes the given byte array into the original installation parameters
395+
function decodeInstallationParameters(bytes memory _data)
396+
public
397+
pure
398+
returns (
399+
MajorityVotingBase.VotingSettings memory votingSettings,
400+
TokenSettings memory tokenSettings,
401+
// only used for GovernanceERC20(token is not passed)
402+
GovernanceERC20.MintSettings memory mintSettings,
403+
IPlugin.TargetConfig memory targetConfig,
404+
uint256 minApprovals,
405+
bytes memory pluginMetadata,
406+
address[] memory excludedAccounts
407+
)
408+
{
409+
return abi.decode(
410+
_data,
411+
(
412+
MajorityVotingBase.VotingSettings,
413+
TokenSettings,
414+
GovernanceERC20.MintSettings,
415+
IPlugin.TargetConfig,
416+
uint256,
417+
bytes,
418+
address[]
419+
)
420+
);
421+
}
422+
368423
/// @notice Unsatisfiably determines if the contract is an ERC20 token.
369424
/// @dev It's important to first check whether token is a contract prior to this call.
370425
/// @param token The token address

0 commit comments

Comments
 (0)