Skip to content

Commit 0888120

Browse files
authored
Merge pull request #839 from morpho-org/refactor/timelocks
refactor timelocks 1
2 parents f6b8f6c + 92d785d commit 0888120

5 files changed

Lines changed: 257 additions & 81 deletions

File tree

certora/confs/IdsMorphoMarketV1AdapterV2.conf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"solc": "solc-0.8.28",
77
"verify": "MorphoMarketV1AdapterV2:certora/specs/IdsMorphoMarketV1AdapterV2.spec",
88
"loop_iter": "5",
9+
"optimistic_hashing": true,
910
"optimistic_loop": true,
1011
"server": "production",
1112
"msg": "VaultV2 MorphoMarketV1AdapterV2 Ids"

src/adapters/MorphoMarketV1AdapterV2.sol

Lines changed: 92 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,12 @@ import {
2626
/// @dev Markets get removed from the marketIds when the allocation is zero, but it doesn't mean that the adapter has
2727
/// zero shares on the market.
2828
/// @dev This adapter can only be used for markets with the adaptive curve irm.
29+
/// @dev Before adding the adapter to the vault, its timelocks must be properly set.
2930
/// @dev Donated shares are lost forever.
3031
///
32+
/// TIMELOCKS
33+
/// @dev The system is the same as the one used in VaultV2. Dev comments in VaultV2.sol on timelocks also apply here.
34+
///
3135
/// BURN SHARES
3236
/// @dev When submitting burnShares, it's recommended to put the caps of the market to zero to avoid losing more.
3337
/// @dev Burning shares takes time, so reactive depositors might be able to exit before the share price reduction.
@@ -46,18 +50,25 @@ contract MorphoMarketV1AdapterV2 is IMorphoMarketV1AdapterV2 {
4650
bytes32 public immutable adapterId;
4751
address public immutable adaptiveCurveIrm;
4852

49-
/* STORAGE */
53+
/* TIMELOCKS STORAGE */
54+
55+
mapping(bytes4 selector => uint256) public timelock;
56+
mapping(bytes4 selector => bool) public abdicated;
57+
mapping(bytes data => uint256) public executableAt;
58+
59+
/* OTHER STORAGE */
5060

5161
address public skimRecipient;
5262
bytes32[] public marketIds;
5363
mapping(bytes32 marketId => uint256) public supplyShares;
54-
mapping(bytes32 marketId => uint256) public burnSharesExecutableAt;
64+
65+
/* GETTERS */
5566

5667
function marketIdsLength() external view returns (uint256) {
5768
return marketIds.length;
5869
}
5970

60-
/* FUNCTIONS */
71+
/* CONSTRUCTOR */
6172

6273
constructor(address _parentVault, address _morpho, address _adaptiveCurveIrm) {
6374
factory = msg.sender;
@@ -70,53 +81,103 @@ contract MorphoMarketV1AdapterV2 is IMorphoMarketV1AdapterV2 {
7081
SafeERC20Lib.safeApprove(asset, _parentVault, type(uint256).max);
7182
}
7283

73-
function setSkimRecipient(address newSkimRecipient) external {
74-
require(msg.sender == IVaultV2(parentVault).owner(), NotAuthorized());
75-
skimRecipient = newSkimRecipient;
76-
emit SetSkimRecipient(newSkimRecipient);
77-
}
84+
/* TIMELOCKS FUNCTIONS */
7885

79-
/// @dev Skims the adapter's balance of `token` and sends it to `skimRecipient`.
80-
/// @dev This is useful to handle rewards that the adapter has earned.
81-
function skim(address token) external {
82-
require(msg.sender == skimRecipient, NotAuthorized());
83-
uint256 balance = IERC20(token).balanceOf(address(this));
84-
SafeERC20Lib.safeTransfer(token, skimRecipient, balance);
85-
emit Skim(token, balance);
86+
/// @dev Will revert if the timelock value is type(uint256).max or any value that overflows when added to the block
87+
/// timestamp.
88+
function submit(bytes calldata data) external {
89+
require(msg.sender == IVaultV2(parentVault).curator(), Unauthorized());
90+
require(executableAt[data] == 0, DataAlreadyPending());
91+
92+
bytes4 selector = bytes4(data);
93+
uint256 _timelock = selector == IMorphoMarketV1AdapterV2.decreaseTimelock.selector
94+
? timelock[bytes4(data[4:8])]
95+
: timelock[selector];
96+
executableAt[data] = block.timestamp + _timelock;
97+
emit Submit(selector, data, executableAt[data]);
8698
}
8799

88-
function submitBurnShares(bytes32 marketId) external {
89-
require(msg.sender == IVaultV2(parentVault).curator(), NotAuthorized());
90-
require(burnSharesExecutableAt[marketId] == 0, AlreadyPending());
91-
burnSharesExecutableAt[marketId] =
92-
block.timestamp + IVaultV2(parentVault).timelock(IVaultV2.removeAdapter.selector);
93-
emit SubmitBurnShares(marketId, burnSharesExecutableAt[marketId]);
100+
function timelocked() internal {
101+
bytes4 selector = bytes4(msg.data);
102+
require(executableAt[msg.data] != 0, DataNotTimelocked());
103+
require(block.timestamp >= executableAt[msg.data], TimelockNotExpired());
104+
require(!abdicated[selector], Abdicated());
105+
executableAt[msg.data] = 0;
106+
emit Accept(selector, msg.data);
94107
}
95108

96-
function revokeBurnShares(bytes32 marketId) external {
109+
function revoke(bytes calldata data) external {
97110
require(
98111
msg.sender == IVaultV2(parentVault).curator() || IVaultV2(parentVault).isSentinel(msg.sender),
99-
NotAuthorized()
112+
Unauthorized()
100113
);
101-
require(burnSharesExecutableAt[marketId] != 0, NotPending());
102-
burnSharesExecutableAt[marketId] = 0;
103-
emit RevokeBurnShares(marketId);
114+
require(executableAt[data] != 0, DataNotTimelocked());
115+
executableAt[data] = 0;
116+
bytes4 selector = bytes4(data);
117+
emit Revoke(msg.sender, selector, data);
118+
}
119+
120+
/* CURATOR FUNCTIONS */
121+
122+
/// @dev This function requires great caution because it can irreversibly disable submit for a selector.
123+
/// @dev Existing pending operations submitted before increasing a timelock can still be executed at the initial
124+
/// executableAt.
125+
function increaseTimelock(bytes4 selector, uint256 newDuration) external {
126+
timelocked();
127+
require(selector != IMorphoMarketV1AdapterV2.decreaseTimelock.selector, AutomaticallyTimelocked());
128+
require(newDuration >= timelock[selector], TimelockNotIncreasing());
129+
130+
timelock[selector] = newDuration;
131+
emit IncreaseTimelock(selector, newDuration);
132+
}
133+
134+
function decreaseTimelock(bytes4 selector, uint256 newDuration) external {
135+
timelocked();
136+
require(selector != IMorphoMarketV1AdapterV2.decreaseTimelock.selector, AutomaticallyTimelocked());
137+
require(newDuration <= timelock[selector], TimelockNotDecreasing());
138+
139+
timelock[selector] = newDuration;
140+
emit DecreaseTimelock(selector, newDuration);
141+
}
142+
143+
/// @dev This function requires great caution because it will irreversibly disable submit for a selector.
144+
/// @dev Existing pending operations submitted before increasing a timelock can not be executed at the initial
145+
/// executableAt.
146+
function abdicate(bytes4 selector) external {
147+
timelocked();
148+
abdicated[selector] = true;
149+
emit Abdicate(selector);
150+
}
151+
152+
function setSkimRecipient(address newSkimRecipient) external {
153+
timelocked();
154+
skimRecipient = newSkimRecipient;
155+
emit SetSkimRecipient(newSkimRecipient);
104156
}
105157

106158
/// @dev Deallocate 0 from the vault after burning shares to update the allocation there.
107159
function burnShares(bytes32 marketId) external {
108-
require(burnSharesExecutableAt[marketId] != 0, NotTimelocked());
109-
require(block.timestamp >= burnSharesExecutableAt[marketId], TimelockNotExpired());
110-
burnSharesExecutableAt[marketId] = 0;
160+
timelocked();
111161
uint256 supplySharesBefore = supplyShares[marketId];
112162
supplyShares[marketId] = 0;
113163
emit BurnShares(marketId, supplySharesBefore);
114164
}
115165

166+
/* OTHER FUNCTIONS */
167+
168+
/// @dev Skims the adapter's balance of `token` and sends it to `skimRecipient`.
169+
/// @dev This is useful to handle rewards that the adapter has earned.
170+
function skim(address token) external {
171+
require(msg.sender == skimRecipient, Unauthorized());
172+
uint256 balance = IERC20(token).balanceOf(address(this));
173+
SafeERC20Lib.safeTransfer(token, skimRecipient, balance);
174+
emit Skim(token, balance);
175+
}
176+
116177
/// @dev Returns the ids of the allocation and the change in allocation.
117178
function allocate(bytes memory data, uint256 assets, bytes4, address) external returns (bytes32[] memory, int256) {
118179
MarketParams memory marketParams = abi.decode(data, (MarketParams));
119-
require(msg.sender == parentVault, NotAuthorized());
180+
require(msg.sender == parentVault, Unauthorized());
120181
require(marketParams.loanToken == asset, LoanAssetMismatch());
121182
require(marketParams.irm == adaptiveCurveIrm, IrmMismatch());
122183
bytes32 marketId = Id.unwrap(marketParams.id());
@@ -145,7 +206,7 @@ contract MorphoMarketV1AdapterV2 is IMorphoMarketV1AdapterV2 {
145206
returns (bytes32[] memory, int256)
146207
{
147208
MarketParams memory marketParams = abi.decode(data, (MarketParams));
148-
require(msg.sender == parentVault, NotAuthorized());
209+
require(msg.sender == parentVault, Unauthorized());
149210
require(marketParams.loanToken == asset, LoanAssetMismatch());
150211
require(marketParams.irm == adaptiveCurveIrm, IrmMismatch());
151212
bytes32 marketId = Id.unwrap(marketParams.id());

src/adapters/interfaces/IMorphoMarketV1AdapterV2.sol

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,31 @@ import {MarketParams} from "../../../lib/morpho-blue/src/interfaces/IMorpho.sol"
88
interface IMorphoMarketV1AdapterV2 is IAdapter {
99
/* EVENTS */
1010

11+
event Submit(bytes4 indexed selector, bytes data, uint256 executableAt);
12+
event Revoke(address indexed sender, bytes4 indexed selector, bytes data);
13+
event Accept(bytes4 indexed selector, bytes data);
14+
event Abdicate(bytes4 indexed selector);
15+
event IncreaseTimelock(bytes4 indexed selector, uint256 newDuration);
16+
event DecreaseTimelock(bytes4 indexed selector, uint256 newDuration);
1117
event SetSkimRecipient(address indexed newSkimRecipient);
1218
event Skim(address indexed token, uint256 assets);
13-
event SubmitBurnShares(bytes32 indexed marketId, uint256 executableAt);
14-
event RevokeBurnShares(bytes32 indexed marketId);
1519
event BurnShares(bytes32 indexed marketId, uint256 supplyShares);
1620
event Allocate(bytes32 indexed marketId, uint256 newAllocation, uint256 mintedShares);
1721
event Deallocate(bytes32 indexed marketId, uint256 newAllocation, uint256 burnedShares);
1822

1923
/* ERRORS */
2024

21-
error AlreadyPending();
25+
error Abdicated();
26+
error AutomaticallyTimelocked();
27+
error DataAlreadyPending();
28+
error DataNotTimelocked();
2229
error IrmMismatch();
2330
error LoanAssetMismatch();
24-
error NotAuthorized();
25-
error NotPending();
26-
error NotTimelocked();
2731
error SharePriceAboveOne();
32+
error TimelockNotDecreasing();
2833
error TimelockNotExpired();
34+
error TimelockNotIncreasing();
35+
error Unauthorized();
2936

3037
/* VIEW FUNCTIONS */
3138

@@ -38,16 +45,22 @@ interface IMorphoMarketV1AdapterV2 is IAdapter {
3845
function adapterId() external view returns (bytes32);
3946
function skimRecipient() external view returns (address);
4047
function marketIdsLength() external view returns (uint256);
48+
function adaptiveCurveIrm() external view returns (address);
4149
function allocation(MarketParams memory marketParams) external view returns (uint256);
4250
function expectedSupplyAssets(bytes32 marketId) external view returns (uint256);
43-
function burnSharesExecutableAt(bytes32 marketId) external view returns (uint256);
4451
function ids(MarketParams memory marketParams) external view returns (bytes32[] memory);
52+
function timelock(bytes4 selector) external view returns (uint256);
53+
function abdicated(bytes4 selector) external view returns (bool);
54+
function executableAt(bytes memory data) external view returns (uint256);
4555

4656
/* NON-VIEW FUNCTIONS */
4757

48-
function submitBurnShares(bytes32 marketId) external;
49-
function revokeBurnShares(bytes32 marketId) external;
50-
function burnShares(bytes32 marketId) external;
58+
function submit(bytes memory data) external;
59+
function revoke(bytes memory data) external;
60+
function increaseTimelock(bytes4 selector, uint256 newDuration) external;
61+
function decreaseTimelock(bytes4 selector, uint256 newDuration) external;
62+
function abdicate(bytes4 selector) external;
5163
function setSkimRecipient(address newSkimRecipient) external;
64+
function burnShares(bytes32 marketId) external;
5265
function skim(address token) external;
5366
}

0 commit comments

Comments
 (0)