From 12917d92ded5284afa12318a8c6bf64d1c250d80 Mon Sep 17 00:00:00 2001 From: wadealexc Date: Mon, 9 Sep 2024 18:37:21 +0000 Subject: [PATCH] feat: token changes --- .../mainnet/bEIGEN_timelock_reduction.s.sol | 72 ++++++++++++ script/deploy/mainnet/bEIGEN_upgrade.s.sol | 104 ++++++++++++++++++ src/contracts/interfaces/IBackingEigen.sol | 27 +++++ src/contracts/interfaces/IEigen.sol | 6 + src/contracts/token/BackingEigen.sol | 30 ++++- src/contracts/token/Eigen.sol | 13 ++- .../token/EigenTransferRestrictions.t.sol | 14 ++- src/test/token/EigenWrapping.t.sol | 33 ++++-- src/test/token/bEIGEN.t.sol | 62 ++++++++++- 9 files changed, 337 insertions(+), 24 deletions(-) create mode 100644 script/deploy/mainnet/bEIGEN_timelock_reduction.s.sol create mode 100644 script/deploy/mainnet/bEIGEN_upgrade.s.sol diff --git a/script/deploy/mainnet/bEIGEN_timelock_reduction.s.sol b/script/deploy/mainnet/bEIGEN_timelock_reduction.s.sol new file mode 100644 index 0000000000..d4d0b48131 --- /dev/null +++ b/script/deploy/mainnet/bEIGEN_timelock_reduction.s.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/governance/TimelockController.sol"; + +import "forge-std/Script.sol"; +import "forge-std/Test.sol"; + +// # To load the variables in the .env file +// source .env + +// # To deploy and verify our contract +// forge script script/deploy/mainnet/bEIGEN_timelock_reduction.s.sol:bEIGEN_timelock_reduction -vvvv --rpc-url $RPC_URL +contract bEIGEN_timelock_reduction is Script, Test { + Vm cheats = Vm(HEVM_ADDRESS); + + TimelockController public bEIGEN_TimelockController = TimelockController(payable(0xd6EC41E453C5E7dA5494f4d51A053Ab571712E6f)); + address public bEIGEN_TimelockAdmin = 0xbb00DDa2832850a43840A3A86515E3Fe226865F2; + + uint256 public newDelay = 0; + + function run() external { + // Read and log the chain ID + uint256 chainId = block.chainid; + emit log_named_uint("You are deploying on ChainID", chainId); + + if (chainId == 1) { + // rpcUrl = "RPC_MAINNET"; + } else { + revert("Chain not supported"); + } + + uint256 minDelayBefore = bEIGEN_TimelockController.getMinDelay(); + + require(minDelayBefore == 24 days, + "something horribly wrong"); + + bytes memory proposalData = abi.encodeWithSelector( + TimelockController.updateDelay.selector, + newDelay + ); + emit log_named_bytes("proposalData", proposalData); + + // propose change to zero delay + vm.startPrank(bEIGEN_TimelockAdmin); + bEIGEN_TimelockController.schedule({ + target: address(bEIGEN_TimelockController), + value: 0, + data: proposalData, + predecessor: bytes32(0), + salt: bytes32(0), + delay: minDelayBefore + }); + + // fast-forward to after current delay and execute + vm.warp(block.timestamp + minDelayBefore); + bEIGEN_TimelockController.execute({ + target: address(bEIGEN_TimelockController), + value: 0, + payload: proposalData, + predecessor: bytes32(0), + salt: bytes32(0) + }); + + cheats.stopPrank(); + + uint256 minDelayAfter = bEIGEN_TimelockController.getMinDelay(); + + require(minDelayAfter == 0, + "min delay not set to zero"); + } +} \ No newline at end of file diff --git a/script/deploy/mainnet/bEIGEN_upgrade.s.sol b/script/deploy/mainnet/bEIGEN_upgrade.s.sol new file mode 100644 index 0000000000..b4eabdbc2c --- /dev/null +++ b/script/deploy/mainnet/bEIGEN_upgrade.s.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import "@openzeppelin/contracts/governance/TimelockController.sol"; + +import "../../../src/contracts/token/BackingEigen.sol"; + +import "forge-std/Script.sol"; +import "forge-std/Test.sol"; + +// # To load the variables in the .env file +// source .env + +// # To deploy and verify our contract +// forge script script/deploy/mainnet/bEIGEN_upgrade.s.sol:bEIGEN_upgrade -vvvv --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast +contract bEIGEN_upgrade is Script, Test { + Vm cheats = Vm(HEVM_ADDRESS); + + BackingEigen public bEIGEN_proxy = BackingEigen(0x83E9115d334D248Ce39a6f36144aEaB5b3456e75); + BackingEigen public bEIGEN_implementation; + ProxyAdmin public bEIGEN_ProxyAdmin = ProxyAdmin(0x3f5Ab2D4418d38568705bFd6672630fCC3435CC9); + TimelockController public bEIGEN_TimelockController = TimelockController(payable(0xd6EC41E453C5E7dA5494f4d51A053Ab571712E6f)); + address public bEIGEN_TimelockAdmin = 0xbb00DDa2832850a43840A3A86515E3Fe226865F2; + + // // RPC url to fork from for pre-upgrade state change tests + // string public rpcUrl; + + IERC20 public EIGEN_addressBefore; + + function run() external { + // Read and log the chain ID + uint256 chainId = block.chainid; + emit log_named_uint("You are deploying on ChainID", chainId); + + if (chainId == 1) { + // rpcUrl = "RPC_MAINNET"; + } else { + revert("Chain not supported"); + } + + EIGEN_addressBefore = bEIGEN_proxy.EIGEN(); + + require(EIGEN_addressBefore == IERC20(0xec53bF9167f50cDEB3Ae105f56099aaaB9061F83), + "something horribly wrong"); + + // Begin deployment + vm.startBroadcast(); + + // Deploy new implmementation contract + bEIGEN_implementation = new BackingEigen({ + _EIGEN: EIGEN_addressBefore + }); + + vm.stopBroadcast(); + + emit log_named_address("bEIGEN_implementation", address(bEIGEN_implementation)); + + // Perform post-upgrade tests + simulatePerformingUpgrade(); + checkUpgradeCorrectness(); + } + + function simulatePerformingUpgrade() public { + // Upgrade beacon + uint256 delay = bEIGEN_TimelockController.getMinDelay(); + bytes memory data = abi.encodeWithSelector( + ProxyAdmin.upgrade.selector, + TransparentUpgradeableProxy(payable(address(bEIGEN_proxy))), + bEIGEN_implementation + ); + emit log_named_bytes("data", data); + + vm.startPrank(bEIGEN_TimelockAdmin); + bEIGEN_TimelockController.schedule({ + target: address(bEIGEN_ProxyAdmin), + value: 0, + data: data, + predecessor: bytes32(0), + salt: bytes32(0), + delay: delay + }); + + vm.warp(block.timestamp + delay); + bEIGEN_TimelockController.execute({ + target: address(bEIGEN_ProxyAdmin), + value: 0, + payload: data, + predecessor: bytes32(0), + salt: bytes32(0) + }); + + cheats.stopPrank(); + } + + function checkUpgradeCorrectness() public { + vm.prank(address(bEIGEN_TimelockController)); + require(bEIGEN_ProxyAdmin.getProxyImplementation(TransparentUpgradeableProxy(payable(address(bEIGEN_proxy)))) == address(bEIGEN_implementation), + "implementation set incorrectly"); + require(bEIGEN_proxy.EIGEN() == EIGEN_addressBefore, + "EIGEN address changed unexpectedly"); + } +} \ No newline at end of file diff --git a/src/contracts/interfaces/IBackingEigen.sol b/src/contracts/interfaces/IBackingEigen.sol index b50c88ba2b..2542d734a4 100644 --- a/src/contracts/interfaces/IBackingEigen.sol +++ b/src/contracts/interfaces/IBackingEigen.sol @@ -23,6 +23,33 @@ interface IBackingEigen is IERC20 { */ function disableTransferRestrictions() external; + /** + * @notice An initializer function that sets initial values for the contract's state variables. + */ + function initialize(address initialOwner) external; + + // @notice Allows the contract owner to modify an entry in the `isMinter` mapping. + function setIsMinter(address minterAddress, bool newStatus) external; + + /** + * @notice Allows any privileged address to mint `amount` new tokens to the address `to`. + * @dev Callable only by an address that has `isMinter` set to true. + */ + function mint(address to, uint256 amount) external; + + /** + * @dev Destroys `amount` tokens from the caller. + * + * See {ERC20-_burn}. + */ + function burn(uint256 amount) external; + + /// @notice the address of the wrapped Eigen token EIGEN + function EIGEN() external view returns (IERC20); + + /// @notice the timestamp after which transfer restrictions are disabled + function transferRestrictionsDisabledAfter() external view returns (uint256); + /** * @dev Clock used for flagging checkpoints. Has been overridden to implement timestamp based * checkpoints (and voting). diff --git a/src/contracts/interfaces/IEigen.sol b/src/contracts/interfaces/IEigen.sol index cd9279e559..1809902d47 100644 --- a/src/contracts/interfaces/IEigen.sol +++ b/src/contracts/interfaces/IEigen.sol @@ -38,6 +38,12 @@ interface IEigen is IERC20 { */ function unwrap(uint256 amount) external; + // @notice Burns EIGEN tokens held by the EIGEN token address itself + function burnExtraTokens() external; + + /// @notice the address of the backing Eigen token bEIGEN + function bEIGEN() external view returns (IERC20); + /** * @dev Clock used for flagging checkpoints. Has been overridden to implement timestamp based * checkpoints (and voting). diff --git a/src/contracts/token/BackingEigen.sol b/src/contracts/token/BackingEigen.sol index 0a3b707ecd..c8a97160de 100644 --- a/src/contracts/token/BackingEigen.sol +++ b/src/contracts/token/BackingEigen.sol @@ -17,6 +17,8 @@ contract BackingEigen is OwnableUpgradeable, ERC20VotesUpgradeable { mapping(address => bool) public allowedFrom; /// @notice mapping of addresses that are allowed to receive tokens from any address mapping(address => bool) public allowedTo; + // @notice whether or not an address is allowed to mint new bEIGEN tokens + mapping(address => bool) public isMinter; /// @notice event emitted when the allowedFrom status of an address is set event SetAllowedFrom(address indexed from, bool isAllowedFrom); @@ -26,12 +28,38 @@ contract BackingEigen is OwnableUpgradeable, ERC20VotesUpgradeable { event TransferRestrictionsDisabled(); /// @notice event emitted when the EIGEN token is backed event Backed(); + // @notice event emitted when the `isMinter` mapping is modified + event IsMinterModified(address indexed minterAddress, bool newStatus); constructor(IERC20 _EIGEN) { EIGEN = _EIGEN; _disableInitializers(); } + // @notice Allows the contract owner to modify an entry in the `isMinter` mapping. + function setIsMinter(address minterAddress, bool newStatus) external onlyOwner { + emit IsMinterModified(minterAddress, newStatus); + isMinter[minterAddress] = newStatus; + } + + /** + * @notice Allows any privileged address to mint `amount` new tokens to the address `to`. + * @dev Callable only by an address that has `isMinter` set to true. + */ + function mint(address to, uint256 amount) external { + require(isMinter[msg.sender], "BackingEigen.mint: caller is not a minter"); + _mint(to, amount); + } + + /** + * @dev Destroys `amount` tokens from the caller. + * + * See {ERC20-_burn}. + */ + function burn(uint256 amount) public virtual { + _burn(_msgSender(), amount); + } + /** * @notice An initializer function that sets initial values for the contract's state variables. */ @@ -51,7 +79,7 @@ contract BackingEigen is OwnableUpgradeable, ERC20VotesUpgradeable { // Mint the entire supply of EIGEN - this is a one-time event that // ensures bEIGEN fully backs EIGEN. - _mint(address(EIGEN), EIGEN.totalSupply()); + _mint(address(EIGEN), 1673646668284660000000000000); emit Backed(); } diff --git a/src/contracts/token/Eigen.sol b/src/contracts/token/Eigen.sol index 327fe13b47..1d3cc37747 100644 --- a/src/contracts/token/Eigen.sol +++ b/src/contracts/token/Eigen.sol @@ -123,14 +123,14 @@ contract Eigen is OwnableUpgradeable, ERC20VotesUpgradeable { */ function wrap(uint256 amount) external { require(bEIGEN.transferFrom(msg.sender, address(this), amount), "Eigen.wrap: bEIGEN transfer failed"); - _transfer(address(this), msg.sender, amount); + _mint(msg.sender, amount); } /** * @notice This function allows Eigen holders to unwrap their tokens into bEIGEN */ function unwrap(uint256 amount) external { - _transfer(msg.sender, address(this), amount); + _burn(msg.sender, amount); require(bEIGEN.transfer(msg.sender, amount), "Eigen.unwrap: bEIGEN transfer failed"); } @@ -162,6 +162,15 @@ contract Eigen is OwnableUpgradeable, ERC20VotesUpgradeable { super._beforeTokenTransfer(from, to, amount); } + /** + * @notice Overridden to return the total bEIGEN supply instead. + * @dev The issued supply of EIGEN should match the bEIGEN balance of this contract, + * less any bEIGEN tokens that were sent directly to the contract (rather than being wrapped) + */ + function totalSupply() public view override returns (uint256) { + return bEIGEN.totalSupply(); + } + /** * @dev Clock used for flagging checkpoints. Has been overridden to implement timestamp based * checkpoints (and voting). diff --git a/src/test/token/EigenTransferRestrictions.t.sol b/src/test/token/EigenTransferRestrictions.t.sol index 37b6902126..c254f97a7b 100644 --- a/src/test/token/EigenTransferRestrictions.t.sol +++ b/src/test/token/EigenTransferRestrictions.t.sol @@ -3,8 +3,9 @@ pragma solidity ^0.8.12; import "forge-std/Test.sol"; -import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; -import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import "@openzeppelin-v4.9.0/contracts/proxy/transparent/ProxyAdmin.sol"; +import "@openzeppelin-v4.9.0/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import "@openzeppelin-v4.9.0/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; import "../harnesses/EigenHarness.sol"; contract EigenTransferRestrictionsTest is Test { @@ -38,8 +39,15 @@ contract EigenTransferRestrictionsTest is Test { vm.startPrank(minter1); proxyAdmin = new ProxyAdmin(); // initialize with dummy BackingEigen address - eigenImpl = new EigenHarness(IERC20(address(0))); + + eigenImpl = new EigenHarness(new ERC20PresetFixedSupply({ + name: "bEIGEN", + symbol: "bEIGEN", + initialSupply: totalSupply, + owner: minter1 + })); eigen = Eigen(address(new TransparentUpgradeableProxy(address(eigenImpl), address(proxyAdmin), ""))); + eigen.bEIGEN().transfer(address(eigen), totalSupply); vm.stopPrank(); fuzzedOutAddresses[minter1] = true; diff --git a/src/test/token/EigenWrapping.t.sol b/src/test/token/EigenWrapping.t.sol index d2a8683aa8..862beb317a 100644 --- a/src/test/token/EigenWrapping.t.sol +++ b/src/test/token/EigenWrapping.t.sol @@ -72,10 +72,6 @@ contract EigenWrappingTests is Test { _simulateMint(); _simulateBackingAndSetTransferRestrictions(); - // initial bEIGEN balance - uint256 initialBEIGENBalanceOfEigenToken = bEIGEN.balanceOf(address(eigen)); - // initial EIGEN token supply - uint256 initialEigenSupply = eigen.totalSupply(); // minter1 balance uint256 minter1Balance = eigen.balanceOf(minter1); @@ -83,15 +79,23 @@ contract EigenWrappingTests is Test { vm.prank(minter1); eigen.transfer(unwrapper, minter1Balance); + // initial bEIGEN balance + uint256 initialBEIGENBalanceOfEigenToken = bEIGEN.balanceOf(address(eigen)); + // initial EIGEN token supply + assertEq(eigen.totalSupply(), bEIGEN.totalSupply(), + "eigen totalSupply changed incorrectly"); + // unwrap // unwrap amount should be less than minter1 balance unwrapAmount = unwrapAmount % minter1Balance; vm.prank(unwrapper); eigen.unwrap(unwrapAmount); - // check that the total supply of bEIGEN is equal to the total supply of EIGEN - assertEq(eigen.totalSupply(), initialEigenSupply); - assertEq(bEIGEN.balanceOf(address(eigen)), initialBEIGENBalanceOfEigenToken - unwrapAmount); + // check total supply and balance changes + assertEq(eigen.totalSupply(), bEIGEN.totalSupply(), + "eigen totalSupply changed incorrectly"); + assertEq(bEIGEN.balanceOf(address(eigen)), initialBEIGENBalanceOfEigenToken - unwrapAmount, + "beigen balance of EIGEN tokens changed incorrectly"); assertEq(eigen.balanceOf(address(unwrapper)), minter1Balance - unwrapAmount); assertEq(bEIGEN.balanceOf(address(unwrapper)), unwrapAmount); } @@ -104,8 +108,6 @@ contract EigenWrappingTests is Test { // initial bEIGEN balance uint256 initialBEIGENBalanceOfEigenToken = bEIGEN.balanceOf(address(eigen)); - // initial EIGEN token supply - uint256 initialEigenSupply = eigen.totalSupply(); // minter1 balance uint256 minter1Balance = eigen.balanceOf(minter1); @@ -117,6 +119,10 @@ contract EigenWrappingTests is Test { bEIGEN.transfer(wrapper, minter1Balance); vm.stopPrank(); + // initial EIGEN token supply + assertEq(eigen.totalSupply(), bEIGEN.totalSupply(), + "eigen totalSupply changed incorrectly"); + // wrap // wrap amount should be less than minter1 balance wrapAmount = wrapAmount % minter1Balance; @@ -127,8 +133,9 @@ contract EigenWrappingTests is Test { eigen.wrap(wrapAmount); vm.stopPrank(); - // check that the total supply of bEIGEN is equal to the total supply of EIGEN - assertEq(eigen.totalSupply(), initialEigenSupply); + // check total supply and balance changes + assertEq(eigen.totalSupply(), bEIGEN.totalSupply(), + "eigen totalSupply changed incorrectly"); assertEq(bEIGEN.balanceOf(address(eigen)), initialBEIGENBalanceOfEigenToken - minter1Balance + wrapAmount); assertEq(eigen.balanceOf(address(wrapper)), wrapAmount); assertEq(bEIGEN.balanceOf(address(wrapper)), minter1Balance - wrapAmount); @@ -147,7 +154,7 @@ contract EigenWrappingTests is Test { // unwrap vm.prank(unwrapper); - vm.expectRevert("ERC20: transfer amount exceeds balance"); + vm.expectRevert("ERC20: burn amount exceeds balance"); eigen.unwrap(unwrapAmount + 1); } @@ -195,6 +202,8 @@ contract EigenWrappingTests is Test { vm.startPrank(minter1); bEIGEN.setAllowedFrom(minter1, true); + eigen.setAllowedTo(address(0), true); vm.stopPrank(); + } } diff --git a/src/test/token/bEIGEN.t.sol b/src/test/token/bEIGEN.t.sol index 5338c57026..06de8b60ed 100644 --- a/src/test/token/bEIGEN.t.sol +++ b/src/test/token/bEIGEN.t.sol @@ -13,7 +13,9 @@ import "../../contracts/token/BackingEigen.sol"; contract bEIGENTest is Test { mapping(address => bool) fuzzedOutAddresses; - address minter1 = 0xbb00DDa2832850a43840A3A86515E3Fe226865F2; + address initialOwner = 0xbb00DDa2832850a43840A3A86515E3Fe226865F2; + address minterToSet = address(500); + address mintTo = address(12345); ProxyAdmin proxyAdmin; @@ -25,7 +27,7 @@ contract bEIGENTest is Test { function setUp() public { - vm.startPrank(minter1); + vm.startPrank(initialOwner); proxyAdmin = new ProxyAdmin(); // deploy proxies @@ -44,10 +46,10 @@ contract bEIGENTest is Test { } function test_Initialize() public { - bEIGEN.initialize(minter1); + bEIGEN.initialize(initialOwner); - // check that the owner is minter1 - assertEq(bEIGEN.owner(), minter1); + // check that the owner is initialOwner + assertEq(bEIGEN.owner(), initialOwner); // check the transfer restrictions are disabled after one year in the future assertEq(bEIGEN.transferRestrictionsDisabledAfter(), type(uint256).max); } @@ -55,10 +57,58 @@ contract bEIGENTest is Test { function testFuzz_CanBackTheEigenToken(uint eigenSupply) public { StdCheats.deal(address(eigen), address(this), eigenSupply); - bEIGEN.initialize(minter1); + bEIGEN.initialize(initialOwner); // check that the total supply of bEIGEN is equal to the total supply of EIGEN assertEq(bEIGEN.totalSupply(), eigen.totalSupply()); assertEq(bEIGEN.balanceOf(address(eigen)), bEIGEN.totalSupply()); } + + function test_setIsMinterAndMint() public { + bEIGEN.initialize(initialOwner); + + vm.prank(initialOwner); + bEIGEN.setIsMinter(minterToSet, true); + require(bEIGEN.isMinter(minterToSet), "minter not set correctly"); + + uint256 amountToMint = 5e25; + uint256 balanceBefore = bEIGEN.balanceOf(mintTo); + vm.prank(minterToSet); + bEIGEN.mint(mintTo, amountToMint); + + uint256 balanceAfter = bEIGEN.balanceOf(mintTo); + uint256 balanceDiff = balanceAfter - balanceBefore; + assertEq(balanceDiff, amountToMint, "mint not working correctly"); + } + + function test_setIsMinter_revertsWhenNotCalledByOwner() public { + bEIGEN.initialize(initialOwner); + + vm.prank(mintTo); + vm.expectRevert("Ownable: caller is not the owner"); + bEIGEN.setIsMinter(minterToSet, true); + } + + function test_burn() public { + test_setIsMinterAndMint(); + vm.prank(initialOwner); + bEIGEN.setAllowedFrom(mintTo, true); + + uint256 amountToBurn = 1005e18; + uint256 balanceBefore = bEIGEN.balanceOf(mintTo); + vm.prank(mintTo); + bEIGEN.burn(amountToBurn); + + uint256 balanceAfter = bEIGEN.balanceOf(mintTo); + uint256 balanceDiff = balanceBefore - balanceAfter; + assertEq(balanceDiff, amountToBurn, "mint not working correctly"); + } + + function test_mint_revertsWhenNotCalledByMinter() public { + test_setIsMinterAndMint(); + + uint256 amountToMint = 5e25; + vm.expectRevert("BackingEigen.mint: caller is not a minter"); + bEIGEN.mint(mintTo, amountToMint); + } }