Skip to content
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
988ccbb
refactor: timelocks in adapter
MathisGD Nov 28, 2025
0241ca5
feat: add abdication
MathisGD Dec 1, 2025
0422e96
verif: fix
MathisGD Dec 1, 2025
435fa50
suggestions
MathisGD Dec 1, 2025
ffd7285
fix: document timelocks
MathisGD Dec 1, 2025
da75aa4
chore:fmt
MathisGD Dec 1, 2025
9a7e251
Merge remote-tracking branch 'origin/refactor/timelocks' into refacto…
MathisGD Dec 1, 2025
19395f3
suggestions
MathisGD Dec 1, 2025
5f1163d
Update src/adapters/MorphoMarketV1Adapter.sol
MathisGD Dec 1, 2025
1a8e156
Merge branch 'refactor/timelocks' into refactor/abdicate
MathisGD Dec 1, 2025
b63b776
Merge branch 'main' into refactor/timelocks
MathisGD Dec 2, 2025
5f558d9
docs
MathisGD Dec 2, 2025
6ab7ab4
test: abdicate
MathisGD Dec 2, 2025
0f75fc5
signature clashes
MathisGD Dec 2, 2025
b97dd52
Merge remote-tracking branch 'origin/main' into refactor/timelocks
MathisGD Dec 2, 2025
90ad30e
completely internalize timelocks in market v1 adapter
adhusson Dec 2, 2025
066c499
fix: merge
MathisGD Dec 2, 2025
9fa5565
Merge branch 'refactor/timelocks' into refactor/internalize-timelocks…
adhusson Dec 2, 2025
962c88c
fix: propagate renaming
adhusson Dec 2, 2025
b047efb
Merge branch 'refactor/timelocks' into refactor/abdicate
MathisGD Dec 2, 2025
297dfc5
chore:fmt
MathisGD Dec 2, 2025
9cb61ac
Merge pull request #840 from morpho-org/refactor/abdicate
MathisGD Dec 2, 2025
8d20efe
fix: test and interfaces
adhusson Dec 2, 2025
1f4613f
test: remove useless skip
adhusson Dec 2, 2025
4d56a2d
Merge pull request #843 from morpho-org/refactor/internalize-timelock…
adhusson Dec 2, 2025
05cfacc
test: fix after merge
adhusson Dec 2, 2025
e7b072d
fix: read abdicated locally
adhusson Dec 2, 2025
e41cd69
chore: remove unused / add comments and test features
adhusson Dec 2, 2025
b492999
chore: fmt
adhusson Dec 2, 2025
9d05faa
docs: remove dead comment
adhusson Dec 2, 2025
c775d8c
style: update sections
adhusson Dec 2, 2025
1eac955
fix: apply review fixes
adhusson Dec 2, 2025
8161132
fix: apply review fixes
adhusson Dec 2, 2025
3b1395e
reorg
MathisGD Dec 2, 2025
0fdeb3c
refactor: move marketIdsLength to getters
adhusson Dec 2, 2025
09717d5
Apply suggestion from @MathisGD
MathisGD Dec 3, 2025
30b2a33
style: reorder functions
MathisGD Dec 3, 2025
973fe66
docs: document timelocks
MathisGD Dec 3, 2025
f89da02
add missing function in IMorphoMarketV1AdapterV2.sol
MathisGD Dec 3, 2025
92d785d
Apply suggestions from code review
MathisGD Dec 3, 2025
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
1 change: 1 addition & 0 deletions certora/confs/IdsMorphoMarketV1AdapterV2.conf
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"solc": "solc-0.8.28",
"verify": "MorphoMarketV1AdapterV2:certora/specs/IdsMorphoMarketV1AdapterV2.spec",
"loop_iter": "5",
"optimistic_hashing": true,
"optimistic_loop": true,
"server": "production",
"msg": "VaultV2 MorphoMarketV1AdapterV2 Ids"
Expand Down
117 changes: 86 additions & 31 deletions src/adapters/MorphoMarketV1AdapterV2.sol
Comment thread
MathisGD marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
/// @dev Markets get removed from the marketIds when the allocation is zero, but it doesn't mean that the adapter has
/// zero shares on the market.
/// @dev This adapter can only be used for markets with the adaptive curve irm.
/// @dev Before adding the adapter to the vault, its timelocks must be properly set.
/// @dev Donated shares are lost forever.
///
/// BURN SHARES
Expand All @@ -46,18 +47,25 @@ contract MorphoMarketV1AdapterV2 is IMorphoMarketV1AdapterV2 {
bytes32 public immutable adapterId;
address public immutable adaptiveCurveIrm;

/* STORAGE */
/* TIMELOCKS RELATED STORAGE */
Comment thread
MathisGD marked this conversation as resolved.
Outdated
Comment thread
MathisGD marked this conversation as resolved.
Outdated

mapping(bytes4 selector => uint256) public timelock;
mapping(bytes4 selector => bool) public abdicated;
mapping(bytes data => uint256) public executableAt;

/* OTHER STORAGE */

address public skimRecipient;
bytes32[] public marketIds;
mapping(bytes32 marketId => uint256) public supplyShares;
mapping(bytes32 marketId => uint256) public burnSharesExecutableAt;

/* GETTERS */

function marketIdsLength() external view returns (uint256) {
return marketIds.length;
}

/* FUNCTIONS */
/* CONSTRUCTOR */

constructor(address _parentVault, address _morpho, address _adaptiveCurveIrm) {
factory = msg.sender;
Expand All @@ -70,53 +78,100 @@ contract MorphoMarketV1AdapterV2 is IMorphoMarketV1AdapterV2 {
SafeERC20Lib.safeApprove(asset, _parentVault, type(uint256).max);
}

function setSkimRecipient(address newSkimRecipient) external {
require(msg.sender == IVaultV2(parentVault).owner(), NotAuthorized());
skimRecipient = newSkimRecipient;
emit SetSkimRecipient(newSkimRecipient);
}
/* TIMELOCKS FUNCTIONS */

/// @dev Skims the adapter's balance of `token` and sends it to `skimRecipient`.
/// @dev This is useful to handle rewards that the adapter has earned.
function skim(address token) external {
require(msg.sender == skimRecipient, NotAuthorized());
uint256 balance = IERC20(token).balanceOf(address(this));
SafeERC20Lib.safeTransfer(token, skimRecipient, balance);
emit Skim(token, balance);
/// @dev Will revert if the timelock value is type(uint256).max or any value that overflows when added to the block
/// timestamp.
function submit(bytes calldata data) external {
require(msg.sender == IVaultV2(parentVault).curator(), Unauthorized());
require(executableAt[data] == 0, DataAlreadyPending());

bytes4 selector = bytes4(data);
uint256 _timelock = selector == IMorphoMarketV1AdapterV2.decreaseTimelock.selector
? timelock[bytes4(data[4:8])]
: timelock[selector];
executableAt[data] = block.timestamp + _timelock;
emit Submit(selector, data, executableAt[data]);
}

function submitBurnShares(bytes32 marketId) external {
require(msg.sender == IVaultV2(parentVault).curator(), NotAuthorized());
require(burnSharesExecutableAt[marketId] == 0, AlreadyPending());
burnSharesExecutableAt[marketId] =
block.timestamp + IVaultV2(parentVault).timelock(IVaultV2.removeAdapter.selector);
emit SubmitBurnShares(marketId, burnSharesExecutableAt[marketId]);
function timelocked() internal {
Comment thread
MathisGD marked this conversation as resolved.
bytes4 selector = bytes4(msg.data);
require(executableAt[msg.data] != 0, DataNotTimelocked());
require(block.timestamp >= executableAt[msg.data], TimelockNotExpired());
require(!abdicated[selector], Abdicated());
executableAt[msg.data] = 0;
emit Accept(selector, msg.data);
}

function revokeBurnShares(bytes32 marketId) external {
function revoke(bytes calldata data) external {
require(
msg.sender == IVaultV2(parentVault).curator() || IVaultV2(parentVault).isSentinel(msg.sender),
NotAuthorized()
Unauthorized()
);
require(burnSharesExecutableAt[marketId] != 0, NotPending());
burnSharesExecutableAt[marketId] = 0;
emit RevokeBurnShares(marketId);
require(executableAt[data] != 0, DataNotTimelocked());
executableAt[data] = 0;
bytes4 selector = bytes4(data);
emit Revoke(msg.sender, selector, data);
}

/* CURATOR FUNCTIONS */
Comment thread
MathisGD marked this conversation as resolved.

/// @dev This function requires great caution because it can irreversibly disable submit for a selector.
/// @dev Existing pending operations submitted before increasing a timelock can still be executed at the initial
/// executableAt.
function increaseTimelock(bytes4 selector, uint256 newDuration) external {
timelocked();
require(selector != IMorphoMarketV1AdapterV2.decreaseTimelock.selector, AutomaticallyTimelocked());
require(newDuration >= timelock[selector], TimelockNotIncreasing());

timelock[selector] = newDuration;
emit IncreaseTimelock(selector, newDuration);
}

function decreaseTimelock(bytes4 selector, uint256 newDuration) external {
timelocked();
require(selector != IMorphoMarketV1AdapterV2.decreaseTimelock.selector, AutomaticallyTimelocked());
require(newDuration <= timelock[selector], TimelockNotDecreasing());

timelock[selector] = newDuration;
emit DecreaseTimelock(selector, newDuration);
}

function abdicate(bytes4 selector) external {
Comment thread
MathisGD marked this conversation as resolved.
timelocked();
abdicated[selector] = true;
emit Abdicate(selector);
}

function setSkimRecipient(address newSkimRecipient) external {
timelocked();
Comment thread
MathisGD marked this conversation as resolved.
skimRecipient = newSkimRecipient;
emit SetSkimRecipient(newSkimRecipient);
}

/// @dev Deallocate 0 from the vault after burning shares to update the allocation there.
function burnShares(bytes32 marketId) external {
require(burnSharesExecutableAt[marketId] != 0, NotTimelocked());
require(block.timestamp >= burnSharesExecutableAt[marketId], TimelockNotExpired());
burnSharesExecutableAt[marketId] = 0;
timelocked();
uint256 supplySharesBefore = supplyShares[marketId];
supplyShares[marketId] = 0;
emit BurnShares(marketId, supplySharesBefore);
}

/* OTHER FUNCTIONS */

/// @dev Skims the adapter's balance of `token` and sends it to `skimRecipient`.
/// @dev This is useful to handle rewards that the adapter has earned.
function skim(address token) external {
require(msg.sender == skimRecipient, Unauthorized());
uint256 balance = IERC20(token).balanceOf(address(this));
SafeERC20Lib.safeTransfer(token, skimRecipient, balance);
emit Skim(token, balance);
}

/// @dev Returns the ids of the allocation and the change in allocation.
function allocate(bytes memory data, uint256 assets, bytes4, address) external returns (bytes32[] memory, int256) {
MarketParams memory marketParams = abi.decode(data, (MarketParams));
require(msg.sender == parentVault, NotAuthorized());
require(msg.sender == parentVault, Unauthorized());
require(marketParams.loanToken == asset, LoanAssetMismatch());
require(marketParams.irm == adaptiveCurveIrm, IrmMismatch());
bytes32 marketId = Id.unwrap(marketParams.id());
Expand Down Expand Up @@ -145,7 +200,7 @@ contract MorphoMarketV1AdapterV2 is IMorphoMarketV1AdapterV2 {
returns (bytes32[] memory, int256)
{
MarketParams memory marketParams = abi.decode(data, (MarketParams));
require(msg.sender == parentVault, NotAuthorized());
require(msg.sender == parentVault, Unauthorized());
require(marketParams.loanToken == asset, LoanAssetMismatch());
require(marketParams.irm == adaptiveCurveIrm, IrmMismatch());
bytes32 marketId = Id.unwrap(marketParams.id());
Expand Down
32 changes: 22 additions & 10 deletions src/adapters/interfaces/IMorphoMarketV1AdapterV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,31 @@ import {MarketParams} from "../../../lib/morpho-blue/src/interfaces/IMorpho.sol"
interface IMorphoMarketV1AdapterV2 is IAdapter {
/* EVENTS */

event Submit(bytes4 indexed selector, bytes data, uint256 executableAt);
event Revoke(address indexed sender, bytes4 indexed selector, bytes data);
event Accept(bytes4 indexed selector, bytes data);
event Abdicate(bytes4 indexed selector);
event IncreaseTimelock(bytes4 indexed selector, uint256 newDuration);
event DecreaseTimelock(bytes4 indexed selector, uint256 newDuration);
event SetSkimRecipient(address indexed newSkimRecipient);
event Skim(address indexed token, uint256 assets);
event SubmitBurnShares(bytes32 indexed marketId, uint256 executableAt);
event RevokeBurnShares(bytes32 indexed marketId);
event BurnShares(bytes32 indexed marketId, uint256 supplyShares);
event Allocate(bytes32 indexed marketId, uint256 newAllocation, uint256 mintedShares);
event Deallocate(bytes32 indexed marketId, uint256 newAllocation, uint256 burnedShares);

/* ERRORS */

error AlreadyPending();
error Abdicated();
error AutomaticallyTimelocked();
error DataAlreadyPending();
error DataNotTimelocked();
error IrmMismatch();
error LoanAssetMismatch();
error NotAuthorized();
error NotPending();
error NotTimelocked();
error SharePriceAboveOne();
error TimelockNotDecreasing();
error TimelockNotExpired();
error TimelockNotIncreasing();
error Unauthorized();

/* VIEW FUNCTIONS */

Expand All @@ -40,14 +47,19 @@ interface IMorphoMarketV1AdapterV2 is IAdapter {
function marketIdsLength() external view returns (uint256);
Comment thread
MathisGD marked this conversation as resolved.
function allocation(MarketParams memory marketParams) external view returns (uint256);
function expectedSupplyAssets(bytes32 marketId) external view returns (uint256);
function burnSharesExecutableAt(bytes32 marketId) external view returns (uint256);
function ids(MarketParams memory marketParams) external view returns (bytes32[] memory);
function timelock(bytes4 selector) external view returns (uint256);
function abdicated(bytes4 selector) external view returns (bool);
function executableAt(bytes memory data) external view returns (uint256);

/* NON-VIEW FUNCTIONS */

function submitBurnShares(bytes32 marketId) external;
function revokeBurnShares(bytes32 marketId) external;
function burnShares(bytes32 marketId) external;
function submit(bytes memory data) external;
function revoke(bytes memory data) external;
function increaseTimelock(bytes4 selector, uint256 newDuration) external;
function decreaseTimelock(bytes4 selector, uint256 newDuration) external;
function abdicate(bytes4 selector) external;
function setSkimRecipient(address newSkimRecipient) external;
function burnShares(bytes32 marketId) external;
function skim(address token) external;
}
Loading