├── 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 | #

Puffer Vault

2 | [![Website][Website-badge]][Website] [![Docs][docs-badge]][docs] 3 | [![Discord][discord-badge]][discord] [![X][X-badge]][X] [![Foundry][foundry-badge]][foundry] 4 | 5 | [Website-badge]: https://img.shields.io/badge/WEBSITE-8A2BE2 6 | [Website]: https://www.puffer.fi 7 | [X-badge]: https://img.shields.io/twitter/follow/puffer_finance 8 | [X]: https://twitter.com/puffer_finance 9 | [discord]: https://discord.gg/pufferfi 10 | [docs-badge]: https://img.shields.io/badge/DOCS-8A2BE2 11 | [docs]: https://docs.puffer.fi/ 12 | [discord-badge]: https://dcbadge.vercel.app/api/server/pufferfi?style=flat 13 | [gha]: https://github.com/PufferFinance/PufferPool/actions 14 | [gha-badge]: https://github.com/PufferFinance/PufferPool/actions/workflows/ci.yml/badge.svg 15 | [foundry]: https://getfoundry.sh 16 | [foundry-badge]: https://img.shields.io/badge/Built%20with-Foundry-FFDB1C.svg 17 | 18 | 19 | ## Overview 20 | Stakers can deposit ETH and mint the [pufETH nLRT](https://docs.puffer.fi/protocol/nlrt#pufeth) via the PufferVault contract, which serves as a redeemable receipt for their restaked ETH. If sufficient exit liquidity is available, stakers can reclaim their ETH from the PufferVault. Over time, the redeemable amount is expected to increase from [validator tickets](https://docs.puffer.fi/protocol/validator-tickets) and restaking rewards. 21 | 22 | In [contrast with conventional liquid staking tokens (LSTs)](https://docs.puffer.fi/protocol/nlrt#what-is-an-lst), pufETH can provide strictly more rewards for its holders. Not only does pufETH encompass PoS rewards and restaking rewards, but its value can accelerate quickly due to validator ticket sales. Furthermore, the PoS rewards for stakers are decoupled from the protocol validators' performance. 23 | 24 | ## pufETH 25 | 26 | pufETH is implemented as a reward-bearing ERC20 token, following [ERC4626](https://ethereum.org/en/developers/docs/standards/tokens/erc-4626/) standard and inspired by [Compound's cToken](https://docs.compound.finance/v2/ctokens/#ctokens) design for optimal DeFi compatibility. It represents a novel approach in the liquid staking domain, introducing several features that enhance stakers' rewards and interaction with DeFi protocols. 27 | 28 | Read more about pufETH and native Liquid Restaking Tokens (nLRTs) in the [Puffer Docs](https://docs.puffer.fi/protocol/nlrt#pufeth) website. 29 | 30 | 31 | ## How pufETH Works 32 | Stakers deposit ETH to the PufferVault contract to mint the pufETH nLRT. At the protocol's inception, pufETH's conversion rate is one-to-one, but is expected to increase over time. Assuming the protocol performs well, i.e., accrues more rewards than penalties, the amount of ETH reedamable for pufETH will increase. 33 | 34 | ### Calculating the Conversion Rate 35 | The conversion rate can be calculated simply as: 36 | 37 | ``` 38 | conversion rate = (deposits + rewards - penalties) / pufETH supply 39 | ``` 40 | 41 | Where: 42 | 43 | - deposits and pufETH supply increase proportionally as stakers deposit ETH to mint pufETH, leaving the conversion rate unaffected. 44 | 45 | - rewards increase as [restaking operators](https://docs.puffer.fi/protocol/puffer-modules#restaking-operators) run AVSs and whenever validator tickets are minted. 46 | 47 | - penalties accrue if validators are slashed on PoS for more than their 1 ETH collateral, which is [disincentivized behavior](https://docs.puffer.fi/protocol/validator-tickets#why--noop-incentives) and mitigated through [anti-slashing technology](https://docs.puffer.fi/technology/secure-signer). Penalties can also accrue if the restaking operator is slashed running AVSs, which is why Puffer is [restricting restaking operator participation](https://docs.puffer.fi/protocol/puffer-modules#restricting-reops) during its nascent stages. 48 | 49 | 50 | 51 | ## Contract addresses 52 | - PufferVault (pufETH token): `0xD9A442856C234a39a81a089C06451EBAa4306a72` 53 | - PufferDepositor: `0x4aA799C5dfc01ee7d790e3bf1a7C2257CE1DcefF` 54 | - AccessManager: `0x8c1686069474410E6243425f4a10177a94EBEE11` 55 | - Timelock: `0x3C28B7c7Ba1A1f55c9Ce66b263B33B204f2126eA` 56 | 57 | For more detailed information on the contract deployments (Mainnet, Holesky, etc) and the ABIs, please check the [Deployments and ACL](https://github.com/PufferFinance/Deployments-and-ACL/blob/main/docs/deployments/) repository. 58 | 59 | 60 | ## Audits 61 | - BlockSec: [v1](./audits/BlockSec-pufETH-v1.pdf), [v2](https://github.com/PufferFinance/PufferPool/blob/polish-docs/docs/audits/Blocksec_audit_April2024.pdf) 62 | - SlowMist: [v1](./audits/SlowMist-pufETH-v1.pdf), v2 63 | - Quantstamp: [v1](./audits/Quantstamp-pufETH-v1.pdf) 64 | - Immunefi [Boost](https://immunefi.com/boost/pufferfinance-boost/): [v1](./audits/Immunefi_Boost_pufETH_v1.pdf) 65 | - Trail of Bits: [v2](https://github.com/trailofbits/publications/blob/master/reviews/2024-03-pufferfinance-securityreview.pdf) 66 | - Nethermind: [v2](https://github.com/NethermindEth/PublicAuditReports/blob/main/NM0202-FINAL_PUFFER.pdf) 67 | - Creed: [v2](https://github.com/PufferFinance/PufferPool/blob/polish-docs/docs/audits/Creed_Puffer_Finance_Audit_April2024.pdf) 68 | 69 | 70 | # Tests 71 | 72 | Make sure you have access to a valid archive node RPC for ETH Mainnet (e.g. Infura) 73 | 74 | Installing dependencies and running tests can be executed running: 75 | ``` 76 | ETH_RPC_URL=https://mainnet.infura.io/v3/YOUR_KEY forge test -vvvv 77 | ``` 78 | 79 | # Echidna 80 | To install Echidna, see the instructions [here](https://github.com/crytic/echidna). To use Echidna, run the following command from the project's root: 81 | ```bash 82 | forge install crytic/properties --no-commit 83 | echidna . --contract EchidnaPufferVaultV2 --config src/echidna/config.yaml 84 | ``` 85 | For more information see the properties [README](https://github.com/crytic/properties/tree/main). 86 | 87 | -------------------------------------------------------------------------------- /test/Integration/PufferVaultV2Sandwich.fork.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import { TestHelper } from "../TestHelper.sol"; 5 | import { IPufferVaultV2 } from "../../src/interface/IPufferVaultV2.sol"; 6 | 7 | contract PufferVaultV2SandwichTest is TestHelper { 8 | address pufferWhale = 0xd164B614FdE7939078c7558F9680FA32f01aed77; 9 | 10 | function setUp() public virtual override { 11 | // Cancun upgrade 12 | vm.createSelectFork(vm.rpcUrl("mainnet"), 19504381); // ~ 2024-03-24 13:24:23) 13 | 14 | // Setup contracts that are deployed to mainnet 15 | _setupLiveContracts(); 16 | 17 | // Upgrade to latest version 18 | _upgradeToMainnetPuffer(); 19 | } 20 | 21 | // Rebase increases Vault's totalAssets by +30~ eth 22 | function test_rebase() public { 23 | uint256 assetsBefore = pufferVault.totalAssets(); 24 | 25 | // Rebase lido is +30.7 ETH for the Vault 26 | _rebaseLido(); 27 | 28 | uint256 assetsAfter = pufferVault.totalAssets(); 29 | 30 | assertGt(assetsAfter, assetsBefore, "assetsAfter > assetsBefore"); 31 | assertEq(assetsAfter - assetsBefore, 30.747233933014735819 ether, "30 eth rebase"); 32 | } 33 | 34 | // Attacker tries to use own capital to sandwich the Vault's withdrawal 35 | // Sandwich attack can'e be in one transaction, it must be a MEV block 36 | function test_sandwich_v2() public { 37 | // Give ETH to the depositor(this contract) 38 | vm.deal(address(this), 100 ether); 39 | 40 | // Give ETH to the PufferVault (withdrawal liqudiity) 41 | vm.deal(address(pufferVault), 130 ether); 42 | 43 | // deposit 100 ETH 44 | uint256 pufETHReceived = pufferVault.depositETH{ value: 100 ether }(address(this)); 45 | 46 | // Rebase lido is +30.7 ETH for the Vault 47 | _rebaseLido(); 48 | 49 | // Withdraw 50 | pufferVault.withdraw(pufferVault.maxWithdraw(address(this)), address(this), address(this)); 51 | 52 | // Attacker got less than 100 ETH 53 | assertEq(_WETH.balanceOf(address(this)), 99.018071600759029089 ether, "~ 99 ether received"); 54 | } 55 | 56 | // Even with fees 0, it is not really worth it to sandwich the Vault 57 | // An attack with 99 ETH would only get 0.008 ETH profit (excluding gas fees) 58 | // The attacked would need to have 100 ETH/WETH for this to be profitable and he would need to sandwich the Vault in a MEV block 59 | function test_sandwich_v2_zero_withdrawal_fee() public { 60 | // Set fees to 0 61 | // Timelock.sol is the admin of AccessManager 62 | vm.startPrank(address(timelock)); 63 | vm.expectEmit(true, true, true, true); 64 | emit IPufferVaultV2.ExitFeeBasisPointsSet(100, 0); 65 | pufferVault.setExitFeeBasisPoints(0); 66 | vm.stopPrank(); 67 | 68 | // Give ETH to the depositor(this contract) 69 | vm.deal(address(this), 99 ether); 70 | 71 | // Give ETH to the PufferVault (withdrawal liqudiity) 72 | vm.deal(address(pufferVault), 130 ether); 73 | 74 | // deposit 100 ETH 75 | uint256 pufETHReceived = pufferVault.depositETH{ value: 99 ether }(address(this)); 76 | 77 | // Rebase lido is +30.7 ETH for the Vault 78 | _rebaseLido(); 79 | 80 | // Withdraw 81 | pufferVault.withdraw(pufferVault.maxWithdraw(address(this)), address(this), address(this)); 82 | 83 | // Attacker got less than 100 ETH 84 | assertEq(_WETH.balanceOf(address(this)), 99.008169815526098175 ether, "~ 99 ether received"); 85 | // ~ 0.08 ETH profit doesn't seem worth the effort 86 | } 87 | 88 | function _rebaseLido() internal { 89 | // Simulates stETH rebasing by fast-forwarding block 19504382 where Lido oracle rebased. 90 | // Submits the same call data as the Lido oracle. 91 | // https://etherscan.io/tx/0x0a80282625c00aaa5b224011b35c3ac56783e62b2f7d55fc1550a0945245a8a7 92 | vm.roll(19504382); 93 | vm.startPrank(0xc79F702202E3A6B0B6310B537E786B9ACAA19BAf); // Lido's whitelisted Oracle 94 | (bool success,) = LIDO_ACCOUNTING_ORACLE.call( 95 | hex"fc7377cd000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000084d31f000000000000000000000000000000000000000000000000000000000005252e0000000000000000000000000000000000000000000000000022abcbe40e8f6500000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000026d9c46441fbb977e00000000000000000000000000000000000000000000000006e51877e064ce1b1900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000003c0abf5096c484be3c46da300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001958bfceee23b63bd94cf905d77f457004f9972d768450eb9a9ae2d564adaf56c000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000086ce00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000007985" 96 | ); 97 | assertTrue(success, "oracle rebase failed"); 98 | vm.stopPrank(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /test/unit/xPufETHTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import { Test } from "forge-std/Test.sol"; 5 | import { PufferDepositor } from "src/PufferDepositor.sol"; 6 | import { Timelock } from "src/Timelock.sol"; 7 | import { PufferVault } from "src/PufferVault.sol"; 8 | import { xPufETH } from "src/l2/xPufETH.sol"; 9 | import { XERC20Lockbox } from "src/XERC20Lockbox.sol"; 10 | import { stETHMock } from "test/mocks/stETHMock.sol"; 11 | import { AccessManager } from "openzeppelin/access/manager/AccessManager.sol"; 12 | import { PufferDeployment } from "src/structs/PufferDeployment.sol"; 13 | import { DeployPufETH } from "script/DeployPufETH.s.sol"; 14 | import { ROLE_ID_DAO, ROLE_ID_LOCKBOX } from "script/Roles.sol"; 15 | import { ERC1967Proxy } from "openzeppelin/proxy/ERC1967/ERC1967Proxy.sol"; 16 | import { Initializable } from "openzeppelin/proxy/utils/Initializable.sol"; 17 | 18 | contract xPufETHTest is Test { 19 | PufferDepositor public pufferDepositor; 20 | PufferVault public pufferVault; 21 | AccessManager public accessManager; 22 | stETHMock public stETH; 23 | Timelock public timelock; 24 | xPufETH public xPufETHProxy; 25 | XERC20Lockbox public xERC20Lockbox; 26 | 27 | function setUp() public { 28 | PufferDeployment memory deployment = new DeployPufETH().run(); 29 | pufferDepositor = PufferDepositor(payable(deployment.pufferDepositor)); 30 | pufferVault = PufferVault(payable(deployment.pufferVault)); 31 | accessManager = AccessManager(payable(deployment.accessManager)); 32 | stETH = stETHMock(payable(deployment.stETH)); 33 | timelock = Timelock(payable(deployment.timelock)); 34 | 35 | // Deploy implementation 36 | xPufETH newImplementation = new xPufETH(); 37 | 38 | // Deploy proxy 39 | vm.expectEmit(true, true, true, true); 40 | emit Initializable.Initialized(1); 41 | xPufETHProxy = xPufETH( 42 | address( 43 | new ERC1967Proxy{ salt: bytes32("xPufETH") }( 44 | address(newImplementation), abi.encodeCall(xPufETH.initialize, (address(accessManager))) 45 | ) 46 | ) 47 | ); 48 | 49 | // Deploy the lockbox 50 | xERC20Lockbox = new XERC20Lockbox(address(xPufETHProxy), address(deployment.pufferVault)); 51 | 52 | // Setup AccessManager stuff 53 | // Setup access 54 | bytes4[] memory daoSelectors = new bytes4[](2); 55 | daoSelectors[0] = xPufETH.setLockbox.selector; 56 | daoSelectors[1] = xPufETH.setLimits.selector; 57 | 58 | bytes4[] memory lockBoxSelectors = new bytes4[](2); 59 | lockBoxSelectors[0] = xPufETH.mint.selector; 60 | lockBoxSelectors[1] = xPufETH.burn.selector; 61 | 62 | // Public selectors 63 | vm.startPrank(address(timelock)); 64 | accessManager.setTargetFunctionRole(address(xPufETHProxy), lockBoxSelectors, accessManager.PUBLIC_ROLE()); 65 | accessManager.setTargetFunctionRole(address(xPufETHProxy), daoSelectors, ROLE_ID_DAO); 66 | accessManager.grantRole(ROLE_ID_LOCKBOX, address(xERC20Lockbox), 0); 67 | accessManager.grantRole(ROLE_ID_DAO, address(this), 0); // this contract is the dao for simplicity 68 | vm.stopPrank(); 69 | 70 | // Set the Lockbox) 71 | xPufETHProxy.setLockbox(address(xERC20Lockbox)); 72 | 73 | // Mint mock steth to this contract 74 | stETH.mint(address(this), type(uint128).max); 75 | } 76 | 77 | // We deposit pufETH to get xpufETH to this contract using .depositTo 78 | function test_mint_xpufETH(uint8 amount) public { 79 | stETH.approve(address(pufferVault), type(uint256).max); 80 | pufferVault.deposit(uint256(amount), address(this)); 81 | 82 | pufferVault.approve(address(xERC20Lockbox), type(uint256).max); 83 | xERC20Lockbox.depositTo(address(this), uint256(amount)); 84 | assertEq(xPufETHProxy.balanceOf(address(this)), uint256(amount), "got xpufETH"); 85 | assertEq(pufferVault.balanceOf(address(xERC20Lockbox)), uint256(amount), "pufETH is in the lockbox"); 86 | } 87 | 88 | // We deposit pufETH to get xpufETH to this contract using .deposit 89 | function test_deposit_pufETH_for_xpufETH(uint8 amount) public { 90 | stETH.approve(address(pufferVault), type(uint256).max); 91 | pufferVault.deposit(uint256(amount), address(this)); 92 | 93 | pufferVault.approve(address(xERC20Lockbox), type(uint256).max); 94 | xERC20Lockbox.deposit(uint256(amount)); 95 | assertEq(xPufETHProxy.balanceOf(address(this)), uint256(amount), "got xpufETH"); 96 | assertEq(pufferVault.balanceOf(address(xERC20Lockbox)), uint256(amount), "pufETH is in the lockbox"); 97 | } 98 | 99 | // We withdraw pufETH to Bob 100 | function test_mint_and_burn_xpufETH(uint8 amount) public { 101 | address bob = makeAddr("bob"); 102 | test_mint_xpufETH(amount); 103 | 104 | xPufETHProxy.approve(address(xERC20Lockbox), type(uint256).max); 105 | xERC20Lockbox.withdrawTo(bob, uint256(amount)); 106 | assertEq(pufferVault.balanceOf(bob), amount, "bob got pufETH"); 107 | } 108 | 109 | // We withdraw to self 110 | function test_mint_and_withdraw_xpufETH(uint8 amount) public { 111 | test_mint_xpufETH(amount); 112 | 113 | xPufETHProxy.approve(address(xERC20Lockbox), type(uint256).max); 114 | 115 | uint256 pufEThBalanceBefore = pufferVault.balanceOf(address(this)); 116 | 117 | xERC20Lockbox.withdraw(uint256(amount)); 118 | assertEq(pufferVault.balanceOf(address(this)), pufEThBalanceBefore + amount, "we got pufETH"); 119 | } 120 | 121 | function test_nativeReverts() public { 122 | vm.expectRevert(); 123 | xERC20Lockbox.depositNativeTo(address(0)); 124 | 125 | vm.expectRevert(); 126 | xERC20Lockbox.depositNative(); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /test/Integration/PufferVaultV2WithdrawFromEl.fork.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import { Test } from "forge-std/Test.sol"; 5 | import { PufferVaultV2 } from "../../src/PufferVaultV2.sol"; 6 | import { IStETH } from "../../src/interface/Lido/IStETH.sol"; 7 | import { ILidoWithdrawalQueue } from "../../src/interface/Lido/ILidoWithdrawalQueue.sol"; 8 | import { IWETH } from "../../src/interface/Other/IWETH.sol"; 9 | import { IStrategy } from "../../src/interface/EigenLayer/IStrategy.sol"; 10 | import { IEigenLayer } from "../../src/interface/EigenLayer/IEigenLayer.sol"; 11 | import { IDelegationManager } from "../../src/interface/EigenLayer/IDelegationManager.sol"; 12 | import { UUPSUpgradeable } from "@openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; 13 | import { IPufferOracle } from "../../src/interface/IPufferOracle.sol"; 14 | import { IERC20 } from "openzeppelin/token/ERC20/IERC20.sol"; 15 | import { AccessManager } from "openzeppelin/access/manager/AccessManager.sol"; 16 | import { ERC1967Utils } from "openzeppelin/proxy/ERC1967/ERC1967Utils.sol"; 17 | import { PufferDeployment } from "../../src/structs/PufferDeployment.sol"; 18 | import { DeployPufETH } from "script/DeployPufETH.s.sol"; 19 | import { PufferDepositor } from "../../src/PufferDepositor.sol"; 20 | import { PufferVault } from "../../src/PufferVault.sol"; 21 | import { Timelock } from "../../src/Timelock.sol"; 22 | import { GenerateAccessManagerCallData } from "script/GenerateAccessManagerCallData.sol"; 23 | import { MockPufferOracle } from "../../test/mocks/MockPufferOracle.sol"; 24 | 25 | contract PufferVaultWithdrawalTest is Test { 26 | PufferVaultV2 newImpl; 27 | 28 | PufferVault pufferVault; 29 | AccessManager accessManager; 30 | Timelock timelock; 31 | IStETH stETH; 32 | 33 | address pufferDevWallet = 0xDDDeAfB492752FC64220ddB3E7C9f1d5CcCdFdF0; 34 | address operations = 0x5568b309259131D3A7c128700195e0A1C94761A0; 35 | address community = 0xf9F846FA49e79BE8d74c68CDC01AaaFfBBf8177F; 36 | 37 | address pufferDepositor; 38 | 39 | function setUp() public { 40 | // Cancun upgrade 41 | vm.createSelectFork(vm.rpcUrl("holesky"), 1304211); 42 | 43 | // Dep 44 | PufferDeployment memory deployment = new DeployPufETH().run(); 45 | pufferDepositor = deployment.pufferDepositor; 46 | pufferVault = PufferVault(payable(deployment.pufferVault)); 47 | accessManager = AccessManager(payable(deployment.accessManager)); 48 | timelock = Timelock(payable(deployment.timelock)); 49 | 50 | stETH = IStETH(address(0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034)); 51 | IWETH weth = IWETH(0xD6eF375Ad62f1d5BC06479fD0c7DCEF28e5Dc898); 52 | ILidoWithdrawalQueue lidoWithdrawalQueue = ILidoWithdrawalQueue(0xc7cc160b58F8Bb0baC94b80847E2CF2800565C50); 53 | IStrategy stETHStrategy = IStrategy(0x7D704507b76571a51d9caE8AdDAbBFd0ba0e63d3); 54 | IEigenLayer eigenStrategyManager = IEigenLayer(0xdfB5f6CE42aAA7830E94ECFCcAd411beF4d4D5b6); 55 | MockPufferOracle oracle = new MockPufferOracle(); 56 | IDelegationManager delegationManager = IDelegationManager(0xA44151489861Fe9e3055d95adC98FbD462B948e7); 57 | 58 | newImpl = new PufferVaultV2( 59 | stETH, 60 | weth, 61 | lidoWithdrawalQueue, 62 | stETHStrategy, 63 | eigenStrategyManager, 64 | IPufferOracle(address(oracle)), 65 | delegationManager 66 | ); 67 | } 68 | 69 | // Update contracts and setup access 70 | function _upgradeContracts() internal { 71 | // Community multisig 72 | vm.startPrank(community); 73 | vm.expectEmit(true, true, true, true); 74 | emit ERC1967Utils.Upgraded(address(newImpl)); 75 | UUPSUpgradeable(pufferVault).upgradeToAndCall(address(newImpl), abi.encodeCall(PufferVaultV2.initialize, ())); 76 | 77 | // Setup access 78 | bytes memory encodedMulticall = new GenerateAccessManagerCallData().run(address(pufferVault), pufferDepositor); 79 | // Timelock is the owner of the AccessManager 80 | timelock.executeTransaction(address(accessManager), encodedMulticall, 1); 81 | } 82 | 83 | function test_m2_el_withdrawal() public { 84 | // On holesky, Dev wallet has some stETH that we can deposit to the contract and to the EL stETH strategy 85 | vm.startPrank(pufferDevWallet); // Puffer dev wallet 86 | stETH.approve(address(pufferVault), type(uint256).max); 87 | 88 | // Deposit stETH to PufferVault 89 | pufferVault.deposit(0.05 ether, pufferDevWallet); 90 | 91 | // Deposit that to EL stETH strategy 92 | vm.startPrank(operations); 93 | pufferVault.depositToEigenLayer(0.05 ether); 94 | 95 | // Upgrade contracts and setup access 96 | _upgradeContracts(); 97 | 98 | uint256 assetsBefore = pufferVault.totalAssets(); 99 | 100 | // After the upgrade use the new flow for withdrawal 101 | vm.startPrank(operations); 102 | PufferVaultV2(payable(pufferVault)).initiateStETHWithdrawalFromEigenLayer(0.05 ether); 103 | 104 | assertApproxEqAbs(assetsBefore, pufferVault.totalAssets(), 1, "assets must not change"); 105 | 106 | uint256 startBlock = block.number; 107 | 108 | // Fast forward 109 | vm.roll(block.number + 10000); 110 | 111 | IEigenLayer.WithdrawerAndNonce memory withdrawerAndNonce = 112 | IEigenLayer.WithdrawerAndNonce({ withdrawer: address(pufferVault), nonce: 0 }); 113 | 114 | IERC20[] memory tokens = new IERC20[](1); 115 | tokens[0] = IERC20(address(stETH)); 116 | 117 | IStrategy[] memory strategies = new IStrategy[](1); 118 | strategies[0] = IStrategy(0x7D704507b76571a51d9caE8AdDAbBFd0ba0e63d3); // stETH strategy 119 | 120 | uint256[] memory shares = new uint256[](1); 121 | shares[0] = 0.05 ether; 122 | 123 | IEigenLayer.QueuedWithdrawal memory queuedWithdrawal = IEigenLayer.QueuedWithdrawal({ 124 | strategies: strategies, 125 | shares: shares, 126 | depositor: address(pufferVault), 127 | withdrawerAndNonce: withdrawerAndNonce, 128 | withdrawalStartBlock: uint32(startBlock), 129 | delegatedAddress: address(0) 130 | }); 131 | 132 | // Claim the withdrawal using the new flow 133 | PufferVaultV2(payable(pufferVault)).claimWithdrawalFromEigenLayerM2(queuedWithdrawal, tokens, 0, 0); 134 | 135 | assertApproxEqAbs( 136 | assetsBefore, pufferVault.totalAssets(), 2, "assets must not change after everything is withdrawn" 137 | ); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/PufferDepositor.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 { PufferVault } from "./PufferVault.sol"; 12 | import { PufferDepositorStorage } from "./PufferDepositorStorage.sol"; 13 | import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 14 | import { ISushiRouter } from "./interface/Other/ISushiRouter.sol"; 15 | import { IPufferDepositor } from "./interface/IPufferDepositor.sol"; 16 | import { Permit } from "./structs/Permit.sol"; 17 | 18 | /** 19 | * @title PufferDepositor 20 | * @author Puffer Finance 21 | * @custom:security-contact security@puffer.fi 22 | */ 23 | contract PufferDepositor is IPufferDepositor, PufferDepositorStorage, AccessManagedUpgradeable, UUPSUpgradeable { 24 | using SafeERC20 for address; 25 | 26 | IStETH internal immutable _ST_ETH; 27 | IWstETH internal constant _WST_ETH = IWstETH(0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0); 28 | 29 | /** 30 | * @dev This is how both 1Inch and Sushi represent native ETH 31 | */ 32 | address internal constant _NATIVE_ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; 33 | address internal constant _1INCH_ROUTER = 0x1111111254EEB25477B68fb85Ed929f73A960582; 34 | ISushiRouter internal constant _SUSHI_ROUTER = ISushiRouter(0x5550D13389bB70F45fCeF58f19f6b6e87F6e747d); 35 | 36 | /** 37 | * @dev The Puffer Vault contract address 38 | */ 39 | PufferVault public immutable PUFFER_VAULT; 40 | 41 | constructor(PufferVault pufferVault, IStETH stETH) payable { 42 | PUFFER_VAULT = pufferVault; 43 | _ST_ETH = stETH; 44 | _disableInitializers(); 45 | } 46 | 47 | function initialize(address accessManager) external initializer { 48 | __AccessManaged_init(accessManager); 49 | SafeERC20.safeIncreaseAllowance(_ST_ETH, address(PUFFER_VAULT), type(uint256).max); 50 | } 51 | 52 | /** 53 | * @inheritdoc IPufferDepositor 54 | */ 55 | function swapAndDeposit1Inch(address tokenIn, uint256 amountIn, bytes calldata callData) 56 | public 57 | payable 58 | virtual 59 | restricted 60 | returns (uint256 pufETHAmount) 61 | { 62 | if (tokenIn != _NATIVE_ETH) { 63 | SafeERC20.safeTransferFrom(IERC20(tokenIn), msg.sender, address(this), amountIn); 64 | SafeERC20.safeIncreaseAllowance(IERC20(tokenIn), address(_1INCH_ROUTER), amountIn); 65 | } 66 | 67 | // PUFFER_VAULT.deposit will revert if we get no stETH from this contract 68 | (bool success, bytes memory returnData) = _1INCH_ROUTER.call{ value: msg.value }(callData); 69 | if (!success) { 70 | revert SwapFailed(address(tokenIn), amountIn); 71 | } 72 | 73 | uint256 amountOut = abi.decode(returnData, (uint256)); 74 | 75 | if (amountOut == 0) { 76 | revert SwapFailed(address(tokenIn), amountIn); 77 | } 78 | 79 | return PUFFER_VAULT.deposit(amountOut, msg.sender); 80 | } 81 | 82 | /** 83 | * @inheritdoc IPufferDepositor 84 | */ 85 | function swapAndDepositWithPermit1Inch(address tokenIn, Permit calldata permitData, bytes calldata callData) 86 | public 87 | payable 88 | virtual 89 | restricted 90 | returns (uint256 pufETHAmount) 91 | { 92 | try ERC20Permit(address(tokenIn)).permit({ 93 | owner: msg.sender, 94 | spender: address(this), 95 | value: permitData.amount, 96 | deadline: permitData.deadline, 97 | v: permitData.v, 98 | s: permitData.s, 99 | r: permitData.r 100 | }) { } catch { } 101 | 102 | return swapAndDeposit1Inch(tokenIn, permitData.amount, callData); 103 | } 104 | 105 | /** 106 | * @inheritdoc IPufferDepositor 107 | */ 108 | function swapAndDeposit(address tokenIn, uint256 amountIn, uint256 amountOutMin, bytes calldata routeCode) 109 | public 110 | payable 111 | virtual 112 | restricted 113 | returns (uint256 pufETHAmount) 114 | { 115 | if (tokenIn != _NATIVE_ETH) { 116 | SafeERC20.safeTransferFrom(IERC20(tokenIn), msg.sender, address(this), amountIn); 117 | SafeERC20.safeIncreaseAllowance(IERC20(tokenIn), address(_SUSHI_ROUTER), amountIn); 118 | } 119 | 120 | uint256 stETHAmountOut = _SUSHI_ROUTER.processRoute{ value: msg.value }({ 121 | tokenIn: tokenIn, 122 | amountIn: amountIn, 123 | tokenOut: address(_ST_ETH), 124 | amountOutMin: amountOutMin, 125 | to: address(this), 126 | route: routeCode 127 | }); 128 | 129 | if (stETHAmountOut == 0) { 130 | revert SwapFailed(address(tokenIn), amountIn); 131 | } 132 | 133 | return PUFFER_VAULT.deposit(stETHAmountOut, msg.sender); 134 | } 135 | 136 | /** 137 | * @inheritdoc IPufferDepositor 138 | */ 139 | function swapAndDepositWithPermit( 140 | address tokenIn, 141 | uint256 amountOutMin, 142 | Permit calldata permitData, 143 | bytes calldata routeCode 144 | ) public payable virtual restricted returns (uint256 pufETHAmount) { 145 | try ERC20Permit(address(tokenIn)).permit({ 146 | owner: msg.sender, 147 | spender: address(this), 148 | value: permitData.amount, 149 | deadline: permitData.deadline, 150 | v: permitData.v, 151 | s: permitData.s, 152 | r: permitData.r 153 | }) { } catch { } 154 | 155 | return swapAndDeposit(tokenIn, permitData.amount, amountOutMin, routeCode); 156 | } 157 | 158 | /** 159 | * @inheritdoc IPufferDepositor 160 | */ 161 | function depositWstETH(Permit calldata permitData) external restricted returns (uint256 pufETHAmount) { 162 | try ERC20Permit(address(_WST_ETH)).permit({ 163 | owner: msg.sender, 164 | spender: address(this), 165 | value: permitData.amount, 166 | deadline: permitData.deadline, 167 | v: permitData.v, 168 | s: permitData.s, 169 | r: permitData.r 170 | }) { } catch { } 171 | 172 | SafeERC20.safeTransferFrom(IERC20(address(_WST_ETH)), msg.sender, address(this), permitData.amount); 173 | uint256 stETHAmount = _WST_ETH.unwrap(permitData.amount); 174 | 175 | return PUFFER_VAULT.deposit(stETHAmount, msg.sender); 176 | } 177 | 178 | /** 179 | * @inheritdoc IPufferDepositor 180 | */ 181 | function depositStETH(Permit calldata permitData) external restricted returns (uint256 pufETHAmount) { 182 | try ERC20Permit(address(_ST_ETH)).permit({ 183 | owner: msg.sender, 184 | spender: address(this), 185 | value: permitData.amount, 186 | deadline: permitData.deadline, 187 | v: permitData.v, 188 | s: permitData.s, 189 | r: permitData.r 190 | }) { } catch { } 191 | 192 | SafeERC20.safeTransferFrom(IERC20(address(_ST_ETH)), msg.sender, address(this), permitData.amount); 193 | 194 | return PUFFER_VAULT.deposit(permitData.amount, msg.sender); 195 | } 196 | 197 | /** 198 | * @dev Authorizes an upgrade to a new implementation 199 | * Restricted access 200 | * @param newImplementation The address of the new implementation 201 | */ 202 | function _authorizeUpgrade(address newImplementation) internal virtual override restricted { } 203 | } 204 | -------------------------------------------------------------------------------- /.gas-snapshot: -------------------------------------------------------------------------------- 1 | PufETHTest:testFail_redeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 627787, ~: 630010) 2 | PufETHTest:testFail_withdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 633476, ~: 635685) 3 | PufETHTest:test_RT_deposit_redeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 2369, ~: 2369) 4 | PufETHTest:test_RT_deposit_withdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 2391, ~: 2391) 5 | PufETHTest:test_RT_mint_redeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 2347, ~: 2347) 6 | PufETHTest:test_RT_mint_withdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 2391, ~: 2391) 7 | PufETHTest:test_RT_redeem_deposit((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 2392, ~: 2392) 8 | PufETHTest:test_RT_redeem_mint((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 2346, ~: 2346) 9 | PufETHTest:test_RT_withdraw_deposit((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 2389, ~: 2389) 10 | PufETHTest:test_RT_withdraw_mint((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 2347, ~: 2347) 11 | PufETHTest:test_asset((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 479542, ~: 483496) 12 | PufETHTest:test_convertToAssets((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 489542, ~: 492229) 13 | PufETHTest:test_convertToShares((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 488969, ~: 491800) 14 | PufETHTest:test_deposit((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 531477, ~: 536026) 15 | PufETHTest:test_erc4626_interface() (gas: 238648) 16 | PufETHTest:test_maxDeposit((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 479526, ~: 483479) 17 | PufETHTest:test_maxMint((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 479462, ~: 483415) 18 | PufETHTest:test_maxRedeem((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 479675, ~: 483628) 19 | PufETHTest:test_maxWithdraw((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 482918, ~: 486768) 20 | PufETHTest:test_mint((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 537901, ~: 542159) 21 | PufETHTest:test_previewDeposit((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 530119, ~: 532895) 22 | PufETHTest:test_previewMint((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 536462, ~: 539119) 23 | PufETHTest:test_previewRedeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 2390, ~: 2390) 24 | PufETHTest:test_previewWithdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 2346, ~: 2346) 25 | PufETHTest:test_redeem((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 2345, ~: 2345) 26 | PufETHTest:test_roles_setup() (gas: 99794) 27 | PufETHTest:test_totalAssets((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 481667, ~: 485618) 28 | PufETHTest:test_withdraw((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 2369, ~: 2369) 29 | PufferDepositorV2ForkTest:test_stETH_approve_deposit() (gas: 321650) 30 | PufferDepositorV2ForkTest:test_stETH_approve_deposit_to_bob() (gas: 282603) 31 | PufferDepositorV2ForkTest:test_stETH_permit_deposit() (gas: 352339) 32 | PufferDepositorV2ForkTest:test_stETH_permit_deposit_to_bob() (gas: 314962) 33 | PufferDepositorV2ForkTest:test_wstETH_approve_deposit() (gas: 266346) 34 | PufferDepositorV2ForkTest:test_wstETH_approve_deposit_to_bob() (gas: 272144) 35 | PufferDepositorV2ForkTest:test_wstETH_permit_deposit() (gas: 291857) 36 | PufferDepositorV2ForkTest:test_wstETH_permit_deposit_to_bob() (gas: 301396) 37 | PufferTest:test_1inch_complex_swap() (gas: 22091412) 38 | PufferTest:test_ape_to_pufETH() (gas: 468864) 39 | PufferTest:test_conversions_and_deposit_to_el() (gas: 1754220) 40 | PufferTest:test_deposit_stETH_permit() (gas: 315987) 41 | PufferTest:test_deposit_wstETH() (gas: 324761) 42 | PufferTest:test_deposit_wstETH_permit() (gas: 324163) 43 | PufferTest:test_depositingStETH_and_withdrawal() (gas: 1502959) 44 | PufferTest:test_eigenlayer_cap_reached() (gas: 232344) 45 | PufferTest:test_eth_1inch_swap() (gas: 393599) 46 | PufferTest:test_eth_sushi_swap() (gas: 367518) 47 | PufferTest:test_failed_swap_1inch() (gas: 179425) 48 | PufferTest:test_lido_withdrawal_dos() (gas: 696274) 49 | PufferTest:test_minting_and_lido_rebasing() (gas: 1039391) 50 | PufferTest:test_swap_1inch() (gas: 439801) 51 | PufferTest:test_swap_1inch_permit() (gas: 431084) 52 | PufferTest:test_upgrade_to_mainnet() (gas: 7411243) 53 | PufferTest:test_usdc_permit_upgrade() (gas: 21887982) 54 | PufferTest:test_usdc_to_pufETH() (gas: 513797) 55 | PufferTest:test_usdc_to_pufETH_permit() (gas: 516169) 56 | PufferTest:test_usdt_to_pufETH() (gas: 511286) 57 | PufferTest:test_withdraw_from_eigenLayer() (gas: 659458) 58 | PufferTest:test_withdraw_from_eigenLayer_dos() (gas: 888388) 59 | PufferTest:test_zero_stETH_deposit() (gas: 225443) 60 | PufferVaultV2ForkTest:test_burn() (gas: 106659) 61 | PufferVaultV2ForkTest:test_change_withdrawal_limit() (gas: 785551) 62 | PufferVaultV2ForkTest:test_deposit() (gas: 318955) 63 | PufferVaultV2ForkTest:test_deposit_fails_when_not_enough_funds() (gas: 298835) 64 | PufferVaultV2ForkTest:test_eth_weth_stETH_deposits() (gas: 491344) 65 | PufferVaultV2ForkTest:test_max_deposit() (gas: 52252) 66 | PufferVaultV2ForkTest:test_max_withdrawal() (gas: 260397) 67 | PufferVaultV2ForkTest:test_mint() (gas: 339225) 68 | PufferVaultV2ForkTest:test_redeem_fails_if_no_eth_seeded() (gas: 212427) 69 | PufferVaultV2ForkTest:test_redeem_fails_if_owner_is_not_caller() (gas: 723141) 70 | PufferVaultV2ForkTest:test_redeem_succeeds_if_seeded_with_eth() (gas: 866934) 71 | PufferVaultV2ForkTest:test_redeem_succeeds_with_allowance() (gas: 768074) 72 | PufferVaultV2ForkTest:test_redeem_transfers_to_receiver() (gas: 740717) 73 | PufferVaultV2ForkTest:test_redemption_fee() (gas: 925832) 74 | PufferVaultV2ForkTest:test_sanity() (gas: 152728) 75 | PufferVaultV2ForkTest:test_setDailyWithdrawalLimit() (gas: 247763) 76 | PufferVaultV2ForkTest:test_set_exit_fee_change() (gas: 923090) 77 | PufferVaultV2ForkTest:test_transferETH() (gas: 696772) 78 | PufferVaultV2ForkTest:test_transferETH_with_weth_liquidity() (gas: 347144) 79 | PufferVaultV2ForkTest:test_withdraw_fee() (gas: 893821) 80 | PufferVaultV2ForkTest:test_withdrawal() (gas: 926616) 81 | PufferVaultV2ForkTest:test_withdrawal_fails_if_owner_is_not_caller() (gas: 723192) 82 | PufferVaultV2ForkTest:test_withdrawal_fails_when_exceeding_maximum() (gas: 682553) 83 | PufferVaultV2ForkTest:test_withdrawal_succeeds_with_allowance() (gas: 768138) 84 | PufferVaultV2ForkTest:test_withdrawal_transfers_to_receiver() (gas: 740805) 85 | PufferVaultV2Property:testFail_redeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 647344, ~: 648411) 86 | PufferVaultV2Property:testFail_withdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 655177, ~: 655297) 87 | PufferVaultV2Property:test_RT_deposit_redeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 554145, ~: 554649) 88 | PufferVaultV2Property:test_RT_deposit_withdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 554668, ~: 555210) 89 | PufferVaultV2Property:test_RT_mint_redeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 561851, ~: 562312) 90 | PufferVaultV2Property:test_RT_mint_withdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 561963, ~: 562441) 91 | PufferVaultV2Property:test_RT_redeem_deposit((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 555072, ~: 555512) 92 | PufferVaultV2Property:test_RT_redeem_mint((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 556767, ~: 557294) 93 | PufferVaultV2Property:test_RT_withdraw_deposit((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 561341, ~: 561517) 94 | PufferVaultV2Property:test_RT_withdraw_mint((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 562886, ~: 563065) 95 | PufferVaultV2Property:test_asset((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 475534, ~: 475557) 96 | PufferVaultV2Property:test_convertToAssets((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 486722, ~: 486684) 97 | PufferVaultV2Property:test_convertToShares((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 486349, ~: 486577) 98 | PufferVaultV2Property:test_deposit((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 528983, ~: 529152) 99 | PufferVaultV2Property:test_maxDeposit((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 475509, ~: 475532) 100 | PufferVaultV2Property:test_maxMint((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 475427, ~: 475450) 101 | PufferVaultV2Property:test_maxRedeem((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 484392, ~: 484416) 102 | PufferVaultV2Property:test_maxWithdraw((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 484485, ~: 484504) 103 | PufferVaultV2Property:test_mint((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 536599, ~: 536658) 104 | PufferVaultV2Property:test_previewDeposit((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 526837, ~: 527236) 105 | PufferVaultV2Property:test_previewMint((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 534587, ~: 534913) 106 | PufferVaultV2Property:test_previewRedeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 546417, ~: 546820) 107 | PufferVaultV2Property:test_previewWithdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 552694, ~: 552806) 108 | PufferVaultV2Property:test_redeem((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 547967, ~: 548486) 109 | PufferVaultV2Property:test_totalAssets((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 479051, ~: 479074) 110 | PufferVaultV2Property:test_withdraw((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 554277, ~: 554418) 111 | TimelockTest:test_cancel_reverts_if_caller_unauthorized(address) (runs: 256, μ: 11272, ~: 11272) 112 | TimelockTest:test_cancel_transaction() (gas: 38801) 113 | TimelockTest:test_change_pauser() (gas: 23527) 114 | TimelockTest:test_execute_reverts_if_caller_unauthorized(address) (runs: 256, μ: 13166, ~: 13166) 115 | TimelockTest:test_initial_access_manager_setup(address) (runs: 256, μ: 68521, ~: 68521) 116 | TimelockTest:test_pause_depositor(address) (runs: 256, μ: 59282, ~: 59282) 117 | TimelockTest:test_pause_should_revert_if_bad_caller(address) (runs: 256, μ: 15709, ~: 15709) 118 | TimelockTest:test_queue_should_revert_if_operations_is_not_the_caller(address) (runs: 256, μ: 11273, ~: 11273) 119 | TimelockTest:test_queueing_duplicate_transaction_different_operation_id() (gas: 79063) 120 | TimelockTest:test_setDelay_reverts_if_caller_unauthorized(address) (runs: 256, μ: 9506, ~: 9506) 121 | TimelockTest:test_setPauser_reverts_if_caller_unauthorized(address) (runs: 256, μ: 9545, ~: 9545) 122 | TimelockTest:test_set_delay_queued() (gas: 43164) 123 | TimelockTest:test_update_delay_from_community_without_timelock() (gas: 31482) -------------------------------------------------------------------------------- /src/interface/EigenLayer/IDelegationManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity >=0.5.0; 3 | 4 | import { IERC20 } from "openzeppelin/token/ERC20/IERC20.sol"; 5 | import { IStrategy } from "./IStrategy.sol"; 6 | 7 | interface IDelegationManager { 8 | // @notice Struct used for storing information about a single operator who has registered with EigenLayer 9 | struct OperatorDetails { 10 | // @notice address to receive the rewards that the operator earns via serving applications built on EigenLayer. 11 | address earningsReceiver; 12 | /** 13 | * @notice Address to verify signatures when a staker wishes to delegate to the operator, as well as controlling "forced undelegations". 14 | * @dev Signature verification follows these rules: 15 | * 1) If this address is left as address(0), then any staker will be free to delegate to the operator, i.e. no signature verification will be performed. 16 | * 2) If this address is an EOA (i.e. it has no code), then we follow standard ECDSA signature verification for delegations to the operator. 17 | * 3) If this address is a contract (i.e. it has code) then we forward a call to the contract and verify that it returns the correct EIP-1271 "magic value". 18 | */ 19 | address delegationApprover; 20 | /** 21 | * @notice A minimum delay -- measured in blocks -- enforced between: 22 | * 1) the operator signalling their intent to register for a service, via calling `Slasher.optIntoSlashing` 23 | * and 24 | * 2) the operator completing registration for the service, via the service ultimately calling `Slasher.recordFirstStakeUpdate` 25 | * @dev note that for a specific operator, this value *cannot decrease*, i.e. if the operator wishes to modify their OperatorDetails, 26 | * then they are only allowed to either increase this value or keep it the same. 27 | */ 28 | uint32 stakerOptOutWindowBlocks; 29 | } 30 | 31 | /** 32 | * @notice Abstract struct used in calculating an EIP712 signature for a staker to approve that they (the staker themselves) delegate to a specific operator. 33 | * @dev Used in computing the `STAKER_DELEGATION_TYPEHASH` and as a reference in the computation of the stakerDigestHash in the `delegateToBySignature` function. 34 | */ 35 | struct StakerDelegation { 36 | // the staker who is delegating 37 | address staker; 38 | // the operator being delegated to 39 | address operator; 40 | // the staker's nonce 41 | uint256 nonce; 42 | // the expiration timestamp (UTC) of the signature 43 | uint256 expiry; 44 | } 45 | 46 | /** 47 | * @notice Abstract struct used in calculating an EIP712 signature for an operator's delegationApprover to approve that a specific staker delegate to the operator. 48 | * @dev Used in computing the `DELEGATION_APPROVAL_TYPEHASH` and as a reference in the computation of the approverDigestHash in the `_delegate` function. 49 | */ 50 | struct DelegationApproval { 51 | // the staker who is delegating 52 | address staker; 53 | // the operator being delegated to 54 | address operator; 55 | // the operator's provided salt 56 | bytes32 salt; 57 | // the expiration timestamp (UTC) of the signature 58 | uint256 expiry; 59 | } 60 | 61 | /** 62 | * Struct type used to specify an existing queued withdrawal. Rather than storing the entire struct, only a hash is stored. 63 | * In functions that operate on existing queued withdrawals -- e.g. completeQueuedWithdrawal`, the data is resubmitted and the hash of the submitted 64 | * data is computed by `calculateWithdrawalRoot` and checked against the stored hash in order to confirm the integrity of the submitted data. 65 | */ 66 | struct Withdrawal { 67 | // The address that originated the Withdrawal 68 | address staker; 69 | // The address that the staker was delegated to at the time that the Withdrawal was created 70 | address delegatedTo; 71 | // The address that can complete the Withdrawal + will receive funds when completing the withdrawal 72 | address withdrawer; 73 | // Nonce used to guarantee that otherwise identical withdrawals have unique hashes 74 | uint256 nonce; 75 | // Block number when the Withdrawal was created 76 | uint32 startBlock; 77 | // Array of strategies that the Withdrawal contains 78 | IStrategy[] strategies; 79 | // Array containing the amount of shares in each Strategy in the `strategies` array 80 | uint256[] shares; 81 | } 82 | 83 | struct QueuedWithdrawalParams { 84 | // Array of strategies that the QueuedWithdrawal contains 85 | IStrategy[] strategies; 86 | // Array containing the amount of shares in each Strategy in the `strategies` array 87 | uint256[] shares; 88 | // The address of the withdrawer 89 | address withdrawer; 90 | } 91 | 92 | // @notice Emitted when a new operator registers in EigenLayer and provides their OperatorDetails. 93 | event OperatorRegistered(address indexed operator, OperatorDetails operatorDetails); 94 | 95 | /// @notice Emitted when an operator updates their OperatorDetails to @param newOperatorDetails 96 | event OperatorDetailsModified(address indexed operator, OperatorDetails newOperatorDetails); 97 | 98 | /** 99 | * @notice Emitted when @param operator indicates that they are updating their MetadataURI string 100 | * @dev Note that these strings are *never stored in storage* and are instead purely emitted in events for off-chain indexing 101 | */ 102 | event OperatorMetadataURIUpdated(address indexed operator, string metadataURI); 103 | 104 | /// @notice Emitted whenever an operator's shares are increased for a given strategy. Note that shares is the delta in the operator's shares. 105 | event OperatorSharesIncreased(address indexed operator, address staker, IStrategy strategy, uint256 shares); 106 | 107 | /// @notice Emitted whenever an operator's shares are decreased for a given strategy. Note that shares is the delta in the operator's shares. 108 | event OperatorSharesDecreased(address indexed operator, address staker, IStrategy strategy, uint256 shares); 109 | 110 | /// @notice Emitted when @param staker delegates to @param operator. 111 | event StakerDelegated(address indexed staker, address indexed operator); 112 | 113 | /// @notice Emitted when @param staker undelegates from @param operator. 114 | event StakerUndelegated(address indexed staker, address indexed operator); 115 | 116 | /// @notice Emitted when @param staker is undelegated via a call not originating from the staker themself 117 | event StakerForceUndelegated(address indexed staker, address indexed operator); 118 | 119 | /** 120 | * @notice Emitted when a new withdrawal is queued. 121 | * @param withdrawalRoot Is the hash of the `withdrawal`. 122 | * @param withdrawal Is the withdrawal itself. 123 | */ 124 | event WithdrawalQueued(bytes32 withdrawalRoot, Withdrawal withdrawal); 125 | 126 | /// @notice Emitted when a queued withdrawal is completed 127 | event WithdrawalCompleted(bytes32 withdrawalRoot); 128 | 129 | /// @notice Emitted when a queued withdrawal is *migrated* from the StrategyManager to the DelegationManager 130 | event WithdrawalMigrated(bytes32 oldWithdrawalRoot, bytes32 newWithdrawalRoot); 131 | 132 | /// @notice Emitted when the `minWithdrawalDelayBlocks` variable is modified from `previousValue` to `newValue`. 133 | event MinWithdrawalDelayBlocksSet(uint256 previousValue, uint256 newValue); 134 | 135 | /// @notice Emitted when the `strategyWithdrawalDelayBlocks` variable is modified from `previousValue` to `newValue`. 136 | event StrategyWithdrawalDelayBlocksSet(IStrategy strategy, uint256 previousValue, uint256 newValue); 137 | 138 | /** 139 | * Allows a staker to withdraw some shares. Withdrawn shares/strategies are immediately removed 140 | * from the staker. If the staker is delegated, withdrawn shares/strategies are also removed from 141 | * their operator. 142 | * 143 | * All withdrawn shares/strategies are placed in a queue and can be fully withdrawn after a delay. 144 | */ 145 | function queueWithdrawals(QueuedWithdrawalParams[] calldata queuedWithdrawalParams) 146 | external 147 | returns (bytes32[] memory); 148 | 149 | /** 150 | * @notice Used to complete the specified `withdrawal`. The caller must match `withdrawal.withdrawer` 151 | * @param withdrawal The Withdrawal to complete. 152 | * @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. 153 | * This input can be provided with zero length if `receiveAsTokens` is set to 'false' (since in that case, this input will be unused) 154 | * @param middlewareTimesIndex is the index in the operator that the staker who triggered the withdrawal was delegated to's middleware times array 155 | * @param receiveAsTokens If true, the shares specified in the withdrawal will be withdrawn from the specified strategies themselves 156 | * and sent to the caller, through calls to `withdrawal.strategies[i].withdraw`. If false, then the shares in the specified strategies 157 | * will simply be transferred to the caller directly. 158 | * @dev middlewareTimesIndex should be calculated off chain before calling this function by finding the first index that satisfies `slasher.canWithdraw` 159 | * @dev beaconChainETHStrategy shares are non-transferrable, so if `receiveAsTokens = false` and `withdrawal.withdrawer != withdrawal.staker`, note that 160 | * any beaconChainETHStrategy shares in the `withdrawal` will be _returned to the staker_, rather than transferred to the withdrawer, unlike shares in 161 | * any other strategies, which will be transferred to the withdrawer. 162 | */ 163 | function completeQueuedWithdrawal( 164 | Withdrawal calldata withdrawal, 165 | IERC20[] calldata tokens, 166 | uint256 middlewareTimesIndex, 167 | bool receiveAsTokens 168 | ) external; 169 | 170 | /** 171 | * @notice Array-ified version of `completeQueuedWithdrawal`. 172 | * Used to complete the specified `withdrawals`. The function caller must match `withdrawals[...].withdrawer` 173 | * @param withdrawals The Withdrawals to complete. 174 | * @param tokens Array of tokens for each Withdrawal. See `completeQueuedWithdrawal` for the usage of a single array. 175 | * @param middlewareTimesIndexes One index to reference per Withdrawal. See `completeQueuedWithdrawal` for the usage of a single index. 176 | * @param receiveAsTokens Whether or not to complete each withdrawal as tokens. See `completeQueuedWithdrawal` for the usage of a single boolean. 177 | * @dev See `completeQueuedWithdrawal` for relevant dev tags 178 | */ 179 | function completeQueuedWithdrawals( 180 | Withdrawal[] calldata withdrawals, 181 | IERC20[][] calldata tokens, 182 | uint256[] calldata middlewareTimesIndexes, 183 | bool[] calldata receiveAsTokens 184 | ) external; 185 | 186 | /// @notice Returns the keccak256 hash of `withdrawal`. 187 | function calculateWithdrawalRoot(Withdrawal memory withdrawal) external pure returns (bytes32); 188 | } 189 | -------------------------------------------------------------------------------- /test/TestHelper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import { Test } from "forge-std/Test.sol"; 5 | import { IERC20 } from "openzeppelin/token/ERC20/IERC20.sol"; 6 | import { PufferVaultV2 } from "../src/PufferVaultV2.sol"; 7 | import { PufferVaultV2Tests } from "../src/PufferVaultV2Tests.sol"; 8 | import { PufferDepositorV2 } from "../src/PufferDepositorV2.sol"; 9 | import { MockPufferOracle } from "./mocks/MockPufferOracle.sol"; 10 | import { IEigenLayer } from "../src/interface/EigenLayer/IEigenLayer.sol"; 11 | import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 12 | import { UUPSUpgradeable } from "@openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; 13 | import { stdStorage, StdStorage } from "forge-std/Test.sol"; 14 | import { AccessManager } from "openzeppelin/access/manager/AccessManager.sol"; 15 | import { IStETH } from "../src/interface/Lido/IStETH.sol"; 16 | import { IWstETH } from "../src/interface/Lido/IWstETH.sol"; 17 | import { ILidoWithdrawalQueue } from "../src/interface/Lido/ILidoWithdrawalQueue.sol"; 18 | import { IStrategy } from "../src/interface/EigenLayer/IStrategy.sol"; 19 | import { Timelock } from "../src/Timelock.sol"; 20 | import { IWETH } from "../src/interface/Other/IWETH.sol"; 21 | import { GenerateAccessManagerCallData } from "script/GenerateAccessManagerCallData.sol"; 22 | import { Permit } from "../src/structs/Permit.sol"; 23 | import { ERC1967Utils } from "openzeppelin/proxy/ERC1967/ERC1967Utils.sol"; 24 | import { IDelegationManager } from "../src/interface/EigenLayer/IDelegationManager.sol"; 25 | 26 | contract TestHelper is Test { 27 | /** 28 | * @dev Ethereum Mainnet addresses 29 | */ 30 | IStrategy internal constant _EIGEN_STETH_STRATEGY = IStrategy(0x93c4b944D05dfe6df7645A86cd2206016c51564D); 31 | IEigenLayer internal constant _EIGEN_STRATEGY_MANAGER = IEigenLayer(0x858646372CC42E1A627fcE94aa7A7033e7CF075A); 32 | IStETH internal constant _ST_ETH = IStETH(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84); 33 | IWETH internal constant _WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); 34 | IWstETH internal constant _WST_ETH = IWstETH(0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0); 35 | ILidoWithdrawalQueue internal constant _LIDO_WITHDRAWAL_QUEUE = 36 | ILidoWithdrawalQueue(0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1); 37 | IDelegationManager internal constant _EIGEN_DELEGATION_MANGER = 38 | IDelegationManager(0x39053D51B77DC0d36036Fc1fCc8Cb819df8Ef37A); 39 | 40 | using stdStorage for StdStorage; 41 | 42 | bytes32 private constant _PERMIT_TYPEHASH = 43 | keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); 44 | 45 | struct _TestTemps { 46 | address owner; 47 | address to; 48 | uint256 amount; 49 | uint256 deadline; 50 | uint8 v; 51 | bytes32 r; 52 | bytes32 s; 53 | uint256 privateKey; 54 | uint256 nonce; 55 | bytes32 domainSeparator; 56 | } 57 | 58 | PufferDepositorV2 public pufferDepositor; 59 | PufferVaultV2 public pufferVault; 60 | PufferVaultV2 public pufferVaultWithBlocking; 61 | // Non blocking version is required because of the foundry tests 62 | PufferVaultV2 public pufferVaultNonBlocking; 63 | AccessManager public accessManager; 64 | Timelock public timelock; 65 | 66 | // Lido contract (stETH) 67 | IStETH stETH = IStETH(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84); 68 | // EL Strategy Manager 69 | IEigenLayer eigenStrategyManager = IEigenLayer(0x858646372CC42E1A627fcE94aa7A7033e7CF075A); 70 | 71 | address alice = makeAddr("alice"); 72 | // Bob.. 73 | address bob; 74 | uint256 bobSK; 75 | address charlie = makeAddr("charlie"); 76 | address dave = makeAddr("dave"); 77 | address eve = makeAddr("eve"); 78 | 79 | // Use Maker address for mainnet fork tests to get wETH 80 | address MAKER_VAULT = 0x2F0b23f53734252Bda2277357e97e1517d6B042A; 81 | // Use Blast deposit contract for mainnet fork tests to get stETH 82 | address BLAST_DEPOSIT = 0x5F6AE08B8AeB7078cf2F96AFb089D7c9f51DA47d; 83 | 84 | address LIDO_WITHDRAWAL_QUEUE = 0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1; 85 | address LIDO_ACCOUNTING_ORACLE = 0x852deD011285fe67063a08005c71a85690503Cee; 86 | 87 | // Storage slot for the Consensus Layer Balance in stETH 88 | bytes32 internal constant CL_BALANCE_POSITION = 0xa66d35f054e68143c18f32c990ed5cb972bb68a68f500cd2dd3a16bbf3686483; // keccak256("lido.Lido.beaconBalance"); 89 | 90 | address COMMUNITY_MULTISIG; 91 | address OPERATIONS_MULTISIG; 92 | 93 | address mockPufferProtocol = makeAddr("mockPufferProtocol"); 94 | 95 | // Transfer `token` from `from` to `to` to fill accounts in mainnet fork tests 96 | modifier giveToken(address from, address token, address to, uint256 amount) { 97 | _giveToken(from, token, to, amount); 98 | _; 99 | } 100 | 101 | function _giveToken(address from, address token, address to, uint256 amount) internal { 102 | vm.startPrank(from); 103 | SafeERC20.safeTransfer(IERC20(token), to, amount); 104 | vm.stopPrank(); 105 | } 106 | 107 | modifier withCaller(address caller) { 108 | vm.startPrank(caller); 109 | _; 110 | vm.stopPrank(); 111 | } 112 | 113 | function setUp() public virtual { 114 | vm.createSelectFork(vm.rpcUrl("mainnet"), 19271279); 115 | 116 | // Setup contracts that are deployed to mainnet 117 | _setupLiveContracts(); 118 | 119 | // Upgrade to latest version 120 | _upgradeToMainnetPuffer(); 121 | } 122 | 123 | function _setupLiveContracts() internal { 124 | pufferDepositor = PufferDepositorV2(payable(0x4aA799C5dfc01ee7d790e3bf1a7C2257CE1DcefF)); 125 | pufferVault = PufferVaultV2(payable(0xD9A442856C234a39a81a089C06451EBAa4306a72)); 126 | accessManager = AccessManager(payable(0x8c1686069474410E6243425f4a10177a94EBEE11)); 127 | timelock = Timelock(payable(0x3C28B7c7Ba1A1f55c9Ce66b263B33B204f2126eA)); 128 | 129 | COMMUNITY_MULTISIG = timelock.COMMUNITY_MULTISIG(); 130 | OPERATIONS_MULTISIG = timelock.OPERATIONS_MULTISIG(); 131 | 132 | vm.label(COMMUNITY_MULTISIG, "COMMUNITY_MULTISIG"); 133 | vm.label(OPERATIONS_MULTISIG, "OPERATIONS_MULTISIG"); 134 | vm.label(address(stETH), "stETH"); 135 | vm.label(address(pufferDepositor), "PufferDepositorProxy"); 136 | vm.label(address(pufferVault), "PufferVaultProxy"); 137 | vm.label(address(accessManager), "AccessManager"); 138 | vm.label(0x17144556fd3424EDC8Fc8A4C940B2D04936d17eb, "stETH implementation"); 139 | vm.label(0x2b33CF282f867A7FF693A66e11B0FcC5552e4425, "stETH kernel"); 140 | vm.label(address(_WETH), "WETH"); 141 | vm.label(0x1111111254EEB25477B68fb85Ed929f73A960582, "1Inch router"); 142 | vm.label(MAKER_VAULT, "MAKER Vault"); 143 | vm.label(0x93c4b944D05dfe6df7645A86cd2206016c51564D, "Eigen stETH strategy"); 144 | 145 | (bob, bobSK) = makeAddrAndKey("bob"); 146 | } 147 | 148 | function _upgradeToMainnetPuffer() internal { 149 | // We use MockOracle + MockPufferProtocol to simulate the Puffer Protocol 150 | MockPufferOracle mockOracle = new MockPufferOracle(); 151 | 152 | pufferVaultNonBlocking = new PufferVaultV2Tests( 153 | _ST_ETH, 154 | _WETH, 155 | _LIDO_WITHDRAWAL_QUEUE, 156 | _EIGEN_STETH_STRATEGY, 157 | _EIGEN_STRATEGY_MANAGER, 158 | mockOracle, 159 | _EIGEN_DELEGATION_MANGER 160 | ); 161 | 162 | // Simulate that our deployed oracle becomes active and starts posting results of Puffer staking 163 | // At this time, we stop accepting stETH, and we accept only native ETH 164 | PufferVaultV2 newImplementation = pufferVaultNonBlocking; 165 | 166 | pufferVaultWithBlocking = new PufferVaultV2( 167 | _ST_ETH, 168 | _WETH, 169 | _LIDO_WITHDRAWAL_QUEUE, 170 | _EIGEN_STETH_STRATEGY, 171 | _EIGEN_STRATEGY_MANAGER, 172 | mockOracle, 173 | _EIGEN_DELEGATION_MANGER 174 | ); 175 | 176 | // Community multisig can do thing instantly 177 | vm.startPrank(COMMUNITY_MULTISIG); 178 | 179 | //Upgrade PufferVault 180 | 181 | //@todo To go this way, we need to setTargetRole for upgradeToAndCall to `0` (admin role of AccessManager) 182 | // timelock.executeTransaction( 183 | // address(pufferVault), 184 | // abi.encodeCall(UUPSUpgradeable.upgradeToAndCall, (address(newImplementation), abi.encodeCall(PufferVaultV2.initialize, ()))), 185 | // 1 186 | // ); 187 | 188 | vm.expectEmit(true, true, true, true); 189 | emit ERC1967Utils.Upgraded(address(newImplementation)); 190 | UUPSUpgradeable(pufferVault).upgradeToAndCall( 191 | address(newImplementation), abi.encodeCall(PufferVaultV2.initialize, ()) 192 | ); 193 | 194 | // Upgrade PufferDepositor 195 | PufferDepositorV2 newDepositorImplementation = 196 | new PufferDepositorV2(PufferVaultV2(payable(pufferVault)), _ST_ETH); 197 | 198 | // Upgrade PufferDepositor - no initializer here 199 | emit ERC1967Utils.Upgraded(address(newDepositorImplementation)); 200 | timelock.executeTransaction( 201 | address(pufferDepositor), 202 | abi.encodeCall( 203 | UUPSUpgradeable.upgradeToAndCall, 204 | (address(newDepositorImplementation), abi.encodeCall(PufferDepositorV2.initialize, ())) 205 | ), 206 | 1 207 | ); 208 | 209 | // Setup access 210 | 211 | bytes memory encodedMulticall = 212 | new GenerateAccessManagerCallData().run(address(pufferVault), address(pufferDepositor)); 213 | // Timelock is the owner of the AccessManager 214 | timelock.executeTransaction(address(accessManager), encodedMulticall, 1); 215 | 216 | vm.stopPrank(); 217 | } 218 | 219 | function _signPermit(_TestTemps memory t) internal pure returns (Permit memory p) { 220 | bytes32 innerHash = keccak256(abi.encode(_PERMIT_TYPEHASH, t.owner, t.to, t.amount, t.nonce, t.deadline)); 221 | bytes32 domainSeparator = t.domainSeparator; 222 | bytes32 outerHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, innerHash)); 223 | (t.v, t.r, t.s) = vm.sign(t.privateKey, outerHash); 224 | 225 | return Permit({ deadline: t.deadline, amount: t.amount, v: t.v, r: t.r, s: t.s }); 226 | } 227 | 228 | function _finalizeWithdrawals(uint256 requestIdFinalized) internal { 229 | // Alter WithdrawalRouter storage slot to mark our withdrawal requests as finalized 230 | vm.store( 231 | LIDO_WITHDRAWAL_QUEUE, 232 | keccak256("lido.WithdrawalQueue.lastFinalizedRequestId"), 233 | bytes32(uint256(requestIdFinalized)) 234 | ); 235 | } 236 | 237 | function _testTemps(string memory seed, address to, uint256 amount, uint256 deadline, bytes32 domainSeparator) 238 | internal 239 | returns (_TestTemps memory t) 240 | { 241 | (t.owner, t.privateKey) = makeAddrAndKey(seed); 242 | t.to = to; 243 | t.amount = amount; 244 | t.deadline = deadline; 245 | t.domainSeparator = domainSeparator; 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /test/Integration/PufferDepositorV2.fork.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import { TestHelper } from "../TestHelper.sol"; 5 | import { Permit } from "../../src/structs/Permit.sol"; 6 | import { IERC20 } from "openzeppelin/token/ERC20/IERC20.sol"; 7 | 8 | contract PufferDepositorV2ForkTest is TestHelper { 9 | /** 10 | * @dev Wallet that transferred pufETH to the PufferDepositor by mistake. 11 | */ 12 | address private constant PUFFER = 0x8A0C1e5cEA8e0F6dF341C005335E7fe5ed18A0a0; 13 | 14 | function setUp() public virtual override { 15 | vm.createSelectFork(vm.rpcUrl("mainnet"), 19419083); // (2024-03-12 12:45:11) UTC block 16 | 17 | // Setup contracts that are deployed to mainnet 18 | _setupLiveContracts(); 19 | 20 | assertEq(pufferVault.balanceOf(address(pufferDepositor)), 0.201 ether, "pufferDepositor pufETH"); 21 | assertEq(pufferVault.balanceOf(PUFFER), 0, "puffer pufETH before"); 22 | 23 | // Upgrade to latest version 24 | _upgradeToMainnetPuffer(); 25 | 26 | assertEq(pufferVault.balanceOf(address(pufferDepositor)), 0 ether, "pufferDepositor 0 pufETH"); 27 | assertEq(pufferVault.balanceOf(PUFFER), 0.201 ether, "returned pufETH"); 28 | } 29 | 30 | // StETH deposit through depositor and directly should mint ~amount 31 | function test_stETH_permit_deposit_to_self() 32 | public 33 | giveToken(BLAST_DEPOSIT, address(stETH), alice, 200 ether) 34 | withCaller(alice) 35 | { 36 | // Deposit amount 37 | uint256 stETHDepositAmount = 100 ether; 38 | Permit memory permit = _signPermit( 39 | _testTemps( 40 | "alice", 41 | address(pufferDepositor), 42 | stETHDepositAmount, 43 | block.timestamp, 44 | hex"260e7e1a220ea89b9454cbcdc1fcc44087325df199a3986e560d75db18b2e253" 45 | ) 46 | ); 47 | 48 | // PufferDepositor deposit 49 | uint256 depositorAmount = pufferDepositor.depositStETH(permit, alice); 50 | 51 | assertEq(depositorAmount, pufferVault.balanceOf(alice), "alice got the tokens"); 52 | 53 | stETH.approve(address(pufferVault), stETHDepositAmount); 54 | 55 | uint256 stETHSharesAmount = _ST_ETH.getSharesByPooledEth(stETHDepositAmount); 56 | 57 | // Direct deposit to the Vault 58 | uint256 directDepositAmount = pufferVault.depositStETH(stETHSharesAmount, alice); 59 | 60 | uint256 depositorAssetsAmount = pufferVault.convertToAssets(depositorAmount); 61 | uint256 directDepositAssetsAmount = pufferVault.convertToAssets(directDepositAmount); 62 | 63 | assertApproxEqAbs(pufferVault.convertToAssets(depositorAmount), depositorAssetsAmount, 1, "depositor"); 64 | assertApproxEqAbs( 65 | pufferVault.convertToAssets(directDepositAmount), directDepositAssetsAmount, 1, "direct deposit" 66 | ); 67 | 68 | assertApproxEqAbs( 69 | depositorAssetsAmount + directDepositAssetsAmount, 70 | 2 * stETHDepositAmount, 71 | 3, // 3 wei difference, because the PufferDepositor already has 1 wei of stETH (leftover) 72 | "should have ~200 eth worth of assets" 73 | ); 74 | 75 | assertApproxEqAbs(depositorAmount, directDepositAmount, 1, "depositor amount should be ~direct deposit amount"); 76 | assertApproxEqAbs(depositorAssetsAmount, directDepositAssetsAmount, 1, "received assets should be ~equal"); 77 | assertApproxEqAbs(depositorAssetsAmount, stETHDepositAmount, 1, "steth received assets should be ~equal"); 78 | } 79 | 80 | function test_stETH_donation_and_first_depositor_after_donation() 81 | public 82 | giveToken(BLAST_DEPOSIT, address(stETH), alice, 100 ether) 83 | giveToken(BLAST_DEPOSIT, address(stETH), bob, 100 ether) 84 | withCaller(alice) 85 | { 86 | // Alice transfers 1 stETH to the Vault by mistake 87 | _ST_ETH.transfer(address(pufferDepositor), 1 ether); 88 | 89 | assertEq(pufferVault.balanceOf(alice), 0, "0 pufETH for alice"); 90 | 91 | vm.startPrank(bob); 92 | 93 | Permit memory permit = _signPermit( 94 | _testTemps( 95 | "bob", 96 | address(pufferDepositor), 97 | 1 ether, 98 | block.timestamp, 99 | hex"260e7e1a220ea89b9454cbcdc1fcc44087325df199a3986e560d75db18b2e253" 100 | ) 101 | ); 102 | assertEq(0, pufferVault.balanceOf(bob), "bob got 0 pufETH"); 103 | 104 | // 1 stETH should be ~ 105 | uint256 expectedAmount = pufferVault.convertToShares(1 ether); 106 | 107 | // Bob deposits 1 stETH via PufferDepositor 108 | // But his deposit will sweep the stETH.balanceOf(pufferDepositor) as well, meaning he will get shares for Alice's 1 stETH 109 | uint256 depositorAmount = pufferDepositor.depositStETH(permit, bob); 110 | 111 | assertEq(depositorAmount, pufferVault.balanceOf(bob), "bob got"); 112 | 113 | // 3 wei difference, because the PufferDepositor already has 1 wei of stETH (leftover) 114 | assertApproxEqAbs((expectedAmount * 2), depositorAmount, 3, "bob got more pufETH than expected"); 115 | 116 | assertApproxEqAbs( 117 | pufferVault.convertToAssets(pufferVault.balanceOf(bob)), 2 ether, 3, "2 eth worth of assets for bob" 118 | ); 119 | 120 | assertEq(0, pufferVault.balanceOf(alice), "alice got 0"); 121 | } 122 | 123 | function test_stETH_share_conversion() public { 124 | uint256 stETHAmount = 100 ether; 125 | uint256 stETHSharesAmount = _ST_ETH.getSharesByPooledEth(stETHAmount); 126 | uint256 stETHAmountFromShares = _ST_ETH.getPooledEthByShares(stETHSharesAmount); 127 | 128 | assertApproxEqAbs(stETHAmount, stETHAmountFromShares, 1, "stETH amount should be ~stETH amount from shares"); 129 | } 130 | 131 | function test_stETH_permit_deposit_to_bob() 132 | public 133 | giveToken(BLAST_DEPOSIT, address(stETH), alice, 200 ether) 134 | withCaller(alice) 135 | { 136 | Permit memory permit = _signPermit( 137 | _testTemps( 138 | "alice", 139 | address(pufferDepositor), 140 | 100 ether, 141 | block.timestamp, 142 | hex"260e7e1a220ea89b9454cbcdc1fcc44087325df199a3986e560d75db18b2e253" 143 | ) 144 | ); 145 | 146 | uint256 depositorAmount = pufferDepositor.depositStETH(permit, bob); 147 | 148 | assertEq(depositorAmount, pufferVault.balanceOf(bob), "bob got the tokens"); 149 | assertEq(0, pufferVault.balanceOf(alice), "alice got 0"); 150 | } 151 | 152 | // stETH approve deposit 153 | function test_stETH_approve_deposit_to_self() 154 | public 155 | giveToken(BLAST_DEPOSIT, address(stETH), alice, 200 ether) 156 | withCaller(alice) 157 | { 158 | uint256 stETHAmount = 100 ether; 159 | // Create an unsigned permit to call function 160 | Permit memory unsignedPermit = Permit(0, stETHAmount, 0, 0, 0); 161 | IERC20(address(stETH)).approve(address(pufferDepositor), stETHAmount); 162 | 163 | uint256 depositorAmount = pufferDepositor.depositStETH(unsignedPermit, alice); 164 | 165 | assertEq(depositorAmount, pufferVault.balanceOf(alice), "alice got the tokens"); 166 | 167 | // StETH deposit through depositor and directly should mint the same amount 168 | stETH.approve(address(pufferVault), stETHAmount); 169 | 170 | uint256 stETHSharesAmount = _ST_ETH.getSharesByPooledEth(stETHAmount); 171 | 172 | uint256 directDepositAmount = pufferVault.depositStETH(stETHSharesAmount, alice); 173 | 174 | uint256 depositorAssetsAmount = pufferVault.convertToAssets(depositorAmount); 175 | uint256 directDepositAssetsAmount = pufferVault.convertToAssets(directDepositAmount); 176 | 177 | assertApproxEqAbs(depositorAmount, directDepositAmount, 1, "1 wei difference"); 178 | assertApproxEqAbs(depositorAssetsAmount, directDepositAssetsAmount, 1, "received assets should be ~equal"); 179 | assertApproxEqAbs( 180 | depositorAssetsAmount, stETHAmount, 1, "amount deposited and convertToAssets should be ~equal" 181 | ); 182 | } 183 | 184 | // stETH approve deposit to bob 185 | function test_stETH_approve_deposit_to_bob() 186 | public 187 | giveToken(BLAST_DEPOSIT, address(stETH), alice, 200 ether) 188 | withCaller(alice) 189 | { 190 | uint256 stETHAmount = 100 ether; 191 | // Create an unsigned permit to call function 192 | Permit memory unsignedPermit = Permit(0, stETHAmount, 0, 0, 0); 193 | IERC20(address(stETH)).approve(address(pufferDepositor), stETHAmount); 194 | 195 | uint256 depositorAmount = pufferDepositor.depositStETH(unsignedPermit, bob); 196 | 197 | assertEq(depositorAmount, pufferVault.balanceOf(bob), "bob got the tokens"); 198 | assertEq(0, pufferVault.balanceOf(alice), "alice got 0"); 199 | } 200 | 201 | // wstETH permit deposit 202 | function test_wstETH_permit_deposit() 203 | public 204 | giveToken(0x0B925eD163218f6662a35e0f0371Ac234f9E9371, address(_WST_ETH), alice, 1 ether) 205 | withCaller(alice) 206 | { 207 | Permit memory permit = _signPermit( 208 | _testTemps( 209 | "alice", 210 | address(pufferDepositor), 211 | 1 ether, 212 | block.timestamp, 213 | hex"d4a8ff90a402dc7d4fcbf60f5488291263c743ccff180e139f47d139cedfd5fe" 214 | ) 215 | ); 216 | uint256 received = pufferDepositor.depositWstETH(permit, alice); 217 | assertEq(received, pufferVault.balanceOf(alice), "alice got 0"); 218 | } 219 | 220 | // wstETH permit deposit to bob 221 | function test_wstETH_permit_deposit_to_bob() 222 | public 223 | giveToken(0x0B925eD163218f6662a35e0f0371Ac234f9E9371, address(_WST_ETH), alice, 1 ether) 224 | withCaller(alice) 225 | { 226 | Permit memory permit = _signPermit( 227 | _testTemps( 228 | "alice", 229 | address(pufferDepositor), 230 | 1 ether, 231 | block.timestamp, 232 | hex"d4a8ff90a402dc7d4fcbf60f5488291263c743ccff180e139f47d139cedfd5fe" 233 | ) 234 | ); 235 | uint256 received = pufferDepositor.depositWstETH(permit, bob); 236 | 237 | assertEq(received, pufferVault.balanceOf(bob), "bob got the tokens"); 238 | assertEq(0, pufferVault.balanceOf(alice), "alice got 0"); 239 | } 240 | 241 | // wstETH approve deposit 242 | function test_wstETH_approve_deposit_to_self() 243 | public 244 | giveToken(0x0B925eD163218f6662a35e0f0371Ac234f9E9371, address(_WST_ETH), alice, 1 ether) 245 | withCaller(alice) 246 | { 247 | // Create an unsigned permit to call function 248 | Permit memory unsignedPermit = Permit(0, 1 ether, 0, 0, 0); 249 | IERC20(address(_WST_ETH)).approve(address(pufferDepositor), 1 ether); 250 | uint256 received = pufferDepositor.depositWstETH(unsignedPermit, alice); 251 | 252 | assertEq(received, pufferVault.balanceOf(alice), "alice got the tokens"); 253 | } 254 | 255 | // wstETH approve deposit to bob 256 | function test_wstETH_approve_deposit_to_bob() 257 | public 258 | giveToken(0x0B925eD163218f6662a35e0f0371Ac234f9E9371, address(_WST_ETH), alice, 1 ether) 259 | withCaller(alice) 260 | { 261 | // Create an unsigned permit to call function 262 | Permit memory unsignedPermit = Permit(0, 1 ether, 0, 0, 0); 263 | IERC20(address(_WST_ETH)).approve(address(pufferDepositor), 1 ether); 264 | uint256 received = pufferDepositor.depositWstETH(unsignedPermit, bob); 265 | 266 | assertEq(received, pufferVault.balanceOf(bob), "bob got the tokens"); 267 | assertEq(0, pufferVault.balanceOf(alice), "alice got 0"); 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/l2/xPufETH.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 { UUPSUpgradeable } from "@openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; 6 | import { AccessManagedUpgradeable } from 7 | "@openzeppelin-contracts-upgradeable/access/manager/AccessManagedUpgradeable.sol"; 8 | import { ERC20PermitUpgradeable } from 9 | "@openzeppelin-contracts-upgradeable/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; 10 | import { xPufETHStorage } from "./xPufETHStorage.sol"; 11 | 12 | /** 13 | * @title xPufETH 14 | * @author Puffer Finance 15 | * @dev It is an XERC20 implementation of pufETH token. This token is to be deployed to L2 chains. 16 | * @custom:security-contact security@puffer.fi 17 | */ 18 | contract xPufETH is xPufETHStorage, IXERC20, AccessManagedUpgradeable, ERC20PermitUpgradeable, UUPSUpgradeable { 19 | /** 20 | * @notice The duration it takes for the limits to fully replenish 21 | */ 22 | uint256 private constant _DURATION = 1 days; 23 | 24 | constructor() { 25 | _disableInitializers(); 26 | } 27 | 28 | function initialize(address accessManager) public initializer { 29 | __AccessManaged_init(accessManager); 30 | __ERC20_init("xPufETH", "xPufETH"); 31 | __ERC20Permit_init("xPufETH"); 32 | } 33 | 34 | /** 35 | * @notice Mints tokens for a user 36 | * @dev Can only be called by a bridge 37 | * @param user The address of the user who needs tokens minted 38 | * @param amount The amount of tokens being minted 39 | * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol 40 | */ 41 | function mint(address user, uint256 amount) external restricted { 42 | _mintWithCaller(msg.sender, user, amount); 43 | } 44 | 45 | /** 46 | * @notice Burns tokens for a user 47 | * @dev Can only be called by a bridge 48 | * @param user The address of the user who needs tokens burned 49 | * @param amount The amount of tokens being burned 50 | * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol 51 | */ 52 | function burn(address user, uint256 amount) external restricted { 53 | if (msg.sender != user) { 54 | _spendAllowance(user, msg.sender, amount); 55 | } 56 | 57 | _burnWithCaller(msg.sender, user, amount); 58 | } 59 | 60 | /** 61 | * @notice Sets the lockbox address 62 | * 63 | * @dev Restricted to the DAO 64 | * @param lockboxAddress The address of the lockbox 65 | */ 66 | function setLockbox(address lockboxAddress) external restricted { 67 | xPufETH storage $ = _getXPufETHStorage(); 68 | $.lockbox = lockboxAddress; 69 | 70 | emit LockboxSet(lockboxAddress); 71 | } 72 | 73 | /** 74 | * @notice Updates the limits of any bridge 75 | * 76 | * @dev Restricted to the DAO 77 | * @param mintingLimit The updated minting limit we are setting to the bridge 78 | * @param burningLimit The updated burning limit we are setting to the bridge 79 | * @param bridge The address of the bridge we are setting the limits too 80 | */ 81 | function setLimits(address bridge, uint256 mintingLimit, uint256 burningLimit) external restricted { 82 | if (mintingLimit > (type(uint256).max / 2) || burningLimit > (type(uint256).max / 2)) { 83 | revert IXERC20_LimitsTooHigh(); 84 | } 85 | 86 | _changeMinterLimit(bridge, mintingLimit); 87 | _changeBurnerLimit(bridge, burningLimit); 88 | emit BridgeLimitsSet(mintingLimit, burningLimit, bridge); 89 | } 90 | 91 | /** 92 | * @notice Returns the max limit of a bridge 93 | * 94 | * @param bridge the bridge we are viewing the limits of 95 | * @return limit The limit the bridge has 96 | */ 97 | function mintingMaxLimitOf(address bridge) public view returns (uint256 limit) { 98 | xPufETH storage $ = _getXPufETHStorage(); 99 | limit = $.bridges[bridge].minterParams.maxLimit; 100 | } 101 | 102 | /** 103 | * @notice Returns the max 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 burningMaxLimitOf(address bridge) public view returns (uint256 limit) { 109 | xPufETH storage $ = _getXPufETHStorage(); 110 | limit = $.bridges[bridge].burnerParams.maxLimit; 111 | } 112 | 113 | /** 114 | * @notice Returns the current limit of a bridge 115 | * 116 | * @param bridge the bridge we are viewing the limits of 117 | * @return limit The limit the bridge has 118 | */ 119 | function mintingCurrentLimitOf(address bridge) public view returns (uint256 limit) { 120 | xPufETH storage $ = _getXPufETHStorage(); 121 | limit = _getCurrentLimit( 122 | $.bridges[bridge].minterParams.currentLimit, 123 | $.bridges[bridge].minterParams.maxLimit, 124 | $.bridges[bridge].minterParams.timestamp, 125 | $.bridges[bridge].minterParams.ratePerSecond 126 | ); 127 | } 128 | 129 | /** 130 | * @notice Returns the current limit of a bridge 131 | * 132 | * @param bridge the bridge we are viewing the limits of 133 | * @return limit The limit the bridge has 134 | */ 135 | function burningCurrentLimitOf(address bridge) public view returns (uint256 limit) { 136 | xPufETH storage $ = _getXPufETHStorage(); 137 | limit = _getCurrentLimit( 138 | $.bridges[bridge].burnerParams.currentLimit, 139 | $.bridges[bridge].burnerParams.maxLimit, 140 | $.bridges[bridge].burnerParams.timestamp, 141 | $.bridges[bridge].burnerParams.ratePerSecond 142 | ); 143 | } 144 | 145 | /** 146 | * @notice Uses the limit of any bridge 147 | * @param bridge The address of the bridge who is being changed 148 | * @param change The change in the limit 149 | */ 150 | function _useMinterLimits(address bridge, uint256 change) internal { 151 | xPufETH storage $ = _getXPufETHStorage(); 152 | uint256 currentLimit = mintingCurrentLimitOf(bridge); 153 | $.bridges[bridge].minterParams.timestamp = block.timestamp; 154 | $.bridges[bridge].minterParams.currentLimit = currentLimit - change; 155 | } 156 | 157 | /** 158 | * @notice Uses the limit of any bridge 159 | * @param bridge The address of the bridge who is being changed 160 | * @param change The change in the limit 161 | */ 162 | function _useBurnerLimits(address bridge, uint256 change) internal { 163 | xPufETH storage $ = _getXPufETHStorage(); 164 | uint256 currentLimit = burningCurrentLimitOf(bridge); 165 | $.bridges[bridge].burnerParams.timestamp = block.timestamp; 166 | $.bridges[bridge].burnerParams.currentLimit = currentLimit - change; 167 | } 168 | 169 | /** 170 | * @notice Updates the limit of any bridge 171 | * @dev Can only be called by the owner 172 | * @param bridge The address of the bridge we are setting the limit too 173 | * @param limit The updated limit we are setting to the bridge 174 | */ 175 | function _changeMinterLimit(address bridge, uint256 limit) internal { 176 | xPufETH storage $ = _getXPufETHStorage(); 177 | uint256 oldLimit = $.bridges[bridge].minterParams.maxLimit; 178 | uint256 currentLimit = mintingCurrentLimitOf(bridge); 179 | $.bridges[bridge].minterParams.maxLimit = limit; 180 | 181 | $.bridges[bridge].minterParams.currentLimit = _calculateNewCurrentLimit(limit, oldLimit, currentLimit); 182 | 183 | $.bridges[bridge].minterParams.ratePerSecond = limit / _DURATION; 184 | $.bridges[bridge].minterParams.timestamp = block.timestamp; 185 | } 186 | 187 | /** 188 | * @notice Updates the limit of any bridge 189 | * @dev Can only be called by the owner 190 | * @param bridge The address of the bridge we are setting the limit too 191 | * @param limit The updated limit we are setting to the bridge 192 | */ 193 | function _changeBurnerLimit(address bridge, uint256 limit) internal { 194 | xPufETH storage $ = _getXPufETHStorage(); 195 | uint256 oldLimit = $.bridges[bridge].burnerParams.maxLimit; 196 | uint256 currentLimit = burningCurrentLimitOf(bridge); 197 | $.bridges[bridge].burnerParams.maxLimit = limit; 198 | 199 | $.bridges[bridge].burnerParams.currentLimit = _calculateNewCurrentLimit(limit, oldLimit, currentLimit); 200 | 201 | $.bridges[bridge].burnerParams.ratePerSecond = limit / _DURATION; 202 | $.bridges[bridge].burnerParams.timestamp = block.timestamp; 203 | } 204 | 205 | /** 206 | * @notice Updates the current limit 207 | * 208 | * @param limit The new limit 209 | * @param oldLimit The old limit 210 | * @param currentLimit The current limit 211 | * @return newCurrentLimit The new current limit 212 | */ 213 | function _calculateNewCurrentLimit(uint256 limit, uint256 oldLimit, uint256 currentLimit) 214 | internal 215 | pure 216 | returns (uint256 newCurrentLimit) 217 | { 218 | uint256 difference; 219 | 220 | if (oldLimit > limit) { 221 | difference = oldLimit - limit; 222 | newCurrentLimit = currentLimit > difference ? currentLimit - difference : 0; 223 | } else { 224 | difference = limit - oldLimit; 225 | newCurrentLimit = currentLimit + difference; 226 | } 227 | } 228 | 229 | /** 230 | * @notice Gets the current limit 231 | * 232 | * @param currentLimit The current limit 233 | * @param maxLimit The max limit 234 | * @param timestamp The timestamp of the last update 235 | * @param ratePerSecond The rate per second 236 | * @return limit The current limit 237 | */ 238 | function _getCurrentLimit(uint256 currentLimit, uint256 maxLimit, uint256 timestamp, uint256 ratePerSecond) 239 | internal 240 | view 241 | returns (uint256 limit) 242 | { 243 | limit = currentLimit; 244 | if (limit == maxLimit) { 245 | return limit; 246 | } else if (timestamp + _DURATION <= block.timestamp) { 247 | limit = maxLimit; 248 | } else if (timestamp + _DURATION > block.timestamp) { 249 | uint256 _timePassed = block.timestamp - timestamp; 250 | uint256 _calculatedLimit = limit + (_timePassed * ratePerSecond); 251 | limit = _calculatedLimit > maxLimit ? maxLimit : _calculatedLimit; 252 | } 253 | } 254 | 255 | /** 256 | * @notice Internal function for burning tokens 257 | * 258 | * @param caller The caller address 259 | * @param user The user address 260 | * @param amount The amount to burn 261 | */ 262 | function _burnWithCaller(address caller, address user, uint256 amount) internal { 263 | xPufETH storage $ = _getXPufETHStorage(); 264 | if (caller != $.lockbox) { 265 | uint256 currentLimit = burningCurrentLimitOf(caller); 266 | if (currentLimit < amount) revert IXERC20_NotHighEnoughLimits(); 267 | _useBurnerLimits(caller, amount); 268 | } 269 | _burn(user, amount); 270 | } 271 | 272 | /** 273 | * @notice Internal function for minting tokens 274 | * 275 | * @param caller The caller address 276 | * @param user The user address 277 | * @param amount The amount to mint 278 | */ 279 | function _mintWithCaller(address caller, address user, uint256 amount) internal { 280 | xPufETH storage $ = _getXPufETHStorage(); 281 | if (caller != $.lockbox) { 282 | uint256 currentLimit = mintingCurrentLimitOf(caller); 283 | if (currentLimit < amount) revert IXERC20_NotHighEnoughLimits(); 284 | _useMinterLimits(caller, amount); 285 | } 286 | _mint(user, amount); 287 | } 288 | 289 | /** 290 | * @dev Authorizes an upgrade to a new implementation 291 | * Restricted access 292 | * @param newImplementation The address of the new implementation 293 | */ 294 | // slither-disable-next-line dead-code 295 | function _authorizeUpgrade(address newImplementation) internal virtual override restricted { } 296 | } 297 | -------------------------------------------------------------------------------- /src/PufferVault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import { IPufferVault } from "./interface/IPufferVault.sol"; 5 | import { IERC20 } from "openzeppelin/token/ERC20/IERC20.sol"; 6 | import { IStETH } from "./interface/Lido/IStETH.sol"; 7 | import { ILidoWithdrawalQueue } from "./interface/Lido/ILidoWithdrawalQueue.sol"; 8 | import { IEigenLayer } from "./interface/EigenLayer/IEigenLayer.sol"; 9 | import { IStrategy } from "./interface/EigenLayer/IStrategy.sol"; 10 | import { PufferVaultStorage } from "./PufferVaultStorage.sol"; 11 | import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 12 | import { IERC721Receiver } from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; 13 | import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; 14 | import { UUPSUpgradeable } from "@openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; 15 | import { AccessManagedUpgradeable } from 16 | "@openzeppelin-contracts-upgradeable/access/manager/AccessManagedUpgradeable.sol"; 17 | import { ERC4626Upgradeable } from "@openzeppelin-contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; 18 | import { ERC20Upgradeable } from "@openzeppelin-contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; 19 | import { ERC20PermitUpgradeable } from 20 | "@openzeppelin-contracts-upgradeable/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; 21 | 22 | /** 23 | * @title PufferVault 24 | * @author Puffer Finance 25 | * @custom:security-contact security@puffer.fi 26 | */ 27 | contract PufferVault is 28 | IPufferVault, 29 | IERC721Receiver, 30 | PufferVaultStorage, 31 | AccessManagedUpgradeable, 32 | ERC20PermitUpgradeable, 33 | ERC4626Upgradeable, 34 | UUPSUpgradeable 35 | { 36 | using EnumerableSet for EnumerableSet.Bytes32Set; 37 | using EnumerableSet for EnumerableSet.UintSet; 38 | using SafeERC20 for address; 39 | 40 | /** 41 | * @dev EigenLayer stETH strategy 42 | */ 43 | IStrategy internal immutable _EIGEN_STETH_STRATEGY; 44 | /** 45 | * @dev EigenLayer Strategy Manager 46 | */ 47 | IEigenLayer internal immutable _EIGEN_STRATEGY_MANAGER; 48 | /** 49 | * @dev stETH contract 50 | */ 51 | IStETH internal immutable _ST_ETH; 52 | /** 53 | * @dev Lido Withdrawal Queue 54 | */ 55 | ILidoWithdrawalQueue internal immutable _LIDO_WITHDRAWAL_QUEUE; 56 | 57 | constructor( 58 | IStETH stETH, 59 | ILidoWithdrawalQueue lidoWithdrawalQueue, 60 | IStrategy stETHStrategy, 61 | IEigenLayer eigenStrategyManager 62 | ) payable { 63 | _ST_ETH = stETH; 64 | _LIDO_WITHDRAWAL_QUEUE = lidoWithdrawalQueue; 65 | _EIGEN_STETH_STRATEGY = stETHStrategy; 66 | _EIGEN_STRATEGY_MANAGER = eigenStrategyManager; 67 | _disableInitializers(); 68 | } 69 | 70 | function initialize(address accessManager) external initializer { 71 | __AccessManaged_init(accessManager); 72 | __ERC20Permit_init("pufETH"); 73 | __ERC4626_init(_ST_ETH); 74 | __ERC20_init("pufETH", "pufETH"); 75 | } 76 | 77 | // solhint-disable-next-line no-complex-fallback 78 | receive() external payable virtual { 79 | // If we don't use this pattern, somebody can create a Lido withdrawal, claim it to this contract 80 | // Making `$.lidoLockedETH -= msg.value` revert 81 | VaultStorage storage $ = _getPufferVaultStorage(); 82 | if ($.isLidoWithdrawal) { 83 | $.lidoLockedETH -= msg.value; 84 | } 85 | } 86 | 87 | /** 88 | * @inheritdoc ERC4626Upgradeable 89 | * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol 90 | */ 91 | function deposit(uint256 assets, address receiver) public virtual override restricted returns (uint256) { 92 | return super.deposit(assets, receiver); 93 | } 94 | 95 | /** 96 | * @inheritdoc ERC4626Upgradeable 97 | * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol 98 | */ 99 | function mint(uint256 shares, address receiver) public virtual override restricted returns (uint256) { 100 | return super.mint(shares, receiver); 101 | } 102 | 103 | /** 104 | * @notice Claims ETH withdrawals from Lido 105 | * @param requestIds An array of request IDs for the withdrawals 106 | */ 107 | function claimWithdrawalsFromLido(uint256[] calldata requestIds) external virtual { 108 | VaultStorage storage $ = _getPufferVaultStorage(); 109 | 110 | // Tell our receive() that we are doing a Lido claim 111 | $.isLidoWithdrawal = true; 112 | 113 | for (uint256 i = 0; i < requestIds.length; ++i) { 114 | bool isValidWithdrawal = $.lidoWithdrawals.remove(requestIds[i]); 115 | if (!isValidWithdrawal) { 116 | revert InvalidWithdrawal(); 117 | } 118 | 119 | // slither-disable-next-line calls-loop 120 | _LIDO_WITHDRAWAL_QUEUE.claimWithdrawal(requestIds[i]); 121 | } 122 | 123 | // Reset back the value 124 | $.isLidoWithdrawal = false; 125 | emit ClaimedWithdrawals(requestIds); 126 | } 127 | 128 | /** 129 | * @notice Not allowed 130 | */ 131 | function redeem(uint256, address, address) public virtual override returns (uint256) { 132 | revert WithdrawalsAreDisabled(); 133 | } 134 | 135 | /** 136 | * @notice Not allowed 137 | */ 138 | function withdraw(uint256, address, address) public virtual override returns (uint256) { 139 | revert WithdrawalsAreDisabled(); 140 | } 141 | 142 | /** 143 | * @dev See {IERC4626-totalAssets}. 144 | * Eventually, stETH will not be part of this vault anymore, and the Vault(pufETH) will represent shares of total ETH holdings 145 | * Because stETH is a rebasing token, its ratio with ETH is 1:1 146 | * Because of that our ETH holdings backing the system are: 147 | * stETH balance of this vault + stETH balance locked in EigenLayer + stETH balance that is the process of withdrawal from Lido 148 | * + ETH balance of this vault 149 | */ 150 | function totalAssets() public view virtual override returns (uint256) { 151 | return _ST_ETH.balanceOf(address(this)) + getELBackingEthAmount() + getPendingLidoETHAmount() 152 | + address(this).balance; 153 | } 154 | 155 | /** 156 | * @notice Returns the ETH amount that is backing this vault locked in EigenLayer stETH strategy 157 | */ 158 | function getELBackingEthAmount() public view virtual returns (uint256 ethAmount) { 159 | VaultStorage storage $ = _getPufferVaultStorage(); 160 | // When we initiate withdrawal from EigenLayer, the shares are deducted from the `lockedAmount` 161 | // In that case the locked amount goes to 0 and the pendingWithdrawalAmount increases 162 | uint256 lockedAmount = _EIGEN_STETH_STRATEGY.userUnderlyingView(address(this)); 163 | uint256 pendingWithdrawalAmount = 164 | _EIGEN_STETH_STRATEGY.sharesToUnderlyingView($.eigenLayerPendingWithdrawalSharesAmount); 165 | return lockedAmount + pendingWithdrawalAmount; 166 | } 167 | 168 | /** 169 | * @notice Returns the amount of ETH that is pending withdrawal from Lido 170 | * @return The amount of ETH pending withdrawal 171 | */ 172 | function getPendingLidoETHAmount() public view virtual returns (uint256) { 173 | VaultStorage storage $ = _getPufferVaultStorage(); 174 | return $.lidoLockedETH; 175 | } 176 | 177 | /** 178 | * @notice Deposits stETH into `stETH EigenLayer strategy` 179 | * Restricted access 180 | * @param amount the amount of stETH to deposit 181 | */ 182 | function depositToEigenLayer(uint256 amount) external virtual restricted { 183 | SafeERC20.safeIncreaseAllowance(_ST_ETH, address(_EIGEN_STRATEGY_MANAGER), amount); 184 | _EIGEN_STRATEGY_MANAGER.depositIntoStrategy({ strategy: _EIGEN_STETH_STRATEGY, token: _ST_ETH, amount: amount }); 185 | } 186 | 187 | /** 188 | * @notice Initiates stETH withdrawals from EigenLayer 189 | * Restricted access 190 | * @param sharesToWithdraw An amount of EigenLayer shares that we want to queue 191 | */ 192 | function initiateStETHWithdrawalFromEigenLayer(uint256 sharesToWithdraw) external virtual restricted { 193 | VaultStorage storage $ = _getPufferVaultStorage(); 194 | 195 | IStrategy[] memory strategies = new IStrategy[](1); 196 | strategies[0] = IStrategy(_EIGEN_STETH_STRATEGY); 197 | 198 | uint256[] memory shares = new uint256[](1); 199 | shares[0] = sharesToWithdraw; 200 | 201 | // Account for the shares 202 | $.eigenLayerPendingWithdrawalSharesAmount += sharesToWithdraw; 203 | 204 | bytes32 withdrawalRoot = _EIGEN_STRATEGY_MANAGER.queueWithdrawal({ 205 | strategyIndexes: new uint256[](1), // [0] 206 | strategies: strategies, 207 | shares: shares, 208 | withdrawer: address(this), 209 | undelegateIfPossible: true 210 | }); 211 | 212 | $.eigenLayerWithdrawals.add(withdrawalRoot); 213 | } 214 | 215 | /** 216 | * @notice Claims stETH withdrawals from EigenLayer 217 | * Restricted access 218 | * @param queuedWithdrawal The queued withdrawal details 219 | * @param tokens The tokens to be withdrawn 220 | * @param middlewareTimesIndex The index of middleware times 221 | */ 222 | function claimWithdrawalFromEigenLayer( 223 | IEigenLayer.QueuedWithdrawal calldata queuedWithdrawal, 224 | IERC20[] calldata tokens, 225 | uint256 middlewareTimesIndex 226 | ) external virtual { 227 | VaultStorage storage $ = _getPufferVaultStorage(); 228 | 229 | bytes32 withdrawalRoot = _EIGEN_STRATEGY_MANAGER.calculateWithdrawalRoot(queuedWithdrawal); 230 | bool isValidWithdrawal = $.eigenLayerWithdrawals.remove(withdrawalRoot); 231 | if (!isValidWithdrawal) { 232 | revert InvalidWithdrawal(); 233 | } 234 | 235 | $.eigenLayerPendingWithdrawalSharesAmount -= queuedWithdrawal.shares[0]; 236 | 237 | _EIGEN_STRATEGY_MANAGER.completeQueuedWithdrawal({ 238 | queuedWithdrawal: queuedWithdrawal, 239 | tokens: tokens, 240 | middlewareTimesIndex: middlewareTimesIndex, 241 | receiveAsTokens: true 242 | }); 243 | } 244 | 245 | /** 246 | * @notice Initiates ETH withdrawals from Lido 247 | * Restricted access 248 | * @param amounts An array of amounts that we want to queue 249 | */ 250 | function initiateETHWithdrawalsFromLido(uint256[] calldata amounts) 251 | external 252 | virtual 253 | restricted 254 | returns (uint256[] memory requestIds) 255 | { 256 | VaultStorage storage $ = _getPufferVaultStorage(); 257 | 258 | uint256 lockedAmount; 259 | for (uint256 i = 0; i < amounts.length; ++i) { 260 | lockedAmount += amounts[i]; 261 | } 262 | $.lidoLockedETH += lockedAmount; 263 | 264 | SafeERC20.safeIncreaseAllowance(_ST_ETH, address(_LIDO_WITHDRAWAL_QUEUE), lockedAmount); 265 | requestIds = _LIDO_WITHDRAWAL_QUEUE.requestWithdrawals(amounts, address(this)); 266 | 267 | for (uint256 i = 0; i < requestIds.length; ++i) { 268 | $.lidoWithdrawals.add(requestIds[i]); 269 | } 270 | emit RequestedWithdrawals(requestIds); 271 | return requestIds; 272 | } 273 | 274 | /** 275 | * @notice Required by the ERC721 Standard 276 | */ 277 | function onERC721Received(address, address, uint256, bytes calldata) external virtual returns (bytes4) { 278 | return IERC721Receiver.onERC721Received.selector; 279 | } 280 | 281 | /** 282 | * @notice Returns the number of decimals used to get its user representation. 283 | */ 284 | function decimals() public pure override(ERC20Upgradeable, ERC4626Upgradeable) returns (uint8) { 285 | return 18; 286 | } 287 | 288 | /** 289 | * @dev Authorizes an upgrade to a new implementation 290 | * Restricted access 291 | * @param newImplementation The address of the new implementation 292 | */ 293 | // slither-disable-next-line dead-code 294 | function _authorizeUpgrade(address newImplementation) internal virtual override restricted { } 295 | } 296 | --------------------------------------------------------------------------------