Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add slippage protection functions #94

Merged
merged 1 commit into from Mar 21, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/PrizeVault.sol
Expand Up @@ -249,6 +249,16 @@ contract PrizeVault is TwabERC20, Claimable, IERC4626, ILiquidationSource, Ownab
/// @param totalSupply The total shares minted and internally accounted for by the vault
error LossyDeposit(uint256 totalAssets, uint256 totalSupply);

/// @notice Thrown when a withdraw call burns more shares than the max share limit provided.
/// @param shares The shares burned by the withdrawal
/// @param maxShares The max share limit provided
error MaxSharesExceeded(uint256 shares, uint256 maxShares);

/// @notice Thrown when a redeem call returns less assets than the min threshold provided.
/// @param assets The assets provided by the redemption
/// @param minAssets The min asset threshold requested
error MinAssetsNotReached(uint256 assets, uint256 minAssets);

////////////////////////////////////////////////////////////////////////////////
// Modifiers
////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -561,6 +571,47 @@ contract PrizeVault is TwabERC20, Claimable, IERC4626, ILiquidationSource, Ownab
return _shares;
}

////////////////////////////////////////////////////////////////////////////////
// Additional Withdrawal Flows
////////////////////////////////////////////////////////////////////////////////

/// @notice Alternate flow for `IERC4626.withdraw` that reverts if the max share limit is exceeded.
/// @param _assets See `IERC4626.withdraw`
/// @param _receiver See `IERC4626.withdraw`
/// @param _owner See `IERC4626.withdraw`
/// @param _maxShares The max shares that can be burned for the withdrawal to succeed.
/// @return The amount of shares burned for the withdrawal
function withdraw(
uint256 _assets,
address _receiver,
address _owner,
uint256 _maxShares
) external returns (uint256) {
uint256 _shares = previewWithdraw(_assets);
if (_shares > _maxShares) revert MaxSharesExceeded(_shares, _maxShares);
_burnAndWithdraw(msg.sender, _receiver, _owner, _shares, _assets);
return _shares;
}

/// @notice Alternate flow for `IERC4626.redeem` that reverts if the assets returned does not reach the
/// minimum asset threshold.
/// @param _shares See `IERC4626.redeem`
/// @param _receiver See `IERC4626.redeem`
/// @param _owner See `IERC4626.redeem`
/// @param _minAssets The minimum assets that can be returned for the redemption to succeed
/// @return The amount of assets returned for the redemption
function redeem(
uint256 _shares,
address _receiver,
address _owner,
uint256 _minAssets
) external returns (uint256) {
uint256 _assets = previewRedeem(_shares);
if (_assets < _minAssets) revert MinAssetsNotReached(_assets, _minAssets);
_burnAndWithdraw(msg.sender, _receiver, _owner, _shares, _assets);
return _assets;
}

////////////////////////////////////////////////////////////////////////////////
// Additional Accounting
////////////////////////////////////////////////////////////////////////////////
Expand Down
104 changes: 104 additions & 0 deletions test/unit/PrizeVault/WithdrawalSlippage.t.sol
@@ -0,0 +1,104 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import { IERC4626 } from "openzeppelin/token/ERC20/extensions/ERC4626.sol";
import { IERC20, UnitBaseSetup, PrizeVault } from "./UnitBaseSetup.t.sol";
import { YieldVaultMaxSetter } from "../../contracts/mock/YieldVaultMaxSetter.sol";

contract PrizeVaultWithdrawalSlippageTest is UnitBaseSetup {

/* ============ withdraw slippage test ============ */

function testWithdrawSlippage() public {
// alice deposits 100 assets and receives 100 shares
vm.startPrank(alice);
underlyingAsset.mint(alice, 100);
underlyingAsset.approve(address(vault), 100);
vault.deposit(100, alice);
vm.stopPrank();

assertEq(vault.balanceOf(alice), 100);
assertEq(vault.totalAssets(), 100);

// yield vault loses 50% of assets
vm.startPrank(address(yieldVault));
underlyingAsset.burn(address(yieldVault), 50);
vm.stopPrank();

assertEq(vault.totalAssets(), 50);

// alice should be able to withdraw up to 50 assets for 100 shares
assertEq(vault.maxWithdraw(alice), 50);
assertEq(vault.maxRedeem(alice), 100);
assertEq(vault.previewWithdraw(50), 100);

vm.startPrank(alice);
{
// make a snapshot
uint256 snap = vm.snapshot();

// should fail if 99 shares is passed as the limit
vm.expectRevert(abi.encodeWithSelector(PrizeVault.MaxSharesExceeded.selector, 100, 99));
vault.withdraw(50, alice, alice, 99);

// should succeed if 100 shares is passed as the limit
vm.revertTo(snap);
uint256 shares = vault.withdraw(50, alice, alice, 100);
assertEq(shares, 100);

// should succeed if 101 shares is passed as the limit
vm.revertTo(snap);
shares = vault.withdraw(50, alice, alice, 101);
assertEq(shares, 100); // still only uses 100
}
vm.stopPrank();
}

/* ============ redeem slippage test ============ */

function testRedeemSlippage() public {
// alice deposits 100 assets and receives 100 shares
vm.startPrank(alice);
underlyingAsset.mint(alice, 100);
underlyingAsset.approve(address(vault), 100);
vault.deposit(100, alice);
vm.stopPrank();

assertEq(vault.balanceOf(alice), 100);
assertEq(vault.totalAssets(), 100);

// yield vault loses 50% of assets
vm.startPrank(address(yieldVault));
underlyingAsset.burn(address(yieldVault), 50);
vm.stopPrank();

assertEq(vault.totalAssets(), 50);

// alice should be able to redeem up to 100 shares for 50 assets
assertEq(vault.maxWithdraw(alice), 50);
assertEq(vault.maxRedeem(alice), 100);
assertEq(vault.previewRedeem(100), 50);

vm.startPrank(alice);
{
// make a snapshot
uint256 snap = vm.snapshot();

// should fail if 51 assets is passed as the threshold
vm.expectRevert(abi.encodeWithSelector(PrizeVault.MinAssetsNotReached.selector, 50, 51));
vault.redeem(100, alice, alice, 51);

// should succeed if 50 assets is passed as the threshold
vm.revertTo(snap);
uint256 assets = vault.redeem(100, alice, alice, 50);
assertEq(assets, 50);

// should succeed if 49 assets is passed as the threshold
vm.revertTo(snap);
assets = vault.redeem(100, alice, alice, 49);
assertEq(assets, 50); // still returns 50 assets
}
vm.stopPrank();
}

}