diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1db9bde..a2bc93e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,5 +56,6 @@ jobs: with: check_hidden: true check_filenames: true + ignore_words_list: amountIn skip: package-lock.json,*.pdf,./.git diff --git a/foundry.toml b/foundry.toml index 0c3745d..9bc3a17 100644 --- a/foundry.toml +++ b/foundry.toml @@ -17,7 +17,7 @@ optimizer = true optimizer_runs = 200 evm_version = "cancun" # is live on mainnet seed = "0x1337" -solc = "0.8.24" +solc = "0.8.26" # via_ir = true [fmt] diff --git a/script/DeployL2XPufETH.s.sol b/script/DeployL2XPufETH.s.sol index 451c4b6..cc47c13 100644 --- a/script/DeployL2XPufETH.s.sol +++ b/script/DeployL2XPufETH.s.sol @@ -4,8 +4,7 @@ pragma solidity >=0.8.0 <0.9.0; import "forge-std/Script.sol"; import { stdJson } from "forge-std/StdJson.sol"; import { BaseScript } from ".//BaseScript.s.sol"; -import { XERC20PufferVault } from "../src/l2/XERC20PufferVault.sol"; -import { UUPSUpgradeable } from "@openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { xPufETH } from "../src/l2/xPufETH.sol"; import { Initializable } from "openzeppelin/proxy/utils/Initializable.sol"; import { Timelock } from "../src/Timelock.sol"; import { ERC1967Proxy } from "openzeppelin/proxy/ERC1967/ERC1967Proxy.sol"; @@ -33,9 +32,9 @@ contract DeployL2XPufETH is BaseScript { address pauserMultisig = vm.envOr("PAUSER_MULTISIG", makeAddr("pauserMultisig")); address communityMultisig = vm.envOr("COMMUNITY_MULTISIG", makeAddr("communityMultisig")); - address _CONNEXT = 0x8247ed6d0a344eeae4edBC7e44572F1B70ECA82A; // change for mainnet - uint256 _MINTING_LIMIT = 1000 * 1e18; - uint256 _BURNING_LIMIT = 1000 * 1e18; + address _CONNEXT = 0x8247ed6d0a344eeae4edBC7e44572F1B70ECA82A; //@todo change for mainnet + uint256 _MINTING_LIMIT = 1000 * 1e18; //@todo + uint256 _BURNING_LIMIT = 1000 * 1e18; //@todo function run() public broadcast { AccessManager accessManager = new AccessManager(_broadcaster); @@ -54,40 +53,45 @@ contract DeployL2XPufETH is BaseScript { initialDelay: 7 days }); - console.log("AccessManager", address(timelock)); + console.log("Timelock", address(timelock)); - XERC20PufferVault newImplementation = new XERC20PufferVault(); - console.log("XERC20PufferVault", address(newImplementation)); + xPufETH newImplementation = new xPufETH(); + console.log("XERC20PufferVaultImplementation", address(newImplementation)); bytes32 xPufETHSalt = bytes32("xPufETH"); - ERC1967Proxy xPufETH = new ERC1967Proxy{ salt: xPufETHSalt }( - address(newImplementation), abi.encodeCall(XERC20PufferVault.initialize, (address(accessManager))) - ); - console.log("xPufETHProxy", address(xPufETH)); - vm.expectEmit(true, true, true, true); emit Initializable.Initialized(1); + ERC1967Proxy xPufETHProxy = new ERC1967Proxy{ salt: xPufETHSalt }( + address(newImplementation), abi.encodeCall(xPufETH.initialize, (address(accessManager))) + ); + console.log("xPufETHProxy", address(xPufETHProxy)); + + bytes memory data = abi.encodeWithSelector(xPufETH.setLimits.selector, _CONNEXT, _MINTING_LIMIT, _BURNING_LIMIT); - bytes memory data = - abi.encodeWithSelector(XERC20PufferVault.setLimits.selector, _CONNEXT, _MINTING_LIMIT, _BURNING_LIMIT); + accessManager.execute(address(xPufETHProxy), data); - accessManager.execute(address(xPufETH), data); + bytes4[] memory daoSelectors = new bytes4[](2); + daoSelectors[0] = xPufETH.setLockbox.selector; + daoSelectors[1] = xPufETH.setLimits.selector; - bytes4[] memory selectors = new bytes4[](2); - selectors[0] = XERC20PufferVault.setLockbox.selector; - selectors[1] = XERC20PufferVault.setLimits.selector; + bytes4[] memory publicSelectors = new bytes4[](2); + publicSelectors[0] = xPufETH.mint.selector; + publicSelectors[1] = xPufETH.burn.selector; // Setup Access - accessManager.setTargetFunctionRole(address(xPufETH), selectors, ROLE_ID_DAO); + // Public selectors + accessManager.setTargetFunctionRole(address(xPufETHProxy), publicSelectors, accessManager.PUBLIC_ROLE()); + // Dao selectors + accessManager.setTargetFunctionRole(address(xPufETHProxy), daoSelectors, ROLE_ID_DAO); accessManager.grantRole(accessManager.ADMIN_ROLE(), address(timelock), 0); - // replace with dao and ops multisigs for mainnet + //@todo replace with dao and ops multisigs for mainnet accessManager.grantRole(ROLE_ID_DAO, _broadcaster, 0); accessManager.grantRole(ROLE_ID_OPERATIONS_MULTISIG, _broadcaster, 0); - // revoke on mainnet + //@todo revoke on mainnet // accessManager.revokeRole(accessManager.ADMIN_ROLE(), _broadcaster); } } diff --git a/script/Roles.sol b/script/Roles.sol index 25bd81f..132f7ce 100644 --- a/script/Roles.sol +++ b/script/Roles.sol @@ -9,7 +9,7 @@ uint64 constant ROLE_ID_UPGRADER = 1; // Role assigned to Operations Multisig uint64 constant ROLE_ID_OPERATIONS_MULTISIG = 22; uint64 constant ROLE_ID_OPERATIONS_PAYMASTER = 23; -uint64 constant ROLE_ID_OPERATIONS_COORDINATOR= 24; +uint64 constant ROLE_ID_OPERATIONS_COORDINATOR = 24; // Role assigned to the Puffer Protocol uint64 constant ROLE_ID_PUFFER_PROTOCOL = 1234; @@ -24,3 +24,6 @@ uint64 constant ADMIN_ROLE = 0; // Allowlister role for AVSContractsRegistry uint64 constant ROLE_ID_AVS_COORDINATOR_ALLOWLISTER = 5; + +// Lockbox role for ETH Mainnet +uint64 constant ROLE_ID_LOCKBOX = 7; diff --git a/src/XERC20Lockbox.sol b/src/XERC20Lockbox.sol new file mode 100644 index 0000000..73ce66a --- /dev/null +++ b/src/XERC20Lockbox.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.4 <0.9.0; + +import { IXERC20 } from "./interface/IXERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { IXERC20Lockbox } from "./interface/IXERC20Lockbox.sol"; + +contract XERC20Lockbox is IXERC20Lockbox { + using SafeERC20 for IERC20; + using SafeCast for uint256; + + /** + * @notice The XERC20 token of this contract + */ + IXERC20 public immutable XERC20; + + /** + * @notice The ERC20 token of this contract + */ + IERC20 public immutable ERC20; + + /** + * @notice Constructor + * + * @param xerc20 The address of the XERC20 contract + * @param erc20 The address of the ERC20 contract + */ + constructor(address xerc20, address erc20) { + XERC20 = IXERC20(xerc20); + ERC20 = IERC20(erc20); + } + + /** + * @notice Deposit ERC20 tokens into the lockbox + * + * @param amount The amount of tokens to deposit + */ + function deposit(uint256 amount) external { + _deposit(msg.sender, amount); + } + + /** + * @notice Deposit ERC20 tokens into the lockbox, and send the XERC20 to a user + * + * @param to The user to send the XERC20 to + * @param amount The amount of tokens to deposit + */ + function depositTo(address to, uint256 amount) external { + _deposit(to, amount); + } + + /** + * @notice Not a native asset + */ + function depositNativeTo(address) public payable { + revert IXERC20Lockbox_NotNative(); + } + + /** + * @notice Deposit native tokens into the lockbox + */ + function depositNative() public payable { + revert IXERC20Lockbox_NotNative(); + } + + /** + * @notice Withdraw ERC20 tokens from the lockbox + * + * @param amount The amount of tokens to withdraw + */ + function withdraw(uint256 amount) external { + _withdraw(msg.sender, amount); + } + + /** + * @notice Withdraw tokens from the lockbox + * + * @param to The user to withdraw to + * @param amount The amount of tokens to withdraw + */ + function withdrawTo(address to, uint256 amount) external { + _withdraw(to, amount); + } + + /** + * @notice Withdraw tokens from the lockbox + * + * @param to The user to withdraw to + * @param amount The amount of tokens to withdraw + */ + function _withdraw(address to, uint256 amount) internal { + emit Withdraw(to, amount); + + XERC20.burn(msg.sender, amount); + ERC20.safeTransfer(to, amount); + } + + /** + * @notice Deposit tokens into the lockbox + * + * @param to The address to send the XERC20 to + * @param amount The amount of tokens to deposit + */ + function _deposit(address to, uint256 amount) internal { + ERC20.safeTransferFrom(msg.sender, address(this), amount); + XERC20.mint(to, amount); + + emit Deposit(to, amount); + } + + /** + * @notice Fallback function to deposit native tokens + */ + receive() external payable { + depositNative(); + } +} diff --git a/src/l2/interface/IXERC20.sol b/src/interface/IXERC20.sol similarity index 50% rename from src/l2/interface/IXERC20.sol rename to src/interface/IXERC20.sol index 4d4b4e5..e07706a 100644 --- a/src/l2/interface/IXERC20.sol +++ b/src/interface/IXERC20.sol @@ -5,18 +5,18 @@ interface IXERC20 { /** * @notice Emits when a lockbox is set * - * @param _lockbox The address of the lockbox + * @param lockbox The address of the lockbox */ - event LockboxSet(address _lockbox); + event LockboxSet(address lockbox); /** * @notice Emits when a limit is set * - * @param _mintingLimit The updated minting limit we are setting to the bridge - * @param _burningLimit The updated burning limit we are setting to the bridge - * @param _bridge The address of the bridge we are setting the limit too + * @param mintingLimit The updated minting limit we are setting to the bridge + * @param burningLimit The updated burning limit we are setting to the bridge + * @param bridge The address of the bridge we are setting the limit too */ - event BridgeLimitsSet(uint256 _mintingLimit, uint256 _burningLimit, address indexed _bridge); + event BridgeLimitsSet(uint256 mintingLimit, uint256 burningLimit, address indexed bridge); /** * @notice Reverts when a user with too low of a limit tries to call mint/burn @@ -62,64 +62,64 @@ interface IXERC20 { /** * @notice Sets the lockbox address * - * @param _lockbox The address of the lockbox + * @param lockbox The address of the lockbox */ - function setLockbox(address _lockbox) external; + function setLockbox(address lockbox) external; /** * @notice Updates the limits of any bridge * @dev Can only be called by the owner - * @param _mintingLimit The updated minting limit we are setting to the bridge - * @param _burningLimit The updated burning limit we are setting to the bridge - * @param _bridge The address of the bridge we are setting the limits too + * @param mintingLimit The updated minting limit we are setting to the bridge + * @param burningLimit The updated burning limit we are setting to the bridge + * @param bridge The address of the bridge we are setting the limits too */ - function setLimits(address _bridge, uint256 _mintingLimit, uint256 _burningLimit) external; + function setLimits(address bridge, uint256 mintingLimit, uint256 burningLimit) external; /** * @notice Returns the max limit of a minter * - * @param _minter The minter we are viewing the limits of - * @return _limit The limit the minter has + * @param minter The minter we are viewing the limits of + * @return limit The limit the minter has */ - function mintingMaxLimitOf(address _minter) external view returns (uint256 _limit); + function mintingMaxLimitOf(address minter) external view returns (uint256 limit); /** * @notice Returns the max limit of a bridge * - * @param _bridge the bridge we are viewing the limits of - * @return _limit The limit the bridge has + * @param bridge the bridge we are viewing the limits of + * @return limit The limit the bridge has */ - function burningMaxLimitOf(address _bridge) external view returns (uint256 _limit); + function burningMaxLimitOf(address bridge) external view returns (uint256 limit); /** * @notice Returns the current limit of a minter * - * @param _minter The minter we are viewing the limits of - * @return _limit The limit the minter has + * @param minter The minter we are viewing the limits of + * @return limit The limit the minter has */ - function mintingCurrentLimitOf(address _minter) external view returns (uint256 _limit); + function mintingCurrentLimitOf(address minter) external view returns (uint256 limit); /** * @notice Returns the current limit of a bridge * - * @param _bridge the bridge we are viewing the limits of - * @return _limit The limit the bridge has + * @param bridge the bridge we are viewing the limits of + * @return limit The limit the bridge has */ - function burningCurrentLimitOf(address _bridge) external view returns (uint256 _limit); + function burningCurrentLimitOf(address bridge) external view returns (uint256 limit); /** * @notice Mints tokens for a user * @dev Can only be called by a minter - * @param _user The address of the user who needs tokens minted - * @param _amount The amount of tokens being minted + * @param user The address of the user who needs tokens minted + * @param amount The amount of tokens being minted */ - function mint(address _user, uint256 _amount) external; + function mint(address user, uint256 amount) external; /** * @notice Burns tokens for a user * @dev Can only be called by a minter - * @param _user The address of the user who needs tokens burned - * @param _amount The amount of tokens being burned + * @param user The address of the user who needs tokens burned + * @param amount The amount of tokens being burned */ - function burn(address _user, uint256 _amount) external; + function burn(address user, uint256 amount) external; } diff --git a/src/interface/IXERC20Lockbox.sol b/src/interface/IXERC20Lockbox.sol new file mode 100644 index 0000000..be563a0 --- /dev/null +++ b/src/interface/IXERC20Lockbox.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.4 <0.9.0; + +interface IXERC20Lockbox { + /** + * @notice Emitted when tokens are deposited into the lockbox + * + * @param sender The address of the user who deposited + * @param amount The amount of tokens deposited + */ + event Deposit(address sender, uint256 amount); + + /** + * @notice Emitted when tokens are withdrawn from the lockbox + * + * @param sender The address of the user who withdrew + * @param amount The amount of tokens withdrawn + */ + event Withdraw(address sender, uint256 amount); + + /** + * @notice Reverts when a user tries to deposit native tokens on a non-native lockbox + */ + error IXERC20Lockbox_NotNative(); + + /** + * @notice Deposit ERC20 tokens into the lockbox + * + * @param amount The amount of tokens to deposit + */ + function deposit(uint256 amount) external; + + /** + * @notice Deposit ERC20 tokens into the lockbox, and send the XERC20 to a user + * + * @param user The user to send the XERC20 to + * @param amount The amount of tokens to deposit + */ + function depositTo(address user, uint256 amount) external; + + /** + * @notice Deposit the native asset into the lockbox, and send the XERC20 to a user + * + * @param user The user to send the XERC20 to + */ + function depositNativeTo(address user) external payable; + + /** + * @notice Withdraw ERC20 tokens from the lockbox + * + * @param amount The amount of tokens to withdraw + */ + function withdraw(uint256 amount) external; + + /** + * @notice Withdraw ERC20 tokens from the lockbox + * + * @param user The user to withdraw to + * @param amount The amount of tokens to withdraw + */ + function withdrawTo(address user, uint256 amount) external; +} diff --git a/src/l2/XERC20PufferVault.sol b/src/l2/xPufETH.sol similarity index 83% rename from src/l2/XERC20PufferVault.sol rename to src/l2/xPufETH.sol index 2fb92a4..22086cb 100644 --- a/src/l2/XERC20PufferVault.sol +++ b/src/l2/xPufETH.sol @@ -1,21 +1,21 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.4 <0.9.0; -import { IXERC20 } from "./interface/IXERC20.sol"; +import { IXERC20 } from "../interface/IXERC20.sol"; import { UUPSUpgradeable } from "@openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { AccessManagedUpgradeable } from "@openzeppelin-contracts-upgradeable/access/manager/AccessManagedUpgradeable.sol"; import { ERC20PermitUpgradeable } from "@openzeppelin-contracts-upgradeable/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; -import { XERC20PufferVaultStorage } from "./XERC20PufferVaultStorage.sol"; +import { xPufETHStorage } from "./xPufETHStorage.sol"; -contract XERC20PufferVault is - XERC20PufferVaultStorage, - IXERC20, - AccessManagedUpgradeable, - ERC20PermitUpgradeable, - UUPSUpgradeable -{ +/** + * @title xPufETH + * @author Puffer Finance + * @dev It is an XERC20 implementation of pufETH token. This token is to be deployed to L2 chains. + * @custom:security-contact security@puffer.fi + */ +contract xPufETH is xPufETHStorage, IXERC20, AccessManagedUpgradeable, ERC20PermitUpgradeable, UUPSUpgradeable { /** * @notice The duration it takes for the limits to fully replenish */ @@ -36,8 +36,9 @@ contract XERC20PufferVault is * @dev Can only be called by a bridge * @param user The address of the user who needs tokens minted * @param amount The amount of tokens being minted + * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol */ - function mint(address user, uint256 amount) public { + function mint(address user, uint256 amount) external restricted { _mintWithCaller(msg.sender, user, amount); } @@ -46,8 +47,9 @@ contract XERC20PufferVault is * @dev Can only be called by a bridge * @param user The address of the user who needs tokens burned * @param amount The amount of tokens being burned + * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol */ - function burn(address user, uint256 amount) public { + function burn(address user, uint256 amount) external restricted { if (msg.sender != user) { _spendAllowance(user, msg.sender, amount); } @@ -58,10 +60,11 @@ contract XERC20PufferVault is /** * @notice Sets the lockbox address * + * @dev Restricted to the DAO * @param lockboxAddress The address of the lockbox */ - function setLockbox(address lockboxAddress) public restricted { - VaultStorage storage $ = _getPufferVaultStorage(); + function setLockbox(address lockboxAddress) external restricted { + xPufETH storage $ = _getXPufETHStorage(); $.lockbox = lockboxAddress; emit LockboxSet(lockboxAddress); @@ -69,7 +72,8 @@ contract XERC20PufferVault is /** * @notice Updates the limits of any bridge - * @dev Can only be called by the owner + * + * @dev Restricted to the DAO * @param mintingLimit The updated minting limit we are setting to the bridge * @param burningLimit The updated burning limit we are setting to the bridge * @param bridge The address of the bridge we are setting the limits too @@ -91,7 +95,7 @@ contract XERC20PufferVault is * @return limit The limit the bridge has */ function mintingMaxLimitOf(address bridge) public view returns (uint256 limit) { - VaultStorage storage $ = _getPufferVaultStorage(); + xPufETH storage $ = _getXPufETHStorage(); limit = $.bridges[bridge].minterParams.maxLimit; } @@ -102,7 +106,7 @@ contract XERC20PufferVault is * @return limit The limit the bridge has */ function burningMaxLimitOf(address bridge) public view returns (uint256 limit) { - VaultStorage storage $ = _getPufferVaultStorage(); + xPufETH storage $ = _getXPufETHStorage(); limit = $.bridges[bridge].burnerParams.maxLimit; } @@ -113,7 +117,7 @@ contract XERC20PufferVault is * @return limit The limit the bridge has */ function mintingCurrentLimitOf(address bridge) public view returns (uint256 limit) { - VaultStorage storage $ = _getPufferVaultStorage(); + xPufETH storage $ = _getXPufETHStorage(); limit = _getCurrentLimit( $.bridges[bridge].minterParams.currentLimit, $.bridges[bridge].minterParams.maxLimit, @@ -129,7 +133,7 @@ contract XERC20PufferVault is * @return limit The limit the bridge has */ function burningCurrentLimitOf(address bridge) public view returns (uint256 limit) { - VaultStorage storage $ = _getPufferVaultStorage(); + xPufETH storage $ = _getXPufETHStorage(); limit = _getCurrentLimit( $.bridges[bridge].burnerParams.currentLimit, $.bridges[bridge].burnerParams.maxLimit, @@ -144,7 +148,7 @@ contract XERC20PufferVault is * @param change The change in the limit */ function _useMinterLimits(address bridge, uint256 change) internal { - VaultStorage storage $ = _getPufferVaultStorage(); + xPufETH storage $ = _getXPufETHStorage(); uint256 currentLimit = mintingCurrentLimitOf(bridge); $.bridges[bridge].minterParams.timestamp = block.timestamp; $.bridges[bridge].minterParams.currentLimit = currentLimit - change; @@ -156,7 +160,7 @@ contract XERC20PufferVault is * @param change The change in the limit */ function _useBurnerLimits(address bridge, uint256 change) internal { - VaultStorage storage $ = _getPufferVaultStorage(); + xPufETH storage $ = _getXPufETHStorage(); uint256 currentLimit = burningCurrentLimitOf(bridge); $.bridges[bridge].burnerParams.timestamp = block.timestamp; $.bridges[bridge].burnerParams.currentLimit = currentLimit - change; @@ -169,7 +173,7 @@ contract XERC20PufferVault is * @param limit The updated limit we are setting to the bridge */ function _changeMinterLimit(address bridge, uint256 limit) internal { - VaultStorage storage $ = _getPufferVaultStorage(); + xPufETH storage $ = _getXPufETHStorage(); uint256 oldLimit = $.bridges[bridge].minterParams.maxLimit; uint256 currentLimit = mintingCurrentLimitOf(bridge); $.bridges[bridge].minterParams.maxLimit = limit; @@ -187,7 +191,7 @@ contract XERC20PufferVault is * @param limit The updated limit we are setting to the bridge */ function _changeBurnerLimit(address bridge, uint256 limit) internal { - VaultStorage storage $ = _getPufferVaultStorage(); + xPufETH storage $ = _getXPufETHStorage(); uint256 oldLimit = $.bridges[bridge].burnerParams.maxLimit; uint256 currentLimit = burningCurrentLimitOf(bridge); $.bridges[bridge].burnerParams.maxLimit = limit; @@ -204,21 +208,21 @@ contract XERC20PufferVault is * @param limit The new limit * @param oldLimit The old limit * @param currentLimit The current limit - * @return _newCurrentLimit The new current limit + * @return newCurrentLimit The new current limit */ function _calculateNewCurrentLimit(uint256 limit, uint256 oldLimit, uint256 currentLimit) internal pure - returns (uint256 _newCurrentLimit) + returns (uint256 newCurrentLimit) { - uint256 _difference; + uint256 difference; if (oldLimit > limit) { - _difference = oldLimit - limit; - _newCurrentLimit = currentLimit > _difference ? currentLimit - _difference : 0; + difference = oldLimit - limit; + newCurrentLimit = currentLimit > difference ? currentLimit - difference : 0; } else { - _difference = limit - oldLimit; - _newCurrentLimit = currentLimit + _difference; + difference = limit - oldLimit; + newCurrentLimit = currentLimit + difference; } } @@ -256,7 +260,7 @@ contract XERC20PufferVault is * @param amount The amount to burn */ function _burnWithCaller(address caller, address user, uint256 amount) internal { - VaultStorage storage $ = _getPufferVaultStorage(); + xPufETH storage $ = _getXPufETHStorage(); if (caller != $.lockbox) { uint256 currentLimit = burningCurrentLimitOf(caller); if (currentLimit < amount) revert IXERC20_NotHighEnoughLimits(); @@ -273,7 +277,7 @@ contract XERC20PufferVault is * @param amount The amount to mint */ function _mintWithCaller(address caller, address user, uint256 amount) internal { - VaultStorage storage $ = _getPufferVaultStorage(); + xPufETH storage $ = _getXPufETHStorage(); if (caller != $.lockbox) { uint256 currentLimit = mintingCurrentLimitOf(caller); if (currentLimit < amount) revert IXERC20_NotHighEnoughLimits(); diff --git a/src/l2/XERC20PufferVaultStorage.sol b/src/l2/xPufETHStorage.sol similarity index 55% rename from src/l2/XERC20PufferVaultStorage.sol rename to src/l2/xPufETHStorage.sol index 3571af5..8ba9d43 100644 --- a/src/l2/XERC20PufferVaultStorage.sol +++ b/src/l2/xPufETHStorage.sol @@ -1,22 +1,23 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0 <0.9.0; -import { IXERC20 } from "./interface/IXERC20.sol"; +import { IXERC20 } from "../interface/IXERC20.sol"; /** - * @title XERC20PufferVaultStorage + * @title xPufETHStorage * @author Puffer Finance + * @custom:security-contact security@puffer.fi */ -abstract contract XERC20PufferVaultStorage { +abstract contract xPufETHStorage { /** - * @custom:storage-location erc7201:puffervault.storage + * @custom:storage-location erc7201:xPufETH.storage * @dev +-----------------------------------------------------------+ * | | * | DO NOT CHANGE, REORDER, REMOVE EXISTING STORAGE VARIABLES | * | | * +-----------------------------------------------------------+ */ - struct VaultStorage { + struct xPufETH { /** * @notice The address of the lockbox contract */ @@ -24,17 +25,16 @@ abstract contract XERC20PufferVaultStorage { /** * @notice Maps bridge address to bridge configurations */ - mapping(address => IXERC20.Bridge) bridges; + mapping(address bridge => IXERC20.Bridge config) bridges; } - // keccak256(abi.encode(uint256(keccak256("puffervault.storage")) - 1)) & ~bytes32(uint256(0xff)) - bytes32 private constant _VAULT_STORAGE_LOCATION = - 0x611ea165ca9257827fc43d2954fdae7d825e82c825d9037db9337fa1bfa93100; + // keccak256(abi.encode(uint256(keccak256("xPufETH.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant _STORAGE_LOCATION = 0xfee41a6d2b86b757dd00cd2166d8727686a349977cbc2b6b6a2ca1c3e7215000; - function _getPufferVaultStorage() internal pure returns (VaultStorage storage $) { + function _getXPufETHStorage() internal pure returns (xPufETH storage $) { // solhint-disable-next-line no-inline-assembly assembly { - $.slot := _VAULT_STORAGE_LOCATION + $.slot := _STORAGE_LOCATION } } } diff --git a/test/unit/xPufETHTest.t.sol b/test/unit/xPufETHTest.t.sol new file mode 100644 index 0000000..c542912 --- /dev/null +++ b/test/unit/xPufETHTest.t.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import { Test } from "forge-std/Test.sol"; +import { PufferDepositor } from "src/PufferDepositor.sol"; +import { Timelock } from "src/Timelock.sol"; +import { PufferVault } from "src/PufferVault.sol"; +import { xPufETH } from "src/l2/xPufETH.sol"; +import { XERC20Lockbox } from "src/XERC20Lockbox.sol"; +import { stETHMock } from "test/mocks/stETHMock.sol"; +import { AccessManager } from "openzeppelin/access/manager/AccessManager.sol"; +import { PufferDeployment } from "src/structs/PufferDeployment.sol"; +import { DeployPufETH } from "script/DeployPufETH.s.sol"; +import { ROLE_ID_DAO, ROLE_ID_LOCKBOX } from "script/Roles.sol"; +import { ERC1967Proxy } from "openzeppelin/proxy/ERC1967/ERC1967Proxy.sol"; +import { Initializable } from "openzeppelin/proxy/utils/Initializable.sol"; + +contract xPufETHTest is Test { + PufferDepositor public pufferDepositor; + PufferVault public pufferVault; + AccessManager public accessManager; + stETHMock public stETH; + Timelock public timelock; + xPufETH public xPufETHProxy; + XERC20Lockbox public xERC20Lockbox; + + function setUp() public { + PufferDeployment memory deployment = new DeployPufETH().run(); + pufferDepositor = PufferDepositor(payable(deployment.pufferDepositor)); + pufferVault = PufferVault(payable(deployment.pufferVault)); + accessManager = AccessManager(payable(deployment.accessManager)); + stETH = stETHMock(payable(deployment.stETH)); + timelock = Timelock(payable(deployment.timelock)); + + // Deploy implementation + xPufETH newImplementation = new xPufETH(); + + // Deploy proxy + vm.expectEmit(true, true, true, true); + emit Initializable.Initialized(1); + xPufETHProxy = xPufETH( + address( + new ERC1967Proxy{ salt: bytes32("xPufETH") }( + address(newImplementation), abi.encodeCall(xPufETH.initialize, (address(accessManager))) + ) + ) + ); + + // Deploy the lockbox + xERC20Lockbox = new XERC20Lockbox(address(xPufETHProxy), address(deployment.pufferVault)); + + // Setup AccessManager stuff + // Setup access + bytes4[] memory daoSelectors = new bytes4[](2); + daoSelectors[0] = xPufETH.setLockbox.selector; + daoSelectors[1] = xPufETH.setLimits.selector; + + bytes4[] memory lockBoxSelectors = new bytes4[](2); + lockBoxSelectors[0] = xPufETH.mint.selector; + lockBoxSelectors[1] = xPufETH.burn.selector; + + // Public selectors + vm.startPrank(address(timelock)); + accessManager.setTargetFunctionRole(address(xPufETHProxy), lockBoxSelectors, accessManager.PUBLIC_ROLE()); + accessManager.setTargetFunctionRole(address(xPufETHProxy), daoSelectors, ROLE_ID_DAO); + accessManager.grantRole(ROLE_ID_LOCKBOX, address(xERC20Lockbox), 0); + accessManager.grantRole(ROLE_ID_DAO, address(this), 0); // this contract is the dao for simplicity + vm.stopPrank(); + + // Set the Lockbox) + xPufETHProxy.setLockbox(address(xERC20Lockbox)); + + // Mint mock steth to this contract + stETH.mint(address(this), type(uint128).max); + } + + // We deposit pufETH to get xpufETH to this contract using .depositTo + function test_mint_xpufETH(uint8 amount) public { + stETH.approve(address(pufferVault), type(uint256).max); + pufferVault.deposit(uint256(amount), address(this)); + + pufferVault.approve(address(xERC20Lockbox), type(uint256).max); + xERC20Lockbox.depositTo(address(this), uint256(amount)); + assertEq(xPufETHProxy.balanceOf(address(this)), uint256(amount), "got xpufETH"); + assertEq(pufferVault.balanceOf(address(xERC20Lockbox)), uint256(amount), "pufETH is in the lockbox"); + } + + // We deposit pufETH to get xpufETH to this contract using .deposit + function test_deposit_pufETH_for_xpufETH(uint8 amount) public { + stETH.approve(address(pufferVault), type(uint256).max); + pufferVault.deposit(uint256(amount), address(this)); + + pufferVault.approve(address(xERC20Lockbox), type(uint256).max); + xERC20Lockbox.deposit(uint256(amount)); + assertEq(xPufETHProxy.balanceOf(address(this)), uint256(amount), "got xpufETH"); + assertEq(pufferVault.balanceOf(address(xERC20Lockbox)), uint256(amount), "pufETH is in the lockbox"); + } + + // We withdraw pufETH to Bob + function test_mint_and_burn_xpufETH(uint8 amount) public { + address bob = makeAddr("bob"); + test_mint_xpufETH(amount); + + xPufETHProxy.approve(address(xERC20Lockbox), type(uint256).max); + xERC20Lockbox.withdrawTo(bob, uint256(amount)); + assertEq(pufferVault.balanceOf(bob), amount, "bob got pufETH"); + } + + // We withdraw to self + function test_mint_and_withdraw_xpufETH(uint8 amount) public { + test_mint_xpufETH(amount); + + xPufETHProxy.approve(address(xERC20Lockbox), type(uint256).max); + + uint256 pufEThBalanceBefore = pufferVault.balanceOf(address(this)); + + xERC20Lockbox.withdraw(uint256(amount)); + assertEq(pufferVault.balanceOf(address(this)), pufEThBalanceBefore + amount, "we got pufETH"); + } + + function test_nativeReverts() public { + vm.expectRevert(); + xERC20Lockbox.depositNativeTo(address(0)); + + vm.expectRevert(); + xERC20Lockbox.depositNative(); + } +}