Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resumed sealable handling on tiebreaker vote #259

Closed
Closed
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
8 changes: 7 additions & 1 deletion contracts/committees/HashConsensus.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ pragma solidity 0.8.26;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";

import {Duration} from "../types/Duration.sol";
import {Duration, Durations} from "../types/Duration.sol";
import {Timestamp, Timestamps} from "../types/Timestamp.sol";

/// @title HashConsensus Contract
Expand Down Expand Up @@ -49,6 +49,8 @@ abstract contract HashConsensus is Ownable {
uint256 private _quorum;
Duration private _timelockDuration;

Duration private immutable EXPIRATION_TIME = Durations.from(3 days);

mapping(bytes32 hash => HashState state) private _hashStates;
EnumerableSet.AddressSet private _members;
mapping(address signer => mapping(bytes32 hash => bool approve)) public approves;
Expand Down Expand Up @@ -306,4 +308,8 @@ abstract contract HashConsensus is Ownable {
revert CallerIsNotMember(msg.sender);
}
}

function _isExpired(bytes32 hash) internal view returns (bool) {
return EXPIRATION_TIME.addTo(_timelockDuration.addTo(_hashStates[hash].scheduledAt)) > Timestamps.now();
}
}
36 changes: 30 additions & 6 deletions contracts/committees/TiebreakerCoreCommittee.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {ITiebreakerCoreCommittee} from "../interfaces/ITiebreakerCoreCommittee.s
import {HashConsensus} from "./HashConsensus.sol";
import {ProposalsList} from "./ProposalsList.sol";

import {SealableCalls} from "../libraries/SealableCalls.sol";

enum ProposalType {
ScheduleProposal,
ResumeSealable
Expand All @@ -27,7 +29,9 @@ contract TiebreakerCoreCommittee is ITiebreakerCoreCommittee, HashConsensus, Pro
error ResumeSealableNonceMismatch();
error ProposalDoesNotExist(uint256 proposalId);
error InvalidSealable(address sealable);
error ExecutionExpired(bytes32 key);

uint256 public constant PAUSE_INFINITELY = type(uint256).max;
address public immutable DUAL_GOVERNANCE;

mapping(address sealable => uint256 nonce) private _sealableResumeNonces;
Expand Down Expand Up @@ -105,9 +109,13 @@ contract TiebreakerCoreCommittee is ITiebreakerCoreCommittee, HashConsensus, Pro
/// @notice Gets the current resume nonce for a sealable address
/// @dev Retrieves the resume nonce for the given sealable address
/// @param sealable The address of the sealable to get the nonce for
/// @return The current resume nonce for the sealable address
function getSealableResumeNonce(address sealable) external view returns (uint256) {
return _sealableResumeNonces[sealable];
/// @return currentNonce The current resume nonce for the sealable address
function getSealableResumeNonce(address sealable) public view returns (uint256 currentNonce) {
currentNonce = _sealableResumeNonces[sealable];
(, bytes32 key) = _encodeSealableResume(sealable, currentNonce);
if (_isExpired(key)) {
currentNonce++;
}
}

/// @notice Votes on a proposal to resume a sealable address
Expand All @@ -118,13 +126,23 @@ contract TiebreakerCoreCommittee is ITiebreakerCoreCommittee, HashConsensus, Pro
function sealableResume(address sealable, uint256 nonce) external {
_checkCallerIsMember();

if (sealable == address(0)) {
(bool isCallSucceed, uint256 resumeSinceTimestamp) = SealableCalls.callGetResumeSinceTimestamp(sealable);

/// @dev Prevents addition of paused or misbehaving sealables.
/// According to the current PausableUntil implementation, a contract is paused if `block.timestamp < resumeSinceTimestamp`.
/// Reference: https://github.com/lidofinance/core/blob/60bc9b77b036eec22b2ab8a3a1d49c6b6614c600/contracts/0.8.9/utils/PausableUntil.sol#L52
if (!isCallSucceed || block.timestamp >= resumeSinceTimestamp || resumeSinceTimestamp == PAUSE_INFINITELY) {
revert InvalidSealable(sealable);
}

if (nonce != _sealableResumeNonces[sealable]) {
uint256 currentNonce = getSealableResumeNonce(sealable);

if (nonce != currentNonce) {
revert ResumeSealableNonceMismatch();
}
if (currentNonce != _sealableResumeNonces[sealable]) {
_sealableResumeNonces[sealable] = currentNonce;
}

(bytes memory proposalData, bytes32 key) = _encodeSealableResume(sealable, nonce);
_vote(key, true);
Expand All @@ -151,7 +169,13 @@ contract TiebreakerCoreCommittee is ITiebreakerCoreCommittee, HashConsensus, Pro
/// @dev Executes the resume sealable proposal by calling the tiebreakerResumeSealable function on the Dual Governance contract
/// @param sealable The address to resume
function executeSealableResume(address sealable) external {
(, bytes32 key) = _encodeSealableResume(sealable, _sealableResumeNonces[sealable]);
uint256 currentNonce = getSealableResumeNonce(sealable);
(, bytes32 key) = _encodeSealableResume(sealable, currentNonce);

if (_isExpired(key)) {
revert ExecutionExpired(key);
}

_markUsed(key);
_sealableResumeNonces[sealable]++;
Address.functionCall(
Expand Down
11 changes: 10 additions & 1 deletion contracts/committees/TiebreakerSubCommittee.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {ITiebreakerCoreCommittee} from "../interfaces/ITiebreakerCoreCommittee.s
import {HashConsensus} from "./HashConsensus.sol";
import {ProposalsList} from "./ProposalsList.sol";

import {SealableCalls} from "../libraries/SealableCalls.sol";

enum ProposalType {
ScheduleProposal,
ResumeSealable
Expand All @@ -23,6 +25,7 @@ enum ProposalType {
contract TiebreakerSubCommittee is HashConsensus, ProposalsList {
error InvalidSealable(address sealable);

uint256 public constant PAUSE_INFINITELY = type(uint256).max;
address public immutable TIEBREAKER_CORE_COMMITTEE;

constructor(
Expand Down Expand Up @@ -100,9 +103,15 @@ contract TiebreakerSubCommittee is HashConsensus, ProposalsList {
function sealableResume(address sealable) external {
_checkCallerIsMember();

if (sealable == address(0)) {
(bool isCallSucceed, uint256 resumeSinceTimestamp) = SealableCalls.callGetResumeSinceTimestamp(sealable);

/// @dev Prevents addition of paused or misbehaving sealables.
/// According to the current PausableUntil implementation, a contract is paused if `block.timestamp < resumeSinceTimestamp`.
/// Reference: https://github.com/lidofinance/core/blob/60bc9b77b036eec22b2ab8a3a1d49c6b6614c600/contracts/0.8.9/utils/PausableUntil.sol#L52
if (!isCallSucceed || block.timestamp >= resumeSinceTimestamp || resumeSinceTimestamp == PAUSE_INFINITELY) {
revert InvalidSealable(sealable);
}

(bytes memory proposalData, bytes32 key,) = _encodeSealableResume(sealable);
_vote(key, true);
_pushProposal(key, uint256(ProposalType.ResumeSealable), proposalData);
Expand Down
11 changes: 11 additions & 0 deletions test/unit/committees/TiebreakerCore.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {Timestamp} from "contracts/types/Timestamp.sol";

import {ITimelock} from "contracts/interfaces/ITimelock.sol";
import {ITiebreaker} from "contracts/interfaces/ITiebreaker.sol";
import {ISealable} from "contracts/libraries/SealableCalls.sol";

import {TargetMock} from "test/utils/target-mock.sol";
import {UnitTest} from "test/utils/unit-test.sol";
Expand Down Expand Up @@ -125,6 +126,7 @@ contract TiebreakerCoreUnitTest is UnitTest {
function test_sealableResume_HappyPath() external {
uint256 nonce = tiebreakerCore.getSealableResumeNonce(sealable);

_mockSealableResumeSinceTimestampResult(sealable, block.timestamp + 1);
vm.prank(committeeMembers[0]);
tiebreakerCore.sealableResume(sealable, nonce);

Expand Down Expand Up @@ -159,6 +161,7 @@ contract TiebreakerCoreUnitTest is UnitTest {
function test_executeSealableResume_HappyPath() external {
uint256 nonce = tiebreakerCore.getSealableResumeNonce(sealable);

_mockSealableResumeSinceTimestampResult(sealable, block.timestamp + 1000);
vm.prank(committeeMembers[0]);
tiebreakerCore.sealableResume(sealable, nonce);
vm.prank(committeeMembers[1]);
Expand Down Expand Up @@ -215,4 +218,12 @@ contract TiebreakerCoreUnitTest is UnitTest {
assertEq(quorumAt, quorumAtExpected);
assertTrue(isExecuted);
}

function _mockSealableResumeSinceTimestampResult(address sealableAddress, uint256 resumeSinceTimestamp) internal {
vm.mockCall(
sealableAddress,
abi.encodeWithSelector(ISealable.getResumeSinceTimestamp.selector),
abi.encode(resumeSinceTimestamp)
);
}
}
15 changes: 14 additions & 1 deletion test/unit/committees/TiebreakerSubCommittee.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {TiebreakerCoreCommittee} from "contracts/committees/TiebreakerCoreCommit
import {TiebreakerSubCommittee, ProposalType} from "contracts/committees/TiebreakerSubCommittee.sol";
import {HashConsensus} from "contracts/committees/HashConsensus.sol";
import {Timestamp} from "contracts/types/Timestamp.sol";
import {ISealable} from "contracts/libraries/SealableCalls.sol";
import {UnitTest} from "test/utils/unit-test.sol";

import {TargetMock} from "test/utils/target-mock.sol";
Expand Down Expand Up @@ -121,6 +122,7 @@ contract TiebreakerSubCommitteeUnitTest is UnitTest {
abi.encode(0)
);

_mockSealableResumeSinceTimestampResult(sealable, block.timestamp + 1);
vm.prank(committeeMembers[0]);
tiebreakerSubCommittee.sealableResume(sealable);

Expand Down Expand Up @@ -158,7 +160,7 @@ contract TiebreakerSubCommitteeUnitTest is UnitTest {
abi.encodeWithSelector(ITiebreakerCoreCommittee.getSealableResumeNonce.selector, sealable),
abi.encode(0)
);

_mockSealableResumeSinceTimestampResult(sealable, block.timestamp + 1);
vm.prank(committeeMembers[0]);
tiebreakerSubCommittee.sealableResume(sealable);
vm.prank(committeeMembers[1]);
Expand All @@ -181,6 +183,8 @@ contract TiebreakerSubCommitteeUnitTest is UnitTest {
abi.encode(0)
);

_mockSealableResumeSinceTimestampResult(sealable, block.timestamp + 1);

vm.prank(committeeMembers[0]);
tiebreakerSubCommittee.sealableResume(sealable);

Expand Down Expand Up @@ -236,6 +240,7 @@ contract TiebreakerSubCommitteeUnitTest is UnitTest {
abi.encodeWithSelector(ITiebreakerCoreCommittee.getSealableResumeNonce.selector, sealable),
abi.encode(0)
);
_mockSealableResumeSinceTimestampResult(sealable, block.timestamp + 1);

(uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) =
tiebreakerSubCommittee.getSealableResumeState(sealable);
Expand Down Expand Up @@ -271,4 +276,12 @@ contract TiebreakerSubCommitteeUnitTest is UnitTest {
assertEq(quorumAt, Timestamp.wrap(uint40(block.timestamp)));
assertTrue(isExecuted);
}

function _mockSealableResumeSinceTimestampResult(address sealableAddress, uint256 resumeSinceTimestamp) internal {
vm.mockCall(
sealableAddress,
abi.encodeWithSelector(ISealable.getResumeSinceTimestamp.selector),
abi.encode(resumeSinceTimestamp)
);
}
}
9 changes: 9 additions & 0 deletions test/utils/scenario-test-blueprint.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {IDualGovernance} from "contracts/interfaces/IDualGovernance.sol";
import {ITimelock} from "contracts/interfaces/ITimelock.sol";
import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol";
import {IPotentiallyDangerousContract} from "./interfaces/IPotentiallyDangerousContract.sol";
import {ISealable} from "contracts/libraries/SealableCalls.sol";

// ---
// Libraries
Expand Down Expand Up @@ -601,4 +602,12 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment {
)
);
}

function _mockSealableResumeSinceTimestampResult(address sealableAddress, uint256 resumeSinceTimestamp) internal {
vm.mockCall(
sealableAddress,
abi.encodeWithSelector(ISealable.getResumeSinceTimestamp.selector),
abi.encode(resumeSinceTimestamp)
);
}
}