├── audits ├── BlockSec-pufETH-v1.pdf ├── Quantstamp-pufETH-v1.pdf ├── SlowMist-pufETH-v1.pdf ├── 0xLuckyLuke-pufETH-v1.pdf ├── Immunefi_Boost_pufETH_v1.pdf └── ToB-2024-03-pufferfinance.pdf ├── src ├── echidna │ ├── config.yaml │ └── EchidnaPufferVaultV2.sol ├── structs │ ├── Permit.sol │ └── PufferDeployment.sol ├── interface │ ├── Lido │ │ ├── ILidoWithdrawalQueue.sol │ │ ├── IWstETH.sol │ │ └── IStETH.sol │ ├── Other │ │ ├── IWETH.sol │ │ └── ISushiRouter.sol │ ├── IPufferVault.sol │ ├── EigenLayer │ │ ├── IStrategy.sol │ │ ├── IEigenLayer.sol │ │ └── IDelegationManager.sol │ ├── IPufferDepositorV2.sol │ ├── IPufferOracle.sol │ ├── IXERC20Lockbox.sol │ ├── IPufferOracleV2.sol │ ├── IPufferVaultV2.sol │ ├── IPufferDepositor.sol │ └── IXERC20.sol ├── NoImplementation.sol ├── PufferDepositorStorage.sol ├── PufferVaultV2Tests.sol ├── l2 │ ├── xPufETHStorage.sol │ └── xPufETH.sol ├── PufferVaultStorage.sol ├── XERC20Lockbox.sol ├── PufferDepositorV2.sol ├── PufferDepositor.sol └── PufferVault.sol ├── .env ├── .gitignore ├── output └── puffer.json ├── remappings.txt ├── test ├── mocks │ ├── LidoWithdrawalQueueMock.sol │ ├── stETHStrategyMock.sol │ ├── MockPufferOracle.sol │ ├── EigenLayerManagerMock.sol │ ├── WETH9.sol │ ├── stETHMock.sol │ └── EigenLayerDelegationManagerMock.sol ├── unit │ ├── PufferVaultV2Property.t.sol │ ├── PufETH.t.sol │ └── xPufETHTest.t.sol ├── Integration │ ├── PufferVaultV2Sandwich.fork.t.sol │ ├── PufferVaultV2WithdrawFromEl.fork.t.sol │ └── PufferDepositorV2.fork.t.sol └── TestHelper.sol ├── .gitmodules ├── shell-scripts └── install_git_hooks.sh ├── foundry.toml ├── .solhint.json ├── slither.config.json ├── script ├── Roles.sol ├── BaseScript.s.sol ├── UpgradePufETH.s.sol ├── DeployL2XPufETH.s.sol └── GenerateAccessManagerCallData.sol ├── .github └── workflows │ └── ci.yml ├── docs ├── Timelock.md ├── PufferDepositorV2.md ├── PufferOracle.md ├── README.md ├── PufferDepositor.md └── PufferVault.md ├── README.md └── .gas-snapshot /audits/BlockSec-pufETH-v1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PufferFinance/pufETH/HEAD/audits/BlockSec-pufETH-v1.pdf -------------------------------------------------------------------------------- /audits/Quantstamp-pufETH-v1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PufferFinance/pufETH/HEAD/audits/Quantstamp-pufETH-v1.pdf -------------------------------------------------------------------------------- /audits/SlowMist-pufETH-v1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PufferFinance/pufETH/HEAD/audits/SlowMist-pufETH-v1.pdf -------------------------------------------------------------------------------- /audits/0xLuckyLuke-pufETH-v1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PufferFinance/pufETH/HEAD/audits/0xLuckyLuke-pufETH-v1.pdf -------------------------------------------------------------------------------- /audits/Immunefi_Boost_pufETH_v1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PufferFinance/pufETH/HEAD/audits/Immunefi_Boost_pufETH_v1.pdf -------------------------------------------------------------------------------- /audits/ToB-2024-03-pufferfinance.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PufferFinance/pufETH/HEAD/audits/ToB-2024-03-pufferfinance.pdf -------------------------------------------------------------------------------- /src/echidna/config.yaml: -------------------------------------------------------------------------------- 1 | corpusDir: "tests/echidna-corpus" 2 | testMode: assertion 3 | testLimit: 1000000 4 | deployer: "0x10000" 5 | sender: ["0x10000"] -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | #Mainnet addresses 2 | OPERATIONS_MULTISIG=0x5568b309259131D3A7c128700195e0A1C94761A0 3 | PAUSER_MULTISIG=0x892075a5B6f42bDDe1800A0eAd891b21cA681031 4 | COMMUNITY_MULTISIG=0xf9F846FA49e79BE8d74c68CDC01AaaFfBBf8177F -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | 12 | # Dotenv file 13 | .DS_Store 14 | -------------------------------------------------------------------------------- /src/structs/Permit.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | /** 5 | * @dev Struct representing a permit for a specific action. 6 | */ 7 | struct Permit { 8 | uint256 deadline; 9 | uint256 amount; 10 | uint8 v; 11 | bytes32 r; 12 | bytes32 s; 13 | } 14 | -------------------------------------------------------------------------------- /output/puffer.json: -------------------------------------------------------------------------------- 1 | { 2 | "": "", 3 | "PufferDepositor": "0xca2f7a7BC2Ea026773Dec1993f33569231CfcA04", 4 | "PufferDepositorImplementation": "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e", 5 | "PufferVault": "0x82c8Ea3945357f79Ca276eDd08a216ba2dCf48c0", 6 | "PufferVaultImplementation": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" 7 | } -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | ds-test/=lib/forge-std/lib/ds-test/src/ 2 | erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/ 3 | forge-std/=lib/forge-std/src/ 4 | openzeppelin/=lib/openzeppelin-contracts/contracts/ 5 | @openzeppelin/=lib/openzeppelin-contracts 6 | @openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ 7 | solady/=lib/solady/src/ -------------------------------------------------------------------------------- /src/interface/Lido/ILidoWithdrawalQueue.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | /** 5 | * ILidoWithdrawalQueue 6 | */ 7 | interface ILidoWithdrawalQueue { 8 | function requestWithdrawals(uint256[] calldata _amounts, address _owner) 9 | external 10 | returns (uint256[] memory requestIds); 11 | 12 | function claimWithdrawal(uint256 _requestId) external; 13 | } 14 | -------------------------------------------------------------------------------- /src/interface/Other/IWETH.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import { IERC20 } from "openzeppelin/token/ERC20/IERC20.sol"; 5 | 6 | interface IWETH is IERC20 { 7 | event Deposit(address indexed dst, uint256 wad); 8 | event Withdrawal(address indexed src, uint256 wad); 9 | 10 | function deposit() external payable; 11 | function withdraw(uint256 wad) external; 12 | } 13 | -------------------------------------------------------------------------------- /src/interface/Other/ISushiRouter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | /** 5 | * @notice Sushiswap Router 6 | */ 7 | interface ISushiRouter { 8 | function processRoute( 9 | address tokenIn, 10 | uint256 amountIn, 11 | address tokenOut, 12 | uint256 amountOutMin, 13 | address to, 14 | bytes memory route 15 | ) external payable returns (uint256 amountOut); 16 | } 17 | -------------------------------------------------------------------------------- /test/mocks/LidoWithdrawalQueueMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import { ILidoWithdrawalQueue } from "../../src/interface/Lido/ILidoWithdrawalQueue.sol"; 5 | 6 | contract LidoWithdrawalQueueMock is ILidoWithdrawalQueue { 7 | function requestWithdrawals(uint256[] calldata _amounts, address _owner) 8 | external 9 | returns (uint256[] memory requestIds) 10 | { } 11 | 12 | function claimWithdrawal(uint256 _requestId) external { } 13 | } 14 | -------------------------------------------------------------------------------- /src/structs/PufferDeployment.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | struct PufferDeployment { 5 | address accessManager; 6 | address pufferDepositorImplementation; 7 | address pufferDepositor; 8 | address pufferVault; 9 | address pufferVaultImplementation; 10 | address pufferOracle; 11 | address stETH; 12 | address weth; 13 | address timelock; 14 | address lidoWithdrawalQueueMock; 15 | address stETHStrategyMock; 16 | address eigenStrategyManagerMock; 17 | } 18 | -------------------------------------------------------------------------------- /test/mocks/stETHStrategyMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import { IStrategy } from "../../src/interface/EigenLayer/IStrategy.sol"; 5 | 6 | contract stETHStrategyMock is IStrategy { 7 | /** 8 | * @notice Returns the amount of underlying tokens for `user` 9 | */ 10 | function userUnderlying(address user) external view returns (uint256) { } 11 | 12 | function userUnderlyingView(address user) external view returns (uint256) { } 13 | 14 | function sharesToUnderlyingView(uint256 amountShares) external view returns (uint256) { } 15 | } 16 | -------------------------------------------------------------------------------- /src/NoImplementation.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import { UUPSUpgradeable } from "@openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; 5 | 6 | contract NoImplementation is UUPSUpgradeable { 7 | address immutable upgrader; 8 | 9 | constructor() { 10 | upgrader = msg.sender; 11 | } 12 | 13 | function _authorizeUpgrade(address) internal virtual override { 14 | // solhint-disable-next-line custom-errors 15 | require(msg.sender == upgrader, "Unauthorized"); 16 | // anybody can steal this proxy 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/openzeppelin-contracts"] 5 | path = lib/openzeppelin-contracts 6 | url = https://github.com/openzeppelin/openzeppelin-contracts 7 | branch = v5.0.1 8 | [submodule "lib/openzeppelin-contracts-upgradeable"] 9 | path = lib/openzeppelin-contracts-upgradeable 10 | url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable 11 | branch = v5.0.1 12 | [submodule "lib/erc4626-tests"] 13 | path = lib/erc4626-tests 14 | url = https://github.com/a16z/erc4626-tests 15 | [submodule "lib/properties"] 16 | path = lib/properties 17 | url = https://github.com/crytic/properties 18 | -------------------------------------------------------------------------------- /shell-scripts/install_git_hooks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Define the path to the pre-commit hook. 4 | HOOK_PATH=".git/hooks/pre-commit" 5 | 6 | # Check if the .git/hooks directory exists 7 | if [ -d ".git/hooks" ]; then 8 | # Write the specified commands to the pre-commit hook 9 | cat > "$HOOK_PATH" << EOF 10 | #!/bin/sh 11 | 12 | # Run Slither 13 | slither . 14 | 15 | # Run Solhint for src and test directories 16 | solhint src/* 17 | solhint test/* 18 | 19 | # Run Forge formatting 20 | forge fmt 21 | EOF 22 | 23 | # Make the pre-commit hook executable 24 | chmod +x "$HOOK_PATH" 25 | 26 | echo "Pre-commit hook has been installed successfully." 27 | else 28 | echo "Error: This directory does not seem to be a Git repository." 29 | fi 30 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | gas_reports=[ 3 | "PufferDepositor", 4 | "PufferOracle", 5 | "PufferVault", 6 | "PufferVaultV2", 7 | "PufferDepositorV2", 8 | ] 9 | fs_permissions = [{ access = "read-write", path = "./"}] 10 | src = "src" 11 | out = "out" 12 | libs = ["lib"] 13 | auto_detect_solc = false 14 | # cbor_metadata = false 15 | # bytecode_hash = "none" 16 | optimizer = true 17 | optimizer_runs = 200 18 | evm_version = "cancun" # is live on mainnet 19 | seed = "0x1337" 20 | solc = "0.8.26" 21 | # via_ir = true 22 | 23 | [fmt] 24 | line_length = 120 25 | int_types = "long" 26 | tab_width = 4 27 | quote_style = "double" 28 | bracket_spacing = true 29 | 30 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options 31 | 32 | [rpc_endpoints] 33 | mainnet="${ETH_RPC_URL}" 34 | holesky="${HOLESKY_RPC_URL}" -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "avoid-low-level-calls": "off", 5 | "code-complexity": ["error", 9], 6 | "compiler-version": ["error", ">=0.8.0 <0.9.0"], 7 | "contract-name-camelcase": "off", 8 | "const-name-snakecase": "off", 9 | "custom-errors": "error", 10 | "func-name-mixedcase": "off", 11 | "func-visibility": ["error", { "ignoreConstructors": true }], 12 | "max-line-length": ["error", 123], 13 | "named-parameters-mapping": "warn", 14 | "no-empty-blocks": "off", 15 | "not-rely-on-time": "off", 16 | "one-contract-per-file": "off", 17 | "var-name-mixedcase": "off", 18 | "max-line-length": "off", 19 | "reason-string": "off", 20 | "func-named-parameters": "error", 21 | "func-param-name-mixedcase": "error", 22 | "modifier-name-mixedcase": "error", 23 | "code-complexity": "error", 24 | "explicit-types": "error" 25 | } 26 | } -------------------------------------------------------------------------------- /src/interface/IPufferVault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | /** 5 | * @title PufferVault 6 | * @author Puffer Finance 7 | * @custom:security-contact security@puffer.fi 8 | */ 9 | interface IPufferVault { 10 | /** 11 | * @notice Emitted when we request withdrawals from Lido 12 | */ 13 | event RequestedWithdrawals(uint256[] requestIds); 14 | /** 15 | * @notice Emitted when we claim the withdrawals from Lido 16 | */ 17 | event ClaimedWithdrawals(uint256[] requestIds); 18 | /** 19 | * @notice Emitted when the user tries to do a withdrawal 20 | */ 21 | 22 | /** 23 | * @dev Thrown when withdrawals are disabled and a withdrawal attempt is made 24 | */ 25 | error WithdrawalsAreDisabled(); 26 | 27 | /** 28 | * @dev Thrown when a withdrawal attempt is made with invalid parameters 29 | */ 30 | error InvalidWithdrawal(); 31 | } 32 | -------------------------------------------------------------------------------- /src/interface/EigenLayer/IStrategy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | interface IStrategy { 5 | /** 6 | * @notice Returns the amount of underlying tokens for `user` 7 | */ 8 | function userUnderlying(address user) external view returns (uint256); 9 | 10 | /** 11 | * @notice Returns the amount of underlying tokens for `user` 12 | */ 13 | function userUnderlyingView(address user) external view returns (uint256); 14 | 15 | /** 16 | * @notice Used to convert a number of shares to the equivalent amount of underlying tokens for this strategy. 17 | * @notice In contrast to `sharesToUnderlying`, this function guarantees no state modifications 18 | * @param amountShares is the amount of shares to calculate its conversion into the underlying token 19 | * @return The amount of shares corresponding to the input `amountUnderlying` 20 | * @dev Implementation for these functions in particular may vary significantly for different strategies 21 | */ 22 | function sharesToUnderlyingView(uint256 amountShares) external view returns (uint256); 23 | } 24 | -------------------------------------------------------------------------------- /src/interface/Lido/IWstETH.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | interface IWstETH { 5 | /** 6 | * @notice Exchanges stETH to wstETH 7 | * @param _stETHAmount amount of stETH to wrap in exchange for wstETH 8 | * @dev Requirements: 9 | * - `_stETHAmount` must be non-zero 10 | * - msg.sender must approve at least `_stETHAmount` stETH to this 11 | * contract. 12 | * - msg.sender must have at least `_stETHAmount` of stETH. 13 | * User should first approve _stETHAmount to the WstETH contract 14 | * @return Amount of wstETH user receives after wrap 15 | */ 16 | function wrap(uint256 _stETHAmount) external returns (uint256); 17 | 18 | /** 19 | * @notice Exchanges wstETH to stETH 20 | * @param _wstETHAmount amount of wstETH to unwrap in exchange for stETH 21 | * @dev Requirements: 22 | * - `_wstETHAmount` must be non-zero 23 | * - msg.sender must have at least `_wstETHAmount` wstETH. 24 | * @return Amount of stETH user receives after unwrap 25 | */ 26 | function unwrap(uint256 _wstETHAmount) external returns (uint256); 27 | } 28 | -------------------------------------------------------------------------------- /slither.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "detectors_to_run": "arbitrary-send-erc20,array-by-reference,encode-packed-collision,incorrect-shift,name-reused,rtlo,shadowing-state,suicidal,uninitialized-state,uninitialized-storage,unprotected-upgrade,arbitrary-send-erc20-permit,arbitrary-send-eth,controlled-array-length,controlled-delegatecall,delegatecall-loop,msg-value-loop,reentrancy-eth,unchecked-transfer,weak-prng,domain-separator-collision,erc20-interface,erc721-interface,locked-ether,incorrect-equality,mapping-deletion,shadowing-abstract,tautology,write-after-write,boolean-cst,reentrancy-no-eth,reused-constructor,divide-before-multiply,tx-origin,unchecked-lowlevel,calls-loop,unchecked-send,variable-scope,void-cst,events-access,events-maths,incorrect-unary,boolean-equal,cyclomatic-complexity,deprecated-standards,erc20-indexed,function-init-state,unused-state,reentrancy-unlimited-gas,constable-states,immutable-states,var-read-using-this,redundant-statements,dead-code", 3 | "exclude_informational": false, 4 | "exclude_low": false, 5 | "exclude_medium": false, 6 | "exclude_high": false, 7 | "disable_color": false, 8 | "filter_paths": "(lib/|test/|script/)", 9 | "legacy_ast": false 10 | } -------------------------------------------------------------------------------- /src/interface/IPufferDepositorV2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import { Permit } from "../structs/Permit.sol"; 5 | 6 | /** 7 | * @title IPufferDepositorV2 8 | * @author Puffer Finance 9 | * @custom:security-contact security@puffer.fi 10 | */ 11 | interface IPufferDepositorV2 { 12 | /** 13 | * @notice Deposits wrapped stETH (wstETH) into the Puffer Vault 14 | * @param permitData The permit data containing the approval information 15 | * @param recipient The recipient of pufETH tokens 16 | * @return pufETHAmount The amount of pufETH received from the deposit 17 | */ 18 | function depositWstETH(Permit calldata permitData, address recipient) external returns (uint256 pufETHAmount); 19 | 20 | /** 21 | * @notice Deposits stETH into the Puffer Vault using Permit 22 | * @param permitData The permit data containing the approval information 23 | * @param recipient The recipient of pufETH tokens 24 | * @return pufETHAmount The amount of pufETH received from the deposit 25 | */ 26 | function depositStETH(Permit calldata permitData, address recipient) external returns (uint256 pufETHAmount); 27 | } 28 | -------------------------------------------------------------------------------- /script/Roles.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | // Operations & Community multisig have this role 5 | // Operations with 7 day delay, Community 0 6 | // Deprecated 7 | uint64 constant ROLE_ID_UPGRADER = 1; 8 | 9 | // Role assigned to Operations Multisig 10 | uint64 constant ROLE_ID_OPERATIONS_MULTISIG = 22; 11 | uint64 constant ROLE_ID_OPERATIONS_PAYMASTER = 23; 12 | uint64 constant ROLE_ID_OPERATIONS_COORDINATOR = 24; 13 | 14 | // Role assigned to validator ticket price setter 15 | uint64 constant ROLE_ID_VT_PRICER = 25; 16 | 17 | // Role assigned to the Puffer Protocol 18 | uint64 constant ROLE_ID_PUFFER_PROTOCOL = 1234; 19 | uint64 constant ROLE_ID_DAO = 77; 20 | uint64 constant ROLE_ID_GUARDIANS = 88; 21 | uint64 constant ROLE_ID_PUFFER_ORACLE = 999; 22 | 23 | // Public role (defined in AccessManager.sol) 24 | uint64 constant PUBLIC_ROLE = type(uint64).max; 25 | // Admin role (defined in AccessManager.sol) (only Timelock.sol must have this role) 26 | uint64 constant ADMIN_ROLE = 0; 27 | 28 | // Allowlister role for AVSContractsRegistry 29 | uint64 constant ROLE_ID_AVS_COORDINATOR_ALLOWLISTER = 5; 30 | 31 | // Lockbox role for ETH Mainnet 32 | uint64 constant ROLE_ID_LOCKBOX = 7; 33 | -------------------------------------------------------------------------------- /test/mocks/MockPufferOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import { IPufferOracleV2 } from "../../src/interface/IPufferOracleV2.sol"; 5 | 6 | /** 7 | * @title MockPufferOracle 8 | * @author Puffer Finance 9 | * @custom:security-contact security@puffer.fi 10 | */ 11 | contract MockPufferOracle is IPufferOracleV2 { 12 | /** 13 | * @dev Number of blocks 14 | */ 15 | // slither-disable-next-line unused-state 16 | uint256 internal constant _UPDATE_INTERVAL = 1; 17 | 18 | uint152 public lockedETH; 19 | 20 | uint56 public lastUpdate; 21 | 22 | uint256 public numberOfActiveValidators; 23 | 24 | function getLastUpdate() external view returns (uint256) { 25 | return lastUpdate; 26 | } 27 | 28 | function getTotalNumberOfValidators() external pure returns (uint256) { 29 | return 99999; 30 | } 31 | 32 | function provisionNode() external { } 33 | function exitValidators(uint256) external { } 34 | 35 | function getValidatorTicketPrice() external view returns (uint256 pricePerVT) { } 36 | 37 | function getLockedEthAmount() external view returns (uint256 lockedEthAmount) { } 38 | 39 | function isOverBurstThreshold() external view returns (bool) { } 40 | } 41 | -------------------------------------------------------------------------------- /test/mocks/EigenLayerManagerMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import { IEigenLayer } from "../../src/interface/EigenLayer/IEigenLayer.sol"; 5 | import { IERC20 } from "openzeppelin/token/ERC20/IERC20.sol"; 6 | import { IStrategy } from "../../src/interface/EigenLayer/IStrategy.sol"; 7 | 8 | contract EigenLayerManagerMock is IEigenLayer { 9 | function depositIntoStrategy(IStrategy strategy, IERC20 token, uint256 amount) external returns (uint256 shares) { } 10 | 11 | function queueWithdrawal( 12 | uint256[] calldata strategyIndexes, 13 | IStrategy[] calldata strategies, 14 | uint256[] calldata shares, 15 | address withdrawer, 16 | bool undelegateIfPossible 17 | ) external returns (bytes32) { } 18 | 19 | function completeQueuedWithdrawal( 20 | IEigenLayer.QueuedWithdrawal calldata queuedWithdrawal, 21 | IERC20[] calldata tokens, 22 | uint256 middlewareTimesIndex, 23 | bool receiveAsTokens 24 | ) external { } 25 | 26 | function stakerStrategyShares(address staker, IStrategy strategy) external view returns (uint256 shares) { } 27 | 28 | function calculateWithdrawalRoot(QueuedWithdrawal memory queuedWithdrawal) external pure returns (bytes32) { } 29 | } 30 | -------------------------------------------------------------------------------- /src/PufferDepositorStorage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | /** 5 | * @title PufferDepositorStorage 6 | * @author Puffer Finance 7 | * @custom:security-contact security@puffer.fi 8 | */ 9 | abstract contract PufferDepositorStorage { 10 | /** 11 | * @custom:storage-location erc7201:pufferdepositor.storage 12 | * @dev +-----------------------------------------------------------+ 13 | * | | 14 | * | DO NOT CHANGE, REORDER, REMOVE EXISTING STORAGE VARIABLES | 15 | * | | 16 | * +-----------------------------------------------------------+ 17 | */ 18 | // struct DepositorStorage { 19 | // } 20 | 21 | // keccak256(abi.encode(uint256(keccak256("pufferdepositor.storage")) - 1)) & ~bytes32(uint256(0xff)) 22 | bytes32 private constant _DEPOSITOR_STORAGE_LOCATION = 23 | 0xfe00eacac09c3a4f9370afc23b4b368378559810af33ed029b1efbfeeaccaf00; 24 | 25 | // function _getDepositorStorage() internal pure returns (DepositorStorage storage $) { 26 | // // solhint-disable-next-line no-inline-assembly 27 | // assembly { 28 | // $.slot := _DEPOSITOR_STORAGE_LOCATION 29 | // } 30 | // } 31 | } 32 | -------------------------------------------------------------------------------- /src/PufferVaultV2Tests.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import { PufferVaultV2 } from "./PufferVaultV2.sol"; 5 | import { IStETH } from "./interface/Lido/IStETH.sol"; 6 | import { ILidoWithdrawalQueue } from "./interface/Lido/ILidoWithdrawalQueue.sol"; 7 | import { IEigenLayer } from "./interface/EigenLayer/IEigenLayer.sol"; 8 | import { IStrategy } from "./interface/EigenLayer/IStrategy.sol"; 9 | import { IWETH } from "./interface/Other/IWETH.sol"; 10 | import { IPufferOracle } from "./interface/IPufferOracle.sol"; 11 | import { IDelegationManager } from "../src/interface/EigenLayer/IDelegationManager.sol"; 12 | 13 | contract PufferVaultV2Tests is PufferVaultV2 { 14 | constructor( 15 | IStETH stETH, 16 | IWETH weth, 17 | ILidoWithdrawalQueue lidoWithdrawalQueue, 18 | IStrategy stETHStrategy, 19 | IEigenLayer eigenStrategyManager, 20 | IPufferOracle oracle, 21 | IDelegationManager delegationManager 22 | ) PufferVaultV2(stETH, weth, lidoWithdrawalQueue, stETHStrategy, eigenStrategyManager, oracle, delegationManager) { 23 | _WETH = weth; 24 | PUFFER_ORACLE = oracle; 25 | _disableInitializers(); 26 | } 27 | 28 | // This functionality must be disabled because of the foundry tests 29 | modifier markDeposit() virtual override { 30 | _; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/interface/IPufferOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | /** 5 | * @title IPufferOracle 6 | * @author Puffer Finance 7 | * @custom:security-contact security@puffer.fi 8 | */ 9 | interface IPufferOracle { 10 | /** 11 | * @notice Thrown if the new ValidatorTicket mint price is invalid 12 | */ 13 | error InvalidValidatorTicketPrice(); 14 | 15 | /** 16 | * @notice Emitted when the price to mint ValidatorTicket is updated 17 | * @dev Signature "0xf76811fec27423d0853e6bf49d7ea78c666629c2f67e29647d689954021ae0ea" 18 | */ 19 | event ValidatorTicketMintPriceUpdated(uint256 oldPrice, uint256 newPrice); 20 | 21 | /** 22 | * @notice Retrieves the current mint price for minting one ValidatorTicket 23 | * @return pricePerVT The current ValidatorTicket mint price 24 | */ 25 | function getValidatorTicketPrice() external view returns (uint256 pricePerVT); 26 | 27 | /** 28 | * @notice Returns true if the number of active Puffer Validators is over the burst threshold 29 | */ 30 | function isOverBurstThreshold() external view returns (bool); 31 | 32 | /** 33 | * @notice Returns the locked ETH amount 34 | * @return lockedEthAmount The amount of ETH locked in Beacon chain 35 | */ 36 | function getLockedEthAmount() external view returns (uint256 lockedEthAmount); 37 | } 38 | -------------------------------------------------------------------------------- /script/BaseScript.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import "forge-std/Script.sol"; 5 | 6 | /** 7 | * @title Base Script 8 | * @author Puffer Finance 9 | */ 10 | abstract contract BaseScript is Script { 11 | uint256 internal PK = 55358659325830545179143827536745912452716312441367500916455484419538098489698; // makeAddr("pufferDeployer") 12 | 13 | /** 14 | * @dev Deployer private key is in `PK` env variable 15 | */ 16 | uint256 internal _deployerPrivateKey = vm.envOr("PK", PK); 17 | address internal _broadcaster = vm.addr(_deployerPrivateKey); 18 | 19 | constructor() { 20 | // For local chain (ANVIL) hardcode the deployer as first account from the blockchain 21 | if (isAnvil()) { 22 | // Fist account from ANVIL 23 | _deployerPrivateKey = uint256(0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80); 24 | _broadcaster = vm.addr(_deployerPrivateKey); 25 | } 26 | } 27 | 28 | modifier broadcast() { 29 | vm.startBroadcast(_deployerPrivateKey); 30 | _; 31 | vm.stopBroadcast(); 32 | } 33 | 34 | function isMainnet() internal view returns (bool) { 35 | return (block.chainid == 1); 36 | } 37 | 38 | function isHolesky() internal view returns (bool) { 39 | return (block.chainid == 17000); 40 | } 41 | 42 | function isAnvil() internal view returns (bool) { 43 | return (block.chainid == 31337); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | env: 9 | FOUNDRY_PROFILE: ci 10 | 11 | jobs: 12 | tests: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Install Foundry 18 | uses: foundry-rs/foundry-toolchain@v1 19 | with: 20 | version: nightly-de33b6af53005037b463318d2628b5cfcaf39916 21 | 22 | - name: Install deps 23 | run: forge install 24 | 25 | - name: Run tests 26 | run: forge test -vvv --match-path './test/unit/*' 27 | 28 | # - name: Check gas snapshots 29 | # run: forge snapshot 30 | 31 | slither: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v4 35 | 36 | - uses: crytic/slither-action@v0.3.0 37 | with: 38 | node-version: 16 39 | 40 | solhint: 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@v4 44 | - uses: actions/setup-node@v3 45 | - name: Install solhint 46 | run: npm i -g solhint 47 | - name: Run solhint 48 | run: solhint 'src/*.sol' 49 | 50 | codespell: 51 | runs-on: ubuntu-latest 52 | steps: 53 | - uses: actions/checkout@v4 54 | - name: Run CodeSpell 55 | uses: codespell-project/actions-codespell@v2.0 56 | with: 57 | check_hidden: true 58 | check_filenames: true 59 | ignore_words_list: amountIn 60 | skip: package-lock.json,*.pdf,./.git 61 | 62 | -------------------------------------------------------------------------------- /src/l2/xPufETHStorage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import { IXERC20 } from "../interface/IXERC20.sol"; 5 | 6 | /** 7 | * @title xPufETHStorage 8 | * @author Puffer Finance 9 | * @custom:security-contact security@puffer.fi 10 | */ 11 | abstract contract xPufETHStorage { 12 | /** 13 | * @custom:storage-location erc7201:xPufETH.storage 14 | * @dev +-----------------------------------------------------------+ 15 | * | | 16 | * | DO NOT CHANGE, REORDER, REMOVE EXISTING STORAGE VARIABLES | 17 | * | | 18 | * +-----------------------------------------------------------+ 19 | */ 20 | struct xPufETH { 21 | /** 22 | * @notice The address of the lockbox contract 23 | */ 24 | address lockbox; 25 | /** 26 | * @notice Maps bridge address to bridge configurations 27 | */ 28 | mapping(address bridge => IXERC20.Bridge config) bridges; 29 | } 30 | 31 | // keccak256(abi.encode(uint256(keccak256("xPufETH.storage")) - 1)) & ~bytes32(uint256(0xff)) 32 | bytes32 private constant _STORAGE_LOCATION = 0xfee41a6d2b86b757dd00cd2166d8727686a349977cbc2b6b6a2ca1c3e7215000; 33 | 34 | function _getXPufETHStorage() internal pure returns (xPufETH storage $) { 35 | // solhint-disable-next-line no-inline-assembly 36 | assembly { 37 | $.slot := _STORAGE_LOCATION 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/echidna/EchidnaPufferVaultV2.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | import { CryticERC4626PropertyTests } from "properties/ERC4626/ERC4626PropertyTests.sol"; 4 | import { PufferVaultV2 } from "../PufferVaultV2.sol"; 5 | import { WETH9 } from "../../test/mocks/WETH9.sol"; 6 | import { stETHMock } from "../../test/mocks/stETHMock.sol"; 7 | import { MockPufferOracle } from "../../test/mocks/MockPufferOracle.sol"; 8 | import { EigenLayerManagerMock } from "../../test/mocks/EigenLayerManagerMock.sol"; 9 | import { EigenLayerDelegationManagerMock } from "../../test/mocks/EigenLayerDelegationManagerMock.sol"; 10 | import { LidoWithdrawalQueueMock } from "../../test/mocks/LidoWithdrawalQueueMock.sol"; 11 | import { stETHStrategyMock } from "../../test/mocks/stETHStrategyMock.sol"; 12 | import { TestERC20Token } from "properties/ERC4626/util/TestERC20Token.sol"; 13 | 14 | contract EchidnaPufferVaultV2 is CryticERC4626PropertyTests { 15 | constructor() { 16 | WETH9 weth = new WETH9(); 17 | stETHMock stETH = new stETHMock(); 18 | MockPufferOracle oracle = new MockPufferOracle(); 19 | EigenLayerManagerMock eigenlayer = new EigenLayerManagerMock(); 20 | LidoWithdrawalQueueMock lido = new LidoWithdrawalQueueMock(); 21 | stETHStrategyMock stETHStrategy = new stETHStrategyMock(); 22 | EigenLayerDelegationManagerMock delegationMock = new EigenLayerDelegationManagerMock(); 23 | PufferVaultV2 vault = new PufferVaultV2(stETH, weth, lido, stETHStrategy, eigenlayer, oracle, delegationMock); 24 | initialize(address(vault), address(weth), false); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/interface/Lido/IStETH.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import { IERC20 } from "openzeppelin/token/ERC20/IERC20.sol"; 5 | 6 | interface IStETH is IERC20 { 7 | /** 8 | * @return the amount of Ether that corresponds to `_sharesAmount` token shares. 9 | */ 10 | function getPooledEthByShares(uint256 _sharesAmount) external view returns (uint256); 11 | 12 | /** 13 | * @return the amount of shares that corresponds to `_ethAmount` protocol-controlled Ether. 14 | */ 15 | function getSharesByPooledEth(uint256 _pooledEthAmount) external view returns (uint256); 16 | 17 | function getTotalPooledEther() external view returns (uint256); 18 | 19 | function transferShares(address _recipient, uint256 _sharesAmount) external returns (uint256); 20 | 21 | function transferSharesFrom(address _sender, address _recipient, uint256 _sharesAmount) 22 | external 23 | returns (uint256); 24 | 25 | /** 26 | * @return the amount of tokens in existence. 27 | * 28 | * @dev Always equals to `_getTotalPooledEther()` since token amount 29 | * is pegged to the total amount of Ether controlled by the protocol. 30 | */ 31 | function totalSupply() external view returns (uint256); 32 | 33 | /** 34 | * @dev Process user deposit, mints liquid tokens and increase the pool buffer 35 | * @param _referral address of referral. 36 | * @return amount of StETH shares generated 37 | */ 38 | function submit(address _referral) external payable returns (uint256); 39 | 40 | /** 41 | * @notice Returns the number of shares owned by `_account` 42 | */ 43 | function sharesOf(address _account) external view returns (uint256); 44 | } 45 | -------------------------------------------------------------------------------- /docs/Timelock.md: -------------------------------------------------------------------------------- 1 | # Timelock 2 | 3 | ### Overview 4 | 5 | These Puffer contracts implement a timelock such that actions cannot be taken by the Puffer team to change the functionality of the protocol without prior warning to users. 6 | 7 | ### Community and Operations Multisigs 8 | 9 | There are three different multisigs that together have the capabilities to upgrade the contract, pause the contract, or perform planned phases corresponding with our launch, such as depositing into EigenLayer's stETH strategy contract if their cap has not been reached, and redeeming stETH for ETH via Lido. These three multisigs are referred to as the community multisig, the operations multisig, and the pauser. The community multisig will consist of trusted partners and respected members of the Ethereum community. This multisig will intervene in the protocol if any issues are found, and is allowed to execute transactions immediately without queueing. The operations multisig will consist of the core Puffer team, and will have a variable time lock period. There is a minimum of a 2 day lock time period enforced for this multisig. This means the operations multisig must always queue desired transactions for at least 2 days before being able to execute them. Both the community and operations multisigs can cancel queued transactions. The pauser multisig is the only multisig capable of pausing functionalities on the contracts, and can do so without queueing or delaying. 10 | 11 | ### Mechanism 12 | 13 | The way that the timelock mechanism will work is as follows: 14 | 15 | 1. Either multisig will queue up a sensitive transaction, e.g. pausing a function on the contracts 16 | 2. The corresponding time must pass before any change can be made to the protocol, giving users a chance to react to the change being made 17 | 2a. During this waiting period, the queued transaction may be cancelled by either multisig 18 | 3. Either multisig can execute the transaction, after their corresponding time period has elapsed, and after confirmation of such transaction, the awaited protocol change will go into effect -------------------------------------------------------------------------------- /src/interface/IXERC20Lockbox.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.4 <0.9.0; 3 | 4 | interface IXERC20Lockbox { 5 | /** 6 | * @notice Emitted when tokens are deposited into the lockbox 7 | * 8 | * @param sender The address of the user who deposited 9 | * @param amount The amount of tokens deposited 10 | */ 11 | event Deposit(address sender, uint256 amount); 12 | 13 | /** 14 | * @notice Emitted when tokens are withdrawn from the lockbox 15 | * 16 | * @param sender The address of the user who withdrew 17 | * @param amount The amount of tokens withdrawn 18 | */ 19 | event Withdraw(address sender, uint256 amount); 20 | 21 | /** 22 | * @notice Reverts when a user tries to deposit native tokens on a non-native lockbox 23 | */ 24 | error IXERC20Lockbox_NotNative(); 25 | 26 | /** 27 | * @notice Deposit ERC20 tokens into the lockbox 28 | * 29 | * @param amount The amount of tokens to deposit 30 | */ 31 | function deposit(uint256 amount) external; 32 | 33 | /** 34 | * @notice Deposit ERC20 tokens into the lockbox, and send the XERC20 to a user 35 | * 36 | * @param user The user to send the XERC20 to 37 | * @param amount The amount of tokens to deposit 38 | */ 39 | function depositTo(address user, uint256 amount) external; 40 | 41 | /** 42 | * @notice Deposit the native asset into the lockbox, and send the XERC20 to a user 43 | * 44 | * @param user The user to send the XERC20 to 45 | */ 46 | function depositNativeTo(address user) external payable; 47 | 48 | /** 49 | * @notice Withdraw ERC20 tokens from the lockbox 50 | * 51 | * @param amount The amount of tokens to withdraw 52 | */ 53 | function withdraw(uint256 amount) external; 54 | 55 | /** 56 | * @notice Withdraw ERC20 tokens from the lockbox 57 | * 58 | * @param user The user to withdraw to 59 | * @param amount The amount of tokens to withdraw 60 | */ 61 | function withdrawTo(address user, uint256 amount) external; 62 | } 63 | -------------------------------------------------------------------------------- /src/interface/EigenLayer/IEigenLayer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import { IERC20 } from "openzeppelin/token/ERC20/IERC20.sol"; 5 | import { IStrategy } from "./IStrategy.sol"; 6 | 7 | interface IEigenLayer { 8 | /** 9 | * packed struct for queued withdrawals; helps deal with stack-too-deep errors 10 | */ 11 | struct WithdrawerAndNonce { 12 | address withdrawer; 13 | uint96 nonce; 14 | } 15 | /** 16 | * Struct type used to specify an existing queued withdrawal. Rather than storing the entire struct, only a hash is stored. 17 | * In functions that operate on existing queued withdrawals -- e.g. `startQueuedWithdrawalWaitingPeriod` or `completeQueuedWithdrawal`, 18 | * the data is resubmitted and the hash of the submitted data is computed by `calculateWithdrawalRoot` and checked against the 19 | * stored hash in order to confirm the integrity of the submitted data. 20 | */ 21 | 22 | struct QueuedWithdrawal { 23 | IStrategy[] strategies; 24 | uint256[] shares; 25 | address depositor; 26 | WithdrawerAndNonce withdrawerAndNonce; 27 | uint32 withdrawalStartBlock; 28 | address delegatedAddress; 29 | } 30 | 31 | function depositIntoStrategy(IStrategy strategy, IERC20 token, uint256 amount) external returns (uint256 shares); 32 | 33 | function stakerStrategyShares(address staker, IStrategy strategy) external view returns (uint256 shares); 34 | 35 | function queueWithdrawal( 36 | uint256[] calldata strategyIndexes, 37 | IStrategy[] calldata strategies, 38 | uint256[] calldata shares, 39 | address withdrawer, 40 | bool undelegateIfPossible 41 | ) external returns (bytes32); 42 | 43 | function completeQueuedWithdrawal( 44 | QueuedWithdrawal calldata queuedWithdrawal, 45 | IERC20[] calldata tokens, 46 | uint256 middlewareTimesIndex, 47 | bool receiveAsTokens 48 | ) external; 49 | 50 | function calculateWithdrawalRoot(QueuedWithdrawal memory queuedWithdrawal) external pure returns (bytes32); 51 | } 52 | -------------------------------------------------------------------------------- /docs/PufferDepositorV2.md: -------------------------------------------------------------------------------- 1 | # PufferDepositor 2 | 3 | ### [PufferDepositorV2](./PufferDepositorV2.md) 4 | 5 | | File | Type | Upgradeable | Inherited | Deployed | 6 | | -------- | -------- | -------- | -------- | -------- | 7 | | [`IPufferDepositorV2.sol`](../src/interface/IPufferDepositorV2.sol) | Singleton | / | YES | / | 8 | | [`PufferDepositorV2.sol`](../src/PufferDepositorV2.sol) | Singleton | UUPS Proxy | NO | / | 9 | | [`PufferDepositorStorage.sol`](../src/PufferDepositorStorage.sol) | Singleton | UUPS Proxy | YES | / | 10 | 11 | The PufferDepositorV2 facilitates depositing stETH and wstETH into the [PufferVaultV2](./PufferVaultV2.md). 12 | 13 | #### Important state variables 14 | 15 | The only state information the PufferDepositor contract holds are the addresses of stETH (`_ST_ETH`) and wstETH (`_WST_ETH`). 16 | 17 | --- 18 | 19 | ### Functions 20 | 21 | #### `depositStETH` 22 | 23 | ```solidity 24 | function depositStETH(Permit calldata permitData, address recipient) 25 | external 26 | restricted 27 | returns (uint256 pufETHAmount) 28 | ``` 29 | 30 | Interface function to deposit stETH into the `PufferVault` contract, which mints pufETH for the `recipient`. 31 | 32 | *Effects* 33 | * Takes the specified amount of stETH from the caller 34 | * Deposits the stETH into the `PufferVault` contract 35 | * Mints pufETH for the `recipient`, corresponding to the stETH amount deposited 36 | 37 | *Requirements* 38 | * Called must have previously approved the amount of stETH to be sent to the `PufferDepositor` contract 39 | 40 | #### `depositWstETH` 41 | 42 | ```solidity 43 | function depositWstETH(Permit calldata permitData, address recipient) 44 | external 45 | restricted 46 | returns (uint256 pufETHAmount) 47 | ``` 48 | 49 | Interface function to deposit wstETH into the `PufferVault` contract, which mints pufETH for the `recipient`. 50 | 51 | *Effects* 52 | * Takes the specified amount of wstETH from the caller 53 | * Unwraps the wstETH into stETH 54 | * Deposits the stETH into the `PufferVault` contract 55 | * Mints pufETH for the `recipient`, corresponding to the stETH amount deposited 56 | 57 | *Requirements* 58 | * Called must have previously approved the amount of wstETH to be sent to the `PufferDepositor` contract -------------------------------------------------------------------------------- /src/interface/IPufferOracleV2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import { IPufferOracle } from "./IPufferOracle.sol"; 5 | 6 | /** 7 | * @title IPufferOracle 8 | * @author Puffer Finance 9 | * @custom:security-contact security@puffer.fi 10 | */ 11 | interface IPufferOracleV2 is IPufferOracle { 12 | error InvalidUpdate(); 13 | /** 14 | * @notice Emitted when the number of active Puffer validators is updated 15 | * @param numberOfActivePufferValidators is the number of active Puffer validators 16 | */ 17 | 18 | event NumberOfActiveValidators(uint256 numberOfActivePufferValidators); 19 | 20 | /** 21 | * @notice Emitted when the total number of validators is updated 22 | * @param oldNumberOfValidators is the old number of validators 23 | * @param newNumberOfValidators is the new number of validators 24 | */ 25 | event TotalNumberOfValidatorsUpdated( 26 | uint256 oldNumberOfValidators, uint256 newNumberOfValidators, uint256 epochNumber 27 | ); 28 | 29 | /** 30 | * @notice Returns the total number of active validators on Ethereum 31 | */ 32 | function getTotalNumberOfValidators() external view returns (uint256); 33 | 34 | /** 35 | * @notice Exits `validatorNumber` validators, decreasing the `lockedETHAmount` by validatorNumber * 32 ETH. 36 | * It is called when when the validator exits the system in the `batchHandleWithdrawals` on the PufferProtocol. 37 | * In the same transaction, we are transferring full withdrawal ETH from the PufferModule to the Vault 38 | * Decrementing the `lockedETHAmount` by 32 ETH and we burn the Node Operator's pufETH (bond) if we need to cover up the loss. 39 | * @dev Restricted to PufferProtocol contract 40 | */ 41 | function exitValidators(uint256 validatorNumber) external; 42 | 43 | /** 44 | * @notice Increases the `lockedETHAmount` on the PufferOracle by 32 ETH to account for a new deposit. 45 | * It is called when the Beacon chain receives a new deposit from the PufferProtocol. 46 | * The PufferVault's balance will simultaneously decrease by 32 ETH as the deposit is made. 47 | * @dev Restricted to PufferProtocol contract 48 | */ 49 | function provisionNode() external; 50 | } 51 | -------------------------------------------------------------------------------- /src/PufferVaultStorage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; 5 | import { EnumerableMap } from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; 6 | 7 | /** 8 | * @title PufferVaultStorage 9 | * @author Puffer Finance 10 | * @custom:security-contact security@puffer.fi 11 | */ 12 | abstract contract PufferVaultStorage { 13 | /** 14 | * @custom:storage-location erc7201:puffervault.storage 15 | * @dev +-----------------------------------------------------------+ 16 | * | | 17 | * | DO NOT CHANGE, REORDER, REMOVE EXISTING STORAGE VARIABLES | 18 | * | | 19 | * +-----------------------------------------------------------+ 20 | */ 21 | struct VaultStorage { 22 | // 6 Slots for Redemption logic 23 | uint256 lidoLockedETH; 24 | uint256 eigenLayerPendingWithdrawalSharesAmount; 25 | bool isLidoWithdrawal; // Not in use in PufferVaultV2 26 | EnumerableSet.UintSet lidoWithdrawals; // Not in use in PufferVaultV2 27 | EnumerableSet.Bytes32Set eigenLayerWithdrawals; 28 | EnumerableMap.UintToUintMap lidoWithdrawalAmounts; 29 | // 1 Slot for daily withdrawal limits 30 | uint96 dailyAssetsWithdrawalLimit; 31 | uint96 assetsWithdrawnToday; 32 | uint64 lastWithdrawalDay; 33 | // 1 slot for withdrawal fee 34 | uint256 exitFeeBasisPoints; 35 | } 36 | 37 | // keccak256(abi.encode(uint256(keccak256("puffervault.depositTracker")) - 1)) & ~bytes32(uint256(0xff)) 38 | bytes32 internal constant _DEPOSIT_TRACKER_LOCATION = 39 | 0x78b7b410d94d33094d5b8a71f1c003e2cbb9e212054d2df1984e3dabc3b25e00; 40 | 41 | // keccak256(abi.encode(uint256(keccak256("puffervault.storage")) - 1)) & ~bytes32(uint256(0xff)) 42 | bytes32 private constant _VAULT_STORAGE_LOCATION = 43 | 0x611ea165ca9257827fc43d2954fdae7d825e82c825d9037db9337fa1bfa93100; 44 | 45 | function _getPufferVaultStorage() internal pure returns (VaultStorage storage $) { 46 | // solhint-disable-next-line no-inline-assembly 47 | assembly { 48 | $.slot := _VAULT_STORAGE_LOCATION 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/mocks/WETH9.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import { IWETH } from "../../src/interface/Other/IWETH.sol"; 5 | 6 | contract WETH9 is IWETH { 7 | string public name = "Wrapped Ether"; 8 | string public symbol = "WETH"; 9 | uint8 public decimals = 18; 10 | 11 | mapping(address => uint256) public balanceOf; 12 | mapping(address => mapping(address => uint256)) public allowance; 13 | 14 | receive() external payable { 15 | deposit(); 16 | } 17 | 18 | fallback() external payable { 19 | deposit(); 20 | } 21 | 22 | function mint(address to, uint256 value) external { 23 | balanceOf[to] += value; 24 | emit Transfer(address(0), to, value); 25 | } 26 | 27 | function burn(address from, uint256 value) external { 28 | balanceOf[from] -= value; 29 | emit Transfer(from, address(0), value); 30 | } 31 | 32 | function deposit() public payable { 33 | balanceOf[msg.sender] += msg.value; 34 | emit Deposit(msg.sender, msg.value); 35 | } 36 | 37 | function withdraw(uint256 wad) public { 38 | require(balanceOf[msg.sender] >= wad); 39 | balanceOf[msg.sender] -= wad; 40 | payable(msg.sender).transfer(wad); 41 | emit Withdrawal(msg.sender, wad); 42 | } 43 | 44 | function totalSupply() public view returns (uint256) { 45 | return address(this).balance; 46 | } 47 | 48 | function approve(address guy, uint256 wad) public returns (bool) { 49 | allowance[msg.sender][guy] = wad; 50 | emit Approval(msg.sender, guy, wad); 51 | return true; 52 | } 53 | 54 | function transfer(address dst, uint256 wad) public returns (bool) { 55 | return transferFrom(msg.sender, dst, wad); 56 | } 57 | 58 | function transferFrom(address src, address dst, uint256 wad) public returns (bool) { 59 | require(balanceOf[src] >= wad, "?????"); 60 | 61 | if (src != msg.sender && allowance[src][msg.sender] != type(uint256).max) { 62 | require(allowance[src][msg.sender] >= wad); 63 | allowance[src][msg.sender] -= wad; 64 | } 65 | 66 | balanceOf[src] -= wad; 67 | balanceOf[dst] += wad; 68 | 69 | emit Transfer(src, dst, wad); 70 | 71 | return true; 72 | } 73 | 74 | function forceApproval(address account, address spender, uint256 amount) public { 75 | allowance[account][spender] = amount; 76 | emit Approval(account, spender, amount); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /test/mocks/stETHMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; 6 | import "../../src/interface/Lido/IStETH.sol"; 7 | 8 | contract stETHMock is IStETH, ERC20, ERC20Burnable { 9 | constructor() ERC20("Mock stETH", "mockStETH") { } 10 | 11 | uint256 public totalShares; 12 | uint256 public totalPooledEther; 13 | 14 | function mint(address to, uint256 value) external { 15 | _mint(to, value); 16 | } 17 | 18 | function burn(address from, uint256 value) external { 19 | _burn(from, value); 20 | } 21 | 22 | function slash(address holder, uint256 amount) public { 23 | _burn(holder, amount); 24 | } 25 | 26 | function submit(address /*referral*/ ) external payable returns (uint256) { 27 | uint256 sharesToMint = getSharesByPooledEth(msg.value); 28 | _mint(msg.sender, sharesToMint); 29 | return sharesToMint; 30 | } 31 | 32 | function setTotalShares(uint256 _totalShares) public { 33 | totalShares = _totalShares; 34 | } 35 | 36 | function setTotalPooledEther(uint256 _totalPooledEther) public { 37 | totalPooledEther = _totalPooledEther; 38 | } 39 | 40 | function getTotalPooledEther() external view returns (uint256) { 41 | return totalPooledEther; 42 | } 43 | 44 | function sharesOf(address _account) external view returns (uint256) { 45 | //not implemented, just there for IstETH conformance 46 | } 47 | 48 | function transferSharesFrom(address _sender, address _recipient, uint256 _sharesAmount) 49 | external 50 | returns (uint256) 51 | { } 52 | 53 | function transferShares(address _recipient, uint256 _sharesAmount) external returns (uint256) { 54 | //not implemented, just there for IstETH conformance 55 | } 56 | 57 | function getPooledEthByShares(uint256 _sharesAmount) public view returns (uint256) { 58 | if (totalShares == 0) { 59 | return 0; 60 | } 61 | return _sharesAmount * totalPooledEther / totalShares; 62 | } 63 | 64 | function getSharesByPooledEth(uint256 _pooledEthAmount) public view returns (uint256) { 65 | if (totalPooledEther == 0) { 66 | return 0; 67 | } 68 | return _pooledEthAmount * totalShares / totalPooledEther; 69 | } 70 | 71 | function totalSupply() public view override(ERC20, IStETH) returns (uint256) { 72 | return super.totalSupply(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /docs/PufferOracle.md: -------------------------------------------------------------------------------- 1 | # PufferOracle 2 | 3 | | File | Type | Upgradeable | Inherited | Deployed | 4 | | -------- | -------- | -------- | -------- | -------- | 5 | | [`PufferOracle.sol`](../src/PufferOracle.sol) | Singleton | / | NO | / | 6 | 7 | This contract allows Guardians to post Proof of Reserves, proving the amount of ETH backing pufETH, and therefore establishing a fair market value of pufETH 8 | 9 | #### High-level Concepts 10 | 11 | This document organizes methods according to the following themes (click each to be taken to the relevant section): 12 | * [Proof of Reserves](#proof-of-reserves) 13 | 14 | #### Helpful Definitions 15 | 16 | * Proof of Reserves: The protocol will regularly post the amount of ETH under its control, which corresponds to the amount of assets backing the pufETH token. Initially this will be done via a trusted party, but eventually Puffer will move to a trustless solution 17 | 18 | #### Important state variables 19 | 20 | The PufferOracle contract maintains state variables related to the amount of ETH backing the pufETH token. The important state variables are described below: 21 | 22 | * `uint256 internal constant _UPDATE_INTERVAL`: Defines an interval of blocks that Proof of Reserves may not be posted twice within. For example, if the interval is 10 blocks, and Proof of Reserves was posted on block 1, then Proof of Reserves may not be posted again until block 11 23 | * `uint256 ethAmount`: The amount of ETH that is not locked in the beacon chain, corresponding to a running validator. This could be ETH that lives within the `PufferVault` in the form of stETH or WETH or it could also correspond to stETH locked in EigenLayer's stETH strategy contract 24 | * `uint256 lockedETH`: The amount of ETH that is locked in the beacon chain, corresponding to a running validator 25 | * `uint256 pufETHTotalSupply`: The total outstanding amount of pufETH 26 | * `uint256 lastUpdate`: The last block for which Proof of Reserves was posted 27 | 28 | --- 29 | 30 | ### Proof of Reserves 31 | 32 | #### `proofOfReserve` 33 | 34 | ```solidity 35 | function proofOfReserve( 36 | uint256 newEthAmountValue, 37 | uint256 newLockedEthValue, 38 | uint256 pufETHTotalSupplyValue, // @todo what to do with this? 39 | uint256 blockNumber, 40 | uint256 numberOfActiveValidators, 41 | bytes[] calldata guardianSignatures 42 | ) external 43 | ``` 44 | 45 | This is the function that Guardians will call to update the total amount of ETH backing pufETH. This may directly impact the exchange rate of pufETH to ETH, setting the fair market value of pufETH. 46 | 47 | *Effects* 48 | * Changes the following state variables to the supplied values: 49 | * `ethAmount` 50 | * `lockedETH` 51 | * `pufETHTotalSupply` 52 | * `lastUpdate` 53 | 54 | *Requirements* 55 | * Must be called by Guardians 56 | * Guardian signatures must be valid 57 | * Must have enough Guardian signatures to meet threshold -------------------------------------------------------------------------------- /src/interface/IPufferVaultV2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import { IPufferVault } from "./IPufferVault.sol"; 5 | 6 | /** 7 | * @title IPufferVaultV2 8 | * @author Puffer Finance 9 | * @custom:security-contact security@puffer.fi 10 | */ 11 | interface IPufferVaultV2 is IPufferVault { 12 | /** 13 | * @dev Thrown if the Vault doesn't have ETH liquidity to transfer to PufferModule 14 | */ 15 | error ETHTransferFailed(); 16 | 17 | /** 18 | * @dev Thrown if there is a deposit and a withdrawal in the same transaction 19 | */ 20 | error DepositAndWithdrawalForbidden(); 21 | 22 | /** 23 | * @dev Thrown if the new exit fee basis points is invalid 24 | */ 25 | error InvalidExitFeeBasisPoints(); 26 | 27 | /** 28 | * Emitted when assets (WETH) are withdrawn 29 | * @dev Signature: 0x139f9ee0762f3b0c92a4b8c7b8fe8be6b12aaece4b9b22de6bf1ba1094dcd998 30 | */ 31 | event AssetsWithdrawnToday(uint256 withdrawalAmount); 32 | 33 | /** 34 | * Emitted daily withdrawal limit is reset 35 | * @dev Signature: 0x190567136e3dd93d29bef98a7c7c87cff34ee88e71d634b52f5fb3b531085f40 36 | */ 37 | event DailyWithdrawalLimitReset(); 38 | 39 | /** 40 | * Emitted when the daily withdrawal limit is set 41 | * @dev Signature: 0x8d5f7487ce1fd25059bd15204a55ea2c293160362b849a6f9244aec7d5a3700b 42 | */ 43 | event DailyWithdrawalLimitSet(uint96 oldLimit, uint96 newLimit); 44 | 45 | /** 46 | * Emitted when the Vault transfers ETH to a specified address 47 | * @dev Signature: 0xba7bb5aa419c34d8776b86cc0e9d41e72d74a893a511f361a11af6c05e920c3d 48 | */ 49 | event TransferredETH(address indexed to, uint256 amount); 50 | 51 | /** 52 | * Emitted when the Vault transfers ETH to a specified address 53 | * @dev Signature: 0xb10a745484e9798f0014ea028d76169706f92e7eea5d5bb66001c1400769785d 54 | */ 55 | event ExitFeeBasisPointsSet(uint256 previousFee, uint256 newFee); 56 | 57 | /** 58 | * Emitted when the Vault gets ETH from Lido 59 | * @dev Signature: 0xb5cd6ba4df0e50a9991fc91db91ea56e2f134e498a70fc7224ad61d123e5bbb0 60 | */ 61 | event LidoWithdrawal(uint256 expectedWithdrawal, uint256 actualWithdrawal); 62 | 63 | /** 64 | * @notice Returns the current exit fee basis points 65 | */ 66 | function getExitFeeBasisPoints() external view returns (uint256); 67 | 68 | /** 69 | * @notice Returns the remaining assets that can be withdrawn today 70 | * @return The remaining assets that can be withdrawn today 71 | */ 72 | function getRemainingAssetsDailyWithdrawalLimit() external view returns (uint256); 73 | 74 | /** 75 | * @notice Deposits native ETH into the Puffer Vault 76 | * @param receiver The recipient of pufETH tokens 77 | * @return shares The amount of pufETH received from the deposit 78 | */ 79 | function depositETH(address receiver) external payable returns (uint256); 80 | 81 | /** 82 | * @notice Deposits stETH into the Puffer Vault 83 | * @param stETHSharesAmount The shares amount of stETH to deposit 84 | * @param receiver The recipient of pufETH tokens 85 | * @return shares The amount of pufETH received from the deposit 86 | */ 87 | function depositStETH(uint256 stETHSharesAmount, address receiver) external returns (uint256); 88 | } 89 | -------------------------------------------------------------------------------- /src/XERC20Lockbox.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.4 <0.9.0; 3 | 4 | import { IXERC20 } from "./interface/IXERC20.sol"; 5 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 7 | import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; 8 | import { IXERC20Lockbox } from "./interface/IXERC20Lockbox.sol"; 9 | 10 | contract XERC20Lockbox is IXERC20Lockbox { 11 | using SafeERC20 for IERC20; 12 | using SafeCast for uint256; 13 | 14 | /** 15 | * @notice The XERC20 token of this contract 16 | */ 17 | IXERC20 public immutable XERC20; 18 | 19 | /** 20 | * @notice The ERC20 token of this contract 21 | */ 22 | IERC20 public immutable ERC20; 23 | 24 | /** 25 | * @notice Constructor 26 | * 27 | * @param xerc20 The address of the XERC20 contract 28 | * @param erc20 The address of the ERC20 contract 29 | */ 30 | constructor(address xerc20, address erc20) { 31 | XERC20 = IXERC20(xerc20); 32 | ERC20 = IERC20(erc20); 33 | } 34 | 35 | /** 36 | * @notice Deposit ERC20 tokens into the lockbox 37 | * 38 | * @param amount The amount of tokens to deposit 39 | */ 40 | function deposit(uint256 amount) external { 41 | _deposit(msg.sender, amount); 42 | } 43 | 44 | /** 45 | * @notice Deposit ERC20 tokens into the lockbox, and send the XERC20 to a user 46 | * 47 | * @param to The user to send the XERC20 to 48 | * @param amount The amount of tokens to deposit 49 | */ 50 | function depositTo(address to, uint256 amount) external { 51 | _deposit(to, amount); 52 | } 53 | 54 | /** 55 | * @notice Not a native asset 56 | */ 57 | function depositNativeTo(address) public payable { 58 | revert IXERC20Lockbox_NotNative(); 59 | } 60 | 61 | /** 62 | * @notice Deposit native tokens into the lockbox 63 | */ 64 | function depositNative() public payable { 65 | revert IXERC20Lockbox_NotNative(); 66 | } 67 | 68 | /** 69 | * @notice Withdraw ERC20 tokens from the lockbox 70 | * 71 | * @param amount The amount of tokens to withdraw 72 | */ 73 | function withdraw(uint256 amount) external { 74 | _withdraw(msg.sender, amount); 75 | } 76 | 77 | /** 78 | * @notice Withdraw tokens from the lockbox 79 | * 80 | * @param to The user to withdraw to 81 | * @param amount The amount of tokens to withdraw 82 | */ 83 | function withdrawTo(address to, uint256 amount) external { 84 | _withdraw(to, amount); 85 | } 86 | 87 | /** 88 | * @notice Withdraw tokens from the lockbox 89 | * 90 | * @param to The user to withdraw to 91 | * @param amount The amount of tokens to withdraw 92 | */ 93 | function _withdraw(address to, uint256 amount) internal { 94 | emit Withdraw(to, amount); 95 | 96 | XERC20.burn(msg.sender, amount); 97 | ERC20.safeTransfer(to, amount); 98 | } 99 | 100 | /** 101 | * @notice Deposit tokens into the lockbox 102 | * 103 | * @param to The address to send the XERC20 to 104 | * @param amount The amount of tokens to deposit 105 | */ 106 | function _deposit(address to, uint256 amount) internal { 107 | ERC20.safeTransferFrom(msg.sender, address(this), amount); 108 | XERC20.mint(to, amount); 109 | 110 | emit Deposit(to, amount); 111 | } 112 | 113 | /** 114 | * @notice Fallback function to deposit native tokens 115 | */ 116 | receive() external payable { 117 | depositNative(); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /test/mocks/EigenLayerDelegationManagerMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import { IEigenLayer } from "../../src/interface/EigenLayer/IEigenLayer.sol"; 5 | import { IERC20 } from "openzeppelin/token/ERC20/IERC20.sol"; 6 | import { IDelegationManager } from "../../src/interface/EigenLayer/IDelegationManager.sol"; 7 | 8 | contract EigenLayerDelegationManagerMock is IDelegationManager { 9 | /** 10 | * Allows a staker to withdraw some shares. Withdrawn shares/strategies are immediately removed 11 | * from the staker. If the staker is delegated, withdrawn shares/strategies are also removed from 12 | * their operator. 13 | * 14 | * All withdrawn shares/strategies are placed in a queue and can be fully withdrawn after a delay. 15 | */ 16 | function queueWithdrawals(QueuedWithdrawalParams[] calldata queuedWithdrawalParams) 17 | external 18 | returns (bytes32[] memory) 19 | { } 20 | 21 | /** 22 | * @notice Used to complete the specified `withdrawal`. The caller must match `withdrawal.withdrawer` 23 | * @param withdrawal The Withdrawal to complete. 24 | * @param tokens Array in which the i-th entry specifies the `token` input to the 'withdraw' function of the i-th Strategy in the `withdrawal.strategies` array. 25 | * This input can be provided with zero length if `receiveAsTokens` is set to 'false' (since in that case, this input will be unused) 26 | * @param middlewareTimesIndex is the index in the operator that the staker who triggered the withdrawal was delegated to's middleware times array 27 | * @param receiveAsTokens If true, the shares specified in the withdrawal will be withdrawn from the specified strategies themselves 28 | * and sent to the caller, through calls to `withdrawal.strategies[i].withdraw`. If false, then the shares in the specified strategies 29 | * will simply be transferred to the caller directly. 30 | * @dev middlewareTimesIndex should be calculated off chain before calling this function by finding the first index that satisfies `slasher.canWithdraw` 31 | * @dev beaconChainETHStrategy shares are non-transferrable, so if `receiveAsTokens = false` and `withdrawal.withdrawer != withdrawal.staker`, note that 32 | * any beaconChainETHStrategy shares in the `withdrawal` will be _returned to the staker_, rather than transferred to the withdrawer, unlike shares in 33 | * any other strategies, which will be transferred to the withdrawer. 34 | */ 35 | function completeQueuedWithdrawal( 36 | Withdrawal calldata withdrawal, 37 | IERC20[] calldata tokens, 38 | uint256 middlewareTimesIndex, 39 | bool receiveAsTokens 40 | ) external { } 41 | 42 | /** 43 | * @notice Array-ified version of `completeQueuedWithdrawal`. 44 | * Used to complete the specified `withdrawals`. The function caller must match `withdrawals[...].withdrawer` 45 | * @param withdrawals The Withdrawals to complete. 46 | * @param tokens Array of tokens for each Withdrawal. See `completeQueuedWithdrawal` for the usage of a single array. 47 | * @param middlewareTimesIndexes One index to reference per Withdrawal. See `completeQueuedWithdrawal` for the usage of a single index. 48 | * @param receiveAsTokens Whether or not to complete each withdrawal as tokens. See `completeQueuedWithdrawal` for the usage of a single boolean. 49 | * @dev See `completeQueuedWithdrawal` for relevant dev tags 50 | */ 51 | function completeQueuedWithdrawals( 52 | Withdrawal[] calldata withdrawals, 53 | IERC20[][] calldata tokens, 54 | uint256[] calldata middlewareTimesIndexes, 55 | bool[] calldata receiveAsTokens 56 | ) external { } 57 | 58 | /// @notice Returns the keccak256 hash of `withdrawal`. 59 | function calculateWithdrawalRoot(Withdrawal memory withdrawal) external pure returns (bytes32) { } 60 | } 61 | -------------------------------------------------------------------------------- /script/UpgradePufETH.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import { stdJson } from "forge-std/StdJson.sol"; 5 | import { BaseScript } from ".//BaseScript.s.sol"; 6 | import { PufferVault } from "../src/PufferVault.sol"; 7 | import { PufferVaultV2 } from "../src/PufferVaultV2.sol"; 8 | import { PufferVaultV2Tests } from "../src/PufferVaultV2Tests.sol"; 9 | import { IEigenLayer } from "../src/interface/EigenLayer/IEigenLayer.sol"; 10 | import { IStrategy } from "../src/interface/EigenLayer/IStrategy.sol"; 11 | import { IDelegationManager } from "../src/interface/EigenLayer/IDelegationManager.sol"; 12 | import { IStETH } from "../src/interface/Lido/IStETH.sol"; 13 | import { ILidoWithdrawalQueue } from "../src/interface/Lido/ILidoWithdrawalQueue.sol"; 14 | import { LidoWithdrawalQueueMock } from "../test/mocks/LidoWithdrawalQueueMock.sol"; 15 | import { stETHStrategyMock } from "../test/mocks/stETHStrategyMock.sol"; 16 | import { UUPSUpgradeable } from "@openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; 17 | import { IWETH } from "../src/interface/Other/IWETH.sol"; 18 | import { IPufferOracle } from "../src/interface/IPufferOracle.sol"; 19 | import { Initializable } from "openzeppelin/proxy/utils/Initializable.sol"; 20 | import { AccessManager } from "openzeppelin/access/manager/AccessManager.sol"; 21 | import { PufferDeployment } from "../src/structs/PufferDeployment.sol"; 22 | 23 | /** 24 | * @title UpgradePufETH 25 | * @author Puffer Finance 26 | * @notice Upgrades PufETH 27 | * @dev 28 | * 29 | * 30 | * NOTE: 31 | * 32 | * If you ran the deployment script, but did not `--broadcast` the transaction, it will still update your local chainId-deployment.json file. 33 | * Other scripts will fail because addresses will be updated in deployments file, but the deployment never happened. 34 | * 35 | * BaseScript.sol holds the private key logic, if you don't have `PK` ENV variable, it will use the default one PK from `makeAddr("pufferDeployer")` 36 | * 37 | * PK=${deployer_pk} forge script script/UpgradePufETH.s.sol:UpgradePufETH --sig 'run(address)' "VAULTADDRESS" -vvvv --rpc-url=... --broadcast 38 | */ 39 | contract UpgradePufETH is BaseScript { 40 | /** 41 | * @dev Ethereum Mainnet addresses 42 | */ 43 | IStrategy internal constant _EIGEN_STETH_STRATEGY = IStrategy(0x93c4b944D05dfe6df7645A86cd2206016c51564D); 44 | IEigenLayer internal constant _EIGEN_STRATEGY_MANAGER = IEigenLayer(0x858646372CC42E1A627fcE94aa7A7033e7CF075A); 45 | IDelegationManager internal constant _DELEGATION_MANAGER = 46 | IDelegationManager(0x39053D51B77DC0d36036Fc1fCc8Cb819df8Ef37A); 47 | IStETH internal constant _ST_ETH = IStETH(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84); 48 | IWETH internal constant _WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); 49 | ILidoWithdrawalQueue internal constant _LIDO_WITHDRAWAL_QUEUE = 50 | ILidoWithdrawalQueue(0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1); 51 | 52 | function run(PufferDeployment memory deployment, address pufferOracle) public broadcast { 53 | //@todo this is for tests only 54 | AccessManager(deployment.accessManager).grantRole(1, _broadcaster, 0); 55 | 56 | PufferVaultV2 newImplementation = new PufferVaultV2Tests( 57 | IStETH(deployment.stETH), 58 | IWETH(deployment.weth), 59 | ILidoWithdrawalQueue(deployment.lidoWithdrawalQueueMock), 60 | IStrategy(deployment.stETHStrategyMock), 61 | IEigenLayer(deployment.eigenStrategyManagerMock), 62 | IPufferOracle(pufferOracle), 63 | _DELEGATION_MANAGER 64 | ); 65 | 66 | vm.expectEmit(true, true, true, true); 67 | emit Initializable.Initialized(2); 68 | UUPSUpgradeable(deployment.pufferVault).upgradeToAndCall( 69 | address(newImplementation), abi.encodeCall(PufferVaultV2.initialize, ()) 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /test/unit/PufferVaultV2Property.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import "erc4626-tests/ERC4626.test.sol"; 5 | import { IStETH } from "../../src/interface/Lido/IStETH.sol"; 6 | import { PufferDepositor } from "../../src/PufferDepositor.sol"; 7 | import { PufferVault } from "../../src/PufferVault.sol"; 8 | import { PufferVaultV2 } from "../../src/PufferVaultV2.sol"; 9 | import { AccessManager } from "openzeppelin/access/manager/AccessManager.sol"; 10 | import { PufferDeployment } from "../../src/structs/PufferDeployment.sol"; 11 | import { DeployPufETH } from "script/DeployPufETH.s.sol"; 12 | import { UpgradePufETH } from "script/UpgradePufETH.s.sol"; 13 | import { MockPufferOracle } from "../mocks/MockPufferOracle.sol"; 14 | import { WETH9 } from "../mocks/WETH9.sol"; 15 | import { ROLE_ID_DAO } from "../../script/Roles.sol"; 16 | import { GenerateAccessManagerCallData } from "script/GenerateAccessManagerCallData.sol"; 17 | 18 | contract PufferVaultV2Property is ERC4626Test { 19 | PufferDepositor public pufferDepositor; 20 | PufferVaultV2 public pufferVault; 21 | AccessManager public accessManager; 22 | IStETH public stETH; 23 | WETH9 public weth; 24 | 25 | // Override the minting of shares and assets (limit to uin64.max) 26 | function setUpVault(Init memory init) public virtual override { 27 | // setup initial shares and assets for individual users 28 | for (uint256 i = 0; i < N; i++) { 29 | address user = init.user[i]; 30 | vm.assume(_isEOA(user)); 31 | // shares 32 | uint256 shares = init.share[i]; 33 | vm.assume(shares < type(uint64).max); 34 | try IMockERC20(_underlying_).mint(user, shares) { } 35 | catch { 36 | vm.assume(false); 37 | } 38 | _approve(_underlying_, user, _vault_, shares); 39 | vm.prank(user); 40 | try IERC4626(_vault_).deposit(shares, user) { } 41 | catch { 42 | vm.assume(false); 43 | } 44 | // assets 45 | uint256 assets = init.asset[i]; 46 | vm.assume(assets < type(uint64).max); 47 | try IMockERC20(_underlying_).mint(user, assets) { } 48 | catch { 49 | vm.assume(false); 50 | } 51 | } 52 | 53 | // setup initial yield for vault 54 | setUpYield(init); 55 | } 56 | 57 | function setUp() public override { 58 | PufferDeployment memory deployment = new DeployPufETH().run(); 59 | 60 | MockPufferOracle mockOracle = new MockPufferOracle(); 61 | 62 | new UpgradePufETH().run(deployment, address(mockOracle)); 63 | 64 | pufferDepositor = PufferDepositor(payable(deployment.pufferDepositor)); 65 | pufferVault = PufferVaultV2(payable(deployment.pufferVault)); 66 | accessManager = AccessManager(payable(deployment.accessManager)); 67 | stETH = IStETH(payable(deployment.stETH)); 68 | 69 | // vm.prank(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266); 70 | // accessManager.grantRole(ROLE_ID_DAO, 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266, 0); 71 | // vm.prank(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266); 72 | // pufferVault.setDailyWithdrawalLimit(type(uint96).max); 73 | 74 | // Setup access for public 75 | bytes memory encodedMulticall = 76 | new GenerateAccessManagerCallData().run(address(pufferVault), address(pufferDepositor)); 77 | 78 | vm.prank(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266); 79 | (bool s,) = address(accessManager).call(encodedMulticall); 80 | require(s, "success"); 81 | 82 | weth = WETH9(payable(deployment.weth)); 83 | 84 | _underlying_ = address(deployment.weth); 85 | _vault_ = address(pufferVault); 86 | _delta_ = 0; 87 | _vaultMayBeEmpty = false; 88 | _unlimitedAmount = false; 89 | } 90 | 91 | // In test/Integration/PufferVaultV2.fork.t.sol we test that ETH and WETH and STETH deposits should give you the same amount of shares 92 | // in `test_eth_weth_stETH_deposits` 93 | } 94 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # pufETH Docs 2 | 3 | ## Overview 4 | 5 | pufETH is a native liquid restaking token (nLRT) that is undergoing development. Before Puffer's mainnet launch, pufETH holders will earn LST yield, Puffer points, and may participate in DeFi all without lockups. 6 | 7 | Prior to Puffer's full mainnet launch, users have the opportunity to deposit stETH into the [PufferVault](./PufferVault.md) to participate in Puffer's early adopter program. Additionally, users without stETH can easily participate, as the contract supports swapping tokens to stETH before depositing. In exchange, depositors receive pufETH which appreciates in value as the underlying stETH in the contract accrues. 8 | 9 | The PufferVault's stETH will be deposited into EigenLayer's stETH strategy contract if it has not reached it's cap. 10 | 11 | ## Puffer Mainnet Launch 12 | 13 | Upon Puffer's full mainnet launch, stETH will be withdrawn from EigenLayer and then converted to ETH via Lido. This entire process is expected to span over a ~10 day period. During this period, depositors can continue to mint pufETH, but the contract will switch to accept ETH & WETH deposits. 14 | 15 | Following the withdrawal process, the ETH will be utilized to provision decentralized Ethereum validators within the Puffer protocol. This marks a transition from Lido LST rewards to Puffer Protocol rewards. Importantly, nothing needs to be done by pufETH holders! However, as the Puffer protocol operates, pufETH value is expected to increase faster as the token now captures both PoS and restaking rewards. 16 | 17 | 18 | ## Dependencies 19 | 20 | - [Openzeppelin smart contracts](https://github.com/OpenZeppelin/openzeppelin-contracts) 21 | - AccessManager 22 | - IERC20 23 | - ERC20Permit 24 | - SafeERC20 25 | - ERC1967Proxy 26 | - IERC721Receiver 27 | - [Openzeppelin upgradeable smart contracts](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable) 28 | - ERC4626Upgradeable 29 | - UUPSUpgradeable 30 | - AccessManagedUpgradeable 31 | - Initializable 32 | 33 | 34 | ## System components: 35 | 36 | ### [PufferVault](./PufferVault.md) 37 | 38 | | File | Type | Upgradeable | Inherited | Deployed | 39 | | -------- | -------- | -------- | -------- | -------- | 40 | | [`IPufferVault.sol`](../src/interface/IPufferVault.sol) | Singleton | / | YES | / | 41 | | [`PufferVault.sol`](../src/PufferVault.sol) | Singleton | UUPS Proxy | YES | / | 42 | | [`PufferVaultV2.sol`](../src/PufferVaultV2.sol) | Singleton | UUPS Proxy | NO | / | 43 | | [`PufferVaultStorage.sol`](../src/PufferVaultStorage.sol) | Singleton | UUPS Proxy | YES | / | 44 | 45 | The Puffer Vault is the contract in charge of holding funds for the Puffer Protocol. Initially, it will store stETH and deposit into EigenLayer. Then, once the Puffer mainnet launch happens, it will withdraw this stETH and hold ETH instead, which will be used to provision validators for the Puffer Protocol. 46 | 47 | See full documentation in [./PufferVault.md](./PufferVault.md) 48 | 49 | ### [PufferDepositor](./PufferDepositor.md) 50 | 51 | | File | Type | Upgradeable | Inherited | Deployed | 52 | | -------- | -------- | -------- | -------- | -------- | 53 | | [`IPufferDepositor.sol`](../src/interface/IPufferDepositor.sol) | Singleton | / | YES | / | 54 | | [`PufferDepositor.sol`](../src/PufferDepositor.sol) | Singleton | UUPS Proxy | NO | / | 55 | | [`PufferDepositorV2.sol`](../src/PufferDepositorV2.sol) | Singleton | UUPS Proxy | NO | / | 56 | | [`PufferDepositorStorage.sol`](../src/PufferDepositorStorage.sol) | Singleton | UUPS Proxy | YES | / | 57 | 58 | These contracts support depositing into our vault, and allow swapping other assets into depositable assets. 59 | 60 | See full documentation in [./PufferDepositor.md](./PufferDepositor.md) 61 | 62 | ### [PufferOracle](./PufferOracle.md) 63 | 64 | | File | Type | Upgradeable | Inherited | Deployed | 65 | | -------- | -------- | -------- | -------- | -------- | 66 | | [`PufferOracle.sol`](../src/PufferOracle.sol) | Singleton | / | NO | / | 67 | 68 | This contract allows Guardians to post Proof of Reserves, proving the amount of ETH backing pufETH, and therefore establishing a fair market value of pufETH. 69 | 70 | See full documentation in [./PufferOracle.md](./PufferOracle.md) 71 | -------------------------------------------------------------------------------- /docs/PufferDepositor.md: -------------------------------------------------------------------------------- 1 | # PufferDepositor 2 | 3 | ### [PufferDepositor](./PufferDepositor.md) 4 | 5 | | File | Type | Upgradeable | Inherited | Deployed | 6 | | -------- | -------- | -------- | -------- | -------- | 7 | | [`IPufferDepositor.sol`](../src/interface/IPufferDepositor.sol) | Singleton | / | YES | / | 8 | | [`PufferDepositor.sol`](../src/PufferDepositor.sol) | Singleton | UUPS Proxy | NO | / | 9 | | [`PufferDepositorStorage.sol`](../src/PufferDepositorStorage.sol) | Singleton | UUPS Proxy | YES | / | 10 | 11 | The PufferDepositor facilitates deposits into the [PufferVault](./PufferVault.md), as well as swapping other tokens for depositable assets 12 | 13 | #### High-level Concepts 14 | 15 | This document organizes methods according to the following themes (click each to be taken to the relevant section): 16 | * [Deposit Functions](#deposit-functions) 17 | 18 | #### Important state variables 19 | 20 | The only state information the PufferDepositor contract holds are the addresses of stETH and wrapped stETH, as well as the SushiSwap Router contract, which the PufferDepositor uses for swapping other tokens into depositable assets. 21 | 22 | * `ISushiRouter internal constant _SUSHI_ROUTER`: The address of the SushiSwap Router contract, used to facilitate swapping other assets into depositable assets (either stETH or WETH) 23 | 24 | --- 25 | 26 | ### Deposit Functions 27 | 28 | #### `swapAndDeposit` 29 | 30 | ```solidity 31 | function swapAndDeposit(address tokenIn, uint256 amountIn, uint256 amountOutMin, bytes calldata routeCode) 32 | public 33 | virtual 34 | returns (uint256 pufETHAmount) 35 | ``` 36 | 37 | This function allows for swapping a token to stETH, and depositing the received stETH into the `PufferVault` smart contract 38 | 39 | *Effects* 40 | * Takes the specified `amountIn` amount of `tokenIn` from the caller 41 | * Swaps the input token for stETH, receiving at least the specified `amountOutMin` of stETH, otherwise reverting 42 | * Deposits the newly received stETH resulting from the swap into the `PufferVault` contract 43 | * Mints a corresponding amount of pufETH token to the caller, based on the amount of assets deposited 44 | 45 | *Requirements* 46 | * The provided `routeCode` calldata must correspond to a proper sequence of assets to swap through, that can result in receiving the specified `amountOutMin` amount of stETH, otherwise the function call will revert 47 | * The caller must have previously approved the `amountIn` of `tokenIn` token to be taken by this `PufferDepositor` contract 48 | 49 | #### `swapAndDepositWithPermit` 50 | 51 | ```solidity 52 | function swapAndDepositWithPermit( 53 | address tokenIn, 54 | uint256 amountOutMin, 55 | IPufferDepositor.Permit calldata permitData, 56 | bytes calldata routeCode 57 | ) public virtual returns (uint256 pufETHAmount) 58 | ``` 59 | 60 | This function is the same as above, except it does not require a preceding transaction to approve the token to be swapped 61 | 62 | *Effects* 63 | * Takes the specified `amountIn` amount of `tokenIn` from the caller 64 | * Swaps the input token for stETH, receiving at least the specified `amountOutMin` of stETH, otherwise reverting 65 | * Deposits the newly received stETH resulting from the swap into the `PufferVault` contract 66 | * Mints a corresponding amount of pufETH token to the caller, based on the amount of assets deposited 67 | 68 | *Requirements* 69 | * The provided `routeCode` calldata must correspond to a proper sequence of assets to swap through, that can result in receiving the specified `amountOutMin` amount of stETH, otherwise the function call will revert 70 | 71 | #### `depositWstETH` 72 | 73 | ```solidity 74 | function depositWstETH(IPufferDepositor.Permit calldata permitData) external returns (uint256 pufETHAmount) 75 | ``` 76 | 77 | Allows the depositing of wrapped stETH into the `PufferVault` contract. It will unwrap the provided wstETH into stETH and then deposit into the `PufferVault` 78 | 79 | *Effects* 80 | * Takes the specified amount of wrapped stETH from the caller 81 | * Unwraps the provided wrapped stETH into stETH 82 | * Deposits the newly unwrapped stETH into the `PufferVault` contract 83 | * Mints pufETH to the caller, corresponding to the asset amount deposited 84 | 85 | *Requirements* 86 | * Called must have previously approved the amount of wrapped stETH to be taken by the `PufferDepositor` contract -------------------------------------------------------------------------------- /src/interface/IPufferDepositor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import { IERC20 } from "openzeppelin/token/ERC20/IERC20.sol"; 5 | import { Permit } from "../structs/Permit.sol"; 6 | 7 | /** 8 | * @title PufferDepositor 9 | * @author Puffer Finance 10 | * @custom:security-contact security@puffer.fi 11 | */ 12 | interface IPufferDepositor { 13 | /** 14 | * @dev Error indicating that the token is not allowed. 15 | */ 16 | error TokenNotAllowed(address token); 17 | /** 18 | * @dev Error indicating that the 1inch swap has failed. 19 | * @param token The address of the token being swapped. 20 | * @param amount The amount of the token being swapped. 21 | */ 22 | error SwapFailed(address token, uint256 amount); 23 | 24 | /** 25 | * @dev Event indicating that the token is allowed. 26 | */ 27 | event TokenAllowed(IERC20 token); 28 | /** 29 | * @dev Event indicating that the token is disallowed. 30 | */ 31 | event TokenDisallowed(IERC20 token); 32 | 33 | /** 34 | * @notice Swaps `amountIn` of `tokenIn` for stETH and deposits it into the Puffer Vault 35 | * @param tokenIn The address of the token being swapped 36 | * @param amountIn The amount of `tokenIn` to swap 37 | * @param callData The encoded calldata for the swap, it is fetched from the 1Inch API `https://api.1inch.dev/swap/v5.2/1/swap` 38 | * @return pufETHAmount The amount of pufETH received from the deposit 39 | */ 40 | function swapAndDeposit1Inch(address tokenIn, uint256 amountIn, bytes calldata callData) 41 | external 42 | payable 43 | returns (uint256 pufETHAmount); 44 | 45 | /** 46 | * @notice Swaps `permitData.amount` of `tokenIn` for stETH using a permit and deposits it into the Puffer Vault 47 | * @param tokenIn The address of the token being swapped 48 | * @param permitData The permit data containing the approval information 49 | * @param callData The encoded calldata for the swap, it is fetched from the 1Inch API `https://api.1inch.dev/swap/v5.2/1/swap` 50 | * @return pufETHAmount The amount of pufETH received from the deposit 51 | */ 52 | function swapAndDepositWithPermit1Inch(address tokenIn, Permit calldata permitData, bytes calldata callData) 53 | external 54 | payable 55 | returns (uint256 pufETHAmount); 56 | 57 | /** 58 | * @notice Swaps `amountIn` of `tokenIn` for stETH and deposits it into the Puffer Vault 59 | * @param tokenIn The address of the token being swapped 60 | * @param amountIn The amount of `tokenIn` to swap 61 | * @param amountOutMin The minimum amount of stETH to receive from the swap 62 | * @param routeCode The encoded route for the swap 63 | * @return pufETHAmount The amount of pufETH received from the deposit 64 | */ 65 | function swapAndDeposit(address tokenIn, uint256 amountIn, uint256 amountOutMin, bytes calldata routeCode) 66 | external 67 | payable 68 | returns (uint256 pufETHAmount); 69 | 70 | /** 71 | * @notice Swaps `permitData.amount` of `tokenIn` for stETH using a permit and deposits it into the Puffer Vault 72 | * @param tokenIn The address of the token being swapped 73 | * @param amountOutMin The minimum amount of stETH to receive from the swap 74 | * @param permitData The permit data containing the approval information 75 | * @param routeCode The encoded route for the swap 76 | * @return pufETHAmount The amount of pufETH received from the deposit 77 | */ 78 | function swapAndDepositWithPermit( 79 | address tokenIn, 80 | uint256 amountOutMin, 81 | Permit calldata permitData, 82 | bytes calldata routeCode 83 | ) external payable returns (uint256 pufETHAmount); 84 | 85 | /** 86 | * @notice Deposits wrapped stETH (wstETH) into the Puffer Vault 87 | * @param permitData The permit data containing the approval information 88 | * @return pufETHAmount The amount of pufETH received from the deposit 89 | */ 90 | function depositWstETH(Permit calldata permitData) external returns (uint256 pufETHAmount); 91 | 92 | /** 93 | * @notice Deposits stETH into the Puffer Vault using Permit 94 | * @param permitData The permit data containing the approval information 95 | * @return pufETHAmount The amount of pufETH received from the deposit 96 | */ 97 | function depositStETH(Permit calldata permitData) external returns (uint256 pufETHAmount); 98 | } 99 | -------------------------------------------------------------------------------- /script/DeployL2XPufETH.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import "forge-std/Script.sol"; 5 | import { stdJson } from "forge-std/StdJson.sol"; 6 | import { BaseScript } from ".//BaseScript.s.sol"; 7 | import { xPufETH } from "../src/l2/xPufETH.sol"; 8 | import { Initializable } from "openzeppelin/proxy/utils/Initializable.sol"; 9 | import { Timelock } from "../src/Timelock.sol"; 10 | import { ERC1967Proxy } from "openzeppelin/proxy/ERC1967/ERC1967Proxy.sol"; 11 | import { AccessManager } from "openzeppelin/access/manager/AccessManager.sol"; 12 | import { ROLE_ID_DAO, ROLE_ID_OPERATIONS_MULTISIG } from "./Roles.sol"; 13 | 14 | /** 15 | * @title DeployL2XPufETH 16 | * @author Puffer Finance 17 | * @notice Deploy XPufETH 18 | * @dev 19 | * 20 | * 21 | * NOTE: 22 | * 23 | * If you ran the deployment script, but did not `--broadcast` the transaction, it will still update your local chainId-deployment.json file. 24 | * Other scripts will fail because addresses will be updated in deployments file, but the deployment never happened. 25 | * 26 | * BaseScript.sol holds the private key logic, if you don't have `PK` ENV variable, it will use the default one PK from `makeAddr("pufferDeployer")` 27 | * 28 | * PK=a990c824d7f6928806d93674ef4acd4b240ad60c9ce575777c87b36f9a3c32a8 forge script script/DeployL2XPufETH.s.sol:DeployL2XPufETH -vvvv --rpc-url=https://holesky.gateway.tenderly.co/5ovlGAOeSvuI3UcQD2PoSD --broadcast 29 | */ 30 | contract DeployL2XPufETH is BaseScript { 31 | address operationsMultisig = vm.envOr("OPERATIONS_MULTISIG", makeAddr("operationsMultisig")); 32 | address pauserMultisig = vm.envOr("PAUSER_MULTISIG", makeAddr("pauserMultisig")); 33 | address communityMultisig = vm.envOr("COMMUNITY_MULTISIG", makeAddr("communityMultisig")); 34 | 35 | address _CONNEXT = 0x8247ed6d0a344eeae4edBC7e44572F1B70ECA82A; //@todo change for mainnet 36 | uint256 _MINTING_LIMIT = 1000 * 1e18; //@todo 37 | uint256 _BURNING_LIMIT = 1000 * 1e18; //@todo 38 | 39 | function run() public broadcast { 40 | AccessManager accessManager = new AccessManager(_broadcaster); 41 | 42 | console.log("AccessManager", address(accessManager)); 43 | 44 | operationsMultisig = _broadcaster; 45 | pauserMultisig = _broadcaster; 46 | communityMultisig = _broadcaster; 47 | 48 | Timelock timelock = new Timelock({ 49 | accessManager: address(accessManager), 50 | communityMultisig: communityMultisig, 51 | operationsMultisig: operationsMultisig, 52 | pauser: pauserMultisig, 53 | initialDelay: 7 days 54 | }); 55 | 56 | console.log("Timelock", address(timelock)); 57 | 58 | xPufETH newImplementation = new xPufETH(); 59 | console.log("XERC20PufferVaultImplementation", address(newImplementation)); 60 | 61 | bytes32 xPufETHSalt = bytes32("xPufETH"); 62 | 63 | vm.expectEmit(true, true, true, true); 64 | emit Initializable.Initialized(1); 65 | ERC1967Proxy xPufETHProxy = new ERC1967Proxy{ salt: xPufETHSalt }( 66 | address(newImplementation), abi.encodeCall(xPufETH.initialize, (address(accessManager))) 67 | ); 68 | console.log("xPufETHProxy", address(xPufETHProxy)); 69 | 70 | bytes memory data = abi.encodeWithSelector(xPufETH.setLimits.selector, _CONNEXT, _MINTING_LIMIT, _BURNING_LIMIT); 71 | 72 | accessManager.execute(address(xPufETHProxy), data); 73 | 74 | bytes4[] memory daoSelectors = new bytes4[](2); 75 | daoSelectors[0] = xPufETH.setLockbox.selector; 76 | daoSelectors[1] = xPufETH.setLimits.selector; 77 | 78 | bytes4[] memory publicSelectors = new bytes4[](2); 79 | publicSelectors[0] = xPufETH.mint.selector; 80 | publicSelectors[1] = xPufETH.burn.selector; 81 | 82 | // Setup Access 83 | // Public selectors 84 | accessManager.setTargetFunctionRole(address(xPufETHProxy), publicSelectors, accessManager.PUBLIC_ROLE()); 85 | // Dao selectors 86 | accessManager.setTargetFunctionRole(address(xPufETHProxy), daoSelectors, ROLE_ID_DAO); 87 | 88 | accessManager.grantRole(accessManager.ADMIN_ROLE(), address(timelock), 0); 89 | 90 | //@todo replace with dao and ops multisigs for mainnet 91 | accessManager.grantRole(ROLE_ID_DAO, _broadcaster, 0); 92 | accessManager.grantRole(ROLE_ID_OPERATIONS_MULTISIG, _broadcaster, 0); 93 | 94 | //@todo revoke on mainnet 95 | // accessManager.revokeRole(accessManager.ADMIN_ROLE(), _broadcaster); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/interface/IXERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.4 <0.9.0; 3 | 4 | interface IXERC20 { 5 | /** 6 | * @notice Emits when a lockbox is set 7 | * 8 | * @param lockbox The address of the lockbox 9 | */ 10 | event LockboxSet(address lockbox); 11 | 12 | /** 13 | * @notice Emits when a limit is set 14 | * 15 | * @param mintingLimit The updated minting limit we are setting to the bridge 16 | * @param burningLimit The updated burning limit we are setting to the bridge 17 | * @param bridge The address of the bridge we are setting the limit too 18 | */ 19 | event BridgeLimitsSet(uint256 mintingLimit, uint256 burningLimit, address indexed bridge); 20 | 21 | /** 22 | * @notice Reverts when a user with too low of a limit tries to call mint/burn 23 | */ 24 | error IXERC20_NotHighEnoughLimits(); 25 | 26 | /** 27 | * @notice Reverts when caller is not the factory 28 | */ 29 | error IXERC20_NotFactory(); 30 | 31 | /** 32 | * @notice Reverts when limits are too high 33 | */ 34 | error IXERC20_LimitsTooHigh(); 35 | 36 | /** 37 | * @notice Contains the full minting and burning data for a particular bridge 38 | * 39 | * @param minterParams The minting parameters for the bridge 40 | * @param burnerParams The burning parameters for the bridge 41 | */ 42 | struct Bridge { 43 | BridgeParameters minterParams; 44 | BridgeParameters burnerParams; 45 | } 46 | 47 | /** 48 | * @notice Contains the mint or burn parameters for a bridge 49 | * 50 | * @param timestamp The timestamp of the last mint/burn 51 | * @param ratePerSecond The rate per second of the bridge 52 | * @param maxLimit The max limit of the bridge 53 | * @param currentLimit The current limit of the bridge 54 | */ 55 | struct BridgeParameters { 56 | uint256 timestamp; 57 | uint256 ratePerSecond; 58 | uint256 maxLimit; 59 | uint256 currentLimit; 60 | } 61 | 62 | /** 63 | * @notice Sets the lockbox address 64 | * 65 | * @param lockbox The address of the lockbox 66 | */ 67 | function setLockbox(address lockbox) external; 68 | 69 | /** 70 | * @notice Updates the limits of any bridge 71 | * @dev Can only be called by the owner 72 | * @param mintingLimit The updated minting limit we are setting to the bridge 73 | * @param burningLimit The updated burning limit we are setting to the bridge 74 | * @param bridge The address of the bridge we are setting the limits too 75 | */ 76 | function setLimits(address bridge, uint256 mintingLimit, uint256 burningLimit) external; 77 | 78 | /** 79 | * @notice Returns the max limit of a minter 80 | * 81 | * @param minter The minter we are viewing the limits of 82 | * @return limit The limit the minter has 83 | */ 84 | function mintingMaxLimitOf(address minter) external view returns (uint256 limit); 85 | 86 | /** 87 | * @notice Returns the max limit of a bridge 88 | * 89 | * @param bridge the bridge we are viewing the limits of 90 | * @return limit The limit the bridge has 91 | */ 92 | function burningMaxLimitOf(address bridge) external view returns (uint256 limit); 93 | 94 | /** 95 | * @notice Returns the current limit of a minter 96 | * 97 | * @param minter The minter we are viewing the limits of 98 | * @return limit The limit the minter has 99 | */ 100 | function mintingCurrentLimitOf(address minter) external view returns (uint256 limit); 101 | 102 | /** 103 | * @notice Returns the current limit of a bridge 104 | * 105 | * @param bridge the bridge we are viewing the limits of 106 | * @return limit The limit the bridge has 107 | */ 108 | function burningCurrentLimitOf(address bridge) external view returns (uint256 limit); 109 | 110 | /** 111 | * @notice Mints tokens for a user 112 | * @dev Can only be called by a minter 113 | * @param user The address of the user who needs tokens minted 114 | * @param amount The amount of tokens being minted 115 | */ 116 | function mint(address user, uint256 amount) external; 117 | 118 | /** 119 | * @notice Burns tokens for a user 120 | * @dev Can only be called by a minter 121 | * @param user The address of the user who needs tokens burned 122 | * @param amount The amount of tokens being burned 123 | */ 124 | function burn(address user, uint256 amount) external; 125 | } 126 | -------------------------------------------------------------------------------- /src/PufferDepositorV2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import { ERC20Permit } from "openzeppelin/token/ERC20/extensions/ERC20Permit.sol"; 5 | import { IERC20 } from "openzeppelin/token/ERC20/IERC20.sol"; 6 | import { AccessManagedUpgradeable } from 7 | "@openzeppelin-contracts-upgradeable/access/manager/AccessManagedUpgradeable.sol"; 8 | import { UUPSUpgradeable } from "@openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; 9 | import { IStETH } from "./interface/Lido/IStETH.sol"; 10 | import { IWstETH } from "./interface/Lido/IWstETH.sol"; 11 | import { PufferVaultV2 } from "./PufferVaultV2.sol"; 12 | import { PufferDepositorStorage } from "./PufferDepositorStorage.sol"; 13 | import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 14 | import { IPufferDepositorV2 } from "./interface/IPufferDepositorV2.sol"; 15 | import { Permit } from "./structs/Permit.sol"; 16 | 17 | /** 18 | * @title PufferDepositorV2 19 | * @author Puffer Finance 20 | * @custom:security-contact security@puffer.fi 21 | */ 22 | contract PufferDepositorV2 is IPufferDepositorV2, PufferDepositorStorage, AccessManagedUpgradeable, UUPSUpgradeable { 23 | using SafeERC20 for address; 24 | 25 | IStETH internal immutable _ST_ETH; 26 | IWstETH internal constant _WST_ETH = IWstETH(0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0); 27 | 28 | /** 29 | * @dev Wallet that transferred pufETH to the PufferDepositor by mistake. 30 | */ 31 | address private constant PUFFER = 0x8A0C1e5cEA8e0F6dF341C005335E7fe5ed18A0a0; 32 | 33 | /** 34 | * @dev The Puffer Vault contract address 35 | */ 36 | PufferVaultV2 public immutable PUFFER_VAULT; 37 | 38 | constructor(PufferVaultV2 pufferVault, IStETH stETH) payable { 39 | PUFFER_VAULT = pufferVault; 40 | _ST_ETH = stETH; 41 | } 42 | 43 | /** 44 | * @notice Returns the pufETH sent to this contract by mistake 45 | */ 46 | function initialize() public reinitializer(2) { 47 | // https://etherscan.io/token/0xd9a442856c234a39a81a089c06451ebaa4306a72?a=0x4aa799c5dfc01ee7d790e3bf1a7c2257ce1dceff 48 | // slither-disable-next-line unchecked-transfer 49 | PUFFER_VAULT.transfer(PUFFER, 0.201 ether); 50 | } 51 | 52 | /** 53 | * @inheritdoc IPufferDepositorV2 54 | */ 55 | function depositWstETH(Permit calldata permitData, address recipient) 56 | external 57 | restricted 58 | returns (uint256 pufETHAmount) 59 | { 60 | try ERC20Permit(address(_WST_ETH)).permit({ 61 | owner: msg.sender, 62 | spender: address(this), 63 | value: permitData.amount, 64 | deadline: permitData.deadline, 65 | v: permitData.v, 66 | s: permitData.s, 67 | r: permitData.r 68 | }) { } catch { } 69 | 70 | SafeERC20.safeTransferFrom(IERC20(address(_WST_ETH)), msg.sender, address(this), permitData.amount); 71 | 72 | _WST_ETH.unwrap(permitData.amount); 73 | 74 | // The PufferDepositor is not supposed to hold any stETH, so we sharesOf(PufferDepositor) to the PufferVault immediately 75 | return PUFFER_VAULT.depositStETH(_ST_ETH.sharesOf(address(this)), recipient); 76 | } 77 | 78 | /** 79 | * @inheritdoc IPufferDepositorV2 80 | */ 81 | function depositStETH(Permit calldata permitData, address recipient) 82 | external 83 | restricted 84 | returns (uint256 pufETHAmount) 85 | { 86 | try ERC20Permit(address(_ST_ETH)).permit({ 87 | owner: msg.sender, 88 | spender: address(this), 89 | value: permitData.amount, 90 | deadline: permitData.deadline, 91 | v: permitData.v, 92 | s: permitData.s, 93 | r: permitData.r 94 | }) { } catch { } 95 | 96 | // Transfer stETH from user to this contract. The amount received here can be 1-2 wei lower than the actual permitData.amount 97 | SafeERC20.safeTransferFrom(IERC20(address(_ST_ETH)), msg.sender, address(this), permitData.amount); 98 | 99 | // The PufferDepositor is not supposed to hold any stETH, so we sharesOf(PufferDepositor) to the PufferVault immediately 100 | return PUFFER_VAULT.depositStETH(_ST_ETH.sharesOf(address(this)), recipient); 101 | } 102 | 103 | /** 104 | * @dev Authorizes an upgrade to a new implementation 105 | * Restricted access 106 | * @param newImplementation The address of the new implementation 107 | */ 108 | function _authorizeUpgrade(address newImplementation) internal virtual override restricted { } 109 | } 110 | -------------------------------------------------------------------------------- /script/GenerateAccessManagerCallData.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import { Script } from "forge-std/Script.sol"; 5 | import { AccessManager } from "openzeppelin/access/manager/AccessManager.sol"; 6 | import { Multicall } from "openzeppelin/utils/Multicall.sol"; 7 | import { console } from "forge-std/console.sol"; 8 | import { PufferVaultV2 } from "../src/PufferVaultV2.sol"; 9 | import { PufferDepositorV2 } from "../src/PufferDepositorV2.sol"; 10 | import { PufferDepositor } from "../src/PufferDepositor.sol"; 11 | import { PUBLIC_ROLE, ROLE_ID_DAO, ROLE_ID_PUFFER_PROTOCOL, ROLE_ID_OPERATIONS_MULTISIG } from "./Roles.sol"; 12 | 13 | /** 14 | * @title GenerateAccessManagerCallData 15 | * @author Puffer Finance 16 | * @notice Generates the AccessManager call data to setup the public access 17 | * The returned calldata is queued and executed by the Operations Multisig 18 | * 1. timelock.queueTransaction(address(accessManager), encodedMulticall, 1) 19 | * 2. ... 7 days later ... 20 | * 3. timelock.executeTransaction(address(accessManager), encodedMulticall, 1) 21 | */ 22 | contract GenerateAccessManagerCallData is Script { 23 | function run(address pufferVaultProxy, address pufferDepositorProxy) public pure returns (bytes memory) { 24 | bytes[] memory calldatas = new bytes[](5); 25 | 26 | // Combine the two calldatas 27 | calldatas[0] = _getPublicSelectorsCalldata({ pufferVaultProxy: pufferVaultProxy }); 28 | calldatas[1] = _getDaoSelectorsCalldataCalldata({ pufferVaultProxy: pufferVaultProxy }); 29 | calldatas[2] = _getProtocolSelectorsCalldata({ pufferVaultProxy: pufferVaultProxy }); 30 | calldatas[3] = _getOperationsSelectorsCalldata({ pufferVaultProxy: pufferVaultProxy }); 31 | calldatas[4] = _getPublicSelectorsForDepositor({ pufferDepositorProxy: pufferDepositorProxy }); 32 | 33 | bytes memory encodedMulticall = abi.encodeCall(Multicall.multicall, (calldatas)); 34 | 35 | // console.log("GenerateAccessManagerCallData:"); 36 | // console.logBytes(encodedMulticall); 37 | 38 | return encodedMulticall; 39 | } 40 | 41 | function _getPublicSelectorsCalldata(address pufferVaultProxy) internal pure returns (bytes memory) { 42 | // Public selectors for PufferVault 43 | bytes4[] memory publicSelectors = new bytes4[](4); 44 | publicSelectors[0] = PufferVaultV2.withdraw.selector; 45 | publicSelectors[1] = PufferVaultV2.redeem.selector; 46 | publicSelectors[2] = PufferVaultV2.depositETH.selector; 47 | publicSelectors[3] = PufferVaultV2.depositStETH.selector; 48 | // `deposit` and `mint` are already `restricted` and allowed for PUBLIC_ROLE (PufferVault deployment) 49 | 50 | return abi.encodeWithSelector( 51 | AccessManager.setTargetFunctionRole.selector, pufferVaultProxy, publicSelectors, PUBLIC_ROLE 52 | ); 53 | } 54 | 55 | function _getDaoSelectorsCalldataCalldata(address pufferVaultProxy) internal pure returns (bytes memory) { 56 | // DAO selectors 57 | bytes4[] memory daoSelectors = new bytes4[](1); 58 | daoSelectors[0] = PufferVaultV2.setDailyWithdrawalLimit.selector; 59 | 60 | return abi.encodeWithSelector( 61 | AccessManager.setTargetFunctionRole.selector, pufferVaultProxy, daoSelectors, ROLE_ID_DAO 62 | ); 63 | } 64 | 65 | function _getProtocolSelectorsCalldata(address pufferVaultProxy) internal pure returns (bytes memory) { 66 | // Puffer Protocol only 67 | // PufferProtocol will get `ROLE_ID_PUFFER_PROTOCOL` when it's deployed 68 | bytes4[] memory protocolSelectors = new bytes4[](2); 69 | protocolSelectors[0] = PufferVaultV2.transferETH.selector; 70 | protocolSelectors[1] = PufferVaultV2.burn.selector; 71 | 72 | return abi.encodeWithSelector( 73 | AccessManager.setTargetFunctionRole.selector, pufferVaultProxy, protocolSelectors, ROLE_ID_PUFFER_PROTOCOL 74 | ); 75 | } 76 | 77 | function _getOperationsSelectorsCalldata(address pufferVaultProxy) internal pure returns (bytes memory) { 78 | // Operations multisig 79 | bytes4[] memory operationsSelectors = new bytes4[](3); 80 | operationsSelectors[0] = PufferVaultV2.initiateETHWithdrawalsFromLido.selector; 81 | operationsSelectors[1] = PufferVaultV2.claimWithdrawalsFromLido.selector; 82 | operationsSelectors[2] = PufferVaultV2.claimWithdrawalFromEigenLayerM2.selector; 83 | 84 | return abi.encodeWithSelector( 85 | AccessManager.setTargetFunctionRole.selector, 86 | pufferVaultProxy, 87 | operationsSelectors, 88 | ROLE_ID_OPERATIONS_MULTISIG 89 | ); 90 | } 91 | 92 | function _getPublicSelectorsForDepositor(address pufferDepositorProxy) internal pure returns (bytes memory) { 93 | // PufferDepositor public selectors 94 | bytes4[] memory publicSelectorsDepositor = new bytes4[](2); 95 | publicSelectorsDepositor[0] = PufferDepositorV2.depositStETH.selector; 96 | publicSelectorsDepositor[1] = PufferDepositorV2.depositWstETH.selector; 97 | 98 | return abi.encodeWithSelector( 99 | AccessManager.setTargetFunctionRole.selector, pufferDepositorProxy, publicSelectorsDepositor, PUBLIC_ROLE 100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /test/unit/PufETH.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import "erc4626-tests/ERC4626.test.sol"; 5 | import { IStETH } from "../../src/interface/Lido/IStETH.sol"; 6 | import { IPufferVault } from "../../src/interface/IPufferVault.sol"; 7 | import { IAccessManaged } from "openzeppelin/access/manager/IAccessManaged.sol"; 8 | import { PufferDepositor } from "../../src/PufferDepositor.sol"; 9 | import { PufferVault } from "../../src/PufferVault.sol"; 10 | import { AccessManager } from "openzeppelin/access/manager/AccessManager.sol"; 11 | import { stETHMock } from "../mocks/stETHMock.sol"; 12 | import { PufferDeployment } from "../../src/structs/PufferDeployment.sol"; 13 | import { DeployPufETH } from "script/DeployPufETH.s.sol"; 14 | 15 | contract PufETHTest is ERC4626Test { 16 | PufferDepositor public pufferDepositor; 17 | PufferVault public pufferVault; 18 | AccessManager public accessManager; 19 | IStETH public stETH; 20 | 21 | address operationsMultisig = makeAddr("operations"); 22 | address communityMultisig = makeAddr("community"); 23 | 24 | function setUp() public override { 25 | PufferDeployment memory deployment = new DeployPufETH().run(); 26 | 27 | pufferDepositor = PufferDepositor(payable(deployment.pufferDepositor)); 28 | pufferVault = PufferVault(payable(deployment.pufferVault)); 29 | accessManager = AccessManager(payable(deployment.accessManager)); 30 | stETH = IStETH(payable(deployment.stETH)); 31 | 32 | _underlying_ = address(stETH); 33 | _vault_ = address(pufferVault); 34 | _delta_ = 0; 35 | _vaultMayBeEmpty = false; 36 | _unlimitedAmount = false; 37 | } 38 | 39 | function test_erc4626_interface() public { 40 | stETHMock(address(stETH)).mint(address(this), 2000 ether); 41 | stETH.approve(address(pufferVault), type(uint256).max); 42 | 43 | // Deposit works 44 | assertEq(pufferVault.deposit(1000 ether, address(this)), 1000 ether, "deposit"); 45 | assertEq(pufferVault.mint(1000 ether, address(this)), 1000 ether, "mint"); 46 | 47 | // Getters work 48 | assertEq(pufferVault.asset(), address(stETH), "bad asset"); 49 | assertEq(pufferVault.totalAssets(), stETH.balanceOf(address(pufferVault)), "bad assets"); 50 | assertEq(pufferVault.convertToShares(1 ether), 1 ether, "bad conversion"); 51 | assertEq(pufferVault.convertToAssets(1 ether), 1 ether, "bad conversion shares"); 52 | assertEq(pufferVault.maxDeposit(address(5)), type(uint256).max, "bad max deposit"); 53 | assertEq(pufferVault.previewDeposit(1 ether), 1 ether, "preview shares"); 54 | assertEq(pufferVault.maxMint(address(5)), type(uint256).max, "max mint"); 55 | assertEq(pufferVault.previewMint(1 ether), 1 ether, "preview mint"); 56 | assertEq(pufferVault.previewWithdraw(1000 ether), 1000 ether, "preview withdraw"); 57 | assertEq(pufferVault.maxRedeem(address(this)), 2000 ether, "maxRedeem"); 58 | assertEq(pufferVault.previewRedeem(1000 ether), 1000 ether, "previewRedeem"); 59 | 60 | // Withdrawals are disabled 61 | vm.expectRevert(IPufferVault.WithdrawalsAreDisabled.selector); 62 | pufferVault.withdraw(1000 ether, address(this), address(this)); 63 | 64 | vm.expectRevert(IPufferVault.WithdrawalsAreDisabled.selector); 65 | pufferVault.redeem(1000 ether, address(this), address(this)); 66 | } 67 | 68 | function test_roles_setup() public { 69 | address msgSender = makeAddr("random"); 70 | vm.startPrank(msgSender); 71 | 72 | uint256[] memory amounts = new uint256[](1); 73 | vm.expectRevert(abi.encodeWithSelector(IAccessManaged.AccessManagedUnauthorized.selector, msgSender)); 74 | pufferVault.initiateETHWithdrawalsFromLido(amounts); 75 | 76 | vm.expectRevert(abi.encodeWithSelector(IAccessManaged.AccessManagedUnauthorized.selector, msgSender)); 77 | pufferVault.depositToEigenLayer(1); 78 | 79 | vm.expectRevert(abi.encodeWithSelector(IAccessManaged.AccessManagedUnauthorized.selector, msgSender)); 80 | pufferVault.initiateStETHWithdrawalFromEigenLayer(1); 81 | 82 | vm.expectRevert(abi.encodeWithSelector(IAccessManaged.AccessManagedUnauthorized.selector, msgSender)); 83 | pufferVault.upgradeToAndCall(address(pufferDepositor), ""); 84 | } 85 | 86 | // All withdrawals are disabled, we override these tests to not revert 87 | function test_RT_deposit_redeem(Init memory init, uint256 assets) public override { } 88 | function test_RT_deposit_withdraw(Init memory init, uint256 assets) public override { } 89 | function test_RT_mint_redeem(Init memory init, uint256 shares) public override { } 90 | function test_RT_mint_withdraw(Init memory init, uint256 shares) public override { } 91 | function test_RT_redeem_deposit(Init memory init, uint256 shares) public override { } 92 | function test_RT_redeem_mint(Init memory init, uint256 shares) public override { } 93 | function test_RT_withdraw_deposit(Init memory init, uint256 assets) public override { } 94 | function test_RT_withdraw_mint(Init memory init, uint256 assets) public override { } 95 | function test_previewRedeem(Init memory init, uint256 shares) public override { } 96 | function test_previewWithdraw(Init memory init, uint256 assets) public override { } 97 | function test_redeem(Init memory init, uint256 shares, uint256 allowance) public override { } 98 | function test_withdraw(Init memory init, uint256 assets, uint256 allowance) public override { } 99 | } 100 | -------------------------------------------------------------------------------- /docs/PufferVault.md: -------------------------------------------------------------------------------- 1 | # PufferVault 2 | 3 | | File | Type | Upgradeable | Inherited | Deployed | 4 | | -------- | -------- | -------- | -------- | -------- | 5 | | [`IPufferVault.sol`](../src/interface/IPufferVault.sol) | Singleton | / | YES | / | 6 | | [`PufferVault.sol`](../src/PufferVault.sol) | Singleton | UUPS Proxy | YES | / | 7 | | [`PufferVaultV2.sol`](../src/PufferVaultV2.sol) | Singleton | UUPS Proxy | NO | / | 8 | | [`PufferVaultStorage.sol`](../src/PufferVaultStorage.sol) | Singleton | UUPS Proxy | YES | / | 9 | 10 | The Puffer Vault is in charge of custodying funds for the protocol. Initially it will facilitate depositing stETH into EigenLayer to farm points. Then it will facilitate both withdrawing stETH from EigenLayer and subsequently withdrawing stETH from Lido to redeem ETH. 11 | 12 | #### High-level Concepts 13 | 14 | This document organizes methods according to the following themes (click each to be taken to the relevant section): 15 | * [Depositing](#depositing) 16 | * [Withdrawing](#withdrawing) 17 | * [Getter Methods](#getter-methods) 18 | 19 | #### Important state variables 20 | 21 | 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: 22 | 23 | #### PufferVault 24 | 25 | * `IStrategy internal immutable _EIGEN_STETH_STRATEGY`: The EigenLayer strategy for depositing stETH and farming points 26 | * `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 27 | * `ILidoWithdrawalQueue internal immutable _LIDO_WITHDRAWAL_QUEUE`: Lido's contract responsible for handling withdrawals of stETH into ETH 28 | 29 | #### PufferVaultStorage 30 | 31 | * `uint256 lidoLockedETH`: The amount of ETH the Puffer Protocol has locked inside of Lido 32 | * `uint256 eigenLayerPendingWithdrawalSharesAmount`: The amount of shares Puffer Protocol has pending for withdrawal from EigenLayer 33 | 34 | --- 35 | 36 | ### Depositing 37 | 38 | #### `depositToEigenLayer` 39 | 40 | ```solidity 41 | function depositToEigenLayer(uint256 amount) external virtual restricted 42 | ``` 43 | 44 | This function allows the vault to deposit stETH into EigenLayer's stETH strategy contract in order to farm points 45 | 46 | *Effects* 47 | * Moves stETH from the vault contract to EigenLayer's stETH strategy contract 48 | * Increases the amount of shares the vault contract has corresponding to EigenLayer's stETH strategy 49 | 50 | *Requirements* 51 | * Only callable by the operations or community multisigs 52 | 53 | --- 54 | 55 | ### Withdrawing 56 | 57 | #### `initiateStETHWithdrawalFromEigenLayer` 58 | 59 | ```solidity 60 | function initiateStETHWithdrawalFromEigenLayer(uint256 sharesToWithdraw) external virtual restricted 61 | ``` 62 | 63 | Initiates the withdrawal process of stETH from EigenLayer's stETH strategy contract 64 | 65 | *Effects* 66 | * Queues a withdrawal from EigenLayer's stETH strategy contract, which can later be redeemed by a separate function call 67 | 68 | *Requirements* 69 | * Only callable by the operations or community multisigs 70 | 71 | #### `claimWithdrawalFromEigenLayer` 72 | 73 | ```solidity 74 | function claimWithdrawalFromEigenLayer( 75 | IEigenLayer.QueuedWithdrawal calldata queuedWithdrawal, 76 | IERC20[] calldata tokens, 77 | uint256 middlewareTimesIndex 78 | ) external virtual 79 | ``` 80 | 81 | Completes the process of withdrawing stETH from EigenLayer's stETH strategy contract 82 | 83 | *Effects* 84 | * Claims the previously queued withdrawal from EigenLayer's stETH strategy contract 85 | * Transfers stETH from EigenLayer's stETH strategy contract to this vault contract 86 | 87 | *Requirements* 88 | * There must be a corresponding queued withdrawal created previously via function `initiateStETHWithdrawalFromEigenLayer` 89 | * Enough time must have elapsed since creation of the queued withdrawal such that it is claimable at the time of this function call 90 | 91 | #### `initiateETHWithdrawalsFromLido` 92 | 93 | ```solidity 94 | function initiateETHWithdrawalsFromLido(uint256[] calldata amounts) 95 | external 96 | virtual 97 | restricted 98 | returns (uint256[] memory requestIds) 99 | ``` 100 | 101 | Begins the process of redeeming stETH for ETH from the Lido protocol 102 | 103 | *Effects* 104 | * Queues a pending withdrawal of stETH for ETH on Lido 105 | 106 | *Requirements* 107 | * Only callable by the operations or community multisigs 108 | 109 | 110 | #### `claimWithdrawalsFromLido` 111 | 112 | ```solidity 113 | function claimWithdrawalsFromLido(uint256[] calldata requestIds) external virtual 114 | ``` 115 | 116 | This function claims withdrawals that were previously queued. This completes the two-step process of withdrawing stETH for ETH on Lido. 117 | 118 | *Effects*: 119 | * Sends the vault contract ETH from Lido 120 | * Marks the pending withdrawal claim as claimed 121 | 122 | *Requirements*: 123 | * There must be a corresponding pending claim that is ready to be claimed from Lido. In other words, the withdrawal must have been previously queued, and that withdrawal claim must be ready for redemption 124 | 125 | --- 126 | 127 | ### Getter Methods 128 | 129 | #### `totalAssets` 130 | 131 | ```solidity 132 | function totalAssets() public view virtual override returns (uint256) 133 | ``` 134 | 135 | This function returns the total amount of assets on the vault, denominated in ETH. This includes any ETH balances directly on the contract, stETH balances (since stETH is 1:1 with ETH), and locked ETH or stETH within EigenLayer or Lido -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #