diff --git a/packages/protocol/src/abstracts/BaseVault.sol b/packages/protocol/src/abstracts/BaseVault.sol index e6f7f9105..c77ee6dd4 100644 --- a/packages/protocol/src/abstracts/BaseVault.sol +++ b/packages/protocol/src/abstracts/BaseVault.sol @@ -598,22 +598,6 @@ abstract contract BaseVault is ERC20, SystemAccessControl, PausableVault, VaultP emit Withdraw(caller, receiver, owner, assets, shares); } - /** - * @dev Hook before all token-share transfers. - * Requirements: - * - Must check `from` can move `amount` of shares. - * - * @param from address - * @param to address - * @param amount of shares - */ - function _beforeTokenTransfer(address from, address to, uint256 amount) internal view override { - to; - if (from != address(0)) { - require(amount <= maxRedeem(from), "Transfer more than max"); - } - } - /*////////////////////////////////////////////////// Debt management: based on IERC4626 semantics //////////////////////////////////////////////////*/ diff --git a/packages/protocol/src/vaults/borrowing/BorrowingVault.sol b/packages/protocol/src/vaults/borrowing/BorrowingVault.sol index c3a7b09e4..4d476860c 100644 --- a/packages/protocol/src/vaults/borrowing/BorrowingVault.sol +++ b/packages/protocol/src/vaults/borrowing/BorrowingVault.sol @@ -59,6 +59,7 @@ contract BorrowingVault is BaseVault { error BorrowingVault__borrow_moreThanAllowed(); error BorrowingVault__payback_invalidInput(); error BorrowingVault__payback_moreThanMax(); + error BorrowingVault__beforeTokenTransfer_moreThanMax(); error BorrowingVault__liquidate_invalidInput(); error BorrowingVault__liquidate_positionHealthy(); error BorrowingVault__rebalance_invalidProvider(); @@ -150,6 +151,27 @@ contract BorrowingVault is BaseVault { /// Debt management overrides /// ///////////////////////////////*/ + /** + * @dev Hook before all asset-share transfers. + * Requirements: + * - Must check `from` can move `amount` of shares. + * + * @param from address + * @param to address + * @param amount of shares + */ + function _beforeTokenTransfer(address from, address to, uint256 amount) internal view override { + /** + * @dev Hook check activated only when called by OZ {ERC20-_transfer} + * User must not be able to transfer asset-shares locked as collateral + */ + if (from != address(0) && to != address(0)) { + if (amount > maxRedeem(from)) { + revert BorrowingVault__beforeTokenTransfer_moreThanMax(); + } + } + } + /// @inheritdoc IVault function debtDecimals() public view override returns (uint8) { return _debtDecimals; diff --git a/packages/protocol/src/vaults/yield/YieldVault.sol b/packages/protocol/src/vaults/yield/YieldVault.sol index 0d2cd1ddd..98827c0a8 100644 --- a/packages/protocol/src/vaults/yield/YieldVault.sol +++ b/packages/protocol/src/vaults/yield/YieldVault.sol @@ -159,6 +159,7 @@ contract YieldVault is BaseVault { /// @inheritdoc BaseVault function _computeFreeAssets(address owner) internal view override returns (uint256) { + // There is no restriction on asset-share movements in a {YieldVault}. return convertToAssets(balanceOf(owner)); } diff --git a/packages/protocol/test/mocking/vaults/Vault.t.sol b/packages/protocol/test/mocking/vaults/Vault.t.sol index 39a324c05..9ca78537e 100644 --- a/packages/protocol/test/mocking/vaults/Vault.t.sol +++ b/packages/protocol/test/mocking/vaults/Vault.t.sol @@ -181,11 +181,44 @@ contract VaultUnitTests is MockingSetup, MockRoutines { do_depositAndBorrow(amount, borrowAmount, vault, ALICE); vm.expectRevert(BaseVault.BaseVault__withdraw_moreThanMax.selector); - vm.prank(ALICE); vault.withdraw(amount, ALICE, ALICE); } + function test_tryTransferWithoutRepay(uint96 amount, uint96 borrowAmount) public { + uint256 minAmount = vault.minAmount(); + vm.assume( + amount > minAmount && borrowAmount > minAmount && _utils_checkMaxLTV(amount, borrowAmount) + ); + do_depositAndBorrow(amount, borrowAmount, vault, ALICE); + + vm.expectRevert(BorrowingVault.BorrowingVault__beforeTokenTransfer_moreThanMax.selector); + vm.prank(ALICE); + vault.transfer(BOB, uint256(amount)); + } + + function test_tryTransferMaxRedeemWithoutRepay(uint96 amount, uint96 borrowAmount) public { + uint256 minAmount = vault.minAmount(); + vm.assume( + amount > minAmount && borrowAmount > minAmount && _utils_checkMaxLTV(amount, borrowAmount) + ); + do_depositAndBorrow(amount, borrowAmount, vault, ALICE); + uint256 maxTransferable = vault.maxRedeem(ALICE); + + vm.prank(ALICE); + vault.transfer(BOB, maxTransferable); + assertEq(vault.balanceOf(BOB), maxTransferable); + + uint256 nonTransferable = amount - maxTransferable; + + vm.expectRevert(BorrowingVault.BorrowingVault__beforeTokenTransfer_moreThanMax.selector); + vm.prank(ALICE); + vault.transfer(BOB, nonTransferable); + + // Bob's shares haven't change. + assertEq(vault.balanceOf(BOB), maxTransferable); + } + function test_setMinAmount(uint256 min) public { vm.expectEmit(true, false, false, false); emit MinAmountChanged(min);