From 15affbcbdda07fd88b8e280ceadf451e9823ec19 Mon Sep 17 00:00:00 2001 From: Pablo Veyrat Date: Mon, 2 Dec 2024 10:20:28 +0100 Subject: [PATCH] feat: Puffer token wrapper --- LICENSE | 2 +- README.md | 86 +- contracts/DistributionCreator.sol | 6 +- contracts/Distributor.sol | 4 +- .../deprecated/OldDistributionCreator.sol | 756 ------------------ contracts/deprecated/OldDistributor.sol | 348 -------- contracts/interfaces/CoreModuleInterfaces.sol | 30 - .../{ICore.sol => IAccessControlManager.sol} | 4 +- contracts/interfaces/IAgToken.sol | 61 -- contracts/interfaces/ITreasury.sol | 29 - .../external/algebra/IAlgebraPool.sol | 32 - .../external/uniswap/IUniswapV3Pool.sol | 19 - contracts/mock/AngleDistributor.sol | 419 ---------- contracts/mock/AngleDistributorEvents.sol | 26 - .../mock/DistributionCreatorUpdatable.sol | 9 +- .../MockMerklFraxIncentivizationHandler.sol | 2 +- contracts/mock/MockMerklGaugeMiddleman.sol | 4 +- contracts/mock/MockTreasury.sol | 36 - .../MerklFraxIncentivizationHandler.sol | 2 +- .../middleman/MerklGaugeMiddleman.sol | 13 +- .../middleman/MerklGaugeMiddlemanPolygon.sol | 2 +- .../tokenWrappers/AaveTokenWrapper.sol | 10 +- .../tokenWrappers/BaseTokenWrapper.sol | 6 +- .../tokenWrappers/PufferPointTokenWrapper.sol | 106 +++ .../tokenWrappers/PullTokenWrapper.sol | 83 ++ .../tokenWrappers/StakedToken.sol | 0 contracts/struct/CampaignParameters.sol | 2 +- contracts/struct/DistributionParameters.sol | 2 +- .../tokenWrappers/RadiantTokenWrapper.sol | 63 -- contracts/utils/Errors.sol | 1 + contracts/utils/UUPSHelper.sol | 6 +- test/foundry/Fixture.t.sol | 4 +- test/foundry/unit/DistributionCreator.t.sol | 60 +- test/foundry/unit/Distributor.t.sol | 56 +- 34 files changed, 322 insertions(+), 1967 deletions(-) delete mode 100644 contracts/deprecated/OldDistributionCreator.sol delete mode 100644 contracts/deprecated/OldDistributor.sol delete mode 100644 contracts/interfaces/CoreModuleInterfaces.sol rename contracts/interfaces/{ICore.sol => IAccessControlManager.sol} (92%) delete mode 100644 contracts/interfaces/IAgToken.sol delete mode 100644 contracts/interfaces/ITreasury.sol delete mode 100644 contracts/interfaces/external/algebra/IAlgebraPool.sol delete mode 100644 contracts/interfaces/external/uniswap/IUniswapV3Pool.sol delete mode 100644 contracts/mock/AngleDistributor.sol delete mode 100644 contracts/mock/AngleDistributorEvents.sol delete mode 100644 contracts/mock/MockTreasury.sol rename contracts/{ => partners}/middleman/MerklFraxIncentivizationHandler.sol (99%) rename contracts/{ => partners}/middleman/MerklGaugeMiddleman.sol (93%) rename contracts/{ => partners}/middleman/MerklGaugeMiddlemanPolygon.sol (77%) rename contracts/{ => partners}/tokenWrappers/AaveTokenWrapper.sol (95%) rename contracts/{ => partners}/tokenWrappers/BaseTokenWrapper.sol (94%) create mode 100644 contracts/partners/tokenWrappers/PufferPointTokenWrapper.sol create mode 100644 contracts/partners/tokenWrappers/PullTokenWrapper.sol rename contracts/{ => partners}/tokenWrappers/StakedToken.sol (100%) delete mode 100644 contracts/tokenWrappers/RadiantTokenWrapper.sol diff --git a/LICENSE b/LICENSE index bdb6946..67471c4 100644 --- a/LICENSE +++ b/LICENSE @@ -10,7 +10,7 @@ Parameters Licensor: Angle Labs, Inc. Licensed Work: Merkl Smart Contracts -The Licensed Work is (c) 2023 Angle Labs, Inc. +The Licensed Work is (c) 2024 Angle Labs, Inc. Additional Use Grant: Any uses listed and defined at merkl-license-grants.angle-labs.eth diff --git a/README.md b/README.md index d88cf37..cca1b9f 100644 --- a/README.md +++ b/README.md @@ -1,96 +1,70 @@ -# Merkl Contracts Merkl Contracts - [![CI](https://github.com/AngleProtocol/merkl-contracts/actions/workflows/ci.yml/badge.svg)](https://github.com/AngleProtocol/merkl-contracts/actions) [![Coverage](https://codecov.io/gh/AngleProtocol/merkl-contracts/branch/main/graph/badge.svg)](https://codecov.io/gh/AngleProtocol/merkl-contracts) -This repository contains the smart contracts of the Merkl product developed by Angle. +This repository contains the smart contracts of Merkl. It basically contains two contracts: -- `DistributionCreator`: to which DAOs and individuals can deposit their rewards to incentivize a pool +- `DistributionCreator`: to which DAOs and individuals can deposit their rewards to incentivize onchain actions - `Distributor`: the contract where users can claim their rewards -You can learn more about the Merkl system in the [documentation](https://docs.angle.money/side-products/merkl). +You can learn more about the Merkl system in the [documentation](https://docs.merkl.xyz). ## Setup -### Install packages - -You can install all dependencies by running - -```bash -yarn -forge i -``` +@@ -25,7 +25,7 @@ forge i ### Create `.env` file -In order to interact with non local networks, you must create an `.env` that has, for all supported networks (Ethereum, Polygon and Arbitrum): +In order to interact with non local networks, you must create an `.env` that has, for all supported networks: - `MNEMONIC` - `ETH_NODE_URI` -- `ETHERSCAN_API_KEY` - -You can copy paste the `.env.example` file into `.env` and fill with your keys/RPCs. + @@ -84,18 +84,52 @@ forge update -Warning: always keep your confidential information safe. - -### Tests - -Contracts in this repo rely on Hardhat tests. You can run tests as follows: +## Verifying -```bash -# Whole test suite -yarn hardhat:test +Blast: -# Only one file -yarn hardhat:test ./test/hardhat/distributor/distributor.test.ts ``` - -You can also check the coverage of the tests with: - -```bash -yarn hardhat:coverage +yarn etherscan blast --api-url https://api.blastscan.io --solc-input --license BUSL-1.1 ``` -### Deploying +Mantle: -```bash -yarn deploy mainnet +``` +yarn etherscan mantle --api-url https://api.routescan.io/v2/network/mainnet/evm/43114/etherscan/api --solc-input --license BUSL-1.1 ``` -## Foundry Installation +Mode: -```bash -curl -L https://foundry.paradigm.xyz | bash +``` +yarn etherscan mode --api-url https://api.routescan.io/v2/network/mainnet/evm/34443/etherscan/api --solc-input --license BUSL-1.1 +``` -source /root/.zshrc -# or, if you're under bash: source /root/.bashrc +ImmutableZKEVM: -foundryup +``` +yarn etherscan immutablezkevm --api-url https://explorer.immutable.com/api --solc-input --license BUSL-1.1 ``` -To install the standard library: +Scroll: -```bash -forge install foundry-rs/forge-std +``` +yarn etherscan scroll --api-url https://api.scrollscan.com --solc-input --license BUSL-1.1 ``` -To update libraries: +Gnosis: -```bash -forge update +``` +yarn etherscan gnosis --api-url https://api.gnosisscan.io --solc-input --license BUSL-1.1 ``` -## Verifying +Linea: -Blast: `yarn etherscan blast --api-url https://api.blastscan.io --solc-input --license BUSL-1.1` -Mantle: `yarn etherscan mantle --api-url https://api.routescan.io/v2/network/mainnet/evm/43114/etherscan/api --solc-input --license BUSL-1.1` -Mode: `yarn etherscan mode --api-url https://api.routescan.io/v2/network/mainnet/evm/34443/etherscan/api --solc-input --license BUSL-1.1` -ImmutableZKEVM: `yarn etherscan immutablezkevm --api-url https://explorer.immutable.com/api --solc-input --license BUSL-1.1` -Scroll:`yarn etherscan scroll --api-url https://api.scrollscan.com --solc-input --license BUSL-1.1` -Gnosis:`yarn etherscan gnosis --api-url https://api.gnosisscan.io --solc-input --license BUSL-1.1` -Linea:`yarn etherscan linea --api-url https://api.lineascan.build --solc-input --license BUSL-1.1` +``` +yarn etherscan linea --api-url https://api.lineascan.build --solc-input --license BUSL-1.1 +``` ## Audits @@ -98,4 +72,4 @@ The Merkl smart contracts have been audited by Code4rena, find the audit report ## Media -Don't hesitate to reach out on [Twitter](https://twitter.com/AngleProtocol) 🐦 +Don't hesitate to reach out on [Twitter](https://x.com/merkl_xyz) diff --git a/contracts/DistributionCreator.sol b/contracts/DistributionCreator.sol index c73ad64..2f4e0b2 100644 --- a/contracts/DistributionCreator.sol +++ b/contracts/DistributionCreator.sol @@ -41,8 +41,6 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; -import { IUniswapV3Pool } from "./interfaces/external/uniswap/IUniswapV3Pool.sol"; - import "./utils/UUPSHelper.sol"; import { CampaignParameters } from "./struct/CampaignParameters.sol"; import { DistributionParameters } from "./struct/DistributionParameters.sol"; @@ -72,7 +70,7 @@ contract DistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable { uint256 public immutable CHAIN_ID = block.chainid; /// @notice `Core` contract handling access control - ICore public core; + IAccessControlManager public core; /// @notice Contract distributing rewards to users address public distributor; @@ -175,7 +173,7 @@ contract DistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable { CONSTRUCTOR //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - function initialize(ICore _core, address _distributor, uint256 _fees) external initializer { + function initialize(IAccessControlManager _core, address _distributor, uint256 _fees) external initializer { if (address(_core) == address(0) || _distributor == address(0)) revert ZeroAddress(); if (_fees >= BASE_9) revert InvalidParam(); distributor = _distributor; diff --git a/contracts/Distributor.sol b/contracts/Distributor.sol index 145893b..2e33cab 100644 --- a/contracts/Distributor.sol +++ b/contracts/Distributor.sol @@ -78,7 +78,7 @@ contract Distributor is UUPSHelper { IERC20 public disputeToken; /// @notice `Core` contract handling access control - ICore public core; + IAccessControlManager public core; /// @notice Address which created the dispute /// @dev Used to store if there is an ongoing dispute @@ -147,7 +147,7 @@ contract Distributor is UUPSHelper { constructor() initializer {} - function initialize(ICore _core) external initializer { + function initialize(IAccessControlManager _core) external initializer { if (address(_core) == address(0)) revert ZeroAddress(); core = _core; } diff --git a/contracts/deprecated/OldDistributionCreator.sol b/contracts/deprecated/OldDistributionCreator.sol deleted file mode 100644 index 47745a7..0000000 --- a/contracts/deprecated/OldDistributionCreator.sol +++ /dev/null @@ -1,756 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -/* - * █ - ***** ▓▓▓ - * ▓▓▓▓▓▓▓ - * ///. ▓▓▓▓▓▓▓▓▓▓▓▓▓ - ***** //////// ▓▓▓▓▓▓▓ - * ///////////// ▓▓▓ - ▓▓ ////////////////// █ ▓▓ - ▓▓ ▓▓ /////////////////////// ▓▓ ▓▓ - ▓▓ ▓▓ //////////////////////////// ▓▓ ▓▓ - ▓▓ ▓▓ /////////▓▓▓///////▓▓▓///////// ▓▓ ▓▓ - ▓▓ ,////////////////////////////////////// ▓▓ ▓▓ - ▓▓ ////////////////////////////////////////// ▓▓ - ▓▓ //////////////////////▓▓▓▓///////////////////// - ,//////////////////////////////////////////////////// - .////////////////////////////////////////////////////////// - .//////////////////////////██.,//////////////////////////█ - .//////////////////////████..,./////////////////////██ - ...////////////////███████.....,.////////////////███ - ,.,////////////████████ ........,///////////████ - .,.,//////█████████ ,.......///////████ - ,..//████████ ........./████ - ..,██████ .....,███ - .██ ,.,█ - - - - ▓▓ ▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓ ▓▓ ▓▓▓▓▓▓▓▓▓▓ - ▓▓▓▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓ ▓▓ ▓▓ ▓▓▓▓ - ▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓ ▓▓ ▓▓▓▓▓ - ▓▓▓ ▓▓ ▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓ -*/ - -pragma solidity ^0.8.17; - -import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; -import { IERC20, IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; - -import "../interfaces/external/uniswap/IUniswapV3Pool.sol"; -import "../interfaces/external/algebra/IAlgebraPool.sol"; -import "../utils/UUPSHelper.sol"; -import "../struct/DistributionParameters.sol"; -import "../struct/ExtensiveDistributionParameters.sol"; -import "../struct/RewardTokenAmounts.sol"; - -interface IDistributionCreator { - function tryGetExtensiveDistributionParameters( - DistributionParameters memory distribution - ) external view returns (bool success, ExtensiveDistributionParameters memory extensiveParams); -} - -/// @title DistributionCreator -/// @author Angle Labs, Inc. -/// @notice Manages the distribution of rewards across different pools with concentrated liquidity (like on Uniswap V3) -/// @dev This contract is mostly a helper for APIs built on top of Merkl -/// @dev People depositing rewards must have signed a `message` with the conditions for using the -/// product -//solhint-disable -contract OldDistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable { - using SafeERC20 for IERC20; - - // =========================== CONSTANTS / VARIABLES =========================== - - /// @notice Epoch duration - uint32 public constant EPOCH_DURATION = 3600; - - /// @notice Base for fee computation - uint256 public constant BASE_9 = 1e9; - - /// @notice `Core` contract handling access control - ICore public core; - - /// @notice User contract for distributing rewards - address public distributor; - - /// @notice Address to which fees are forwarded - address public feeRecipient; - - /// @notice Value (in base 10**9) of the fees taken when creating a distribution for a pool which do not - /// have a whitelisted token in it - uint256 public fees; - - /// @notice Message that needs to be acknowledged by users creating a distribution - string public message; - - /// @notice Hash of the message that needs to be signed - bytes32 public messageHash; - - /// @notice List of all rewards ever distributed or to be distributed in the contract - /// @dev An attacker could try to populate this list. It shouldn't be an issue as only view functions - /// iterate on it - DistributionParameters[] public distributionList; - - /// @notice Maps an address to its fee rebate - mapping(address => uint256) public feeRebate; - - /// @notice Maps a token to whether it is whitelisted or not. No fees are to be paid for incentives given - /// on pools with whitelisted tokens - mapping(address => uint256) public isWhitelistedToken; - - /// @notice Maps an address to its nonce for creating a distribution - mapping(address => uint256) public nonces; - - /// @notice Maps an address to the last valid hash signed - mapping(address => bytes32) public userSignatures; - - /// @notice Maps a user to whether it is whitelisted for not signing - mapping(address => uint256) public userSignatureWhitelist; - - /// @notice Maps a token to the minimum amount that must be sent per epoch for a distribution to be valid - /// @dev If `rewardTokenMinAmounts[token] == 0`, then `token` cannot be used as a reward - mapping(address => uint256) public rewardTokenMinAmounts; - - /// @notice List of all reward tokens that have at some point been accepted - address[] public rewardTokens; - - uint256[36] private __gap; - - // =================================== EVENTS ================================== - - event DistributorUpdated(address indexed _distributor); - event FeeRebateUpdated(address indexed user, uint256 userFeeRebate); - event FeeRecipientUpdated(address indexed _feeRecipient); - event FeesSet(uint256 _fees); - event MessageUpdated(bytes32 _messageHash); - event NewDistribution(DistributionParameters distribution, address indexed sender); - event RewardTokenMinimumAmountUpdated(address indexed token, uint256 amount); - event TokenWhitelistToggled(address indexed token, uint256 toggleStatus); - event UserSigned(bytes32 messageHash, address indexed user); - event UserSigningWhitelistToggled(address indexed user, uint256 toggleStatus); - - // ================================= MODIFIERS ================================= - - /// @notice Checks whether the `msg.sender` has the governor role or the guardian role - modifier onlyGovernorOrGuardian() { - if (!core.isGovernorOrGuardian(msg.sender)) revert NotGovernorOrGuardian(); - _; - } - - /// @notice Checks whether an address has signed the message or not - modifier hasSigned() { - if ( - userSignatureWhitelist[msg.sender] == 0 && - userSignatures[msg.sender] != messageHash && - userSignatureWhitelist[tx.origin] == 0 && - userSignatures[tx.origin] != messageHash - ) revert NotSigned(); - _; - } - - // ================================ CONSTRUCTOR ================================ - - function initialize(ICore _core, address _distributor, uint256 _fees) external initializer { - if (address(_core) == address(0) || _distributor == address(0)) revert ZeroAddress(); - if (_fees >= BASE_9) revert InvalidParam(); - distributor = _distributor; - core = _core; - fees = _fees; - } - - constructor() initializer {} - - /// @inheritdoc UUPSUpgradeable - function _authorizeUpgrade(address) internal view override onlyGuardianUpgrader(core) {} - - // ============================== DEPOSIT FUNCTION ============================= - - /// @notice Creates a `distribution` to incentivize a given pool for a specific period of time - /// @return distributionAmount How many reward tokens are actually taken into consideration in the contract - /// @dev If the address specified as a UniV3 pool is not effectively a pool, it will not be handled by the - /// distribution script and rewards may be lost - /// @dev Reward tokens sent as part of distributions must have been whitelisted before and amounts - /// sent should be bigger than a minimum amount specific to each token - /// @dev The `positionWrappers` specified in the `distribution` struct need to be supported by the script - /// List of supported `positionWrappers` can be found in the docs. - /// @dev If the pool incentivized contains one whitelisted token, then no fees are taken on the rewards - /// @dev This function reverts if the sender has not signed the message `messageHash` once through one of - /// the functions enabling to sign - function createDistribution( - DistributionParameters memory distribution - ) external hasSigned returns (uint256 distributionAmount) { - return _createDistribution(distribution); - } - - /// @notice Same as the function above but for multiple distributions at once - /// @return List of all the distribution amounts actually deposited for each `distribution` in the `distributions` list - function createDistributions( - DistributionParameters[] memory distributions - ) external hasSigned returns (uint256[] memory) { - uint256 distributionsLength = distributions.length; - uint256[] memory distributionAmounts = new uint256[](distributionsLength); - for (uint256 i; i < distributionsLength; ) { - distributionAmounts[i] = _createDistribution(distributions[i]); - unchecked { - ++i; - } - } - return distributionAmounts; - } - - /// @notice Checks whether the `msg.sender`'s `signature` is compatible with the message - /// to sign and stores the signature - /// @dev If you signed the message once, and the message has not been modified, then you do not - /// need to sign again - function sign(bytes calldata signature) external { - _sign(signature); - } - - /// @notice Combines signing the message and creating a distribution - function signAndCreateDistribution( - DistributionParameters memory distribution, - bytes calldata signature - ) external returns (uint256 distributionAmount) { - _sign(signature); - return _createDistribution(distribution); - } - - /// @notice Internal version of `createDistribution` - function _createDistribution( - DistributionParameters memory distribution - ) internal nonReentrant returns (uint256 distributionAmount) { - uint32 epochStart = _getRoundedEpoch(distribution.epochStart); - uint256 minDistributionAmount = rewardTokenMinAmounts[distribution.rewardToken]; - distribution.epochStart = epochStart; - // Reward are not accepted in the following conditions: - if ( - // if epoch parameters lead to a past distribution - epochStart + EPOCH_DURATION < block.timestamp || - // if the amount of epochs for which this distribution should last is zero - distribution.numEpoch == 0 || - // if the distribution parameters are not correctly specified - distribution.propFees + distribution.propToken0 + distribution.propToken1 != 1e4 || - // if boosted addresses get less than non-boosted addresses in case of - (distribution.boostingAddress != address(0) && distribution.boostedReward < 1e4) || - // if the type of the position wrappers is not well specified - distribution.positionWrappers.length != distribution.wrapperTypes.length || - // if the reward token is not whitelisted as an incentive token - minDistributionAmount == 0 || - // if the amount distributed is too small with respect to what is allowed - distribution.amount / distribution.numEpoch < minDistributionAmount - ) revert InvalidReward(); - distributionAmount = distribution.amount; - // Computing fees: these are waived for whitelisted addresses and if there is a whitelisted token in a pool - uint256 userFeeRebate = feeRebate[msg.sender]; - if ( - userFeeRebate < BASE_9 && - // Algebra pools also have these `token0` and `token1` parameters - isWhitelistedToken[IUniswapV3Pool(distribution.uniV3Pool).token0()] == 0 && - isWhitelistedToken[IUniswapV3Pool(distribution.uniV3Pool).token1()] == 0 - ) { - uint256 _fees = (fees * (BASE_9 - userFeeRebate)) / BASE_9; - uint256 distributionAmountMinusFees = (distributionAmount * (BASE_9 - _fees)) / BASE_9; - address _feeRecipient = feeRecipient; - _feeRecipient = _feeRecipient == address(0) ? address(this) : _feeRecipient; - IERC20(distribution.rewardToken).safeTransferFrom( - msg.sender, - _feeRecipient, - distributionAmount - distributionAmountMinusFees - ); - distributionAmount = distributionAmountMinusFees; - distribution.amount = distributionAmount; - } - - IERC20(distribution.rewardToken).safeTransferFrom(msg.sender, distributor, distributionAmount); - uint256 senderNonce = nonces[msg.sender]; - nonces[msg.sender] = senderNonce + 1; - distribution.rewardId = bytes32(keccak256(abi.encodePacked(msg.sender, senderNonce))); - distributionList.push(distribution); - emit NewDistribution(distribution, msg.sender); - } - - /// @notice Internal version of the `sign` function - function _sign(bytes calldata signature) internal { - bytes32 _messageHash = messageHash; - if (ECDSA.recover(_messageHash, signature) != msg.sender) revert InvalidSignature(); - userSignatures[msg.sender] = _messageHash; - emit UserSigned(_messageHash, msg.sender); - } - - // ================================= UI HELPERS ================================ - // These functions are not to be queried on-chain and hence are not optimized for gas consumption - - /// @notice Returns the list of all distributions ever made or to be done in the future - function getAllDistributions() external view returns (DistributionParameters[] memory) { - return distributionList; - } - - /// @notice Returns the list of all currently active distributions on pools of supported AMMs (like Uniswap V3) - function getActiveDistributions() - external - view - returns (ExtensiveDistributionParameters[] memory searchDistributions) - { - uint32 roundedEpoch = _getRoundedEpoch(uint32(block.timestamp)); - (searchDistributions, ) = _getPoolDistributionsBetweenEpochs( - address(0), - roundedEpoch, - roundedEpoch + EPOCH_DURATION, - 0, - type(uint32).max - ); - } - - /// @notice Similar to `getActiveDistributions()` with additional parameters to prevent out of gas error - /// @param skip Disregard distibutions with a global index lower than `skip` - /// @param first Limit the length of the returned array to `first` - /// @return searchDistributions Eligible distributions - /// @return lastIndexDistribution Index of the last distribution assessed in the list of all distributions - /// For pagniation purpose, in case of out of gas, you can call back the same function but with `skip` set to `lastIndexDistribution` - function getActiveDistributions( - uint32 skip, - uint32 first - ) - external - view - returns (ExtensiveDistributionParameters[] memory searchDistributions, uint256 lastIndexDistribution) - { - uint32 roundedEpoch = _getRoundedEpoch(uint32(block.timestamp)); - return _getPoolDistributionsBetweenEpochs(address(0), roundedEpoch, roundedEpoch + EPOCH_DURATION, skip, first); - } - - /// @notice Returns the list of all the reward tokens supported as well as their minimum amounts - function getValidRewardTokens() external view returns (RewardTokenAmounts[] memory) { - uint256 length; - uint256 rewardTokenListLength = rewardTokens.length; - RewardTokenAmounts[] memory validRewardTokens = new RewardTokenAmounts[](rewardTokenListLength); - for (uint32 i; i < rewardTokenListLength; ) { - address token = rewardTokens[i]; - uint256 minAmount = rewardTokenMinAmounts[token]; - if (minAmount > 0) { - validRewardTokens[length] = RewardTokenAmounts(token, minAmount); - length += 1; - } - unchecked { - ++i; - } - } - assembly { - mstore(validRewardTokens, length) - } - return validRewardTokens; - } - - /// @notice Returns the list of all the distributions that were or that are going to be live at - /// a specific epoch - function getDistributionsForEpoch( - uint32 epoch - ) external view returns (ExtensiveDistributionParameters[] memory searchDistributions) { - uint32 roundedEpoch = _getRoundedEpoch(epoch); - (searchDistributions, ) = _getPoolDistributionsBetweenEpochs( - address(0), - roundedEpoch, - roundedEpoch + EPOCH_DURATION, - 0, - type(uint32).max - ); - } - - /// @notice Similar to `getDistributionsForEpoch(uint256 epoch)` with additional parameters to prevent out of gas error - /// @param skip Disregard distibutions with a global index lower than `skip` - /// @param first Limit the length of the returned array to `first` - /// @return searchDistributions Eligible distributions - /// @return lastIndexDistribution Index of the last distribution assessed in the list of all distributions - /// For pagniation purpose, in case of out of gas, you can call back the same function but with `skip` set to `lastIndexDistribution` - function getDistributionsForEpoch( - uint32 epoch, - uint32 skip, - uint32 first - ) external view returns (ExtensiveDistributionParameters[] memory, uint256 lastIndexDistribution) { - uint32 roundedEpoch = _getRoundedEpoch(epoch); - return _getPoolDistributionsBetweenEpochs(address(0), roundedEpoch, roundedEpoch + EPOCH_DURATION, skip, first); - } - - /// @notice Gets the distributions that were or will be live at some point between `epochStart` (included) and `epochEnd` (excluded) - /// @dev If a distribution starts during `epochEnd`, it is not be returned by this function - /// @dev Conversely, if a distribution starts after `epochStart` and ends before `epochEnd`, it is returned by this function - function getDistributionsBetweenEpochs( - uint32 epochStart, - uint32 epochEnd - ) external view returns (ExtensiveDistributionParameters[] memory searchDistributions) { - (searchDistributions, ) = _getPoolDistributionsBetweenEpochs( - address(0), - _getRoundedEpoch(epochStart), - _getRoundedEpoch(epochEnd), - 0, - type(uint32).max - ); - } - - /// @notice Similar to `getDistributionsBetweenEpochs(uint256 epochStart, uint256 epochEnd)` with additional parameters to prevent out of gas error - /// @param skip Disregard distibutions with a global index lower than `skip` - /// @param first Limit the length of the returned array to `first` - /// @return searchDistributions Eligible distributions - /// @return lastIndexDistribution Index of the last distribution assessed in the list of all distributions - /// For pagniation purpose, in case of out of gas, you can call back the same function but with `skip` set to `lastIndexDistribution` - function getDistributionsBetweenEpochs( - uint32 epochStart, - uint32 epochEnd, - uint32 skip, - uint32 first - ) external view returns (ExtensiveDistributionParameters[] memory, uint256 lastIndexDistribution) { - return - _getPoolDistributionsBetweenEpochs( - address(0), - _getRoundedEpoch(epochStart), - _getRoundedEpoch(epochEnd), - skip, - first - ); - } - - /// @notice Returns the list of all distributions that were or will be live after `epochStart` (included) - function getDistributionsAfterEpoch( - uint32 epochStart - ) external view returns (ExtensiveDistributionParameters[] memory searchDistributions) { - (searchDistributions, ) = _getPoolDistributionsBetweenEpochs( - address(0), - _getRoundedEpoch(epochStart), - type(uint32).max, - 0, - type(uint32).max - ); - } - - /// @notice Similar to `getDistributionsAfterEpoch(uint256 epochStart)` with additional parameters to prevent out of gas error - /// @param skip Disregard distibutions with a global index lower than `skip` - /// @param first Limit the length of the returned array to `first` - /// @return searchDistributions Eligible distributions - /// @return lastIndexDistribution Index of the last distribution assessed in the list of all distributions - /// For pagniation purpose, in case of out of gas, you can call back the same function but with `skip` set to `lastIndexDistribution` - function getDistributionsAfterEpoch( - uint32 epochStart, - uint32 skip, - uint32 first - ) external view returns (ExtensiveDistributionParameters[] memory, uint256 lastIndexDistribution) { - return - _getPoolDistributionsBetweenEpochs(address(0), _getRoundedEpoch(epochStart), type(uint32).max, skip, first); - } - - /// @notice Returns the list of all currently active distributions for a specific UniswapV3 pool - function getActivePoolDistributions( - address uniV3Pool - ) external view returns (ExtensiveDistributionParameters[] memory searchDistributions) { - uint32 roundedEpoch = _getRoundedEpoch(uint32(block.timestamp)); - (searchDistributions, ) = _getPoolDistributionsBetweenEpochs( - uniV3Pool, - roundedEpoch, - roundedEpoch + EPOCH_DURATION, - 0, - type(uint32).max - ); - } - - /// @notice Similar to `getActivePoolDistributions(address uniV3Pool)` with additional parameters to prevent out of gas error - /// @param skip Disregard distibutions with a global index lower than `skip` - /// @param first Limit the length of the returned array to `first` - /// @return searchDistributions Eligible distributions - /// @return lastIndexDistribution Index of the last distribution assessed in the list of all distributions - /// For pagniation purpose, in case of out of gas, you can call back the same function but with `skip` set to `lastIndexDistribution` - function getActivePoolDistributions( - address uniV3Pool, - uint32 skip, - uint32 first - ) external view returns (ExtensiveDistributionParameters[] memory, uint256 lastIndexDistribution) { - uint32 roundedEpoch = _getRoundedEpoch(uint32(block.timestamp)); - return _getPoolDistributionsBetweenEpochs(uniV3Pool, roundedEpoch, roundedEpoch + EPOCH_DURATION, skip, first); - } - - /// @notice Returns the list of all the distributions that were or that are going to be live at a - /// specific epoch and for a specific pool - function getPoolDistributionsForEpoch( - address uniV3Pool, - uint32 epoch - ) external view returns (ExtensiveDistributionParameters[] memory searchDistributions) { - uint32 roundedEpoch = _getRoundedEpoch(epoch); - (searchDistributions, ) = _getPoolDistributionsBetweenEpochs( - uniV3Pool, - roundedEpoch, - roundedEpoch + EPOCH_DURATION, - 0, - type(uint32).max - ); - } - - /// @notice Similar to `getPoolDistributionsForEpoch(address uniV3Pool,uint32 epoch)` with additional parameters to prevent out of gas error - /// @param skip Disregard distibutions with a global index lower than `skip` - /// @param first Limit the length of the returned array to `first` - /// @return searchDistributions Eligible distributions - /// @return lastIndexDistribution Index of the last distribution assessed in the list of all distributions - /// For pagniation purpose, in case of out of gas, you can call back the same function but with `skip` set to `lastIndexDistribution` - function getPoolDistributionsForEpoch( - address uniV3Pool, - uint32 epoch, - uint32 skip, - uint32 first - ) external view returns (ExtensiveDistributionParameters[] memory, uint256 lastIndexDistribution) { - uint32 roundedEpoch = _getRoundedEpoch(epoch); - return _getPoolDistributionsBetweenEpochs(uniV3Pool, roundedEpoch, roundedEpoch + EPOCH_DURATION, skip, first); - } - - /// @notice Returns the list of all distributions that were or will be live at some point between - /// `epochStart` (included) and `epochEnd` (excluded) for a specific pool - function getPoolDistributionsBetweenEpochs( - address uniV3Pool, - uint32 epochStart, - uint32 epochEnd - ) external view returns (ExtensiveDistributionParameters[] memory searchDistributions) { - (searchDistributions, ) = _getPoolDistributionsBetweenEpochs( - uniV3Pool, - _getRoundedEpoch(epochStart), - _getRoundedEpoch(epochEnd), - 0, - type(uint32).max - ); - } - - /// @notice Similar to `getPoolDistributionsBetweenEpochs(address uniV3Pool,uint32 epochStart, uint32 epochEnd)` with additional parameters to prevent out of gas error - /// @param skip Disregard distibutions with a global index lower than `skip` - /// @param first Limit the length of the returned array to `first` - /// @return searchDistributions Eligible distributions - /// @return lastIndexDistribution Index of the last distribution assessed in the list of all distributions - /// For pagniation purpose, in case of out of gas, you can call back the same function but with `skip` set to `lastIndexDistribution` - function getPoolDistributionsBetweenEpochs( - address uniV3Pool, - uint32 epochStart, - uint32 epochEnd, - uint32 skip, - uint32 first - ) external view returns (ExtensiveDistributionParameters[] memory, uint256 lastIndexDistribution) { - return - _getPoolDistributionsBetweenEpochs( - uniV3Pool, - _getRoundedEpoch(epochStart), - _getRoundedEpoch(epochEnd), - skip, - first - ); - } - - /// @notice Returns the list of all distributions that were or will be live after `epochStart` (included) - /// for a specific pool - function getPoolDistributionsAfterEpoch( - address uniV3Pool, - uint32 epochStart - ) external view returns (ExtensiveDistributionParameters[] memory searchDistributions) { - (searchDistributions, ) = _getPoolDistributionsBetweenEpochs( - uniV3Pool, - _getRoundedEpoch(epochStart), - type(uint32).max, - 0, - type(uint32).max - ); - } - - /// @notice Similar to `getPoolDistributionsAfterEpoch(address uniV3Pool,uint32 epochStart)` with additional parameters to prevent out of gas error - /// @param skip Disregard distibutions with a global index lower than `skip` - /// @param first Limit the length of the returned array to `first` - /// @return searchDistributions Eligible distributions - /// @return lastIndexDistribution Index of the last distribution assessed in the list of all distributions - /// For pagniation purpose, in case of out of gas, you can call back the same function but with `skip` set to `lastIndexDistribution` - function getPoolDistributionsAfterEpoch( - address uniV3Pool, - uint32 epochStart, - uint32 skip, - uint32 first - ) external view returns (ExtensiveDistributionParameters[] memory, uint256 lastIndexDistribution) { - return - _getPoolDistributionsBetweenEpochs(uniV3Pool, _getRoundedEpoch(epochStart), type(uint32).max, skip, first); - } - - // ============================ GOVERNANCE FUNCTIONS =========================== - - /// @notice Sets a new `distributor` to which rewards should be distributed - function setNewDistributor(address _distributor) external onlyGovernorOrGuardian { - if (_distributor == address(0)) revert InvalidParam(); - distributor = _distributor; - emit DistributorUpdated(_distributor); - } - - /// @notice Sets the fees on deposit - function setFees(uint256 _fees) external onlyGovernorOrGuardian { - if (_fees >= BASE_9) revert InvalidParam(); - fees = _fees; - emit FeesSet(_fees); - } - - /// @notice Sets fee rebates for a given user - function setUserFeeRebate(address user, uint256 userFeeRebate) external onlyGovernorOrGuardian { - feeRebate[user] = userFeeRebate; - emit FeeRebateUpdated(user, userFeeRebate); - } - - /// @notice Toggles the fee whitelist for `token` - function toggleTokenWhitelist(address token) external onlyGovernorOrGuardian { - uint256 toggleStatus = 1 - isWhitelistedToken[token]; - isWhitelistedToken[token] = toggleStatus; - emit TokenWhitelistToggled(token, toggleStatus); - } - - /// @notice Recovers fees accrued on the contract for a list of `tokens` - function recoverFees(IERC20[] calldata tokens, address to) external onlyGovernorOrGuardian { - uint256 tokensLength = tokens.length; - for (uint256 i; i < tokensLength; ) { - tokens[i].safeTransfer(to, tokens[i].balanceOf(address(this))); - unchecked { - ++i; - } - } - } - - /// @notice Sets the minimum amounts per distribution epoch for different reward tokens - function setRewardTokenMinAmounts( - address[] calldata tokens, - uint256[] calldata amounts - ) external onlyGovernorOrGuardian { - uint256 tokensLength = tokens.length; - if (tokensLength != amounts.length) revert InvalidLengths(); - for (uint256 i; i < tokensLength; ++i) { - uint256 amount = amounts[i]; - // Basic logic check to make sure there are no duplicates in the `rewardTokens` table. If a token is - // removed then re-added, it will appear as a duplicate in the list - if (amount > 0 && rewardTokenMinAmounts[tokens[i]] == 0) rewardTokens.push(tokens[i]); - rewardTokenMinAmounts[tokens[i]] = amount; - emit RewardTokenMinimumAmountUpdated(tokens[i], amount); - } - } - - /// @notice Sets a new address to receive fees - function setFeeRecipient(address _feeRecipient) external onlyGovernorOrGuardian { - feeRecipient = _feeRecipient; - emit FeeRecipientUpdated(_feeRecipient); - } - - /// @notice Sets the message that needs to be signed by users before posting rewards - function setMessage(string memory _message) external onlyGovernorOrGuardian { - message = _message; - bytes32 _messageHash = ECDSA.toEthSignedMessageHash(bytes(_message)); - messageHash = _messageHash; - emit MessageUpdated(_messageHash); - } - - /// @notice Toggles the whitelist status for `user` when it comes to signing messages before depositing rewards. - function toggleSigningWhitelist(address user) external onlyGovernorOrGuardian { - uint256 whitelistStatus = 1 - userSignatureWhitelist[user]; - userSignatureWhitelist[user] = whitelistStatus; - emit UserSigningWhitelistToggled(user, whitelistStatus); - } - - // ============================== INTERNAL HELPERS ============================= - - /// @notice Rounds an `epoch` timestamp to the start of the corresponding period - function _getRoundedEpoch(uint32 epoch) internal pure returns (uint32) { - return (epoch / EPOCH_DURATION) * EPOCH_DURATION; - } - - /// @notice Checks whether `distribution` was live between `roundedEpochStart` and `roundedEpochEnd` - function _isDistributionLiveBetweenEpochs( - DistributionParameters memory distribution, - uint32 roundedEpochStart, - uint32 roundedEpochEnd - ) internal pure returns (bool) { - uint256 distributionEpochStart = distribution.epochStart; - return (distributionEpochStart + distribution.numEpoch * EPOCH_DURATION > roundedEpochStart && - distributionEpochStart < roundedEpochEnd); - } - - /// @notice Fetches data for `token` on the Uniswap `pool` - function _getUniswapTokenData( - IERC20Metadata token, - address pool - ) internal view returns (UniswapTokenData memory data) { - data.add = address(token); - data.decimals = token.decimals(); - data.symbol = token.symbol(); - data.poolBalance = token.balanceOf(pool); - } - - /// @notice Fetches extra data about the parameters in a distribution - function getExtensiveDistributionParameters( - DistributionParameters memory distribution - ) external view returns (ExtensiveDistributionParameters memory extensiveParams) { - extensiveParams.base = distribution; - try IUniswapV3Pool(distribution.uniV3Pool).fee() returns (uint24 fee) { - extensiveParams.poolFee = fee; - } catch { - extensiveParams.poolFee = 0; - } - extensiveParams.token0 = _getUniswapTokenData( - IERC20Metadata(IUniswapV3Pool(distribution.uniV3Pool).token0()), - distribution.uniV3Pool - ); - extensiveParams.token1 = _getUniswapTokenData( - IERC20Metadata(IUniswapV3Pool(distribution.uniV3Pool).token1()), - distribution.uniV3Pool - ); - extensiveParams.rewardTokenSymbol = IERC20Metadata(distribution.rewardToken).symbol(); - extensiveParams.rewardTokenDecimals = IERC20Metadata(distribution.rewardToken).decimals(); - } - - /// @notice Tries to fetch extra data about the parameters in a distribution - function tryGetExtensiveDistributionParameters( - DistributionParameters memory distribution - ) external returns (bool success, ExtensiveDistributionParameters memory extensiveParams) { - (bool callSuccess, bytes memory returndata) = address(this).delegatecall( - abi.encodeWithSelector(OldDistributionCreator.getExtensiveDistributionParameters.selector, distribution) - ); - success = callSuccess; - if (success) { - extensiveParams = abi.decode(returndata, (ExtensiveDistributionParameters)); - } - return (success, extensiveParams); - } - - /// @notice Gets the list of all the distributions for `uniV3Pool` that have been active between `epochStart` and `epochEnd` (excluded) - /// @dev If the `uniV3Pool` parameter is equal to 0, then this function will return the distributions for all pools - function _getPoolDistributionsBetweenEpochs( - address uniV3Pool, - uint32 epochStart, - uint32 epochEnd, - uint32 skip, - uint32 first - ) internal view returns (ExtensiveDistributionParameters[] memory, uint256) { - uint256 length; - uint256 distributionListLength = distributionList.length; - uint256 returnSize = first > distributionListLength ? distributionListLength : first; - ExtensiveDistributionParameters[] memory activeRewards = new ExtensiveDistributionParameters[](returnSize); - uint32 i = skip; - while (i < distributionListLength) { - DistributionParameters memory distribution = distributionList[i]; - if ( - _isDistributionLiveBetweenEpochs(distribution, epochStart, epochEnd) && - (uniV3Pool == address(0) || distribution.uniV3Pool == uniV3Pool) - ) { - (bool success, ExtensiveDistributionParameters memory extensiveParams) = IDistributionCreator( - address(this) - ).tryGetExtensiveDistributionParameters(distribution); - if (success) { - activeRewards[length] = extensiveParams; - length += 1; - } - } - unchecked { - ++i; - } - if (length == returnSize) break; - } - assembly { - mstore(activeRewards, length) - } - return (activeRewards, i); - } -} diff --git a/contracts/deprecated/OldDistributor.sol b/contracts/deprecated/OldDistributor.sol deleted file mode 100644 index 9afff90..0000000 --- a/contracts/deprecated/OldDistributor.sol +++ /dev/null @@ -1,348 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -/* - * █ - ***** ▓▓▓ - * ▓▓▓▓▓▓▓ - * ///. ▓▓▓▓▓▓▓▓▓▓▓▓▓ - ***** //////// ▓▓▓▓▓▓▓ - * ///////////// ▓▓▓ - ▓▓ ////////////////// █ ▓▓ - ▓▓ ▓▓ /////////////////////// ▓▓ ▓▓ - ▓▓ ▓▓ //////////////////////////// ▓▓ ▓▓ - ▓▓ ▓▓ /////////▓▓▓///////▓▓▓///////// ▓▓ ▓▓ - ▓▓ ,////////////////////////////////////// ▓▓ ▓▓ - ▓▓ ////////////////////////////////////////// ▓▓ - ▓▓ //////////////////////▓▓▓▓///////////////////// - ,//////////////////////////////////////////////////// - .////////////////////////////////////////////////////////// - .//////////////////////////██.,//////////////////////////█ - .//////////////////////████..,./////////////////////██ - ...////////////////███████.....,.////////////////███ - ,.,////////////████████ ........,///////////████ - .,.,//////█████████ ,.......///////████ - ,..//████████ ........./████ - ..,██████ .....,███ - .██ ,.,█ - - - - ▓▓ ▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓ ▓▓ ▓▓▓▓▓▓▓▓▓▓ - ▓▓▓▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓ ▓▓ ▓▓ ▓▓▓▓ - ▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓ ▓▓ ▓▓▓▓▓ - ▓▓▓ ▓▓ ▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓ -*/ - -pragma solidity ^0.8.17; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; - -import "../utils/UUPSHelper.sol"; - -struct MerkleTree { - // Root of a Merkle tree which leaves are `(address user, address token, uint amount)` - // representing an amount of tokens accumulated by `user`. - // The Merkle tree is assumed to have only increasing amounts: that is to say if a user can claim 1, - // then after the amount associated in the Merkle tree for this token should be x > 1 - bytes32 merkleRoot; - // Ipfs hash of the tree data - bytes32 ipfsHash; -} - -struct Claim { - uint208 amount; - uint48 timestamp; - bytes32 merkleRoot; -} - -/// @title Distributor -/// @notice Allows LPs on AMMs with concentrated liquidity to claim the rewards that were distributed to them -/// @author Angle Labs. Inc -contract OldDistributor is UUPSHelper { - using SafeERC20 for IERC20; - - /// @notice Epoch duration - uint32 internal constant _EPOCH_DURATION = 3600; - - // ================================= VARIABLES ================================= - - /// @notice Tree of claimable tokens through this contract - MerkleTree public tree; - - /// @notice Tree that was in place in the contract before the last `tree` update - MerkleTree public lastTree; - - /// @notice Token to deposit to freeze the roots update - IERC20 public disputeToken; - - /// @notice `Core` contract handling access control - ICore public core; - - /// @notice Address which created the dispute - /// @dev Used to store if there is an ongoing dispute - address public disputer; - - /// @notice When the current tree will become valid - uint48 public endOfDisputePeriod; - - /// @notice Time after which a change in a tree becomes effective, in EPOCH_DURATION - uint48 public disputePeriod; - - /// @notice Amount to deposit to freeze the roots update - uint256 public disputeAmount; - - /// @notice Mapping user -> token -> amount to track claimed amounts - mapping(address => mapping(address => Claim)) public claimed; - - /// @notice Trusted EOAs to update the Merkle root - mapping(address => uint256) public canUpdateMerkleRoot; - - /// @notice Whether or not to disable permissionless claiming - mapping(address => uint256) public onlyOperatorCanClaim; - - /// @notice user -> operator -> authorisation to claim - mapping(address => mapping(address => uint256)) public operators; - - uint256[38] private __gap; - - // =================================== EVENTS ================================== - - event Claimed(address indexed user, address indexed token, uint256 amount); - event DisputeAmountUpdated(uint256 _disputeAmount); - event Disputed(string reason); - event DisputePeriodUpdated(uint48 _disputePeriod); - event DisputeResolved(bool valid); - event DisputeTokenUpdated(address indexed _disputeToken); - event OperatorClaimingToggled(address indexed user, bool isEnabled); - event OperatorToggled(address indexed user, address indexed operator, bool isWhitelisted); - event Recovered(address indexed token, address indexed to, uint256 amount); - event Revoked(); // With this event an indexer could maintain a table (timestamp, merkleRootUpdate) - event TreeUpdated(bytes32 merkleRoot, bytes32 ipfsHash, uint48 endOfDisputePeriod); - event TrustedToggled(address indexed eoa, bool trust); - - // ================================= MODIFIERS ================================= - - /// @notice Checks whether the `msg.sender` has the governor role or the guardian role - modifier onlyGovernorOrGuardian() { - if (!core.isGovernorOrGuardian(msg.sender)) revert NotGovernorOrGuardian(); - _; - } - - /// @notice Checks whether the `msg.sender` is the `user` address or is a trusted address - modifier onlyTrustedOrUser(address user) { - if (user != msg.sender && canUpdateMerkleRoot[msg.sender] != 1 && !core.isGovernorOrGuardian(msg.sender)) - revert NotTrusted(); - _; - } - - // ================================ CONSTRUCTOR ================================ - - constructor() initializer {} - - function initialize(ICore _core) external initializer { - if (address(_core) == address(0)) revert ZeroAddress(); - core = _core; - } - - /// @inheritdoc UUPSUpgradeable - function _authorizeUpgrade(address) internal view override onlyGuardianUpgrader(core) {} - - // =============================== MAIN FUNCTION =============================== - - /// @notice Claims rewards for a given set of users - /// @dev Anyone may call this function for anyone else, funds go to destination regardless, it's just a question of - /// who provides the proof and pays the gas: `msg.sender` is used only for addresses that require a trusted operator - /// @param users Recipient of tokens - /// @param tokens ERC20 claimed - /// @param amounts Amount of tokens that will be sent to the corresponding users - /// @param proofs Array of hashes bridging from a leaf `(hash of user | token | amount)` to the Merkle root - function claim( - address[] calldata users, - address[] calldata tokens, - uint256[] calldata amounts, - bytes32[][] calldata proofs - ) external { - uint256 usersLength = users.length; - if ( - usersLength == 0 || - usersLength != tokens.length || - usersLength != amounts.length || - usersLength != proofs.length - ) revert InvalidLengths(); - - for (uint256 i; i < usersLength; ) { - address user = users[i]; - address token = tokens[i]; - uint256 amount = amounts[i]; - - // Checking if only an approved operator can claim for `user` - if (onlyOperatorCanClaim[user] == 1 && operators[user][msg.sender] == 0) revert NotWhitelisted(); - - // Verifying proof - bytes32 leaf = keccak256(abi.encode(user, token, amount)); - if (!_verifyProof(leaf, proofs[i])) revert InvalidProof(); - - // Closing reentrancy gate here - uint256 toSend = amount - claimed[user][token].amount; - claimed[user][token] = Claim(SafeCast.toUint208(amount), uint48(block.timestamp), getMerkleRoot()); - - IERC20(token).safeTransfer(user, toSend); - emit Claimed(user, token, toSend); - unchecked { - ++i; - } - } - } - - /// @notice Returns the MerkleRoot that is currently live for the contract - function getMerkleRoot() public view returns (bytes32) { - if (block.timestamp >= endOfDisputePeriod && disputer == address(0)) return tree.merkleRoot; - else return lastTree.merkleRoot; - } - - // ============================ GOVERNANCE FUNCTIONS =========================== - - /// @notice Adds or removes EOAs which are trusted to update the Merkle root - function toggleTrusted(address eoa) external onlyGovernorOrGuardian { - uint256 trustedStatus = 1 - canUpdateMerkleRoot[eoa]; - canUpdateMerkleRoot[eoa] = trustedStatus; - emit TrustedToggled(eoa, trustedStatus == 1); - } - - /// @notice Updates Merkle Tree - function updateTree(MerkleTree calldata _tree) external { - if ( - disputer != address(0) || - // A trusted address cannot update a tree right after a precedent tree update otherwise it can de facto - // validate a tree which has not passed the dispute period - ((canUpdateMerkleRoot[msg.sender] != 1 || block.timestamp < endOfDisputePeriod) && - !core.isGovernorOrGuardian(msg.sender)) - ) revert NotTrusted(); - MerkleTree memory _lastTree = tree; - tree = _tree; - lastTree = _lastTree; - - uint48 _endOfPeriod = _endOfDisputePeriod(uint48(block.timestamp)); - endOfDisputePeriod = _endOfPeriod; - emit TreeUpdated(_tree.merkleRoot, _tree.ipfsHash, _endOfPeriod); - } - - /// @notice Freezes the Merkle tree update until the dispute is resolved - /// @dev Requires a deposit of `disputeToken` that'll be slashed if the dispute is not accepted - /// @dev It is only possible to create a dispute within `disputePeriod` after each tree update - function disputeTree(string memory reason) external { - if (disputer != address(0)) revert UnresolvedDispute(); - if (block.timestamp >= endOfDisputePeriod) revert InvalidDispute(); - IERC20(disputeToken).safeTransferFrom(msg.sender, address(this), disputeAmount); - disputer = msg.sender; - emit Disputed(reason); - } - - /// @notice Resolve the ongoing dispute, if any - /// @param valid Whether the dispute was valid - function resolveDispute(bool valid) external onlyGovernorOrGuardian { - if (disputer == address(0)) revert NoDispute(); - if (valid) { - IERC20(disputeToken).safeTransfer(disputer, disputeAmount); - // If a dispute is valid, the contract falls back to the last tree that was updated - _revokeTree(); - } else { - IERC20(disputeToken).safeTransfer(msg.sender, disputeAmount); - endOfDisputePeriod = _endOfDisputePeriod(uint48(block.timestamp)); - } - disputer = address(0); - emit DisputeResolved(valid); - } - - /// @notice Allows the governor or the guardian of this contract to fallback to the last version of the tree - /// immediately - function revokeTree() external onlyGovernorOrGuardian { - if (disputer != address(0)) revert UnresolvedDispute(); - _revokeTree(); - } - - /// @notice Toggles permissioned claiming for a given user - function toggleOnlyOperatorCanClaim(address user) external onlyTrustedOrUser(user) { - uint256 oldValue = onlyOperatorCanClaim[user]; - onlyOperatorCanClaim[user] = 1 - oldValue; - emit OperatorClaimingToggled(user, oldValue == 0); - } - - /// @notice Toggles whitelisting for a given user and a given operator - function toggleOperator(address user, address operator) external onlyTrustedOrUser(user) { - uint256 oldValue = operators[user][operator]; - operators[user][operator] = 1 - oldValue; - emit OperatorToggled(user, operator, oldValue == 0); - } - - /// @notice Recovers any ERC20 token - function recoverERC20(address tokenAddress, address to, uint256 amountToRecover) external onlyGovernorOrGuardian { - IERC20(tokenAddress).safeTransfer(to, amountToRecover); - emit Recovered(tokenAddress, to, amountToRecover); - } - - /// @notice Sets the dispute period after which a tree update becomes effective - function setDisputePeriod(uint48 _disputePeriod) external onlyGovernorOrGuardian { - disputePeriod = uint48(_disputePeriod); - emit DisputePeriodUpdated(_disputePeriod); - } - - /// @notice Sets the token used as a caution during disputes - function setDisputeToken(IERC20 _disputeToken) external onlyGovernorOrGuardian { - if (disputer != address(0)) revert UnresolvedDispute(); - disputeToken = _disputeToken; - emit DisputeTokenUpdated(address(_disputeToken)); - } - - /// @notice Sets the amount of `disputeToken` used as a caution during disputes - function setDisputeAmount(uint256 _disputeAmount) external onlyGovernorOrGuardian { - if (disputer != address(0)) revert UnresolvedDispute(); - disputeAmount = _disputeAmount; - emit DisputeAmountUpdated(_disputeAmount); - } - - // ============================= INTERNAL FUNCTIONS ============================ - - /// @notice Fallback to the last version of the tree - function _revokeTree() internal { - MerkleTree memory _tree = lastTree; - endOfDisputePeriod = 0; - tree = _tree; - emit Revoked(); - emit TreeUpdated( - _tree.merkleRoot, - _tree.ipfsHash, - (uint48(block.timestamp) / _EPOCH_DURATION) * (_EPOCH_DURATION) // Last hour - ); - } - - /// @notice Returns the end of the dispute period - /// @dev treeUpdate is rounded up to next hour and then `disputePeriod` hours are added - function _endOfDisputePeriod(uint48 treeUpdate) internal view returns (uint48) { - return ((treeUpdate - 1) / _EPOCH_DURATION + 1 + disputePeriod) * (_EPOCH_DURATION); - } - - /// @notice Checks the validity of a proof - /// @param leaf Hashed leaf data, the starting point of the proof - /// @param proof Array of hashes forming a hash chain from leaf to root - /// @return true If proof is correct, else false - function _verifyProof(bytes32 leaf, bytes32[] memory proof) internal view returns (bool) { - bytes32 currentHash = leaf; - uint256 proofLength = proof.length; - for (uint256 i; i < proofLength; ) { - if (currentHash < proof[i]) { - currentHash = keccak256(abi.encode(currentHash, proof[i])); - } else { - currentHash = keccak256(abi.encode(proof[i], currentHash)); - } - unchecked { - ++i; - } - } - bytes32 root = getMerkleRoot(); - if (root == bytes32(0)) revert InvalidUninitializedRoot(); - return currentHash == root; - } -} diff --git a/contracts/interfaces/CoreModuleInterfaces.sol b/contracts/interfaces/CoreModuleInterfaces.sol deleted file mode 100644 index 2e62c24..0000000 --- a/contracts/interfaces/CoreModuleInterfaces.sol +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity ^0.8.7; - -/// @title IAngleMiddlemanGauge -/// @author Angle Core Team -/// @notice Interface for the `AngleMiddleman` contract -interface IAngleMiddlemanGauge { - function notifyReward(address gauge, uint256 amount) external; -} - -interface IStakingRewards { - function notifyRewardAmount(uint256 reward) external; -} - -interface IGaugeController { - //solhint-disable-next-line - function gauge_types(address addr) external view returns (int128); - - //solhint-disable-next-line - function gauge_relative_weight_write(address addr, uint256 timestamp) external returns (uint256); - - //solhint-disable-next-line - function gauge_relative_weight(address addr, uint256 timestamp) external view returns (uint256); -} - -interface ILiquidityGauge { - // solhint-disable-next-line - function deposit_reward_token(address _rewardToken, uint256 _amount) external; -} diff --git a/contracts/interfaces/ICore.sol b/contracts/interfaces/IAccessControlManager.sol similarity index 92% rename from contracts/interfaces/ICore.sol rename to contracts/interfaces/IAccessControlManager.sol index 2ebbc06..045827f 100644 --- a/contracts/interfaces/ICore.sol +++ b/contracts/interfaces/IAccessControlManager.sol @@ -2,10 +2,10 @@ pragma solidity ^0.8.17; -/// @title ICore +/// @title IAccessControlManager /// @author Angle Labs, Inc. /// @notice Interface for the `Core` contracts of smart contract modules used in Angle Labs contracts -interface ICore { +interface IAccessControlManager { /// @notice Checks whether an address is governor /// @param admin Address to check /// @return Whether the address has the `GOVERNOR_ROLE` or not diff --git a/contracts/interfaces/IAgToken.sol b/contracts/interfaces/IAgToken.sol deleted file mode 100644 index 7a00907..0000000 --- a/contracts/interfaces/IAgToken.sol +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity ^0.8.17; - -import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; - -/// @title IAgToken -/// @author Angle Core Team -/// @notice Interface for the stablecoins `AgToken` contracts -/// @dev This interface only contains functions of the `AgToken` contract which are called by other contracts -/// of this module or of the first module of the Angle Protocol -interface IAgToken is IERC20Upgradeable { - // ======================= Minter Role Only Functions =========================== - - /// @notice Lets the `StableMaster` contract or another whitelisted contract mint agTokens - /// @param account Address to mint to - /// @param amount Amount to mint - /// @dev The contracts allowed to issue agTokens are the `StableMaster` contract, `VaultManager` contracts - /// associated to this stablecoin as well as the flash loan module (if activated) and potentially contracts - /// whitelisted by governance - function mint(address account, uint256 amount) external; - - /// @notice Burns `amount` tokens from a `burner` address after being asked to by `sender` - /// @param amount Amount of tokens to burn - /// @param burner Address to burn from - /// @param sender Address which requested the burn from `burner` - /// @dev This method is to be called by a contract with the minter right after being requested - /// to do so by a `sender` address willing to burn tokens from another `burner` address - /// @dev The method checks the allowance between the `sender` and the `burner` - function burnFrom(uint256 amount, address burner, address sender) external; - - /// @notice Burns `amount` tokens from a `burner` address - /// @param amount Amount of tokens to burn - /// @param burner Address to burn from - /// @dev This method is to be called by a contract with a minter right on the AgToken after being - /// requested to do so by an address willing to burn tokens from its address - function burnSelf(uint256 amount, address burner) external; - - // ========================= Treasury Only Functions =========================== - - /// @notice Adds a minter in the contract - /// @param minter Minter address to add - /// @dev Zero address checks are performed directly in the `Treasury` contract - function addMinter(address minter) external; - - /// @notice Removes a minter from the contract - /// @param minter Minter address to remove - /// @dev This function can also be called by a minter wishing to revoke itself - function removeMinter(address minter) external; - - /// @notice Sets a new treasury contract - /// @param _treasury New treasury address - function setTreasury(address _treasury) external; - - // ========================= External functions ================================ - - /// @notice Checks whether an address has the right to mint agTokens - /// @param minter Address for which the minting right should be checked - /// @return Whether the address has the right to mint agTokens or not - function isMinter(address minter) external view returns (bool); -} diff --git a/contracts/interfaces/ITreasury.sol b/contracts/interfaces/ITreasury.sol deleted file mode 100644 index c5ca881..0000000 --- a/contracts/interfaces/ITreasury.sol +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity ^0.8.17; - -import { IAgToken } from "./IAgToken.sol"; -import { ICore } from "./ICore.sol"; - -/// @title ITreasury -/// @author Angle Core Team -/// @notice Interface for the `Treasury` contract -/// @dev This interface only contains functions of the `Treasury` which are called by other contracts -/// of this module -interface ITreasury { - /// @notice Stablecoin handled by this `treasury` contract - function stablecoin() external view returns (IAgToken); - - /// @notice Checks whether a given address has the governor role - /// @param admin Address to check - /// @return Whether the address has the governor role - /// @dev Access control is only kept in the `CoreBorrow` contract - function isGovernor(address admin) external view returns (bool); - - /// @notice Checks whether a given address has the guardian or the governor role - /// @param admin Address to check - /// @return Whether the address has the guardian or the governor role - /// @dev Access control is only kept in the `CoreBorrow` contract which means that this function - /// queries the `CoreBorrow` contract - function isGovernorOrGuardian(address admin) external view returns (bool); -} diff --git a/contracts/interfaces/external/algebra/IAlgebraPool.sol b/contracts/interfaces/external/algebra/IAlgebraPool.sol deleted file mode 100644 index 9f44bd9..0000000 --- a/contracts/interfaces/external/algebra/IAlgebraPool.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity >=0.5.0; - -/// @title IAlgebraPool -/// @dev Copied from: https://github.com/cryptoalgebra/Algebra/blob/master/src/core/contracts/interfaces/pool/IAlgebraPoolState.sol -interface IAlgebraPool { - /** - * @notice The globalState structure in the pool stores many values but requires only one slot - * and is exposed as a single method to save gas when accessed externally. - * @return price The current price of the pool as a sqrt(token1/token0) Q64.96 value; - * Returns tick The current tick of the pool, i.e. according to the last tick transition that was run; - * Returns This value may not always be equal to SqrtTickMath.getTickAtSqrtRatio(price) if the price is on a tick - * boundary; - * Returns fee The last pool fee value in hundredths of a bip, i.e. 1e-6; - * Returns timepointIndex The index of the last written timepoint; - * Returns communityFeeToken0 The community fee percentage of the swap fee in thousandths (1e-3) for token0; - * Returns communityFeeToken1 The community fee percentage of the swap fee in thousandths (1e-3) for token1; - * Returns unlocked Whether the pool is currently locked to reentrancy; - */ - function globalState() - external - view - returns ( - uint160 price, - int24 tick, - uint16 fee, - uint16 timepointIndex, - uint8 communityFeeToken0, - uint8 communityFeeToken1, - bool unlocked - ); -} diff --git a/contracts/interfaces/external/uniswap/IUniswapV3Pool.sol b/contracts/interfaces/external/uniswap/IUniswapV3Pool.sol deleted file mode 100644 index de1432f..0000000 --- a/contracts/interfaces/external/uniswap/IUniswapV3Pool.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity >=0.5.0; - -/// @title Pool state that never changes -/// @notice These parameters are fixed for a pool forever, i.e., the methods will always return the same values -interface IUniswapV3Pool { - /// @notice The first of the two tokens of the pool, sorted by address - /// @return The token contract address - function token0() external view returns (address); - - /// @notice The second of the two tokens of the pool, sorted by address - /// @return The token contract address - function token1() external view returns (address); - - /// @notice The pool's fee in hundredths of a bip, i.e. 1e-6 - /// @return The fee - function fee() external view returns (uint24); -} diff --git a/contracts/mock/AngleDistributor.sol b/contracts/mock/AngleDistributor.sol deleted file mode 100644 index 86faaae..0000000 --- a/contracts/mock/AngleDistributor.sol +++ /dev/null @@ -1,419 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity ^0.8.7; - -import "./AngleDistributorEvents.sol"; - -/// @title AngleDistributor -/// @author Forked from contracts developed by Curve and Frax and adapted by Angle Core Team -/// - ERC20CRV.vy (https://github.com/curvefi/curve-dao-contracts/blob/master/contracts/ERC20CRV.vy) -/// - FraxGaugeFXSRewardsDistributor.sol (https://github.com/FraxFinance/frax-solidity/blob/master/src/hardhat/contracts/Curve/FraxGaugeFXSRewardsDistributor.sol) -/// @notice All the events used in `AngleDistributor` contract -contract AngleDistributor is AngleDistributorEvents, ReentrancyGuardUpgradeable, AccessControlUpgradeable { - using SafeERC20 for IERC20; - - /// @notice Role for governors only - bytes32 public constant GOVERNOR_ROLE = keccak256("GOVERNOR_ROLE"); - /// @notice Role for the guardian - bytes32 public constant GUARDIAN_ROLE = keccak256("GUARDIAN_ROLE"); - - /// @notice Length of a week in seconds - uint256 public constant WEEK = 3600 * 24 * 7; - - /// @notice Time at which the emission rate is updated - uint256 public constant RATE_REDUCTION_TIME = WEEK; - - /// @notice Reduction of the emission rate - uint256 public constant RATE_REDUCTION_COEFFICIENT = 1007827884862117171; // 1.5 ^ (1/52) * 10**18 - - /// @notice Base used for computation - uint256 public constant BASE = 10**18; - - /// @notice Maps the address of a gauge to the last time this gauge received rewards - mapping(address => uint256) public lastTimeGaugePaid; - - /// @notice Maps the address of a gauge to whether it was killed or not - /// A gauge killed in this contract cannot receive any rewards - mapping(address => bool) public killedGauges; - - /// @notice Maps the address of a type >= 2 gauge to a delegate address responsible - /// for giving rewards to the actual gauge - mapping(address => address) public delegateGauges; - - /// @notice Maps the address of a gauge delegate to whether this delegate supports the `notifyReward` interface - /// and is therefore built for automation - mapping(address => bool) public isInterfaceKnown; - - /// @notice Address of the ANGLE token given as a reward - IERC20 public rewardToken; - - /// @notice Address of the `GaugeController` contract - IGaugeController public controller; - - /// @notice Address responsible for pulling rewards of type >= 2 gauges and distributing it to the - /// associated contracts if there is not already an address delegated for this specific contract - address public delegateGauge; - - /// @notice ANGLE current emission rate, it is first defined in the initializer and then updated every week - uint256 public rate; - - /// @notice Timestamp at which the current emission epoch started - uint256 public startEpochTime; - - /// @notice Amount of ANGLE tokens distributed through staking at the start of the epoch - /// This is an informational variable used to track how much has been distributed through liquidity mining - uint256 public startEpochSupply; - - /// @notice Index of the current emission epoch - /// Here also, this variable is not useful per se inside the smart contracts of the protocol, it is - /// just an informational variable - uint256 public miningEpoch; - - /// @notice Whether ANGLE distribution through this contract is on or no - bool public distributionsOn; - - /// @notice Constructor of the contract - /// @param _rewardToken Address of the ANGLE token - /// @param _controller Address of the GaugeController - /// @param _initialRate Initial ANGLE emission rate - /// @param _startEpochSupply Amount of ANGLE tokens already distributed via liquidity mining - /// @param governor Governor address of the contract - /// @param guardian Address of the guardian of this contract - /// @param _delegateGauge Address that will be used to pull rewards for type 2 gauges - /// @dev After this contract is created, the correct amount of ANGLE tokens should be transferred to the contract - /// @dev The `_delegateGauge` can be the zero address - function initialize( - address _rewardToken, - address _controller, - uint256 _initialRate, - uint256 _startEpochSupply, - address governor, - address guardian, - address _delegateGauge - ) external initializer { - require( - _controller != address(0) && _rewardToken != address(0) && guardian != address(0) && governor != address(0), - "0" - ); - rewardToken = IERC20(_rewardToken); - controller = IGaugeController(_controller); - startEpochSupply = _startEpochSupply; - miningEpoch = 0; - // Some ANGLE tokens should be sent to the contract directly after initialization - rate = _initialRate; - delegateGauge = _delegateGauge; - distributionsOn = false; - startEpochTime = block.timestamp; - _setRoleAdmin(GOVERNOR_ROLE, GOVERNOR_ROLE); - _setRoleAdmin(GUARDIAN_ROLE, GOVERNOR_ROLE); - _setupRole(GUARDIAN_ROLE, guardian); - _setupRole(GOVERNOR_ROLE, governor); - _setupRole(GUARDIAN_ROLE, governor); - } - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() initializer {} - - // ======================== Internal Functions ================================= - - /// @notice Internal function to distribute rewards to a gauge - /// @param gaugeAddr Address of the gauge to distribute rewards to - /// @return weeksElapsed Weeks elapsed since the last call - /// @return rewardTally Amount of rewards distributed to the gauge - /// @dev The reason for having an internal function is that it's called by the `distributeReward` and the - /// `distributeRewardToMultipleGauges` - /// @dev Although they would need to be performed all the time this function is called, this function does not - /// contain checks on whether distribution is on, and on whether rate should be reduced. These are done in each external - /// function calling this function for gas efficiency - function _distributeReward(address gaugeAddr) internal returns (uint256 weeksElapsed, uint256 rewardTally) { - // Checking if the gauge has been added or if it still possible to distribute rewards to this gauge - int128 gaugeType = IGaugeController(controller).gauge_types(gaugeAddr); - require(gaugeType >= 0 && !killedGauges[gaugeAddr], "110"); - - // Calculate the elapsed time in weeks. - uint256 lastTimePaid = lastTimeGaugePaid[gaugeAddr]; - - // Edge case for first reward for this gauge - if (lastTimePaid == 0) { - weeksElapsed = 1; - if (gaugeType == 0) { - // We give a full approval for the gauges with type zero which correspond to the staking - // contracts of the protocol - rewardToken.safeApprove(gaugeAddr, type(uint256).max); - } - } else { - // Truncation desired - weeksElapsed = (block.timestamp - lastTimePaid) / WEEK; - // Return early here for 0 weeks instead of throwing, as it could have bad effects in other contracts - if (weeksElapsed == 0) { - return (0, 0); - } - } - rewardTally = 0; - // We use this variable to keep track of the emission rate across different weeks - uint256 weeklyRate = rate; - for (uint256 i = 0; i < weeksElapsed; i++) { - uint256 relWeightAtWeek; - if (i == 0) { - // Mutative, for the current week: makes sure the weight is checkpointed. Also returns the weight. - relWeightAtWeek = controller.gauge_relative_weight_write(gaugeAddr, block.timestamp); - } else { - // View - relWeightAtWeek = controller.gauge_relative_weight(gaugeAddr, (block.timestamp - WEEK * i)); - } - rewardTally += (weeklyRate * relWeightAtWeek * WEEK) / BASE; - - // To get the rate of the week prior from the current rate we just have to multiply by the weekly division - // factor - // There may be some precisions error: inferred previous values of the rate may be different to what we would - // have had if the rate had been computed correctly in these weeks: we expect from empirical observations - // this `weeklyRate` to be inferior to what the `rate` would have been - weeklyRate = (weeklyRate * RATE_REDUCTION_COEFFICIENT) / BASE; - } - - // Update the last time paid, rounded to the closest week - // in order not to have an ever moving time on when to call this function - lastTimeGaugePaid[gaugeAddr] = (block.timestamp / WEEK) * WEEK; - - // If the `gaugeType >= 2`, this means that the gauge is a gauge on another chain (and corresponds to tokens - // that need to be bridged) or is associated to an external contract of the Angle Protocol - if (gaugeType >= 2) { - // If it is defined, we use the specific delegate attached to the gauge - address delegate = delegateGauges[gaugeAddr]; - if (delegate == address(0)) { - // If not, we check if a delegate common to all gauges with type >= 2 can be used - delegate = delegateGauge; - } - if (delegate != address(0)) { - _sendDelegateRewards(gaugeAddr, delegate, rewardTally); - } else { - rewardToken.safeTransfer(gaugeAddr, rewardTally); - } - } else if (gaugeType == 1) { - // This is for the case of Perpetual contracts which need to be able to receive their reward tokens - rewardToken.safeTransfer(gaugeAddr, rewardTally); - IStakingRewards(gaugeAddr).notifyRewardAmount(rewardTally); - } else { - // Potentially override the gauge address - address delegate = delegateGauges[gaugeAddr]; - if (delegate != address(0)) { - _sendDelegateRewards(gaugeAddr, delegate, rewardTally); - } else { - ILiquidityGauge(gaugeAddr).deposit_reward_token(address(rewardToken), rewardTally); - } - } - emit RewardDistributed(gaugeAddr, rewardTally); - } - - /// @notice Helper to send `rewardTally` for `gaugeAddr` to the non zero `delegate` address - function _sendDelegateRewards( - address gaugeAddr, - address delegate, - uint256 rewardTally - ) internal { - rewardToken.safeTransfer(delegate, rewardTally); - // If this delegate supports a specific interface, then rewards sent are notified through this - // interface - if (isInterfaceKnown[delegate]) { - IAngleMiddlemanGauge(delegate).notifyReward(gaugeAddr, rewardTally); - } - } - - /// @notice Updates mining rate and supply at the start of the epoch - /// @dev Any modifying mining call must also call this - /// @dev It is possible that more than one week past between two calls of this function, and for this reason - /// this function has been slightly modified from Curve implementation by Angle Team - function _updateMiningParameters() internal { - // When entering this function, we always have: `(block.timestamp - startEpochTime) / RATE_REDUCTION_TIME >= 1` - uint256 epochDelta = (block.timestamp - startEpochTime) / RATE_REDUCTION_TIME; - - // Storing intermediate values for the rate and for the `startEpochSupply` - uint256 _rate = rate; - uint256 _startEpochSupply = startEpochSupply; - - startEpochTime += RATE_REDUCTION_TIME * epochDelta; - miningEpoch += epochDelta; - - for (uint256 i = 0; i < epochDelta; i++) { - // Updating the intermediate values of the `startEpochSupply` - _startEpochSupply += _rate * RATE_REDUCTION_TIME; - _rate = (_rate * BASE) / RATE_REDUCTION_COEFFICIENT; - } - rate = _rate; - startEpochSupply = _startEpochSupply; - emit UpdateMiningParameters(block.timestamp, _rate, _startEpochSupply); - } - - /// @notice Toggles the fact that a gauge delegate can be used for automation or not and therefore supports - /// the `notifyReward` interface - /// @param _delegateGauge Address of the gauge to change - function _toggleInterfaceKnown(address _delegateGauge) internal { - bool isInterfaceKnownMem = isInterfaceKnown[_delegateGauge]; - isInterfaceKnown[_delegateGauge] = !isInterfaceKnownMem; - emit InterfaceKnownToggled(_delegateGauge, !isInterfaceKnownMem); - } - - // ================= Permissionless External Functions ========================= - - /// @notice Distributes rewards to a staking contract (also called gauge) - /// @param gaugeAddr Address of the gauge to send tokens too - /// @return weeksElapsed Number of weeks elapsed since the last time rewards were distributed - /// @return rewardTally Amount of tokens sent to the gauge - /// @dev Anyone can call this function to distribute rewards to the different staking contracts - function distributeReward(address gaugeAddr) external nonReentrant returns (uint256, uint256) { - // Checking if distribution is on - require(distributionsOn == true, "109"); - // Updating rate distribution parameters if need be - if (block.timestamp >= startEpochTime + RATE_REDUCTION_TIME) { - _updateMiningParameters(); - } - return _distributeReward(gaugeAddr); - } - - /// @notice Distributes rewards to multiple staking contracts - /// @param gauges Addresses of the gauge to send tokens too - /// @dev Anyone can call this function to distribute rewards to the different staking contracts - /// @dev Compared with the `distributeReward` function, this function sends rewards to multiple - /// contracts at the same time - function distributeRewardToMultipleGauges(address[] memory gauges) external nonReentrant { - // Checking if distribution is on - require(distributionsOn == true, "109"); - // Updating rate distribution parameters if need be - if (block.timestamp >= startEpochTime + RATE_REDUCTION_TIME) { - _updateMiningParameters(); - } - for (uint256 i = 0; i < gauges.length; i++) { - _distributeReward(gauges[i]); - } - } - - /// @notice Updates mining rate and supply at the start of the epoch - /// @dev Callable by any address, but only once per epoch - function updateMiningParameters() external { - require(block.timestamp >= startEpochTime + RATE_REDUCTION_TIME, "108"); - _updateMiningParameters(); - } - - // ========================= Governor Functions ================================ - - /// @notice Withdraws ERC20 tokens that could accrue on this contract - /// @param tokenAddress Address of the ERC20 token to withdraw - /// @param to Address to transfer to - /// @param amount Amount to transfer - /// @dev Added to support recovering LP Rewards and other mistaken tokens - /// from other systems to be distributed to holders - /// @dev This function could also be used to recover ANGLE tokens in case the rate got smaller - function recoverERC20( - address tokenAddress, - address to, - uint256 amount - ) external onlyRole(GOVERNOR_ROLE) { - // If the token is the ANGLE token, we need to make sure that governance is not going to withdraw - // too many tokens and that it'll be able to sustain the weekly distribution forever - // This check assumes that `distributeReward` has been called for gauges and that there are no gauges - // which have not received their past week's rewards - if (tokenAddress == address(rewardToken)) { - uint256 currentBalance = rewardToken.balanceOf(address(this)); - // The amount distributed till the end is `rate * WEEK / (1 - RATE_REDUCTION_FACTOR)` where - // `RATE_REDUCTION_FACTOR = BASE / RATE_REDUCTION_COEFFICIENT` which translates to: - require( - currentBalance >= - ((rate * RATE_REDUCTION_COEFFICIENT) * WEEK) / (RATE_REDUCTION_COEFFICIENT - BASE) + amount, - "4" - ); - } - IERC20(tokenAddress).safeTransfer(to, amount); - emit Recovered(tokenAddress, to, amount); - } - - /// @notice Sets a new gauge controller - /// @param _controller Address of the new gauge controller - function setGaugeController(address _controller) external onlyRole(GOVERNOR_ROLE) { - require(_controller != address(0), "0"); - controller = IGaugeController(_controller); - emit GaugeControllerUpdated(_controller); - } - - /// @notice Sets a new delegate gauge for pulling rewards of a type >= 2 gauges or of all type >= 2 gauges - /// @param gaugeAddr Gauge to change the delegate of - /// @param _delegateGauge Address of the new gauge delegate related to `gaugeAddr` - /// @param toggleInterface Whether we should toggle the fact that the `_delegateGauge` is built for automation or not - /// @dev This function can be used to remove delegating or introduce the pulling of rewards to a given address - /// @dev If `gaugeAddr` is the zero address, this function updates the delegate gauge common to all gauges with type >= 2 - /// @dev The `toggleInterface` parameter has been added for convenience to save one transaction when adding a gauge delegate - /// which supports the `notifyReward` interface - function setDelegateGauge( - address gaugeAddr, - address _delegateGauge, - bool toggleInterface - ) external onlyRole(GOVERNOR_ROLE) { - if (gaugeAddr != address(0)) { - delegateGauges[gaugeAddr] = _delegateGauge; - } else { - delegateGauge = _delegateGauge; - } - emit DelegateGaugeUpdated(gaugeAddr, _delegateGauge); - - if (toggleInterface) { - _toggleInterfaceKnown(_delegateGauge); - } - } - - /// @notice Changes the ANGLE emission rate - /// @param _newRate New ANGLE emission rate - /// @dev It is important to be super wary when calling this function and to make sure that `distributeReward` - /// has been called for all gauges in the past weeks. If not, gauges may get an incorrect distribution of ANGLE rewards - /// for these past weeks based on the new rate and not on the old rate - /// @dev Governance should thus make sure to call this function rarely and when it does to do it after the weekly `distributeReward` - /// calls for all existing gauges - /// @dev As this function assumes that `distributeReward` has been called during the week, it also assumes that the `startEpochSupply` - /// parameter has been put up to date - function setRate(uint256 _newRate) external onlyRole(GOVERNOR_ROLE) { - // Checking if the new rate is compatible with the amount of ANGLE tokens this contract has in balance - // This check assumes, like this function, that `distributeReward` has correctly been called before - require( - rewardToken.balanceOf(address(this)) >= - ((_newRate * RATE_REDUCTION_COEFFICIENT) * WEEK) / (RATE_REDUCTION_COEFFICIENT - BASE), - "4" - ); - rate = _newRate; - emit RateUpdated(_newRate); - } - - /// @notice Toggles the status of a gauge to either killed or unkilled - /// @param gaugeAddr Gauge to toggle the status of - /// @dev It is impossible to kill a gauge in the `GaugeController` contract, for this reason killing of gauges - /// takes place in the `AngleDistributor` contract - /// @dev This means that people could vote for a gauge in the gauge controller contract but that rewards are not going - /// to be distributed to it in the end: people would need to remove their weights on the gauge killed to end the diminution - /// in rewards - /// @dev In the case of a gauge being killed, this function resets the timestamps at which this gauge has been approved and - /// disapproves the gauge to spend the token - /// @dev It should be cautiously called by governance as it could result in less ANGLE overall rewards than initially planned - /// if people do not remove their voting weights to the killed gauge - function toggleGauge(address gaugeAddr) external onlyRole(GOVERNOR_ROLE) { - bool gaugeKilledMem = killedGauges[gaugeAddr]; - if (!gaugeKilledMem) { - delete lastTimeGaugePaid[gaugeAddr]; - rewardToken.safeApprove(gaugeAddr, 0); - } - killedGauges[gaugeAddr] = !gaugeKilledMem; - emit GaugeToggled(gaugeAddr, !gaugeKilledMem); - } - - // ========================= Guardian Function ================================= - - /// @notice Halts or activates distribution of rewards - function toggleDistributions() external onlyRole(GUARDIAN_ROLE) { - bool distributionsOnMem = distributionsOn; - distributionsOn = !distributionsOnMem; - emit DistributionsToggled(!distributionsOnMem); - } - - /// @notice Notifies that the interface of a gauge delegate is known or has changed - /// @param _delegateGauge Address of the gauge to change - /// @dev Gauge delegates that are built for automation should be toggled - function toggleInterfaceKnown(address _delegateGauge) external onlyRole(GUARDIAN_ROLE) { - _toggleInterfaceKnown(_delegateGauge); - } -} diff --git a/contracts/mock/AngleDistributorEvents.sol b/contracts/mock/AngleDistributorEvents.sol deleted file mode 100644 index c572788..0000000 --- a/contracts/mock/AngleDistributorEvents.sol +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity ^0.8.7; - -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; - -import "../interfaces/CoreModuleInterfaces.sol"; - -import "../external/AccessControlUpgradeable.sol"; - -/// @title AngleDistributorEvents -/// @author Angle Core Team -/// @notice All the events used in `AngleDistributor` contract -contract AngleDistributorEvents { - event DelegateGaugeUpdated(address indexed _gaugeAddr, address indexed _delegateGauge); - event DistributionsToggled(bool _distributionsOn); - event GaugeControllerUpdated(address indexed _controller); - event GaugeToggled(address indexed gaugeAddr, bool newStatus); - event InterfaceKnownToggled(address indexed _delegateGauge, bool _isInterfaceKnown); - event RateUpdated(uint256 _newRate); - event Recovered(address indexed tokenAddress, address indexed to, uint256 amount); - event RewardDistributed(address indexed gaugeAddr, uint256 rewardTally); - event UpdateMiningParameters(uint256 time, uint256 rate, uint256 supply); -} diff --git a/contracts/mock/DistributionCreatorUpdatable.sol b/contracts/mock/DistributionCreatorUpdatable.sol index d74390d..d6e210d 100644 --- a/contracts/mock/DistributionCreatorUpdatable.sol +++ b/contracts/mock/DistributionCreatorUpdatable.sol @@ -41,17 +41,14 @@ import "../DistributionCreator.sol"; /// @author Angle Labs, Inc. //solhint-disable contract DistributionCreatorUpdatable is DistributionCreator { - uint8 public coreUpdated; uint256[49] private __gapUpdatable; - function updateCore(address _newCore) external { - if(coreUpdated == 0) { - core = ICore(_newCore); + if (coreUpdated == 0) { + core = IAccessControlManager(_newCore); coreUpdated = 1; } } - -} \ No newline at end of file +} diff --git a/contracts/mock/MockMerklFraxIncentivizationHandler.sol b/contracts/mock/MockMerklFraxIncentivizationHandler.sol index 1012c89..4c6c9e0 100644 --- a/contracts/mock/MockMerklFraxIncentivizationHandler.sol +++ b/contracts/mock/MockMerklFraxIncentivizationHandler.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.17; -import "../middleman/MerklFraxIncentivizationHandler.sol"; +import "../partners/middleman/MerklFraxIncentivizationHandler.sol"; contract MockMerklFraxIncentivizationHandler is MerklFraxIncentivizationHandler { DistributionCreator public manager; diff --git a/contracts/mock/MockMerklGaugeMiddleman.sol b/contracts/mock/MockMerklGaugeMiddleman.sol index 9b17d37..41a4c17 100644 --- a/contracts/mock/MockMerklGaugeMiddleman.sol +++ b/contracts/mock/MockMerklGaugeMiddleman.sol @@ -2,14 +2,14 @@ pragma solidity ^0.8.17; -import "../middleman/MerklGaugeMiddleman.sol"; +import "../partners/middleman/MerklGaugeMiddleman.sol"; contract MockMerklGaugeMiddleman is MerklGaugeMiddleman { address public angleDistributorAddress; IERC20 public angleAddress; DistributionCreator public manager; - constructor(ICore _coreBorrow) MerklGaugeMiddleman(_coreBorrow) {} + constructor(IAccessControlManager _coreBorrow) MerklGaugeMiddleman(_coreBorrow) {} function angle() public view override returns (IERC20) { return angleAddress; diff --git a/contracts/mock/MockTreasury.sol b/contracts/mock/MockTreasury.sol deleted file mode 100644 index 388861c..0000000 --- a/contracts/mock/MockTreasury.sol +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity ^0.8.17; - -contract MockTreasury { - address public stablecoin; - address public governor; - address public guardian; - address public vaultManager1; - address public vaultManager2; - address public flashLoanModule; - - constructor( - address _stablecoin, - address _governor, - address _guardian, - address _vaultManager1, - address _vaultManager2, - address _flashLoanModule - ) { - stablecoin = _stablecoin; - governor = _governor; - guardian = _guardian; - vaultManager1 = _vaultManager1; - vaultManager2 = _vaultManager2; - flashLoanModule = _flashLoanModule; - } - - function isGovernor(address admin) external view returns (bool) { - return (admin == governor); - } - - function isGovernorOrGuardian(address admin) external view returns (bool) { - return (admin == governor || admin == guardian); - } -} diff --git a/contracts/middleman/MerklFraxIncentivizationHandler.sol b/contracts/partners/middleman/MerklFraxIncentivizationHandler.sol similarity index 99% rename from contracts/middleman/MerklFraxIncentivizationHandler.sol rename to contracts/partners/middleman/MerklFraxIncentivizationHandler.sol index c67794d..e5e54c4 100644 --- a/contracts/middleman/MerklFraxIncentivizationHandler.sol +++ b/contracts/partners/middleman/MerklFraxIncentivizationHandler.sol @@ -6,7 +6,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; -import "../DistributionCreator.sol"; +import "../../DistributionCreator.sol"; /// @title MerklFraxIncentivizationHandler /// @author Angle Labs, Inc. diff --git a/contracts/middleman/MerklGaugeMiddleman.sol b/contracts/partners/middleman/MerklGaugeMiddleman.sol similarity index 93% rename from contracts/middleman/MerklGaugeMiddleman.sol rename to contracts/partners/middleman/MerklGaugeMiddleman.sol index d694c9e..00bfc22 100644 --- a/contracts/middleman/MerklGaugeMiddleman.sol +++ b/contracts/partners/middleman/MerklGaugeMiddleman.sol @@ -38,7 +38,7 @@ pragma solidity ^0.8.17; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "../DistributionCreator.sol"; +import "../../DistributionCreator.sol"; /// @title MerklGaugeMiddleman /// @author Angle Labs, Inc. @@ -52,7 +52,7 @@ contract MerklGaugeMiddleman { // ================================= PARAMETERS ================================ /// @notice Contract handling access control - ICore public accessControlManager; + IAccessControlManager public accessControlManager; /// @notice Maps a gauge to its reward parameters mapping(address => DistributionParameters) public gaugeParams; @@ -61,7 +61,7 @@ contract MerklGaugeMiddleman { event GaugeSet(address indexed gauge); - constructor(ICore _accessControlManager) { + constructor(IAccessControlManager _accessControlManager) { if (address(_accessControlManager) == address(0)) revert ZeroAddress(); accessControlManager = _accessControlManager; IERC20 _angle = angle(); @@ -97,13 +97,6 @@ contract MerklGaugeMiddleman { /// @notice Specifies the reward distribution parameters for `gauge` function setGauge(address gauge, DistributionParameters memory params) external { if (!accessControlManager.isGovernorOrGuardian(msg.sender)) revert NotGovernorOrGuardian(); - DistributionCreator manager = merklDistributionCreator(); - if ( - gauge == address(0) || - params.rewardToken != address(angle()) || - (manager.isWhitelistedToken(IUniswapV3Pool(params.uniV3Pool).token0()) == 0 && - manager.isWhitelistedToken(IUniswapV3Pool(params.uniV3Pool).token1()) == 0) - ) revert InvalidParams(); gaugeParams[gauge] = params; emit GaugeSet(gauge); } diff --git a/contracts/middleman/MerklGaugeMiddlemanPolygon.sol b/contracts/partners/middleman/MerklGaugeMiddlemanPolygon.sol similarity index 77% rename from contracts/middleman/MerklGaugeMiddlemanPolygon.sol rename to contracts/partners/middleman/MerklGaugeMiddlemanPolygon.sol index cc06ddc..1585039 100644 --- a/contracts/middleman/MerklGaugeMiddlemanPolygon.sol +++ b/contracts/partners/middleman/MerklGaugeMiddlemanPolygon.sol @@ -7,7 +7,7 @@ import "./MerklGaugeMiddleman.sol"; /// @title MerklGaugeMiddlemanPolygon /// @author Angle Labs, Inc. contract MerklGaugeMiddlemanPolygon is MerklGaugeMiddleman { - constructor(ICore _accessControlManager) MerklGaugeMiddleman(_accessControlManager) {} + constructor(IAccessControlManager _accessControlManager) MerklGaugeMiddleman(_accessControlManager) {} function angle() public pure override returns (IERC20) { return IERC20(0x900F717EA076E1E7a484ad9DD2dB81CEEc60eBF1); diff --git a/contracts/tokenWrappers/AaveTokenWrapper.sol b/contracts/partners/tokenWrappers/AaveTokenWrapper.sol similarity index 95% rename from contracts/tokenWrappers/AaveTokenWrapper.sol rename to contracts/partners/tokenWrappers/AaveTokenWrapper.sol index 2d5d895..e2ddab3 100644 --- a/contracts/tokenWrappers/AaveTokenWrapper.sol +++ b/contracts/partners/tokenWrappers/AaveTokenWrapper.sol @@ -5,9 +5,9 @@ pragma solidity ^0.8.17; import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -import "../DistributionCreator.sol"; +import "../../DistributionCreator.sol"; -import "../utils/UUPSHelper.sol"; +import "../../utils/UUPSHelper.sol"; contract AaveTokenWrapper is UUPSHelper, ERC20Upgradeable { using SafeERC20 for IERC20; @@ -15,7 +15,7 @@ contract AaveTokenWrapper is UUPSHelper, ERC20Upgradeable { // ================================= VARIABLES ================================= /// @notice `Core` contract handling access control - ICore public core; + IAccessControlManager public core; // could be put as immutable in non upgradeable contract address public token; @@ -53,11 +53,11 @@ contract AaveTokenWrapper is UUPSHelper, ERC20Upgradeable { __UUPSUpgradeable_init(); if (underlyingToken == address(0) || _distributor == address(0) || _distributionCreator == address(0)) revert ZeroAddress(); - ICore(_core).isGovernor(msg.sender); + IAccessControlManager(_core).isGovernor(msg.sender); token = underlyingToken; distributor = _distributor; distributionCreator = _distributionCreator; - core = ICore(_core); + core = IAccessControlManager(_core); } function _beforeTokenTransfer(address from, address to, uint256 amount) internal override { diff --git a/contracts/tokenWrappers/BaseTokenWrapper.sol b/contracts/partners/tokenWrappers/BaseTokenWrapper.sol similarity index 94% rename from contracts/tokenWrappers/BaseTokenWrapper.sol rename to contracts/partners/tokenWrappers/BaseTokenWrapper.sol index 4a13e41..5432062 100644 --- a/contracts/tokenWrappers/BaseTokenWrapper.sol +++ b/contracts/partners/tokenWrappers/BaseTokenWrapper.sol @@ -6,7 +6,7 @@ import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC2 import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IERC20, IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -import "../utils/UUPSHelper.sol"; +import "../../utils/UUPSHelper.sol"; interface IDistributionCreator { function distributor() external view returns (address); @@ -27,7 +27,7 @@ abstract contract BaseMerklTokenWrapper is UUPSHelper, ERC20Upgradeable { // ================================= VARIABLES ================================= /// @notice `Core` contract handling access control - ICore public core; + IAccessControlManager public core; // =================================== EVENTS ================================== @@ -49,7 +49,7 @@ abstract contract BaseMerklTokenWrapper is UUPSHelper, ERC20Upgradeable { return true; } - function initialize(ICore _core) public initializer onlyProxy { + function initialize(IAccessControlManager _core) public initializer onlyProxy { __ERC20_init( string.concat("Merkl Token Wrapper - ", IERC20Metadata(token()).name()), string.concat("mtw", IERC20Metadata(token()).symbol()) diff --git a/contracts/partners/tokenWrappers/PufferPointTokenWrapper.sol b/contracts/partners/tokenWrappers/PufferPointTokenWrapper.sol new file mode 100644 index 0000000..58cc6e8 --- /dev/null +++ b/contracts/partners/tokenWrappers/PufferPointTokenWrapper.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.17; + +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +import { BaseMerklTokenWrapper, IAccessControlManager } from "./BaseTokenWrapper.sol"; + +import "../../utils/Errors.sol"; + +struct VestingID { + uint128 amount; + uint128 unlockTimestamp; +} + +struct VestingData { + VestingID[] allVestings; + uint256 nextClaimIndex; +} + +/// @title PufferPointTokenWrapper +/// @dev This token can only be held by Merkl distributor +/// @dev Transferring to the distributor will require transferring the underlying token to this contract +/// @dev Transferring from the distributor will trigger a vesting action +/// @dev Transferring token to the distributor is permissionless so anyone could mint this wrapper - the only +/// impact would be to forfeit these tokens +contract PufferPointTokenWrapper is BaseMerklTokenWrapper { + using SafeERC20 for IERC20; + + // ================================= CONSTANTS ================================= + + mapping(address => VestingData) public vestingData; + uint256 public cliffDuration; + address public underlying; + + // ================================= FUNCTIONS ================================= + + function initializeWrapper(address _underlying, uint256 _cliffDuration, IAccessControlManager _core) public { + super.initialize(_core); + if (_underlying == address(0)) revert ZeroAddress(); + underlying = _underlying; + cliffDuration = _cliffDuration; + } + + function token() public view override returns (address) { + return underlying; + } + + function _beforeTokenTransfer(address from, address to, uint256 amount) internal override { + // Needs an RDNT approval beforehand, this is how mints of coupons are done + if (to == DISTRIBUTOR) { + IERC20(underlying).safeTransferFrom(from, address(this), amount); + _mint(from, amount); // These are then transferred to the distributor + } + + // Will be burn right after, to avoid having any token aside from on the distributor + if (to == FEE_RECIPIENT) { + IERC20(underlying).safeTransferFrom(from, FEE_RECIPIENT, amount); + _mint(from, amount); // These are then transferred to the fee manager + } + } + + function _afterTokenTransfer(address from, address to, uint256 amount) internal override { + if (to == FEE_RECIPIENT) { + _burn(to, amount); // To avoid having any token aside from on the distributor + } + + if (from == DISTRIBUTOR) { + _burn(to, amount); + _createVesting(to, amount); + } + } + + function _createVesting(address user, uint256 amount) internal { + VestingData storage userVestingData = vestingData[user]; + VestingID[] storage userAllVestings = userVestingData.allVestings; + userAllVestings.push(VestingID(uint128(amount), uint128(block.timestamp + cliffDuration))); + } + + function claim(address user) external { + VestingData storage userVestingData = vestingData[user]; + VestingID[] storage userAllVestings = userVestingData.allVestings; + uint256 i = userVestingData.nextClaimIndex; + uint256 claimable; + while (true) { + VestingID storage userCurrentVesting = userAllVestings[i]; + if (userCurrentVesting.unlockTimestamp > block.timestamp) { + claimable += userCurrentVesting.amount; + ++i; + } else { + userVestingData.nextClaimIndex = i; + break; + } + } + IERC20(token()).safeTransfer(user, claimable); + } + + function getUserVestings( + address user + ) external view returns (VestingID[] memory allVestings, uint256 nextClaimIndex) { + VestingData storage userVestingData = vestingData[user]; + allVestings = userVestingData.allVestings; + nextClaimIndex = userVestingData.nextClaimIndex; + } +} diff --git a/contracts/partners/tokenWrappers/PullTokenWrapper.sol b/contracts/partners/tokenWrappers/PullTokenWrapper.sol new file mode 100644 index 0000000..0c1701f --- /dev/null +++ b/contracts/partners/tokenWrappers/PullTokenWrapper.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.17; + +import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import "../../DistributionCreator.sol"; + +import "../../utils/UUPSHelper.sol"; + +/// @title PullTokenWrapper +/// @notice Wrapper for a reward token on Merkl so campaigns do not have to be prefunded +contract PullTokenWrapper is UUPSHelper, ERC20Upgradeable { + using SafeERC20 for IERC20; + + // ================================= VARIABLES ================================= + + /// @notice `Core` contract handling access control + IAccessControlManager public core; + + // Could be put as immutable in a non upgradeable contract + address public token; + address public holder; + address public distributor; + address public distributionCreator; + + // ================================= MODIFIERS ================================= + + /// @notice Checks whether the `msg.sender` has the governor role or the guardian role + modifier onlyHolderOrGovernor() { + if (msg.sender != holder && !core.isGovernor(msg.sender)) revert NotAllowed(); + _; + } + + // ================================= FUNCTIONS ================================= + + function initialize( + address underlyingToken, + address _core, + address _distributionCreator, + address _holder, + string memory name, + string memory symbol + ) public initializer { + __ERC20_init(string.concat(name), string.concat(symbol)); + __UUPSUpgradeable_init(); + if (underlyingToken == address(0) || holder == address(0)) revert ZeroAddress(); + IAccessControlManager(_core).isGovernor(msg.sender); + distributor = DistributionCreator(_distributionCreator).distributor(); + token = underlyingToken; + distributionCreator = _distributionCreator; + holder = _holder; + core = IAccessControlManager(_core); + } + + function _beforeTokenTransfer(address from, address to, uint256 amount) internal override { + // During claim transactions, tokens are pulled and transferred to the `to` address + if (from == distributor || to == _getFeeRecipient()) IERC20(token).safeTransferFrom(holder, to, amount); + } + + function _afterTokenTransfer(address, address to, uint256 amount) internal override { + // No leftover tokens can be kept except on the holder address + if (to != address(distributor) && to != holder) _burn(to, amount); + } + + function _getFeeRecipient() internal view returns (address feeRecipient) { + address _distributionCreator = distributionCreator; + feeRecipient = DistributionCreator(_distributionCreator).feeRecipient(); + feeRecipient = feeRecipient == address(0) ? _distributionCreator : feeRecipient; + } + + function setHolder(address _newHolder) external onlyHolderOrGovernor { + holder = _newHolder; + } + + function mint(uint256 amount) external onlyHolderOrGovernor { + _mint(holder, amount); + } + + /// @inheritdoc UUPSUpgradeable + function _authorizeUpgrade(address) internal view override onlyGovernorUpgrader(core) {} +} diff --git a/contracts/tokenWrappers/StakedToken.sol b/contracts/partners/tokenWrappers/StakedToken.sol similarity index 100% rename from contracts/tokenWrappers/StakedToken.sol rename to contracts/partners/tokenWrappers/StakedToken.sol diff --git a/contracts/struct/CampaignParameters.sol b/contracts/struct/CampaignParameters.sol index 4f75f4e..b5164ad 100644 --- a/contracts/struct/CampaignParameters.sol +++ b/contracts/struct/CampaignParameters.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.17; +pragma solidity >=0.8.0; struct CampaignParameters { // POPULATED ONCE CREATED diff --git a/contracts/struct/DistributionParameters.sol b/contracts/struct/DistributionParameters.sol index 8efdf8f..20e0621 100644 --- a/contracts/struct/DistributionParameters.sol +++ b/contracts/struct/DistributionParameters.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.17; +pragma solidity >=0.8.0; struct DistributionParameters { // ID of the reward (populated once created). This can be left as a null bytes32 when creating distributions diff --git a/contracts/tokenWrappers/RadiantTokenWrapper.sol b/contracts/tokenWrappers/RadiantTokenWrapper.sol deleted file mode 100644 index 5f2c0e6..0000000 --- a/contracts/tokenWrappers/RadiantTokenWrapper.sol +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -pragma solidity ^0.8.17; - -import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { IERC20, IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; - -import { BaseMerklTokenWrapper } from "./BaseTokenWrapper.sol"; - -interface IVesting { - function rdntToken() external view returns (address); - function vestTokens(address, uint256, bool) external; -} - -/// @title Radiant MTW -/// @dev This token can only be held by merkl distributor -/// @dev Transferring to the distributor will require transferring the underlying token to this contract -/// @dev Transferring from the distributor will trigger vesting action -/// @dev Transferring token to the distributor is permissionless so anyone could mint this wrapper - the only -/// impact would be to forfeit these tokens -contract RadiantMerklTokenWrapper is BaseMerklTokenWrapper { - using SafeERC20 for IERC20; - - // ================================= CONSTANTS ================================= - - IVesting public constant VESTING = IVesting(0x76ba3eC5f5adBf1C58c91e86502232317EeA72dE); - address internal immutable _UNDERLYING = VESTING.rdntToken(); - - // ================================= FUNCTIONS ================================= - - function token() public view override returns (address) { - return _UNDERLYING; - } - - function _beforeTokenTransfer(address from, address to, uint256 amount) internal override { - // Needs an RDNT approval beforehand, this is how mints of coupons are done - if (to == DISTRIBUTOR) { - IERC20(_UNDERLYING).safeTransferFrom(from, address(this), amount); - _mint(from, amount); // These are then transferred to the distributor - } - - // Will be burn right after, to avoid having any token aside from on the distributor - if (to == FEE_RECIPIENT) { - IERC20(_UNDERLYING).safeTransferFrom(from, FEE_RECIPIENT, amount); - _mint(from, amount); // These are then transferred to the fee manager - } - } - - function _afterTokenTransfer(address from, address to, uint256 amount) internal override { - if (to == FEE_RECIPIENT) { - _burn(to, amount); // To avoid having any token aside from on the distributor - } - - if (from == DISTRIBUTOR) { - _burn(to, amount); - - // Vesting logic - IERC20(_UNDERLYING).transfer(address(VESTING), amount); - VESTING.vestTokens(to, amount, true); - } - } -} diff --git a/contracts/utils/Errors.sol b/contracts/utils/Errors.sol index b2ddd9e..709e22b 100644 --- a/contracts/utils/Errors.sol +++ b/contracts/utils/Errors.sol @@ -17,6 +17,7 @@ error InvalidUninitializedRoot(); error InvalidReward(); error InvalidSignature(); error NoDispute(); +error NotAllowed(); error NotGovernor(); error NotGovernorOrGuardian(); error NotSigned(); diff --git a/contracts/utils/UUPSHelper.sol b/contracts/utils/UUPSHelper.sol index 286454d..d6a5a3a 100644 --- a/contracts/utils/UUPSHelper.sol +++ b/contracts/utils/UUPSHelper.sol @@ -37,7 +37,7 @@ pragma solidity ^0.8.17; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import { ICore } from "../interfaces/ICore.sol"; +import { IAccessControlManager } from "../interfaces/IAccessControlManager.sol"; import "../utils/Errors.sol"; /// @title UUPSHelper @@ -45,12 +45,12 @@ import "../utils/Errors.sol"; /// @author Angle Labs., Inc /// @dev The 0 address check in the modifier allows the use of these modifiers during initialization abstract contract UUPSHelper is UUPSUpgradeable { - modifier onlyGuardianUpgrader(ICore _core) { + modifier onlyGuardianUpgrader(IAccessControlManager _core) { if (address(_core) != address(0) && !_core.isGovernorOrGuardian(msg.sender)) revert NotGovernorOrGuardian(); _; } - modifier onlyGovernorUpgrader(ICore _core) { + modifier onlyGovernorUpgrader(IAccessControlManager _core) { if (address(_core) != address(0) && !_core.isGovernor(msg.sender)) revert NotGovernor(); _; } diff --git a/test/foundry/Fixture.t.sol b/test/foundry/Fixture.t.sol index ccb7b77..681dfa6 100644 --- a/test/foundry/Fixture.t.sol +++ b/test/foundry/Fixture.t.sol @@ -10,7 +10,7 @@ import { DistributionCreator } from "../../contracts/DistributionCreator.sol"; import { MockTokenPermit } from "../../contracts/mock/MockTokenPermit.sol"; import { MockUniswapV3Pool } from "../../contracts/mock/MockUniswapV3Pool.sol"; import { MockCoreBorrow } from "../../contracts/mock/MockCoreBorrow.sol"; -import { ICore } from "../../contracts/interfaces/ICore.sol"; +import { IAccessControlManager } from "../../contracts/interfaces/IAccessControlManager.sol"; import "../../contracts/utils/UUPSHelper.sol"; import { console } from "forge-std/console.sol"; @@ -72,7 +72,7 @@ contract Fixture is Test { pool.setToken(address(token1), 1); coreBorrow.toggleGuardian(address(guardian)); coreBorrow.toggleGovernor(address(governor)); - creator.initialize(ICore(address(coreBorrow)), address(bob), 1e8); + creator.initialize(IAccessControlManager(address(coreBorrow)), address(bob), 1e8); } /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/test/foundry/unit/DistributionCreator.t.sol b/test/foundry/unit/DistributionCreator.t.sol index 86b56db..92c898b 100644 --- a/test/foundry/unit/DistributionCreator.t.sol +++ b/test/foundry/unit/DistributionCreator.t.sol @@ -96,24 +96,24 @@ contract Test_DistributionCreator_Initialize is DistributionCreatorTest { function test_RevertWhen_CalledOnImplem() public { vm.expectRevert("Initializable: contract is already initialized"); - creatorImpl.initialize(ICore(address(0)), address(bob), 1e8); + creatorImpl.initialize(IAccessControlManager(address(0)), address(bob), 1e8); } function test_RevertWhen_ZeroAddress() public { vm.expectRevert(ZeroAddress.selector); - d.initialize(ICore(address(0)), address(bob), 1e8); + d.initialize(IAccessControlManager(address(0)), address(bob), 1e8); vm.expectRevert(ZeroAddress.selector); - d.initialize(ICore(address(coreBorrow)), address(0), 1e8); + d.initialize(IAccessControlManager(address(coreBorrow)), address(0), 1e8); } function test_RevertWhen_InvalidParam() public { vm.expectRevert(InvalidParam.selector); - d.initialize(ICore(address(coreBorrow)), address(bob), 1e9); + d.initialize(IAccessControlManager(address(coreBorrow)), address(bob), 1e9); } function test_Success() public { - d.initialize(ICore(address(coreBorrow)), address(bob), 1e8); + d.initialize(IAccessControlManager(address(coreBorrow)), address(bob), 1e8); assertEq(address(d.distributor()), address(bob)); assertEq(address(d.core()), address(coreBorrow)); @@ -842,14 +842,14 @@ contract Test_DistributionCreator_signAndCreateCampaign is DistributionCreatorTe }); { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(2, creator.messageHash()); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(2, creator.messageHash()); - vm.startPrank(bob); + vm.startPrank(bob); - angle.approve(address(creator), 1e8); - creator.signAndCreateCampaign(campaign, abi.encodePacked(r, s, v)); + angle.approve(address(creator), 1e8); + creator.signAndCreateCampaign(campaign, abi.encodePacked(r, s, v)); - vm.stopPrank(); + vm.stopPrank(); } address[] memory whitelist = new address[](1); @@ -890,7 +890,7 @@ contract Test_DistributionCreator_signAndCreateCampaign is DistributionCreatorTe assertEq(campaign.duration, fetchedDuration); assertEq(extraData, fetchedCampaignData); assertEq(campaignId, fetchedCampaignId); - assertEq(campaign.amount, fetchedAmount * 10 / 9); + assertEq(campaign.amount, (fetchedAmount * 10) / 9); } function test_InvalidSignature() public { @@ -906,15 +906,15 @@ contract Test_DistributionCreator_signAndCreateCampaign is DistributionCreatorTe }); { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, creator.messageHash()); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, creator.messageHash()); - vm.startPrank(bob); + vm.startPrank(bob); - angle.approve(address(creator), 1e8); - vm.expectRevert(InvalidSignature.selector); - creator.signAndCreateCampaign(campaign, abi.encodePacked(r, s, v)); + angle.approve(address(creator), 1e8); + vm.expectRevert(InvalidSignature.selector); + creator.signAndCreateCampaign(campaign, abi.encodePacked(r, s, v)); - vm.stopPrank(); + vm.stopPrank(); } } } @@ -930,7 +930,6 @@ contract DistributionCreatorForkTest is Test { } contract Test_DistributionCreator_distribution is DistributionCreatorForkTest { - function test_Success() public { CampaignParameters memory distribution = creator.distribution(0); @@ -941,14 +940,21 @@ contract Test_DistributionCreator_distribution is DistributionCreatorForkTest { assertEq(distribution.campaignType, 2); assertEq(distribution.startTimestamp, 1681380000); assertEq(distribution.duration, 86400); - assertEq(distribution.campaignData, hex"000000000000000000000000149e36e72726e0bcea5c59d40df2c43f60f5a22d0000000000000000000000000000000000000000000000000000000000000bb800000000000000000000000000000000000000000000000000000000000007d000000000000000000000000000000000000000000000000000000000000013880000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000023078000000000000000000000000000000000000000000000000000000000000"); + assertEq( + distribution.campaignData, + hex"000000000000000000000000149e36e72726e0bcea5c59d40df2c43f60f5a22d0000000000000000000000000000000000000000000000000000000000000bb800000000000000000000000000000000000000000000000000000000000007d000000000000000000000000000000000000000000000000000000000000013880000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000023078000000000000000000000000000000000000000000000000000000000000" + ); } } contract Test_DistributionCreator_getDistributionsBetweenEpochs is DistributionCreatorForkTest { - function test_Success() public { - (DistributionParameters[] memory distributions,) = creator.getDistributionsBetweenEpochs(1681380000, 1681380000 + 3600, 0, type(uint32).max); + (DistributionParameters[] memory distributions, ) = creator.getDistributionsBetweenEpochs( + 1681380000, + 1681380000 + 3600, + 0, + type(uint32).max + ); assertEq(distributions.length, 1); assertEq(distributions[0].uniV3Pool, address(0x149e36E72726e0BceA5c59d40df2c43F60f5A22D)); @@ -964,7 +970,13 @@ contract Test_DistributionCreator_getDistributionsBetweenEpochs is DistributionC assertEq(distributions[0].numEpoch, 24); assertEq(distributions[0].boostedReward, 0); assertEq(distributions[0].boostingAddress, address(0)); - assertEq(distributions[0].rewardId, bytes32(0x7570c9deb1660ed82ff01f760b2883edb9bdb881933b0e4085854d0d717ea268)); - assertEq(distributions[0].additionalData, hex"290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563"); + assertEq( + distributions[0].rewardId, + bytes32(0x7570c9deb1660ed82ff01f760b2883edb9bdb881933b0e4085854d0d717ea268) + ); + assertEq( + distributions[0].additionalData, + hex"290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563" + ); } -} \ No newline at end of file +} diff --git a/test/foundry/unit/Distributor.t.sol b/test/foundry/unit/Distributor.t.sol index 0124a11..1e4ca14 100644 --- a/test/foundry/unit/Distributor.t.sol +++ b/test/foundry/unit/Distributor.t.sol @@ -13,7 +13,7 @@ contract DistributorCreatorTest is Fixture { distributorImpl = new Distributor(); distributor = Distributor(deployUUPS(address(distributorImpl), hex"")); - distributor.initialize(ICore(address(coreBorrow))); + distributor.initialize(IAccessControlManager(address(coreBorrow))); vm.startPrank(governor); distributor.setDisputeAmount(1e18); @@ -39,16 +39,16 @@ contract Test_Distributor_Initialize is DistributorCreatorTest { function test_RevertWhen_CalledOnImplem() public { vm.expectRevert("Initializable: contract is already initialized"); - distributorImpl.initialize(ICore(address(0))); + distributorImpl.initialize(IAccessControlManager(address(0))); } function test_RevertWhen_ZeroAddress() public { vm.expectRevert(ZeroAddress.selector); - d.initialize(ICore(address(0))); + d.initialize(IAccessControlManager(address(0))); } function test_Success() public { - d.initialize(ICore(address(coreBorrow))); + d.initialize(IAccessControlManager(address(coreBorrow))); assertEq(address(coreBorrow), address(d.core())); } @@ -164,12 +164,12 @@ contract Test_Distributor_setDisputeAmount is DistributorCreatorTest { contract Test_Distributor_updateTree is DistributorCreatorTest { function test_RevertWhen_NotTrusted() public { vm.expectRevert(NotTrusted.selector); - distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + distributor.updateTree(MerkleTree({ merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH") })); } function test_RevertWhen_DisputeOngoing() public { vm.prank(governor); - distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + distributor.updateTree(MerkleTree({ merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH") })); vm.warp(distributor.endOfDisputePeriod() - 1); vm.startPrank(alice); @@ -179,12 +179,12 @@ contract Test_Distributor_updateTree is DistributorCreatorTest { vm.expectRevert(NotTrusted.selector); vm.prank(governor); - distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + distributor.updateTree(MerkleTree({ merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH") })); } function test_RevertWhen_DisputeNotFinished() public { vm.prank(governor); - distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + distributor.updateTree(MerkleTree({ merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH") })); vm.warp(distributor.endOfDisputePeriod() - 1); vm.startPrank(alice); @@ -196,12 +196,12 @@ contract Test_Distributor_updateTree is DistributorCreatorTest { vm.expectRevert(NotTrusted.selector); vm.prank(governor); - distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + distributor.updateTree(MerkleTree({ merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH") })); } function test_Success() public { vm.prank(governor); - distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + distributor.updateTree(MerkleTree({ merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH") })); (bytes32 merkleRoot, bytes32 ipfsHash) = distributor.tree(); assertEq(merkleRoot, getRoot()); @@ -220,7 +220,7 @@ contract Test_Distributor_updateTree is DistributorCreatorTest { assertEq(merkleRoot, getRoot()); vm.prank(governor); - distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HAS")})); + distributor.updateTree(MerkleTree({ merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HAS") })); (merkleRoot, ipfsHash) = distributor.lastTree(); assertEq(merkleRoot, getRoot()); @@ -231,7 +231,7 @@ contract Test_Distributor_updateTree is DistributorCreatorTest { contract Test_Distributor_revokeTree is DistributorCreatorTest { function test_RevertWhen_NotGovernorOrGuardian() public { vm.prank(governor); - distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + distributor.updateTree(MerkleTree({ merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH") })); vm.expectRevert(NotGovernorOrGuardian.selector); distributor.revokeTree(); @@ -239,7 +239,7 @@ contract Test_Distributor_revokeTree is DistributorCreatorTest { function test_RevertWhen_UnresolvedDispute() public { vm.prank(governor); - distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + distributor.updateTree(MerkleTree({ merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH") })); vm.warp(distributor.endOfDisputePeriod() - 1); vm.startPrank(alice); @@ -254,7 +254,7 @@ contract Test_Distributor_revokeTree is DistributorCreatorTest { function test_Success() public { vm.prank(governor); - distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + distributor.updateTree(MerkleTree({ merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH") })); vm.prank(governor); distributor.revokeTree(); @@ -271,7 +271,7 @@ contract Test_Distributor_revokeTree is DistributorCreatorTest { contract Test_Distributor_disputeTree is DistributorCreatorTest { function test_RevertWhen_UnresolvedDispute() public { vm.prank(governor); - distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + distributor.updateTree(MerkleTree({ merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH") })); vm.warp(distributor.endOfDisputePeriod() - 1); vm.startPrank(alice); @@ -292,7 +292,7 @@ contract Test_Distributor_disputeTree is DistributorCreatorTest { function test_Success() public { vm.prank(governor); - distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + distributor.updateTree(MerkleTree({ merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH") })); vm.warp(distributor.endOfDisputePeriod() - 1); vm.startPrank(alice); @@ -318,7 +318,7 @@ contract Test_Distributor_resolveDispute is DistributorCreatorTest { function test_SuccessValid() public { vm.prank(governor); - distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + distributor.updateTree(MerkleTree({ merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH") })); vm.warp(distributor.endOfDisputePeriod() - 1); vm.startPrank(alice); @@ -343,7 +343,7 @@ contract Test_Distributor_resolveDispute is DistributorCreatorTest { function test_SuccessInvalid() public { vm.prank(governor); - distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + distributor.updateTree(MerkleTree({ merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH") })); vm.warp(distributor.endOfDisputePeriod() - 1); vm.startPrank(alice); @@ -370,7 +370,7 @@ contract Test_Distributor_resolveDispute is DistributorCreatorTest { contract Test_Distributor_claim is DistributorCreatorTest { function test_RevertWhen_NotWhitelisted() public { vm.prank(governor); - distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + distributor.updateTree(MerkleTree({ merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH") })); vm.warp(distributor.endOfDisputePeriod() + 1); @@ -393,7 +393,7 @@ contract Test_Distributor_claim is DistributorCreatorTest { function test_RevertWhen_InvalidLengths() public { vm.prank(governor); - distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + distributor.updateTree(MerkleTree({ merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH") })); vm.warp(distributor.endOfDisputePeriod() + 1); @@ -427,7 +427,7 @@ contract Test_Distributor_claim is DistributorCreatorTest { function test_RevertWhen_InvalidProof() public { vm.prank(governor); - distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + distributor.updateTree(MerkleTree({ merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH") })); vm.warp(distributor.endOfDisputePeriod() + 1); @@ -447,7 +447,12 @@ contract Test_Distributor_claim is DistributorCreatorTest { function test_SuccessGovernor() public { console.log(alice, bob, address(angle), address(agEUR)); vm.prank(governor); - distributor.updateTree(MerkleTree({merkleRoot: bytes32(0x0b70a97c062cb747158b89e27df5bbda859ba072232efcbe92e383e9d74b8555), ipfsHash: keccak256("IPFS_HASH")})); + distributor.updateTree( + MerkleTree({ + merkleRoot: bytes32(0x0b70a97c062cb747158b89e27df5bbda859ba072232efcbe92e383e9d74b8555), + ipfsHash: keccak256("IPFS_HASH") + }) + ); angle.mint(address(distributor), 1e18); agEUR.mint(address(distributor), 5e17); @@ -482,7 +487,12 @@ contract Test_Distributor_claim is DistributorCreatorTest { function test_SuccessOperator() public { console.log(alice, bob, address(angle), address(agEUR)); vm.prank(governor); - distributor.updateTree(MerkleTree({merkleRoot: bytes32(0x0b70a97c062cb747158b89e27df5bbda859ba072232efcbe92e383e9d74b8555), ipfsHash: keccak256("IPFS_HASH")})); + distributor.updateTree( + MerkleTree({ + merkleRoot: bytes32(0x0b70a97c062cb747158b89e27df5bbda859ba072232efcbe92e383e9d74b8555), + ipfsHash: keccak256("IPFS_HASH") + }) + ); angle.mint(address(distributor), 1e18); agEUR.mint(address(distributor), 5e17);