diff --git a/mainnet-contracts/script/AccessManagerMigrations/05_GenerateRevenueDepositorCalldata.s.sol b/mainnet-contracts/script/AccessManagerMigrations/06_GenerateRevenueDepositorCalldata.s.sol similarity index 68% rename from mainnet-contracts/script/AccessManagerMigrations/05_GenerateRevenueDepositorCalldata.s.sol rename to mainnet-contracts/script/AccessManagerMigrations/06_GenerateRevenueDepositorCalldata.s.sol index a966acc1..fbf377cd 100644 --- a/mainnet-contracts/script/AccessManagerMigrations/05_GenerateRevenueDepositorCalldata.s.sol +++ b/mainnet-contracts/script/AccessManagerMigrations/06_GenerateRevenueDepositorCalldata.s.sol @@ -4,23 +4,22 @@ pragma solidity >=0.8.0 <0.9.0; import { Script } from "forge-std/Script.sol"; import { AccessManager } from "@openzeppelin/contracts/access/manager/AccessManager.sol"; import { Multicall } from "@openzeppelin/contracts/utils/Multicall.sol"; -import { ROLE_ID_DAO, ROLE_ID_OPERATIONS_MULTISIG, ROLE_ID_REVENUE_DEPOSITOR } from "../../script/Roles.sol"; +import { ROLE_ID_DAO, ROLE_ID_REVENUE_DEPOSITOR, ROLE_ID_OPERATIONS_MULTISIG } from "../../script/Roles.sol"; import { PufferRevenueDepositor } from "../../src/PufferRevenueDepositor.sol"; contract GenerateRevenueDepositorCalldata is Script { function run(address revenueDepositorProxy, address operationsMultisig) public pure returns (bytes memory) { bytes[] memory calldatas = new bytes[](5); - bytes4[] memory daoSelectors = new bytes4[](3); - daoSelectors[0] = PufferRevenueDepositor.setRnoRewardsBps.selector; - daoSelectors[1] = PufferRevenueDepositor.setTreasuryRewardsBps.selector; - daoSelectors[2] = PufferRevenueDepositor.setRewardsDistributionWindow.selector; + bytes4[] memory daoSelectors = new bytes4[](1); + daoSelectors[0] = PufferRevenueDepositor.setRewardsDistributionWindow.selector; calldatas[0] = abi.encodeCall(AccessManager.setTargetFunctionRole, (revenueDepositorProxy, daoSelectors, ROLE_ID_DAO)); - bytes4[] memory revenueDepositorSelectors = new bytes4[](1); + bytes4[] memory revenueDepositorSelectors = new bytes4[](2); revenueDepositorSelectors[0] = PufferRevenueDepositor.depositRevenue.selector; + revenueDepositorSelectors[1] = PufferRevenueDepositor.withdrawAndDeposit.selector; calldatas[1] = abi.encodeCall( AccessManager.setTargetFunctionRole, @@ -31,13 +30,12 @@ contract GenerateRevenueDepositorCalldata is Script { calldatas[3] = abi.encodeCall(AccessManager.labelRole, (ROLE_ID_REVENUE_DEPOSITOR, "Revenue Depositor")); - bytes4[] memory opsMultisigSelectors = new bytes4[](2); - opsMultisigSelectors[0] = PufferRevenueDepositor.removeRestakingOperator.selector; - opsMultisigSelectors[1] = PufferRevenueDepositor.addRestakingOperators.selector; + bytes4[] memory operationsMultisigSelectors = new bytes4[](1); + operationsMultisigSelectors[0] = PufferRevenueDepositor.callTargets.selector; calldatas[4] = abi.encodeCall( AccessManager.setTargetFunctionRole, - (revenueDepositorProxy, opsMultisigSelectors, ROLE_ID_OPERATIONS_MULTISIG) + (revenueDepositorProxy, operationsMultisigSelectors, ROLE_ID_OPERATIONS_MULTISIG) ); bytes memory encodedMulticall = abi.encodeCall(Multicall.multicall, (calldatas)); diff --git a/mainnet-contracts/script/DeployEverything.s.sol b/mainnet-contracts/script/DeployEverything.s.sol index f92f2f49..8572dd02 100644 --- a/mainnet-contracts/script/DeployEverything.s.sol +++ b/mainnet-contracts/script/DeployEverything.s.sol @@ -14,7 +14,8 @@ import { GuardiansDeployment, PufferProtocolDeployment, BridgingDeployment } fro import { PufferRevenueDepositor } from "src/PufferRevenueDepositor.sol"; import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import { GenerateRevenueDepositorCalldata } from - "script/AccessManagerMigrations/05_GenerateRevenueDepositorCalldata.s.sol"; + "script/AccessManagerMigrations/06_GenerateRevenueDepositorCalldata.s.sol"; +import { MockAeraVault } from "test/mocks/MockAeraVault.sol"; /** * @title Deploy all protocol contracts @@ -107,30 +108,20 @@ contract DeployEverything is BaseScript { // script/DeployRevenueDepositor.s.sol It should match the one in the script function _deployRevenueDepositor(PufferDeployment memory puffETHDeployment) internal returns (address) { + MockAeraVault mockAeraVault = new MockAeraVault(); + PufferRevenueDepositor revenueDepositorImpl = new PufferRevenueDepositor({ vault: address(puffETHDeployment.pufferVault), weth: address(puffETHDeployment.weth), - treasury: makeAddr("Treasury") + aeraVault: address(mockAeraVault) }); - address[] memory operatorsAddresses = new address[](7); - operatorsAddresses[0] = makeAddr("RNO1"); - operatorsAddresses[1] = makeAddr("RNO2"); - operatorsAddresses[2] = makeAddr("RNO3"); - operatorsAddresses[3] = makeAddr("RNO4"); - operatorsAddresses[4] = makeAddr("RNO5"); - operatorsAddresses[5] = makeAddr("RNO6"); - operatorsAddresses[6] = makeAddr("RNO7"); - PufferRevenueDepositor revenueDepositor = PufferRevenueDepositor( ( payable( new ERC1967Proxy{ salt: bytes32("revenueDepositor") }( address(revenueDepositorImpl), - abi.encodeCall( - PufferRevenueDepositor.initialize, - (address(puffETHDeployment.accessManager), operatorsAddresses) - ) + abi.encodeCall(PufferRevenueDepositor.initialize, (address(puffETHDeployment.accessManager))) ) ) ) diff --git a/mainnet-contracts/script/DeployRevenueDepositor.s.sol b/mainnet-contracts/script/DeployRevenueDepositor.s.sol index a414c410..da679499 100644 --- a/mainnet-contracts/script/DeployRevenueDepositor.s.sol +++ b/mainnet-contracts/script/DeployRevenueDepositor.s.sol @@ -5,7 +5,7 @@ import "forge-std/Script.sol"; import { DeployerHelper } from "./DeployerHelper.s.sol"; import { PufferRevenueDepositor } from "../src/PufferRevenueDepositor.sol"; import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import { GenerateRevenueDepositorCalldata } from "./AccessManagerMigrations/05_GenerateRevenueDepositorCalldata.s.sol"; +import { GenerateRevenueDepositorCalldata } from "./AccessManagerMigrations/06_GenerateRevenueDepositorCalldata.s.sol"; /** * forge script script/DeployRevenueDepositor.s.sol:DeployRevenueDepositor --rpc-url=$RPC_URL --private-key $PK @@ -20,27 +20,15 @@ contract DeployRevenueDepositor is DeployerHelper { vm.startBroadcast(); - //@todo Get from RNOs - address[] memory operatorsAddresses = new address[](7); - operatorsAddresses[0] = makeAddr("RNO1"); - operatorsAddresses[1] = makeAddr("RNO2"); - operatorsAddresses[2] = makeAddr("RNO3"); - operatorsAddresses[3] = makeAddr("RNO4"); - operatorsAddresses[4] = makeAddr("RNO5"); - operatorsAddresses[5] = makeAddr("RNO6"); - operatorsAddresses[6] = makeAddr("RNO7"); - PufferRevenueDepositor revenueDepositorImpl = - new PufferRevenueDepositor({ vault: _getPufferVault(), weth: _getWETH(), treasury: _getTreasury() }); + new PufferRevenueDepositor({ vault: _getPufferVault(), weth: _getWETH(), aeraVault: _getAeraVault() }); revenueDepositor = PufferRevenueDepositor( ( payable( new ERC1967Proxy{ salt: bytes32("RevenueDepositor") }( address(revenueDepositorImpl), - abi.encodeCall( - PufferRevenueDepositor.initialize, (address(_getAccessManager()), operatorsAddresses) - ) + abi.encodeCall(PufferRevenueDepositor.initialize, (address(_getAccessManager()))) ) ) ) diff --git a/mainnet-contracts/script/DeployerHelper.s.sol b/mainnet-contracts/script/DeployerHelper.s.sol index 5809e245..d8c847eb 100644 --- a/mainnet-contracts/script/DeployerHelper.s.sol +++ b/mainnet-contracts/script/DeployerHelper.s.sol @@ -432,4 +432,31 @@ abstract contract DeployerHelper is Script { revert("OPSMultisig not available for this chain"); } + + function _getAeraVault() internal view returns (address) { + if (block.chainid == mainnet) { + // https://etherscan.io/address/0x6c25aE178aC3466A63A552d4D6509c3d7385A0b8 + return 0x6c25aE178aC3466A63A552d4D6509c3d7385A0b8; + } + + revert("AeraVault not available for this chain"); + } + + function _getAeraAssetRegistry() internal view returns (address) { + if (block.chainid == mainnet) { + // https://etherscan.io/address/0xc71C52425969286dAAd647e4088394C572d64fd9 + return 0xc71C52425969286dAAd647e4088394C572d64fd9; + } + + revert("AeraAssetRegistry not available for this chain"); + } + + function _getAeraVaultHooks() internal view returns (address) { + if (block.chainid == mainnet) { + // https://etherscan.io/address/0x933AD39feb35793B4d6B0A543db39b033Eb5D2C1 + return 0x933AD39feb35793B4d6B0A543db39b033Eb5D2C1; + } + + revert("AeraVaultHooks not available for this chain"); + } } diff --git a/mainnet-contracts/src/PufferRevenueDepositor.sol b/mainnet-contracts/src/PufferRevenueDepositor.sol index 6070e61a..94b96045 100644 --- a/mainnet-contracts/src/PufferRevenueDepositor.sol +++ b/mainnet-contracts/src/PufferRevenueDepositor.sol @@ -9,8 +9,9 @@ import { AccessManagedUpgradeable } from "@openzeppelin/contracts-upgradeable/access/manager/AccessManagedUpgradeable.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { PufferRevenueDepositorStorage } from "./PufferRevenueDepositorStorage.sol"; +import { IAeraVault, AssetValue } from "./interface/Other/IAeraVault.sol"; import { IPufferRevenueDepositor } from "./interface/IPufferRevenueDepositor.sol"; -import { InvalidAddress } from "./Errors.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /** * @title PufferRevenueDepositor @@ -31,16 +32,6 @@ contract PufferRevenueDepositor is */ uint256 private constant _MAXIMUM_DISTRIBUTION_WINDOW = 7 days; - /** - * @notice The basis point scale. (10000 bps = 100%) - */ - uint256 private constant _BASIS_POINT_SCALE = 10000; - - /** - * @notice The maximum rewards in basis points. (10% is the max rewards amount for treasury and RNO) - */ - uint256 private constant _MAX_REWARDS_BPS = 1000; - /** * @notice PufferVault contract. * @custom:oz-upgrades-unsafe-allow state-variable-immutable @@ -48,40 +39,34 @@ contract PufferRevenueDepositor is PufferVaultV4 public immutable PUFFER_VAULT; /** - * @notice WETH contract. - * @custom:oz-upgrades-unsafe-allow state-variable-immutable + * @notice AeraVault contract. */ - IWETH public immutable WETH; + IAeraVault public immutable AERA_VAULT; /** - * @notice Treasury contract. + * @notice WETH contract. * @custom:oz-upgrades-unsafe-allow state-variable-immutable */ - address public immutable TREASURY; + IWETH public immutable WETH; /** * @param vault PufferVault contract * @param weth WETH contract - * @param treasury Puffer Treasury contract * @custom:oz-upgrades-unsafe-allow constructor */ - constructor(address vault, address weth, address treasury) { + constructor(address vault, address weth, address aeraVault) { PUFFER_VAULT = PufferVaultV4(payable(vault)); + AERA_VAULT = IAeraVault(aeraVault); WETH = IWETH(weth); - TREASURY = treasury; _disableInitializers(); } /** * @notice Initialize the contract. * @param accessManager The address of the access manager. - * @param operatorsAddresses The addresses of the restaking operators. */ - function initialize(address accessManager, address[] calldata operatorsAddresses) external initializer { + function initialize(address accessManager) external initializer { __AccessManaged_init(accessManager); - _addRestakingOperators(operatorsAddresses); - _setTreasuryRewardsBps(500); // 5% - _setRnoRewardsBps(400); // 4% } /** @@ -121,39 +106,7 @@ contract PufferRevenueDepositor is * @dev Restricted access to `ROLE_ID_REVENUE_DEPOSITOR` */ function depositRevenue() external restricted { - require(getPendingDistributionAmount() == 0, VaultHasUndepositedRewards()); - - RevenueDepositorStorage storage $ = _getRevenueDepositorStorage(); - $.lastDepositTimestamp = uint48(block.timestamp); - - // Wrap any ETH sent to the contract - if (address(this).balance > 0) { - WETH.deposit{ value: address(this).balance }(); - } - // nosemgrep tin-reentrant-dbl-diff-balance - uint256 rewardsAmount = WETH.balanceOf(address(this)); - - uint256 treasuryRewards = (rewardsAmount * $.treasuryRewardsBps) / _BASIS_POINT_SCALE; - - require(treasuryRewards > 0, NothingToDistribute()); - WETH.transfer(TREASURY, treasuryRewards); - - uint256 rnoRewards = (rewardsAmount * $.rNORewardsBps) / _BASIS_POINT_SCALE; - - // Distribute RNO rewards using push pattern because it is WETH and more convenient - uint256 operatorCount = $.restakingOperators.length(); - uint256 rewardPerOperator = rnoRewards / operatorCount; - - for (uint256 i = 0; i < operatorCount; ++i) { - WETH.transfer($.restakingOperators.at(i), rewardPerOperator); - } - - // Deposit remaining rewards to the PufferVault - uint256 vaultRewards = WETH.balanceOf(address(this)); - $.lastDepositAmount = uint104(vaultRewards); - WETH.transfer(address(PUFFER_VAULT), vaultRewards); - - emit RevenueDeposited(vaultRewards); + _depositRevenue(); } /** @@ -164,30 +117,6 @@ contract PufferRevenueDepositor is return _getRevenueDepositorStorage().lastDepositTimestamp; } - /** - * @notice Get restaking operators. - * @return The addresses of the restaking operators. - */ - function getRestakingOperators() external view returns (address[] memory) { - return _getRevenueDepositorStorage().restakingOperators.values(); - } - - /** - * @notice Get the RNO rewards basis points. - * @return The RNO rewards in basis points. - */ - function getRnoRewardsBps() external view returns (uint128) { - return _getRevenueDepositorStorage().rNORewardsBps; - } - - /** - * @notice Get the treasury rewards basis points. - * @return The RNO rewards in basis points. - */ - function getTreasuryRewardsBps() external view returns (uint128) { - return _getRevenueDepositorStorage().treasuryRewardsBps; - } - /** * @notice Set the rewards distribution window. * @dev Restricted access to `ROLE_ID_DAO` @@ -203,72 +132,52 @@ contract PufferRevenueDepositor is } /** - * @notice Set the RNO rewards basis points. - * @dev Restricted access to `ROLE_ID_DAO` - * @param newBps The new RNO rewards in basis points. - */ - function setRnoRewardsBps(uint128 newBps) external restricted { - _setRnoRewardsBps(newBps); - } - - function _setRnoRewardsBps(uint128 newBps) internal { - require(newBps <= _MAX_REWARDS_BPS, InvalidBps()); - - RevenueDepositorStorage storage $ = _getRevenueDepositorStorage(); - - emit RnoRewardsBpsChanged($.rNORewardsBps, newBps); - $.rNORewardsBps = newBps; - } - - /** - * @notice Set the treasury rewards basis points. - * @dev Restricted access to `ROLE_ID_DAO` - * @param newBps The new treasury rewards in basis points. + * @notice Withdraw WETH from AeraVault and deposit into PufferVault. + * @dev Restricted access to `ROLE_ID_REVENUE_DEPOSITOR` */ - function setTreasuryRewardsBps(uint128 newBps) external restricted { - _setTreasuryRewardsBps(newBps); - } + function withdrawAndDeposit() external restricted { + AssetValue[] memory assets = new AssetValue[](1); - function _setTreasuryRewardsBps(uint128 newBps) internal { - require(newBps <= _MAX_REWARDS_BPS, InvalidBps()); + assets[0] = AssetValue({ asset: IERC20(address(WETH)), value: WETH.balanceOf(address(AERA_VAULT)) }); - RevenueDepositorStorage storage $ = _getRevenueDepositorStorage(); + // Withdraw WETH to this contract + AERA_VAULT.withdraw(assets); - emit TreasuryRewardsBpsChanged($.treasuryRewardsBps, newBps); - $.treasuryRewardsBps = newBps; + _depositRevenue(); } /** - * @notice Remove restaking operator. + * @notice Call multiple targets with the given data. + * @param targets The targets to call + * @param data The data to call the targets with * @dev Restricted access to `ROLE_ID_OPERATIONS_MULTISIG` - * @param operatorAddress The address of the restaking operator. */ - function removeRestakingOperator(address operatorAddress) external restricted { - RevenueDepositorStorage storage $ = _getRevenueDepositorStorage(); - - bool success = $.restakingOperators.remove(operatorAddress); - require(success, RestakingOperatorNotSet()); - emit RestakingOperatorRemoved(operatorAddress); + function callTargets(address[] calldata targets, bytes[] calldata data) external restricted { + for (uint256 i = 0; i < targets.length; ++i) { + // nosemgrep arbitrary-low-level-call + (bool success,) = targets[i].call(data[i]); + require(success, TargetCallFailed()); + } } - /** - * @notice Add new restaking operators. - * @dev Restricted access to `ROLE_ID_OPERATIONS_MULTISIG` - * @param operatorsAddresses The addresses of the restaking operators. - */ - function addRestakingOperators(address[] calldata operatorsAddresses) external restricted { - _addRestakingOperators(operatorsAddresses); - } + function _depositRevenue() internal { + require(getPendingDistributionAmount() == 0, VaultHasUndepositedRewards()); - function _addRestakingOperators(address[] calldata operatorsAddresses) internal { RevenueDepositorStorage storage $ = _getRevenueDepositorStorage(); + $.lastDepositTimestamp = uint48(block.timestamp); - for (uint256 i = 0; i < operatorsAddresses.length; ++i) { - require(operatorsAddresses[i] != address(0), InvalidAddress()); - bool success = $.restakingOperators.add(operatorsAddresses[i]); - require(success, RestakingOperatorAlreadySet()); - emit RestakingOperatorAdded(operatorsAddresses[i]); + // Wrap any ETH sent to the contract + if (address(this).balance > 0) { + WETH.deposit{ value: address(this).balance }(); } + // nosemgrep tin-reentrant-dbl-diff-balance + uint256 rewardsAmount = WETH.balanceOf(address(this)); + require(rewardsAmount > 0, NothingToDistribute()); + + $.lastDepositAmount = uint104(rewardsAmount); + WETH.transfer(address(PUFFER_VAULT), rewardsAmount); + + emit RevenueDeposited(rewardsAmount); } /** diff --git a/mainnet-contracts/src/interface/IPufferRevenueDepositor.sol b/mainnet-contracts/src/interface/IPufferRevenueDepositor.sol index 49161175..6736ef57 100644 --- a/mainnet-contracts/src/interface/IPufferRevenueDepositor.sol +++ b/mainnet-contracts/src/interface/IPufferRevenueDepositor.sol @@ -8,25 +8,15 @@ pragma solidity >=0.8.0 <0.9.0; */ interface IPufferRevenueDepositor { /** - * @notice Thrown when the restaking operator is already set. + * @notice Thrown when the target call fails. */ - error RestakingOperatorAlreadySet(); - - /** - * @notice Thrown when the restaking operator is not set. - */ - error RestakingOperatorNotSet(); + error TargetCallFailed(); /** * @notice Thrown when the vault has undeposited rewards. */ error VaultHasUndepositedRewards(); - /** - * @notice Thrown when the bps is invalid. - */ - error InvalidBps(); - /** * @notice Thrown when there is nothing to distribute. */ @@ -52,32 +42,6 @@ interface IPufferRevenueDepositor { */ event RewardsDistributionWindowChanged(uint256 oldWindow, uint256 newWindow); - /** - * @notice Emitted when a new restaking operator is added. - * @param operator The address of the restaking operator. - */ - event RestakingOperatorAdded(address indexed operator); - - /** - * @notice Emitted when a restaking operator is removed. - * @param operator The address of the restaking operator. - */ - event RestakingOperatorRemoved(address indexed operator); - - /** - * @notice Emitted when the RNO rewards basis points is changed. - * @param oldBps The old RNO rewards in basis points. - * @param newBps The new RNO rewards in basis points. - */ - event RnoRewardsBpsChanged(uint256 oldBps, uint256 newBps); - - /** - * @notice Emitted when the treasury rewards basis points is changed. - * @param oldBps The old treasury rewards in basis points. - * @param newBps The new treasury rewards in basis points. - */ - event TreasuryRewardsBpsChanged(uint256 oldBps, uint256 newBps); - /** * @notice Calculates the remaining amount of ETH that hasn't been fully accounted for in the vault's total assets. * @dev This is used to prevent potential sandwich attacks related to large deposits and to smooth out the restaking rewards deposits. diff --git a/mainnet-contracts/src/interface/Other/IAeraVault.sol b/mainnet-contracts/src/interface/Other/IAeraVault.sol new file mode 100644 index 00000000..8c137901 --- /dev/null +++ b/mainnet-contracts/src/interface/Other/IAeraVault.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +struct AssetValue { + IERC20 asset; + uint256 value; +} + +/// @title IAeraVault +/// @notice Interface for the vault. +/// @dev Any implementation MUST also implement Ownable2Step. +/// Copied and modified https://github.com/aera-finance/aera-contracts-public/blob/main/v2/interfaces/IVault.sol +interface IAeraVault { + /// ERRORS /// + + error Aera__AssetRegistryIsZeroAddress(); + error Aera__AssetRegistryIsNotValid(address assetRegistry); + error Aera__AssetRegistryHasInvalidVault(); + error Aera__HooksIsZeroAddress(); + error Aera__HooksIsNotValid(address hooks); + error Aera__HooksHasInvalidVault(); + error Aera__GuardianIsZeroAddress(); + error Aera__GuardianIsOwner(); + error Aera__InitialOwnerIsZeroAddress(); + error Aera__FeeRecipientIsZeroAddress(); + error Aera__ExecuteTargetIsHooksAddress(); + error Aera__ExecuteTargetIsVaultAddress(); + error Aera__SubmitTransfersAssetFromOwner(); + error Aera__SubmitRedeemERC4626AssetFromOwner(); + error Aera__SubmitTargetIsVaultAddress(); + error Aera__SubmitTargetIsHooksAddress(uint256 index); + error Aera__FeeRecipientIsOwner(); + error Aera__FeeIsAboveMax(uint256 actual, uint256 max); + error Aera__CallerIsNotOwnerAndGuardian(); + error Aera__CallerIsNotGuardian(); + error Aera__AssetIsNotRegistered(IERC20 asset); + error Aera__AmountExceedsAvailable(IERC20 asset, uint256 amount, uint256 available); + error Aera__ExecutionFailed(bytes result); + error Aera__VaultIsFinalized(); + error Aera__SubmissionFailed(uint256 index, bytes result); + error Aera__CannotUseReservedFees(); + error Aera__SpotPricesReverted(); + error Aera__AmountsOrderIsIncorrect(uint256 index); + error Aera__NoAvailableFeesForCaller(address caller); + error Aera__NoClaimableFeesForCaller(address caller); + error Aera__NotWrappedNativeTokenContract(); + error Aera__CannotRenounceOwnership(); + + /// FUNCTIONS /// + + /// @notice Deposit assets. + /// @param amounts Assets and amounts to deposit. + /// @dev MUST revert if not called by owner. + function deposit(AssetValue[] memory amounts) external; + + /// @notice Withdraw assets. + /// @param amounts Assets and amounts to withdraw. + /// @dev MUST revert if not called by owner. + function withdraw(AssetValue[] memory amounts) external; + + /// @notice Set current guardian and fee recipient. + /// @param guardian New guardian address. + /// @param feeRecipient New fee recipient address. + /// @dev MUST revert if not called by owner. + function setGuardianAndFeeRecipient(address guardian, address feeRecipient) external; + + /// @notice Sets the current hooks module. + /// @param hooks New hooks module address. + /// @dev MUST revert if not called by owner. + function setHooks(address hooks) external; + + /// @notice Execute a transaction via the vault. + /// @dev Execution still should work when vault is finalized. + /// @param operation Struct details for target and calldata to execute. + /// @dev MUST revert if not called by owner. + // function execute(Operation memory operation) external; + + /// @notice Terminate the vault and return all funds to owner. + /// @dev MUST revert if not called by owner. + function finalize() external; + + /// @notice Stops the guardian from submission and halts fee accrual. + /// @dev MUST revert if not called by owner or guardian. + function pause() external; + + /// @notice Resume fee accrual and guardian submissions. + /// @dev MUST revert if not called by owner. + function resume() external; + + /// @notice Submit a series of transactions for execution via the vault. + /// @param operations Sequence of operations to execute. + /// @dev MUST revert if not called by guardian. + // function submit(Operation[] memory operations) external; + + /// @notice Claim fees on behalf of a current or previous fee recipient. + function claim() external; + + /// @notice Get the current guardian. + /// @return guardian Address of guardian. + function guardian() external view returns (address guardian); + + /// @notice Get the current fee recipient. + /// @return feeRecipient Address of fee recipient. + function feeRecipient() external view returns (address feeRecipient); + + /// @notice Get the current asset registry. + /// @return assetRegistry Address of asset registry. + // function assetRegistry() + // external + // view + // returns (IAssetRegistry assetRegistry); + + /// @notice Get the current hooks module address. + /// @return hooks Address of hooks module. + // function hooks() external view returns (IHooks hooks); + + /// @notice Get fee per second. + /// @return fee Fee per second in 18 decimal fixed point format. + function fee() external view returns (uint256 fee); + + /// @notice Get current balances of all assets. + /// @return assetAmounts Amounts of registered assets. + function holdings() external view returns (AssetValue[] memory assetAmounts); + + /// @notice Get current total value of assets in vault. + /// @return value Current total value. + function value() external view returns (uint256 value); +} diff --git a/mainnet-contracts/test/Integration/PufferRevenueDepositor.fork.t.sol b/mainnet-contracts/test/Integration/PufferRevenueDepositor.fork.t.sol new file mode 100644 index 00000000..9a1afd3d --- /dev/null +++ b/mainnet-contracts/test/Integration/PufferRevenueDepositor.fork.t.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import { MainnetForkTestHelper } from "../MainnetForkTestHelper.sol"; +import { DeployRevenueDepositor } from "../../script/DeployRevenueDepositor.s.sol"; +import { PufferRevenueDepositor } from "../../src/PufferRevenueDepositor.sol"; +import { Ownable2Step } from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ROLE_ID_REVENUE_DEPOSITOR } from "../../script/Roles.sol"; +import { IPufferRevenueDepositor } from "../../src/interface/IPufferRevenueDepositor.sol"; +import { PufferVaultV4 } from "../../src/PufferVaultV4.sol"; +import { IStETH } from "../../src/interface/Lido/IStETH.sol"; +import { IWETH } from "../../src/interface/Other/IWETH.sol"; +import { ILidoWithdrawalQueue } from "../../src/interface/Lido/ILidoWithdrawalQueue.sol"; +import { IStrategy } from "../../src/interface/EigenLayer/IStrategy.sol"; +import { IEigenLayer } from "../../src/interface/EigenLayer/IEigenLayer.sol"; +import { IPufferOracle } from "../../src/interface/IPufferOracle.sol"; +import { IDelegationManager } from "../../src/interface/EigenLayer/IDelegationManager.sol"; + +struct AssetValue { + IERC20 asset; + uint256 value; +} + +interface IAeraVault { + function withdraw(AssetValue[] calldata amounts) external; +} + +contract PufferRevenueDepositorForkTest is MainnetForkTestHelper { + PufferRevenueDepositor public revenueDepositor; + + uint24 public constant REWARDS_DISTRIBUTION_WINDOW = 1 days; + + function setUp() public virtual override { + // Cancun upgrade + vm.createSelectFork(vm.rpcUrl("mainnet"), 21112808); // (Nov-04-2024 07:31:35 AM +UTC) + + // Setup contracts that are deployed to mainnet + _setupLiveContracts(); + + // Deploy revenue depositor + DeployRevenueDepositor depositorDeployer = new DeployRevenueDepositor(); + depositorDeployer.run(); + revenueDepositor = depositorDeployer.revenueDepositor(); + + // Upgrade PufferVault to V4 + address newVault = address( + new PufferVaultV4( + IStETH(_getStETH()), + IWETH(_getWETH()), + ILidoWithdrawalQueue(_getLidoWithdrawalQueue()), + IStrategy(_getStETHStrategy()), + IEigenLayer(_getEigenLayerStrategyManager()), + IPufferOracle(_getPufferOracle()), + IDelegationManager(_getDelegationManager()), + revenueDepositor + ) + ); + + // Setup AccessManager + vm.startPrank(_getTimelock()); + + // Upgrade PufferVault to V4 + pufferVault.upgradeToAndCall(newVault, ""); + + (bool success,) = address(accessManager).call(depositorDeployer.encodedCalldata()); + assertTrue(success, "Failed to deploy revenue depositor"); + + // Transfer ownership of Aera vault to revenue depositor + vm.startPrank(_getOPSMultisig()); + Ownable2Step(_getAeraVault()).transferOwnership(address(revenueDepositor)); + + // Accept ownership of Aera vault + address[] memory targets = new address[](1); + targets[0] = address(_getAeraVault()); + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeCall(Ownable2Step.acceptOwnership, ()); + + revenueDepositor.callTargets(targets, data); + + // Grant the revenue depositor role to the revenue depositor itesels so that we can use callTargets to withdraw & deposit in 1 tx + vm.startPrank(_getTimelock()); + accessManager.grantRole(ROLE_ID_REVENUE_DEPOSITOR, address(revenueDepositor), 0); + + // Set rewards distribution window to 1 day + vm.startPrank(_getDAO()); + revenueDepositor.setRewardsDistributionWindow(REWARDS_DISTRIBUTION_WINDOW); + + vm.stopPrank(); + } + + function test_sanity() public view { + assertTrue(address(revenueDepositor) != address(0), "Revenue depositor not deployed"); + assertEq( + pufferVault.convertToAssets(1 ether), + 1.026019081620562074 ether, + "1 pufETH should be 1.026019081620562074 WETH" + ); + } + + function test_deposit_revenue() public { + vm.startPrank(_getOPSMultisig()); + + uint256 wethBalance = IERC20(_getWETH()).balanceOf(_getAeraVault()); + + AssetValue[] memory assets = new AssetValue[](1); + assets[0] = AssetValue({ asset: IERC20(_getWETH()), value: wethBalance }); + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeCall(IAeraVault(_getAeraVault()).withdraw, (assets)); + + address[] memory targets = new address[](1); + targets[0] = address(_getAeraVault()); + + revenueDepositor.callTargets(targets, data); + + vm.expectEmit(true, true, true, true); + emit IPufferRevenueDepositor.RevenueDeposited(wethBalance); + revenueDepositor.depositRevenue(); + } + + function test_withdrawAndDeposit() public { + vm.startPrank(_getOPSMultisig()); + + uint256 wethBalance = IERC20(_getWETH()).balanceOf(_getAeraVault()); + + vm.expectEmit(true, true, true, true); + emit IPufferRevenueDepositor.RevenueDeposited(wethBalance); + revenueDepositor.withdrawAndDeposit(); + } + + // Deposit revenue from Aera Vault to Puffer Vault + function test_deposit_weth_to_puffer_vault() public { + vm.startPrank(_getOPSMultisig()); + + address[] memory targets = new address[](2); + targets[0] = address(_getAeraVault()); + targets[1] = address(revenueDepositor); + + uint256 wethBalance = IERC20(_getWETH()).balanceOf(_getAeraVault()); + + uint256 vaultWethBalance = IERC20(_getWETH()).balanceOf(address(pufferVault)); + uint256 pufferVaultAssetsBefore = pufferVault.totalAssets(); + + assertEq(pufferVaultAssetsBefore, 317212543571614106164392, "Puffer Vault Assets before"); + assertEq(vaultWethBalance, 0, "Puffer Vault should have 0 WETH before deposit"); + assertEq(wethBalance, 67.612355147076514123 ether, "Aera Vault should have 67.612355147076514123 WETH"); + + AssetValue[] memory assets = new AssetValue[](1); + assets[0] = AssetValue({ asset: IERC20(_getWETH()), value: wethBalance }); + + // 1. Withdraw WETH from Aera Vault -> Owner (Revenue Depositor) + // 2. Deposit WETH into Puffer Vault -> Revenue Depositor + bytes[] memory data = new bytes[](2); + data[0] = abi.encodeCall(IAeraVault(_getAeraVault()).withdraw, (assets)); + data[1] = abi.encodeCall(revenueDepositor.depositRevenue, ()); + + vm.expectEmit(true, true, true, true); + emit IPufferRevenueDepositor.RevenueDeposited(wethBalance); + revenueDepositor.callTargets(targets, data); + + uint256 wethBalanceAfter = IERC20(_getWETH()).balanceOf(_getAeraVault()); + assertEq(wethBalanceAfter, 0, "Aera Vault should have 0 WETH after deposit"); + + assertEq( + IERC20(_getWETH()).balanceOf(address(pufferVault)), + vaultWethBalance + wethBalance, + "Puffer Vault received WETH" + ); + + // Assets after in the same block are the same + assertEq(pufferVault.totalAssets(), pufferVaultAssetsBefore, "Puffer Vault total assets after"); + + vm.warp(block.timestamp + REWARDS_DISTRIBUTION_WINDOW); + + // Everything is deposited + assertEq( + pufferVault.totalAssets(), + pufferVaultAssetsBefore + wethBalance, + "Puffer Vault total assets after everything is deposited" + ); + } +} diff --git a/mainnet-contracts/test/mocks/MockAeraVault.sol b/mainnet-contracts/test/mocks/MockAeraVault.sol new file mode 100644 index 00000000..efa74d51 --- /dev/null +++ b/mainnet-contracts/test/mocks/MockAeraVault.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import { IAeraVault, AssetValue } from "src/interface/Other/IAeraVault.sol"; + +contract MockAeraVault is IAeraVault { + function deposit(AssetValue[] memory amounts) external { } + + function withdraw(AssetValue[] memory amounts) external { } + + function setGuardianAndFeeRecipient(address, address) external { } + + function setHooks(address hooks) external { } + + function finalize() external { } + + function pause() external { } + + function resume() external { } + + function claim() external { } + + function guardian() external view returns (address) { } + + function feeRecipient() external view returns (address) { } + + function fee() external view returns (uint256) { } + + function holdings() external view returns (AssetValue[] memory) { } + + function value() external view returns (uint256) { } +} diff --git a/mainnet-contracts/test/unit/PufferRevenueDepositorTest.t.sol b/mainnet-contracts/test/unit/PufferRevenueDepositorTest.t.sol index dfe42eb8..8d63166d 100644 --- a/mainnet-contracts/test/unit/PufferRevenueDepositorTest.t.sol +++ b/mainnet-contracts/test/unit/PufferRevenueDepositorTest.t.sol @@ -3,7 +3,24 @@ pragma solidity >=0.8.0 <0.9.0; import { UnitTestHelper } from "../helpers/UnitTestHelper.sol"; import { IPufferRevenueDepositor } from "src/interface/IPufferRevenueDepositor.sol"; -import { InvalidAddress } from "src/Errors.sol"; +import { IWETH } from "src/interface/IWETH.sol"; +import { ROLE_ID_REVENUE_DEPOSITOR } from "../../script/Roles.sol"; + +contract AeraVaultMock { + IWETH public immutable WETH; + address public immutable REVENUE_DEPOSITOR; + + constructor(address weth, address revenueDepositor) { + WETH = IWETH(weth); + REVENUE_DEPOSITOR = revenueDepositor; + } + + // Super simplified vault mock that withdraws only WETH + function withdraw(uint256 amount) external { + require(msg.sender == REVENUE_DEPOSITOR, "Only revenue depositor (owner) can withdraw"); + WETH.transfer(msg.sender, amount); + } +} /** * @title PufferRevenueDepositorTest @@ -13,6 +30,26 @@ import { InvalidAddress } from "src/Errors.sol"; * forge test --mc PufferRevenueDepositorTest -vvvv */ contract PufferRevenueDepositorTest is UnitTestHelper { + AeraVaultMock public aeraVault; + + function setUp() public override { + super.setUp(); + + aeraVault = new AeraVaultMock(address(weth), address(revenueDepositor)); + // Deposit 1000 WETH to the AeraVault + deal(address(weth), address(aeraVault), 1000 ether); + + vm.prank(address(timelock)); + // Grant the revenue depositor role to the revenue depositor itesels so that we can use callTargets to withdraw & deposit in 1 tx + accessManager.grantRole(ROLE_ID_REVENUE_DEPOSITOR, address(revenueDepositor), 0); + } + + function test_setup() public view { + assertEq(address(aeraVault.WETH()), address(weth), "WETH should be the same"); + assertEq(weth.balanceOf(address(aeraVault)), 1000 ether, "AeraVault should have 1000 WETH"); + assertEq(aeraVault.REVENUE_DEPOSITOR(), address(revenueDepositor), "Revenue depositor should be the same"); + } + /** * @dev Modifier to set the rewards distribution window for the test */ @@ -24,10 +61,6 @@ contract PufferRevenueDepositorTest is UnitTestHelper { } function test_sanity() public view { - assertEq(revenueDepositor.getRnoRewardsBps(), 400, "RNO rewards bps should be 400"); - assertEq(revenueDepositor.getTreasuryRewardsBps(), 500, "Treasury rewards bps should be 500"); - assertEq(revenueDepositor.getRestakingOperators().length, 7, "Should have 7 restaking operators"); - assertTrue(revenueDepositor.TREASURY() != address(0), "Treasury should not be 0"); assertTrue(address(revenueDepositor.WETH()) != address(0), "WETH should not be 0"); assertTrue(address(revenueDepositor.PUFFER_VAULT()) != address(0), "PufferVault should not be 0"); } @@ -68,20 +101,14 @@ contract PufferRevenueDepositorTest is UnitTestHelper { vm.startPrank(OPERATIONS_MULTISIG); - vm.expectRevert(IPufferRevenueDepositor.NothingToDistribute.selector); - revenueDepositor.depositRevenue(); - - vm.deal(address(revenueDepositor), 20); // 20 wei revenueDepositor.depositRevenue(); - // 1 wei went to the treasury - assertEq(revenueDepositor.getPendingDistributionAmount(), 19, "Pending distribution amount should be 19"); + assertEq(revenueDepositor.getPendingDistributionAmount(), 1, "Pending distribution amount should be 1"); // After half of the distribution window, the pending distribution amount is half of the total amount vm.warp(block.timestamp + 12 hours); - // 19/2 = 9.5, rounded down to 9 - assertEq(revenueDepositor.getPendingDistributionAmount(), 9, "Pending distribution amount should be 9"); + assertEq(revenueDepositor.getPendingDistributionAmount(), 0, "Pending distribution amount should be 0"); } function test_distributeRewards() public withRewardsDistributionWindow(1 days) { @@ -96,34 +123,23 @@ contract PufferRevenueDepositorTest is UnitTestHelper { // 4 Wei precision loss // Right away, the pending distribution amount is the amount deposited to the Vault - assertApproxEqAbs( - revenueDepositor.getPendingDistributionAmount(), - 91 ether, - 4, - "Pending distribution amount should be the amount deposited" + assertEq( + revenueDepositor.getPendingDistributionAmount(), 100 ether, "Pending distribution amount should be 100 ETH" ); assertEq(pufferVault.totalAssets(), totalAssetsBefore, "PufferVault should have the same total assets"); vm.warp(block.timestamp + 1 days); - assertApproxEqAbs( - pufferVault.totalAssets(), totalAssetsBefore + 91 ether, 4, "PufferVault should have +91 ether assets" - ); + assertEq(pufferVault.totalAssets(), totalAssetsBefore + 100 ether, "PufferVault should have +100 ether assets"); // After the distribution window, the pending distribution amount is 0 - assertApproxEqAbs( - revenueDepositor.getPendingDistributionAmount(), 0, 4, "Pending distribution amount should be 0" - ); + assertEq(revenueDepositor.getPendingDistributionAmount(), 0, "Pending distribution amount should be 0"); vm.warp(block.timestamp + 10 days); - assertApproxEqAbs( - pufferVault.totalAssets(), totalAssetsBefore + 91 ether, 4, "PufferVault should have +91 ether assets" - ); + assertEq(pufferVault.totalAssets(), totalAssetsBefore + 100 ether, "PufferVault should have +100 ether assets"); // After the distribution window, the pending distribution amount is 0 - assertApproxEqAbs( - revenueDepositor.getPendingDistributionAmount(), 0, 4, "Pending distribution amount should be 0" - ); + assertEq(revenueDepositor.getPendingDistributionAmount(), 0, "Pending distribution amount should be 0"); } function testRevert_nothingToDistribute() public { @@ -145,74 +161,6 @@ contract PufferRevenueDepositorTest is UnitTestHelper { revenueDepositor.depositRevenue(); } - function test_setRnoRewardsBps() public { - uint256 oldFeeBps = revenueDepositor.getRnoRewardsBps(); - uint256 newFeeBps = 100; - - vm.startPrank(DAO); - vm.expectEmit(true, true, true, true); - emit IPufferRevenueDepositor.RnoRewardsBpsChanged(oldFeeBps, newFeeBps); - revenueDepositor.setRnoRewardsBps(uint128(newFeeBps)); - } - - function test_setTreasuryRewardsBps() public { - uint256 oldFeeBps = revenueDepositor.getTreasuryRewardsBps(); - uint256 newFeeBps = 100; - - vm.startPrank(DAO); - vm.expectEmit(true, true, true, true); - emit IPufferRevenueDepositor.TreasuryRewardsBpsChanged(oldFeeBps, newFeeBps); - revenueDepositor.setTreasuryRewardsBps(uint128(newFeeBps)); - } - - function testRevert_setRnoRewardsBps_InvalidBps() public { - vm.startPrank(DAO); - vm.expectRevert(IPufferRevenueDepositor.InvalidBps.selector); - revenueDepositor.setRnoRewardsBps(uint128(1001)); - } - - function testRevert_setTreasuryRewardsBps_InvalidBps() public { - vm.startPrank(DAO); - vm.expectRevert(IPufferRevenueDepositor.InvalidBps.selector); - revenueDepositor.setTreasuryRewardsBps(uint128(2000)); - } - - function testRevert_removeRestakingOperator_NotOperator() public { - vm.startPrank(OPERATIONS_MULTISIG); - vm.expectRevert(IPufferRevenueDepositor.RestakingOperatorNotSet.selector); - revenueDepositor.removeRestakingOperator(address(1)); - } - - function testRevert_addRestakingOperators_InvalidOperatorZeroAddress() public { - vm.startPrank(OPERATIONS_MULTISIG); - - address[] memory operators = new address[](1); - operators[0] = address(0); - - vm.expectRevert(InvalidAddress.selector); - revenueDepositor.addRestakingOperators(operators); - } - - function test_addAndRemoveRestakingOperator() public { - vm.startPrank(OPERATIONS_MULTISIG); - - address newOperator = makeAddr("operator50"); - - address[] memory operators = new address[](1); - operators[0] = newOperator; - - vm.expectEmit(true, true, true, true); - emit IPufferRevenueDepositor.RestakingOperatorAdded(operators[0]); - revenueDepositor.addRestakingOperators(operators); - - vm.expectRevert(IPufferRevenueDepositor.RestakingOperatorAlreadySet.selector); - revenueDepositor.addRestakingOperators(operators); - - vm.expectEmit(true, true, true, true); - emit IPufferRevenueDepositor.RestakingOperatorRemoved(newOperator); - revenueDepositor.removeRestakingOperator(newOperator); - } - function test_depositRestakingRewardsInstantly() public { // Deposit WETH 100 ETH to the Depositor contract vm.deal(address(this), 100 ether); @@ -235,18 +183,23 @@ contract PufferRevenueDepositorTest is UnitTestHelper { revenueDepositor.getLastDepositTimestamp(), 1, "Last deposit timestamp should be the current timestamp 1" ); - // We have precision loss from the RNO rewards distribution, 4 wei is going to the Vault because of the rounding - assertApproxEqAbs( - pufferVault.totalAssets(), totalAssetsBefore + 91 ether, 4, "Total assets should be 91 ETH more" - ); + assertEq(pufferVault.totalAssets(), totalAssetsBefore + 100 ether, "Total assets should be 100 ETH more"); + } + + // Withdraw 100 WETH from AeraVault and deposit it into PufferVault in 1 tx + function test_callTargets() public { + vm.startPrank(OPERATIONS_MULTISIG); - assertEq(weth.balanceOf(address(revenueDepositor.TREASURY())), 5 ether, "Treasury should have 5 WETH"); - assertEq(weth.balanceOf(RNO1), 0.571428571428571428 ether, "RNO1 should have 0.571428571428571428 WETH"); - assertEq(weth.balanceOf(RNO2), 0.571428571428571428 ether, "RNO1 should have 0.571428571428571428 WETH"); - assertEq(weth.balanceOf(RNO3), 0.571428571428571428 ether, "RNO3 should have 0.571428571428571428 WETH"); - assertEq(weth.balanceOf(RNO4), 0.571428571428571428 ether, "RNO4 should have 0.571428571428571428 WETH"); - assertEq(weth.balanceOf(RNO5), 0.571428571428571428 ether, "RNO5 should have 0.571428571428571428 WETH"); - assertEq(weth.balanceOf(RNO6), 0.571428571428571428 ether, "RNO6 should have 0.571428571428571428 WETH"); - assertEq(weth.balanceOf(RNO7), 0.571428571428571428 ether, "RNO7 should have 0.571428571428571428 WETH"); + address[] memory targets = new address[](2); + targets[0] = address(aeraVault); + targets[1] = address(revenueDepositor); + + bytes[] memory data = new bytes[](2); + data[0] = abi.encodeCall(aeraVault.withdraw, (100 ether)); + data[1] = abi.encodeCall(revenueDepositor.depositRevenue, ()); + + vm.expectEmit(true, true, true, true); + emit IPufferRevenueDepositor.RevenueDeposited(100 ether); + revenueDepositor.callTargets(targets, data); } }