diff --git a/script/deploy.s.sol b/script/deploy.s.sol index d60cc2f..c275be1 100644 --- a/script/deploy.s.sol +++ b/script/deploy.s.sol @@ -16,8 +16,9 @@ contract DeployScript is Script { uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); vm.startBroadcast(deployerPrivateKey); - Strategy strategy = new Strategy(); - Engine engine = new Engine(address(strategy)); + Engine engine = new Engine(); + Strategy strategy = new Strategy(address(engine)); + Oracle oracle = new Oracle(); MoonwellConnector mwConnector = new MoonwellConnector( diff --git a/src/BaseConnector.sol b/src/BaseConnector.sol index ea04b4a..d41c574 100644 --- a/src/BaseConnector.sol +++ b/src/BaseConnector.sol @@ -42,20 +42,28 @@ abstract contract BaseConnector is IConnector { /** * @notice Executes a function call on the connected connector - * @dev This function must be implemented by derived contracts - * @param actionType The Core actions that a connector can perform - * @param data The calldata for the function call containing the parameters - * @return amountOut The amount out from the function call */ function execute( ActionType actionType, address[] memory assetsIn, uint256[] memory amounts, address assetOut, + uint256 stepIndex, uint256 amountRatio, - uint256 prevLoopAmountOut, bytes32 strategyId, address userAddress, bytes calldata data - ) external payable virtual returns (uint256 amountOut); + ) + external + payable + virtual + returns ( + address protocol, + address[] memory assets, + uint256[] memory assetsAmount, + address shareToken, + uint256 shareAmount, + address[] memory underlyingTokens, + uint256[] memory underlyingAmounts + ); } diff --git a/src/curators/engine.sol b/src/curators/engine.sol index a0e0257..20d3bd9 100644 --- a/src/curators/engine.sol +++ b/src/curators/engine.sol @@ -5,9 +5,6 @@ import "./interface/IStrategy.sol"; import {ERC4626, ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; contract Engine is ERC4626 { - // might change later - ILiquidStrategy strategyModule; - /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* EVENTS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ @@ -21,65 +18,98 @@ contract Engine is ERC4626 { */ event Join(bytes32 indexed strategyId, address indexed depositor, address[] tokenAddress, uint256[] amount); - constructor(address _strategyModule) ERC4626(IERC20(address(this))) ERC20("LIQUID", "LLP") { - strategyModule = ILiquidStrategy(_strategyModule); - } + /** + * @dev Emitted when a user joins a strategy + * @param strategyId unique identifier for the strategy + * @param user address of the user exiting the strategy + */ + event Exit(bytes32 indexed strategyId, address indexed user); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* ERROR */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + error InvalidActionType(); + + constructor() ERC4626(IERC20(address(this))) ERC20("LIQUID", "LLP") {} + + function join(bytes32 _strategyId, address _strategyModule, uint256[] memory _amounts) public { + // restrict to join a strategy once - function join(bytes32 _strategyId, uint256[] memory amounts) public { // Fetch the strategy - ILiquidStrategy.Strategy memory _strategy = strategyModule.getStrategy(_strategyId); + ILiquidStrategy.Strategy memory strategy = ILiquidStrategy(_strategyModule).getStrategy(_strategyId); // Validate strategy - not necessary single we validate the steps before strategy creation // Transfer initial deposit(s) from caller - uint256 initialAssetsInLength = _strategy.steps[0].assetsIn.length; + uint256 initialAssetsInLength = strategy.steps[0].assetsIn.length; for (uint256 i; i < initialAssetsInLength; i++) { - address asset = _strategy.steps[0].assetsIn[i]; + address asset = strategy.steps[0].assetsIn[i]; // approve `this` as spender in client first - ERC4626(asset).transferFrom(msg.sender, address(this), amounts[i]); + ERC4626(asset).transferFrom(msg.sender, address(this), _amounts[i]); // tranfer token to connector - ERC4626(asset).transfer(_strategy.steps[0].connector, amounts[i]); + ERC4626(asset).transfer(strategy.steps[0].connector, _amounts[i]); } - uint256 prevLoopAmountOut; + uint256[] memory prevAmounts = _amounts; + // Execute all steps atomically - for (uint256 i; i < _strategy.steps.length; i++) { + for (uint256 i; i < strategy.steps.length; i++) { // Fetch step - ILiquidStrategy.Step memory _step = _strategy.steps[i]; + ILiquidStrategy.Step memory step = strategy.steps[i]; // Default ratio to 100% for first step - uint256 _amountRatio = i == 0 ? 10_000 : _step.amountRatio; + uint256 amountRatio = i == 0 ? 10_000 : step.amountRatio; // Constrain the first step to certain actions if ( i == 0 && ( - (_step.actionType == IConnector.ActionType.BORROW) - || (_step.actionType == IConnector.ActionType.UNSTAKE) - || (_step.actionType == IConnector.ActionType.WITHDRAW) - || (_step.actionType == IConnector.ActionType.REPAY) + (step.actionType == IConnector.ActionType.BORROW) + || (step.actionType == IConnector.ActionType.UNSTAKE) + || (step.actionType == IConnector.ActionType.WITHDRAW) + || (step.actionType == IConnector.ActionType.REPAY) ) ) revert(); // Execute connector action - try IConnector(_step.connector).execute( - _step.actionType, - _step.assetsIn, - amounts, - _step.assetOut, - _amountRatio, - prevLoopAmountOut, + try IConnector(step.connector).execute( + step.actionType, + step.assetsIn, + prevAmounts, + step.assetOut, + type(uint256).max, + amountRatio, _strategyId, msg.sender, - _step.data - ) returns (uint256 amountOut) { + step.data + ) returns ( + address protocol, + address[] memory assets, + uint256[] memory assetsAmount, + address shareToken, + uint256 shareAmount, + address[] memory underlyingTokens, + uint256[] memory underlyingAmounts + ) { // Verify result - // require(verifyResult(amountOut, _step.assetOut, _step.connector), "Invalid result"); - - prevLoopAmountOut = amountOut; - - // Update the strategy module + require(_verifyResult(shareAmount, step.assetOut, _strategyModule), "Invalid shareAmount"); + + // update user info + ILiquidStrategy(_strategyModule).updateUserStats( + _strategyId, + msg.sender, + protocol, + assets, + assetsAmount, + shareToken, + shareAmount, + underlyingTokens, + underlyingAmounts + ); + + prevAmounts = assetsAmount; } catch Error(string memory reason) { revert(string(abi.encodePacked("Step ", i, " failed: ", reason))); } @@ -89,14 +119,81 @@ contract Engine is ERC4626 { uint256 _fee = 0; // update strategy stats - strategyModule.updateStrategyStats(_strategyId, amounts, _fee); + ILiquidStrategy(_strategyModule).updateStrategyStats(_strategyId, strategy.steps[0].assetsIn, _amounts, _fee); + + // update user's strategy array + ILiquidStrategy(_strategyModule).updateUserStrategy(_strategyId, msg.sender, 0); // Emits Join event - emit Join(_strategyId, msg.sender, _strategy.steps[0].assetsIn, amounts); + emit Join(_strategyId, msg.sender, strategy.steps[0].assetsIn, _amounts); + } + + function exit(bytes32 _strategyId, address _strategyModule) public { + // check and burn user's liquid share token (also prevent re-enterancy) + + // Fetch the strategy + ILiquidStrategy.Step[] memory steps = ILiquidStrategy(_strategyModule).getStrategy(_strategyId).steps; + + // Execute all steps in reverse atomically + for (uint256 i = steps.length; i > 0; i--) { + // Fetch step + ILiquidStrategy.Step memory step = steps[i - 1]; + + address[] memory assetsIn; + address assetOut; + // Flip action type (unsure if repay and withdraw would be part of strategy steps) + IConnector.ActionType actionType; + if (step.actionType == IConnector.ActionType.SUPPLY) { + actionType = IConnector.ActionType.WITHDRAW; + + // asset in + assetsIn = new address[](1); + assetsIn[0] = step.assetOut; + + // asset out + assetOut = step.assetsIn[0]; + } else if (step.actionType == IConnector.ActionType.BORROW) { + actionType = IConnector.ActionType.REPAY; + + // asset in + assetsIn = new address[](3); + assetsIn[0] = step.assetOut; + assetsIn[1] = step.assetsIn[1]; + assetsIn[2] = step.assetsIn[2]; + } else { + revert InvalidActionType(); + } + + // Execute connector action + try IConnector(step.connector).execute( + actionType, assetsIn, new uint256[](0), assetOut, i - 1, 0, _strategyId, msg.sender, step.data + ) returns ( + address protocol, + address[] memory assets, + uint256[] memory assetsAmount, + address shareToken, + uint256 shareAmount, + address[] memory underlyingTokens, + uint256[] memory underlyingAmounts + ) { + // Some checks here + } catch Error(string memory reason) { + revert(string(abi.encodePacked("Step ", i, " failed: ", reason))); + } + } + + // update user strategy stats + ILiquidStrategy(_strategyModule).updateUserStrategy(_strategyId, msg.sender, 1); + + // Emits Exit event + emit Exit(_strategyId, msg.sender); } - function verifyResult(uint256 _amountOut, address _assetOut, address _connector) internal view returns (bool) { - // return ERC4626(_assetOut).balanceOf(address(this)) == _amountOut; - return ERC4626(_assetOut).balanceOf(_connector) == _amountOut; + function _verifyResult(uint256 _shareAmount, address _assetOut, address _strategyModule) + internal + view + returns (bool) + { + return ERC4626(_assetOut).balanceOf(_strategyModule) == _shareAmount; } } diff --git a/src/curators/interface/IEngine.sol b/src/curators/interface/IEngine.sol index 43e90bc..dc6195e 100644 --- a/src/curators/interface/IEngine.sol +++ b/src/curators/interface/IEngine.sol @@ -1,4 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.13; -interface IEngine {} +interface IEngine { + function join(bytes32 _strategyId, address _strategyModule, uint256[] memory _amounts) external; +} diff --git a/src/curators/interface/IStrategy.sol b/src/curators/interface/IStrategy.sol index a321fa1..a2d6eb3 100644 --- a/src/curators/interface/IStrategy.sol +++ b/src/curators/interface/IStrategy.sol @@ -19,22 +19,18 @@ interface ILiquidStrategy is IConnector { string strategyDescription; // Strategy description Step[] steps; // Execution steps uint256 minDeposit; // Minimum deposit - uint256 maxTVL; // Maximum TVL - uint256 performanceFee; // Curator fee (basis points) } struct StrategyStats { - uint256[] totalDeposits; // Total amount deposited (total tvl) + mapping(address => uint256) totalDeposits; // Total amount deposited (total tvl) uint256 totalUsers; // Total unique users uint256 totalFeeGenerated; // Total fees generated uint256 lastUpdated; // Last stats update timestamp } struct AssetBalance { - address asset; // Token address - uint256 amount; // Raw token amount - uint256 usdValue; // USD value at last update - uint256 lastUpdated; // Last balance update timestamp + address[] assets; // Token address + uint256[] amounts; // Raw token amount } struct ShareBalance { @@ -43,18 +39,13 @@ interface ILiquidStrategy is IConnector { uint256 shareAmount; // Amount of Share tokens address[] underlyingTokens; // Underlying token addresses uint256[] underlyingAmounts; // Amounts of underlying tokens - uint256 lastUpdated; // Last balance update timestamp } struct UserStats { // Basic stats - uint256 initialDeposit; // User's initial deposit in USD - uint256 totalDepositedUSD; // Total amount deposited in USD - uint256 totalWithdrawnUSD; // Total amount withdrawn in USD - uint256 totalReward; // Total Reward generated in USD - uint256 feesPaid; // Total fees paid in USD uint256 joinTimestamp; // When user joined uint256 lastActionTimestamp; // Last action timestamp + bool isActive; // Detailed balance tracking AssetBalance[] tokenBalances; // Individual token balances ShareBalance[] shareBalances; // Protocol-specific share balances (LP tokens etc) @@ -66,16 +57,12 @@ interface ILiquidStrategy is IConnector { * @param _strategyDescription human-readable description for the strategy * @param _steps array representing the individual steps involved in the strategy * @param _minDeposit minimum amount of liquidity a user must provide to participate in the strategy - * @param _maxTVL maximum total value of liquidity allowed in the strategy - * @param _performanceFee fee charged on the strategy */ function createStrategy( string memory _name, string memory _strategyDescription, Step[] memory _steps, - uint256 _minDeposit, - uint256 _maxTVL, - uint256 _performanceFee + uint256 _minDeposit ) external; function transferToken(address _token, uint256 _amount) external returns (bool); @@ -83,17 +70,23 @@ interface ILiquidStrategy is IConnector { function updateUserStats( bytes32 _strategyId, address _userAddress, - address _asset, address _protocol, + address[] memory _assets, + uint256[] memory _assetsAmount, address _shareToken, - address[] memory _underlyingTokens, - uint256 _assetAmount, - uint256 _amountInUsd, uint256 _shareAmount, + address[] memory _underlyingTokens, uint256[] memory _underlyingAmounts ) external; - function updateStrategyStats(bytes32 strategyId, uint256[] memory amounts, uint256 performanceFee) external; + function updateStrategyStats( + bytes32 strategyId, + address[] memory assetIn, + uint256[] memory amounts, + uint256 performanceFee + ) external; + + function updateUserStrategy(bytes32 _strategyId, address _user, uint256 _indicator) external; /** * @dev Get strategy by strategy id @@ -111,7 +104,7 @@ interface ILiquidStrategy is IConnector { * @dev Get data on a particular strategy * @param _strategyId ID of a strategy */ - function getStrategyStats(bytes32 _strategyId) external view returns (StrategyStats memory); + // function getStrategyStats(bytes32 _strategyId) external view returns (StrategyStats memory); /** * @dev Get all strategies @@ -136,10 +129,11 @@ interface ILiquidStrategy is IConnector { * @dev Get user's balance for a specific asset in a strategy * @param _strategyId ID of the strategy * @param _user Address of the user - * @param _asset Address of the token to check balance for + * @param _assets Address of the token to check balance for + * @param _stepIndex Index of a step * @return AssetBalance struct containing token balance details */ - function getUserAssetBalance(bytes32 _strategyId, address _user, address _asset) + function getUserAssetBalance(bytes32 _strategyId, address _user, address[] memory _assets, uint256 _stepIndex) external view returns (AssetBalance memory); @@ -150,10 +144,14 @@ interface ILiquidStrategy is IConnector { * @param _user Address of the user * @param _protocol Address of the protocol (e.g. Aerodrome) * @param _shareToken Address of the LP token + * @param _stepIndex Index of a step * @return ShareBalance struct containing share balance details */ - function getUserShareBalance(bytes32 _strategyId, address _user, address _protocol, address _shareToken) - external - view - returns (ShareBalance memory); + function getUserShareBalance( + bytes32 _strategyId, + address _user, + address _protocol, + address _shareToken, + uint256 _stepIndex + ) external view returns (ShareBalance memory); } diff --git a/src/curators/strategy.sol b/src/curators/strategy.sol index 359bbb5..ec5726f 100644 --- a/src/curators/strategy.sol +++ b/src/curators/strategy.sol @@ -1,27 +1,39 @@ // SPDX-License-Identifier: GNU pragma solidity ^0.8.20; +import "@openzeppelin/contracts/access/Ownable2Step.sol"; + import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "./interface/IStrategy.sol"; +import "./interface/IEngine.sol"; + +contract Strategy is Ownable2Step { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STATE VARIABLES */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + IEngine engine; -contract Strategy { // curator => array of strategies mapping(address => ILiquidStrategy.Strategy[]) curatorStrategies; // strategy ID => strategies - mapping(bytes32 => ILiquidStrategy.Strategy) public strategies; + mapping(bytes32 => ILiquidStrategy.Strategy) strategies; // strategyId => stats - mapping(bytes32 => ILiquidStrategy.StrategyStats) public strategyStats; + mapping(bytes32 => ILiquidStrategy.StrategyStats) strategyStats; // strategyId => user => userStats - mapping(bytes32 => mapping(address => ILiquidStrategy.UserStats)) public userStats; + mapping(bytes32 => mapping(address => ILiquidStrategy.UserStats)) userStats; // user => strategyIds - mapping(address => bytes32[]) public userStrategies; + mapping(address => bytes32[]) userStrategies; + + // connector => true/false + mapping(address => bool) public approveConnector; // Array to keep track of all strategy IDs - bytes32[] public allStrategyIds; + bytes32[] allStrategyIds; /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* EVENTS */ @@ -35,8 +47,6 @@ contract Strategy { * @param strategyDescription human-readable description for the strategy * @param steps array representing the individual steps involved in the strategy * @param minDeposit minimum amount of liquidity a user must provide to participate in the strategy - * @param maxTVL maximum total value of liquidity allowed in the strategy - * @param performanceFee fee charged on the strategy */ event CreateStrategy( bytes32 indexed strategyId, @@ -44,9 +54,7 @@ contract Strategy { string indexed name, string strategyDescription, ILiquidStrategy.Step[] steps, - uint256 minDeposit, - uint256 maxTVL, - uint256 performanceFee + uint256 minDeposit ); /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ @@ -57,19 +65,23 @@ contract Strategy { error StrategyAlreadyExists(bytes32 strategyId); error Unauthorized(address caller); + constructor(address _engine) Ownable(msg.sender) { + engine = IEngine(_engine); + } + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* MODIFIERS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - // modifier onlyConnectors() { - // require(msg.sender == connectors, "Not connectors"); - // _; - // } + modifier onlyConnector() { + require(approveConnector[msg.sender], "caller is not a connector"); + _; + } - // modifier onlyEngine() { - // require(msg.sender == engine, "Not engine"); - // _; - // } + modifier onlyEngine() { + require(msg.sender == address(engine), "caller is not the execution engine"); + _; + } /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* PUBLIC FUNCTIONS */ @@ -81,60 +93,48 @@ contract Strategy { * @param _strategyDescription human-readable description for the strategy * @param _steps array representing the individual steps involved in the strategy * @param _minDeposit minimum amount of liquidity a user must provide to participate in the strategy - * @param _maxTVL maximum total value of liquidity allowed in the strategy - * @param _performanceFee fee charged on the strategy */ function createStrategy( string memory _name, string memory _strategyDescription, ILiquidStrategy.Step[] memory _steps, - uint256 _minDeposit, - uint256 _maxTVL, - uint256 _performanceFee + uint256 _minDeposit ) public { - bytes32 _strategyId = keccak256(abi.encodePacked(msg.sender, _name, _strategyDescription)); + bytes32 strategyId = keccak256(abi.encodePacked(msg.sender, _name, _strategyDescription)); // Check if strategy already exists - if (strategies[_strategyId].curator != address(0)) { - revert StrategyAlreadyExists(_strategyId); + if (strategies[strategyId].curator != address(0)) { + revert StrategyAlreadyExists(strategyId); } ILiquidStrategy.Strategy memory _strategy = ILiquidStrategy.Strategy({ - strategyId: _strategyId, + strategyId: strategyId, curator: msg.sender, name: _name, strategyDescription: _strategyDescription, steps: _steps, - minDeposit: _minDeposit, - maxTVL: _maxTVL, - performanceFee: _performanceFee + minDeposit: _minDeposit }); // Validate curator's strategy steps require(_validateSteps(_steps), "Invalid steps"); // Store strategy in all relevant mappings - strategies[_strategyId] = _strategy; + strategies[strategyId] = _strategy; // Store curator's strategy curatorStrategies[msg.sender].push(_strategy); // Add to array of all strategy IDs - allStrategyIds.push(_strategyId); + allStrategyIds.push(strategyId); - uint256[] memory _totalDeposits = new uint256[](_steps[0].assetsIn.length); // Initialize strategy stats - strategyStats[_strategyId] = ILiquidStrategy.StrategyStats({ - totalDeposits: _totalDeposits, - totalUsers: 0, - totalFeeGenerated: 0, - lastUpdated: block.timestamp - }); + ILiquidStrategy.StrategyStats storage stats = strategyStats[strategyId]; + stats.lastUpdated = block.timestamp; - emit CreateStrategy( - _strategyId, msg.sender, _name, _strategyDescription, _steps, _minDeposit, _maxTVL, _performanceFee - ); + emit CreateStrategy(strategyId, msg.sender, _name, _strategyDescription, _steps, _minDeposit); } - function transferToken(address _token, uint256 _amount) public returns (bool) { + function transferToken(address _token, uint256 _amount) public onlyConnector returns (bool) { // check if amount equals or more than available balance first + // only connectors can call return IERC20(_token).transfer(msg.sender, _amount); } @@ -144,64 +144,102 @@ contract Strategy { function updateUserStats( bytes32 _strategyId, address _userAddress, - address _asset, address _protocol, + address[] memory _assets, + uint256[] memory _assetsAmount, address _shareToken, - address[] memory _underlyingTokens, - uint256 _assetAmount, - uint256 _amountInUsd, uint256 _shareAmount, + address[] memory _underlyingTokens, uint256[] memory _underlyingAmounts - ) public { + ) public onlyEngine { // Get user's stats ILiquidStrategy.UserStats storage _userStats = userStats[_strategyId][_userAddress]; - ILiquidStrategy.AssetBalance memory tempAssetBal = ILiquidStrategy.AssetBalance({ - asset: _asset, - amount: _assetAmount, - usdValue: _amountInUsd, - lastUpdated: block.timestamp - }); + ILiquidStrategy.AssetBalance memory tempAssetBal = + ILiquidStrategy.AssetBalance({assets: _assets, amounts: _assetsAmount}); ILiquidStrategy.ShareBalance memory tempShareBal = ILiquidStrategy.ShareBalance({ protocol: _protocol, shareToken: _shareToken, shareAmount: _shareAmount, underlyingTokens: _underlyingTokens, - underlyingAmounts: _underlyingAmounts, - lastUpdated: block.timestamp + underlyingAmounts: _underlyingAmounts }); // - if (_userStats.initialDeposit == 0) { - _userStats.initialDeposit = _amountInUsd; - _userStats.totalDepositedUSD = _amountInUsd; + if (_userStats.tokenBalances.length == 0) { + _userStats.isActive = true; + // _userStats.initialDeposit = _assetAmount; _userStats.joinTimestamp = block.timestamp; + } - _userStats.tokenBalances.push(tempAssetBal); - _userStats.shareBalances.push(tempShareBal); - } else { - // totalWithdrawnUSD, totalReward, feesPaid - _userStats.totalDepositedUSD += _amountInUsd; + _userStats.tokenBalances.push(tempAssetBal); + _userStats.shareBalances.push(tempShareBal); + _userStats.lastActionTimestamp = block.timestamp; + } + + /** + * @dev Update strategy stats + * @param _strategyId unique identifier of the strategy to update. + * @param _assets list of asset addresses associated with the deposits. + * @param _amounts corresponding amounts of each asset being deposited. + * @param _performanceFee the performance fee generated by the strategy. + */ + function updateStrategyStats( + bytes32 _strategyId, + address[] memory _assets, + uint256[] memory _amounts, + uint256 _performanceFee + ) public onlyEngine { + ILiquidStrategy.StrategyStats storage stats = strategyStats[_strategyId]; - _userStats.tokenBalances.push(tempAssetBal); - _userStats.shareBalances.push(tempShareBal); + for (uint256 i; i < _amounts.length; i++) { + stats.totalDeposits[_assets[i]] += _amounts[i]; } + + stats.totalUsers++; + stats.totalFeeGenerated += _performanceFee; + stats.lastUpdated = block.timestamp; } /** - * @dev Update strategy stats + * @dev Update user strategy + * @param _strategyId unique identifier of the strategy to update. + * @param _user address of the user whose strategy is being updated. + * @param _indicator determines the operation: + * 0 to add a strategyId, + * any other value to remove the strategyId. */ - function updateStrategyStats(bytes32 strategyId, uint256[] memory amounts, uint256 performanceFee) public { - ILiquidStrategy.StrategyStats storage _strategyStats = strategyStats[strategyId]; + function updateUserStrategy(bytes32 _strategyId, address _user, uint256 _indicator) public onlyEngine { + if (_indicator == 0) { + userStrategies[_user].push(_strategyId); + } else { + bytes32[] memory cachedStrategies = userStrategies[_user]; + uint256 len = cachedStrategies.length; - for (uint256 i; i < amounts.length; i++) { - _strategyStats.totalDeposits[i] += amounts[i]; + for (uint256 i; i < len; i++) { + if (cachedStrategies[i] == _strategyId) { + for (uint256 j = i; j < len - 1; j++) { + userStrategies[_user][j] = userStrategies[_user][j + 1]; + } + + userStrategies[_user].pop(); + } + } + + // Get user's stats + ILiquidStrategy.UserStats storage _userStats = userStats[_strategyId][_user]; + _userStats.isActive = false; } + } - _strategyStats.totalUsers++; - _strategyStats.totalFeeGenerated += performanceFee; - _strategyStats.lastUpdated = block.timestamp; + /** + * @dev Toggles the approval status of a connector. If the connector is currently approved, + * it will be revoked, and vice versa. + * @param _connector The address of the connector to toggle. + */ + function toggleConnector(address _connector) public onlyOwner { + approveConnector[_connector] = !approveConnector[_connector]; } /** @@ -224,14 +262,6 @@ contract Strategy { return curatorStrategies[_curator]; } - /** - * @dev Get data on a particular strategy - * @param _strategyId ID of a strategy - */ - function getStrategyStats(bytes32 _strategyId) public view returns (ILiquidStrategy.StrategyStats memory) { - return strategyStats[_strategyId]; - } - /** * @dev Get all strategies * @return allStrategies Array of all strategies @@ -263,26 +293,44 @@ contract Strategy { function getUserStrategies(address _user) public view returns (bytes32[] memory) { return userStrategies[_user]; } + + /** + * @dev Get user's strategy statistics + * @param _strategyId id of the strategy + * @param _user address of the user to get strategies for + */ + function getUserStrategyStats(bytes32 _strategyId, address _user) + public + view + returns (ILiquidStrategy.UserStats memory) + { + return userStats[_strategyId][_user]; + } + /** * @dev Get user's balance for a specific asset in a strategy * @param _strategyId ID of the strategy * @param _user Address of the user - * @param _asset Address of the token to check balance for + * @param _assets Address of the token to check balance for + * @param _stepIndex Index of a step * @return AssetBalance struct containing token balance details */ - - function getUserAssetBalance(bytes32 _strategyId, address _user, address _asset) + function getUserAssetBalance(bytes32 _strategyId, address _user, address[] memory _assets, uint256 _stepIndex) public view returns (ILiquidStrategy.AssetBalance memory) { ILiquidStrategy.UserStats memory stats = userStats[_strategyId][_user]; for (uint256 i = 0; i < stats.tokenBalances.length; i++) { - if (stats.tokenBalances[i].asset == _asset) { + uint256 stepIndex = _stepIndex == type(uint256).max ? i : _stepIndex; + + if ( + keccak256(abi.encode(stats.tokenBalances[i].assets)) == keccak256(abi.encode(_assets)) && i == stepIndex + ) { return stats.tokenBalances[i]; } } - return ILiquidStrategy.AssetBalance(_asset, 0, 0, 0); + return ILiquidStrategy.AssetBalance(_assets, new uint256[](0)); } /** @@ -291,20 +339,27 @@ contract Strategy { * @param _user Address of the user * @param _protocol Address of the protocol (e.g. Aerodrome) * @param _shareToken Address of the Share token + * @param _stepIndex Index of a step * @return ShareBalance struct containing share balance details */ - function getUserShareBalance(bytes32 _strategyId, address _user, address _protocol, address _shareToken) - public - view - returns (ILiquidStrategy.ShareBalance memory) - { + function getUserShareBalance( + bytes32 _strategyId, + address _user, + address _protocol, + address _shareToken, + uint256 _stepIndex + ) public view returns (ILiquidStrategy.ShareBalance memory) { ILiquidStrategy.UserStats memory stats = userStats[_strategyId][_user]; for (uint256 i = 0; i < stats.shareBalances.length; i++) { - if (stats.shareBalances[i].protocol == _protocol && stats.shareBalances[i].shareToken == _shareToken) { + uint256 stepIndex = _stepIndex == type(uint256).max ? i : _stepIndex; + if ( + stats.shareBalances[i].protocol == _protocol && stats.shareBalances[i].shareToken == _shareToken + && i == stepIndex + ) { return stats.shareBalances[i]; } } - return ILiquidStrategy.ShareBalance(_protocol, _shareToken, 0, new address[](0), new uint256[](0), 0); + return ILiquidStrategy.ShareBalance(_protocol, _shareToken, 0, new address[](0), new uint256[](0)); } /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ diff --git a/src/interface/IConnector.sol b/src/interface/IConnector.sol index 8737a8b..5b0b196 100644 --- a/src/interface/IConnector.sol +++ b/src/interface/IConnector.sol @@ -23,16 +23,28 @@ interface IConnector { function getConnectorName() external view returns (bytes32); function getConnectorType() external view returns (ConnectorType); + /// @notice Standard action execution interface function execute( ActionType actionType, address[] memory assetsIn, uint256[] memory amounts, address assetOut, + uint256 stepIndex, uint256 amountRatio, - uint256 prevLoopAmountOut, bytes32 strategyId, address userAddress, bytes calldata data - ) external payable returns (uint256 amountOut); + ) + external + payable + returns ( + address protocol, + address[] memory assets, + uint256[] memory assetsAmount, + address shareToken, + uint256 shareAmount, + address[] memory underlyingTokens, + uint256[] memory underlyingAmounts + ); } diff --git a/src/protocols/dex/base/aerodrome-basic/main.sol b/src/protocols/dex/base/aerodrome-basic/main.sol index ea40be6..5cafec4 100644 --- a/src/protocols/dex/base/aerodrome-basic/main.sol +++ b/src/protocols/dex/base/aerodrome-basic/main.sol @@ -65,32 +65,38 @@ contract AerodromeBasicConnector is BaseConnector, Constants, AerodromeEvents { address[] memory assetsIn, uint256[] memory amounts, address assetOut, + uint256 stepIndex, uint256 amountRatio, - uint256 prevLoopAmountOut, bytes32 strategyId, address userAddress, + // uint256 prevLoopAmountOut, bytes calldata data - ) external payable override returns (uint256) { + ) + external + payable + override + returns (address, address[] memory, uint256[] memory, address, uint256, address[] memory, uint256[] memory) + { // TODO: also ensure that the original caller is execution engine address executionEngine = msg.sender; - if (actionType == ActionType.SUPPLY) { - (uint256 amountA, uint256 amountB, uint256 liquidity) = _depositBasicLiquidity(data, executionEngine); - // return abi.encode(amountA, amountB, liquidity); - return 1; - } else if (actionType == ActionType.WITHDRAW) { - (uint256 amountA, uint256 amountB) = _removeBasicLiquidity(data, executionEngine); - // return abi.encode(amountA, amountB); - return 1; - } else if (actionType == ActionType.SWAP) { - uint256[] memory amounts = _swapExactTokensForTokens(data, executionEngine); - // return abi.encode(amounts); - return 1; - } else if (actionType == ActionType.STAKE) { - // return _depositToGauge(data, executionEngine); - return 1; - } - revert InvalidAction(); + // if (actionType == ActionType.SUPPLY) { + // (uint256 amountA, uint256 amountB, uint256 liquidity) = _depositBasicLiquidity(data, executionEngine); + // // return abi.encode(amountA, amountB, liquidity); + // return 1; + // } else if (actionType == ActionType.WITHDRAW) { + // (uint256 amountA, uint256 amountB) = _removeBasicLiquidity(data, executionEngine); + // // return abi.encode(amountA, amountB); + // return 1; + // } else if (actionType == ActionType.SWAP) { + // uint256[] memory amounts = _swapExactTokensForTokens(data, executionEngine); + // // return abi.encode(amounts); + // return 1; + // } else if (actionType == ActionType.STAKE) { + // // return _depositToGauge(data, executionEngine); + // return 1; + // } + // revert InvalidAction(); } /// @notice Swaps exact tokens for tokens on the Aerodrome protocol diff --git a/src/protocols/lending/base/moonwell/main.sol b/src/protocols/lending/base/moonwell/main.sol index a2d7fc5..511c284 100644 --- a/src/protocols/lending/base/moonwell/main.sol +++ b/src/protocols/lending/base/moonwell/main.sol @@ -27,12 +27,17 @@ contract MoonwellConnector is BaseConnector, Constants, MoonwellEvents { /// @notice Initializes the MoonwellConnector /// @param name Name of the Connector /// @param connectorType Type of connector - constructor(string memory name, ConnectorType connectorType, address a, address b, address c) + constructor(string memory name, ConnectorType connectorType, address _strategy, address _engine, address _oracle) BaseConnector(name, connectorType) { - strategyModule = ILiquidStrategy(a); - engine = IEngine(b); - oracle = IOracle(c); + strategyModule = ILiquidStrategy(_strategy); + engine = IEngine(_engine); + oracle = IOracle(_oracle); + } + + modifier onlyEngine() { + require(msg.sender == address(engine), "caller is not the execution engine"); + _; } // TODO: only the execution engine should be able to call this execute method @@ -43,31 +48,33 @@ contract MoonwellConnector is BaseConnector, Constants, MoonwellEvents { address[] memory assetsIn, uint256[] memory amounts, address assetOut, + uint256 stepIndex, uint256 amountRatio, - uint256 prevLoopAmountOut, bytes32 strategyId, address userAddress, bytes calldata data - ) external payable override returns (uint256) { - require(address(engine) == msg.sender, "caller is not the execution engine"); - + ) + external + payable + override + onlyEngine + returns (address, address[] memory, uint256[] memory, address, uint256, address[] memory, uint256[] memory) + { if (actionType == IConnector.ActionType.SUPPLY) { - return _mintToken(assetsIn[0], assetOut, amounts[0], strategyId, userAddress); + return _mintToken(assetsIn[0], assetOut, amounts[0]); } else if (actionType == IConnector.ActionType.BORROW) { - return _borrowToken(assetsIn, assetOut, amounts[0], amountRatio, prevLoopAmountOut, strategyId, userAddress); + return _borrowToken(assetsIn, assetOut, amounts[0], amountRatio, strategyId, userAddress, stepIndex); + } else if (actionType == IConnector.ActionType.REPAY) { + return _repayBorrowToken(assetsIn, strategyId, userAddress, stepIndex); + } else if (actionType == IConnector.ActionType.WITHDRAW) { + return _withdrawToken(assetsIn, assetOut, strategyId, userAddress, stepIndex); } - // else if (actionType == IConnector.ActionType.SWAP) { - // uint256[] memory amounts = _swapExactTokensForTokens(data, executionEngine); - // return abi.encode(amounts); - // } else if (actionType == IConnector.ActionType.STAKE) { - // return _depositToGauge(data, executionEngine); - // } // revert InvalidAction(); } - function _mintToken(address assetIn, address assetOut, uint256 amount, bytes32 strategyId, address userAddress) + function _mintToken(address assetIn, address assetOut, uint256 amount) internal - returns (uint256) + returns (address, address[] memory, uint256[] memory, address, uint256, address[] memory, uint256[] memory) { // approve and supply asset ERC20(assetIn).approve(assetOut, amount); @@ -83,28 +90,12 @@ contract MoonwellConnector is BaseConnector, Constants, MoonwellEvents { uint256[] memory underlyingAmounts = new uint256[](1); underlyingAmounts[0] = amount; - (int256 _priceInUsd,) = _tokenAandTokenBPriceInUsd(assetIn, address(0)); - uint256 amountInUsd = (uint256(_priceInUsd) * amount) / 10 ** ERC20(assetIn).decimals(); - // transfer token to Strategy Module require(_transferToken(assetOut, shareAmount), "Invalid token amount"); - // update user info - strategyModule.updateUserStats( - strategyId, - userAddress, - assetIn, - COMPTROLLER, - assetOut, - underlyingTokens, - amount, - amountInUsd, - shareAmount, - underlyingAmounts + return ( + COMPTROLLER, underlyingTokens, underlyingAmounts, assetOut, shareAmount, underlyingTokens, underlyingAmounts ); - - // returns the balance of `assetOut` - return shareAmount; } function _borrowToken( @@ -112,21 +103,29 @@ contract MoonwellConnector is BaseConnector, Constants, MoonwellEvents { address assetOut, uint256 amount, uint256 amountRatio, - uint256 prevLoopAmountOut, bytes32 strategyId, - address userAddress - ) internal returns (uint256) { - // transfer token from Strategy Module - require(strategyModule.transferToken(assetsIn[1], prevLoopAmountOut), "Not enough collateral token"); + address userAddress, + uint256 stepIndex + ) + internal + returns (address, address[] memory, uint256[] memory, address, uint256, address[] memory, uint256[] memory) + { + // expects 3 assetsIn: e.g [token(cbBtc), collateralToken(mw_cbBtc), borrowMwContract(mw_usdc)] - // expects 3: assetsIn [token(cbBtc), collateralToken(mw_cbBtc), borrowContract(mw_usdc)] + // get collateral token balance + ILiquidStrategy.ShareBalance memory userShareBalance = + strategyModule.getUserShareBalance(strategyId, userAddress, COMPTROLLER, assetsIn[1], stepIndex); + uint256 ctBalance = userShareBalance.shareAmount; + + // transfer token from Strategy Module + require(strategyModule.transferToken(assetsIn[1], ctBalance), "Not enough collateral token"); // to borrow, first enter market by calling enterMarkets in comptroller address[] memory mTokens = new address[](1); mTokens[0] = assetsIn[1]; ComptrollerInterface(COMPTROLLER).enterMarkets(mTokens); - // to borrow + // borrow uint256 currentTokenAToBPrice = _getOneTokenAPriceInTokenB(assetsIn[0], assetOut) / 10 ** 18 - ERC20(assetOut).decimals(); uint256 suppliedAmount = (amount * currentTokenAToBPrice) / 10 ** ERC20(assetsIn[0]).decimals(); @@ -134,28 +133,109 @@ contract MoonwellConnector is BaseConnector, Constants, MoonwellEvents { uint256 success = MErc20Interface(assetsIn[2]).borrow(amountToBorrow); if (success != 0) revert(); - // update user info + address[] memory assets = new address[](1); + assets[0] = assetsIn[1]; + + uint256[] memory amounts = new uint256[](1); + amounts[0] = ctBalance; + address[] memory underlyingTokens = new address[](1); underlyingTokens[0] = assetsIn[0]; uint256[] memory underlyingAmounts = new uint256[](1); underlyingAmounts[0] = amount; - strategyModule.updateUserStats( - strategyId, - userAddress, - assetsIn[1], + // transfer tokens to Strategy Module + require(_transferToken(assetOut, amountToBorrow), "Invalid token amount"); + + return (COMPTROLLER, assets, amounts, assetOut, amountToBorrow, underlyingTokens, underlyingAmounts); + } + + function _repayBorrowToken(address[] memory assetsIn, bytes32 strategyId, address userAddress, uint256 stepIndex) + internal + returns (address, address[] memory, uint256[] memory, address, uint256, address[] memory, uint256[] memory) + { + // expects 3 assetsIn: e.g [token(usdc), collateralToken(mw_cbBtc), borrowMwContract(mw_usdc)] + + // get borrowed token balance + ILiquidStrategy.ShareBalance memory userShareBalance = + strategyModule.getUserShareBalance(strategyId, userAddress, COMPTROLLER, assetsIn[0], stepIndex); + uint256 btBalance = userShareBalance.shareAmount; + + // get asset balance + address[] memory assets = new address[](1); + assets[0] = assetsIn[1]; + + ILiquidStrategy.AssetBalance memory userAssetBalance = + strategyModule.getUserAssetBalance(strategyId, userAddress, assets, stepIndex); + + // transfer token from Strategy Module + require(strategyModule.transferToken(assetsIn[0], btBalance), "Not enough borrowed token"); + + // repay + ERC20(assetsIn[0]).approve(assetsIn[2], btBalance); + uint256 status = MErc20Interface(assetsIn[2]).repayBorrow(btBalance); + if (status != 0) revert(); + + // exit market + status = ComptrollerInterface(COMPTROLLER).exitMarket(assetsIn[1]); + if (status != 0) revert(); + + return ( COMPTROLLER, - assetOut, - underlyingTokens, - 0, + userAssetBalance.assets, + userAssetBalance.amounts, + assetsIn[0], 0, - amountToBorrow, - underlyingAmounts + userShareBalance.underlyingTokens, + userShareBalance.underlyingAmounts ); + } - // returns the balance of `assetOut` - return amountToBorrow; + function _withdrawToken( + address[] memory assetsIn, + address assetOut, + bytes32 strategyId, + address userAddress, + uint256 stepIndex + ) + internal + returns (address, address[] memory, uint256[] memory, address, uint256, address[] memory, uint256[] memory) + { + // get share token balance + ILiquidStrategy.ShareBalance memory userShareBalance = + strategyModule.getUserShareBalance(strategyId, userAddress, COMPTROLLER, assetsIn[0], stepIndex); + uint256 stBalance = userShareBalance.shareAmount; + + // get underlying token balance + address[] memory assets = new address[](1); + assets[0] = assetOut; + + ILiquidStrategy.AssetBalance memory userAssetBalance = + strategyModule.getUserAssetBalance(strategyId, userAddress, assets, stepIndex); + uint256 utBalance = userAssetBalance.amounts[0]; + + uint256 tokenBalanceBefore = ERC20(assetOut).balanceOf(address(this)); + + // redeem + ERC20(assetsIn[0]).approve(assetsIn[0], stBalance); + uint256 status = MErc20Interface(assetsIn[0]).redeem(stBalance); + if (status != 0) revert(); + + uint256 tokenBalanceDiff = ERC20(assetOut).balanceOf(address(this)) - tokenBalanceBefore; + + // check that final withdraw amount is less than initial deposit + require(tokenBalanceDiff <= utBalance, "taaaaaa"); + + return ( + COMPTROLLER, + userAssetBalance.assets, + new uint256[](0), + assetsIn[0], + 0, + userShareBalance.underlyingTokens, + new uint256[](0) + ); } // Helper function @@ -163,13 +243,13 @@ contract MoonwellConnector is BaseConnector, Constants, MoonwellEvents { return ERC20(_token).transfer(address(strategyModule), _amount); } - function _getOneTokenAPriceInTokenB(address _tokenA, address _tokenB) internal returns (uint256) { + function _getOneTokenAPriceInTokenB(address _tokenA, address _tokenB) internal view returns (uint256) { (int256 _tokenAPriceInUsd, int256 _tokenBPriceInUsd) = _tokenAandTokenBPriceInUsd(_tokenA, _tokenB); return oracle.getTokenAPriceInTokenB(uint256(_tokenAPriceInUsd), 8, uint256(_tokenBPriceInUsd), 8); } - function _tokenAandTokenBPriceInUsd(address _tokenA, address _tokenB) internal returns (int256, int256) { + function _tokenAandTokenBPriceInUsd(address _tokenA, address _tokenB) internal view returns (int256, int256) { int256 _tokenAPriceInUsd; int256 _tokenBPriceInUsd; diff --git a/test/curators/Join.t.sol b/test/curators/Join.t.sol index ec81363..fed123e 100644 --- a/test/curators/Join.t.sol +++ b/test/curators/Join.t.sol @@ -35,13 +35,18 @@ contract JoinTest is Test { // oracle = Oracle(0x333Cd307bd0d8fDB3c38b14eacC4072FF548176B); // moonwellConnector = MoonwellConnector(0x01249b37d803573c071186BC4C3ea92872B93F5E); - strategy = new Strategy(); - engine = new Engine(address(strategy)); + vm.startPrank(address(0xB0b)); + engine = new Engine(); + strategy = new Strategy(address(engine)); oracle = new Oracle(); moonwellConnector = new MoonwellConnector( - "Tett", IConnector.ConnectorType.LENDING, address(strategy), address(engine), address(oracle) + "Moonwell Connector", IConnector.ConnectorType.LENDING, address(strategy), address(engine), address(oracle) ); aerodromeBasicConnector = new AerodromeBasicConnector("Aero Connector", IConnector.ConnectorType.LENDING); + + // toggle connectors + strategy.toggleConnector(address(moonwellConnector)); + vm.stopPrank(); } function test_Join_Strategy() public { @@ -53,24 +58,26 @@ contract JoinTest is Test { vm.startPrank(curator); ERC20(cbBTC).approve(address(engine), a1); - engine.join(strategyId, amounts); - vm.stopPrank(); + engine.join(strategyId, address(strategy), amounts); + + ILiquidStrategy.UserStats memory ss = strategy.getUserStrategyStats(strategyId, curator); - ILiquidStrategy.ShareBalance memory bal = - strategy.getUserShareBalance(strategyId, curator, COMPTROLLER, moonwell_cbBTC); + assert(ERC20(USDC).balanceOf(address(moonwellConnector)) == 0); + assert(ss.isActive); - ILiquidStrategy.AssetBalance memory bal1 = strategy.getUserAssetBalance(strategyId, curator, cbBTC); + engine.exit(strategyId, address(strategy)); - assert(bal.protocol == COMPTROLLER); - assert(bal1.amount == a1); + ss = strategy.getUserStrategyStats(strategyId, curator); + + assert(!ss.isActive); + + vm.stopPrank(); } function _createStrategy() internal returns (bytes32 strategyId) { string memory name = "cbBTC"; string memory strategyDescription = "cbBTC strategy on base"; uint256 minDeposit; - uint256 maxTVL; - uint256 performanceFee; ILiquidStrategy.Step[] memory steps = new ILiquidStrategy.Step[](2); @@ -101,7 +108,7 @@ contract JoinTest is Test { }); vm.prank(curator); - strategy.createStrategy(name, strategyDescription, steps, minDeposit, maxTVL, performanceFee); + strategy.createStrategy(name, strategyDescription, steps, minDeposit); strategyId = keccak256(abi.encodePacked(curator, name, strategyDescription)); } diff --git a/test/curators/Strategy.t.sol b/test/curators/Strategy.t.sol index c02218d..815d1b5 100644 --- a/test/curators/Strategy.t.sol +++ b/test/curators/Strategy.t.sol @@ -3,11 +3,13 @@ pragma solidity ^0.8.13; import {Test, console, Vm} from "forge-std/Test.sol"; import {Strategy} from "../../src/curators/strategy.sol"; +import {Engine} from "../../src/curators/engine.sol"; import {AerodromeBasicConnector} from "../../src/protocols/dex/base/aerodrome-basic/main.sol"; import "../../src/curators/interface/IStrategy.sol"; contract StrategyTest is Test { Strategy public strategy; + Engine public engine; AerodromeBasicConnector public aerodromeBasicConnector; address constant cbBTC = 0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf; @@ -18,7 +20,8 @@ contract StrategyTest is Test { address constant aero_cbbtc_udsc_lpt = 0x827922686190790b37229fd06084350E74485b72; function setUp() public { - strategy = new Strategy(); + engine = new Engine(); + strategy = new Strategy(address(engine)); aerodromeBasicConnector = new AerodromeBasicConnector("Aero Connector", IConnector.ConnectorType.LENDING); } @@ -72,7 +75,7 @@ contract StrategyTest is Test { vm.recordLogs(); vm.prank(address(0xAAAA)); - strategy.createStrategy(name, strategyDescription, steps, minDeposit, maxTVL, performanceFee); + strategy.createStrategy(name, strategyDescription, steps, minDeposit); Vm.Log[] memory entries = vm.getRecordedLogs();