diff --git a/script/DeployL2XPufETH.s.sol b/script/DeployL2XPufETH.s.sol new file mode 100644 index 0000000..6a3845c --- /dev/null +++ b/script/DeployL2XPufETH.s.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +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 { Initializable } from "openzeppelin/proxy/utils/Initializable.sol"; +import { NoImplementation } from "../src/NoImplementation.sol"; +import { Timelock } from "../src/Timelock.sol"; +import { ERC1967Proxy } from "openzeppelin/proxy/ERC1967/ERC1967Proxy.sol"; +import { AccessManager } from "openzeppelin/access/manager/AccessManager.sol"; + +/** + * @title DeployL2XPufETH + * @author Puffer Finance + * @notice Deploy XPufETH + * @dev + * + * + * NOTE: + * + * If you ran the deployment script, but did not `--broadcast` the transaction, it will still update your local chainId-deployment.json file. + * Other scripts will fail because addresses will be updated in deployments file, but the deployment never happened. + * + * 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")` + * + * PK=${deployer_pk} forge script script/DeployL2XPufETH.s.sol:DeployL2XPufETH -vvvv --rpc-url=... --broadcast + */ +contract DeployL2XPufETH is BaseScript { + address operationsMultisig = vm.envOr("OPERATIONS_MULTISIG", makeAddr("operationsMultisig")); + address pauserMultisig = vm.envOr("PAUSER_MULTISIG", makeAddr("pauserMultisig")); + address communityMultisig = vm.envOr("COMMUNITY_MULTISIG", makeAddr("communityMultisig")); + + function run() public broadcast { + AccessManager accessManager = new AccessManager(_broadcaster); + + Timelock timelock = new Timelock({ + accessManager: address(accessManager), + communityMultisig: communityMultisig, + operationsMultisig: operationsMultisig, + pauser: pauserMultisig, + initialDelay: 7 days + 1 + }); + + address noImpl = address(new NoImplementation()); + + bytes32 xPufETHSalt = bytes32("xPufETH"); + + ERC1967Proxy xPufETH = new ERC1967Proxy{ salt: xPufETHSalt }(noImpl, ""); + vm.label(address(xPufETH), "xPufETH"); + + XERC20PufferVault newImplementation = new XERC20PufferVault(); + + vm.expectEmit(true, true, true, true); + emit Initializable.Initialized(1); + NoImplementation(payable(address(xPufETH))).upgradeToAndCall( + address(newImplementation), abi.encodeCall(XERC20PufferVault.initialize, (address(accessManager))) + ); + } +} diff --git a/src/l2/XERC20PufferVault.sol b/src/l2/XERC20PufferVault.sol new file mode 100644 index 0000000..a22b1e1 --- /dev/null +++ b/src/l2/XERC20PufferVault.sol @@ -0,0 +1,285 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.4 <0.9.0; + +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"; + +contract XERC20PufferVault is IXERC20, AccessManagedUpgradeable, ERC20PermitUpgradeable, UUPSUpgradeable { + /** + * @notice The duration it takes for the limits to fully replenish + */ + uint256 private constant _DURATION = 1 days; + + /** + * @notice The address of the lockbox contract + */ + address public lockbox; + + /** + * @notice Maps bridge address to bridge configurations + */ + mapping(address => Bridge) public bridges; + + constructor() { + _disableInitializers(); + } + + function initialize(address accessManager) public initializer { + __AccessManaged_init(accessManager); + __ERC20_init("xPufETH", "xPufETH"); + __ERC20Permit_init("xPufETH"); + } + + /** + * @notice Mints tokens for a user + * @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 + */ + function mint(address _user, uint256 _amount) public { + _mintWithCaller(msg.sender, _user, _amount); + } + + /** + * @notice Burns tokens for a user + * @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 + */ + function burn(address _user, uint256 _amount) public { + if (msg.sender != _user) { + _spendAllowance(_user, msg.sender, _amount); + } + + _burnWithCaller(msg.sender, _user, _amount); + } + + /** + * @notice Sets the lockbox address + * + * @param _lockbox The address of the lockbox + */ + function setLockbox(address _lockbox) public restricted { + // if (msg.sender != FACTORY) revert IXERC20_NotFactory(); + lockbox = _lockbox; + + emit LockboxSet(_lockbox); + } + + /** + * @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 + */ + function setLimits(address _bridge, uint256 _mintingLimit, uint256 _burningLimit) external restricted { + if (_mintingLimit > (type(uint256).max / 2) || _burningLimit > (type(uint256).max / 2)) { + revert IXERC20_LimitsTooHigh(); + } + + _changeMinterLimit(_bridge, _mintingLimit); + _changeBurnerLimit(_bridge, _burningLimit); + emit BridgeLimitsSet(_mintingLimit, _burningLimit, _bridge); + } + + /** + * @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 + */ + function mintingMaxLimitOf(address _bridge) public view returns (uint256 _limit) { + _limit = bridges[_bridge].minterParams.maxLimit; + } + + /** + * @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 + */ + function burningMaxLimitOf(address _bridge) public view returns (uint256 _limit) { + _limit = bridges[_bridge].burnerParams.maxLimit; + } + + /** + * @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 + */ + function mintingCurrentLimitOf(address _bridge) public view returns (uint256 _limit) { + _limit = _getCurrentLimit( + bridges[_bridge].minterParams.currentLimit, + bridges[_bridge].minterParams.maxLimit, + bridges[_bridge].minterParams.timestamp, + bridges[_bridge].minterParams.ratePerSecond + ); + } + + /** + * @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 + */ + function burningCurrentLimitOf(address _bridge) public view returns (uint256 _limit) { + _limit = _getCurrentLimit( + bridges[_bridge].burnerParams.currentLimit, + bridges[_bridge].burnerParams.maxLimit, + bridges[_bridge].burnerParams.timestamp, + bridges[_bridge].burnerParams.ratePerSecond + ); + } + + /** + * @notice Uses the limit of any bridge + * @param _bridge The address of the bridge who is being changed + * @param _change The change in the limit + */ + function _useMinterLimits(address _bridge, uint256 _change) internal { + uint256 _currentLimit = mintingCurrentLimitOf(_bridge); + bridges[_bridge].minterParams.timestamp = block.timestamp; + bridges[_bridge].minterParams.currentLimit = _currentLimit - _change; + } + + /** + * @notice Uses the limit of any bridge + * @param _bridge The address of the bridge who is being changed + * @param _change The change in the limit + */ + function _useBurnerLimits(address _bridge, uint256 _change) internal { + uint256 _currentLimit = burningCurrentLimitOf(_bridge); + bridges[_bridge].burnerParams.timestamp = block.timestamp; + bridges[_bridge].burnerParams.currentLimit = _currentLimit - _change; + } + + /** + * @notice Updates the limit of any bridge + * @dev Can only be called by the owner + * @param _bridge The address of the bridge we are setting the limit too + * @param _limit The updated limit we are setting to the bridge + */ + function _changeMinterLimit(address _bridge, uint256 _limit) internal { + uint256 _oldLimit = bridges[_bridge].minterParams.maxLimit; + uint256 _currentLimit = mintingCurrentLimitOf(_bridge); + bridges[_bridge].minterParams.maxLimit = _limit; + + bridges[_bridge].minterParams.currentLimit = _calculateNewCurrentLimit(_limit, _oldLimit, _currentLimit); + + bridges[_bridge].minterParams.ratePerSecond = _limit / _DURATION; + bridges[_bridge].minterParams.timestamp = block.timestamp; + } + + /** + * @notice Updates the limit of any bridge + * @dev Can only be called by the owner + * @param _bridge The address of the bridge we are setting the limit too + * @param _limit The updated limit we are setting to the bridge + */ + function _changeBurnerLimit(address _bridge, uint256 _limit) internal { + uint256 _oldLimit = bridges[_bridge].burnerParams.maxLimit; + uint256 _currentLimit = burningCurrentLimitOf(_bridge); + bridges[_bridge].burnerParams.maxLimit = _limit; + + bridges[_bridge].burnerParams.currentLimit = _calculateNewCurrentLimit(_limit, _oldLimit, _currentLimit); + + bridges[_bridge].burnerParams.ratePerSecond = _limit / _DURATION; + bridges[_bridge].burnerParams.timestamp = block.timestamp; + } + + /** + * @notice Updates the current limit + * + * @param _limit The new limit + * @param _oldLimit The old limit + * @param _currentLimit The current limit + * @return _newCurrentLimit The new current limit + */ + function _calculateNewCurrentLimit(uint256 _limit, uint256 _oldLimit, uint256 _currentLimit) + internal + pure + returns (uint256 _newCurrentLimit) + { + uint256 _difference; + + if (_oldLimit > _limit) { + _difference = _oldLimit - _limit; + _newCurrentLimit = _currentLimit > _difference ? _currentLimit - _difference : 0; + } else { + _difference = _limit - _oldLimit; + _newCurrentLimit = _currentLimit + _difference; + } + } + + /** + * @notice Gets the current limit + * + * @param _currentLimit The current limit + * @param _maxLimit The max limit + * @param _timestamp The timestamp of the last update + * @param _ratePerSecond The rate per second + * @return _limit The current limit + */ + function _getCurrentLimit(uint256 _currentLimit, uint256 _maxLimit, uint256 _timestamp, uint256 _ratePerSecond) + internal + view + returns (uint256 _limit) + { + _limit = _currentLimit; + if (_limit == _maxLimit) { + return _limit; + } else if (_timestamp + _DURATION <= block.timestamp) { + _limit = _maxLimit; + } else if (_timestamp + _DURATION > block.timestamp) { + uint256 _timePassed = block.timestamp - _timestamp; + uint256 _calculatedLimit = _limit + (_timePassed * _ratePerSecond); + _limit = _calculatedLimit > _maxLimit ? _maxLimit : _calculatedLimit; + } + } + + /** + * @notice Internal function for burning tokens + * + * @param _caller The caller address + * @param _user The user address + * @param _amount The amount to burn + */ + function _burnWithCaller(address _caller, address _user, uint256 _amount) internal { + if (_caller != lockbox) { + uint256 _currentLimit = burningCurrentLimitOf(_caller); + if (_currentLimit < _amount) revert IXERC20_NotHighEnoughLimits(); + _useBurnerLimits(_caller, _amount); + } + _burn(_user, _amount); + } + + /** + * @notice Internal function for minting tokens + * + * @param _caller The caller address + * @param _user The user address + * @param _amount The amount to mint + */ + function _mintWithCaller(address _caller, address _user, uint256 _amount) internal { + if (_caller != lockbox) { + uint256 _currentLimit = mintingCurrentLimitOf(_caller); + if (_currentLimit < _amount) revert IXERC20_NotHighEnoughLimits(); + _useMinterLimits(_caller, _amount); + } + _mint(_user, _amount); + } + + /** + * @dev Authorizes an upgrade to a new implementation + * Restricted access + * @param newImplementation The address of the new implementation + */ + // slither-disable-next-line dead-code + function _authorizeUpgrade(address newImplementation) internal virtual override restricted { } +} diff --git a/src/l2/interface/IXERC20.sol b/src/l2/interface/IXERC20.sol new file mode 100644 index 0000000..4d4b4e5 --- /dev/null +++ b/src/l2/interface/IXERC20.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.4 <0.9.0; + +interface IXERC20 { + /** + * @notice Emits when a lockbox is set + * + * @param _lockbox The address of the 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 + */ + 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 + */ + error IXERC20_NotHighEnoughLimits(); + + /** + * @notice Reverts when caller is not the factory + */ + error IXERC20_NotFactory(); + + /** + * @notice Reverts when limits are too high + */ + error IXERC20_LimitsTooHigh(); + + /** + * @notice Contains the full minting and burning data for a particular bridge + * + * @param minterParams The minting parameters for the bridge + * @param burnerParams The burning parameters for the bridge + */ + struct Bridge { + BridgeParameters minterParams; + BridgeParameters burnerParams; + } + + /** + * @notice Contains the mint or burn parameters for a bridge + * + * @param timestamp The timestamp of the last mint/burn + * @param ratePerSecond The rate per second of the bridge + * @param maxLimit The max limit of the bridge + * @param currentLimit The current limit of the bridge + */ + struct BridgeParameters { + uint256 timestamp; + uint256 ratePerSecond; + uint256 maxLimit; + uint256 currentLimit; + } + + /** + * @notice Sets the lockbox address + * + * @param _lockbox The address of the lockbox + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + function burn(address _user, uint256 _amount) external; +}