Skip to content
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
150 changes: 150 additions & 0 deletions solidity/src/libraries/MorphoVaultV1PositionManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.28;

import {Library} from "./Library.sol";
import {BaseAccount} from "../accounts/BaseAccount.sol";
import {IERC20} from "forge-std/src/interfaces/IERC20.sol";
import {IERC4626} from "forge-std/src/interfaces/IERC4626.sol";
import {IMetaMorphoV1_1} from "./interfaces/morpho/IMetaMorphoV1_1.sol";

/**
* @title MorphoVaultV1PositionManager
* @dev Contract for managing Morpho Vault V1.1 through deposit and withdraw operations.
* It leverages BaseAccount contract to interact with the Morpho Vault V1.1 protocol, enabling automated position management.
*/
contract MorphoVaultV1PositionManager is Library {
/**
* @title MorphoVaultV1PositionManagerConfig
* @notice Configuration struct for Morpho Vault V1.1 Position Manager
* @dev Used to define parameters for interacting with Morpho Vault V1.1 protocol
* @param inputAccount The Base Account from which transactions will be initiated
* @param outputAccount The Base Account that will receive withdrawals
* @param vaultAddress Address of the Morpho Vault V1
* @param assetAddress Address of the underlying asset to manage
*/
struct MorphoVaultV1PositionManagerConfig {
BaseAccount inputAccount;
BaseAccount outputAccount;
address vaultAddress;
address assetAddress;
}

/// @notice Holds the current configuration for the MorphoVaultV1PositionManager.
MorphoVaultV1PositionManagerConfig public config;

/**
* @dev Constructor initializes the contract with the owner, processor, and initial configuration.
* @param _owner Address of the contract owner.
* @param _processor Address of the processor that can execute functions.
* @param _config Encoded configuration parameters for the MorphoVaultV1PositionManager.
*/
constructor(address _owner, address _processor, bytes memory _config) Library(_owner, _processor, _config) {}

/**
* @notice Deposits assets to MorphoVaultV1.
* @param amount The amount to deposit (0 for all available balance).
*/
function deposit(uint256 amount) external onlyProcessor {
MorphoVaultV1PositionManagerConfig memory storedConfig = config;
IERC20 asset = IERC20(storedConfig.assetAddress);

uint256 depositAmount = amount;
if (amount == 0) {
depositAmount = asset.balanceOf(address(storedConfig.inputAccount));
}

require(depositAmount > 0, "No assets to deposit");

//Approve the MorphoVaultV1 to spend the base asset from the input account
bytes memory encodedApproveCall = abi.encodeCall(IERC20.approve, (storedConfig.vaultAddress, depositAmount));

storedConfig.inputAccount.execute(address(asset), 0, encodedApproveCall);
Comment on lines +58 to +61

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The use of IERC20.approve can be unsafe for certain tokens (like USDT) that don't revert when changing a non-zero allowance. This can lead to a race condition. It's highly recommended to use a safe approval mechanism. Since you are encoding the call, you can't directly use OpenZeppelin's SafeERC20 library.

A common pattern to mitigate this is to first approve zero, and then the desired amount. However, this would require two separate execute calls, increasing gas costs.

Consider if the BaseAccount can be extended with a safeApprove function or a multicall capability. A simpler, though less flexible, approach would be to approve type(uint256).max once during configuration and remove the repeated approve calls from the deposit function.

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.

the inputAccount is meant to be interacting with only this library hence we don't expect a non-zero approval before or after this function call.


// Deposit the base asset to the MorphoVaultV1
bytes memory encodedDepositCall =
abi.encodeCall(IERC4626.deposit, (depositAmount, address(storedConfig.inputAccount)));

storedConfig.inputAccount.execute(storedConfig.vaultAddress, 0, encodedDepositCall);
}

/**
* @notice Withdraws assets from MorphoVaultV1 position.
* @param amount The amount to withdraw (0 to withdraw the entire balance).
*/
function withdraw(uint256 amount) external onlyProcessor {
MorphoVaultV1PositionManagerConfig memory storedConfig = config;
IMetaMorphoV1_1 vault = IMetaMorphoV1_1(storedConfig.vaultAddress);

uint256 withdrawAmount = amount;
if (amount == 0) {
withdrawAmount = vault.maxWithdraw(address(storedConfig.inputAccount));
}

require(withdrawAmount > 0, "No vault tokens to withdraw");

// Withdraw from MorphoVaultV1 to output account
bytes memory encodedWithdrawCall = abi.encodeCall(
IERC4626.withdraw, (withdrawAmount, address(storedConfig.outputAccount), address(storedConfig.inputAccount))
);

storedConfig.inputAccount.execute(storedConfig.vaultAddress, 0, encodedWithdrawCall);
}

/**
* @dev Internal initialization function called during construction
* @param _config New configuration
*/
function _initConfig(bytes memory _config) internal override {
config = validateConfig(_config);
}

/**
* @dev Updates the MorphoVaultV1PositionManager configuration.
* Only the contract owner is authorized to call this function.
* @param _config New encoded configuration parameters.
*/
function updateConfig(bytes memory _config) public override onlyOwner {
// Validate and update the configuration.
config = validateConfig(_config);
}

/**
* @notice Validates the provided configuration parameters
* @dev Checks for validity of input account, output account, base asset, and market proxy address
* @param _config The encoded configuration bytes to validate
* @return MorphoVaultV1PositionManagerConfig A validated configuration struct
*/
function validateConfig(bytes memory _config) internal view returns (MorphoVaultV1PositionManagerConfig memory) {
// Decode the configuration bytes into the MorphoVaultV1PositionManagerConfig struct.
MorphoVaultV1PositionManagerConfig memory decodedConfig =
abi.decode(_config, (MorphoVaultV1PositionManagerConfig));

// Ensure the Vault address is valid (non-zero).
if (decodedConfig.vaultAddress == address(0)) {
revert("Vault address can't be zero address");
}
Comment on lines +123 to +125

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

For gas efficiency and better error handling, it's recommended to use custom errors instead of revert strings. This applies to all require and revert statements in the contract.

For example:

error VaultAddressIsZero();

// ...

if (decodedConfig.vaultAddress == address(0)) {
    revert VaultAddressIsZero();
}

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.

We follow the string practice as it is easy to debug and these functions are meant to be used in rare conditions.


// Ensure the input account address is valid (non-zero).
if (decodedConfig.inputAccount == BaseAccount(payable(address(0)))) {
revert("Input account can't be zero address");
}

// Ensure the output account address is valid (non-zero).
if (decodedConfig.outputAccount == BaseAccount(payable(address(0)))) {
revert("Output account can't be zero address");
}

// Ensure the asset address is the same as the asset address of the vault
if (decodedConfig.assetAddress != IMetaMorphoV1_1(decodedConfig.vaultAddress).asset()) {
revert("Vault asset and given asset are not same");
}

return decodedConfig;
}

function balance() external view returns (uint256) {
return IMetaMorphoV1_1(config.vaultAddress).previewRedeem(
IMetaMorphoV1_1(config.vaultAddress).balanceOf(address(config.inputAccount))
);
}
}
Loading
Loading