diff --git a/docs/PufferDepositorV2.md b/docs/PufferDepositorV2.md new file mode 100644 index 0000000..dea2acb --- /dev/null +++ b/docs/PufferDepositorV2.md @@ -0,0 +1,58 @@ +# PufferDepositor + +### [PufferDepositorV2](./PufferDepositorV2.md) + +| File | Type | Upgradeable | Inherited | Deployed | +| -------- | -------- | -------- | -------- | -------- | +| [`IPufferDepositorV2.sol`](../src/interface/IPufferDepositorV2.sol) | Singleton | / | YES | / | +| [`PufferDepositorV2.sol`](../src/PufferDepositorV2.sol) | Singleton | UUPS Proxy | NO | / | +| [`PufferDepositorStorage.sol`](../src/PufferDepositorStorage.sol) | Singleton | UUPS Proxy | YES | / | + +The PufferDepositorV2 facilitates depositing stETH and wstETH into the [PufferVaultV2](./PufferVaultV2.md). + +#### Important state variables + +The only state information the PufferDepositor contract holds are the addresses of stETH (`_ST_ETH`) and wstETH (`_WST_ETH`). + +--- + +### Functions + +#### `depositStETH` + +```solidity + function depositStETH(Permit calldata permitData, address recipient) + external + restricted + returns (uint256 pufETHAmount) +``` + +Interface function to deposit stETH into the `PufferVault` contract, which mints pufETH for the `recipient`. + +*Effects* +* Takes the specified amount of stETH from the caller +* Deposits the stETH into the `PufferVault` contract +* Mints pufETH for the `recipient`, corresponding to the stETH amount deposited + +*Requirements* +* Called must have previously approved the amount of stETH to be sent to the `PufferDepositor` contract + +#### `depositWstETH` + +```solidity + function depositWstETH(Permit calldata permitData, address recipient) + external + restricted + returns (uint256 pufETHAmount) +``` + +Interface function to deposit wstETH into the `PufferVault` contract, which mints pufETH for the `recipient`. + +*Effects* +* Takes the specified amount of wstETH from the caller +* Unwraps the wstETH into stETH +* Deposits the stETH into the `PufferVault` contract +* Mints pufETH for the `recipient`, corresponding to the stETH amount deposited + +*Requirements* +* Called must have previously approved the amount of wstETH to be sent to the `PufferDepositor` contract \ No newline at end of file diff --git a/docs/PufferVaultV2.md b/docs/PufferVaultV2.md new file mode 100644 index 0000000..d49d06c --- /dev/null +++ b/docs/PufferVaultV2.md @@ -0,0 +1,373 @@ +# PufferVault + + +| File | Type | Upgradeable | Inherited | Deployed | +| -------- | -------- | -------- | -------- | -------- | +| [`IPufferVault.sol`](../src/interface/IPufferVault.sol) | Singleton | / | YES | / | +| [`PufferVault.sol`](../src/PufferVault.sol) | Singleton | UUPS Proxy | YES | [0xd9a4...a72](https://etherscan.io/address/0xd9a442856c234a39a81a089c06451ebaa4306a72) | +| [`PufferVaultV2.sol`](../src/PufferVaultV2.sol) | Singleton | UUPS Proxy | NO | / | +| [`PufferVaultStorage.sol`](../src/PufferVaultStorage.sol) | Singleton | UUPS Proxy | YES | / | + +The PufferVault is in charge of custodying funds for the Puffer protocol. The [initial V1 deployment](https://etherscan.io/address/0xd9a442856c234a39a81a089c06451ebaa4306a72) is an ERC4626 vault with stETH as the underlying asset. The PufferVaultV2 contract is the next upgrade, which changes the underlying asset to wETH and adds functionality to support the Puffer protocol's mainnet deployment. + +#### High-level Concepts + +This document organizes methods according to the following themes (click each to be taken to the relevant section): +* [Inherited from PufferVault](#inherited-from-puffervault) +* [Depositing](#depositing) +* [Withdrawing](#withdrawing) +* [Redeeming](#redeeming) +* [Transferring](#transferring) +* [Burning](#burning) +* [Setter Methods](#setter-methods) +* [Getter Methods](#getter-methods) + +#### Important state variables + +The PufferVault maintains the addresses of important contracts related to EigenLayer and Lido. The PufferVaultV2 accesses PufferVaultStorage, where other important information is maintained. Important state variables are described below: + +#### PufferVault + +* `IStrategy internal immutable _EIGEN_STETH_STRATEGY`: The EigenLayer strategy for depositing stETH +* `IEigenLayer internal immutable _EIGEN_STRATEGY_MANAGER`: EigenLayer's StrategyManager contract, responsible for handling deposits and withdrawals related to EigenLayer's strategy contracts, such as the EigenLayer stETH strategy contract +* `IStETH internal immutable _ST_ETH`: Lido's stETH address +* `ILidoWithdrawalQueue internal immutable _LIDO_WITHDRAWAL_QUEUE`: Lido's contract responsible for handling withdrawals of stETH into ETH + +#### PufferVaultStorage + +* `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.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` + +#### PufferVaultV2 +* `IWETH internal immutable _WETH`: Address of wrapped ETH contract (wETH) +* `IPufferOracle public immutable PUFFER_ORACLE`: The address of the Puffer Oracle responsible for submitting proof-of-reserves. + +--- + + +### Inherited from PufferVault +- [`depositToEigenLayer`](./PufferVault.md#depositToEigenLayer) +- [`initiateStETHWithdrawalFromEigenLayer`](./PufferVault.md#initiateStETHWithdrawalFromEigenLayer) +- [`claimWithdrawalFromEigenLayer`](./PufferVault.md#claimWithdrawalFromEigenLayer) + + +### Depositing + +#### `depositETH` + +```solidity +function depositETH(address receiver) + public + payable + virtual + restricted + returns (uint256) +``` + +This function is used to deposit native ETH into the Puffer Vault. + +The function is restricted, meaning it can only be executed when the contract is not paused. This is similar to the whenNotPaused modifier from the Pausable.sol contract. + +The function takes one parameter: + +> `receiver`: This is the address of the recipient who will receive the pufETH tokens. + +The function returns one value: + +> `shares`: This is the amount of pufETH tokens that the receiver gets from the deposit. + +#### `depositStETH` + +```solidity +function depositStETH(uint256 assets, address receiver) + public + virtual + restricted + returns (uint256) +``` + +This function is used to deposit stETH into the Puffer Vault. + +Similar to the previous function, it is restricted and can only be executed when the contract is not paused. This is akin to the whenNotPaused modifier from the Pausable.sol contract. + +The function takes two parameters: + +> `assets`: This is the amount of stETH that is to be deposited into the vault. + +> `receiver`: This is the address of the recipient who will receive the pufETH tokens. + +The function returns one value: + +> `shares`: This is the amount of pufETH tokens that the receiver gets from the deposit. + +### Withdrawing + +#### `initiateETHWithdrawalsFromLido` + +```solidity +function initiateETHWithdrawalsFromLido(uint256[] calldata amounts) + external + virtual + override + restricted + returns (uint256[] memory requestIds) +``` + +This function is used to initiate withdrawals of ETH from Lido (was overloaded from PufferVault version 1). + +The function is restricted to the Operations Multisig, meaning only the operations multi-sig wallet can execute this function. + +The function takes one parameter: + +> `amounts`: This is an array of stETH amounts that are to be queued for withdrawal. + +The function returns one value: + +> `requestIds`: This is an array of request IDs corresponding to the withdrawals. Each withdrawal request has a unique ID for tracking and reference purposes. + +#### `claimWithdrawalsFromLido` + +```solidity +function claimWithdrawalsFromLido(uint256[] calldata requestIds) + external + virtual + override + restricted +``` + +This function is used to claim ETH withdrawals from Lido (was overloaded from PufferVault version 1). + +The function is restricted to the Operations Multisig, meaning only the operations multi-signature wallet can execute this function. + +The function takes one parameter: + +> `requestIds`: This is an array of request IDs corresponding to the withdrawals that are to be claimed. Each withdrawal request has a unique ID for tracking and reference purposes. + +#### `withdraw` + +```solidity +function withdraw(uint256 assets, address receiver, address owner) + public + virtual + override + restricted + returns (uint256) +``` + +This function is used to withdraw wETH assets from the vault. In the process, the pufETH shares of the owner are burned. + +The caller of this function does not have to be the owner if the owner has approved the caller to spend their pufETH. + +The function is restricted, meaning it can only be executed when the contract is not paused. This is similar to the whenNotPaused modifier from the Pausable.sol contract. + +The function takes three parameters: + +> `assets`: This is the amount of wETH assets that are to be withdrawn. + +> `receiver`: This is the address that will receive the WETH assets. + +> `owner`: This is the address of the owner whose pufETH shares are to be burned. + +The function returns one value: + +> `shares`: This is the amount of pufETH shares that are burned in the process. + +--- + +### Redeeming + +#### `redeem` + +```solidity +function redeem(uint256 shares, address receiver, address owner) + public + virtual + override + restricted + returns (uint256) +``` + +This function is used to redeem pufETH shares in exchange for wETH assets from the vault. In the process, the pufETH shares of the owner are burned. + +The caller of this function does not have to be the owner if the owner has approved the caller to spend their pufETH. + +The function is restricted, meaning it can only be executed when the contract is not paused. This is similar to the whenNotPaused modifier from the Pausable.sol contract. + +The function takes three parameters: + +> `shares`: This is the amount of pufETH shares that are to be withdrawn. + +> `receiver`: This is the address that will receive the wETH assets. + +> `owner`: This is the address of the owner whose pufETH shares are to be burned. + +The function returns one value: + +> `assets`: This is the amount of wETH assets that are redeemed. + +--- + + +### Transferring + +#### `transferETH` + +```solidity +function transferETH(address to, uint256 ethAmount) + external + restricted +``` + +This function is used to transfer ETH from the vault to a specified address. + +The function is restricted to the PufferProtocol contract, meaning only this contract can execute the function. + +The function is used to transfer ETH to PufferModules in order to fund Puffer validators. + +The function takes two parameters: + +> `to`: This is the address of the PufferModule where the ETH will be transferred to. + +> `ethAmount`: This is the amount of ETH that is to be transferred. + +--- + +### Burning + +#### `burn` + +```solidity +function burn(uint256 shares) + public + restricted +``` + +This function allows the msg.sender (the one who initiates the transaction) to burn their pufETH shares. + +The function is restricted, meaning it can only be executed when the contract is not paused. This is similar to the whenNotPaused modifier from the Pausable.sol contract. + +The function is primarily used to burn portions of Puffer validator bonds due to inactivity or slashing. + +The function takes one parameter: + +> `shares`: This is the amount of pufETH shares that are to be burned. + +--- + +### Setter Methods + +#### `setDailyWithdrawalLimit` + +```solidity +function setDailyWithdrawalLimit(uint96 newLimit) + external + restricted +``` + +This function is used to set a new daily withdrawal limit (restricted to the DAO). + +The function takes one parameter: + +> `newLimit`: This is the new daily limit that is to be set for withdrawals. + +--- + + +### Getter Methods + +#### `totalAssets` + +```solidity +function totalAssets() + public + view + virtual + override + returns (uint256) +``` + +This function is used to calculate the total assets backing the pufETH nLRT. + +The pufETH shares of the vault are primarily backed by the wETH asset. However, at any point in time, the full backing may be a combination of stETH, WETH, and ETH. + +The `totalAssets()` function calculates the total assets by summing the following: + +- wETH held in the vault contract +- ETH held in the vault contract +- The oracle-reported Puffer validator ETH locked in the Beacon chain +- stETH held in the vault contract, in EigenLayer's stETH strategy, and in Lido's withdrawal queue. (It is assumed that stETH is always 1:1 with ETH since it's rebasing) + +The function does not take any parameters and returns one value: + +> The total assets of the vault. This is a numerical value represented as a uint256. + +#### `maxWithdraw` + +```solidity +function maxWithdraw(address owner) + public + view + virtual + override + returns (uint256 maxAssets) +``` + +This function is used to calculate the maximum amount of wETH assets that can be withdrawn by the owner. + +The function considers both the remaining daily withdrawal limit and the owner's balance. + +The function takes one parameter: + +> `owner`: This is the address of the owner for whom the maximum withdrawal amount is calculated. + +The function returns one value: + +> `maxAssets`: This is the maximum amount of WETH assets that can be withdrawn by the owner. + +#### `maxRedeem` + +```solidity +function maxRedeem(address owner) + public + view + virtual + override + returns (uint256 maxShares) +``` + +This function is used to calculate the maximum amount of pufETH shares that can be redeemed by the owner. + +The function considers both the remaining daily withdrawal limit in terms of assets and converts it to shares, and the owner's share balance. + +The function takes one parameter: + +> `owner`: This is the address of the owner for whom the maximum redeemable shares are calculated. + +The function returns one value: + +> `maxShares`: This is the maximum amount of pufETH shares that can be redeemed by the owner. + +#### `getRemainingAssetsDailyWithdrawalLimit` + +```solidity +function getRemainingAssetsDailyWithdrawalLimit() + public + view + virtual + returns (uint96) +``` + +This function is used to get the remaining assets that can be withdrawn for the current day. + +The function does not take any parameters. + +The function returns one value: + +> The remaining assets (wETH) that can be withdrawn today. This is a numerical value represented as a uint96. \ No newline at end of file diff --git a/src/PufferVaultV2.sol b/src/PufferVaultV2.sol index 2034432..dd21eb3 100644 --- a/src/PufferVaultV2.sol +++ b/src/PufferVaultV2.sol @@ -82,16 +82,20 @@ contract PufferVaultV2 is PufferVault { /** * @dev See {IERC4626-totalAssets}. - * Eventually, stETH will not exist anymore, and the Vault will represent shares of total ETH holdings - * ETH to stETH is always 1:1 (stETH is rebasing token) - * Sum of EL assets + Vault Assets + * 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 + * - ETH held in the vault contract + * - PUFFER_ORACLE.getLockedEthAmount(), which is the oracle-reported Puffer validator ETH locked in the Beacon chain + * - stETH held in the vault contract, in EigenLayer's stETH strategy, and in Lido's withdrawal queue. (we assume stETH is always 1:1 with ETH since it's rebasing) * - * NOTE on the native ETH deposit: + * NOTE on the native ETH deposits: * When dealing with NATIVE ETH deposits, we need to deduct callvalue from the balance. * The contract calculates the amount of shares(pufETH) to mint based on the total assets. - * When a user sends ETH, the msg.value is immediately added to address(this).balance, and address(this.balance) is included in the total assets. - * Because of that we must deduct the callvalue from the balance to avoid the user getting more shares than he should. - * We can't use msg.value in a view function, so we use assembly to get the callvalue. + * When a user sends ETH, the msg.value is immediately added to address(this).balance. + * Since address(this.balance)` is used in calculating `totalAssets()`, we must deduct the `callvalue()` from the balance to prevent the user from minting excess shares. + * `msg.value` cannot be accessed from a view function, so we use assembly to get the callvalue. */ function totalAssets() public view virtual override returns (uint256) { uint256 callValue; @@ -104,7 +108,7 @@ contract PufferVaultV2 is PufferVault { } /** - * @notice Calculates the maximum amount of assets that can be withdrawn by the `owner`. + * @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`. @@ -116,7 +120,7 @@ contract PufferVaultV2 is PufferVault { } /** - * @notice Calculates the maximum amount of shares that can be redeemed by the `owner`. + * @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`. @@ -128,9 +132,14 @@ contract PufferVaultV2 is PufferVault { } /** - * @notice Withdrawals are allowed if the asset out is WETH + * @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 + * @param assets The amount of assets (WETH) to withdraw + * @param receiver The address to receive the assets (WETH) + * @param owner The address of the owner for which the shares (pufETH) are burned. + * @return shares The amount of shares (pufETH) burned */ function withdraw(uint256 assets, address receiver, address owner) public @@ -156,9 +165,14 @@ contract PufferVaultV2 is PufferVault { } /** - * @notice Withdrawals are allowed an the asset out is WETH + * @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 + * @param shares The amount of shares (pufETH) to withdraw + * @param receiver The address to receive the assets (WETH) + * @param owner The address of the owner for which the shares (pufETH) are burned. + * @return assets The amount of assets (WETH) redeemed */ function redeem(uint256 shares, address receiver, address owner) public @@ -185,8 +199,10 @@ contract PufferVaultV2 is PufferVault { } /** - * @notice Deposits native ETH + * @notice Deposits native ETH into the Puffer Vault * @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); @@ -202,8 +218,11 @@ contract PufferVaultV2 is PufferVault { } /** - * @notice Deposits stETH + * @notice Deposits stETH into the Puffer Vault * @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); @@ -224,8 +243,9 @@ contract PufferVaultV2 is PufferVault { /** * @notice Initiates ETH withdrawals from Lido - * Restricted to Operations Multisig - * @param amounts An array of amounts that we want to queue + * @dev Restricted to Operations Multisig + * @param amounts An array of stETH amounts to queue + * @return requestIds An array of request IDs for the withdrawals */ function initiateETHWithdrawalsFromLido(uint256[] calldata amounts) external @@ -254,7 +274,7 @@ contract PufferVaultV2 is PufferVault { /** * @notice Claims ETH withdrawals from Lido - * Restricted to Operations Multisig + * @dev Restricted to Operations Multisig * @param requestIds An array of request IDs for the withdrawals */ function claimWithdrawalsFromLido(uint256[] calldata requestIds) external virtual override restricted { @@ -284,10 +304,10 @@ contract PufferVaultV2 is PufferVault { } /** - * @notice Transfers ETH to a specified address + * @notice Transfers ETH to a specified address. * @dev Restricted to PufferProtocol smart contract - * We use it to transfer ETH to PufferModule - * @param to The address of the module to transfer ETH to + * @dev It is used to transfer ETH to PufferModules to fund Puffer validators. + * @param to The address of the PufferModule to transfer ETH to * @param ethAmount The amount of ETH to transfer */ function transferETH(address to, uint256 ethAmount) external restricted { @@ -310,9 +330,9 @@ contract PufferVaultV2 is PufferVault { } /** - * @notice Allows the `msg.sender` to burn his shares + * @notice Allows the `msg.sender` to burn their (pufETH) shares * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol - * We use it to burn the bond if the node operator gets slashed + * @dev It is used to burn portions of Puffer validator bonds due to inactivity or slashing * @param shares The amount of shares to burn */ function burn(uint256 shares) public restricted { @@ -343,6 +363,10 @@ contract PufferVaultV2 is PufferVault { return dailyAssetsWithdrawalLimit - assetsWithdrawnToday; } + /** + * @notice Wraps the vault's ETH balance to WETH. + * @dev Used to provide WETH liquidity + */ function _wrapETH(uint256 assets) internal { uint256 wethBalance = _WETH.balanceOf(address(this)); @@ -352,7 +376,8 @@ contract PufferVaultV2 is PufferVault { } /** - * @param withdrawalAmount is the assets amount, not shares + * @notice Updates the amount of assets (WETH) withdrawn today + * @param withdrawalAmount is the assets (WETH) amount */ function _updateDailyWithdrawals(uint256 withdrawalAmount) internal { VaultStorage storage $ = _getPufferVaultStorage(); @@ -367,7 +392,8 @@ contract PufferVaultV2 is PufferVault { } /** - * @param newLimit is the assets amount, not shares + * @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 { VaultStorage storage $ = _getPufferVaultStorage(); diff --git a/src/interface/IPufferOracle.sol b/src/interface/IPufferOracle.sol index 264f479..f7061cb 100644 --- a/src/interface/IPufferOracle.sol +++ b/src/interface/IPufferOracle.sol @@ -8,19 +8,19 @@ pragma solidity >=0.8.0 <0.9.0; */ interface IPufferOracle { /** - * @notice Thrown if the new VT mint price is is invalid + * @notice Thrown if the new ValidatorTicket mint price is invalid */ error InvalidValidatorTicketPrice(); /** - * @notice Emitted when the price to mint VT is updated + * @notice Emitted when the price to mint ValidatorTicket is updated * @dev Signature "0xf76811fec27423d0853e6bf49d7ea78c666629c2f67e29647d689954021ae0ea" */ event ValidatorTicketMintPriceUpdated(uint256 oldPrice, uint256 newPrice); /** - * @notice Retrieves the current mint price for minting one Validator Ticket - * @return pricePerVT The current mint price + * @notice Retrieves the current mint price for minting one ValidatorTicket + * @return pricePerVT The current ValidatorTicket mint price */ function getValidatorTicketPrice() external view returns (uint256 pricePerVT); diff --git a/src/interface/IPufferOracleV2.sol b/src/interface/IPufferOracleV2.sol index 2f0a141..7d4e4f6 100644 --- a/src/interface/IPufferOracleV2.sol +++ b/src/interface/IPufferOracleV2.sol @@ -10,16 +10,18 @@ import { IPufferOracle } from "./IPufferOracle.sol"; */ interface IPufferOracleV2 is IPufferOracle { /** - * @notice Thrown if Guardians try to re-submit the backing data + * @notice Thrown if proof-of-reserves is submitted outside of the acceptable window * @dev Signature "0xf93417f7" */ error OutsideUpdateWindow(); /** - * @notice Emitted when the Guardians update state of the protocol + * @notice Emitted when the proof-of-reserves updates the PufferVault's state * @dev Signature "0xaabc7a8108435a4fc30d1e2cecd59cbdec96ee6fa583c6eebf9a20bc9d14d3ed" - * @param blockNumber is the block number of the update - * @param lockedETH is the locked ETH amount in Beacon chain + * @param blockNumber is the block number of the proof-of-reserves update + * @param lockedETH is the validator ETH locked in the Beacon chain + * @param numberOfActivePufferValidators is the number of active Puffer validators, used in enforcing the burst threshold + * @param totalNumberOfValidators is the total number of active validators on Ethereum, used in enforcing the burst threshold */ event ReservesUpdated( uint256 blockNumber, uint256 lockedETH, uint256 numberOfActivePufferValidators, uint256 totalNumberOfValidators @@ -31,14 +33,15 @@ interface IPufferOracleV2 is IPufferOracle { function getTotalNumberOfValidators() external view returns (uint256); /** - * @notice Returns the block number of the last update + * @notice Returns the block number of the last proof-of-reserves update */ function getLastUpdate() external view returns (uint256); /** - * @notice Increases the `_lockedETH` amount on the Oracle by 32 ETH - * It is called when the Beacon chain receives a new deposit from PufferProtocol - * The PufferVault balance is decreased by the same amount + * @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. * @dev Restricted to PufferProtocol contract */ function provisionNode() external; diff --git a/test/Integration/PufferVaultV2.fork.t.sol b/test/Integration/PufferVaultV2.fork.t.sol index e125b3a..7892737 100644 --- a/test/Integration/PufferVaultV2.fork.t.sol +++ b/test/Integration/PufferVaultV2.fork.t.sol @@ -77,6 +77,68 @@ contract PufferVaultV2ForkTest is TestHelper { assertEq(pufferVault.getRemainingAssetsDailyWithdrawalLimit(), 0 ether, "everything withdrawn"); } + function test_withdrawal_transfers_to_receiver() public { + // Get withdrawal liquidity + _withdraw_stETH_from_lido(); + + // Initial state + assertEq(_WETH.balanceOf(address(alice)), 0, "alice balance"); + uint256 whaleShares = pufferVault.balanceOf(pufferWhale); + + // Withdraw with alice as receiver + vm.startPrank(pufferWhale); + uint256 sharesBurned = pufferVault.withdraw({ assets: 50 ether, receiver: alice, owner: pufferWhale}); + vm.stopPrank(); + + // Alice received 50 wETH + assertEq(_WETH.balanceOf(address(alice)), 50 ether, "alice balance"); + + // Whale burned shares + assertApproxEqAbs(pufferVault.balanceOf(pufferWhale), whaleShares - sharesBurned, 1e9, "asset change"); + } + + function test_withdrawal_succeeds_with_allowance() public { + // Get withdrawal liquidity + _withdraw_stETH_from_lido(); + + // Initial state + assertEq(_WETH.balanceOf(address(alice)), 0, "alice balance"); + uint256 whaleShares = pufferVault.balanceOf(pufferWhale); + + // pufferWhale approves alice to burn their pufETH + vm.startPrank(pufferWhale); + pufferVault.approve(address(alice), type(uint256).max); + vm.stopPrank(); + + // Alice tries to withdraw on behalf of pufferWhale + vm.startPrank(alice); + uint256 sharesBurned = pufferVault.withdraw({ assets: 50 ether, receiver: alice, owner: pufferWhale}); + vm.stopPrank(); + + // Alice should receives 50 wETH + assertEq(_WETH.balanceOf(address(alice)), 50 ether, "alice balance"); + + // Whale burned shares + assertApproxEqAbs(pufferVault.balanceOf(pufferWhale), whaleShares - sharesBurned, 1e9, "asset change"); + } + + function test_withdrawal_fails_if_owner_is_not_caller() public { + // Get withdrawal liquidity + _withdraw_stETH_from_lido(); + + // Initial state + assertEq(_WETH.balanceOf(address(alice)), 0, "alice balance"); + + // Alice tries to withdraw on behalf of pufferWhale + vm.startPrank(alice); + vm.expectRevert(); + pufferVault.withdraw({ assets: 50 ether, receiver: alice, owner: pufferWhale}); + vm.stopPrank(); + + // Alice should not receive + assertEq(_WETH.balanceOf(address(alice)), 0 ether, "alice balance"); + } + function test_withdrawal_fails_when_exceeding_maximum() public giveToken(MAKER_VAULT, address(_WETH), alice, 100 ether) @@ -227,13 +289,13 @@ contract PufferVaultV2ForkTest is TestHelper { pufferVault.redeem(maxWhaleRedeemableShares, pufferWhale, pufferWhale); } - function test_redeem_succeeds_if_seeded_with_eth() public withCaller(pufferWhale) { + // function test_redeem_succeeds_if_seeded_with_eth() public withCaller(pufferWhale) { + function test_redeem_succeeds_if_seeded_with_eth() public { // mainnet vault start with 0 eth assertEq(address(pufferVault).balance, 0 ether, "vault ETH"); - // fill it so there is something to redeem - vm.deal(address(pufferVault), 100 ether); - assertEq(address(pufferVault).balance, 100 ether, "vault ETH"); + // Fill vault with withdrawal liquidity + _withdraw_stETH_from_lido(); // before state uint256 assetsBefore = pufferVault.totalAssets(); @@ -241,8 +303,10 @@ contract PufferVaultV2ForkTest is TestHelper { uint256 whaleShares = pufferVault.balanceOf(pufferWhale); // redeem all of whale's shares + vm.startPrank(pufferWhale); uint256 maxWhaleRedeemableShares = pufferVault.maxRedeem(pufferWhale); uint256 redeemedAssets = pufferVault.redeem(maxWhaleRedeemableShares, pufferWhale, pufferWhale); + vm.stopPrank(); // no more to redeem assertEq(pufferVault.maxRedeem(pufferWhale), 0, "max redeem"); @@ -257,6 +321,66 @@ contract PufferVaultV2ForkTest is TestHelper { ); } + function test_redeem_transfers_to_receiver() public { + // Get withdrawal liquidity + _withdraw_stETH_from_lido(); + + // Initial state + assertEq(_WETH.balanceOf(address(alice)), 0, "alice balance"); + uint256 whaleShares = pufferVault.balanceOf(pufferWhale); + + // Withdraw with alice as receiver + vm.startPrank(pufferWhale); + uint256 assets = pufferVault.redeem({ shares: 50 ether, receiver: alice, owner: pufferWhale}); + vm.stopPrank(); + + // Alice received 50 wETH + assertEq(_WETH.balanceOf(address(alice)), assets, "alice balance"); + + // Whale burned shares + assertApproxEqAbs(pufferVault.balanceOf(pufferWhale), whaleShares - 50 ether, 1e9, "asset change"); + } + + function test_redeem_succeeds_with_allowance() public { + // Get withdrawal liquidity + _withdraw_stETH_from_lido(); + + // Initial state + assertEq(_WETH.balanceOf(address(alice)), 0, "alice balance"); + uint256 whaleShares = pufferVault.balanceOf(pufferWhale); + + // pufferWhale approves alice to burn their pufETH + vm.startPrank(pufferWhale); + pufferVault.approve(address(alice), type(uint256).max); + vm.stopPrank(); + + // Alice tries to withdraw on behalf of pufferWhale + vm.startPrank(alice); + uint256 assets = pufferVault.redeem({ shares: 50 ether, receiver: alice, owner: pufferWhale}); + vm.stopPrank(); + + // Alice should receives 50 wETH + assertEq(_WETH.balanceOf(address(alice)), assets, "alice balance"); + assertApproxEqAbs(pufferVault.balanceOf(pufferWhale), whaleShares - 50 ether, 1e9, "asset change"); + } + + function test_redeem_fails_if_owner_is_not_caller() public { + // Get withdrawal liquidity + _withdraw_stETH_from_lido(); + + // Initial state + assertEq(_WETH.balanceOf(address(alice)), 0, "alice balance"); + + // Alice tries to withdraw on behalf of pufferWhale + vm.startPrank(alice); + vm.expectRevert(); + pufferVault.redeem({ shares: 50 ether, receiver: alice, owner: pufferWhale}); + vm.stopPrank(); + + // Alice should not receive + assertEq(_WETH.balanceOf(address(alice)), 0 ether, "alice balance"); + } + // mint with WETH function test_mint() public giveToken(MAKER_VAULT, address(_WETH), alice, 100 ether) withCaller(alice) { uint256 sharesAmount = 5 ether;