Skip to content

Commit

Permalink
Add proposal scopes logic (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
corydickson authored Aug 12, 2024
2 parents 1813bd7 + d012289 commit d683339
Show file tree
Hide file tree
Showing 7 changed files with 328 additions and 36 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@
[submodule "lib/openzeppelin-contracts-v4"]
path = lib/openzeppelin-contracts-v4
url = https://github.com/openzeppelin/openzeppelin-contracts
[submodule "lib/ERC20VotesPartialDelegationUpgradeable"]
path = lib/ERC20VotesPartialDelegationUpgradeable
url = https://github.com/voteagora/ERC20VotesPartialDelegationUpgradeable
1 change: 1 addition & 0 deletions lib/ERC20VotesPartialDelegationUpgradeable
3 changes: 2 additions & 1 deletion remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
@openzeppelin/contracts-v5/=lib/openzeppelin-contracts-v5/contracts/
@openzeppelin/contracts-upgradeable-v4/=lib/openzeppelin-contracts-upgradeable-v4/contracts/
forge-std/=lib/forge-std/src/
@solady/=lib/solady/src
@solady/=lib/solady/src
ERC20VotesPartialDelegationUpgradeable/=lib/ERC20VotesPartialDelegationUpgradeable/src/
98 changes: 86 additions & 12 deletions src/ProposalTypesConfigurator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,21 @@ contract ProposalTypesConfigurator is IProposalTypesConfigurator {
//////////////////////////////////////////////////////////////*/

mapping(uint8 proposalTypeId => ProposalType) internal _proposalTypes;
mapping(uint8 proposalTypeId => bool) internal _proposalTypesExists;
mapping(uint8 proposalTypeId => Scope[]) public scopes;
mapping(uint8 proposalTypeId => mapping(bytes32 typeHash => bool)) public scopeExists;

/*//////////////////////////////////////////////////////////////
MODIFIERS
//////////////////////////////////////////////////////////////*/

modifier onlyAdminOrTimelock() {
if (msg.sender != governor.admin() && msg.sender != governor.timelock()) {
revert NotAdminOrTimelock();
}
if (msg.sender != governor.admin() && msg.sender != governor.timelock()) revert NotAdminOrTimelock();
_;
}

modifier onlyAdmin() {
if (msg.sender != governor.admin()) revert NotAdmin();
_;
}

Expand All @@ -50,7 +56,8 @@ contract ProposalTypesConfigurator is IProposalTypesConfigurator {
_proposalTypesInit[i].quorum,
_proposalTypesInit[i].approvalThreshold,
_proposalTypesInit[i].name,
_proposalTypesInit[i].module
_proposalTypesInit[i].module,
_proposalTypesInit[i].txTypeHashes
);
}
}
Expand All @@ -64,38 +71,105 @@ contract ProposalTypesConfigurator is IProposalTypesConfigurator {
return _proposalTypes[proposalTypeId];
}

/**
* @notice Sets the scope for a given proposal type.
* @param proposalTypeId Id of the proposal type.
* @param txTypeHash A function selector that represent the type hash, i.e. keccak256("foobar(uint,address)").
* @param encodedLimit An ABI encoded string containing the function selector and relevant parameter values.
* @param parameters The list of byte represented values to be compared against the encoded limits.
* @param comparators List of enumuerated values represent which comparison to use when enforcing limit checks on parameters.
*/
function setScopeForProposalType(
uint8 proposalTypeId,
bytes32 txTypeHash,
bytes calldata encodedLimit,
bytes[] memory parameters,
Comparators[] memory comparators
) external override onlyAdmin {
if (!_proposalTypesExists[proposalTypeId]) revert InvalidProposalType();
if (parameters.length != comparators.length) revert InvalidParameterConditions();
if (scopeExists[proposalTypeId][txTypeHash]) revert NoDuplicateTxTypes(); // Do not allow multiple scopes for a single transaction type

for (uint8 i = 0; i < _proposalTypes[proposalTypeId].txTypeHashes.length; i++) {
if (_proposalTypes[proposalTypeId].txTypeHashes[i] == txTypeHash) {
revert NoDuplicateTxTypes();
}
}

Scope memory scope = Scope(txTypeHash, encodedLimit, parameters, comparators);
scopes[proposalTypeId].push(scope);

scopeExists[proposalTypeId][txTypeHash] = true;

_proposalTypes[proposalTypeId].txTypeHashes.push(txTypeHash);
}

/**
* @notice Set the parameters for a proposal type. Only callable by the admin or timelock.
* @param proposalTypeId Id of the proposal type
* @param quorum Quorum percentage, scaled by `PERCENT_DIVISOR`
* @param approvalThreshold Approval threshold percentage, scaled by `PERCENT_DIVISOR`
* @param name Name of the proposal type
* @param module Address of module that can only use this proposal type
* @param txTypeHashes A list of transaction function selectors that represent the type hash, i.e. keccak256("foobar(uint,address)")
*/
function setProposalType(
uint8 proposalTypeId,
uint16 quorum,
uint16 approvalThreshold,
string calldata name,
address module
address module,
bytes32[] memory txTypeHashes
) external override onlyAdminOrTimelock {
_setProposalType(proposalTypeId, quorum, approvalThreshold, name, module);
_setProposalType(proposalTypeId, quorum, approvalThreshold, name, module, txTypeHashes);
}

function _setProposalType(
uint8 proposalTypeId,
uint16 quorum,
uint16 approvalThreshold,
string calldata name,
address module
address module,
bytes32[] memory txTypeHashes
) internal {
if (quorum > PERCENT_DIVISOR) revert InvalidQuorum();
if (approvalThreshold > PERCENT_DIVISOR) {
revert InvalidApprovalThreshold();
}
if (approvalThreshold > PERCENT_DIVISOR) revert InvalidApprovalThreshold();

_proposalTypes[proposalTypeId] = ProposalType(quorum, approvalThreshold, name, module, txTypeHashes);
_proposalTypesExists[proposalTypeId] = true;

emit ProposalTypeSet(proposalTypeId, quorum, approvalThreshold, name, txTypeHashes);
}

_proposalTypes[proposalTypeId] = ProposalType(quorum, approvalThreshold, name, module);
/**
* @notice Adds an additional scope for a given proposal type.
* @param proposalTypeId Id of the proposal type
* @param scope An object that contains the scope for a transaction type hash
*/
function updateScopeForProposalType(uint8 proposalTypeId, Scope calldata scope) external override onlyAdmin {
if (!_proposalTypesExists[proposalTypeId]) revert InvalidProposalType();
if (scope.parameters.length != scope.comparators.length) revert InvalidParameterConditions();
if (scopeExists[proposalTypeId][scope.txTypeHash]) revert NoDuplicateTxTypes(); // Do not allow multiple scopes for a single transaction type

emit ProposalTypeSet(proposalTypeId, quorum, approvalThreshold, name);
scopes[proposalTypeId].push(scope);
scopeExists[proposalTypeId][scope.txTypeHash] = true;
}

/**
* @notice Retrives the encoded limit of a transaction type signature for a given proposal type.
* @param proposalTypeId Id of the proposal type
* @param txTypeHash A type signature of a function that has a limit specified in a scope
*/
function getLimit(uint8 proposalTypeId, bytes32 txTypeHash) public view returns (bytes memory encodedLimits) {
if (!_proposalTypesExists[proposalTypeId]) revert InvalidProposalType();

if (!scopeExists[proposalTypeId][txTypeHash]) revert InvalidScope();
Scope[] memory validScopes = scopes[proposalTypeId];

for (uint8 i = 0; i < validScopes.length; i++) {
if (validScopes[i].txTypeHash == txTypeHash) {
return validScopes[i].encodedLimits;
}
}
}
}
38 changes: 36 additions & 2 deletions src/interfaces/IProposalTypesConfigurator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,21 @@ interface IProposalTypesConfigurator {

error InvalidQuorum();
error InvalidApprovalThreshold();
error InvalidProposalType();
error InvalidParameterConditions();
error NoDuplicateTxTypes();
error InvalidScope();
error NotAdminOrTimelock();
error NotAdmin();
error AlreadyInit();

/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/

event ProposalTypeSet(uint8 indexed proposalTypeId, uint16 quorum, uint16 approvalThreshold, string name);
event ProposalTypeSet(
uint8 indexed proposalTypeId, uint16 quorum, uint16 approvalThreshold, string name, bytes32[] txTypeHashes
);

/*//////////////////////////////////////////////////////////////
STRUCTS
Expand All @@ -26,6 +33,21 @@ interface IProposalTypesConfigurator {
uint16 approvalThreshold;
string name;
address module;
bytes32[] txTypeHashes;
}

enum Comparators {
EMPTY,
EQUAL,
LESS_THAN,
GREATER_THAN
}

struct Scope {
bytes32 txTypeHash;
bytes encodedLimits;
bytes[] parameters;
Comparators[] comparators;
}

/*//////////////////////////////////////////////////////////////
Expand All @@ -41,6 +63,18 @@ interface IProposalTypesConfigurator {
uint16 quorum,
uint16 approvalThreshold,
string memory name,
address module
address module,
bytes32[] memory txTypeHashes
) external;

function setScopeForProposalType(
uint8 proposalTypeId,
bytes32 txTypeHash,
bytes calldata encodedLimit,
bytes[] memory parameters,
Comparators[] memory comparators
) external;

function updateScopeForProposalType(uint8 proposalTypeId, Scope calldata scope) external;
function getLimit(uint8 proposalTypeId, bytes32 txTypeHash) external returns (bytes memory);
}
37 changes: 25 additions & 12 deletions test/AgoraGovernor.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {IVotesUpgradeable} from "@openzeppelin/contracts-upgradeable-v4/governan
import {IGovernorUpgradeable} from "@openzeppelin/contracts-upgradeable-v4/governance/IGovernorUpgradeable.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts-v4/proxy/ERC1967/ERC1967Proxy.sol";
import {Timelock, TimelockControllerUpgradeable} from "test/mocks/TimelockMock.sol";
import {TokenMock} from "test/mocks/TokenMock.sol";
import {L2GovToken} from "ERC20VotesPartialDelegationUpgradeable/L2GovToken.sol";
import {VotingModule} from "src/modules/VotingModule.sol";
import {ProposalTypesConfigurator, IProposalTypesConfigurator} from "src/ProposalTypesConfigurator.sol";
import {
Expand Down Expand Up @@ -108,7 +108,7 @@ contract AgoraGovernorTest is Test {
uint256 proposalTypesIndex = 1;
uint256 timelockDelay;

TokenMock internal govToken;
L2GovToken internal govToken;
address public implementation;
address internal governorProxy;
AgoraGovernorMock public governor;
Expand All @@ -123,7 +123,13 @@ contract AgoraGovernorTest is Test {
vm.startPrank(deployer);

// Deploy token
govToken = new TokenMock(minter);
govToken = L2GovToken(
address(
new ERC1967Proxy(
address(new L2GovToken()), abi.encodeCall(govToken.initialize, (admin, "L2 Gov Token", "gL2"))
)
)
);

// Deploy Proposal Types Configurator
proposalTypesConfigurator = new ProposalTypesConfigurator();
Expand Down Expand Up @@ -163,13 +169,16 @@ contract AgoraGovernorTest is Test {
module = new ApprovalVotingModuleMock(address(governor));
optimisticModule = new OptimisticModule(address(governor));

bytes32[] memory transactions = new bytes32[](1);

// do admin stuff
vm.startPrank(admin);
govToken.grantRole(govToken.MINTER_ROLE(), minter);
governor.setModuleApproval(address(module), true);
governor.setModuleApproval(address(optimisticModule), true);
proposalTypesConfigurator.setProposalType(0, 3_000, 5_000, "Default", address(0));
proposalTypesConfigurator.setProposalType(1, 5_000, 7_000, "Alt", address(module));
proposalTypesConfigurator.setProposalType(2, 0, 0, "Optimistic", address(optimisticModule));
proposalTypesConfigurator.setProposalType(0, 3_000, 5_000, "Default", address(0), transactions);
proposalTypesConfigurator.setProposalType(1, 5_000, 7_000, "Alt", address(module), transactions);
proposalTypesConfigurator.setProposalType(2, 0, 0, "Optimistic", address(optimisticModule), transactions);
vm.stopPrank();
targetFake = new ExecutionTargetFake();
}
Expand Down Expand Up @@ -252,13 +261,15 @@ contract Initialize is AgoraGovernorTest {
public
virtual
{
bytes32[] memory transactions = new bytes32[](1);
ProposalTypesConfigurator _proposalTypesConfigurator = new ProposalTypesConfigurator();
IProposalTypesConfigurator.ProposalType[] memory _proposalTypes =
new IProposalTypesConfigurator.ProposalType[](4);
_proposalTypes[0] = IProposalTypesConfigurator.ProposalType(1_500, 9_000, "Default", address(0));
_proposalTypes[1] = IProposalTypesConfigurator.ProposalType(3_500, 7_000, "Alt", address(0));
_proposalTypes[2] = IProposalTypesConfigurator.ProposalType(7_500, 3_100, "Whatever", address(0));
_proposalTypes[3] = IProposalTypesConfigurator.ProposalType(0, 0, "Optimistic", address(optimisticModule));
_proposalTypes[0] = IProposalTypesConfigurator.ProposalType(1_500, 9_000, "Default", address(0), transactions);
_proposalTypes[1] = IProposalTypesConfigurator.ProposalType(3_500, 7_000, "Alt", address(0), transactions);
_proposalTypes[2] = IProposalTypesConfigurator.ProposalType(7_500, 3_100, "Whatever", address(0), transactions);
_proposalTypes[3] =
IProposalTypesConfigurator.ProposalType(0, 0, "Optimistic", address(optimisticModule), transactions);
AgoraGovernor _governor = AgoraGovernor(
payable(
new TransparentUpgradeableProxy(
Expand Down Expand Up @@ -2022,7 +2033,8 @@ contract CastVote is AgoraGovernorTest {
_mintAndDelegate(_voter, 100e18);
_mintAndDelegate(_voter2, 100e18);
vm.prank(admin);
proposalTypesConfigurator.setProposalType(0, 3_000, 9_910, "Default", address(0));
bytes32[] memory transactions = new bytes32[](1);
proposalTypesConfigurator.setProposalType(0, 3_000, 9_910, "Default", address(0), transactions);
address[] memory targets = new address[](1);
targets[0] = address(this);
uint256[] memory values = new uint256[](1);
Expand Down Expand Up @@ -2141,7 +2153,8 @@ contract CastVoteWithReasonAndParams is AgoraGovernorTest {
contract EditProposalType is AgoraGovernorTest {
function testFuzz_EditProposalTypeByAdminOrTimelock(uint256 _actorSeed) public virtual {
vm.startPrank(_adminOrTimelock(_actorSeed));
proposalTypesConfigurator.setProposalType(0, 3_000, 9_910, "Default", address(0));
bytes32[] memory transactions = new bytes32[](1);
proposalTypesConfigurator.setProposalType(0, 3_000, 9_910, "Default", address(0), transactions);

address[] memory targets = new address[](1);
targets[0] = address(this);
Expand Down
Loading

0 comments on commit d683339

Please sign in to comment.