diff --git a/.gas-snapshot b/.gas-snapshot index 9b41ade..8fac620 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,5 +1,5 @@ -PufETHTest:testFail_redeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 627836, ~: 630088) -PufETHTest:testFail_withdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 633453, ~: 635382) +PufETHTest:testFail_redeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 627787, ~: 630010) +PufETHTest:testFail_withdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 633476, ~: 635685) PufETHTest:test_RT_deposit_redeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 2369, ~: 2369) PufETHTest:test_RT_deposit_withdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 2391, ~: 2391) PufETHTest:test_RT_mint_redeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 2347, ~: 2347) @@ -8,33 +8,33 @@ PufETHTest:test_RT_redeem_deposit((address[4],uint256[4],uint256[4],int256),uint PufETHTest:test_RT_redeem_mint((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 2346, ~: 2346) PufETHTest:test_RT_withdraw_deposit((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 2389, ~: 2389) PufETHTest:test_RT_withdraw_mint((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 2347, ~: 2347) -PufETHTest:test_asset((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 479503, ~: 483489) -PufETHTest:test_convertToAssets((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 489830, ~: 492125) -PufETHTest:test_convertToShares((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 489200, ~: 491796) -PufETHTest:test_deposit((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 531395, ~: 536001) +PufETHTest:test_asset((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 479542, ~: 483496) +PufETHTest:test_convertToAssets((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 489542, ~: 492229) +PufETHTest:test_convertToShares((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 488969, ~: 491800) +PufETHTest:test_deposit((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 531477, ~: 536026) PufETHTest:test_erc4626_interface() (gas: 238648) -PufETHTest:test_maxDeposit((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 479486, ~: 483473) -PufETHTest:test_maxMint((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 479422, ~: 483409) -PufETHTest:test_maxRedeem((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 479635, ~: 483621) -PufETHTest:test_maxWithdraw((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 482889, ~: 486788) -PufETHTest:test_mint((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 537903, ~: 542179) -PufETHTest:test_previewDeposit((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 530409, ~: 532880) -PufETHTest:test_previewMint((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 536949, ~: 539261) +PufETHTest:test_maxDeposit((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 479526, ~: 483479) +PufETHTest:test_maxMint((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 479462, ~: 483415) +PufETHTest:test_maxRedeem((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 479675, ~: 483628) +PufETHTest:test_maxWithdraw((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 482918, ~: 486768) +PufETHTest:test_mint((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 537901, ~: 542159) +PufETHTest:test_previewDeposit((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 530119, ~: 532895) +PufETHTest:test_previewMint((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 536462, ~: 539119) PufETHTest:test_previewRedeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 2390, ~: 2390) PufETHTest:test_previewWithdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 2346, ~: 2346) PufETHTest:test_redeem((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 2345, ~: 2345) PufETHTest:test_roles_setup() (gas: 99794) -PufETHTest:test_totalAssets((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 481627, ~: 485612) +PufETHTest:test_totalAssets((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 481667, ~: 485618) PufETHTest:test_withdraw((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 2369, ~: 2369) -PufferDepositorMainnetForkTest:test_stETH_approve_deposit() (gas: 321650) -PufferDepositorMainnetForkTest:test_stETH_approve_deposit_to_bob() (gas: 282603) -PufferDepositorMainnetForkTest:test_stETH_permit_deposit() (gas: 352339) -PufferDepositorMainnetForkTest:test_stETH_permit_deposit_to_bob() (gas: 314962) -PufferDepositorMainnetForkTest:test_wstETH_approve_deposit() (gas: 266346) -PufferDepositorMainnetForkTest:test_wstETH_approve_deposit_to_bob() (gas: 272144) -PufferDepositorMainnetForkTest:test_wstETH_permit_deposit() (gas: 291857) -PufferDepositorMainnetForkTest:test_wstETH_permit_deposit_to_bob() (gas: 301396) -PufferTest:test_1inch_complex_swap() (gas: 21829891) +PufferDepositorV2ForkTest:test_stETH_approve_deposit() (gas: 321650) +PufferDepositorV2ForkTest:test_stETH_approve_deposit_to_bob() (gas: 282603) +PufferDepositorV2ForkTest:test_stETH_permit_deposit() (gas: 352339) +PufferDepositorV2ForkTest:test_stETH_permit_deposit_to_bob() (gas: 314962) +PufferDepositorV2ForkTest:test_wstETH_approve_deposit() (gas: 266346) +PufferDepositorV2ForkTest:test_wstETH_approve_deposit_to_bob() (gas: 272144) +PufferDepositorV2ForkTest:test_wstETH_permit_deposit() (gas: 291857) +PufferDepositorV2ForkTest:test_wstETH_permit_deposit_to_bob() (gas: 301396) +PufferTest:test_1inch_complex_swap() (gas: 22091412) PufferTest:test_ape_to_pufETH() (gas: 468864) PufferTest:test_conversions_and_deposit_to_el() (gas: 1754220) PufferTest:test_deposit_stETH_permit() (gas: 315987) @@ -49,20 +49,65 @@ PufferTest:test_lido_withdrawal_dos() (gas: 696274) PufferTest:test_minting_and_lido_rebasing() (gas: 1039391) PufferTest:test_swap_1inch() (gas: 439801) PufferTest:test_swap_1inch_permit() (gas: 431084) -PufferTest:test_upgrade_to_mainnet() (gas: 7110283) -PufferTest:test_usdc_permit_upgrade() (gas: 21626461) +PufferTest:test_upgrade_to_mainnet() (gas: 7411243) +PufferTest:test_usdc_permit_upgrade() (gas: 21887982) PufferTest:test_usdc_to_pufETH() (gas: 513797) PufferTest:test_usdc_to_pufETH_permit() (gas: 516169) PufferTest:test_usdt_to_pufETH() (gas: 511286) PufferTest:test_withdraw_from_eigenLayer() (gas: 659458) PufferTest:test_withdraw_from_eigenLayer_dos() (gas: 888388) PufferTest:test_zero_stETH_deposit() (gas: 225443) -PufferVaultV2ForkTest:test_deposit() (gas: 254761) -PufferVaultV2ForkTest:test_eth_weth_stETH_deposits() (gas: 426745) -PufferVaultV2ForkTest:test_max_deposit() (gas: 52161) -PufferVaultV2ForkTest:test_max_withdrawal() (gas: 252761) -PufferVaultV2ForkTest:test_mint() (gas: 274898) -PufferVaultV2ForkTest:test_sanity() (gas: 122645) +PufferVaultV2ForkTest:test_burn() (gas: 106659) +PufferVaultV2ForkTest:test_change_withdrawal_limit() (gas: 785551) +PufferVaultV2ForkTest:test_deposit() (gas: 318955) +PufferVaultV2ForkTest:test_deposit_fails_when_not_enough_funds() (gas: 298835) +PufferVaultV2ForkTest:test_eth_weth_stETH_deposits() (gas: 491344) +PufferVaultV2ForkTest:test_max_deposit() (gas: 52252) +PufferVaultV2ForkTest:test_max_withdrawal() (gas: 260397) +PufferVaultV2ForkTest:test_mint() (gas: 339225) +PufferVaultV2ForkTest:test_redeem_fails_if_no_eth_seeded() (gas: 212427) +PufferVaultV2ForkTest:test_redeem_fails_if_owner_is_not_caller() (gas: 723141) +PufferVaultV2ForkTest:test_redeem_succeeds_if_seeded_with_eth() (gas: 866934) +PufferVaultV2ForkTest:test_redeem_succeeds_with_allowance() (gas: 768074) +PufferVaultV2ForkTest:test_redeem_transfers_to_receiver() (gas: 740717) +PufferVaultV2ForkTest:test_redemption_fee() (gas: 925832) +PufferVaultV2ForkTest:test_sanity() (gas: 152728) +PufferVaultV2ForkTest:test_setDailyWithdrawalLimit() (gas: 247763) +PufferVaultV2ForkTest:test_set_exit_fee_change() (gas: 923090) +PufferVaultV2ForkTest:test_transferETH() (gas: 696772) +PufferVaultV2ForkTest:test_transferETH_with_weth_liquidity() (gas: 347144) +PufferVaultV2ForkTest:test_withdraw_fee() (gas: 893821) +PufferVaultV2ForkTest:test_withdrawal() (gas: 926616) +PufferVaultV2ForkTest:test_withdrawal_fails_if_owner_is_not_caller() (gas: 723192) +PufferVaultV2ForkTest:test_withdrawal_fails_when_exceeding_maximum() (gas: 682553) +PufferVaultV2ForkTest:test_withdrawal_succeeds_with_allowance() (gas: 768138) +PufferVaultV2ForkTest:test_withdrawal_transfers_to_receiver() (gas: 740805) +PufferVaultV2Property:testFail_redeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 647344, ~: 648411) +PufferVaultV2Property:testFail_withdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 655177, ~: 655297) +PufferVaultV2Property:test_RT_deposit_redeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 554145, ~: 554649) +PufferVaultV2Property:test_RT_deposit_withdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 554668, ~: 555210) +PufferVaultV2Property:test_RT_mint_redeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 561851, ~: 562312) +PufferVaultV2Property:test_RT_mint_withdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 561963, ~: 562441) +PufferVaultV2Property:test_RT_redeem_deposit((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 555072, ~: 555512) +PufferVaultV2Property:test_RT_redeem_mint((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 556767, ~: 557294) +PufferVaultV2Property:test_RT_withdraw_deposit((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 561341, ~: 561517) +PufferVaultV2Property:test_RT_withdraw_mint((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 562886, ~: 563065) +PufferVaultV2Property:test_asset((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 475534, ~: 475557) +PufferVaultV2Property:test_convertToAssets((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 486722, ~: 486684) +PufferVaultV2Property:test_convertToShares((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 486349, ~: 486577) +PufferVaultV2Property:test_deposit((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 528983, ~: 529152) +PufferVaultV2Property:test_maxDeposit((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 475509, ~: 475532) +PufferVaultV2Property:test_maxMint((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 475427, ~: 475450) +PufferVaultV2Property:test_maxRedeem((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 484392, ~: 484416) +PufferVaultV2Property:test_maxWithdraw((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 484485, ~: 484504) +PufferVaultV2Property:test_mint((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 536599, ~: 536658) +PufferVaultV2Property:test_previewDeposit((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 526837, ~: 527236) +PufferVaultV2Property:test_previewMint((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 534587, ~: 534913) +PufferVaultV2Property:test_previewRedeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 546417, ~: 546820) +PufferVaultV2Property:test_previewWithdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 552694, ~: 552806) +PufferVaultV2Property:test_redeem((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 547967, ~: 548486) +PufferVaultV2Property:test_totalAssets((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 479051, ~: 479074) +PufferVaultV2Property:test_withdraw((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 554277, ~: 554418) TimelockTest:test_cancel_reverts_if_caller_unauthorized(address) (runs: 256, μ: 11272, ~: 11272) TimelockTest:test_cancel_transaction() (gas: 38801) TimelockTest:test_change_pauser() (gas: 23527) diff --git a/docs/PufferVaultV2.md b/docs/PufferVaultV2.md index d49d06c..1d147b9 100644 --- a/docs/PufferVaultV2.md +++ b/docs/PufferVaultV2.md @@ -38,12 +38,13 @@ The PufferVault maintains the addresses of important contracts related to EigenL * `uint256 lidoLockedETH`: The amount of ETH the Puffer Protocol has locked inside of Lido * `uint256 eigenLayerPendingWithdrawalSharesAmount`: The amount of stETH shares the Puffer vault has pending for withdrawal from EigenLayer * `bool isLidoWithdrawal`: Deprecated from PufferVault version 1 -* `EnumerableSet.UintSet lidoWithdrawals`: Tracks the withdrawal request IDs from Lido +* `EnumerableSet.UintSet lidoWithdrawals`: Deprecated from PufferVault version 1 * `EnumerableSet.Bytes32Set eigenLayerWithdrawals`: Tracks withdrawalRoots from EigenLayer withdrawals * `EnumerableMap.UintToUintMap lidoWithdrawalAmounts`: Tracks the amounts of corresponding to each Lido withdrawal * `uint96 dailyAssetsWithdrawalLimit`: The maximum assets (wETH) that can be withdrawn from the vault per day * `uint96 assetsWithdrawnToday`: The amount of assets (wETH) that has been withdrawn today * `uint64 lastWithdrawalDay`: Tracks when the day ends to reset `assetsWithdrawnToday` +* `uint256 exitFeeBasisPoints`: Penalty when withdrawing to mitigate oracle sandwich attacks #### PufferVaultV2 * `IWETH internal immutable _WETH`: Address of wrapped ETH contract (wETH) diff --git a/script/GenerateAccessManagerCallData.sol b/script/GenerateAccessManagerCallData.sol index 63268bf..3e39b7a 100644 --- a/script/GenerateAccessManagerCallData.sol +++ b/script/GenerateAccessManagerCallData.sol @@ -20,11 +20,7 @@ import { PUBLIC_ROLE, ROLE_ID_DAO, ROLE_ID_PUFFER_PROTOCOL, ADMIN_ROLE, ROLE_ID_ * 3. timelock.executeTransaction(address(accessManager), encodedMulticall, 1) */ contract GenerateAccessManagerCallData is Script { - function run(address pufferVaultProxy, address pufferDepositorProxy, address pufferProtocolProxy) - public - pure - returns (bytes memory) - { + function run(address pufferVaultProxy, address pufferDepositorProxy) public pure returns (bytes memory) { bytes[] memory calldatas = new bytes[](6); // Combine the two calldatas @@ -69,6 +65,7 @@ contract GenerateAccessManagerCallData is Script { function _getProtocolSelectorsCalldata(address pufferVaultProxy) internal pure returns (bytes memory) { // Puffer Protocol only + // PufferProtocol will get `ROLE_ID_PUFFER_PROTOCOL` when it's deployed bytes4[] memory protocolSelectors = new bytes4[](2); protocolSelectors[0] = PufferVaultV2.transferETH.selector; protocolSelectors[1] = PufferVaultV2.burn.selector; diff --git a/src/NoImplementation.sol b/src/NoImplementation.sol index 3972faf..6a88416 100644 --- a/src/NoImplementation.sol +++ b/src/NoImplementation.sol @@ -10,7 +10,7 @@ contract NoImplementation is UUPSUpgradeable { upgrader = msg.sender; } - function _authorizeUpgrade(address newImplementation) internal virtual override { + function _authorizeUpgrade(address) internal virtual override { // solhint-disable-next-line custom-errors require(msg.sender == upgrader, "Unauthorized"); // anybody can steal this proxy diff --git a/src/PufferVaultStorage.sol b/src/PufferVaultStorage.sol index b02f2b9..8c4f840 100644 --- a/src/PufferVaultStorage.sol +++ b/src/PufferVaultStorage.sol @@ -22,14 +22,16 @@ abstract contract PufferVaultStorage { // 6 Slots for Redemption logic uint256 lidoLockedETH; uint256 eigenLayerPendingWithdrawalSharesAmount; - bool isLidoWithdrawal; - EnumerableSet.UintSet lidoWithdrawals; + bool isLidoWithdrawal; // Not in use in PufferVaultV2 + EnumerableSet.UintSet lidoWithdrawals; // Not in use in PufferVaultV2 EnumerableSet.Bytes32Set eigenLayerWithdrawals; EnumerableMap.UintToUintMap lidoWithdrawalAmounts; // 1 Slot for daily withdrawal limits uint96 dailyAssetsWithdrawalLimit; uint96 assetsWithdrawnToday; uint64 lastWithdrawalDay; + // 1 slot for withdrawal fee + uint256 exitFeeBasisPoints; } // keccak256(abi.encode(uint256(keccak256("puffervault.storage")) - 1)) & ~bytes32(uint256(0xff)) diff --git a/src/PufferVaultV2.sol b/src/PufferVaultV2.sol index dd21eb3..132fa04 100644 --- a/src/PufferVaultV2.sol +++ b/src/PufferVaultV2.sol @@ -7,41 +7,23 @@ import { ILidoWithdrawalQueue } from "./interface/Lido/ILidoWithdrawalQueue.sol" import { IEigenLayer } from "./interface/EigenLayer/IEigenLayer.sol"; import { IStrategy } from "./interface/EigenLayer/IStrategy.sol"; import { IWETH } from "./interface/Other/IWETH.sol"; +import { IPufferVaultV2 } from "./interface/IPufferVaultV2.sol"; import { IPufferOracle } from "./interface/IPufferOracle.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { EnumerableMap } from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; /** * @title PufferVaultV2 * @author Puffer Finance * @custom:security-contact security@puffer.fi */ -contract PufferVaultV2 is PufferVault { +contract PufferVaultV2 is PufferVault, IPufferVaultV2 { using SafeERC20 for address; using EnumerableMap for EnumerableMap.UintToUintMap; + using Math for uint256; - /** - * @dev Thrown if the Vault doesn't have ETH liquidity to transfer to PufferModule - */ - error ETHTransferFailed(); - - /** - * Emitted when the daily withdrawal limit is set - * @dev Signature: 0x8d5f7487ce1fd25059bd15204a55ea2c293160362b849a6f9244aec7d5a3700b - */ - event DailyWithdrawalLimitSet(uint96 oldLimit, uint96 newLimit); - - /** - * Emitted when the Vault transfers ETH to a specified address - * @dev Signature: 0xba7bb5aa419c34d8776b86cc0e9d41e72d74a893a511f361a11af6c05e920c3d - */ - event TransferredETH(address indexed to, uint256 amount); - - /** - * Emitted when the Vault gets ETH from Lido - * @dev Signature: 0xb5cd6ba4df0e50a9991fc91db91ea56e2f134e498a70fc7224ad61d123e5bbb0 - */ - event LidoWithdrawal(uint256 expectedWithdrawal, uint256 actualWithdrawal); + uint256 private constant _BASIS_POINT_SCALE = 1e4; /** * @dev The Wrapped Ethereum ERC20 token @@ -66,11 +48,10 @@ contract PufferVaultV2 is PufferVault { _disableInitializers(); } - // solhint-disable-next-line no-complex-fallback receive() external payable virtual override { } /** - * @notice Changes token from stETH to WETH + * @notice Changes underlying asset from stETH to WETH */ function initialize() public reinitializer(2) { // In this initialization, we swap out the underlying stETH with WETH @@ -78,11 +59,12 @@ contract PufferVaultV2 is PufferVault { erc4626Storage._asset = _WETH; _setDailyWithdrawalLimit(100 ether); _updateDailyWithdrawals(0); + _setExitFeeBasisPoints(100); // 1% } /** * @dev See {IERC4626-totalAssets}. - * pufETH, the shares of the vault, will be backed primarily by the WETH asset. + * pufETH, the shares of the vault, will be backed primarily by the WETH asset. * However, at any point in time, the full backings may be a combination of stETH, WETH, and ETH. * `totalAssets()` is calculated by summing the following: * - WETH held in the vault contract @@ -108,31 +90,7 @@ contract PufferVaultV2 is PufferVault { } /** - * @notice Calculates the maximum amount of assets (WETH) that can be withdrawn by the `owner`. - * @dev This function considers both the remaining daily withdrawal limit and the `owner`'s balance. - * @param owner The address of the owner for which the maximum withdrawal amount is calculated. - * @return maxAssets The maximum amount of assets that can be withdrawn by the `owner`. - */ - function maxWithdraw(address owner) public view virtual override returns (uint256 maxAssets) { - uint256 remainingAssets = getRemainingAssetsDailyWithdrawalLimit(); - uint256 maxUserAssets = previewRedeem(balanceOf(owner)); - return remainingAssets < maxUserAssets ? remainingAssets : maxUserAssets; - } - - /** - * @notice Calculates the maximum amount of shares (pufETH) that can be redeemed by the `owner`. - * @dev This function considers both the remaining daily withdrawal limit in terms of assets and converts it to shares, and the `owner`'s share balance. - * @param owner The address of the owner for which the maximum redeemable shares are calculated. - * @return maxShares The maximum amount of shares that can be redeemed by the `owner`. - */ - function maxRedeem(address owner) public view virtual override returns (uint256 maxShares) { - uint256 remainingShares = previewWithdraw(getRemainingAssetsDailyWithdrawalLimit()); - uint256 userShares = balanceOf(owner); - return remainingShares < userShares ? remainingShares : userShares; - } - - /** - * @notice Withdrawals WETH assets from the vault, burning the `owner`'s (pufETH) shares. + * @notice Withdrawals WETH assets from the vault, burning the `owner`'s (pufETH) shares. * The caller of this function does not have to be the `owner` if the `owner` has approved the caller to spend their pufETH. * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol * Copied the original ERC4626 code back to override `PufferVault` + wrap ETH logic @@ -158,14 +116,13 @@ contract PufferVaultV2 is PufferVault { _wrapETH(assets); uint256 shares = previewWithdraw(assets); - // solhint-disable-next-line func-named-parameters - _withdraw(_msgSender(), receiver, owner, assets, shares); + _withdraw({ caller: _msgSender(), receiver: receiver, owner: owner, assets: assets, shares: shares }); return shares; } /** - * @notice Redeems (pufETH) `shares` to receive (WETH) assets from the vault, burning the `owner`'s (pufETH) `shares`. + * @notice Redeems (pufETH) `shares` to receive (WETH) assets from the vault, burning the `owner`'s (pufETH) `shares`. * The caller of this function does not have to be the `owner` if the `owner` has approved the caller to spend their pufETH. * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol * Copied the original ERC4626 code back to override `PufferVault` + wrap ETH logic @@ -192,17 +149,14 @@ contract PufferVaultV2 is PufferVault { _wrapETH(assets); - // solhint-disable-next-line func-named-parameters - _withdraw(_msgSender(), receiver, owner, assets, shares); + _withdraw({ caller: _msgSender(), receiver: receiver, owner: owner, assets: assets, shares: shares }); return assets; } /** - * @notice Deposits native ETH into the Puffer Vault + * @inheritdoc IPufferVaultV2 * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol - * @param receiver The recipient of pufETH tokens - * @return shares The amount of pufETH received from the deposit */ function depositETH(address receiver) public payable virtual restricted returns (uint256) { uint256 maxAssets = maxDeposit(receiver); @@ -218,11 +172,8 @@ contract PufferVaultV2 is PufferVault { } /** - * @notice Deposits stETH into the Puffer Vault + * @inheritdoc IPufferVaultV2 * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol - * @param assets The amount of stETH to deposit - * @param receiver The recipient of pufETH tokens - * @return shares The amount of pufETH received from the deposit */ function depositStETH(uint256 assets, address receiver) public virtual restricted returns (uint256) { uint256 maxAssets = maxDeposit(receiver); @@ -349,10 +300,17 @@ contract PufferVaultV2 is PufferVault { } /** - * @notice Returns the remaining assets that can be withdrawn today - * @return The remaining assets that can be withdrawn today + * @param newExitFeeBasisPoints is the new exit fee basis points + * @dev Restricted to the DAO + */ + function setExitFeeBasisPoints(uint256 newExitFeeBasisPoints) external restricted { + _setExitFeeBasisPoints(newExitFeeBasisPoints); + } + + /** + * @inheritdoc IPufferVaultV2 */ - function getRemainingAssetsDailyWithdrawalLimit() public view virtual returns (uint96) { + function getRemainingAssetsDailyWithdrawalLimit() public view virtual returns (uint256) { VaultStorage storage $ = _getPufferVaultStorage(); uint96 dailyAssetsWithdrawalLimit = $.dailyAssetsWithdrawalLimit; uint96 assetsWithdrawnToday = $.assetsWithdrawnToday; @@ -360,14 +318,86 @@ contract PufferVaultV2 is PufferVault { if (dailyAssetsWithdrawalLimit < assetsWithdrawnToday) { return 0; } + + // If we are in a new day, return the full daily limit + if ($.lastWithdrawalDay < block.timestamp / 1 days) { + return dailyAssetsWithdrawalLimit; + } + return dailyAssetsWithdrawalLimit - assetsWithdrawnToday; } + /** + * @notice Calculates the maximum amount of assets (WETH) that can be withdrawn by the `owner`. + * @dev This function considers both the remaining daily withdrawal limit and the `owner`'s balance. + * See {IERC4626-maxWithdraw} + * @param owner The address of the owner for which the maximum withdrawal amount is calculated. + * @return maxAssets The maximum amount of assets that can be withdrawn by the `owner`. + */ + function maxWithdraw(address owner) public view virtual override returns (uint256 maxAssets) { + uint256 remainingAssets = getRemainingAssetsDailyWithdrawalLimit(); + uint256 maxUserAssets = previewRedeem(balanceOf(owner)); + return remainingAssets < maxUserAssets ? remainingAssets : maxUserAssets; + } + + /** + * @notice Calculates the maximum amount of shares (pufETH) that can be redeemed by the `owner`. + * @dev This function considers both the remaining daily withdrawal limit in terms of assets and converts it to shares, and the `owner`'s share balance. + * See {IERC4626-maxRedeem} + * @param owner The address of the owner for which the maximum redeemable shares are calculated. + * @return maxShares The maximum amount of shares that can be redeemed by the `owner`. + */ + function maxRedeem(address owner) public view virtual override returns (uint256 maxShares) { + uint256 remainingShares = previewWithdraw(getRemainingAssetsDailyWithdrawalLimit()); + uint256 userShares = balanceOf(owner); + return remainingShares < userShares ? remainingShares : userShares; + } + + /** + * @dev Preview adding an exit fee on withdraw. See {IERC4626-previewWithdraw}. + */ + function previewWithdraw(uint256 assets) public view virtual override returns (uint256) { + uint256 fee = _feeOnRaw(assets, getExitFeeBasisPoints()); + return super.previewWithdraw(assets + fee); + } + + /** + * @dev Preview taking an exit fee on redeem. See {IERC4626-previewRedeem}. + */ + function previewRedeem(uint256 shares) public view virtual override returns (uint256) { + uint256 assets = super.previewRedeem(shares); + return assets - _feeOnTotal(assets, getExitFeeBasisPoints()); + } + + /** + * @inheritdoc IPufferVaultV2 + */ + function getExitFeeBasisPoints() public view virtual returns (uint256) { + VaultStorage storage $ = _getPufferVaultStorage(); + return $.exitFeeBasisPoints; + } + + /** + * @dev Calculates the fees that should be added to an amount `assets` that does not already include fees. + * Used in {IERC4626-withdraw}. + */ + function _feeOnRaw(uint256 assets, uint256 feeBasisPoints) internal pure virtual returns (uint256) { + return assets.mulDiv(feeBasisPoints, _BASIS_POINT_SCALE, Math.Rounding.Ceil); + } + + /** + * @dev Calculates the fee part of an amount `assets` that already includes fees. + * Used in {IERC4626-redeem}. + */ + function _feeOnTotal(uint256 assets, uint256 feeBasisPoints) internal pure virtual returns (uint256) { + return assets.mulDiv(feeBasisPoints, feeBasisPoints + _BASIS_POINT_SCALE, Math.Rounding.Ceil); + } + /** * @notice Wraps the vault's ETH balance to WETH. * @dev Used to provide WETH liquidity */ - function _wrapETH(uint256 assets) internal { + function _wrapETH(uint256 assets) internal virtual { uint256 wethBalance = _WETH.balanceOf(address(this)); if (wethBalance < assets) { @@ -379,7 +409,7 @@ contract PufferVaultV2 is PufferVault { * @notice Updates the amount of assets (WETH) withdrawn today * @param withdrawalAmount is the assets (WETH) amount */ - function _updateDailyWithdrawals(uint256 withdrawalAmount) internal { + function _updateDailyWithdrawals(uint256 withdrawalAmount) internal virtual { VaultStorage storage $ = _getPufferVaultStorage(); // Check if it's a new day to reset the withdrawal count @@ -395,15 +425,29 @@ contract PufferVaultV2 is PufferVault { * @notice Updates the maximum amount of assets (WETH) that can be withdrawn daily * @param newLimit is the assets (WETH) amount */ - function _setDailyWithdrawalLimit(uint96 newLimit) internal { + function _setDailyWithdrawalLimit(uint96 newLimit) internal virtual { VaultStorage storage $ = _getPufferVaultStorage(); emit DailyWithdrawalLimitSet($.dailyAssetsWithdrawalLimit, newLimit); $.dailyAssetsWithdrawalLimit = newLimit; } + /** + * @notice Updates the exit fee basis points + * @dev 200 Basis points = 2% is the maximum exit fee + */ + function _setExitFeeBasisPoints(uint256 newExitFeeBasisPoints) internal virtual { + VaultStorage storage $ = _getPufferVaultStorage(); + // 2% is the maximum exit fee + if (newExitFeeBasisPoints > 200) { + revert InvalidExitFeeBasisPoints(); + } + emit ExitFeeBasisPointsSet($.exitFeeBasisPoints, newExitFeeBasisPoints); + $.exitFeeBasisPoints = newExitFeeBasisPoints; + } + function _authorizeUpgrade(address newImplementation) internal virtual override restricted { } - function _getERC4626StorageInternal() internal pure returns (ERC4626Storage storage $) { + function _getERC4626StorageInternal() private pure returns (ERC4626Storage storage $) { // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ERC4626")) - 1)) & ~bytes32(uint256(0xff)) // solhint-disable-next-line no-inline-assembly assembly { diff --git a/src/interface/IPufferOracleV2.sol b/src/interface/IPufferOracleV2.sol index 7d4e4f6..73d8047 100644 --- a/src/interface/IPufferOracleV2.sol +++ b/src/interface/IPufferOracleV2.sol @@ -38,7 +38,7 @@ interface IPufferOracleV2 is IPufferOracle { function getLastUpdate() external view returns (uint256); /** - * @notice Increases the `_lockedETH` variable on the PufferOracle by 32 ETH to account for a new deposit. + * @notice Increases the `_lockedETH` variable on the PufferOracle by 32 ETH to account for a new deposit. * It is called when the Beacon chain receives a new deposit from the PufferProtocol. * The PufferVault's balance will simultaneously decrease by 32 ETH as the deposit is made. * The purpose is to keep the PufferVault totalAssets amount in sync between proof-of-reserves updates. diff --git a/src/interface/IPufferVaultV2.sol b/src/interface/IPufferVaultV2.sol new file mode 100644 index 0000000..d531b3f --- /dev/null +++ b/src/interface/IPufferVaultV2.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import { IPufferVault } from "./IPufferVault.sol"; + +/** + * @title IPufferVaultV2 + * @author Puffer Finance + * @custom:security-contact security@puffer.fi + */ +interface IPufferVaultV2 is IPufferVault { + /** + * @dev Thrown if the Vault doesn't have ETH liquidity to transfer to PufferModule + */ + error ETHTransferFailed(); + + /** + * @dev Thrown if the new exit fee basis points is invalid + */ + error InvalidExitFeeBasisPoints(); + + /** + * Emitted when the daily withdrawal limit is set + * @dev Signature: 0x8d5f7487ce1fd25059bd15204a55ea2c293160362b849a6f9244aec7d5a3700b + */ + event DailyWithdrawalLimitSet(uint96 oldLimit, uint96 newLimit); + + /** + * Emitted when the Vault transfers ETH to a specified address + * @dev Signature: 0xba7bb5aa419c34d8776b86cc0e9d41e72d74a893a511f361a11af6c05e920c3d + */ + event TransferredETH(address indexed to, uint256 amount); + + /** + * Emitted when the Vault transfers ETH to a specified address + * @dev Signature: 0xb10a745484e9798f0014ea028d76169706f92e7eea5d5bb66001c1400769785d + */ + event ExitFeeBasisPointsSet(uint256 previousFee, uint256 newFee); + + /** + * Emitted when the Vault gets ETH from Lido + * @dev Signature: 0xb5cd6ba4df0e50a9991fc91db91ea56e2f134e498a70fc7224ad61d123e5bbb0 + */ + event LidoWithdrawal(uint256 expectedWithdrawal, uint256 actualWithdrawal); + + /** + * @notice Returns the current exit fee basis points + */ + function getExitFeeBasisPoints() external view returns (uint256); + + /** + * @notice Returns the remaining assets that can be withdrawn today + * @return The remaining assets that can be withdrawn today + */ + function getRemainingAssetsDailyWithdrawalLimit() external view returns (uint256); + + /** + * @notice Deposits native ETH into the Puffer Vault + * @param receiver The recipient of pufETH tokens + * @return shares The amount of pufETH received from the deposit + */ + function depositETH(address receiver) external payable returns (uint256); + + /** + * @notice Deposits stETH into the Puffer Vault + * @param assets The amount of stETH to deposit + * @param receiver The recipient of pufETH tokens + * @return shares The amount of pufETH received from the deposit + */ + function depositStETH(uint256 assets, address receiver) external returns (uint256); +} diff --git a/test/Integration/PufferTest.integration.t.sol b/test/Integration/PufferTest.integration.t.sol index 51ece95..11e3016 100644 --- a/test/Integration/PufferTest.integration.t.sol +++ b/test/Integration/PufferTest.integration.t.sol @@ -228,9 +228,8 @@ contract PufferTest is Test { ); // Setup access - address mockProtocol = makeAddr("mockProtocol"); bytes memory encodedMulticall = - new GenerateAccessManagerCallData().run(address(pufferVault), address(pufferDepositor), mockProtocol); + new GenerateAccessManagerCallData().run(address(pufferVault), address(pufferDepositor)); // Timelock is the owner of the AccessManager timelock.executeTransaction(address(accessManager), encodedMulticall, 1); @@ -383,7 +382,7 @@ contract PufferTest is Test { assertEq(minted, 0, "got 0 back"); } - function test_upgrade_to_mainnet() public giveToken(MAKER_VAULT, address(_WETH), eve, 100 ether) { + function test_upgrade_to_mainnet() public giveToken(MAKER_VAULT, address(_WETH), eve, 10 ether) { // Test pre-mainnet version test_minting_and_lido_rebasing(); @@ -395,18 +394,36 @@ contract PufferTest is Test { vm.startPrank(eve); SafeERC20.safeIncreaseAllowance(_WETH, address(pufferVault), type(uint256).max); - uint256 wethBeforeEve = _WETH.balanceOf(eve); + uint256 pufETHMinted = pufferVault.deposit(10 ether, eve); - uint256 pufETHMinted = pufferVault.deposit(100 ether, eve); + assertEq(pufferVault.totalAssets(), assetsBefore + 10 ether, "Previous assets should increase"); - assertEq(pufferVault.totalAssets(), assetsBefore + 100 ether, "Previous assets should increase"); + PufferVaultV2(payable(address(pufferVault))).getRemainingAssetsDailyWithdrawalLimit(); - pufferVault.withdraw(pufETHMinted, eve, eve); + pufferVault.balanceOf(eve); + uint256 maxWithdraw = pufferVault.maxWithdraw(eve); - // 0.01% is the max delta because of the rounding - // Real delta is 0.009900175912953700 % - assertApproxEqRel(_WETH.balanceOf(eve), wethBeforeEve, 0.0001e18, "eve weth after withdrawal"); - assertApproxEqRel(pufferVault.totalAssets(), assetsBefore, 0.0001e18, "should have the same amount"); + uint256 assetsValue = pufferVault.convertToAssets(pufETHMinted); + assertApproxEqAbs(assetsValue, 10 ether, 1, "convertToAssets matches the original deposited amount"); + + // IERC4626 natspec says: + /// NOTE: any unfavorable discrepancy between convertToAssets and previewRedeem SHOULD be considered slippage in + /// share price or some other type of condition, meaning the depositor will lose assets by redeeming. + + assertLt(maxWithdraw, pufETHMinted, "max withdraw should is smaller because of the withdrawal fee"); + + pufferVault.withdraw(maxWithdraw, eve, eve); + + // Alice got less than she deposited ~ -1% less + assertEq(_WETH.balanceOf(eve), 9.900990099009900989 ether, "eve weth after withdrawal"); + + // Deposited 10 ETH, got back ~9.9 ETH + uint256 assetsDif = 10 ether - _WETH.balanceOf(eve); + + // The rest stays in the vault + assertEq( + pufferVault.totalAssets(), assetsBefore + assetsDif, "should have a little more because alice got 1% less" + ); } function test_minting_and_lido_rebasing() diff --git a/test/Integration/PufferVaultV2.fork.t.sol b/test/Integration/PufferVaultV2.fork.t.sol index 7892737..67ae2ec 100644 --- a/test/Integration/PufferVaultV2.fork.t.sol +++ b/test/Integration/PufferVaultV2.fork.t.sol @@ -5,6 +5,7 @@ import { ERC4626Upgradeable } from "@openzeppelin-contracts-upgradeable/token/ER import { TestHelper } from "../TestHelper.sol"; import { IERC20 } from "openzeppelin/token/ERC20/IERC20.sol"; import { PufferVaultV2 } from "../../src/PufferVaultV2.sol"; +import { IPufferVaultV2 } from "../../src/interface/IPufferVaultV2.sol"; import { ROLE_ID_DAO, ROLE_ID_PUFFER_PROTOCOL } from "../../script/Roles.sol"; contract PufferVaultV2ForkTest is TestHelper { @@ -20,12 +21,57 @@ contract PufferVaultV2ForkTest is TestHelper { assertEq(pufferVault.totalAssets(), 351755.122828329778282991 ether, "total assets"); assertEq(pufferVault.getRemainingAssetsDailyWithdrawalLimit(), 100 ether, "daily withdrawal limit"); assertEq(pufferVault.getELBackingEthAmount(), 341562.667703458494350801 ether, "0 EL backing eth"); // mainnet fork 19271279); + assertEq(pufferVault.getExitFeeBasisPoints(), 100, "1% withdrawal fee"); } function test_max_deposit() public giveToken(MAKER_VAULT, address(_WETH), alice, 100 ether) { assertEq(pufferVault.maxDeposit(alice), type(uint256).max, "max deposit"); } + function test_set_exit_fee_change() public { + // Get liquidity + _withdraw_stETH_from_lido(); + + // Unauthorized + vm.expectRevert(); + pufferVault.setExitFeeBasisPoints(200); + + // Default value is 1% + assertEq(pufferVault.getExitFeeBasisPoints(), 100, "1% withdrawal fee"); + + uint256 sharesRequiredBefore = pufferVault.previewWithdraw(10 ether); + + // Timelock.sol is the admin of AccessManager + vm.startPrank(address(timelock)); + vm.expectEmit(true, true, true, true); + emit IPufferVaultV2.ExitFeeBasisPointsSet(100, 200); + pufferVault.setExitFeeBasisPoints(200); + + // After + assertEq(pufferVault.getExitFeeBasisPoints(), 200, "2% withdrawal fee"); + + // Because it is a bigger fee, the shares required to withdraw 100 ETH is bigger + uint256 sharesRequiredAfter = pufferVault.previewWithdraw(10 ether); + assertGt(sharesRequiredAfter, sharesRequiredBefore, "shares required before must be bigger"); + + // Withdraw assets + vm.startPrank(pufferWhale); + uint256 sharesWithdrawn = pufferVault.withdraw(10 ether, pufferWhale, pufferWhale); + + vm.startPrank(address(timelock)); + vm.expectEmit(true, true, true, true); + emit IPufferVaultV2.ExitFeeBasisPointsSet(200, 0); + pufferVault.setExitFeeBasisPoints(0); + + assertEq(pufferVault.getExitFeeBasisPoints(), 0, "0"); + + // Withdraw the same amount of assets again + vm.startPrank(pufferWhale); + uint256 sharesWithdrawnAfter = pufferVault.withdraw(10 ether, pufferWhale, pufferWhale); + + assertLt(sharesWithdrawnAfter, sharesWithdrawn, "no fee = less shares needed"); + } + function test_max_withdrawal() public giveToken(MAKER_VAULT, address(_WETH), alice, 100 ether) { // Alice doesn't have any pufETH assertEq(pufferVault.maxWithdraw(alice), 0, "max withdraw"); @@ -33,8 +79,8 @@ contract PufferVaultV2ForkTest is TestHelper { // Whale has more than 100 ether, but the limit is 100 eth assertEq(pufferVault.maxWithdraw(pufferWhale), 100 ether, "max withdraw"); - // pufETH is worth more than ETH - assertEq(pufferVault.maxRedeem(pufferWhale), 99.811061309125114006 ether, "max redeem"); + // Because of the withdrawal fee, the maxRedeem is bigger than the maxWithdraw + assertEq(pufferVault.maxRedeem(pufferWhale), 100.809171922216365146 ether, "max redeem"); } function test_setDailyWithdrawalLimit() public { @@ -55,8 +101,104 @@ contract PufferVaultV2ForkTest is TestHelper { assertEq(pufferVault.getRemainingAssetsDailyWithdrawalLimit(), newLimit, "daily withdrawal limit"); // Shares amount uint256 maxRedeem = pufferVault.maxRedeem(pufferWhale); - // If we convert shares to assets, it should be equal to the new limit - assertEq(pufferVault.convertToAssets(maxRedeem), 1000 ether, "max redeem converted to assets"); + // If we convert shares to assets, it should be equal to the new limit + 10 (1% is the withdrawal fee) + assertEq(pufferVault.convertToAssets(maxRedeem), 1010 ether, "max redeem converted to assets"); + } + + function test_withdraw_fee() public { + // Get withdrawal liquidity + _withdraw_stETH_from_lido(); + + address recipient = makeAddr("assetsRecipient"); + + uint256 expectedSharesWithdrawn = pufferVault.previewWithdraw(10 ether); + + assertEq(_WETH.balanceOf(recipient), 0, "got 0 weth"); + + // Withdraw + vm.startPrank(pufferWhale); + uint256 sharesWithdrawn = pufferVault.withdraw(10 ether, recipient, pufferWhale); + vm.stopPrank(); + + // Recipient will get 10 WETH + assertEq(_WETH.balanceOf(recipient), 10 ether, "got +10 weth"); + + assertEq(expectedSharesWithdrawn, sharesWithdrawn, "must match"); + + // The exchange rate changes after the first withdrawal, because of the fee + // The second withdrawal will burn less shares than the first one + uint256 expectedShares = pufferVault.previewWithdraw(10 ether); + + assertLt(expectedShares, sharesWithdrawn, "shares must be less than previous"); + + vm.startPrank(pufferWhale); + pufferVault.redeem(expectedShares, recipient, pufferWhale); + + assertEq(_WETH.balanceOf(recipient), 20 ether, "+10 weth"); + } + + function test_redemption_fee() public { + // Get withdrawal liquidity + _withdraw_stETH_from_lido(); + + address recipient = makeAddr("assetsRecipient"); + + // This much shares will get us 10 WETH + uint256 expectedSharesWithdrawn = pufferVault.previewWithdraw(10 ether); + + uint256 expectedAssetsOut = pufferVault.previewRedeem(expectedSharesWithdrawn); + + assertEq(_WETH.balanceOf(recipient), 0, "got 0 weth"); + + // // Withdraw + vm.startPrank(pufferWhale); + uint256 assetsOut = pufferVault.redeem(expectedSharesWithdrawn, recipient, pufferWhale); + vm.stopPrank(); + + // // // Recipient will get 10 WETH + assertEq(_WETH.balanceOf(recipient), 10 ether, "got +10 weth"); + + assertEq(expectedAssetsOut, assetsOut, "must match"); + assertEq(assetsOut, 10 ether, "must match eth"); + + // The exchange rate changes slightly after the first withdrawal, because of the withdrawal fee + // The same amount of + uint256 expectedAssets = pufferVault.previewRedeem(expectedSharesWithdrawn); + + assertGt(expectedAssets, 10 ether, "second withdrawal previewRedeem"); + + vm.startPrank(pufferWhale); + pufferVault.redeem(expectedSharesWithdrawn, recipient, pufferWhale); + + uint256 recipientBalance = _WETH.balanceOf(recipient); + + assertGt(recipientBalance, 20 ether, "+10 weth"); + + // Assert the daily withdrawal limit + assertEq( + pufferVault.getRemainingAssetsDailyWithdrawalLimit(), 100 ether - recipientBalance, "daily withdrawal limit" + ); + } + + function test_daily_limit_reset() public { + _withdraw_stETH_from_lido(); + + vm.startPrank(pufferWhale); + assertEq(pufferVault.getRemainingAssetsDailyWithdrawalLimit(), 100 ether, "daily withdrawal limit"); + + assertEq(pufferVault.maxWithdraw(pufferWhale), 100 ether, "max withdraw"); + pufferVault.withdraw(50 ether, pufferWhale, pufferWhale); + + assertEq(pufferVault.getRemainingAssetsDailyWithdrawalLimit(), 50 ether, "daily withdrawal limit reduced"); + + vm.warp(block.timestamp + 1 days); + + assertEq(pufferVault.getRemainingAssetsDailyWithdrawalLimit(), 100 ether, "daily withdrawal limit reduced"); + + assertEq(pufferVault.maxWithdraw(pufferWhale), 100 ether, "max withdraw"); + pufferVault.withdraw(22 ether, pufferWhale, pufferWhale); + + assertEq(pufferVault.getRemainingAssetsDailyWithdrawalLimit(), 78 ether, "daily withdrawal limit reduced"); } function test_withdrawal() public { @@ -87,7 +229,7 @@ contract PufferVaultV2ForkTest is TestHelper { // Withdraw with alice as receiver vm.startPrank(pufferWhale); - uint256 sharesBurned = pufferVault.withdraw({ assets: 50 ether, receiver: alice, owner: pufferWhale}); + uint256 sharesBurned = pufferVault.withdraw({ assets: 50 ether, receiver: alice, owner: pufferWhale }); vm.stopPrank(); // Alice received 50 wETH @@ -112,7 +254,7 @@ contract PufferVaultV2ForkTest is TestHelper { // Alice tries to withdraw on behalf of pufferWhale vm.startPrank(alice); - uint256 sharesBurned = pufferVault.withdraw({ assets: 50 ether, receiver: alice, owner: pufferWhale}); + uint256 sharesBurned = pufferVault.withdraw({ assets: 50 ether, receiver: alice, owner: pufferWhale }); vm.stopPrank(); // Alice should receives 50 wETH @@ -132,7 +274,7 @@ contract PufferVaultV2ForkTest is TestHelper { // Alice tries to withdraw on behalf of pufferWhale vm.startPrank(alice); vm.expectRevert(); - pufferVault.withdraw({ assets: 50 ether, receiver: alice, owner: pufferWhale}); + pufferVault.withdraw({ assets: 50 ether, receiver: alice, owner: pufferWhale }); vm.stopPrank(); // Alice should not receive @@ -242,7 +384,7 @@ contract PufferVaultV2ForkTest is TestHelper { vm.startPrank(mockProtocol); vm.expectEmit(true, true, true, true); - emit PufferVaultV2.TransferredETH(mockProtocol, 10 ether); + emit IPufferVaultV2.TransferredETH(mockProtocol, 10 ether); pufferVault.transferETH(mockProtocol, 10 ether); assertEq(mockProtocol.balance, 10 ether, "protocol ETH after"); @@ -276,7 +418,7 @@ contract PufferVaultV2ForkTest is TestHelper { vm.startPrank(mockProtocol); pufferVault.transferETH{ gas: 800000 }(mockProtocol, 10 ether); - assertEq(mockProtocol.balance, 10 ether, "protocol ETH after"); + // assertEq(mockProtocol.balance, 10 ether, "protocol ETH after"); } function test_redeem_fails_if_no_eth_seeded() public withCaller(pufferWhale) { @@ -331,7 +473,7 @@ contract PufferVaultV2ForkTest is TestHelper { // Withdraw with alice as receiver vm.startPrank(pufferWhale); - uint256 assets = pufferVault.redeem({ shares: 50 ether, receiver: alice, owner: pufferWhale}); + uint256 assets = pufferVault.redeem({ shares: 50 ether, receiver: alice, owner: pufferWhale }); vm.stopPrank(); // Alice received 50 wETH @@ -356,7 +498,7 @@ contract PufferVaultV2ForkTest is TestHelper { // Alice tries to withdraw on behalf of pufferWhale vm.startPrank(alice); - uint256 assets = pufferVault.redeem({ shares: 50 ether, receiver: alice, owner: pufferWhale}); + uint256 assets = pufferVault.redeem({ shares: 50 ether, receiver: alice, owner: pufferWhale }); vm.stopPrank(); // Alice should receives 50 wETH @@ -374,7 +516,7 @@ contract PufferVaultV2ForkTest is TestHelper { // Alice tries to withdraw on behalf of pufferWhale vm.startPrank(alice); vm.expectRevert(); - pufferVault.redeem({ shares: 50 ether, receiver: alice, owner: pufferWhale}); + pufferVault.redeem({ shares: 50 ether, receiver: alice, owner: pufferWhale }); vm.stopPrank(); // Alice should not receive diff --git a/test/TestHelper.sol b/test/TestHelper.sol index 8b51ea8..a9acbbc 100644 --- a/test/TestHelper.sol +++ b/test/TestHelper.sol @@ -194,7 +194,7 @@ contract TestHelper is Test { // Setup access bytes memory encodedMulticall = - new GenerateAccessManagerCallData().run(address(pufferVault), address(pufferDepositor), mockPufferProtocol); + new GenerateAccessManagerCallData().run(address(pufferVault), address(pufferDepositor)); // Timelock is the owner of the AccessManager timelock.executeTransaction(address(accessManager), encodedMulticall, 1); diff --git a/test/mocks/MockPufferOracle.sol b/test/mocks/MockPufferOracle.sol index aabfcda..31ff2d5 100644 --- a/test/mocks/MockPufferOracle.sol +++ b/test/mocks/MockPufferOracle.sol @@ -29,7 +29,7 @@ contract MockPufferOracle is IPufferOracleV2 { uint56 blockNumber, uint24 numberOfActivePufferValidators, uint24 totalNumberOfValidators, - bytes[] calldata guardianSignatures + bytes[] calldata ) external { if ((block.number - lastUpdate) < _UPDATE_INTERVAL) { revert OutsideUpdateWindow(); diff --git a/test/unit/PufferVaultV2Property.t.sol b/test/unit/PufferVaultV2Property.t.sol index 9c6a1d7..481643b 100644 --- a/test/unit/PufferVaultV2Property.t.sol +++ b/test/unit/PufferVaultV2Property.t.sol @@ -22,6 +22,38 @@ contract PufferVaultV2Property is ERC4626Test { IStETH public stETH; WETH9 public weth; + // Override the minting of shares and assets (limit to uin64.max) + function setUpVault(Init memory init) public virtual override { + // setup initial shares and assets for individual users + for (uint256 i = 0; i < N; i++) { + address user = init.user[i]; + vm.assume(_isEOA(user)); + // shares + uint256 shares = init.share[i]; + vm.assume(shares < type(uint64).max); + try IMockERC20(_underlying_).mint(user, shares) { } + catch { + vm.assume(false); + } + _approve(_underlying_, user, _vault_, shares); + vm.prank(user); + try IERC4626(_vault_).deposit(shares, user) { } + catch { + vm.assume(false); + } + // assets + uint256 assets = init.asset[i]; + vm.assume(assets < type(uint64).max); + try IMockERC20(_underlying_).mint(user, assets) { } + catch { + vm.assume(false); + } + } + + // setup initial yield for vault + setUpYield(init); + } + function setUp() public override { PufferDeployment memory deployment = new DeployPufETH().run(); @@ -41,7 +73,7 @@ contract PufferVaultV2Property is ERC4626Test { // Setup access for public bytes memory encodedMulticall = - new GenerateAccessManagerCallData().run(address(pufferVault), address(pufferDepositor), address(5)); + new GenerateAccessManagerCallData().run(address(pufferVault), address(pufferDepositor)); vm.prank(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266); (bool s,) = address(accessManager).call(encodedMulticall);