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

Initial timelock can be zero #19

Merged
merged 3 commits into from
Aug 17, 2024
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Those rewards can be transferred to the `skimRecipient`.
The vault's owner has the choice to distribute back these rewards to vault depositors however they want.
For more information about this use case, see the [Rewards](#rewards) section.

All actions that may be against users' interests (e.g. enabling a market with a high exposure) can be subject to a timelock of up to two weeks.
All actions that may be against users' interests (e.g. enabling a market with a high exposure) is subject to a timelock. To make vault setup easier, the initial timelock can be anywhere between 0 seconds and 2 weeks. Any further timelock change must set the value between 24 hours and 2 weeks.
The `owner`, or the `guardian` if set, can revoke the action during the timelock.
After the timelock, the action can be executed by anyone.

Expand Down
4 changes: 4 additions & 0 deletions certora/helpers/MetaMorphoHarness.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ contract MetaMorphoHarness is MetaMorpho {
return pendingCap[id];
}

function minTimelock() external pure returns (uint256) {
return ConstantsLib.MIN_TIMELOCK;
}

function maxTimelock() external pure returns (uint256) {
return ConstantsLib.MAX_TIMELOCK;
}
Expand Down
4 changes: 2 additions & 2 deletions certora/specs/PendingValues.spec
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import "Range.spec";
function hasNoBadPendingTimelock() returns bool {
MetaMorphoHarness.PendingUint192 pendingTimelock = pendingTimelock_();

return pendingTimelock.validAt == 0 => pendingTimelock.value == 0;
return pendingTimelock.validAt == 0 <=> pendingTimelock.value == 0;
}

// Check that having no pending timelock value is equivalent to having its valid timestamp at 0.
Expand All @@ -22,7 +22,7 @@ invariant noBadPendingTimelock()

// Check that the pending timelock value is always strictly smaller than the current timelock value.
invariant smallerPendingTimelock()
assert_uint256(pendingTimelock_().value) < timelock() || timelock() == 0
assert_uint256(pendingTimelock_().value) < timelock()
{
preserved with (env e) {
requireInvariant pendingTimelockInRange();
Expand Down
4 changes: 3 additions & 1 deletion certora/specs/Range.spec
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ methods {
function isAllocator(address) external returns(bool) envfree;
function skimRecipient() external returns(address) envfree;

function minTimelock() external returns(uint256) envfree;
function maxTimelock() external returns(uint256) envfree;
function maxQueueLength() external returns(uint256) envfree;
function maxFee() external returns(uint256) envfree;
Expand All @@ -32,7 +33,8 @@ function isPendingTimelockInRange() returns bool {
MetaMorphoHarness.PendingUint192 pendingTimelock = pendingTimelock_();

return pendingTimelock.validAt != 0 =>
assert_uint256(pendingTimelock.value) <= maxTimelock();
assert_uint256(pendingTimelock.value) <= maxTimelock() &&
assert_uint256(pendingTimelock.value) >= minTimelock();
}

// Check that the pending timelock is bounded by the max timelock.
Expand Down
12 changes: 12 additions & 0 deletions certora/specs/Timelock.spec
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,15 @@ rule removableTime(env e_next, method f, calldataarg args) {
}
assert true;
}

rule timelockZero(env e, method f, calldataarg args) {
requireInvariant pendingTimelockInRange();

uint256 timelockBefore = timelock();

f(e, args);

uint256 timelockAfter = timelock();

assert timelockAfter == 0 => timelockBefore == 0;
}
1 change: 1 addition & 0 deletions src/MetaMorpho.sol
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph
if (newTimelock == timelock) revert ErrorsLib.AlreadySet();
if (pendingTimelock.validAt != 0) revert ErrorsLib.AlreadyPending();
if (newTimelock > ConstantsLib.MAX_TIMELOCK) revert ErrorsLib.AboveMaxTimelock();
if (newTimelock < ConstantsLib.MIN_TIMELOCK) revert ErrorsLib.BelowMinTimelock();

if (newTimelock > timelock) {
_setTimelock(newTimelock);
Expand Down
3 changes: 3 additions & 0 deletions src/libraries/ConstantsLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ library ConstantsLib {
/// @dev The maximum delay of a timelock.
uint256 internal constant MAX_TIMELOCK = 2 weeks;

/// @dev The minimum delay of a timelock.
uint256 internal constant MIN_TIMELOCK = 1 days;

/// @dev The maximum number of markets in the supply/withdraw queue.
uint256 internal constant MAX_QUEUE_LENGTH = 30;

Expand Down
3 changes: 3 additions & 0 deletions src/libraries/ErrorsLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ library ErrorsLib {
/// @notice Thrown when the submitted timelock is above the max timelock.
error AboveMaxTimelock();

/// @notice Thrown when the submitted timelock is below the min timelock.
error BelowMinTimelock();

/// @notice Thrown when the timelock is not elapsed.
error TimelockNotElapsed();

Expand Down
4 changes: 2 additions & 2 deletions test/forge/GuardianTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ contract GuardianTest is IntegrationTest {
}

function testGuardianRevokePendingTimelockDecreased(uint256 timelock, uint256 elapsed) public {
timelock = bound(timelock, 0, TIMELOCK - 1);
timelock = bound(timelock, ConstantsLib.MIN_TIMELOCK, TIMELOCK - 1);
elapsed = bound(elapsed, 0, TIMELOCK - 1);

vm.prank(OWNER);
Expand All @@ -48,7 +48,7 @@ contract GuardianTest is IntegrationTest {
}

function testOwnerRevokePendingTimelockDecreased(uint256 timelock, uint256 elapsed) public {
timelock = bound(timelock, 0, TIMELOCK - 1);
timelock = bound(timelock, ConstantsLib.MIN_TIMELOCK, TIMELOCK - 1);
elapsed = bound(elapsed, 0, TIMELOCK - 1);

vm.prank(OWNER);
Expand Down
2 changes: 1 addition & 1 deletion test/forge/RevokeTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ contract RevokeTest is IntegrationTest {
}

function testOwnerRevokeTimelockDecreased(uint256 timelock, uint256 elapsed) public {
timelock = bound(timelock, 0, TIMELOCK - 1);
timelock = bound(timelock, ConstantsLib.MIN_TIMELOCK, TIMELOCK - 1);
elapsed = bound(elapsed, 0, TIMELOCK - 1);

vm.prank(OWNER);
Expand Down
20 changes: 14 additions & 6 deletions test/forge/TimelockTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ contract TimelockTest is IntegrationTest {
}

function testSubmitTimelockDecreased(uint256 timelock) public {
timelock = bound(timelock, 0, TIMELOCK - 1);
timelock = bound(timelock, ConstantsLib.MIN_TIMELOCK, TIMELOCK - 1);

vm.expectEmit();
emit EventsLib.SubmitTimelock(timelock);
Expand All @@ -50,7 +50,7 @@ contract TimelockTest is IntegrationTest {
}

function testSubmitTimelockAlreadyPending(uint256 timelock) public {
timelock = bound(timelock, 0, TIMELOCK - 1);
timelock = bound(timelock, ConstantsLib.MIN_TIMELOCK, TIMELOCK - 1);

vm.prank(OWNER);
vault.submitTimelock(timelock);
Expand Down Expand Up @@ -88,8 +88,16 @@ contract TimelockTest is IntegrationTest {
vault.submitTimelock(timelock);
}

function testSubmitTimelockBelowMinTimelock(uint256 timelock) public {
timelock = bound(timelock, 0, ConstantsLib.MIN_TIMELOCK - 1);

vm.prank(OWNER);
vm.expectRevert(ErrorsLib.BelowMinTimelock.selector);
vault.submitTimelock(timelock);
}

function testAcceptTimelock(uint256 timelock) public {
timelock = bound(timelock, 0, TIMELOCK - 1);
timelock = bound(timelock, ConstantsLib.MIN_TIMELOCK, TIMELOCK - 1);

vm.prank(OWNER);
vault.submitTimelock(timelock);
Expand All @@ -114,7 +122,7 @@ contract TimelockTest is IntegrationTest {
}

function testAcceptTimelockTimelockNotElapsed(uint256 timelock, uint256 elapsed) public {
timelock = bound(timelock, 0, TIMELOCK - 1);
timelock = bound(timelock, ConstantsLib.MIN_TIMELOCK, TIMELOCK - 1);
elapsed = bound(elapsed, 1, TIMELOCK - 1);

vm.prank(OWNER);
Expand Down Expand Up @@ -227,7 +235,7 @@ contract TimelockTest is IntegrationTest {
}

function testAcceptGuardianTimelockDecreased(uint256 timelock, uint256 elapsed) public {
timelock = bound(timelock, 0, TIMELOCK - 1);
timelock = bound(timelock, ConstantsLib.MIN_TIMELOCK, TIMELOCK - 1);
elapsed = bound(elapsed, 1, TIMELOCK - 1);

vm.prank(OWNER);
Expand Down Expand Up @@ -384,7 +392,7 @@ contract TimelockTest is IntegrationTest {

function testAcceptCapIncreasedTimelockDecreased(uint256 cap, uint256 timelock, uint256 elapsed) public {
cap = bound(cap, CAP + 1, type(uint184).max);
timelock = bound(timelock, 0, TIMELOCK - 1);
timelock = bound(timelock, ConstantsLib.MIN_TIMELOCK, TIMELOCK - 1);
elapsed = bound(elapsed, 1, TIMELOCK - 1);

vm.prank(OWNER);
Expand Down
Loading