Skip to content
2 changes: 2 additions & 0 deletions src/VaultV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ import {IReceiveSharesGate, ISendSharesGate, IReceiveAssetsGate, ISendAssetsGate
/// ).
/// @dev Nothing is checked on the timelocked data, so it could be not executable (function does not exist, argument
/// encoding is wrong, function' conditions are not met, etc.).
/// @dev Adapters might reuse the timelocks mappings for their own timelocked functions. Make sure to avoid function
/// signature clashes between their timelocked functions.
///
/// ABDICATION
/// @dev When a timelocked function is abdicated, it can't be called anymore.
Expand Down
66 changes: 31 additions & 35 deletions src/adapters/MorphoMarketV1AdapterV2.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.
/// @dev Donated shares are lost forever.
///
/// BURN SHARES
Expand All @@ -51,13 +52,12 @@ contract MorphoMarketV1AdapterV2 is IMorphoMarketV1AdapterV2 {
address public skimRecipient;
bytes32[] public marketIds;
mapping(bytes32 marketId => uint256) public supplyShares;
mapping(bytes32 marketId => uint256) public burnSharesExecutableAt;

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 +70,49 @@ 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 */

/// @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 timelocked() internal {
uint256 executableAt = IVaultV2(parentVault).executableAt(msg.data);
require(executableAt != 0, DataNotTimelocked());
require(block.timestamp >= executableAt, TimelockNotExpired());
IVaultV2(parentVault).revoke(msg.data);
emit Accept(bytes4(msg.data), msg.data);
Comment on lines 75 to 80
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid unauthorized revoke in timelocked adapter flow

Timelocked adapter calls now clear the vault timelock via IVaultV2(parentVault).revoke(msg.data) (lines 66‑70), but VaultV2.revoke only allows the curator or a sentinel to call it (see VaultV2.sol lines 362‑364). When the curator executes setSkimRecipient or burnShares after submitting them through the vault, the adapter performs this revoke as itself and will hit Unauthorized() unless the adapter was pre-added as a sentinel (not the default). That bricks all timelocked adapter actions in production unless an extra sentinel configuration step is done.

Useful? React with 👍 / 👎.

Comment on lines +78 to 80
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it also emits the Revoke event on the vault, but it is actually accepted. It could be confusing, or difficult to index (maybe that's ok because Revoke logs the sender, which is the adapter in this case)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree that's misleading. but on the other side you gain that you don't need to index the adapter's storage

}

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]);
}
/* TIMELOCKED FUNCTIONS */

function revokeBurnShares(bytes32 marketId) external {
require(
msg.sender == IVaultV2(parentVault).curator() || IVaultV2(parentVault).isSentinel(msg.sender),
NotAuthorized()
);
require(burnSharesExecutableAt[marketId] != 0, NotPending());
burnSharesExecutableAt[marketId] = 0;
emit RevokeBurnShares(marketId);
/// @dev Function name to avoid selector clash with other adapters.
function morphoMarketV1AdapterV2SetSkimRecipient(address newSkimRecipient) external {
timelocked();
skimRecipient = newSkimRecipient;
emit SetSkimRecipient(newSkimRecipient);
}

/// @dev Function name to avoid selector clash with other adapters.
/// @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;
function morphoMarketV1AdapterV2BurnShares(bytes32 marketId) external {
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 +141,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
18 changes: 7 additions & 11 deletions src/adapters/interfaces/IMorphoMarketV1AdapterV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,23 @@ 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 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 DataNotTimelocked();
error IrmMismatch();
error LoanAssetMismatch();
error NotAuthorized();
error NotPending();
error NotTimelocked();
error SharePriceAboveOne();
error TimelockNotExpired();
error Unauthorized();

/* VIEW FUNCTIONS */

Expand All @@ -40,14 +39,11 @@ interface IMorphoMarketV1AdapterV2 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 marketId) external view returns (uint256);
function ids(MarketParams memory marketParams) external view returns (bytes32[] memory);

/* NON-VIEW FUNCTIONS */

function submitBurnShares(bytes32 marketId) external;
function revokeBurnShares(bytes32 marketId) external;
function burnShares(bytes32 marketId) external;
function setSkimRecipient(address newSkimRecipient) external;
function morphoMarketV1AdapterV2SetSkimRecipient(address newSkimRecipient) external;
function morphoMarketV1AdapterV2BurnShares(bytes32 marketId) external;
function skim(address token) external;
}
Loading