Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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/IdsMorphoMarketV1Adapter.conf
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"solc": "solc-0.8.28",
"verify": "MorphoMarketV1Adapter:certora/specs/IdsMorphoMarketV1Adapter.spec",
"loop_iter": "5",
"optimistic_hashing": true,
"optimistic_loop": true,
"server": "production",
"msg": "VaultV2 MorphoMarketV1Adapter Ids"
Expand Down
66 changes: 36 additions & 30 deletions src/adapters/MorphoMarketV1Adapter.sol
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.
contract MorphoMarketV1Adapter is IMorphoMarketV1Adapter {
using MarketParamsLib for MarketParams;
using SharesMathLib for uint256;
Expand All @@ -44,7 +45,7 @@ contract MorphoMarketV1Adapter is IMorphoMarketV1Adapter {
address public skimRecipient;
bytes32[] public marketIds;
mapping(bytes32 marketId => uint256) public supplyShares;
mapping(bytes32 marketId => uint256) public burnSharesExecutableAt;
mapping(bytes data => uint256) public executableAt;

function marketIdsLength() external view returns (uint256) {
return marketIds.length;
Expand All @@ -63,54 +64,59 @@ contract MorphoMarketV1Adapter is IMorphoMarketV1Adapter {
SafeERC20Lib.safeApprove(asset, _parentVault, type(uint256).max);
}

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

/// @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);
function submit(bytes calldata data) external {
require(msg.sender == IVaultV2(parentVault).curator(), Unauthorized());
require(executableAt[data] == 0, AlreadyPending());
bytes4 selector = bytes4(data);
executableAt[data] = block.timestamp + IVaultV2(parentVault).timelock(selector);
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 {
require(executableAt[msg.data] != 0, DataNotTimelocked());
require(block.timestamp >= executableAt[msg.data], TimelockNotExpired());
executableAt[msg.data] = 0;
emit Accept(bytes4(msg.data), 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;
emit Revoke(msg.sender, bytes4(data), data);
}

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

/// @dev Deallocate 0 from the vault after burning shares to update the allocation there.
function burnShares(bytes32 marketId) external {
Comment thread
MathisGD marked this conversation as resolved.
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);
}

/// @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 Does not log anything because the ids (logged in the parent vault) are enough.
/// @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 @@ -140,7 +146,7 @@ contract MorphoMarketV1Adapter is IMorphoMarketV1Adapter {
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
18 changes: 9 additions & 9 deletions src/adapters/interfaces/IMorphoMarketV1Adapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,24 @@ import {MarketParams} from "../../../lib/morpho-blue/src/interfaces/IMorpho.sol"
interface IMorphoMarketV1Adapter 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 SetSkimRecipient(address indexed newSkimRecipient);
event Skim(address indexed token, uint256 assets);
event SubmitBurnShares(bytes32 indexed id, uint256 executableAt);
event RevokeBurnShares(bytes32 indexed id);
event BurnShares(bytes32 indexed id, uint256 supplyShares);
event Allocate(bytes32 indexed marketId, uint256 newAllocation, uint256 mintedShares);
event Deallocate(bytes32 indexed marketId, uint256 newAllocation, uint256 burnedShares);

/* ERRORS */

error AlreadyPending();
error DataNotTimelocked();
error IrmMismatch();
error LoanAssetMismatch();
error NotAuthorized();
error NotPending();
error NotTimelocked();
error SharePriceAboveOne();
error TimelockNotExpired();
error Unauthorized();

/* VIEW FUNCTIONS */

Expand All @@ -40,14 +40,14 @@ interface IMorphoMarketV1Adapter is IAdapter {
function marketIdsLength() external view returns (uint256);
function allocation(MarketParams memory marketParams) external view returns (uint256);
function expectedSupplyAssets(bytes32 marketId) external view returns (uint256);
function burnSharesExecutableAt(bytes32 id) external view returns (uint256);
function ids(MarketParams memory marketParams) external view returns (bytes32[] memory);
function executableAt(bytes memory data) external view returns (uint256);

/* NON-VIEW FUNCTIONS */

function submitBurnShares(bytes32 id) external;
function revokeBurnShares(bytes32 id) external;
function burnShares(bytes32 id) external;
function submit(bytes memory data) external;
function revoke(bytes memory data) external;
function setSkimRecipient(address newSkimRecipient) external;
function burnShares(bytes32 marketId) external;
function skim(address token) external;
}
Loading