Skip to content

Commit

Permalink
Merge pull request #94 from GenerationSoftware/gen-1221-m-274-slippag…
Browse files Browse the repository at this point in the history
…e-protection-functions

Add slippage protection functions
  • Loading branch information
trmid committed Mar 21, 2024
2 parents 3f3aade + c3e471c commit 72af610
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 0 deletions.
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();
}

}

0 comments on commit 72af610

Please sign in to comment.