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
38 changes: 2 additions & 36 deletions src/vault/SingleStrategyVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
pragma solidity 0.8.29;

import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol";
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";

import { ControlledERC7575Vault, SafeERC20, IERC20, IController } from "./ControlledERC7575Vault.sol";

Expand All @@ -14,17 +13,6 @@ import { ControlledERC7575Vault, SafeERC20, IERC20, IController } from "./Contro
* and deallocates assets when withdrawals require more than the available unallocated balance
*/
contract SingleStrategyVault is ControlledERC7575Vault {
using Math for uint256;

/**
* @notice The maximum allowed slippage during allocation/deallocation operations
*/
uint256 public constant MAX_ALLOCATION_SLIPPAGE = 1; // 0.00001%
/**
* @notice The number of decimals used for slippage calculations
*/
uint256 public constant ALLOCATION_SLIPPAGE_DECIMALS = 7;

/**
* @notice The ERC4626 vault used for yield generation
*/
Expand Down Expand Up @@ -64,10 +52,6 @@ contract SingleStrategyVault is ControlledERC7575Vault {
* @notice Thrown when caller is not the authorized manager
*/
error CallerNotManager();
/**
* @notice Thrown when slippage exceeds the maximum allowed during allocation/deallocation
*/
error ExceedsMaxSlippage();

/**
* @notice Constructs a new SingleStrategyVault
Expand Down Expand Up @@ -153,44 +137,26 @@ contract SingleStrategyVault is ControlledERC7575Vault {

/**
* @notice Internal function to allocate assets to the strategy
* @dev Deposits assets into the strategy and emits an Allocate event.
* Reverts if allocation slippage exceeds the maximum allowed.
* @dev Deposits assets into the strategy and emits an Allocate event
* @param assets The amount of assets to deposit into the strategy
* @return shares The number of strategy shares received
*/
function _allocate(uint256 assets) private returns (uint256 shares) {
uint256 originalTotalAssets = totalAssets();
shares = _strategy.deposit(assets, address(this));
require(totalAssets() >= _slippageMinAssets(originalTotalAssets), ExceedsMaxSlippage());
emit Allocate(address(_strategy), assets);
}

/**
* @notice Internal function to deallocate assets from the strategy
* @dev Withdraws assets from the strategy to this vault and emits a Deallocate event.
* Reverts if deallocation slippage exceeds the maximum allowed.
* @dev Withdraws assets from the strategy to this vault and emits a Deallocate event
* @param assets The amount of assets to withdraw from the strategy
* @return shares The number of strategy shares burned
*/
function _deallocate(uint256 assets) private returns (uint256 shares) {
uint256 originalTotalAssets = totalAssets();
shares = _strategy.withdraw(assets, address(this), address(this));
require(totalAssets() >= _slippageMinAssets(originalTotalAssets), ExceedsMaxSlippage());
emit Deallocate(address(_strategy), assets);
}

/**
* @notice Calculates the minimum acceptable assets after accounting for maximum slippage
* @dev Used to ensure that allocation/deallocation does not exceed the allowed slippage
* @param assets The original amount of assets before allocation/deallocation
* @return The minimum acceptable amount of assets after slippage
*/
function _slippageMinAssets(uint256 assets) private pure returns (uint256) {
return assets.mulDiv(
10 ** ALLOCATION_SLIPPAGE_DECIMALS - MAX_ALLOCATION_SLIPPAGE, 10 ** ALLOCATION_SLIPPAGE_DECIMALS
);
}

/**
* @notice Calculates assets owned by the vault but not held directly in the vault
* @dev Override from ControlledERC7575Vault. Returns only assets allocated to strategies,
Expand Down
42 changes: 0 additions & 42 deletions tests/fork/SingleStrategyVault.fork.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -95,46 +95,4 @@ contract SingleStrategyVault_Strategy_ForkTest is SingleStrategyVaultForkTest {
assertEq(USDC.balanceOf(address(vault)), 0);
assertApproxEqAbs(vault.totalAssets(), unallocatedDeposit, maxErrorDelta); // strategy rounds down
}

function test_shouldRevert_whenAllocateWithSlippage() public {
uint256 depositAssets = 200e6; // 200 USDC

vm.prank(users[0]);
USDC.approve(address(vault), depositAssets);

bytes[] memory shareBalances = new bytes[](2);
shareBalances[0] = abi.encode(0);
shareBalances[1] = abi.encode(USDC_STRATEGY.convertToShares(depositAssets - 1e6)); // simulate 1 USDC slippage
vm.mockCalls(
address(USDC_STRATEGY), abi.encodeWithSelector(IERC20.balanceOf.selector, address(vault)), shareBalances
);

vm.expectRevert(Vault.ExceedsMaxSlippage.selector);
vm.prank(users[0]);
vault.deposit(depositAssets, users[0]);
}

function test_shouldRevert_whenDeallocateWithSlippage() public {
uint256 assets = 500e6; // 500 USDC

vm.startPrank(users[0]);
USDC.approve(address(vault), assets);
vault.deposit(assets, address(this));
vm.stopPrank();

assertEq(USDC.balanceOf(address(vault)), 0);
assertApproxEqAbs(vault.totalAssets(), assets, maxErrorDelta); // strategy rounds down

uint256 withdrawAssets = 300e6; // 300 USDC

bytes[] memory assetBalances = new bytes[](3);
assetBalances[0] = abi.encode(0);
assetBalances[1] = abi.encode(0);
assetBalances[2] = abi.encode(withdrawAssets - 1e6); // simulate 1 USDC slippage
vm.mockCalls(address(USDC), abi.encodeWithSelector(IERC20.balanceOf.selector, address(vault)), assetBalances);

vm.expectRevert(Vault.ExceedsMaxSlippage.selector);
vm.prank(users[0]);
vault.withdraw(withdrawAssets, users[0], users[0]);
}
}
79 changes: 1 addition & 78 deletions tests/unit/vault/SingleStrategyVault.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -66,19 +66,6 @@ contract SingleStrategyVault_Allocate_Test is SingleStrategyVaultTest {
function setUp() public override {
super.setUp();

bytes[] memory assetBalances = new bytes[](2);
assetBalances[0] = abi.encode(assets);
assetBalances[1] = abi.encode(0);
vm.mockCalls(asset, abi.encodeWithSelector(IERC20.balanceOf.selector, address(vault)), assetBalances);

bytes[] memory shareBalances = new bytes[](2);
shareBalances[0] = abi.encode(0);
shareBalances[1] = abi.encode(shares);
vm.mockCalls(strategy, abi.encodeWithSelector(IERC20.balanceOf.selector, address(vault)), shareBalances);

vm.mockCall(strategy, abi.encodeWithSelector(IERC4626.convertToAssets.selector, 0), abi.encode(0));
vm.mockCall(strategy, abi.encodeWithSelector(IERC4626.convertToAssets.selector, shares), abi.encode(assets));

vm.mockCall(strategy, abi.encodeWithSelector(IERC4626.deposit.selector), abi.encode(shares));
}

Expand All @@ -95,16 +82,6 @@ contract SingleStrategyVault_Allocate_Test is SingleStrategyVaultTest {
vault.allocate(assets);
}

function testFuzz_shouldRevert_whenSlippageTooHigh(uint256 _assets) public {
_assets = bound(_assets, 0, assets * (1e7 - 1) / 1e7 - 1);

vm.mockCall(strategy, abi.encodeWithSelector(IERC4626.convertToAssets.selector, shares), abi.encode(_assets));

vm.prank(manager);
vm.expectRevert(Vault.ExceedsMaxSlippage.selector);
vault.allocate(assets);
}

function test_shouldEmit_Allocate() public {
vm.expectEmit();
emit Vault.Allocate(strategy, assets);
Expand All @@ -123,19 +100,6 @@ contract SingleStrategyVault_Deallocate_Test is SingleStrategyVaultTest {
function setUp() public override {
super.setUp();

bytes[] memory assetBalances = new bytes[](2);
assetBalances[0] = abi.encode(0);
assetBalances[1] = abi.encode(assets);
vm.mockCalls(asset, abi.encodeWithSelector(IERC20.balanceOf.selector, address(vault)), assetBalances);

bytes[] memory shareBalances = new bytes[](2);
shareBalances[0] = abi.encode(shares);
shareBalances[1] = abi.encode(0);
vm.mockCalls(strategy, abi.encodeWithSelector(IERC20.balanceOf.selector, address(vault)), shareBalances);

vm.mockCall(strategy, abi.encodeWithSelector(IERC4626.convertToAssets.selector, 0), abi.encode(0));
vm.mockCall(strategy, abi.encodeWithSelector(IERC4626.convertToAssets.selector, shares), abi.encode(assets));

vm.mockCall(strategy, abi.encodeWithSelector(IERC4626.withdraw.selector), abi.encode(shares));
}

Expand All @@ -152,19 +116,6 @@ contract SingleStrategyVault_Deallocate_Test is SingleStrategyVaultTest {
vault.deallocate(assets);
}

function testFuzz_shouldRevert_whenSlippageTooHigh(uint256 _assets) public {
_assets = bound(_assets, 0, assets * (1e7 - 1) / 1e7 - 1);

bytes[] memory assetBalances = new bytes[](2);
assetBalances[0] = abi.encode(0);
assetBalances[1] = abi.encode(_assets);
vm.mockCalls(asset, abi.encodeWithSelector(IERC20.balanceOf.selector, address(vault)), assetBalances);

vm.prank(manager);
vm.expectRevert(Vault.ExceedsMaxSlippage.selector);
vault.deallocate(assets);
}

function test_shouldEmit_Deallocate() public {
vm.expectEmit();
emit Vault.Deallocate(strategy, assets);
Expand Down Expand Up @@ -233,22 +184,9 @@ contract SingleStrategyVault_BeforeWithdraw_Test is SingleStrategyVaultTest {
vm.assume(withdrawAssets > unallocatedAssets);
uint256 deallocateAssets = withdrawAssets - unallocatedAssets;

bytes[] memory assetBalances = new bytes[](3);
assetBalances[0] = abi.encode(unallocatedAssets);
assetBalances[1] = abi.encode(unallocatedAssets);
assetBalances[2] = abi.encode(withdrawAssets);
vm.mockCalls(asset, abi.encodeWithSelector(IERC20.balanceOf.selector, address(vault)), assetBalances);

bytes[] memory shareBalances = new bytes[](2);
shareBalances[0] = abi.encode(shares);
shareBalances[1] = abi.encode(0);
vm.mockCalls(strategy, abi.encodeWithSelector(IERC20.balanceOf.selector, address(vault)), shareBalances);

vm.mockCall(strategy, abi.encodeWithSelector(IERC4626.convertToAssets.selector, 0), abi.encode(0));
vm.mockCall(
strategy, abi.encodeWithSelector(IERC4626.convertToAssets.selector, shares), abi.encode(deallocateAssets)
asset, abi.encodeWithSelector(IERC20.balanceOf.selector, address(vault)), abi.encode(unallocatedAssets)
);

vm.mockCall(strategy, abi.encodeWithSelector(IERC4626.withdraw.selector), abi.encode(shares));

vm.expectCall(
Expand Down Expand Up @@ -291,21 +229,6 @@ contract SingleStrategyVault_AfterDeposit_Test is SingleStrategyVaultTest {
function testFuzz_shouldAllocateToStrategy_whenAboveThreshold(uint256 depositAssets) public {
vm.assume(depositAssets >= threshold);

bytes[] memory assetBalances = new bytes[](2);
assetBalances[0] = abi.encode(depositAssets);
assetBalances[1] = abi.encode(0);
vm.mockCalls(asset, abi.encodeWithSelector(IERC20.balanceOf.selector, address(vault)), assetBalances);

bytes[] memory shareBalances = new bytes[](2);
shareBalances[0] = abi.encode(0);
shareBalances[1] = abi.encode(shares);
vm.mockCalls(strategy, abi.encodeWithSelector(IERC20.balanceOf.selector, address(vault)), shareBalances);

vm.mockCall(strategy, abi.encodeWithSelector(IERC4626.convertToAssets.selector, 0), abi.encode(0));
vm.mockCall(
strategy, abi.encodeWithSelector(IERC4626.convertToAssets.selector, shares), abi.encode(depositAssets)
);

vm.mockCall(strategy, abi.encodeWithSelector(IERC4626.deposit.selector), abi.encode(shares));

vm.expectCall(strategy, abi.encodeWithSelector(IERC4626.deposit.selector, depositAssets, address(vault)));
Expand Down