Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions l1-contracts/src/governance/Governance.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {CompressedProposal, CompressedProposalLib} from "@aztec/governance/libra
import {ConfigurationLib} from "@aztec/governance/libraries/ConfigurationLib.sol";
import {Errors} from "@aztec/governance/libraries/Errors.sol";
import {ProposalLib, VoteTabulationReturn} from "@aztec/governance/libraries/ProposalLib.sol";
import {CompressedTimeMath, CompressedTimestamp} from "@aztec/shared/libraries/CompressedTimeMath.sol";
import {Timestamp} from "@aztec/shared/libraries/TimeMath.sol";
import {IERC20} from "@oz/token/ERC20/IERC20.sol";
import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol";
Expand Down Expand Up @@ -141,6 +142,7 @@ contract Governance is IGovernance {
using ConfigurationLib for CompressedConfiguration;
using CompressedConfigurationLib for CompressedConfiguration;
using CompressedProposalLib for CompressedProposal;
using CompressedTimeMath for CompressedTimestamp;
using BallotLib for CompressedBallot;

IERC20 public immutable ASSET;
Expand Down Expand Up @@ -219,6 +221,14 @@ contract Governance is IGovernance {
*/
uint256 public withdrawalCount;

/**
* @dev The earliest timestamp at which a user may complete a withdrawal after casting votes.
*
* Voting locks all of the voter's power until the latest proposal they voted on is executable, so a later
* configuration update cannot shorten the withdrawal delay for votes already counted under frozen proposal timing.
*/
mapping(address user => Timestamp exitTimestamp) internal earliestVoteExit;

/**
* @dev Modifier to ensure that the caller is the governance contract itself.
*
Expand Down Expand Up @@ -469,6 +479,13 @@ contract Governance is IGovernance {
ballots[_proposalId][msg.sender] = userBallot.addNay(_amount);
proposal.addNay(_amount);
}
if (_amount > 0) {
Timestamp executionBuffer = Timestamp.wrap(Timestamp.unwrap(proposal.votingDelay.decompress()) / 5 + 1);
Timestamp executableAt = proposal.queuedThrough() + executionBuffer;
if (executableAt > earliestVoteExit[msg.sender]) {
earliestVoteExit[msg.sender] = executableAt;
}
}

emit VoteCast(_proposalId, msg.sender, _support, _amount);
}
Expand Down Expand Up @@ -738,9 +755,12 @@ contract Governance is IGovernance {

uint256 withdrawalId = withdrawalCount++;

withdrawals[withdrawalId] = Withdrawal({
amount: _amount, unlocksAt: Timestamp.wrap(block.timestamp) + _delay, recipient: _to, claimed: false
});
Timestamp unlocksAt = Timestamp.wrap(block.timestamp) + _delay;
if (earliestVoteExit[_from] > unlocksAt) {
unlocksAt = earliestVoteExit[_from];
}

withdrawals[withdrawalId] = Withdrawal({amount: _amount, unlocksAt: unlocksAt, recipient: _to, claimed: false});

emit WithdrawInitiated(withdrawalId, _to, _amount);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.27;

import {GovernanceBase} from "../base.t.sol";
import {EmptyPayload} from "../TestPayloads.sol";
import {
IGovernance,
Configuration,
Proposal,
ProposalState,
Withdrawal
} from "@aztec/governance/interfaces/IGovernance.sol";
import {IPayload} from "@aztec/governance/interfaces/IPayload.sol";
import {ConfigurationLib} from "@aztec/governance/libraries/ConfigurationLib.sol";
import {Errors} from "@aztec/governance/libraries/Errors.sol";
import {Timestamp} from "@aztec/core/libraries/TimeLib.sol";

contract UpdateConfigurationPayload is IPayload {
IGovernance internal immutable GOVERNANCE;
Configuration internal configuration;

constructor(IGovernance _governance, Configuration memory _configuration) {
GOVERNANCE = _governance;
configuration = _configuration;
}

function getActions() external view override(IPayload) returns (IPayload.Action[] memory) {
IPayload.Action[] memory res = new IPayload.Action[](1);
Configuration memory updatedConfiguration = configuration;

res[0] = Action({
target: address(GOVERNANCE),
data: abi.encodeWithSelector(IGovernance.updateConfiguration.selector, updatedConfiguration)
});

return res;
}

function getURI() external pure override(IPayload) returns (string memory) {
return "UpdateConfigurationPayload";
}
}

contract VoteAndExitAfterConfigChangeTest is GovernanceBase {
address internal constant VOTER = address(0xb0b);
uint256 internal constant VOTER_POWER = 400e18;

function test_CannotFinalizeWithdrawBeforeVotedProposalIsExecutableAfterConfigReduction() external {
token.mint(VOTER, VOTER_POWER);
vm.startPrank(VOTER);
token.approve(address(governance), VOTER_POWER);
governance.deposit(VOTER, VOTER_POWER);
vm.stopPrank();

Configuration memory shortConfig = governance.getConfiguration();
shortConfig.votingDelay = ConfigurationLib.TIME_LOWER;
shortConfig.votingDuration = ConfigurationLib.TIME_LOWER;
shortConfig.executionDelay = ConfigurationLib.TIME_LOWER;

UpdateConfigurationPayload configPayload =
new UpdateConfigurationPayload(IGovernance(address(governance)), shortConfig);

vm.prank(address(governanceProposer));
governance.propose(configPayload);
uint256 configProposalId = governance.proposalCount() - 1;
Proposal memory configProposal = governance.getProposal(configProposalId);

vm.warp(block.timestamp + 200);

EmptyPayload laterPayload = new EmptyPayload();
vm.prank(address(governanceProposer));
governance.propose(laterPayload);
uint256 laterProposalId = governance.proposalCount() - 1;
Proposal memory laterProposal = governance.getProposal(laterProposalId);

vm.warp(Timestamp.unwrap(upw.pendingThrough(laterProposal)) + 1);

vm.startPrank(VOTER);
governance.vote(configProposalId, VOTER_POWER, true);
governance.vote(laterProposalId, VOTER_POWER, true);
vm.stopPrank();

Timestamp laterVoteExit = Timestamp.wrap(
Timestamp.unwrap(upw.queuedThrough(laterProposal)) + Timestamp.unwrap(laterProposal.config.votingDelay) / 5 + 1
);

vm.warp(Timestamp.unwrap(upw.queuedThrough(configProposal)) + 1);
assertEq(governance.getProposalState(configProposalId), ProposalState.Executable);
governance.execute(configProposalId);

Timestamp liveConfigUnlock = Timestamp.wrap(block.timestamp) + upw.getWithdrawalDelay(shortConfig);
assertLt(Timestamp.unwrap(liveConfigUnlock), Timestamp.unwrap(laterVoteExit));

vm.prank(VOTER);
uint256 withdrawalId = governance.initiateWithdraw(VOTER, VOTER_POWER);

Withdrawal memory withdrawal = governance.getWithdrawal(withdrawalId);
assertEq(withdrawal.unlocksAt, laterVoteExit);

vm.warp(Timestamp.unwrap(liveConfigUnlock));
vm.expectRevert(
abi.encodeWithSelector(
Errors.Governance__WithdrawalNotUnlockedYet.selector, Timestamp.wrap(block.timestamp), withdrawal.unlocksAt
)
);
governance.finalizeWithdraw(withdrawalId);

vm.warp(Timestamp.unwrap(withdrawal.unlocksAt));
assertEq(governance.getProposalState(laterProposalId), ProposalState.Executable);
governance.finalizeWithdraw(withdrawalId);
}
}
Loading