Skip to content

Commit fee53be

Browse files
LHerskindMaddiaa0
authored andcommitted
feat: lock to propose (#9430)
Fixes #9348
1 parent efdd3db commit fee53be

14 files changed

+262
-42
lines changed

l1-contracts/src/governance/Apella.sol

+72-28
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ contract Apella is IApella {
3737
gerousia = _gerousia;
3838

3939
configuration = DataStructures.Configuration({
40+
proposeConfig: DataStructures.ProposeConfiguration({
41+
lockDelay: Timestamp.wrap(3600),
42+
lockAmount: 1000e18
43+
}),
4044
votingDelay: Timestamp.wrap(3600),
4145
votingDuration: Timestamp.wrap(3600),
4246
executionDelay: Timestamp.wrap(3600),
@@ -81,21 +85,7 @@ contract Apella is IApella {
8185
override(IApella)
8286
returns (uint256)
8387
{
84-
users[msg.sender].sub(_amount);
85-
total.sub(_amount);
86-
87-
uint256 withdrawalId = withdrawalCount++;
88-
89-
withdrawals[withdrawalId] = DataStructures.Withdrawal({
90-
amount: _amount,
91-
unlocksAt: Timestamp.wrap(block.timestamp) + configuration.lockDelay(),
92-
recipient: _to,
93-
claimed: false
94-
});
95-
96-
emit WithdrawInitiated(withdrawalId, _to, _amount);
97-
98-
return withdrawalId;
88+
return _initiateWithdraw(_to, _amount, configuration.withdrawalDelay());
9989
}
10090

10191
function finaliseWithdraw(uint256 _withdrawalId) external override(IApella) {
@@ -114,21 +104,37 @@ contract Apella is IApella {
114104

115105
function propose(IPayload _proposal) external override(IApella) returns (bool) {
116106
require(msg.sender == gerousia, Errors.Apella__CallerNotGerousia(msg.sender, gerousia));
107+
return _propose(_proposal);
108+
}
117109

118-
uint256 proposalId = proposalCount++;
119-
120-
proposals[proposalId] = DataStructures.Proposal({
121-
config: configuration,
122-
state: DataStructures.ProposalState.Pending,
123-
payload: _proposal,
124-
creator: msg.sender,
125-
creation: Timestamp.wrap(block.timestamp),
126-
summedBallot: DataStructures.Ballot({yea: 0, nea: 0})
127-
});
110+
/**
111+
* @notice Propose a new proposal by locking up a bunch of power
112+
*
113+
* Beware that if the gerousia changes these proposals will also be dropped
114+
* This is to ensure consistency around way proposals are made, and they should
115+
* really be using the proposal logic in Gerousia, which might have a similar
116+
* mechanism in place as well.
117+
* It is here for emergency purposes.
118+
* Using the lock should be a last resort if the Gerousia is broken.
119+
*
120+
* @param _proposal The proposal to propose
121+
* @param _to The address to send the lock to
122+
* @return True if the proposal was proposed
123+
*/
124+
function proposeWithLock(IPayload _proposal, address _to)
125+
external
126+
override(IApella)
127+
returns (bool)
128+
{
129+
uint256 availablePower = users[msg.sender].powerNow();
130+
uint256 amount = configuration.proposeConfig.lockAmount;
128131

129-
emit Proposed(proposalId, address(_proposal));
132+
require(
133+
amount <= availablePower, Errors.Apella__InsufficientPower(msg.sender, availablePower, amount)
134+
);
130135

131-
return true;
136+
_initiateWithdraw(_to, amount, configuration.proposeConfig.lockDelay);
137+
return _propose(_proposal);
132138
}
133139

134140
function vote(uint256 _proposalId, uint256 _amount, bool _support)
@@ -264,7 +270,7 @@ contract Apella is IApella {
264270
}
265271

266272
// If the gerousia have changed we mark is as dropped
267-
if (gerousia != self.creator) {
273+
if (gerousia != self.gerousia) {
268274
return DataStructures.ProposalState.Dropped;
269275
}
270276

@@ -294,4 +300,42 @@ contract Apella is IApella {
294300

295301
return DataStructures.ProposalState.Expired;
296302
}
303+
304+
function _initiateWithdraw(address _to, uint256 _amount, Timestamp _delay)
305+
internal
306+
returns (uint256)
307+
{
308+
users[msg.sender].sub(_amount);
309+
total.sub(_amount);
310+
311+
uint256 withdrawalId = withdrawalCount++;
312+
313+
withdrawals[withdrawalId] = DataStructures.Withdrawal({
314+
amount: _amount,
315+
unlocksAt: Timestamp.wrap(block.timestamp) + _delay,
316+
recipient: _to,
317+
claimed: false
318+
});
319+
320+
emit WithdrawInitiated(withdrawalId, _to, _amount);
321+
322+
return withdrawalId;
323+
}
324+
325+
function _propose(IPayload _proposal) internal returns (bool) {
326+
uint256 proposalId = proposalCount++;
327+
328+
proposals[proposalId] = DataStructures.Proposal({
329+
config: configuration,
330+
state: DataStructures.ProposalState.Pending,
331+
payload: _proposal,
332+
gerousia: gerousia,
333+
creation: Timestamp.wrap(block.timestamp),
334+
summedBallot: DataStructures.Ballot({yea: 0, nea: 0})
335+
});
336+
337+
emit Proposed(proposalId, address(_proposal));
338+
339+
return true;
340+
}
297341
}

l1-contracts/src/governance/interfaces/IApella.sol

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ interface IApella {
2222
function initiateWithdraw(address _to, uint256 _amount) external returns (uint256);
2323
function finaliseWithdraw(uint256 _withdrawalId) external;
2424
function propose(IPayload _proposal) external returns (bool);
25+
function proposeWithLock(IPayload _proposal, address _to) external returns (bool);
2526
function vote(uint256 _proposalId, uint256 _amount, bool _support) external returns (bool);
2627
function execute(uint256 _proposalId) external returns (bool);
2728
function dropProposal(uint256 _proposalId) external returns (bool);

l1-contracts/src/governance/libraries/ConfigurationLib.sol

+21-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ library ConfigurationLib {
1818
Timestamp internal constant TIME_LOWER = Timestamp.wrap(3600);
1919
Timestamp internal constant TIME_UPPER = Timestamp.wrap(30 * 24 * 3600);
2020

21-
function lockDelay(DataStructures.Configuration storage _self) internal view returns (Timestamp) {
21+
function withdrawalDelay(DataStructures.Configuration storage _self)
22+
internal
23+
view
24+
returns (Timestamp)
25+
{
2226
return Timestamp.wrap(Timestamp.unwrap(_self.votingDelay) / 5) + _self.votingDuration
2327
+ _self.executionDelay;
2428
}
@@ -40,6 +44,22 @@ library ConfigurationLib {
4044
require(
4145
_self.minimumVotes >= VOTES_LOWER, Errors.Apella__ConfigurationLib__InvalidMinimumVotes()
4246
);
47+
require(
48+
_self.proposeConfig.lockAmount >= VOTES_LOWER,
49+
Errors.Apella__ConfigurationLib__LockAmountTooSmall()
50+
);
51+
52+
// Beyond checking the bounds like this, it might be useful to ensure that the value is larger than the withdrawal delay
53+
// this, can be useful if one want to ensure that the "locker" cannot himself vote in the proposal, but as it is unclear
54+
// if this is a useful property, it is not enforced.
55+
require(
56+
_self.proposeConfig.lockDelay >= TIME_LOWER,
57+
Errors.Apella__ConfigurationLib__TimeTooSmall("LockDelay")
58+
);
59+
require(
60+
_self.proposeConfig.lockDelay <= TIME_UPPER,
61+
Errors.Apella__ConfigurationLib__TimeTooBig("LockDelay")
62+
);
4363

4464
require(
4565
_self.votingDelay >= TIME_LOWER, Errors.Apella__ConfigurationLib__TimeTooSmall("VotingDelay")

l1-contracts/src/governance/libraries/DataStructures.sol

+7-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ library DataStructures {
2323
}
2424
// docs:end:registry_snapshot
2525

26+
struct ProposeConfiguration {
27+
Timestamp lockDelay;
28+
uint256 lockAmount;
29+
}
30+
2631
struct Configuration {
32+
ProposeConfiguration proposeConfig;
2733
Timestamp votingDelay;
2834
Timestamp votingDuration;
2935
Timestamp executionDelay;
@@ -53,7 +59,7 @@ library DataStructures {
5359
Configuration config;
5460
ProposalState state;
5561
IPayload payload;
56-
address creator;
62+
address gerousia;
5763
Timestamp creation;
5864
Ballot summedBallot;
5965
}

l1-contracts/src/governance/libraries/Errors.sol

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ library Errors {
3131
error Apella__UserLib__NotInPast();
3232

3333
error Apella__ConfigurationLib__InvalidMinimumVotes();
34+
error Apella__ConfigurationLib__LockAmountTooSmall();
3435
error Apella__ConfigurationLib__QuorumTooSmall();
3536
error Apella__ConfigurationLib__QuorumTooBig();
3637
error Apella__ConfigurationLib__DifferentialTooSmall();

l1-contracts/test/governance/apella/base.t.sol

+1-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ contract ApellaBase is TestBase {
9999
proposal = proposals[_proposalName];
100100
proposalId = proposalIds[_proposalName];
101101

102-
vm.assume(_gerousia != proposal.creator);
102+
vm.assume(_gerousia != proposal.gerousia);
103103

104104
vm.prank(address(apella));
105105
apella.updateGerousia(_gerousia);

l1-contracts/test/governance/apella/getProposalState.t.sol

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// SPDX-License-Identifier: UNLICENSED
22
pragma solidity >=0.8.27;
33

4+
import {stdStorage, StdStorage} from "forge-std/Test.sol";
5+
46
import {ApellaBase} from "./base.t.sol";
57
import {DataStructures} from "@aztec/governance/libraries/DataStructures.sol";
68
import {Errors} from "@aztec/governance/libraries/Errors.sol";
@@ -9,6 +11,7 @@ import {ProposalLib, VoteTabulationReturn} from "@aztec/governance/libraries/Pro
911

1012
contract GetProposalStateTest is ApellaBase {
1113
using ProposalLib for DataStructures.Proposal;
14+
using stdStorage for StdStorage;
1215

1316
function test_WhenProposalIsOutOfBounds(uint256 _index) external {
1417
// it revert
@@ -153,9 +156,8 @@ contract GetProposalStateTest is ApellaBase {
153156

154157
// We can overwrite the quorum to be 0 to hit an invalid case
155158
assertGt(apella.getProposal(proposalId).config.quorum, 0);
156-
bytes32 slot =
157-
bytes32(uint256(keccak256(abi.encodePacked(uint256(proposalId), uint256(1)))) + 4);
158-
vm.store(address(apella), slot, 0);
159+
stdstore.target(address(apella)).sig("getProposal(uint256)").with_key(proposalId).depth(6)
160+
.checked_write(uint256(0));
159161
assertEq(apella.getProposal(proposalId).config.quorum, 0);
160162

161163
uint256 totalPower = apella.totalPowerAt(Timestamp.wrap(block.timestamp));

l1-contracts/test/governance/apella/initiateWithdraw.t.sol

+1-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ pragma solidity >=0.8.27;
33

44
import {ApellaBase} from "./base.t.sol";
55
import {IApella} from "@aztec/governance/interfaces/IApella.sol";
6-
import {IERC20Errors} from "@oz/interfaces/draft-IERC6093.sol";
76
import {Timestamp} from "@aztec/core/libraries/TimeMath.sol";
87
import {Errors} from "@aztec/governance/libraries/Errors.sol";
98
import {DataStructures} from "@aztec/governance/libraries/DataStructures.sol";
@@ -88,7 +87,7 @@ contract InitiateWithdrawTest is ApellaBase {
8887
assertEq(withdrawal.amount, amount, "invalid amount");
8988
assertEq(
9089
withdrawal.unlocksAt,
91-
Timestamp.wrap(block.timestamp) + config.lockDelay(),
90+
Timestamp.wrap(block.timestamp) + config.withdrawalDelay(),
9291
"Invalid timestamp"
9392
);
9493
assertEq(withdrawal.recipient, recipient, "invalid recipient");

l1-contracts/test/governance/apella/propose.t.sol

+1-3
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@ pragma solidity >=0.8.27;
44
import {IPayload} from "@aztec/governance/interfaces/IPayload.sol";
55
import {ApellaBase} from "./base.t.sol";
66
import {IApella} from "@aztec/governance/interfaces/IApella.sol";
7-
import {IERC20Errors} from "@oz/interfaces/draft-IERC6093.sol";
87
import {Timestamp} from "@aztec/core/libraries/TimeMath.sol";
98
import {Errors} from "@aztec/governance/libraries/Errors.sol";
109
import {DataStructures} from "@aztec/governance/libraries/DataStructures.sol";
11-
import {ConfigurationLib} from "@aztec/governance/libraries/ConfigurationLib.sol";
1210

1311
contract ProposeTest is ApellaBase {
1412
function test_WhenCallerIsNotGerousia() external {
@@ -45,7 +43,7 @@ contract ProposeTest is ApellaBase {
4543
assertEq(proposal.config.votingDelay, config.votingDelay);
4644
assertEq(proposal.config.votingDuration, config.votingDuration);
4745
assertEq(proposal.creation, Timestamp.wrap(block.timestamp));
48-
assertEq(proposal.creator, address(gerousia));
46+
assertEq(proposal.gerousia, address(gerousia));
4947
assertEq(proposal.summedBallot.nea, 0);
5048
assertEq(proposal.summedBallot.yea, 0);
5149
assertTrue(proposal.state == DataStructures.ProposalState.Pending);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity >=0.8.27;
3+
4+
import {IPayload} from "@aztec/governance/interfaces/IPayload.sol";
5+
import {ApellaBase} from "./base.t.sol";
6+
import {IApella} from "@aztec/governance/interfaces/IApella.sol";
7+
import {Timestamp} from "@aztec/core/libraries/TimeMath.sol";
8+
import {Errors} from "@aztec/governance/libraries/Errors.sol";
9+
import {DataStructures} from "@aztec/governance/libraries/DataStructures.sol";
10+
11+
contract ProposeWithLockTest is ApellaBase {
12+
function test_WhenCallerHasInsufficientPower() external {
13+
// it revert
14+
DataStructures.Configuration memory config = apella.getConfiguration();
15+
vm.expectRevert(
16+
abi.encodeWithSelector(
17+
Errors.Apella__InsufficientPower.selector, address(this), 0, config.proposeConfig.lockAmount
18+
)
19+
);
20+
apella.proposeWithLock(IPayload(address(0)), address(this));
21+
}
22+
23+
function test_WhenCallerHasSufficientPower(address _proposal) external {
24+
// it creates a withdrawal with the lock amount and delay
25+
// it creates a new proposal with current config
26+
// it emits a {ProposalCreated} event
27+
// it returns true
28+
DataStructures.Configuration memory config = apella.getConfiguration();
29+
token.mint(address(this), config.proposeConfig.lockAmount);
30+
31+
token.approve(address(apella), config.proposeConfig.lockAmount);
32+
apella.deposit(address(this), config.proposeConfig.lockAmount);
33+
34+
proposalId = apella.proposalCount();
35+
36+
vm.expectEmit(true, true, true, true, address(apella));
37+
emit IApella.Proposed(proposalId, _proposal);
38+
39+
assertTrue(apella.proposeWithLock(IPayload(_proposal), address(this)));
40+
41+
DataStructures.Proposal memory proposal = apella.getProposal(proposalId);
42+
assertEq(proposal.config.executionDelay, config.executionDelay);
43+
assertEq(proposal.config.gracePeriod, config.gracePeriod);
44+
assertEq(proposal.config.minimumVotes, config.minimumVotes);
45+
assertEq(proposal.config.quorum, config.quorum);
46+
assertEq(proposal.config.voteDifferential, config.voteDifferential);
47+
assertEq(proposal.config.votingDelay, config.votingDelay);
48+
assertEq(proposal.config.votingDuration, config.votingDuration);
49+
assertEq(proposal.creation, Timestamp.wrap(block.timestamp));
50+
assertEq(proposal.gerousia, address(gerousia));
51+
assertEq(proposal.summedBallot.nea, 0);
52+
assertEq(proposal.summedBallot.yea, 0);
53+
assertTrue(proposal.state == DataStructures.ProposalState.Pending);
54+
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
ProposeWithLockTest
2+
├── when caller has insufficient power
3+
│ └── it revert
4+
└── when caller has sufficient power
5+
├── it creates a withdrawal with the lock amount and delay
6+
├── it creates a new proposal with current config
7+
├── it emits a {ProposalCreated} event
8+
└── it returns true

0 commit comments

Comments
 (0)