diff --git a/foundry.toml b/foundry.toml index 449d6a5..3fa81ce 100644 --- a/foundry.toml +++ b/foundry.toml @@ -8,6 +8,7 @@ remappings = [ "openzeppelin/=lib/openzeppelin-contracts/contracts/", "evc/=lib/ethereum-vault-connector/src/", "evc-playground/=lib/evc-playground/src/", + "solmate/=lib/evc-playground/lib/solmate/src/", "a16z-erc4626-tests/=lib/erc4626-tests/" ] diff --git a/src/workshop_2/WorkshopVault.sol b/src/workshop_2/WorkshopVault.sol index d0a3f4e..9f8e2b8 100644 --- a/src/workshop_2/WorkshopVault.sol +++ b/src/workshop_2/WorkshopVault.sol @@ -5,11 +5,35 @@ pragma solidity ^0.8.19; import "openzeppelin/token/ERC20/extensions/ERC4626.sol"; import "evc/interfaces/IEthereumVaultConnector.sol"; import "evc/interfaces/IVault.sol"; +import "solmate/utils/FixedPointMathLib.sol"; import "./IWorkshopVault.sol"; contract WorkshopVault is ERC4626, IVault, IWorkshopVault { + using FixedPointMathLib for uint256; + + event Borrow(address indexed caller, address indexed owner, uint256 assets); + event Repay(address indexed caller, address indexed receiver, uint256 assets); + + error ControllerDisabled(); + error SharesSeizureFailed(); + error NoLiquidationOpportunity(); + IEVC internal immutable evc; + // Borrowing model variable + bytes private snapshot; + uint256 public totalBorrowed; + uint256 internal _totalAssets; + mapping(address account => uint256 assets) internal owed; + + // Interest rate model variables + uint256 internal interestRate = 10; // 10% APY + uint256 internal lastInterestUpdate = block.timestamp; + uint256 internal constant ONE = 1e27; + uint256 internal interestAccumulator = ONE; + + mapping(address account => uint256) internal userInterestAccumulator; + constructor( IEVC _evc, IERC20 _asset, @@ -59,6 +83,7 @@ contract WorkshopVault is ERC4626, IVault, IWorkshopVault { // IVault // [ASSIGNMENT]: why this function is necessary? is it safe to unconditionally disable the controller? function disableController() external { + require(_debtOf(_msgSender()) == 0); evc.disableController(_msgSender()); } @@ -92,6 +117,7 @@ contract WorkshopVault is ERC4626, IVault, IWorkshopVault { uint256 assets, address receiver ) public virtual override callThroughEVC withChecks(address(0)) returns (uint256 shares) { + _totalAssets += assets; return super.deposit(assets, receiver); } @@ -107,6 +133,7 @@ contract WorkshopVault is ERC4626, IVault, IWorkshopVault { address receiver, address owner ) public virtual override callThroughEVC withChecks(owner) returns (uint256 shares) { + _totalAssets -= assets; return super.withdraw(assets, receiver, owner); } @@ -133,9 +160,237 @@ contract WorkshopVault is ERC4626, IVault, IWorkshopVault { return super.transferFrom(from, to, value); } + function maxWithdraw(address owner) public view virtual override returns (uint256) { + return super.maxWithdraw(owner); + } + + function maxRedeem(address owner) public view virtual override returns (uint256) { + return balanceOf(owner) - owed[owner]; + } + + function _convertToShares(uint256 assets, bool roundUp) internal view virtual returns (uint256) { + (uint256 currentTotalBorrowed,,) = _accrueInterestCalculate(); + + return roundUp + ? assets.mulDivUp(totalSupply() + 1, _totalAssets + currentTotalBorrowed + 1) + : assets.mulDivDown(totalSupply() + 1, _totalAssets + currentTotalBorrowed + 1); + } + + /// @dev This function is overridden to take into account the fact that some of the assets may be borrowed. + function _convertToAssets(uint256 shares, bool roundUp) internal view virtual returns (uint256) { + (uint256 currentTotalBorrowed,,) = _accrueInterestCalculate(); + return roundUp + ? shares.mulDivUp(_totalAssets + currentTotalBorrowed + 1, totalSupply() + 1) + : shares.mulDivDown(_totalAssets + currentTotalBorrowed + 1, totalSupply() + 1); + } + + function convertToAssets(uint256 shares) public view virtual override returns (uint256) { + return _convertToAssets(shares, true); + } + + function convertToShares(uint256 assets) public view virtual override returns (uint256) { + return _convertToShares(assets, true); + } + + function _msgSenderForBorrow() internal view returns (address) { + address sender = msg.sender; + bool controllerEnabled; + + if (sender == address(evc)) { + (sender, controllerEnabled) = evc.getCurrentOnBehalfOfAccount(address(this)); + } else { + controllerEnabled = evc.isControllerEnabled(sender, address(this)); + } + + if (!controllerEnabled) { + revert ControllerDisabled(); + } + + return sender; + } + + function createVaultSnapshot() internal { + if (snapshot.length == 0) { + snapshot = doCreateVaultSnapshot(); + } + } + + function doCreateVaultSnapshot() internal virtual returns (bytes memory) { + (uint256 currentTotalBorrowed,) = _accrueInterest(); + + return abi.encode(_convertToAssets(totalSupply(), false), currentTotalBorrowed); + } + + function requireAccountAndVaultStatusCheck(address account) internal { + if (account == address(0)) { + evc.requireVaultStatusCheck(); + } else { + evc.requireAccountAndVaultStatusCheck(account); + } + } + + function getAccountOwner(address account) internal view returns (address owner) { + try evc.getAccountOwner(account) returns (address _owner) { + owner = _owner; + } catch { + owner = account; + } + } + + function _increaseOwed(address account, uint256 assets) internal virtual { + owed[account] = _debtOf(account) + assets; + totalBorrowed += assets; + userInterestAccumulator[account] = interestAccumulator; + } + + function _decreaseOwed(address account, uint256 assets) internal virtual { + owed[account] = _debtOf(account) - assets; + totalBorrowed -= assets; + userInterestAccumulator[account] = interestAccumulator; + } + + function _debtOf(address account) internal view virtual returns (uint256) { + uint256 debt = owed[account]; + + if (debt == 0) return 0; + + (, uint256 currentInterestAccumulator, bool shouldUpdate) = _accrueInterestCalculate(); + if(!shouldUpdate){ + return debt; + } + return (debt * currentInterestAccumulator) / userInterestAccumulator[account]; + } + + function debtOf(address account) public view virtual returns (uint256) { + return _debtOf(account); + } + + function _accrueInterest() internal virtual returns (uint256, uint256) { + (uint256 currentTotalBorrowed, uint256 currentInterestAccumulator, bool shouldUpdate) = + _accrueInterestCalculate(); + + if (shouldUpdate) { + totalBorrowed = currentTotalBorrowed; + interestAccumulator = currentInterestAccumulator; + lastInterestUpdate = block.timestamp; + } + + return (currentTotalBorrowed, currentInterestAccumulator); + } + + function _accrueInterestCalculate() internal view virtual returns (uint256, uint256, bool) { + uint256 timeElapsed = block.timestamp - lastInterestUpdate; + uint256 oldTotalBorrowed = totalBorrowed; + uint256 oldInterestAccumulator = interestAccumulator; + + if (timeElapsed == 0) { + return (oldTotalBorrowed, oldInterestAccumulator, false); + } + + uint256 newInterestAccumulator = ( + FixedPointMathLib.rpow(uint256(int256(computeInterestRate()) + int256(ONE)), timeElapsed, ONE) + * oldInterestAccumulator + ) / ONE; + + uint256 newTotalBorrowed = (oldTotalBorrowed * newInterestAccumulator) / oldInterestAccumulator; + + return (newTotalBorrowed, newInterestAccumulator, true); + } + + function computeInterestRate() internal view virtual returns (int96) { + return int96(int256(uint256((1e27 * interestRate) / 100) / (86400 * 365))); + } + + function isControllerEnabled(address account, address vault) internal view returns (bool) { + return evc.isControllerEnabled(account, vault); + } + // IWorkshopVault - function borrow(uint256 assets, address receiver) external {} - function repay(uint256 assets, address receiver) external {} - function pullDebt(address from, uint256 assets) external returns (bool) {} - function liquidate(address violator, address collateral) external {} + function borrow(uint256 assets, address receiver) external callThroughEVC { + address msgSender = _msgSenderForBorrow(); + + createVaultSnapshot(); + + require(assets != 0, "ZERO_ASSETS"); + + // users might input an EVC subaccount, in which case we want to send tokens to the owner + receiver = getAccountOwner(receiver); + + _increaseOwed(msgSender, assets); + + emit Borrow(msgSender, receiver, assets); + + IERC20(asset()).transfer(receiver, assets); + + _totalAssets -= assets; + + requireAccountAndVaultStatusCheck(msgSender); + } + + function repay(uint256 assets, address receiver) external callThroughEVC { + address msgSender = _msgSender(); + + if (!isControllerEnabled(receiver, address(this))) { + revert ControllerDisabled(); + } + + createVaultSnapshot(); + + require(assets != 0, "ZERO_ASSETS"); + + IERC20(asset()).transferFrom(msgSender, address(this), assets); + + _totalAssets += assets; + + _decreaseOwed(receiver, assets); + + emit Repay(msgSender, receiver, assets); + + requireAccountAndVaultStatusCheck(address(0)); + } + + function pullDebt(address from, uint256 assets) external callThroughEVC returns (bool) { + address msgSender = _msgSenderForBorrow(); + + if (!isControllerEnabled(from, address(this))) { + revert ControllerDisabled(); + } + + createVaultSnapshot(); + + require(assets != 0, "ZERO_AMOUNT"); + require(msgSender != from, "SELF_DEBT_PULL"); + + _decreaseOwed(from, assets); + _increaseOwed(msgSender, assets); + + emit Repay(msgSender, from, assets); + emit Borrow(msgSender, msgSender, assets); + + requireAccountAndVaultStatusCheck(msgSender); + + return true; + } + + /// Extremely naive liquidation implementation + function liquidate(address violator, address collateral) external callThroughEVC { + address msgSender = _msgSenderForBorrow(); + if (balanceOf(violator) >= debtOf(violator) ) { + revert NoLiquidationOpportunity(); + } + + doCreateVaultSnapshot(); + + uint256 seizeShares = debtOf(violator) ; + + _decreaseOwed(violator, seizeShares); + _increaseOwed(msgSender, seizeShares); + + emit Repay(msgSender, violator, seizeShares); + emit Borrow(msgSender, msgSender, seizeShares); + + _transfer(violator, msgSender, seizeShares); + + emit Transfer(violator, msgSender, seizeShares); + } } diff --git a/src/workshop_3/PositionManager.sol b/src/workshop_3/PositionManager.sol index 2836d51..6d9a7ea 100644 --- a/src/workshop_3/PositionManager.sol +++ b/src/workshop_3/PositionManager.sol @@ -1,7 +1,57 @@ // SPDX-License-Identifier: GPL-2.0-or-later - pragma solidity ^0.8.19; import "evc-playground/vaults/VaultRegularBorrowable.sol"; +import "evc-playground/vaults/VaultSimple.sol"; +import "solmate/utils/SafeTransferLib.sol"; +import "forge-std/console2.sol"; + +contract PositionManager { + using SafeTransferLib for ERC20; + + IEVC public immutable evc; + mapping(address => bool) public vaults; + mapping(address => uint256) public lastRebalanceTimestamp; + + constructor(IEVC _evc, address[] memory _vaults) { + evc = _evc; + for (uint256 i = 0; i < _vaults.length; i += 1) { + vaults[_vaults[i]] = true; + } + } + + function rebalance(address _onBehalfOfAccount, address _currentVault, address _newVault) public { + require( + lastRebalanceTimestamp[_onBehalfOfAccount] == 0 + || block.timestamp >= lastRebalanceTimestamp[_onBehalfOfAccount] + 1 days, + "Rebalance can only be performed once a day" + ); + require(vaults[_newVault], "Not allowed vault"); + require( + VaultRegularBorrowable(_newVault).getInterestRate() + > VaultRegularBorrowable(_currentVault).getInterestRate(), + "Cannot rebalance to a lowest rate vault" + ); + + ERC20 asset = ERC4626(_currentVault).asset(); + uint256 assets = VaultRegularBorrowable(_currentVault).maxWithdraw(_onBehalfOfAccount); + + // if there's anything to withdraw, withdraw it to this contract + evc.call( + _currentVault, + _onBehalfOfAccount, + 0, + abi.encodeWithSelector(VaultSimple.withdraw.selector, assets, address(this), _onBehalfOfAccount) + ); + + // transfer 1% of the withdrawn assets as a tip to the msg.sender + asset.safeTransfer(msg.sender, assets / 100); + + // rebalance the rest on behalf of account + asset.approve(_newVault, ERC20(asset).balanceOf(address(this))); + VaultRegularBorrowable(_newVault).deposit(ERC20(asset).balanceOf(address(this)), _onBehalfOfAccount); -contract PositionManager {} + // Update the last rebalance timestamp + lastRebalanceTimestamp[_onBehalfOfAccount] = block.timestamp; + } +} diff --git a/test/mocks/IRMMock.sol b/test/mocks/IRMMock.sol new file mode 100644 index 0000000..858bd16 --- /dev/null +++ b/test/mocks/IRMMock.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.19; + +import "evc-playground/interfaces/IIRM.sol"; + +contract IRMMock is IIRM { + uint256 internal constant SECONDS_PER_YEAR = 365.2425 * 86400; // Gregorian calendar + int96 internal constant MAX_ALLOWED_INTEREST_RATE = int96(int256(uint256(5 * 1e27) / SECONDS_PER_YEAR)); // 500% APR + int96 internal constant MIN_ALLOWED_INTEREST_RATE = 0; + + uint256 internal interestRate; + + function setInterestRate(uint256 _interestRate) external { + interestRate = _interestRate; + } + + function computeInterestRate(address market, address asset, uint32 utilisation) external returns (int96) { + int96 rate = computeInterestRateImpl(market, asset, utilisation); + + if (rate > MAX_ALLOWED_INTEREST_RATE) { + rate = MAX_ALLOWED_INTEREST_RATE; + } else if (rate < MIN_ALLOWED_INTEREST_RATE) { + rate = MIN_ALLOWED_INTEREST_RATE; + } + + return rate; + } + + function computeInterestRateImpl(address, address, uint32) internal virtual returns (int96) { + return int96(int256(uint256((1e27 * interestRate) / 100) / (86400 * 365))); // not SECONDS_PER_YEAR to avoid + // breaking tests + } + + function reset(address market, bytes calldata resetParams) external virtual {} +} diff --git a/test/mocks/PriceOracleMock.sol b/test/mocks/PriceOracleMock.sol new file mode 100644 index 0000000..d82d529 --- /dev/null +++ b/test/mocks/PriceOracleMock.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.19; + +import "solmate/tokens/ERC20.sol"; +import "evc-playground/interfaces/IPriceOracle.sol"; + +contract PriceOracleMock is IPriceOracle { + mapping(address base => mapping(address quote => uint256)) internal quotes; + + function setQuote(address base, address quote, uint256 quoteValue) external { + quotes[base][quote] = quoteValue; + } + + function name() external pure returns (string memory) { + return "PriceOracleMock"; + } + + function getQuote(uint256 amount, address base, address quote) external view returns (uint256 out) { + return (quotes[base][quote] * amount) / 10 ** ERC20(base).decimals(); + } + + function getQuotes( + uint256 amount, + address base, + address quote + ) external view returns (uint256 bidOut, uint256 askOut) { + uint256 out = (quotes[base][quote] * amount) / 10 ** ERC20(base).decimals(); + return (out, out); + } + + function getTick(uint256, address, address) external pure returns (uint256) { + return 0; + } + + function getTicks(uint256, address, address) external pure returns (uint256, uint256) { + return (0, 0); + } +} diff --git a/test/workshop_2/WorkshopVault.t.sol b/test/workshop_2/WorkshopVault.t.sol index 7fa4cd7..13d2cfa 100644 --- a/test/workshop_2/WorkshopVault.t.sol +++ b/test/workshop_2/WorkshopVault.t.sol @@ -235,4 +235,60 @@ contract VaultTest is ERC4626Test { vm.expectRevert(); vault.disableController(); } + + function test_assigment_liquidate(address alice, address bob, uint64 amount) public { + vm.assume(alice != address(0) && alice != address(_evc_) && alice != address(_vault_)); + vm.assume(bob != address(0) && bob != address(_evc_) && bob != address(_vault_)); + vm.assume(!_evc_.haveCommonOwner(alice, bob)); + vm.assume(amount > 1e18); + + uint256 amountToBorrow = amount - amount / 11; + ERC20 underlying = ERC20(_underlying_); + WorkshopVault vault = WorkshopVault(_vault_); + + // mint some assets to alice + ERC20Mock(_underlying_).mint(alice, amount); + + // mint some assets to bob + ERC20Mock(_underlying_).mint(bob, amount); + + // alice approves the vault to spend her assets + vm.prank(alice); + underlying.approve(address(vault), type(uint256).max); + + // alice deposits an amount + vm.prank(alice); + vault.deposit(amount, alice); + + // alice enables controller + vm.prank(alice); + _evc_.enableController(alice, address(vault)); + + // alice borrows + vm.prank(alice); + vault.borrow(amountToBorrow, alice); + + // varify alice's balance. despite amount borrowed, she should still hold shares worth the full amount + assertEq(underlying.balanceOf(alice), amountToBorrow); + assertEq(vault.convertToAssets(vault.balanceOf(alice)), amount); + + // verify maxWithdraw and maxRedeem functions + assertEq(vault.maxWithdraw(alice), amount - amountToBorrow); + assertEq(vault.convertToAssets(vault.maxRedeem(alice)), amount - amountToBorrow); + + // verify conversion functions + assertEq(vault.convertToShares(amount), vault.balanceOf(alice)); + assertEq(vault.convertToAssets(vault.balanceOf(alice)), amount); + + // Time passes and alice's account becomes unhealthy + vm.warp(block.timestamp + 365 days); + + // bob enables controller + vm.prank(bob); + _evc_.enableController(bob, address(vault)); + + // bob liquidates alice's debt + vm.prank(bob); + vault.liquidate(alice, address(_underlying_)); + } } diff --git a/test/workshop_3/PositionManager.t.sol b/test/workshop_3/PositionManager.t.sol new file mode 100644 index 0000000..0f66e3d --- /dev/null +++ b/test/workshop_3/PositionManager.t.sol @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import "forge-std/console2.sol"; +import "evc/EthereumVaultConnector.sol"; +import "evc-playground/vaults/VaultRegularBorrowable.sol"; +import "evc-playground/vaults/VaultSimple.sol"; +import "solmate/test/utils/mocks/MockERC20.sol"; +import "../mocks/IRMMock.sol"; +import "../mocks/PriceOracleMock.sol"; +import "../../src/workshop_3/PositionManager.sol"; + +contract PositionManagerTest is Test { + IEVC evc; + MockERC20 referenceAsset; + MockERC20 liabilityAsset; + MockERC20 collateralAsset; + IRMMock irm1; + IRMMock irm2; + IRMMock irm3; + PriceOracleMock oracle; + VaultRegularBorrowable vault1; + VaultRegularBorrowable vault2; + VaultRegularBorrowable vault3; + VaultSimple collateralVault; + + PositionManager positionManager; + + function setUp() public { + evc = new EthereumVaultConnector(); + referenceAsset = new MockERC20("Reference Asset", "RA", 18); + liabilityAsset = new MockERC20("Liability Asset", "LA", 18); + collateralAsset = new MockERC20("Collateral Asset", "CA", 18); + irm1 = new IRMMock(); + irm2 = new IRMMock(); + irm3 = new IRMMock(); + oracle = new PriceOracleMock(); + + vault1 = + new VaultRegularBorrowable(evc, liabilityAsset, irm1, oracle, referenceAsset, "Liability Vault 1", "LV1"); + + vault2 = + new VaultRegularBorrowable(evc, liabilityAsset, irm2, oracle, referenceAsset, "Liability Vault 2", "LV2"); + + vault3 = + new VaultRegularBorrowable(evc, liabilityAsset, irm3, oracle, referenceAsset, "Liability Vault 3", "LV3"); + + collateralVault = new VaultSimple(evc, collateralAsset, "Collateral Vault", "CV"); + + irm1.setInterestRate(10); // 10% APY + irm2.setInterestRate(15); // 15% APY + irm3.setInterestRate(20); // 20% APY + oracle.setQuote(address(liabilityAsset), address(referenceAsset), 1e17); // 1 LA = 0.1 RA + oracle.setQuote(address(collateralAsset), address(referenceAsset), 1e16); // 1 CA = 0.01 RA + + address[] memory allowedVaults = new address[](3); + allowedVaults[0] = address(vault1); + allowedVaults[1] = address(vault2); + allowedVaults[2] = address(vault3); + + positionManager = new PositionManager(evc, allowedVaults); + + // all vaults accept collateralVault as collateral + vault1.setCollateralFactor(collateralVault, 100); + vault2.setCollateralFactor(collateralVault, 100); + vault3.setCollateralFactor(collateralVault, 100); + } + + function test_PositionManagementCreated() public { + assert(address(positionManager) != address(0)); + assert(positionManager.evc() == evc); + assertTrue(positionManager.vaults(address(vault1))); + assertTrue(positionManager.vaults(address(vault2))); + assertTrue(positionManager.vaults(address(vault3))); + assertFalse(positionManager.vaults(address(collateralVault))); + } + + function test_PositionRebalance(address alice, address bob, address carol, address dave) public { + vm.assume(alice != address(0) && bob != address(0) && !evc.haveCommonOwner(alice, bob)); + vm.assume( + carol != address(0) && dave != address(0) && !evc.haveCommonOwner(bob, carol) + && !evc.haveCommonOwner(carol, dave) + ); + vm.assume( + !evc.haveCommonOwner(alice, bob) && !evc.haveCommonOwner(alice, carol) && !evc.haveCommonOwner(bob, carol) + && !evc.haveCommonOwner(alice, dave) && !evc.haveCommonOwner(bob, dave) && !evc.haveCommonOwner(carol, dave) + ); + vm.assume(alice != address(evc) && bob != address(evc) && carol != address(evc) && dave != address(evc)); + vm.assume( + address(vault1) != address(evc) && address(vault2) != address(evc) && address(vault3) != address(evc) + && address(collateralVault) != address(evc) + ); + vm.assume( + alice != address(positionManager) && bob != address(positionManager) && carol != address(positionManager) + && dave != address(positionManager) + ); + vm.assume( + alice != address(vault1) && bob != address(vault1) && carol != address(vault1) && dave != address(vault1) + ); + vm.assume( + alice != address(vault2) && bob != address(vault2) && carol != address(vault2) && dave != address(vault2) + ); + vm.assume( + alice != address(vault3) && bob != address(vault3) && carol != address(vault3) && dave != address(vault3) + ); + vm.assume( + alice != address(collateralVault) && bob != address(collateralVault) && carol != address(collateralVault) + && dave != address(collateralVault) + ); + + /* + * Initial Asset Minting + */ + // alice and bob will act as the lenders of reference asset + liabilityAsset.mint(alice, 100e18); + liabilityAsset.mint(bob, 100e18); + // carol will act as the borrower while also holds enough to repay the loadn + collateralAsset.mint(carol, 1000e18); + + // alice deposits liability asset in vault1 and authorizes position manager + vm.startPrank(alice); + liabilityAsset.approve(address(vault1), 100e18); + vault1.deposit(100e18, alice); + // alice authorizes the Position Manager to act on behalf of her account + evc.setAccountOperator(alice, address(positionManager), true); + vm.stopPrank(); + // assert that all ok + assertEq(liabilityAsset.balanceOf(address(alice)), 0); + assertEq(vault1.maxWithdraw(alice), 100e18); + + // bob deposits half liability asset in vault2 and half in vault3 + vm.startPrank(bob); + liabilityAsset.approve(address(vault2), 50e18); + liabilityAsset.approve(address(vault3), 50e18); + vault2.deposit(50e18, bob); + vault3.deposit(50e18, bob); + vm.stopPrank(); + + // dave rebalances alice's sub account from vault1 to vault2 that has higher interest rate + vm.prank(dave); + positionManager.rebalance(alice, address(vault1), address(vault2)); + + // alice's account balance on vault 2 is the initial minus the operator's tip + assertEq(vault2.maxWithdraw(alice), 100e18 - 100e18 / 100); + + // bot cannot rebalance more often than every after day + vm.prank(dave); + vm.expectRevert("Rebalance can only be performed once a day"); + positionManager.rebalance(alice, address(vault2), address(vault3)); + + // make it pass a day + vm.warp(block.timestamp + 1 days ); + + // bot cannot rebalance to a non allowed vault + vm.prank(dave); + vm.expectRevert("Not allowed vault"); + positionManager.rebalance(alice, address(vault2), address(collateralVault)); + + // bot cannot rebalance to a lowest rate vault + vm.prank(dave); + vm.expectRevert("Cannot rebalance to a lowest rate vault"); + positionManager.rebalance(alice, address(vault2), address(vault1)); + + // bot can rebalance to a highest rate vault + vm.prank(dave); + positionManager.rebalance(alice, address(vault2), address(vault3)); + + // alice's account balance on vault 2 is the initial minus twice operator's tips + uint256 alicesBalance = 100e18 - 100e18 / 100; // after first rebalance + assertEq(vault3.maxWithdraw(alice), alicesBalance - alicesBalance / 100 ); + // and dave has earned twice tips from alice + assertEq(liabilityAsset.balanceOf(dave), 100e18 / 100 + alicesBalance / 100); + } +} \ No newline at end of file