diff --git a/.gitmodules b/.gitmodules index 888d42d..74640c6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "lib/utils"] + path = lib/utils + url = https://github.com/AngleProtocol/utils diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index ef8ff11..714d0a8 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -60,7 +60,7 @@ representative at an online or offline event. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at -contact@angle.money. +. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the @@ -116,7 +116,7 @@ the community. This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at -https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. +. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). @@ -124,5 +124,5 @@ enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at -https://www.contributor-covenant.org/faq. Translations are available at -https://www.contributor-covenant.org/translations. +. Translations are available at +. 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..f76b2a0 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,14 @@ [![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 @@ -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` @@ -84,13 +84,47 @@ forge update ## Verifying -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` +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 +``` ## Audits @@ -98,4 +132,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..7a1a0fc 100644 --- a/contracts/DistributionCreator.sol +++ b/contracts/DistributionCreator.sol @@ -36,26 +36,25 @@ 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 { IERC20 } 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 { 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"; import { RewardTokenAmounts } from "./struct/RewardTokenAmounts.sol"; +import { Distributor } from "./Distributor.sol"; /// @title DistributionCreator /// @author Angle Labs, Inc. /// @notice Manages the distribution of rewards through the Merkl system /// @dev This contract is mostly a helper for APIs built on top of Merkl -/// @dev This contract is an upgraded version and distinguishes two types of different rewards: +/// @dev This contract distinguishes two types of different rewards: /// - distributions: type of campaign for concentrated liquidity pools created before Feb 15 2024, /// now deprecated -/// - campaigns: the new more global name to describe any reward program on top of Merkl +/// - campaigns: the more global name to describe any reward program on top of Merkl //solhint-disable contract DistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable { using SafeERC20 for IERC20; @@ -71,8 +70,8 @@ contract DistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable { uint256 public immutable CHAIN_ID = block.chainid; - /// @notice `Core` contract handling access control - ICore public core; + /// @notice Contract handling access control + IAccessControlManager public core; /// @notice Contract distributing rewards to users address public distributor; @@ -127,6 +126,18 @@ contract DistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable { /// @notice Maps a campaign type to the fees for this specific campaign mapping(uint32 => uint256) public campaignSpecificFees; + /// @notice Maps a campaignId to a potential override written + mapping(bytes32 => CampaignParameters) public campaignOverrides; + + /// @notice Maps a campaignId to the block numbers at which it's been updated + mapping(bytes32 => uint256[]) public campaignOverridesTimestamp; + + /// @notice Maps one address to another one to reallocate rewards for a given campaign + mapping(bytes32 => mapping(address => address)) public campaignReallocation; + + /// @notice List all reallocated address for a given campaign + mapping(bytes32 => address[]) public campaignListReallocation; + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ @@ -135,6 +146,8 @@ contract DistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable { event FeeRebateUpdated(address indexed user, uint256 userFeeRebate); event FeeRecipientUpdated(address indexed _feeRecipient); event FeesSet(uint256 _fees); + event CampaignOverride(bytes32 _campaignId, CampaignParameters campaign); + event CampaignReallocation(bytes32 _campaignId, address[] indexed from, address indexed to); event CampaignSpecificFeesSet(uint32 campaignType, uint256 _fees); event MessageUpdated(bytes32 _messageHash); event NewCampaign(CampaignParameters campaign); @@ -175,7 +188,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; @@ -263,6 +276,64 @@ contract DistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable { return distributionAmounts; } + /// @notice Overrides a campaign with new parameters + /// @dev Some overrides maybe incorrect, but their correctness cannot be checked onchain. It is up to the Merkl + /// engine to check the validity of the override. If the override is invalid, then the first campaign details + /// will still apply. + /// @dev Some fields in the new campaign parameters will be disregarded anyway (like the amount) + function overrideCampaign(bytes32 _campaignId, CampaignParameters memory newCampaign) external { + CampaignParameters memory _campaign = campaign(_campaignId); + if ( + _campaign.creator != msg.sender || + newCampaign.rewardToken != _campaign.rewardToken || + newCampaign.amount != _campaign.amount || + newCampaign.startTimestamp != _campaign.startTimestamp || + // End timestamp should be in the future + newCampaign.duration <= block.timestamp - _campaign.startTimestamp + ) revert InvalidOverride(); + + // Take a new fee to not trick the system by creating a campaign with the smallest fee + // and then overriding it with a campaign with a bigger fee + _computeFees(newCampaign.campaignType, newCampaign.amount, newCampaign.rewardToken); + + newCampaign.campaignId = _campaignId; + newCampaign.creator = msg.sender; + campaignOverrides[_campaignId] = newCampaign; + campaignOverridesTimestamp[_campaignId].push(block.timestamp); + emit CampaignOverride(_campaignId, newCampaign); + } + + /// @notice Reallocates rewards of a given campaign from one address to another + /// @dev To prevent manipulations by campaign creators, this function can only be called by the + /// initial campaign creator if the `from` address has never claimed any reward on the chain + /// @dev Compute engine should also make sure when reallocating rewards that `from` claimed amount + /// is still 0 - otherwise double allocation can happen + /// @dev It is meant to be used for the case of addresses accruing rewards but unable to claim them + function reallocateCampaignRewards(bytes32 _campaignId, address[] memory froms, address to) external { + CampaignParameters memory _campaign = campaign(_campaignId); + if (_campaign.creator != msg.sender || block.timestamp < _campaign.startTimestamp + _campaign.duration) + revert InvalidOverride(); + + uint256 fromsLength = froms.length; + address[] memory successfullFrom = new address[](fromsLength); + uint256 count = 0; + for (uint256 i; i < fromsLength; i++) { + (uint208 amount, uint48 timestamp, ) = Distributor(distributor).claimed(froms[i], _campaign.rewardToken); + if (amount == 0 && timestamp == 0) { + successfullFrom[count] = froms[i]; + campaignReallocation[_campaignId][froms[i]] = to; + campaignListReallocation[_campaignId].push(froms[i]); + count++; + } + } + assembly { + mstore(successfullFrom, count) + } + + if (count == 0) revert InvalidOverride(); + emit CampaignReallocation(_campaignId, successfullFrom, to); + } + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// GETTERS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ @@ -280,12 +351,14 @@ contract DistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable { } /// @notice Returns the campaign parameters of a given campaignId - function campaign(bytes32 _campaignId) external view returns (CampaignParameters memory) { + /// @dev If a campaign has been overriden, this function still shows the original state of the campaign + function campaign(bytes32 _campaignId) public view returns (CampaignParameters memory) { return campaignList[campaignLookup(_campaignId)]; } /// @notice Returns the campaign ID for a given campaign /// @dev The campaign ID is computed as the hash of the following parameters: + /// - `campaign.chainId` /// - `campaign.creator` /// - `campaign.rewardToken` /// - `campaign.campaignType` @@ -342,20 +415,12 @@ contract DistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable { return _getCampaignsBetween(start, end, skip, first); } - /// @notice Gets all the distributions which were live at some point between `start` and `end` timestamp - /// @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 - /// @dev For pagniation purpose, in case of out of gas, you can call back the same function but with `skip` set to `lastIndexDistribution` - /// @dev Not to be queried on-chain and hence not optimized for gas consumption - function getDistributionsBetweenEpochs( - uint32 epochStart, - uint32 epochEnd, - uint32 skip, - uint32 first - ) external view returns (DistributionParameters[] memory, uint256 lastIndexDistribution) { - return _getDistributionsBetweenEpochs(_getRoundedEpoch(epochStart), _getRoundedEpoch(epochEnd), skip, first); + function getCampaignOverridesTimestamp(bytes32 _campaignId) external view returns (uint256[] memory) { + return campaignOverridesTimestamp[_campaignId]; + } + + function getCampaignListReallocation(bytes32 _campaignId) external view returns (address[] memory) { + return campaignListReallocation[_campaignId]; } /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -376,6 +441,31 @@ contract DistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable { emit FeesSet(_defaultFees); } + /// @notice Recovers fees accrued on the contract for a list of `tokens` + function recoverFees(IERC20[] calldata tokens, address to) external onlyGovernor { + uint256 tokensLength = tokens.length; + for (uint256 i; i < tokensLength; ) { + tokens[i].safeTransfer(to, tokens[i].balanceOf(address(this))); + unchecked { + ++i; + } + } + } + + /// @notice Sets a new address to receive fees + function setFeeRecipient(address _feeRecipient) external onlyGovernor { + 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 onlyGovernor { + message = _message; + bytes32 _messageHash = ECDSA.toEthSignedMessageHash(bytes(_message)); + messageHash = _messageHash; + emit MessageUpdated(_messageHash); + } + /// @notice Sets the fees specific for a campaign /// @dev To waive the fees for a campaign, set its fees to 1 function setCampaignFees(uint32 campaignType, uint256 _fees) external onlyGovernorOrGuardian { @@ -391,17 +481,6 @@ contract DistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable { emit TokenWhitelistToggled(token, toggleStatus); } - /// @notice Recovers fees accrued on the contract for a list of `tokens` - function recoverFees(IERC20[] calldata tokens, address to) external onlyGovernor { - uint256 tokensLength = tokens.length; - for (uint256 i; i < tokensLength; ) { - tokens[i].safeTransfer(to, tokens[i].balanceOf(address(this))); - unchecked { - ++i; - } - } - } - /// @notice Sets fee rebates for a given user function setUserFeeRebate(address user, uint256 userFeeRebate) external onlyGovernorOrGuardian { feeRebate[user] = userFeeRebate; @@ -419,26 +498,12 @@ contract DistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable { 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]); + 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 onlyGovernor { - 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 onlyGovernor { - 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]; @@ -465,12 +530,13 @@ contract DistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable { if (newCampaign.creator == address(0)) newCampaign.creator = msg.sender; // Computing fees: these are waived for whitelisted addresses and if there is a whitelisted token in a pool - uint256 _fees = campaignSpecificFees[newCampaign.campaignType]; - if (_fees == 1) _fees = 0; - else if (_fees == 0) _fees = defaultFees; - uint256 campaignAmountMinusFees = _computeFees(_fees, newCampaign.amount, newCampaign.rewardToken); + uint256 campaignAmountMinusFees = _computeFees( + newCampaign.campaignType, + newCampaign.amount, + newCampaign.rewardToken + ); + IERC20(newCampaign.rewardToken).safeTransferFrom(msg.sender, distributor, campaignAmountMinusFees); newCampaign.amount = campaignAmountMinusFees; - newCampaign.campaignId = campaignId(newCampaign); if (_campaignLookup[newCampaign.campaignId] != 0) revert CampaignAlreadyExists(); @@ -539,10 +605,14 @@ contract DistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable { /// @notice Computes the fees to be taken on a campaign and transfers them to the fee recipient function _computeFees( - uint256 baseFeesValue, + uint32 campaignType, uint256 distributionAmount, address rewardToken ) internal returns (uint256 distributionAmountMinusFees) { + uint256 baseFeesValue = campaignSpecificFees[campaignType]; + if (baseFeesValue == 1) baseFeesValue = 0; + else if (baseFeesValue == 0) baseFeesValue = defaultFees; + uint256 _fees = (baseFeesValue * (BASE_9 - feeRebate[msg.sender])) / BASE_9; distributionAmountMinusFees = distributionAmount; if (_fees != 0) { @@ -555,7 +625,6 @@ contract DistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable { distributionAmount - distributionAmountMinusFees ); } - IERC20(rewardToken).safeTransferFrom(msg.sender, distributor, distributionAmountMinusFees); } /// @notice Internal version of the `sign` function @@ -603,35 +672,6 @@ contract DistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable { return (activeRewards, i); } - /// @notice Internal version of `getDistributionsBetweenEpochs` - function _getDistributionsBetweenEpochs( - uint32 epochStart, - uint32 epochEnd, - uint32 skip, - uint32 first - ) internal view returns (DistributionParameters[] memory, uint256) { - uint256 length; - uint256 distributionListLength = distributionList.length; - uint256 returnSize = first > distributionListLength ? distributionListLength : first; - DistributionParameters[] memory activeRewards = new DistributionParameters[](returnSize); - uint32 i = skip; - while (i < distributionListLength) { - DistributionParameters memory d = distributionList[i]; - if (d.epochStart + d.numEpoch * HOUR > epochStart && d.epochStart < epochEnd) { - activeRewards[length] = d; - length += 1; - } - unchecked { - ++i; - } - if (length == returnSize) break; - } - assembly { - mstore(activeRewards, length) - } - return (activeRewards, i); - } - /// @notice Builds the list of valid reward tokens function _getValidRewardTokens( uint32 skip, @@ -665,5 +705,5 @@ contract DistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable { * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ - uint256[33] private __gap; + uint256[31] private __gap; } diff --git a/contracts/Distributor.sol b/contracts/Distributor.sol index 145893b..035083b 100644 --- a/contracts/Distributor.sol +++ b/contracts/Distributor.sol @@ -57,16 +57,26 @@ struct Claim { bytes32 merkleRoot; } +interface IClaimRecipient { + /// @notice Hook to call within contracts receiving token rewards on behalf of users + function onClaim(address user, address token, uint256 amount, bytes memory data) external returns (bytes32); +} + /// @title Distributor /// @notice Allows to claim rewards distributed to them through Merkl /// @author Angle Labs. Inc contract Distributor is UUPSHelper { using SafeERC20 for IERC20; - /// @notice Epoch duration + /// @notice Default epoch duration uint32 internal constant _EPOCH_DURATION = 3600; - // ================================= VARIABLES ================================= + /// @notice Success message received when calling a `ClaimRecipient` contract + bytes32 public constant CALLBACK_SUCCESS = keccak256("IClaimRecipient.onClaim"); + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + VARIABLES + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ /// @notice Tree of claimable tokens through this contract MerkleTree public tree; @@ -77,14 +87,14 @@ contract Distributor is UUPSHelper { /// @notice Token to deposit to freeze the roots update IERC20 public disputeToken; - /// @notice `Core` contract handling access control - ICore public core; + /// @notice Contract handling access control + IAccessControlManager public core; - /// @notice Address which created the dispute + /// @notice Address which created the last dispute /// @dev Used to store if there is an ongoing dispute address public disputer; - /// @notice When the current tree will become valid + /// @notice When the current tree becomes valid uint48 public endOfDisputePeriod; /// @notice Time after which a change in a tree becomes effective, in EPOCH_DURATION @@ -99,38 +109,52 @@ contract Distributor is UUPSHelper { /// @notice Trusted EOAs to update the Merkle root mapping(address => uint256) public canUpdateMerkleRoot; - /// @notice Whether or not to disable permissionless claiming + /// @notice Deprecated mapping mapping(address => uint256) public onlyOperatorCanClaim; - /// @notice user -> operator -> authorisation to claim + /// @notice User -> Operator -> authorisation to claim on behalf of the user mapping(address => mapping(address => uint256)) public operators; - uint256[38] private __gap; + /// @notice Whether the contract has been made non upgradeable or not + uint128 public upgradeabilityDeactivated; + + /// @notice Reentrancy status + uint96 private _status; + + /// @notice Epoch duration for dispute periods (in seconds) + uint32 internal _epochDuration; - // =================================== EVENTS ================================== + /// @notice user -> token -> recipient address for when user claims `token` + /// @dev If the mapping is empty, by default rewards will accrue on the user address + mapping(address => mapping(address => address)) public claimRecipient; + + uint256[36] private __gap; + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ event Claimed(address indexed user, address indexed token, uint256 amount); + event ClaimRecipientUpdated(address indexed user, address indexed token, address indexed recipient); event DisputeAmountUpdated(uint256 _disputeAmount); event Disputed(string reason); event DisputePeriodUpdated(uint48 _disputePeriod); event DisputeResolved(bool valid); event DisputeTokenUpdated(address indexed _disputeToken); + event EpochDurationUpdated(uint32 newEpochDuration); 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); + event UpgradeabilityRevoked(); - // ================================= MODIFIERS ================================= + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + 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` has the governor role or the guardian role + /// @notice Checks whether the `msg.sender` has the governor role modifier onlyGovernor() { if (!core.isGovernor(msg.sender)) revert NotGovernor(); _; @@ -143,25 +167,49 @@ contract Distributor is UUPSHelper { _; } - // ================================ CONSTRUCTOR ================================ + /// @notice Checks whether the contract is upgradeable or whether the caller is allowed to upgrade the contract + modifier onlyUpgradeableInstance() { + if (upgradeabilityDeactivated == 1) revert NotUpgradeable(); + else if (!core.isGovernor(msg.sender)) revert NotGovernor(); + _; + } + + /// @notice Checks whether a call is reentrant or not + modifier nonReentrant() { + if (_status == 2) revert ReentrantCall(); + + // Any calls to nonReentrant after this point will fail + _status = 2; + + _; + + // By storing the original value once again, a refund is triggered (see + // https://eips.ethereum.org/EIPS/eip-2200) + _status = 1; + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ constructor() initializer {} - function initialize(ICore _core) external initializer { + function initialize(IAccessControlManager _core) external initializer { if (address(_core) == address(0)) revert ZeroAddress(); core = _core; } /// @inheritdoc UUPSUpgradeable - function _authorizeUpgrade(address) internal view override onlyGovernorUpgrader(core) {} + function _authorizeUpgrade(address) internal view override onlyUpgradeableInstance {} - // =============================== MAIN FUNCTION =============================== + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + MAIN FUNCTIONS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ /// @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 + /// @dev Unless another address has been approved for claiming, only an address can claim for itself + /// @param users Addresses for which claiming is taking place + /// @param tokens ERC20 token 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( @@ -170,54 +218,76 @@ contract Distributor is UUPSHelper { 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]; - - // Only approved operator can claim for `user` - if (msg.sender != user && tx.origin != user && 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()); + address[] memory recipients = new address[](users.length); + bytes[] memory datas = new bytes[](users.length); + _claim(users, tokens, amounts, proofs, recipients, datas); + } - IERC20(token).safeTransfer(user, toSend); - emit Claimed(user, token, toSend); - unchecked { - ++i; - } - } + /// @notice Same as the function above except that for each token claimed, the caller may set different + /// recipients for rewards and pass arbitrary data to the reward recipient on claim + /// @dev Only a `msg.sender` calling for itself can set a different recipient for the token rewards + /// within the context of a call to claim + /// @dev Non-zero recipient addresses given by the `msg.sender` can override any previously set reward address + function claimWithRecipient( + address[] calldata users, + address[] calldata tokens, + uint256[] calldata amounts, + bytes32[][] calldata proofs, + address[] calldata recipients, + bytes[] memory datas + ) external { + _claim(users, tokens, amounts, proofs, recipients, datas); } - /// @notice Returns the MerkleRoot that is currently live for the contract + /// @notice Returns the Merkle root 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 =========================== + function getEpochDuration() public view returns (uint32 epochDuration) { + epochDuration = _epochDuration; + if (epochDuration == 0) epochDuration = _EPOCH_DURATION; + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + USER ADMIN FUNCTIONS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + /// @notice Toggles whitelisting for a given user and a given operator + /// @dev When an operator is whitelisted for a user, the operator can claim rewards on behalf of the user + 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 Sets a recipient for a user claiming rewards for a token + /// @dev This is an optional functionality and if the `recipient` is set to the zero address, then + /// the user will still accrue all rewards to its address + /// @dev Users may still specify a different recipient when they claim token rewards with the + /// `claimWithRecipient` function + function setClaimRecipient(address recipient, address token) external { + claimRecipient[msg.sender][token] = recipient; + emit ClaimRecipientUpdated(msg.sender, recipient, token); + } - /// @notice Adds or removes EOAs which are trusted to update the Merkle root - function toggleTrusted(address eoa) external onlyGovernor { - uint256 trustedStatus = 1 - canUpdateMerkleRoot[eoa]; - canUpdateMerkleRoot[eoa] = trustedStatus; - emit TrustedToggled(eoa, trustedStatus == 1); + /// @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 Updates Merkle Tree + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + GOVERNANCE FUNCTIONS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + /// @notice Updates the Merkle tree function updateTree(MerkleTree calldata _tree) external { if ( disputer != address(0) || @@ -235,15 +305,23 @@ contract Distributor is UUPSHelper { 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 Adds or removes addresses which are trusted to update the Merkle root + function toggleTrusted(address trustAddress) external onlyGovernor { + uint256 trustedStatus = 1 - canUpdateMerkleRoot[trustAddress]; + canUpdateMerkleRoot[trustAddress] = trustedStatus; + emit TrustedToggled(trustAddress, trustedStatus == 1); + } + + /// @notice Prevents future contract upgrades + function revokeUpgradeability() external onlyGovernor { + upgradeabilityDeactivated = 1; + emit UpgradeabilityRevoked(); + } + + /// @notice Updates the epoch duration period + function setEpochDuration(uint32 epochDuration) external onlyGovernor { + _epochDuration = epochDuration; + emit EpochDurationUpdated(epochDuration); } /// @notice Resolve the ongoing dispute, if any @@ -269,22 +347,7 @@ contract Distributor is UUPSHelper { _revokeTree(); } - /// @notice Toggles permissioned claiming for a given user - /// @dev deprecated - 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 + /// @notice Recovers any ERC20 token left on the contract function recoverERC20(address tokenAddress, address to, uint256 amountToRecover) external onlyGovernor { IERC20(tokenAddress).safeTransfer(to, amountToRecover); emit Recovered(tokenAddress, to, amountToRecover); @@ -310,25 +373,91 @@ contract Distributor is UUPSHelper { emit DisputeAmountUpdated(_disputeAmount); } - // ============================= INTERNAL FUNCTIONS ============================ + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + INTERNAL HELPERS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + /// @notice Internal version of `claimWithRecipient` + function _claim( + address[] calldata users, + address[] calldata tokens, + uint256[] calldata amounts, + bytes32[][] calldata proofs, + address[] memory recipients, + bytes[] memory datas + ) internal nonReentrant { + uint256 usersLength = users.length; + if ( + usersLength == 0 || + usersLength != tokens.length || + usersLength != amounts.length || + usersLength != proofs.length || + usersLength != recipients.length || + usersLength != datas.length + ) revert InvalidLengths(); + + for (uint256 i; i < usersLength; ) { + address user = users[i]; + address token = tokens[i]; + uint256 amount = amounts[i]; + bytes memory data = datas[i]; + + // Only approved operator can claim for `user` + if (msg.sender != user && tx.origin != user && 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()); + emit Claimed(user, token, toSend); + + address recipient = recipients[i]; + // Only `msg.sender` can set a different recipient for itself within the context of a call to claim + // The recipient set in the context of the call to `claim` can override the default recipient set by the user + if (msg.sender != user || recipient == address(0)) { + address userSetRecipient = claimRecipient[user][token]; + if (userSetRecipient == address(0)) recipient = user; + else recipient = userSetRecipient; + } + + if (toSend != 0) { + IERC20(token).safeTransfer(recipient, toSend); + if (data.length != 0) { + try IClaimRecipient(recipient).onClaim(user, token, amount, data) returns ( + bytes32 callbackSuccess + ) { + if (callbackSuccess != CALLBACK_SUCCESS) revert InvalidReturnMessage(); + } catch {} + } + } + unchecked { + ++i; + } + } + } /// @notice Fallback to the last version of the tree function _revokeTree() internal { MerkleTree memory _tree = lastTree; endOfDisputePeriod = 0; tree = _tree; + uint32 epochDuration = getEpochDuration(); emit Revoked(); emit TreeUpdated( _tree.merkleRoot, _tree.ipfsHash, - (uint48(block.timestamp) / _EPOCH_DURATION) * (_EPOCH_DURATION) // Last hour + (uint48(block.timestamp) / epochDuration) * (epochDuration) // 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); + uint32 epochDuration = getEpochDuration(); + return ((treeUpdate - 1) / epochDuration + 1 + disputePeriod) * (epochDuration); } /// @notice Checks the validity of a proof diff --git a/contracts/deprecated/OldDistributionCreator.sol b/contracts/deprecated/OldDistributionCreator.sol index 47745a7..bc4e4d4 100644 --- a/contracts/deprecated/OldDistributionCreator.sol +++ b/contracts/deprecated/OldDistributionCreator.sol @@ -39,60 +39,56 @@ import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/ 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 { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.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); -} +import { CampaignParameters } from "../struct/CampaignParameters.sol"; +import { DistributionParameters } from "../struct/DistributionParameters.sol"; +import { RewardTokenAmounts } from "../struct/RewardTokenAmounts.sol"; -/// @title DistributionCreator +/// @title OldDistributionCreator /// @author Angle Labs, Inc. -/// @notice Manages the distribution of rewards across different pools with concentrated liquidity (like on Uniswap V3) +/// @notice Manages the distribution of rewards through the Merkl system /// @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 +/// @dev This contract is an upgraded version and distinguishes two types of different rewards: +/// - distributions: type of campaign for concentrated liquidity pools created before Feb 15 2024, +/// now deprecated +/// - campaigns: the new more global name to describe any reward program on top of Merkl //solhint-disable contract OldDistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable { using SafeERC20 for IERC20; - // =========================== CONSTANTS / VARIABLES =========================== + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + CONSTANTS / VARIABLES + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - /// @notice Epoch duration - uint32 public constant EPOCH_DURATION = 3600; + uint32 public constant HOUR = 3600; /// @notice Base for fee computation uint256 public constant BASE_9 = 1e9; - /// @notice `Core` contract handling access control - ICore public core; + uint256 public immutable CHAIN_ID = block.chainid; + + /// @notice Contract handling access control + IAccessControlManager public core; - /// @notice User contract for distributing rewards + /// @notice Contract distributing rewards to users 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 Value (in base 10**9) of the fees taken when creating a campaign + uint256 public defaultFees; - /// @notice Message that needs to be acknowledged by users creating a distribution + /// @notice Message that needs to be acknowledged by users creating a campaign 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 + /// @notice List of all rewards distributed in the contract on campaigns created before mid Feb 2024 + /// for concentrated liquidity pools DistributionParameters[] public distributionList; /// @notice Maps an address to its fee rebate @@ -102,8 +98,8 @@ contract OldDistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable { /// 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 Deprecated, kept for storage compatibility + mapping(address => uint256) public _nonces; /// @notice Maps an address to the last valid hash signed mapping(address => bytes32) public userSignatures; @@ -118,22 +114,37 @@ contract OldDistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable { /// @notice List of all reward tokens that have at some point been accepted address[] public rewardTokens; - uint256[36] private __gap; + /// @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 + CampaignParameters[] public campaignList; + + /// @notice Maps a campaignId to the ID of the campaign in the campaign list + 1 + mapping(bytes32 => uint256) internal _campaignLookup; - // =================================== EVENTS ================================== + /// @notice Maps a campaign type to the fees for this specific campaign + mapping(uint32 => uint256) public campaignSpecificFees; + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ event DistributorUpdated(address indexed _distributor); event FeeRebateUpdated(address indexed user, uint256 userFeeRebate); event FeeRecipientUpdated(address indexed _feeRecipient); event FeesSet(uint256 _fees); + event CampaignSpecificFeesSet(uint32 campaignType, uint256 _fees); event MessageUpdated(bytes32 _messageHash); + event NewCampaign(CampaignParameters campaign); 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 ================================= + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + MODIFIERS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ /// @notice Checks whether the `msg.sender` has the governor role or the guardian role modifier onlyGovernorOrGuardian() { @@ -141,6 +152,12 @@ contract OldDistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable { _; } + /// @notice Checks whether the `msg.sender` has the governor role or the guardian role + modifier onlyGovernor() { + if (!core.isGovernor(msg.sender)) revert NotGovernor(); + _; + } + /// @notice Checks whether an address has signed the message or not modifier hasSigned() { if ( @@ -152,54 +169,57 @@ contract OldDistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable { _; } - // ================================ CONSTRUCTOR ================================ + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + 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; core = _core; - fees = _fees; + defaultFees = _fees; } constructor() initializer {} /// @inheritdoc UUPSUpgradeable - function _authorizeUpgrade(address) internal view override onlyGuardianUpgrader(core) {} + function _authorizeUpgrade(address) internal view override onlyGovernorUpgrader(core) {} - // ============================== DEPOSIT FUNCTION ============================= + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + USER FACING FUNCTIONS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - /// @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 + /// @notice Creates a `campaign` to incentivize a given pool for a specific period of time + /// @return The campaignId of the new campaign + /// @dev If the campaign is badly specified, it will not be handled by the campaign script and rewards may be lost + /// @dev Reward tokens sent as part of campaigns 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]); + /// @dev This function reverts if the sender has not accepted the terms and conditions + function createCampaign(CampaignParameters memory newCampaign) external nonReentrant hasSigned returns (bytes32) { + return _createCampaign(newCampaign); + } + + /// @notice Same as the function above but for multiple campaigns at once + /// @return List of all the campaign amounts actually deposited for each `campaign` in the `campaigns` list + function createCampaigns( + CampaignParameters[] memory campaigns + ) external nonReentrant hasSigned returns (bytes32[] memory) { + uint256 campaignsLength = campaigns.length; + bytes32[] memory campaignIds = new bytes32[](campaignsLength); + for (uint256 i; i < campaignsLength; ) { + campaignIds[i] = _createCampaign(campaigns[i]); unchecked { ++i; } } - return distributionAmounts; + return campaignIds; + } + + /// @notice Allows a user to accept the conditions without signing the message + /// @dev Users may either call `acceptConditions` here or `sign` the message + function acceptConditions() external { + userSignatureWhitelist[msg.sender] = 1; } /// @notice Checks whether the `msg.sender`'s `signature` is compatible with the message @@ -210,388 +230,156 @@ contract OldDistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable { _sign(signature); } - /// @notice Combines signing the message and creating a distribution - function signAndCreateDistribution( - DistributionParameters memory distribution, + /// @notice Combines signing the message and creating a campaign + function signAndCreateCampaign( + CampaignParameters memory newCampaign, bytes calldata signature - ) external returns (uint256 distributionAmount) { + ) external returns (bytes32) { _sign(signature); - return _createDistribution(distribution); + return _createCampaign(newCampaign); } - /// @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 Creates a `distribution` to incentivize a given pool for a specific period of time + function createDistribution( + DistributionParameters memory newDistribution + ) external nonReentrant hasSigned returns (uint256 distributionAmount) { + return _createDistribution(newDistribution); } - /// @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; - } + /// @notice Same as the function above but for multiple distributions at once + function createDistributions( + DistributionParameters[] memory distributions + ) external nonReentrant 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; } } - assembly { - mstore(validRewardTokens, length) - } - return validRewardTokens; + return distributionAmounts; } - /// @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 - ); - } + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + GETTERS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - /// @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 Returns the distribution at a given index converted into a campaign + function distribution(uint256 index) external view returns (CampaignParameters memory) { + return _convertDistribution(distributionList[index]); } - /// @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 index of a campaign in the campaign list + function campaignLookup(bytes32 _campaignId) public view returns (uint256) { + uint256 index = _campaignLookup[_campaignId]; + if (index == 0) revert CampaignDoesNotExist(); + return index - 1; } - /// @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 Returns the campaign parameters of a given campaignId + function campaign(bytes32 _campaignId) external view returns (CampaignParameters memory) { + return campaignList[campaignLookup(_campaignId)]; } - /// @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) { + /// @notice Returns the campaign ID for a given campaign + /// @dev The campaign ID is computed as the hash of the following parameters: + /// - `campaign.creator` + /// - `campaign.rewardToken` + /// - `campaign.campaignType` + /// - `campaign.startTimestamp` + /// - `campaign.duration` + /// - `campaign.campaignData` + /// This prevents the creation by the same account of two campaigns with the same parameters + /// which is not a huge issue + function campaignId(CampaignParameters memory campaignData) public view returns (bytes32) { return - _getPoolDistributionsBetweenEpochs(address(0), _getRoundedEpoch(epochStart), type(uint32).max, skip, first); + bytes32( + keccak256( + abi.encodePacked( + CHAIN_ID, + campaignData.creator, + campaignData.rewardToken, + campaignData.campaignType, + campaignData.startTimestamp, + campaignData.duration, + campaignData.campaignData + ) + ) + ); } - /// @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 Returns the list of all the reward tokens supported as well as their minimum amounts + /// @dev Not to be queried on-chain and hence not optimized for gas consumption + function getValidRewardTokens() external view returns (RewardTokenAmounts[] memory) { + (RewardTokenAmounts[] memory validRewardTokens, ) = _getValidRewardTokens(0, type(uint32).max); + return validRewardTokens; } - /// @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, + /// @dev Not to be queried on-chain and hence not optimized for gas consumption + function getValidRewardTokens( 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); + ) external view returns (RewardTokenAmounts[] memory, uint256) { + return _getValidRewardTokens(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 + /// @notice Gets all the campaigns which were live at some point between `start` and `end` timestamp /// @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, + /// @return searchCampaigns Eligible campaigns + /// @return lastIndexCampaign Index of the last campaign assessed in the list of all campaigns + /// @dev For pagniation purpose, in case of out of gas, you can call back the same function but with `skip` set to `lastIndexCampaign` + /// @dev Not to be queried on-chain and hence not optimized for gas consumption + function getCampaignsBetween( + uint32 start, + uint32 end, 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 - ); + ) external view returns (CampaignParameters[] memory, uint256 lastIndexCampaign) { + return _getCampaignsBetween(start, end, skip, first); } - /// @notice Similar to `getPoolDistributionsAfterEpoch(address uniV3Pool,uint32 epochStart)` with additional parameters to prevent out of gas error + /// @notice Gets all the distributions which were live at some point between `start` and `end` timestamp /// @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, + /// @dev For pagniation purpose, in case of out of gas, you can call back the same function but with `skip` set to `lastIndexDistribution` + /// @dev Not to be queried on-chain and hence not optimized for gas consumption + function getDistributionsBetweenEpochs( uint32 epochStart, + uint32 epochEnd, uint32 skip, uint32 first - ) external view returns (ExtensiveDistributionParameters[] memory, uint256 lastIndexDistribution) { - return - _getPoolDistributionsBetweenEpochs(uniV3Pool, _getRoundedEpoch(epochStart), type(uint32).max, skip, first); + ) external view returns (DistributionParameters[] memory, uint256 lastIndexDistribution) { + return _getDistributionsBetweenEpochs(_getRoundedEpoch(epochStart), _getRoundedEpoch(epochEnd), skip, first); } - // ============================ GOVERNANCE FUNCTIONS =========================== + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + GOVERNANCE FUNCTIONS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ /// @notice Sets a new `distributor` to which rewards should be distributed - function setNewDistributor(address _distributor) external onlyGovernorOrGuardian { + function setNewDistributor(address _distributor) external onlyGovernor { 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 the defaultFees on deposit + function setFees(uint256 _defaultFees) external onlyGovernor { + if (_defaultFees >= BASE_9) revert InvalidParam(); + defaultFees = _defaultFees; + emit FeesSet(_defaultFees); } - /// @notice Sets fee rebates for a given user - function setUserFeeRebate(address user, uint256 userFeeRebate) external onlyGovernorOrGuardian { - feeRebate[user] = userFeeRebate; - emit FeeRebateUpdated(user, userFeeRebate); + /// @notice Sets the fees specific for a campaign + /// @dev To waive the fees for a campaign, set its fees to 1 + function setCampaignFees(uint32 campaignType, uint256 _fees) external onlyGovernorOrGuardian { + if (_fees >= BASE_9) revert InvalidParam(); + campaignSpecificFees[campaignType] = _fees; + emit CampaignSpecificFeesSet(campaignType, _fees); } /// @notice Toggles the fee whitelist for `token` @@ -602,7 +390,7 @@ contract OldDistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable { } /// @notice Recovers fees accrued on the contract for a list of `tokens` - function recoverFees(IERC20[] calldata tokens, address to) external onlyGovernorOrGuardian { + function recoverFees(IERC20[] calldata tokens, address to) external onlyGovernor { uint256 tokensLength = tokens.length; for (uint256 i; i < tokensLength; ) { tokens[i].safeTransfer(to, tokens[i].balanceOf(address(this))); @@ -612,6 +400,12 @@ contract OldDistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable { } } + /// @notice Sets fee rebates for a given user + function setUserFeeRebate(address user, uint256 userFeeRebate) external onlyGovernorOrGuardian { + feeRebate[user] = userFeeRebate; + emit FeeRebateUpdated(user, userFeeRebate); + } + /// @notice Sets the minimum amounts per distribution epoch for different reward tokens function setRewardTokenMinAmounts( address[] calldata tokens, @@ -630,13 +424,13 @@ contract OldDistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable { } /// @notice Sets a new address to receive fees - function setFeeRecipient(address _feeRecipient) external onlyGovernorOrGuardian { + function setFeeRecipient(address _feeRecipient) external onlyGovernor { 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 { + function setMessage(string memory _message) external onlyGovernor { message = _message; bytes32 _messageHash = ECDSA.toEthSignedMessageHash(bytes(_message)); messageHash = _messageHash; @@ -650,98 +444,180 @@ contract OldDistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable { emit UserSigningWhitelistToggled(user, whitelistStatus); } - // ============================== INTERNAL HELPERS ============================= + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + INTERNAL + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + /// @notice Internal version of `createCampaign` + function _createCampaign(CampaignParameters memory newCampaign) internal returns (bytes32) { + uint256 rewardTokenMinAmount = rewardTokenMinAmounts[newCampaign.rewardToken]; + // if epoch parameters lead to a past campaign + if (newCampaign.startTimestamp < block.timestamp) revert CampaignSouldStartInFuture(); + // if the campaign doesn't last at least one hour + if (newCampaign.duration < HOUR) revert CampaignDurationBelowHour(); + // if the reward token is not whitelisted as an incentive token + if (rewardTokenMinAmount == 0) revert CampaignRewardTokenNotWhitelisted(); + // if the amount distributed is too small with respect to what is allowed + if ((newCampaign.amount * HOUR) / newCampaign.duration < rewardTokenMinAmount) revert CampaignRewardTooLow(); + + if (newCampaign.creator == address(0)) newCampaign.creator = msg.sender; + + // Computing fees: these are waived for whitelisted addresses and if there is a whitelisted token in a pool + uint256 _fees = campaignSpecificFees[newCampaign.campaignType]; + if (_fees == 1) _fees = 0; + else if (_fees == 0) _fees = defaultFees; + uint256 campaignAmountMinusFees = _computeFees(_fees, newCampaign.amount, newCampaign.rewardToken); + newCampaign.amount = campaignAmountMinusFees; + + newCampaign.campaignId = campaignId(newCampaign); + + if (_campaignLookup[newCampaign.campaignId] != 0) revert CampaignAlreadyExists(); + _campaignLookup[newCampaign.campaignId] = campaignList.length + 1; + campaignList.push(newCampaign); + emit NewCampaign(newCampaign); + + return newCampaign.campaignId; + } + + /// @notice Creates a distribution from a deprecated distribution type + function _createDistribution(DistributionParameters memory newDistribution) internal returns (uint256) { + _createCampaign(_convertDistribution(newDistribution)); + // Not gas efficient but deprecated + return campaignList[campaignList.length - 1].amount; + } + + /// @notice Converts the deprecated distribution type into a campaign + function _convertDistribution( + DistributionParameters memory distributionToConvert + ) internal view returns (CampaignParameters memory) { + uint256 wrapperLength = distributionToConvert.wrapperTypes.length; + address[] memory whitelist = new address[](wrapperLength); + address[] memory blacklist = new address[](wrapperLength); + uint256 whitelistLength; + uint256 blacklistLength; + for (uint256 k = 0; k < wrapperLength; k++) { + if (distributionToConvert.wrapperTypes[k] == 0) { + whitelist[whitelistLength] = (distributionToConvert.positionWrappers[k]); + whitelistLength += 1; + } + if (distributionToConvert.wrapperTypes[k] == 3) { + blacklist[blacklistLength] = (distributionToConvert.positionWrappers[k]); + blacklistLength += 1; + } + } + + assembly { + mstore(whitelist, whitelistLength) + mstore(blacklist, blacklistLength) + } + + return + CampaignParameters({ + campaignId: distributionToConvert.rewardId, + creator: msg.sender, + rewardToken: distributionToConvert.rewardToken, + amount: distributionToConvert.amount, + campaignType: 2, + startTimestamp: distributionToConvert.epochStart, + duration: distributionToConvert.numEpoch * HOUR, + campaignData: abi.encode( + distributionToConvert.uniV3Pool, + distributionToConvert.propFees, // eg. 6000 + distributionToConvert.propToken0, // eg. 3000 + distributionToConvert.propToken1, // eg. 1000 + distributionToConvert.isOutOfRangeIncentivized, // eg. 0 + distributionToConvert.boostingAddress, // eg. NULL_ADDRESS + distributionToConvert.boostedReward, // eg. 0 + whitelist, // eg. [] + blacklist, // eg. [] + "0x" + ) + }); + } + + /// @notice Computes the fees to be taken on a campaign and transfers them to the fee recipient + function _computeFees( + uint256 baseFeesValue, + uint256 distributionAmount, + address rewardToken + ) internal returns (uint256 distributionAmountMinusFees) { + uint256 _fees = (baseFeesValue * (BASE_9 - feeRebate[msg.sender])) / BASE_9; + distributionAmountMinusFees = distributionAmount; + if (_fees != 0) { + distributionAmountMinusFees = (distributionAmount * (BASE_9 - _fees)) / BASE_9; + address _feeRecipient = feeRecipient; + _feeRecipient = _feeRecipient == address(0) ? address(this) : _feeRecipient; + IERC20(rewardToken).safeTransferFrom( + msg.sender, + _feeRecipient, + distributionAmount - distributionAmountMinusFees + ); + } + IERC20(rewardToken).safeTransferFrom(msg.sender, distributor, distributionAmountMinusFees); + } + + /// @notice Internal version of the `sign` function + function _sign(bytes calldata signature) internal { + bytes32 _messageHash = messageHash; + if (!SignatureChecker.isValidSignatureNow(msg.sender, _messageHash, signature)) revert InvalidSignature(); + userSignatures[msg.sender] = _messageHash; + emit UserSigned(_messageHash, msg.sender); + } /// @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; + return (epoch / HOUR) * HOUR; + } + + /// @notice Internal version of `getCampaignsBetween` + function _getCampaignsBetween( + uint32 start, + uint32 end, + uint32 skip, + uint32 first + ) internal view returns (CampaignParameters[] memory, uint256) { + uint256 length; + uint256 campaignListLength = campaignList.length; + uint256 returnSize = first > campaignListLength ? campaignListLength : first; + CampaignParameters[] memory activeRewards = new CampaignParameters[](returnSize); + uint32 i = skip; + while (i < campaignListLength) { + CampaignParameters memory campaignToProcess = campaignList[i]; + if ( + campaignToProcess.startTimestamp + campaignToProcess.duration > start && + campaignToProcess.startTimestamp < end + ) { + activeRewards[length] = campaignToProcess; + length += 1; + } + unchecked { + ++i; + } + if (length == returnSize) break; } - 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)); + assembly { + mstore(activeRewards, length) } - return (success, extensiveParams); + return (activeRewards, i); } - /// @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, + /// @notice Internal version of `getDistributionsBetweenEpochs` + function _getDistributionsBetweenEpochs( uint32 epochStart, uint32 epochEnd, uint32 skip, uint32 first - ) internal view returns (ExtensiveDistributionParameters[] memory, uint256) { + ) internal view returns (DistributionParameters[] memory, uint256) { uint256 length; uint256 distributionListLength = distributionList.length; uint256 returnSize = first > distributionListLength ? distributionListLength : first; - ExtensiveDistributionParameters[] memory activeRewards = new ExtensiveDistributionParameters[](returnSize); + DistributionParameters[] memory activeRewards = new DistributionParameters[](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; - } + DistributionParameters memory d = distributionList[i]; + if (d.epochStart + d.numEpoch * HOUR > epochStart && d.epochStart < epochEnd) { + activeRewards[length] = d; + length += 1; } unchecked { ++i; @@ -753,4 +629,39 @@ contract OldDistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable { } return (activeRewards, i); } + + /// @notice Builds the list of valid reward tokens + function _getValidRewardTokens( + uint32 skip, + uint32 first + ) internal view returns (RewardTokenAmounts[] memory, uint256) { + uint256 length; + uint256 rewardTokenListLength = rewardTokens.length; + uint256 returnSize = first > rewardTokenListLength ? rewardTokenListLength : first; + RewardTokenAmounts[] memory validRewardTokens = new RewardTokenAmounts[](returnSize); + uint32 i = skip; + while (i < rewardTokenListLength) { + address token = rewardTokens[i]; + uint256 minAmount = rewardTokenMinAmounts[token]; + if (minAmount > 0) { + validRewardTokens[length] = RewardTokenAmounts(token, minAmount); + length += 1; + } + unchecked { + ++i; + } + if (length == returnSize) break; + } + assembly { + mstore(validRewardTokens, length) + } + return (validRewardTokens, i); + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[33] private __gap; } diff --git a/contracts/deprecated/OldDistributor.sol b/contracts/deprecated/OldDistributor.sol index 9afff90..104b9d4 100644 --- a/contracts/deprecated/OldDistributor.sol +++ b/contracts/deprecated/OldDistributor.sol @@ -57,8 +57,8 @@ struct Claim { bytes32 merkleRoot; } -/// @title Distributor -/// @notice Allows LPs on AMMs with concentrated liquidity to claim the rewards that were distributed to them +/// @title OldDistributor +/// @notice Allows to claim rewards distributed to them through Merkl /// @author Angle Labs. Inc contract OldDistributor is UUPSHelper { using SafeERC20 for IERC20; @@ -77,8 +77,8 @@ contract OldDistributor is UUPSHelper { /// @notice Token to deposit to freeze the roots update IERC20 public disputeToken; - /// @notice `Core` contract handling access control - ICore public core; + /// @notice `AccessControlManager` contract handling access control + IAccessControlManager public core; /// @notice Address which created the dispute /// @dev Used to store if there is an ongoing dispute @@ -130,6 +130,12 @@ contract OldDistributor is UUPSHelper { _; } + /// @notice Checks whether the `msg.sender` has the governor role or the guardian role + modifier onlyGovernor() { + if (!core.isGovernor(msg.sender)) revert NotGovernor(); + _; + } + /// @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)) @@ -141,13 +147,13 @@ contract OldDistributor 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; } /// @inheritdoc UUPSUpgradeable - function _authorizeUpgrade(address) internal view override onlyGuardianUpgrader(core) {} + function _authorizeUpgrade(address) internal view override onlyGovernorUpgrader(core) {} // =============================== MAIN FUNCTION =============================== @@ -177,8 +183,8 @@ contract OldDistributor is UUPSHelper { 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(); + // Only approved operator can claim for `user` + if (msg.sender != user && tx.origin != user && operators[user][msg.sender] == 0) revert NotWhitelisted(); // Verifying proof bytes32 leaf = keccak256(abi.encode(user, token, amount)); @@ -205,7 +211,7 @@ contract OldDistributor is UUPSHelper { // ============================ GOVERNANCE FUNCTIONS =========================== /// @notice Adds or removes EOAs which are trusted to update the Merkle root - function toggleTrusted(address eoa) external onlyGovernorOrGuardian { + function toggleTrusted(address eoa) external onlyGovernor { uint256 trustedStatus = 1 - canUpdateMerkleRoot[eoa]; canUpdateMerkleRoot[eoa] = trustedStatus; emit TrustedToggled(eoa, trustedStatus == 1); @@ -218,7 +224,7 @@ contract OldDistributor is UUPSHelper { // 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)) + !core.isGovernor(msg.sender)) ) revert NotTrusted(); MerkleTree memory _lastTree = tree; tree = _tree; @@ -242,7 +248,7 @@ contract OldDistributor is UUPSHelper { /// @notice Resolve the ongoing dispute, if any /// @param valid Whether the dispute was valid - function resolveDispute(bool valid) external onlyGovernorOrGuardian { + function resolveDispute(bool valid) external onlyGovernor { if (disputer == address(0)) revert NoDispute(); if (valid) { IERC20(disputeToken).safeTransfer(disputer, disputeAmount); @@ -256,14 +262,15 @@ contract OldDistributor is UUPSHelper { emit DisputeResolved(valid); } - /// @notice Allows the governor or the guardian of this contract to fallback to the last version of the tree + /// @notice Allows the governor of this contract to fallback to the last version of the tree /// immediately - function revokeTree() external onlyGovernorOrGuardian { + function revokeTree() external onlyGovernor { if (disputer != address(0)) revert UnresolvedDispute(); _revokeTree(); } /// @notice Toggles permissioned claiming for a given user + /// @dev deprecated function toggleOnlyOperatorCanClaim(address user) external onlyTrustedOrUser(user) { uint256 oldValue = onlyOperatorCanClaim[user]; onlyOperatorCanClaim[user] = 1 - oldValue; @@ -278,26 +285,26 @@ contract OldDistributor is UUPSHelper { } /// @notice Recovers any ERC20 token - function recoverERC20(address tokenAddress, address to, uint256 amountToRecover) external onlyGovernorOrGuardian { + function recoverERC20(address tokenAddress, address to, uint256 amountToRecover) external onlyGovernor { 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 { + function setDisputePeriod(uint48 _disputePeriod) external onlyGovernor { disputePeriod = uint48(_disputePeriod); emit DisputePeriodUpdated(_disputePeriod); } /// @notice Sets the token used as a caution during disputes - function setDisputeToken(IERC20 _disputeToken) external onlyGovernorOrGuardian { + function setDisputeToken(IERC20 _disputeToken) external onlyGovernor { 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 { + function setDisputeAmount(uint256 _disputeAmount) external onlyGovernor { if (disputer != address(0)) revert UnresolvedDispute(); disputeAmount = _disputeAmount; emit DisputeAmountUpdated(_disputeAmount); 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 83% rename from contracts/interfaces/ICore.sol rename to contracts/interfaces/IAccessControlManager.sol index 2ebbc06..ef1ac3d 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 { +/// @notice Interface for the `AccessControlManager` contracts of Merkl contracts +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/TokenLocker.sol b/contracts/partners/TokenLocker.sol similarity index 96% rename from contracts/TokenLocker.sol rename to contracts/partners/TokenLocker.sol index 4fafa69..360e966 100644 --- a/contracts/TokenLocker.sol +++ b/contracts/partners/TokenLocker.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.24; +pragma solidity ^0.8.17; import { IERC20, IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { Ownable2Step, Ownable } from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import { Ownable2Step } from "@openzeppelin/contracts/access/Ownable2Step.sol"; contract TokenLocker is Ownable2Step { IERC20 public asset; 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..950efcd 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,7 @@ 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(); + if (gauge == address(0) || params.rewardToken != address(angle())) 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..959369a 100644 --- a/contracts/tokenWrappers/BaseTokenWrapper.sol +++ b/contracts/partners/tokenWrappers/BaseTokenWrapper.sol @@ -6,10 +6,11 @@ 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); + function feeRecipient() external view returns (address); } @@ -27,7 +28,7 @@ abstract contract BaseMerklTokenWrapper is UUPSHelper, ERC20Upgradeable { // ================================= VARIABLES ================================= /// @notice `Core` contract handling access control - ICore public core; + IAccessControlManager public core; // =================================== EVENTS ================================== @@ -49,7 +50,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/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/RadiantTokenWrapper.sol b/contracts/partners/tokenWrappers/RadiantTokenWrapper.sol similarity index 96% rename from contracts/tokenWrappers/RadiantTokenWrapper.sol rename to contracts/partners/tokenWrappers/RadiantTokenWrapper.sol index 5f2c0e6..b187b98 100644 --- a/contracts/tokenWrappers/RadiantTokenWrapper.sol +++ b/contracts/partners/tokenWrappers/RadiantTokenWrapper.sol @@ -2,7 +2,6 @@ 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"; @@ -10,6 +9,7 @@ import { BaseMerklTokenWrapper } from "./BaseTokenWrapper.sol"; interface IVesting { function rdntToken() external view returns (address); + function vestTokens(address, uint256, bool) external; } 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/struct/ExtensiveDistributionParameters.sol b/contracts/struct/ExtensiveDistributionParameters.sol deleted file mode 100644 index 582e762..0000000 --- a/contracts/struct/ExtensiveDistributionParameters.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -pragma solidity ^0.8.17; - -import { DistributionParameters } from "./DistributionParameters.sol"; - -struct UniswapTokenData { - address add; - uint8 decimals; - string symbol; - uint256 poolBalance; -} - -struct ExtensiveDistributionParameters { - DistributionParameters base; - // Uniswap pool data - uint24 poolFee; - UniswapTokenData token0; - UniswapTokenData token1; - // rewardToken data - string rewardTokenSymbol; - uint8 rewardTokenDecimals; -} diff --git a/contracts/struct/RewardTokenAmounts.sol b/contracts/struct/RewardTokenAmounts.sol index 1a1b7e4..b00443a 100644 --- a/contracts/struct/RewardTokenAmounts.sol +++ b/contracts/struct/RewardTokenAmounts.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.17; +pragma solidity >=0.8.0; struct RewardTokenAmounts { address token; diff --git a/contracts/utils/Errors.sol b/contracts/utils/Errors.sol index b2ddd9e..7720f8d 100644 --- a/contracts/utils/Errors.sol +++ b/contracts/utils/Errors.sol @@ -10,17 +10,23 @@ error CampaignRewardTooLow(); error CampaignSouldStartInFuture(); error InvalidDispute(); error InvalidLengths(); +error InvalidOverride(); error InvalidParam(); error InvalidParams(); error InvalidProof(); error InvalidUninitializedRoot(); +error InvalidReturnMessage(); error InvalidReward(); error InvalidSignature(); error NoDispute(); +error NoOverrideForCampaign(); +error NotAllowed(); error NotGovernor(); error NotGovernorOrGuardian(); error NotSigned(); error NotTrusted(); +error NotUpgradeable(); error NotWhitelisted(); +error ReentrantCall(); error UnresolvedDispute(); error ZeroAddress(); diff --git a/contracts/utils/UUPSHelper.sol b/contracts/utils/UUPSHelper.sol index 286454d..2a3b489 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,13 +45,15 @@ 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) { - if (address(_core) != address(0) && !_core.isGovernorOrGuardian(msg.sender)) revert NotGovernorOrGuardian(); + modifier onlyGuardianUpgrader(IAccessControlManager _accessControlManager) { + if (address(_accessControlManager) != address(0) && !_accessControlManager.isGovernorOrGuardian(msg.sender)) + revert NotGovernorOrGuardian(); _; } - modifier onlyGovernorUpgrader(ICore _core) { - if (address(_core) != address(0) && !_core.isGovernor(msg.sender)) revert NotGovernor(); + modifier onlyGovernorUpgrader(IAccessControlManager _accessControlManager) { + if (address(_accessControlManager) != address(0) && !_accessControlManager.isGovernor(msg.sender)) + revert NotGovernor(); _; } diff --git a/foundry.toml b/foundry.toml index 8714838..815f78c 100644 --- a/foundry.toml +++ b/foundry.toml @@ -9,7 +9,7 @@ gas_reports = ["*"] via_ir = true optimizer_runs=100 -# solc_version = '0.8.17' +solc_version = '0.8.25' ffi = true @@ -20,15 +20,31 @@ runs = 500 runs = 500 [profile.dev] -via_ir = false +via_ir = true [rpc_endpoints] +arbitrum = "${ETH_NODE_URI_ARBITRUM}" +gnosis = "${ETH_NODE_URI_GNOSIS}" mainnet = "${ETH_NODE_URI_MAINNET}" +optimism = "${ETH_NODE_URI_OPTIMISM}" polygon = "${ETH_NODE_URI_POLYGON}" +fork = "${ETH_NODE_URI_FORK}" +avalanche = "${ETH_NODE_URI_AVALANCHE}" +celo = "${ETH_NODE_URI_CELO}" polygonzkevm = "${ETH_NODE_URI_POLYGONZKEVM}" +bsc = "${ETH_NODE_URI_BSC}" +base = "${ETH_NODE_URI_BASE}" +linea = "${ETH_NODE_URI_LINEA}" [etherscan] +arbitrum = { key = "${ARBITRUM_ETHERSCAN_API_KEY}" } +gnosis = { key = "${GNOSIS_ETHERSCAN_API_KEY}" , url = "https://api.gnosisscan.io/api"} mainnet = { key = "${MAINNET_ETHERSCAN_API_KEY}" } +optimism = { key = "${OPTIMISM_ETHERSCAN_API_KEY}" } polygon = { key = "${POLYGON_ETHERSCAN_API_KEY}" } -gnosis = { key = "${GNOSIS_ETHERSCAN_API_KEY}" , url = "https://api.gnosisscan.io/api"} -polygon-zkevm = { key = "${POLYGONZKEVM_ETHERSCAN_API_KEY}" , url = "https://api-zkevm.polygonscan.com/api"} \ No newline at end of file +avalanche = { key = "${AVALANCHE_ETHERSCAN_API_KEY}" } +celo = { key = "${CELO_ETHERSCAN_API_KEY}", url = "https://api.celoscan.io/api" } +base = { key = "${BASE_ETHERSCAN_API_KEY}", url = "https://api.basescan.org/api" } +polygon-zkevm = { key = "${POLYGONZKEVM_ETHERSCAN_API_KEY}", url = "https://api-zkevm.polygonscan.com/api" } +bsc = { key = "${BSC_ETHERSCAN_API_KEY}"} +linea = { key = "${LINEA_ETHERSCAN_API_KEY}"} \ No newline at end of file diff --git a/hardhat.config.ts b/hardhat.config.ts index f8bca59..48ad169 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -571,7 +571,7 @@ const config: HardhatUserConfig = { rootstock: { live: true, url: nodeUrl('rootstock'), - accounts: accounts("rootstock"), + accounts: accounts('rootstock'), gas: 'auto', chainId: 30, verify: { @@ -657,13 +657,13 @@ const config: HardhatUserConfig = { apiKey: { worldchain: etherscanKey('worldchain'), }, - customChains:[ + customChains: [ { network: 'taiko', chainId: 167000, urls: { - apiURL: "https://api.taikoscan.io/api", - browserURL: "https://taikoscan.io/" + apiURL: 'https://api.taikoscan.io/api', + browserURL: 'https://taikoscan.io/', }, }, { @@ -694,7 +694,7 @@ const config: HardhatUserConfig = { network: 'worldchain', chainId: 480, urls: { - apiURL: "https://worldchain-mainnet.explorer.alchemy.com/api", + apiURL: 'https://worldchain-mainnet.explorer.alchemy.com/api', browserURL: 'https://worldchain-mainnet.explorer.alchemy.com/', }, }, diff --git a/helpers/fork.sh b/helpers/fork.sh new file mode 100644 index 0000000..6102bbf --- /dev/null +++ b/helpers/fork.sh @@ -0,0 +1,46 @@ +#! /bin/bash + +source lib/utils/helpers/common.sh + +function main { + if [ ! -f .env ]; then + echo ".env not found!" + exit 1 + fi + source .env + + echo "Which chain would you like to fork ?" + echo "- 1: Ethereum Mainnet" + echo "- 2: Arbitrum" + echo "- 3: Polygon" + echo "- 4: Gnosis" + echo "- 5: Avalanche" + echo "- 6: Base" + echo "- 7: Binance Smart Chain" + echo "- 8: Celo" + echo "- 9: Polygon ZkEvm" + echo "- 10: Optimism" + echo "- 11: Linea" + + read option + + uri=$(chain_to_uri $option) + if [ -z "$uri" ]; then + echo "Unknown network" + exit 1 + fi + + echo "What block do you want to fork ? (Can leave empty for instant)" + + read block + + if [ -z "$block" ]; then + echo "Forking $uri" + anvil --fork-url $uri + else + echo "Forking $uri at block $block" + anvil --fork-url $uri --fork-block-number $block + fi +} + +main diff --git a/lib/forge-std b/lib/forge-std index cb69e9c..0e70977 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit cb69e9c07fbd002819c8c6c8db3caeab76b90d6b +Subproject commit 0e7097750918380d84dd3cfdef595bee74dabb70 diff --git a/lib/utils b/lib/utils new file mode 160000 index 0000000..d4a8da3 --- /dev/null +++ b/lib/utils @@ -0,0 +1 @@ +Subproject commit d4a8da343cd8de28885ec5a2352d3be1cbd3b0ec diff --git a/package.json b/package.json index 543c5df..4080011 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "deploy:agla": "hardhat deploy --tags aglaMerkl --network", "deploy": "hardhat deploy --tags distributionCreator --network", "etherscan": "hardhat etherscan-verify --network", + "fork": "bash helpers/fork.sh", "foundry:compile": "forge build --optimize --optimizer-runs 1000", "foundry:coverage": "forge coverage --ir-minimum --report lcov && yarn lcov:clean && yarn lcov:generate-html", "foundry:deploy": "forge script --broadcast --verify -vvvv", @@ -36,6 +37,10 @@ "hardhat:test": "hardhat test", "resolve-dispute": "hardhat run scripts/buildDisputeResolution.ts", "check-upgradeability": "hardhat run scripts/checkUpgradeability.ts", + "impersonate": "cast rpc anvil_impersonateAccount", + "impersonate:script": "FOUNDRY_PROFILE=dev forge script --skip test --fork-url fork --broadcast -vvvv --gas-price 0 --priority-gas-price 0 --unlocked --sender", + "impersonate:setBalance": "cast rpc anvil_setBalance 0x0022228a2cc5E7eF0274A7Baa600d44da5aB5776 1000000000000000000", + "fork:advanceTime": "cast rpc evm_increaseTime 704800 && cast rpc anvil_mine", "license": "hardhat prepend-spdx-license", "node:fork": "FORK=true hardhat node", "size": "yarn hardhat:compile && hardhat size-contracts", @@ -120,4 +125,4 @@ "solc": "0.8.12", "yargs": "^17.5.1" } -} +} \ No newline at end of file diff --git a/remappings.txt b/remappings.txt index 1076a42..8652b30 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,2 +1,4 @@ ds-test/=lib/forge-std/lib/ds-test/src/ -forge-std/=lib/forge-std/src/ \ No newline at end of file +forge-std/=lib/forge-std/src/ +oz/=node_modules/@openzeppelin/contracts/ +utils/=lib/utils \ No newline at end of file diff --git a/scripts/foundry/ClaimDistributor.s.sol b/scripts/foundry/ClaimDistributor.s.sol new file mode 100644 index 0000000..5a1c45a --- /dev/null +++ b/scripts/foundry/ClaimDistributor.s.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.17; + +import { console } from "forge-std/console.sol"; +import { Distributor } from "contracts/Distributor.sol"; +import { MockToken, IERC20 } from "contracts/mock/MockToken.sol"; +import { CommonUtils, ContractType } from "utils/src/CommonUtils.sol"; +import { CHAIN_BASE } from "utils/src/Constants.sol"; +import { StdAssertions } from "forge-std/Test.sol"; + +contract ClaimDistributor is CommonUtils, StdAssertions { + function run() external { + uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); + address deployer = vm.addr(deployerPrivateKey); + + /// TODO: COMPLETE + uint256 chainId = CHAIN_BASE; + IERC20 rewardToken = IERC20(0xC011882d0f7672D8942e7fE2248C174eeD640c8f); + address claimer = 0x15775b23340C0f50E0428D674478B0e9D3D0a759; + uint256 balanceToClaim = 1918683165360; + bytes32[][] memory proofs = new bytes32[][](1); + address[] memory users = new address[](1); + address[] memory tokens = new address[](1); + uint256[] memory amounts = new uint256[](1); + proofs[0] = new bytes32[](17); + proofs[0][0] = bytes32(0xb4273243bd0ec5add5e6d803f13bf6866ed1904d24626766ab2836454ba1ec0a); + proofs[0][1] = bytes32(0x3ee0ead23e2fe3f664ccb5e13683f27e27a4d7fefa8405545fb6421244630375); + proofs[0][2] = bytes32(0x69f54e33351af15236b33bb4695470f1af96cd1a9f154aa511ff16faa6886791); + proofs[0][3] = bytes32(0xa9d77ad46850fbfb8c196c693acdbb0c6241a2e561a8b0073ec71297a565673d); + proofs[0][4] = bytes32(0xe1b57f280e556c7f217e8d375f0cef7977a9467d5496d32bb8ec461f0d4c4f19); + proofs[0][5] = bytes32(0x0fc7ddc7cc9ecc7f7b0be5692f671394f6245ffdabe5c0fd2062eb71b7c11826); + proofs[0][6] = bytes32(0x94445a98fe6679760e5ac2edeacfe0bfa397f805c7adeaf3558a82accb78f201); + proofs[0][7] = bytes32(0x14a6fec66cdfece5c73ec44196f1414326236131ff9a60350cca603e54985c4e); + proofs[0][8] = bytes32(0x84679751230af3e3242ea1cecfc8daee3d2187ab647281cbf8c52e649a43e84c); + proofs[0][9] = bytes32(0xc0fc15960178fe4d542c93e64ec58648e5ff17bd02b27f841bd6ab838fc5ee67); + proofs[0][10] = bytes32(0x9b84efe5d11bc4de32ecd204c3962dd9270694d93a50e2840d763eaeac6c194b); + proofs[0][11] = bytes32(0x5c8025dbe663cf4b4e19fbc7b1e54259af5822fd774fd60a98e7c7a60112efe0); + proofs[0][12] = bytes32(0x301b573f9a6503ebe00ff7031a33cd41170d8b4c09a31fcafb9feb7529400a79); + proofs[0][13] = bytes32(0xc89942ad2dcb0ac96d2620ef9475945bdbe6d40a9f6c4e9f6d9437a953bf881c); + proofs[0][14] = bytes32(0xce6ca90077dc547f9a52a24d2636d659642fbae1d16c81c9e47c5747a472c63f); + proofs[0][15] = bytes32(0xe34667d2e10b515dd1f7b29dcd7990d25ea9caa7a7de571c4fb221c0a8fc82a1); + proofs[0][16] = bytes32(0x8316d6488fd22b823cc35ee673297ea2a753f0a89e5384ef20b38d053c881628); + users[0] = claimer; + tokens[0] = address(rewardToken); + amounts[0] = balanceToClaim; + /// END + + Distributor distributor = Distributor(_chainToContract(chainId, ContractType.Distributor)); + + vm.startBroadcast(claimer); + + distributor.claim(users, tokens, amounts, proofs); + + assertEq(rewardToken.balanceOf(claimer), balanceToClaim); + + vm.stopBroadcast(); + } +} diff --git a/scripts/foundry/CreateCampaign.s.sol b/scripts/foundry/CreateCampaign.s.sol new file mode 100644 index 0000000..b097a93 --- /dev/null +++ b/scripts/foundry/CreateCampaign.s.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.17; + +import { console } from "forge-std/console.sol"; +import { DistributionCreator, DistributionParameters, CampaignParameters } from "contracts/DistributionCreator.sol"; +import { MockToken, IERC20 } from "contracts/mock/MockToken.sol"; +import { CommonUtils, ContractType } from "utils/src/CommonUtils.sol"; +import { CHAIN_BASE } from "utils/src/Constants.sol"; +import { StdAssertions } from "forge-std/Test.sol"; + +contract CreateCampaign is CommonUtils, StdAssertions { + function run() external { + uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); + address deployer = vm.addr(deployerPrivateKey); + + /// TODO: COMPLETE + uint256 chainId = CHAIN_BASE; + IERC20 rewardToken = IERC20(0xC011882d0f7672D8942e7fE2248C174eeD640c8f); + uint256 amount = 100 ether; + /// END + + DistributionCreator distributionCreator = DistributionCreator( + _chainToContract(chainId, ContractType.DistributionCreator) + ); + + vm.startBroadcast(deployer); + + MockToken(address(rewardToken)).mint(deployer, amount); + rewardToken.approve(address(distributionCreator), amount); + uint32 startTimestamp = uint32(block.timestamp + 600); + bytes32 campaignId = distributionCreator.createCampaign( + CampaignParameters({ + campaignId: bytes32(0), + creator: deployer, + rewardToken: address(rewardToken), + amount: amount, + campaignType: 1, + startTimestamp: startTimestamp, + duration: 3600 * 24, + campaignData: abi.encode( + 0xbEEfa1aBfEbE621DF50ceaEF9f54FdB73648c92C, + new address[](0), + new address[](0), + "", + new bytes[](0), + new bytes[](0), + hex"" + ) + }) + ); + + CampaignParameters memory campaign = distributionCreator.campaign(campaignId); + assertEq(campaign.creator, deployer); + assertEq(campaign.rewardToken, address(rewardToken)); + assertEq(campaign.amount, (amount * (1e9 - distributionCreator.defaultFees())) / 1e9); + assertEq(campaign.campaignType, 1); + assertEq(campaign.startTimestamp, startTimestamp); + assertEq(campaign.duration, 3600 * 24); + + vm.stopBroadcast(); + } +} diff --git a/scripts/foundry/ReallocateCampaignRewards.s.sol b/scripts/foundry/ReallocateCampaignRewards.s.sol new file mode 100644 index 0000000..1c8f10f --- /dev/null +++ b/scripts/foundry/ReallocateCampaignRewards.s.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.17; + +import { console } from "forge-std/console.sol"; +import { DistributionCreator, DistributionParameters, CampaignParameters } from "contracts/DistributionCreator.sol"; +import { MockToken, IERC20 } from "contracts/mock/MockToken.sol"; +import { CommonUtils, ContractType } from "utils/src/CommonUtils.sol"; +import { CHAIN_BASE } from "utils/src/Constants.sol"; +import { StdAssertions } from "forge-std/Test.sol"; +import { ERC4626 } from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; + +contract ReallocateCampaignRewards is CommonUtils, StdAssertions { + function run() external { + uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); + address deployer = vm.addr(deployerPrivateKey); + + /// TODO: COMPLETE + uint256 chainId = CHAIN_BASE; + IERC20 rewardToken = IERC20(0xC011882d0f7672D8942e7fE2248C174eeD640c8f); + uint256 amount = 97 ether; // after fees + bytes32 campaignId = 0x1d1231a7a6958431a5760b929c56f0e44a20f06e92a52324c19a2e4d2ec529bc; + address to = 0xA9DdD91249DFdd450E81E1c56Ab60E1A62651701; + address[] memory froms = new address[](2); + froms[0] = 0x15775b23340C0f50E0428D674478B0e9D3D0a759; + froms[1] = 0xe4BB74804edf5280c9203f034036f7CB15196078; + /// END + + DistributionCreator distributionCreator = DistributionCreator( + _chainToContract(chainId, ContractType.DistributionCreator) + ); + uint32 timestamp = uint32(block.timestamp); + + vm.startBroadcast(deployer); + + distributionCreator.reallocateCampaignRewards(campaignId, froms, to); + + assertEq(distributionCreator.campaignReallocation(campaignId, froms[0]), to); + assertEq(distributionCreator.campaignListReallocation(campaignId, 0), froms[0]); + assertEq(distributionCreator.campaignListReallocation(campaignId, 1), froms[1]); + + vm.stopBroadcast(); + } +} diff --git a/scripts/foundry/UpdateCampaign.s.sol b/scripts/foundry/UpdateCampaign.s.sol new file mode 100644 index 0000000..c1bb952 --- /dev/null +++ b/scripts/foundry/UpdateCampaign.s.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.17; + +import { console } from "forge-std/console.sol"; +import { DistributionCreator, DistributionParameters, CampaignParameters } from "contracts/DistributionCreator.sol"; +import { MockToken, IERC20 } from "contracts/mock/MockToken.sol"; +import { CommonUtils, ContractType } from "utils/src/CommonUtils.sol"; +import { CHAIN_BASE } from "utils/src/Constants.sol"; +import { StdAssertions } from "forge-std/Test.sol"; +import { ERC4626 } from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; + +contract UpdateCampaign is CommonUtils, StdAssertions { + function run() external { + uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); + address deployer = vm.addr(deployerPrivateKey); + + /// TODO: COMPLETE + uint256 chainId = CHAIN_BASE; + IERC20 rewardToken = IERC20(0xC011882d0f7672D8942e7fE2248C174eeD640c8f); + uint256 amount = 97 ether; // after fees + bytes32 campaignId = 0x6628165d9b509afe46d9009fecc7012c68cc0ce24aafdc4ce11f23a01ccc1a22; + uint32 startTimestamp = uint32(1733155692); + uint32 duration = 3600 * 6; + /// END + + DistributionCreator distributionCreator = DistributionCreator( + _chainToContract(chainId, ContractType.DistributionCreator) + ); + uint32 timestamp = uint32(block.timestamp); + + // // Do some mint and deposit to change a lot reward distribution + // vm.startBroadcast(0x0022228a2cc5E7eF0274A7Baa600d44da5aB5776); + // MockToken(address(0x0000206329b97DB379d5E1Bf586BbDB969C63274)).mint(deployer, 100_000 ether); + // vm.stopBroadcast(); + + vm.startBroadcast(deployer); + + // It will be less than that but we don't care + MockToken(address(rewardToken)).mint(deployer, amount); + rewardToken.approve(address(distributionCreator), amount); + + // IERC20(0x0000206329b97DB379d5E1Bf586BbDB969C63274).approve( + // address(0xbEEfa1aBfEbE621DF50ceaEF9f54FdB73648c92C), + // 100_000 ether + // ); + // ERC4626(0xbEEfa1aBfEbE621DF50ceaEF9f54FdB73648c92C).deposit(100_000 ether, deployer); + + // // ERC20 distrib change duration + // uint32 campaignType = 1; + // bytes memory campaignData = abi.encode( + // 0xbEEfa1aBfEbE621DF50ceaEF9f54FdB73648c92C, + // new address[](0), + // new address[](0), + // "", + // new bytes[](0), + // new bytes[](0), + // hex"" + // ); + + // ERC20 distrib + uint32 campaignType = 1; + bytes memory campaignData = abi.encode( + 0x70F796946eD919E4Bc6cD506F8dACC45E4539771, + new address[](0), + new address[](0), + "", + new bytes[](0), + new bytes[](0), + hex"" + ); + + // // Silo distrib + // address[] memory whitelist = new address[](1); + // whitelist[0] = 0x8095806d8753C0443C118D1C5e5eEC472e30BFeC; + // uint32 campaignType = 5; + // bytes memory campaignData = abi.encode( + // 0x04C0599Ae5A44757c0af6F9eC3b93da8976c150A, + // 2, + // 0xa42001D6d2237d2c74108FE360403C4b796B7170, + // whitelist, + // new address[](0), + // hex"" + // ); + + // // CLAMM distrib + // uint32 campaignType = 2; + // bytes memory campaignData = abi.encode( + // 0x5280d5E63b416277d0F81FAe54Bb1e0444cAbDAA, + // 5100, + // 1700, + // 3200, + // false, + // address(0), + // 1, + // new address[](0), + // new address[](0), + // "", + // new bytes[](0), + // hex"" + // ); + + distributionCreator.overrideCampaign( + campaignId, + CampaignParameters({ + campaignId: campaignId, + creator: deployer, + rewardToken: address(rewardToken), + amount: amount, + campaignType: campaignType, + startTimestamp: startTimestamp, + duration: duration, + campaignData: campaignData + }) + ); + + ( + , + address campaignCreator, + address campaignRewardToken, + uint256 campaignAmount, + uint256 campaignCampaignType, + uint32 campaignStartTimestamp, + uint32 campaignDuration, + bytes memory campaignCampaignData + ) = distributionCreator.campaignOverrides(campaignId); + assertEq(campaignCreator, deployer); + assertEq(campaignRewardToken, address(rewardToken)); + assertEq(campaignAmount, amount); + assertEq(campaignCampaignType, campaignType); + assertEq(campaignStartTimestamp, startTimestamp); + assertEq(campaignDuration, duration); + assertEq(campaignCampaignData, campaignData); + assertLt(distributionCreator.campaignOverridesTimestamp(campaignId, 0), timestamp); + assertLt(distributionCreator.campaignOverridesTimestamp(campaignId, 1), timestamp); + // assertLt(distributionCreator.campaignOverridesTimestamp(campaignId, 2), timestamp); + assertGe(distributionCreator.campaignOverridesTimestamp(campaignId, 2), timestamp); + vm.expectRevert(); + distributionCreator.campaignOverridesTimestamp(campaignId, 3); + + vm.stopBroadcast(); + } +} diff --git a/scripts/foundry/UpdateRoute.s.sol b/scripts/foundry/UpdateRoute.s.sol new file mode 100644 index 0000000..505adad --- /dev/null +++ b/scripts/foundry/UpdateRoute.s.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.17; + +import { console } from "forge-std/console.sol"; +import { Distributor, MerkleTree } from "contracts/Distributor.sol"; +import { MockToken, IERC20 } from "contracts/mock/MockToken.sol"; +import { CommonUtils, ContractType } from "utils/src/CommonUtils.sol"; +import { CHAIN_BASE } from "utils/src/Constants.sol"; +import { StdAssertions } from "forge-std/Test.sol"; + +contract UpdateRoute is CommonUtils, StdAssertions { + function run() external { + uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); + address deployer = vm.addr(deployerPrivateKey); + + /// TODO: COMPLETE + uint256 chainId = CHAIN_BASE; + MerkleTree memory newTree = MerkleTree({ + merkleRoot: 0xb402de8ed2f573c780a39e6d41aa5276706c439849d1e4925d379f2aa8913577, + ipfsHash: bytes32(0) + }); + address updater = 0x435046800Fb9149eE65159721A92cB7d50a7534b; + /// END + + Distributor distributor = Distributor(_chainToContract(chainId, ContractType.Distributor)); + + vm.startBroadcast(updater); + + distributor.updateTree(newTree); + + vm.stopBroadcast(); + + // You then need to wait 1 hour to be effective + } +} diff --git a/scripts/foundry/UpgradeDistributionCreator.s.sol b/scripts/foundry/UpgradeDistributionCreator.s.sol new file mode 100644 index 0000000..220e6f8 --- /dev/null +++ b/scripts/foundry/UpgradeDistributionCreator.s.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.17; + +import { console } from "forge-std/console.sol"; +import { DistributionCreator, DistributionParameters, CampaignParameters } from "contracts/DistributionCreator.sol"; +import { CommonUtils, ContractType } from "utils/src/CommonUtils.sol"; +import { CHAIN_BASE } from "utils/src/Constants.sol"; +import { StdAssertions } from "forge-std/Test.sol"; + +contract UpgradeDistributionCreator is CommonUtils, StdAssertions { + function run() external { + uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); + address deployer = vm.addr(deployerPrivateKey); + + /// TODO: COMPLETE + uint256 chainId = CHAIN_BASE; + /// END + + DistributionCreator distributionCreator = DistributionCreator( + _chainToContract(chainId, ContractType.DistributionCreator) + ); + address governor = _chainToContract(chainId, ContractType.AngleLabsMultisig); + + vm.startBroadcast(deployer); + // We deploy the new implementation + address creatorImpl = address(new DistributionCreator()); + vm.stopBroadcast(); + + // Upgrade + vm.startBroadcast(governor); + distributionCreator.upgradeTo(address(creatorImpl)); + vm.stopBroadcast(); + + // Test storage + assertEq(address(distributionCreator.core()), _chainToContract(chainId, ContractType.CoreMerkl)); + assertEq(address(distributionCreator.distributor()), _chainToContract(chainId, ContractType.Distributor)); + assertEq(distributionCreator.defaultFees(), 0.03e9); + assertEq( + distributionCreator.message(), + '" 1. Merkl is experimental software provided as is, use it at your own discretion. There may notably be delays in the onchain Merkle root updates and there may be flaws in the script (or engine) or in the infrastructure used to update results onchain. In that regard, everyone can permissionlessly dispute the rewards which are posted onchain, and when creating a distribution, you are responsible for checking the results and eventually dispute them. 2. If you are specifying an invalid pool address or a pool from an AMM that is not marked as supported, your rewards will not be taken into account and you will not be able to recover them. 3. If you do not blacklist liquidity position managers or smart contract addresses holding LP tokens that are not natively supported by the Merkl system, or if you don\'t specify the addresses of the liquidity position managers that are not automatically handled by the system, then the script will not be able to take the specifities of these addresses into account, and it will reward them like a normal externally owned account would be. If these are smart contracts that do not support external rewards, then rewards that should be accruing to it will be lost. 4. If rewards sent through Merkl remain unclaimed for a period of more than 1 year after the end of the distribution (because they are meant for instance for smart contract addresses that cannot claim or deal with them), then we reserve the right to recover these rewards. 5. Fees apply to incentives deposited on Merkl, unless the pools incentivized contain a whitelisted token (e.g an Angle Protocol stablecoin). 6. By interacting with the Merkl smart contract to deposit an incentive for a pool, you are exposed to smart contract risk and to the offchain mechanism used to compute reward distribution. 7. If the rewards you are sending are too small in value, or if you are sending rewards using a token that is not approved for it, your rewards will not be handled by the script, and they may be lost. 8. If you mistakenly send too much rewards compared with what you wanted to send, you will not be able to call them back. You will also not be able to prematurely end a reward distribution once created. 9. The engine handling reward distribution for a pool may not look at all the swaps occurring on the pool during the time for which you are incentivizing, but just at a subset of it to gain in efficiency. Overall, if you distribute incentives using Merkl, it means that you are aware of how the engine works, of the approximations it makes and of the behaviors it may trigger (e.g. just in time liquidity). 10. Rewards corresponding to incentives distributed through Merkl do not compound block by block, but are regularly made available (through a Merkle root update) at a frequency which depends on the chain. "' + ); + assertEq(distributionCreator.messageHash(), 0x08dabc24dcfcb230453d08bce47c730ed6f1cce205bc153680488959b503644e); + { + (bytes32 campaignId, , , , , , , , , , , , ) = distributionCreator.distributionList(0); + assertEq(campaignId, 0xb3fc2abc303c70a16ab9d5fc38d7e8aeae66593a87a3d971b024dd34b97e94b1); + } + { + (bytes32 campaignId, , , , , , , , , , , , ) = distributionCreator.distributionList(73); + assertEq(campaignId, 0x157a32c11ce34030465e1c28c309f38c18161028355f3446f54b677d11ceb63a); + } + assertEq(distributionCreator.feeRebate(0xfdA462548Ce04282f4B6D6619823a7C64Fdc0185), 0); + assertEq(distributionCreator.isWhitelistedToken(_chainToContract(chainId, ContractType.AgEUR)), 1); + assertEq(distributionCreator._nonces(0xfdA462548Ce04282f4B6D6619823a7C64Fdc0185), 4); + assertEq( + distributionCreator.userSignatures(0xfdA462548Ce04282f4B6D6619823a7C64Fdc0185), + 0x08dabc24dcfcb230453d08bce47c730ed6f1cce205bc153680488959b503644e + ); + assertEq(distributionCreator.userSignatureWhitelist(0xfdA462548Ce04282f4B6D6619823a7C64Fdc0185), 0); + + assertEq(distributionCreator.rewardTokens(0), 0x7D49a065D17d6d4a55dc13649901fdBB98B2AFBA); + assertEq(distributionCreator.rewardTokens(21), 0xF734eFdE0C424BA2B547b186586dE417b0954802); + assertEq(distributionCreator.rewardTokenMinAmounts(0x7D49a065D17d6d4a55dc13649901fdBB98B2AFBA), 1 ether); + + { + (bytes32 campaignId, , , , , , , ) = distributionCreator.campaignList(0); + assertEq(campaignId, 0x4e2bf13f682a244a80e0f25e1545fc8ad3a181d60658d22a3d347ee493e2a740); + } + { + (bytes32 campaignId, , , , , , , ) = distributionCreator.campaignList(67); + assertEq(campaignId, 0xf7d416acc480a41cd4cbb1bd68941f2f585adb659bd95d45e193589175356972); + } + assertEq(distributionCreator.campaignSpecificFees(4), 0.005e9); + + { + (bytes32 campaignId, , , , , , , ) = distributionCreator.campaignOverrides( + 0xf7d416acc480a41cd4cbb1bd68941f2f585adb659bd95d45e193589175356972 + ); + assertEq(campaignId, bytes32(0)); + } + + vm.expectRevert(); + distributionCreator.campaignOverridesTimestamp( + 0x4e2bf13f682a244a80e0f25e1545fc8ad3a181d60658d22a3d347ee493e2a740, + 0 + ); + } +} diff --git a/scripts/mintMockToken.ts b/scripts/mintMockToken.ts index 23730a6..6c98404 100644 --- a/scripts/mintMockToken.ts +++ b/scripts/mintMockToken.ts @@ -7,11 +7,11 @@ import { MockToken__factory } from '../typechain'; async function main() { const { deployer } = await ethers.getNamedSigners(); - const MockToken = MockToken__factory.connect('0xA7c167f58833c5e25848837f45A1372491A535eD', deployer); + const MockToken = MockToken__factory.connect('0xC011882d0f7672D8942e7fE2248C174eeD640c8f', deployer); console.log(`Minting MockToken to ${deployer.address}...`); await ( - await MockToken.mint(deployer.address, parseUnits('100000', 6), { + await MockToken.mint('0x81Dd955D02D337DB81BA6c9C5F6213E647672052', parseUnits('1000', 18), { // gasLimit: 300_000, // maxPriorityFeePerGas: 100e9, // maxFeePerGas: 700e9, diff --git a/test/foundry/DistributionCreator.t.sol b/test/foundry/DistributionCreator.t.sol index adba03d..7b3c424 100644 --- a/test/foundry/DistributionCreator.t.sol +++ b/test/foundry/DistributionCreator.t.sol @@ -4,8 +4,11 @@ pragma solidity ^0.8.17; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { DistributionParameters } from "../../contracts/DistributionCreator.sol"; import "./Fixture.t.sol"; +import { CampaignParameters } from "contracts/DistributionCreator.sol"; +import { Distributor, MerkleTree } from "contracts/Distributor.sol"; +import "contracts/utils/Errors.sol" as Errors; -contract DistributionCreatorTest is Fixture { +contract DistributionCreatorCreateCampaignTest is Fixture { using SafeERC20 for IERC20; uint256 constant maxDistribForOOG = 1e4; @@ -23,6 +26,9 @@ contract DistributionCreatorTest is Fixture { numEpoch = 25; initEndTime = startTime + numEpoch * EPOCH_DURATION; + vm.prank(governor); + creator.setFeeRecipient(dylan); + vm.startPrank(guardian); creator.toggleSigningWhitelist(alice); creator.toggleTokenWhitelist(address(agEUR)); @@ -37,115 +43,1101 @@ contract DistributionCreatorTest is Fixture { vm.prank(alice); angle.approve(address(creator), type(uint256).max); - address[] memory positionWrappers = new address[](3); - uint32[] memory wrapperTypes = new uint32[](3); - positionWrappers[0] = alice; - positionWrappers[1] = bob; - positionWrappers[2] = charlie; - wrapperTypes[0] = 0; - wrapperTypes[1] = 1; - wrapperTypes[2] = 2; - - vm.startPrank(alice); - // struct DistributionParameters memory - // create a bunch of distributions to make the view function call fail - DistributionParameters memory params = DistributionParameters({ - uniV3Pool: address(pool), - rewardToken: address(angle), - positionWrappers: positionWrappers, - wrapperTypes: wrapperTypes, - amount: 1e10, - propToken0: 4000, - propToken1: 2000, - propFees: 4000, - isOutOfRangeIncentivized: 0, - epochStart: initStartTime, - numEpoch: 25, - boostedReward: 0, - boostingAddress: address(0), - rewardId: keccak256("TEST"), - additionalData: hex"" - }); - // create a first distrib way before the others - creator.createDistribution(params); - - vm.warp(startTime + 3600 * 24 * 1000); - startTime = uint32(block.timestamp); - endTime = startTime + numEpoch * EPOCH_DURATION; - params.epochStart = startTime; - for (uint256 i; i < nbrDistrib; i++) creator.createDistribution(params); + vm.stopPrank(); + } + + function testUnit_CreateCampaignWithDefaultFees() public { + IERC20 rewardToken = IERC20(address(angle)); + uint256 amount = 100 ether; + uint256 amountAfterFees = 90 ether; + uint32 startTimestamp = uint32(block.timestamp + 600); + + vm.prank(alice); + bytes32 campaignId = creator.createCampaign( + CampaignParameters({ + campaignId: bytes32(0), + creator: alice, + rewardToken: address(rewardToken), + amount: amount, + campaignType: 1, + startTimestamp: startTimestamp, + duration: 3600 * 24, + campaignData: abi.encode( + 0xbEEfa1aBfEbE621DF50ceaEF9f54FdB73648c92C, + new address[](0), + new address[](0), + "", + new bytes[](0), + new bytes[](0), + hex"" + ) + }) + ); + + CampaignParameters memory campaign = creator.campaign(campaignId); + assertEq(campaign.creator, alice); + assertEq(campaign.rewardToken, address(rewardToken)); + assertEq(campaign.amount, (amount * (1e9 - creator.defaultFees())) / 1e9); + assertEq(campaign.campaignType, 1); + assertEq(campaign.startTimestamp, startTimestamp); + assertEq(campaign.duration, 3600 * 24); + } + + function testUnit_CreateCampaignWithSetFees() public { + uint32 campaignType = 1; + vm.prank(guardian); + creator.setCampaignFees(campaignType, 1e7); + + IERC20 rewardToken = IERC20(address(angle)); + uint256 amount = 100 ether; + uint256 amountAfterFees = 90 ether; + uint32 startTimestamp = uint32(block.timestamp + 600); + + uint256 prevBalance = rewardToken.balanceOf(alice); + + vm.prank(alice); + bytes32 campaignId = creator.createCampaign( + CampaignParameters({ + campaignId: bytes32(0), + creator: alice, + rewardToken: address(rewardToken), + amount: amount, + campaignType: campaignType, + startTimestamp: startTimestamp, + duration: 3600 * 24, + campaignData: abi.encode( + 0xbEEfa1aBfEbE621DF50ceaEF9f54FdB73648c92C, + new address[](0), + new address[](0), + "", + new bytes[](0), + new bytes[](0), + hex"" + ) + }) + ); + + CampaignParameters memory campaign = creator.campaign(campaignId); + assertEq(campaign.creator, alice); + assertEq(campaign.rewardToken, address(rewardToken)); + assertEq(campaign.amount, (amount * (1e9 - 1e7)) / 1e9); + assertEq(campaign.campaignType, 1); + assertEq(campaign.startTimestamp, startTimestamp); + assertEq(campaign.duration, 3600 * 24); + assertEq(rewardToken.balanceOf(alice), prevBalance - amount); + assertEq(rewardToken.balanceOf(dylan), (amount * 1e7) / 1e9); + } +} +contract DistributionCreatorCreateReallocationTest is Fixture { + using SafeERC20 for IERC20; + + Distributor public distributor; + Distributor public distributorImpl; + uint32 initStartTime; + uint32 initEndTime; + uint32 startTime; + uint32 endTime; + uint32 numEpoch; + + function setUp() public override { + super.setUp(); + + distributorImpl = new Distributor(); + distributor = Distributor(deployUUPS(address(distributorImpl), hex"")); + distributor.initialize(IAccessControlManager(address(coreBorrow))); + + vm.startPrank(governor); + distributor.setDisputeAmount(1e18); + distributor.setDisputePeriod(1 days); + distributor.setDisputeToken(angle); vm.stopPrank(); + + initStartTime = uint32(block.timestamp); + numEpoch = 25; + initEndTime = startTime + numEpoch * EPOCH_DURATION; + + vm.startPrank(governor); + creator.setNewDistributor(address(distributor)); + creator.setFeeRecipient(dylan); + vm.stopPrank(); + + vm.startPrank(guardian); + creator.toggleSigningWhitelist(alice); + creator.toggleTokenWhitelist(address(agEUR)); + address[] memory tokens = new address[](2); + uint256[] memory amounts = new uint256[](2); + tokens[0] = address(angle); + amounts[0] = 1e8; + tokens[1] = address(agEUR); + amounts[1] = 1e8; + creator.setRewardTokenMinAmounts(tokens, amounts); + vm.stopPrank(); + + angle.mint(address(alice), 1e22); + vm.prank(alice); + angle.approve(address(creator), type(uint256).max); + + agEUR.mint(address(alice), 1e22); + vm.prank(alice); + agEUR.approve(address(creator), type(uint256).max); + + vm.stopPrank(); + } + + function testUnit_ReallocationCampaignRewards_revertWhen_TooSoon() public { + IERC20 rewardToken = IERC20(address(agEUR)); + uint256 amount = 100 ether; + uint32 startTimestamp = uint32(block.timestamp + 600); + + vm.prank(alice); + bytes32 campaignId = creator.createCampaign( + CampaignParameters({ + campaignId: bytes32(0), + creator: alice, + rewardToken: address(rewardToken), + amount: amount, + campaignType: 1, + startTimestamp: startTimestamp, + duration: 3600 * 48, + campaignData: abi.encode( + 0xbEEfa1aBfEbE621DF50ceaEF9f54FdB73648c92C, + new address[](0), + new address[](0), + "", + new bytes[](0), + new bytes[](0), + hex"" + ) + }) + ); + + vm.prank(governor); + // Create false tree + distributor.updateTree( + MerkleTree({ + merkleRoot: bytes32(0x0b70a97c062cb747158b89e27df5bbda859ba072232efcbe92e383e9d74b8555), + ipfsHash: keccak256("IPFS_HASH") + }) + ); + + angle.mint(address(distributor), 1e18); + agEUR.mint(address(distributor), 5e17); + + vm.warp(distributor.endOfDisputePeriod() + 1); + { + bytes32[][] memory proofs = new bytes32[][](1); + address[] memory users = new address[](1); + address[] memory tokens = new address[](1); + uint256[] memory amounts = new uint256[](1); + // proofs[0] = new bytes32[](1); + // proofs[0][0] = bytes32(0x6f46ee2909b99367a0d9932a11f1bdb85c9354480c9de277d21086f9a8925c0a); + // users[0] = alice; + // tokens[0] = address(angle); + // amounts[0] = 1e18; + proofs[0] = new bytes32[](1); + proofs[0][0] = bytes32(0x3a64e591d79db8530701e6f3dbdd95dc74681291b327d0ce4acc97024a61430c); + users[0] = bob; + tokens[0] = address(agEUR); + amounts[0] = 5e17; + vm.prank(bob); + distributor.claim(users, tokens, amounts, proofs); + } + + { + address[] memory users = new address[](1); + users[0] = bob; + + vm.prank(alice); + vm.expectRevert(Errors.InvalidOverride.selector); + creator.reallocateCampaignRewards(campaignId, users, address(governor)); + + assertEq(creator.campaignReallocation(campaignId, alice), address(0)); + address[] memory listReallocation = creator.getCampaignListReallocation(campaignId); + assertEq(listReallocation.length, 0); + } + } + + function testUnit_ReallocationCampaignRewards_revertWhen_AlreadyClaimed() public { + IERC20 rewardToken = IERC20(address(agEUR)); + uint256 amount = 100 ether; + uint32 startTimestamp = uint32(block.timestamp + 600); + + vm.prank(alice); + bytes32 campaignId = creator.createCampaign( + CampaignParameters({ + campaignId: bytes32(0), + creator: alice, + rewardToken: address(rewardToken), + amount: amount, + campaignType: 1, + startTimestamp: startTimestamp, + duration: 3600 * 24, + campaignData: abi.encode( + 0xbEEfa1aBfEbE621DF50ceaEF9f54FdB73648c92C, + new address[](0), + new address[](0), + "", + new bytes[](0), + new bytes[](0), + hex"" + ) + }) + ); + + vm.prank(governor); + // Create false tree + distributor.updateTree( + MerkleTree({ + merkleRoot: bytes32(0x0b70a97c062cb747158b89e27df5bbda859ba072232efcbe92e383e9d74b8555), + ipfsHash: keccak256("IPFS_HASH") + }) + ); + + angle.mint(address(distributor), 1e18); + agEUR.mint(address(distributor), 5e17); + + vm.warp(distributor.endOfDisputePeriod() + 1); + { + bytes32[][] memory proofs = new bytes32[][](1); + address[] memory users = new address[](1); + address[] memory tokens = new address[](1); + uint256[] memory amounts = new uint256[](1); + // proofs[0] = new bytes32[](1); + // proofs[0][0] = bytes32(0x6f46ee2909b99367a0d9932a11f1bdb85c9354480c9de277d21086f9a8925c0a); + // users[0] = alice; + // tokens[0] = address(angle); + // amounts[0] = 1e18; + proofs[0] = new bytes32[](1); + proofs[0][0] = bytes32(0x3a64e591d79db8530701e6f3dbdd95dc74681291b327d0ce4acc97024a61430c); + users[0] = bob; + tokens[0] = address(agEUR); + amounts[0] = 5e17; + vm.prank(bob); + distributor.claim(users, tokens, amounts, proofs); + } + + { + address[] memory users = new address[](1); + users[0] = bob; + + vm.prank(alice); + vm.expectRevert(Errors.InvalidOverride.selector); + creator.reallocateCampaignRewards(campaignId, users, address(governor)); + + assertEq(creator.campaignReallocation(campaignId, alice), address(0)); + address[] memory listReallocation = creator.getCampaignListReallocation(campaignId); + assertEq(listReallocation.length, 0); + } } - /* - // Commented because of an update in Foundry which does not handle well out of gas issues - function testFuzz_GetDistributionsOutOfGas() public { - address[] memory positionWrappers = new address[](3); - uint32[] memory wrapperTypes = new uint32[](3); - positionWrappers[0] = alice; - positionWrappers[1] = bob; - positionWrappers[2] = charlie; - wrapperTypes[0] = 0; - wrapperTypes[1] = 1; - wrapperTypes[2] = 2; - - vm.startPrank(alice); - // struct DistributionParameters memory - // create a bunch of distributions to make the view function call fail - DistributionParameters memory params = DistributionParameters({ - uniV3Pool: address(pool), - rewardToken: address(angle), - positionWrappers: positionWrappers, - wrapperTypes: wrapperTypes, - amount: 1e10, - propToken0: 4000, - propToken1: 2000, - propFees: 4000, - isOutOfRangeIncentivized: 0, - epochStart: startTime, - numEpoch: 25, - boostedReward: 0, - boostingAddress: address(0), - rewardId: keccak256("TEST"), - additionalData: hex"" - }); - - vm.warp(startTime + 3600 * 24 * 10); - startTime = uint32(block.timestamp); - endTime = startTime + numEpoch * EPOCH_DURATION; - params.epochStart = startTime; - for (uint256 i; i < maxDistribForOOG; i++) creator.createDistribution(params); + function testUnit_ReallocationCampaignRewards_Success() public { + IERC20 rewardToken = IERC20(address(angle)); + uint256 amount = 100 ether; + uint32 startTimestamp = uint32(block.timestamp + 600); + + vm.prank(alice); + bytes32 campaignId = creator.createCampaign( + CampaignParameters({ + campaignId: bytes32(0), + creator: alice, + rewardToken: address(rewardToken), + amount: amount, + campaignType: 1, + startTimestamp: startTimestamp, + duration: 3600 * 24, + campaignData: abi.encode( + 0xbEEfa1aBfEbE621DF50ceaEF9f54FdB73648c92C, + new address[](0), + new address[](0), + "", + new bytes[](0), + new bytes[](0), + hex"" + ) + }) + ); + + vm.prank(governor); + // Create false tree + distributor.updateTree( + MerkleTree({ + merkleRoot: bytes32(0x0b70a97c062cb747158b89e27df5bbda859ba072232efcbe92e383e9d74b8555), + ipfsHash: keccak256("IPFS_HASH") + }) + ); + + angle.mint(address(distributor), 1e18); + agEUR.mint(address(distributor), 5e17); + + vm.warp(distributor.endOfDisputePeriod() + 1); + { + bytes32[][] memory proofs = new bytes32[][](1); + address[] memory users = new address[](1); + address[] memory tokens = new address[](1); + uint256[] memory amounts = new uint256[](1); + // proofs[0] = new bytes32[](1); + // proofs[0][0] = bytes32(0x6f46ee2909b99367a0d9932a11f1bdb85c9354480c9de277d21086f9a8925c0a); + // users[0] = alice; + // tokens[0] = address(angle); + // amounts[0] = 1e18; + proofs[0] = new bytes32[](1); + proofs[0][0] = bytes32(0x3a64e591d79db8530701e6f3dbdd95dc74681291b327d0ce4acc97024a61430c); + users[0] = bob; + tokens[0] = address(agEUR); + amounts[0] = 5e17; + + uint256 aliceBalance = angle.balanceOf(address(alice)); + uint256 bobBalance = agEUR.balanceOf(address(bob)); + + vm.prank(bob); + distributor.claim(users, tokens, amounts, proofs); + } + { + address[] memory users = new address[](1); + users[0] = alice; + + vm.prank(bob); + vm.expectRevert(Errors.InvalidOverride.selector); + creator.reallocateCampaignRewards(campaignId, users, address(governor)); + + assertEq(creator.campaignReallocation(campaignId, alice), address(0)); + } + + { + address[] memory users = new address[](1); + users[0] = alice; + + vm.prank(alice); + creator.reallocateCampaignRewards(campaignId, users, address(governor)); + + assertEq(creator.campaignReallocation(campaignId, alice), address(governor)); + + address[] memory listReallocation = creator.getCampaignListReallocation(campaignId); + assertEq(listReallocation.length, 1); + assertEq(listReallocation[0], alice); + } + } +} + +contract DistributionCreatorOverrideTest is Fixture { + using SafeERC20 for IERC20; + + uint256 constant maxDistribForOOG = 1e4; + uint256 constant nbrDistrib = 10; + uint32 initStartTime; + uint32 initEndTime; + uint32 startTime; + uint32 endTime; + uint32 numEpoch; + + function setUp() public override { + super.setUp(); + + initStartTime = uint32(block.timestamp); + numEpoch = 25; + initEndTime = startTime + numEpoch * EPOCH_DURATION; + + vm.startPrank(guardian); + creator.toggleSigningWhitelist(alice); + creator.toggleTokenWhitelist(address(agEUR)); + address[] memory tokens = new address[](1); + uint256[] memory amounts = new uint256[](1); + tokens[0] = address(angle); + amounts[0] = 1e8; + creator.setRewardTokenMinAmounts(tokens, amounts); vm.stopPrank(); - // All calls will revert because it is oog - vm.expectRevert(); - creator.getActiveDistributions(); - vm.expectRevert(); - creator.getDistributionsForEpoch(startTime); + angle.mint(address(alice), 1e22); + vm.prank(alice); + angle.approve(address(creator), type(uint256).max); - vm.expectRevert(); - creator.getDistributionsBetweenEpochs(startTime, endTime); + vm.stopPrank(); + } - vm.expectRevert(); - creator.getDistributionsAfterEpoch(startTime); + function testUnit_OverrideCampaignData_RevertWhen_IncorrectCampaignId() public { + IERC20 rewardToken = IERC20(address(angle)); + uint256 amount = 100 ether; + uint256 amountAfterFees = 90 ether; + uint32 startTimestamp = uint32(block.timestamp + 600); - vm.expectRevert(); - creator.getActivePoolDistributions(address(pool)); + vm.prank(alice); + bytes32 campaignId = creator.createCampaign( + CampaignParameters({ + campaignId: bytes32(0), + creator: alice, + rewardToken: address(rewardToken), + amount: amount, + campaignType: 1, + startTimestamp: startTimestamp, + duration: 3600 * 24, + campaignData: abi.encode( + 0xbEEfa1aBfEbE621DF50ceaEF9f54FdB73648c92C, + new address[](0), + new address[](0), + "", + new bytes[](0), + new bytes[](0), + hex"" + ) + }) + ); - vm.expectRevert(); - creator.getPoolDistributionsForEpoch(address(pool), startTime); + vm.warp(block.timestamp + 1000); + vm.roll(4); + // override + + // Silo distrib + address[] memory whitelist = new address[](1); + whitelist[0] = 0x8095806d8753C0443C118D1C5e5eEC472e30BFeC; + bytes memory campaignData = abi.encode( + 0x04C0599Ae5A44757c0af6F9eC3b93da8976c150A, + 2, + 0xa42001D6d2237d2c74108FE360403C4b796B7170, + whitelist, + new address[](0), + hex"" + ); + + vm.expectRevert(Errors.CampaignDoesNotExist.selector); + vm.prank(alice); + creator.overrideCampaign( + keccak256("test"), + CampaignParameters({ + campaignId: campaignId, + creator: alice, + rewardToken: address(rewardToken), + amount: amountAfterFees, + campaignType: 5, + startTimestamp: startTimestamp, + duration: 3600 * 24, + campaignData: campaignData + }) + ); + } + + function testUnit_OverrideCampaignData_RevertWhen_IncorrectCreator() public { + IERC20 rewardToken = IERC20(address(angle)); + uint256 amount = 100 ether; + uint256 amountAfterFees = 90 ether; + uint32 startTimestamp = uint32(block.timestamp + 600); + vm.prank(alice); + bytes32 campaignId = creator.createCampaign( + CampaignParameters({ + campaignId: bytes32(0), + creator: alice, + rewardToken: address(rewardToken), + amount: amount, + campaignType: 1, + startTimestamp: startTimestamp, + duration: 3600 * 24, + campaignData: abi.encode( + 0xbEEfa1aBfEbE621DF50ceaEF9f54FdB73648c92C, + new address[](0), + new address[](0), + "", + new bytes[](0), + new bytes[](0), + hex"" + ) + }) + ); + + vm.warp(block.timestamp + 1000); + vm.roll(4); + // override + + // Silo distrib + address[] memory whitelist = new address[](1); + whitelist[0] = 0x8095806d8753C0443C118D1C5e5eEC472e30BFeC; + bytes memory campaignData = abi.encode( + 0x04C0599Ae5A44757c0af6F9eC3b93da8976c150A, + 2, + 0xa42001D6d2237d2c74108FE360403C4b796B7170, + whitelist, + new address[](0), + hex"" + ); + + vm.expectRevert(Errors.InvalidOverride.selector); + vm.prank(bob); + creator.overrideCampaign( + campaignId, + CampaignParameters({ + campaignId: campaignId, + creator: bob, + rewardToken: address(rewardToken), + amount: amountAfterFees, + campaignType: 5, + startTimestamp: startTimestamp, + duration: 3600 * 24, + campaignData: campaignData + }) + ); + + vm.expectRevert(Errors.InvalidOverride.selector); + vm.prank(bob); + creator.overrideCampaign( + campaignId, + CampaignParameters({ + campaignId: campaignId, + creator: alice, + rewardToken: address(rewardToken), + amount: amountAfterFees, + campaignType: 5, + startTimestamp: startTimestamp, + duration: 3600 * 24, + campaignData: campaignData + }) + ); + } + + function testUnit_OverrideCampaignData_RevertWhen_IncorrectRewardToken() public { + IERC20 rewardToken = IERC20(address(angle)); + uint256 amount = 100 ether; + uint256 amountAfterFees = 90 ether; + uint32 startTimestamp = uint32(block.timestamp + 600); + + vm.prank(alice); + bytes32 campaignId = creator.createCampaign( + CampaignParameters({ + campaignId: bytes32(0), + creator: alice, + rewardToken: address(rewardToken), + amount: amount, + campaignType: 1, + startTimestamp: startTimestamp, + duration: 3600 * 24, + campaignData: abi.encode( + 0xbEEfa1aBfEbE621DF50ceaEF9f54FdB73648c92C, + new address[](0), + new address[](0), + "", + new bytes[](0), + new bytes[](0), + hex"" + ) + }) + ); + + vm.warp(block.timestamp + 1000); + vm.roll(4); + // override + + // Silo distrib + address[] memory whitelist = new address[](1); + whitelist[0] = 0x8095806d8753C0443C118D1C5e5eEC472e30BFeC; + bytes memory campaignData = abi.encode( + 0x04C0599Ae5A44757c0af6F9eC3b93da8976c150A, + 2, + 0xa42001D6d2237d2c74108FE360403C4b796B7170, + whitelist, + new address[](0), + hex"" + ); + + vm.expectRevert(Errors.InvalidOverride.selector); + vm.prank(alice); + creator.overrideCampaign( + campaignId, + CampaignParameters({ + campaignId: campaignId, + creator: alice, + rewardToken: address(alice), + amount: amountAfterFees, + campaignType: 5, + startTimestamp: startTimestamp, + duration: 3600 * 24, + campaignData: campaignData + }) + ); + } + + function testUnit_OverrideCampaignData_RevertWhen_IncorrectRewardAmount() public { + IERC20 rewardToken = IERC20(address(angle)); + uint256 amount = 100 ether; + uint256 amountAfterFees = 90 ether; + uint32 startTimestamp = uint32(block.timestamp + 600); + + vm.prank(alice); + bytes32 campaignId = creator.createCampaign( + CampaignParameters({ + campaignId: bytes32(0), + creator: alice, + rewardToken: address(rewardToken), + amount: amount, + campaignType: 1, + startTimestamp: startTimestamp, + duration: 3600 * 24, + campaignData: abi.encode( + 0xbEEfa1aBfEbE621DF50ceaEF9f54FdB73648c92C, + new address[](0), + new address[](0), + "", + new bytes[](0), + new bytes[](0), + hex"" + ) + }) + ); + + vm.warp(block.timestamp + 1000); + vm.roll(4); + // override + + // Silo distrib + address[] memory whitelist = new address[](1); + whitelist[0] = 0x8095806d8753C0443C118D1C5e5eEC472e30BFeC; + bytes memory campaignData = abi.encode( + 0x04C0599Ae5A44757c0af6F9eC3b93da8976c150A, + 2, + 0xa42001D6d2237d2c74108FE360403C4b796B7170, + whitelist, + new address[](0), + hex"" + ); + + vm.expectRevert(Errors.InvalidOverride.selector); + vm.prank(alice); + creator.overrideCampaign( + campaignId, + CampaignParameters({ + campaignId: campaignId, + creator: alice, + rewardToken: address(alice), + amount: amount, + campaignType: 5, + startTimestamp: startTimestamp, + duration: 3600 * 24, + campaignData: campaignData + }) + ); + } + + function testUnit_OverrideCampaignData_RevertWhen_IncorrectStartTimestamp() public { + IERC20 rewardToken = IERC20(address(angle)); + uint256 amount = 100 ether; + uint256 amountAfterFees = 90 ether; + uint32 startTimestamp = uint32(block.timestamp + 600); + + vm.prank(alice); + bytes32 campaignId = creator.createCampaign( + CampaignParameters({ + campaignId: bytes32(0), + creator: alice, + rewardToken: address(rewardToken), + amount: amount, + campaignType: 1, + startTimestamp: startTimestamp, + duration: 3600 * 24, + campaignData: abi.encode( + 0xbEEfa1aBfEbE621DF50ceaEF9f54FdB73648c92C, + new address[](0), + new address[](0), + "", + new bytes[](0), + new bytes[](0), + hex"" + ) + }) + ); + + vm.warp(block.timestamp + 1000); + vm.roll(4); + // override + + // Silo distrib + address[] memory whitelist = new address[](1); + whitelist[0] = 0x8095806d8753C0443C118D1C5e5eEC472e30BFeC; + bytes memory campaignData = abi.encode( + 0x04C0599Ae5A44757c0af6F9eC3b93da8976c150A, + 2, + 0xa42001D6d2237d2c74108FE360403C4b796B7170, + whitelist, + new address[](0), + hex"" + ); + + vm.expectRevert(Errors.InvalidOverride.selector); + vm.prank(alice); + creator.overrideCampaign( + campaignId, + CampaignParameters({ + campaignId: campaignId, + creator: alice, + rewardToken: address(rewardToken), + amount: amount, + campaignType: 5, + startTimestamp: startTimestamp + 1, + duration: 3600 * 24, + campaignData: campaignData + }) + ); + } + + function testUnit_OverrideCampaignData_RevertWhen_IncorrectDuration() public { + IERC20 rewardToken = IERC20(address(angle)); + uint256 amount = 100 ether; + uint256 amountAfterFees = 90 ether; + uint32 startTimestamp = uint32(block.timestamp + 600); + + vm.prank(alice); + bytes32 campaignId = creator.createCampaign( + CampaignParameters({ + campaignId: bytes32(0), + creator: alice, + rewardToken: address(rewardToken), + amount: amount, + campaignType: 1, + startTimestamp: startTimestamp, + duration: 3600 * 24, + campaignData: abi.encode( + 0xbEEfa1aBfEbE621DF50ceaEF9f54FdB73648c92C, + new address[](0), + new address[](0), + "", + new bytes[](0), + new bytes[](0), + hex"" + ) + }) + ); + + vm.warp(block.timestamp + 1000); + vm.roll(4); + // override + + // Silo distrib + address[] memory whitelist = new address[](1); + whitelist[0] = 0x8095806d8753C0443C118D1C5e5eEC472e30BFeC; + bytes memory campaignData = abi.encode( + 0x04C0599Ae5A44757c0af6F9eC3b93da8976c150A, + 2, + 0xa42001D6d2237d2c74108FE360403C4b796B7170, + whitelist, + new address[](0), + hex"" + ); + + vm.expectRevert(Errors.InvalidOverride.selector); + vm.prank(alice); + creator.overrideCampaign( + campaignId, + CampaignParameters({ + campaignId: campaignId, + creator: alice, + rewardToken: address(rewardToken), + amount: amount, + campaignType: 5, + startTimestamp: startTimestamp, + duration: 399, + campaignData: campaignData + }) + ); + } + + function testUnit_OverrideCampaignData() public { + IERC20 rewardToken = IERC20(address(angle)); + uint256 amount = 100 ether; + uint256 amountAfterFees = 90 ether; + uint32 startTimestamp = uint32(block.timestamp + 600); + + vm.prank(alice); + bytes32 campaignId = creator.createCampaign( + CampaignParameters({ + campaignId: bytes32(0), + creator: alice, + rewardToken: address(rewardToken), + amount: amount, + campaignType: 1, + startTimestamp: startTimestamp, + duration: 3600 * 24, + campaignData: abi.encode( + 0xbEEfa1aBfEbE621DF50ceaEF9f54FdB73648c92C, + new address[](0), + new address[](0), + "", + new bytes[](0), + new bytes[](0), + hex"" + ) + }) + ); + + CampaignParameters memory campaign = creator.campaign(campaignId); + assertEq(campaign.creator, alice); + assertEq(campaign.rewardToken, address(rewardToken)); + assertEq(campaign.amount, (amount * (1e9 - creator.defaultFees())) / 1e9); + assertEq(campaign.campaignType, 1); + assertEq(campaign.startTimestamp, startTimestamp); + assertEq(campaign.duration, 3600 * 24); + + vm.warp(block.timestamp + 1000); + vm.roll(4); + // override + + // Silo distrib + address[] memory whitelist = new address[](1); + whitelist[0] = 0x8095806d8753C0443C118D1C5e5eEC472e30BFeC; + bytes memory campaignData = abi.encode( + 0x04C0599Ae5A44757c0af6F9eC3b93da8976c150A, + 2, + 0xa42001D6d2237d2c74108FE360403C4b796B7170, + whitelist, + new address[](0), + hex"" + ); + + vm.prank(alice); + creator.overrideCampaign( + campaignId, + CampaignParameters({ + campaignId: campaignId, + creator: alice, + rewardToken: address(rewardToken), + amount: amountAfterFees, + campaignType: 5, + startTimestamp: startTimestamp, + duration: 3600 * 24, + campaignData: campaignData + }) + ); + + ( + , + address campaignCreator, + address campaignRewardToken, + uint256 campaignAmount, + uint256 campaignType, + uint32 campaignStartTimestamp, + uint32 campaignDuration, + bytes memory campaignCampaignData + ) = creator.campaignOverrides(campaignId); + assertEq(campaignCreator, alice); + assertEq(campaignRewardToken, address(rewardToken)); + assertEq(campaignAmount, amountAfterFees); + assertEq(campaignType, 5); + assertEq(campaignStartTimestamp, startTimestamp); + assertEq(campaignDuration, 3600 * 24); + assertEq(campaignCampaignData, campaignData); + assertGe(creator.campaignOverridesTimestamp(campaignId, 0), startTimestamp); vm.expectRevert(); - creator.getPoolDistributionsBetweenEpochs(address(pool), startTime, endTime); + creator.campaignOverridesTimestamp(campaignId, 1); + } + + function testUnit_OverrideCampaignDuration() public { + IERC20 rewardToken = IERC20(address(angle)); + uint256 amount = 100 ether; + uint256 amountAfterFees = 90 ether; + uint32 startTimestamp = uint32(block.timestamp + 600); + uint32 duration = 3600 * 24; + uint32 durationAfterOverride = 3600 * 12; + + bytes memory campaignData = abi.encode( + 0xbEEfa1aBfEbE621DF50ceaEF9f54FdB73648c92C, + new address[](0), + new address[](0), + "", + new bytes[](0), + new bytes[](0), + hex"" + ); + vm.prank(alice); + bytes32 campaignId = creator.createCampaign( + CampaignParameters({ + campaignId: bytes32(0), + creator: alice, + rewardToken: address(rewardToken), + amount: amount, + campaignType: 1, + startTimestamp: startTimestamp, + duration: duration, + campaignData: campaignData + }) + ); + + CampaignParameters memory campaign = creator.campaign(campaignId); + assertEq(campaign.creator, alice); + assertEq(campaign.rewardToken, address(rewardToken)); + assertEq(campaign.amount, (amount * (1e9 - creator.defaultFees())) / 1e9); + assertEq(campaign.campaignType, 1); + assertEq(campaign.startTimestamp, startTimestamp); + assertEq(campaign.duration, duration); + vm.warp(block.timestamp + 1000); + vm.roll(4); + // override + + vm.prank(alice); + creator.overrideCampaign( + campaignId, + CampaignParameters({ + campaignId: campaignId, + creator: alice, + rewardToken: address(rewardToken), + amount: amountAfterFees, + campaignType: 1, + startTimestamp: startTimestamp, + duration: durationAfterOverride, + campaignData: campaignData + }) + ); + + ( + , + address campaignCreator, + address campaignRewardToken, + uint256 campaignAmount, + uint256 campaignType, + uint32 campaignStartTimestamp, + uint32 campaignDuration, + bytes memory campaignCampaignData + ) = creator.campaignOverrides(campaignId); + assertEq(campaignCreator, alice); + assertEq(campaignRewardToken, address(rewardToken)); + assertEq(campaignAmount, amountAfterFees); + assertEq(campaignType, 1); + assertEq(campaignStartTimestamp, startTimestamp); + assertEq(campaignDuration, durationAfterOverride); + assertEq(campaignCampaignData, campaignData); + assertGe(creator.campaignOverridesTimestamp(campaignId, 0), startTimestamp); vm.expectRevert(); - creator.getPoolDistributionsAfterEpoch(address(pool), startTime); + creator.campaignOverridesTimestamp(campaignId, 1); } - */ - /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - WITH DIFFERENT POOLS - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + function testUnit_GetCampaignOverridesTimestamp() public { + IERC20 rewardToken = IERC20(address(angle)); + uint256 amount = 100 ether; + uint256 amountAfterFees = 90 ether; + uint32 startTimestamp = uint32(block.timestamp + 600); + uint32 duration = 3600 * 24; + uint32 durationAfterOverride = 3600 * 12; + + bytes memory campaignData = abi.encode( + 0xbEEfa1aBfEbE621DF50ceaEF9f54FdB73648c92C, + new address[](0), + new address[](0), + "", + new bytes[](0), + new bytes[](0), + hex"" + ); + vm.prank(alice); + bytes32 campaignId = creator.createCampaign( + CampaignParameters({ + campaignId: bytes32(0), + creator: alice, + rewardToken: address(rewardToken), + amount: amount, + campaignType: 1, + startTimestamp: startTimestamp, + duration: duration, + campaignData: campaignData + }) + ); + + vm.warp(block.timestamp + 1000); + vm.roll(4); + // override + + vm.prank(alice); + creator.overrideCampaign( + campaignId, + CampaignParameters({ + campaignId: campaignId, + creator: alice, + rewardToken: address(rewardToken), + amount: amountAfterFees, + campaignType: 1, + startTimestamp: startTimestamp, + duration: durationAfterOverride, + campaignData: campaignData + }) + ); + + vm.warp(block.timestamp + 1000); + vm.roll(4); + // override + + vm.prank(alice); + creator.overrideCampaign( + campaignId, + CampaignParameters({ + campaignId: campaignId, + creator: alice, + rewardToken: address(rewardToken), + amount: amountAfterFees, + campaignType: 10, + startTimestamp: startTimestamp, + duration: durationAfterOverride * 10, + campaignData: campaignData + }) + ); + + uint256[] memory timestamps = creator.getCampaignOverridesTimestamp(campaignId); + assertEq(timestamps.length, 2); + assertEq(timestamps[0], 1001); + assertEq(timestamps[1], 2001); + } + + function testUnit_OverrideCampaignAdditionalFee() public { + vm.prank(governor); + creator.setCampaignFees(3, 1e7); + + IERC20 rewardToken = IERC20(address(angle)); + uint256 amount = 100 ether; + uint256 amountAfterFees = 90 ether; + uint32 startTimestamp = uint32(block.timestamp + 600); + uint32 duration = 3600 * 24; + uint32 durationAfterOverride = 3600 * 12; + + uint256 prevBalance = rewardToken.balanceOf(alice); + + bytes memory campaignData = abi.encode( + 0xbEEfa1aBfEbE621DF50ceaEF9f54FdB73648c92C, + new address[](0), + new address[](0), + "", + new bytes[](0), + new bytes[](0), + hex"" + ); + vm.prank(alice); + bytes32 campaignId = creator.createCampaign( + CampaignParameters({ + campaignId: bytes32(0), + creator: alice, + rewardToken: address(rewardToken), + amount: amount, + campaignType: 1, + startTimestamp: startTimestamp, + duration: duration, + campaignData: campaignData + }) + ); + + assertEq(rewardToken.balanceOf(alice), prevBalance - amount); + + vm.warp(block.timestamp + 1000); + vm.roll(4); + // override + vm.prank(alice); + creator.overrideCampaign( + campaignId, + CampaignParameters({ + campaignId: campaignId, + creator: alice, + rewardToken: address(rewardToken), + amount: amountAfterFees, + campaignType: 3, + startTimestamp: startTimestamp, + duration: durationAfterOverride, + campaignData: campaignData + }) + ); + + assertEq(rewardToken.balanceOf(alice), prevBalance - amount - (amountAfterFees * 1e7) / 1e9); + } } 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..33ba762 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,30 +940,43 @@ 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); - - assertEq(distributions.length, 1); - assertEq(distributions[0].uniV3Pool, address(0x149e36E72726e0BceA5c59d40df2c43F60f5A22D)); - assertEq(distributions[0].rewardToken, address(0xE0688A2FE90d0f93F17f273235031062a210d691)); - assertEq(distributions[0].amount, 9700000000000000000000); - assertEq(distributions[0].positionWrappers.length, 0); - assertEq(distributions[0].wrapperTypes.length, 0); - assertEq(distributions[0].propToken0, 2000); - assertEq(distributions[0].propToken1, 5000); - assertEq(distributions[0].propFees, 3000); - assertEq(distributions[0].isOutOfRangeIncentivized, 0); - assertEq(distributions[0].epochStart, 1681380000); - 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"); - } -} \ No newline at end of file +// contract Test_DistributionCreator_getDistributionsBetweenEpochs is DistributionCreatorForkTest { +// function test_Success() public { +// (DistributionParameters[] memory distributions, ) = creator.getDistributionsBetweenEpochs( +// 1681380000, +// 1681380000 + 3600, +// 0, +// type(uint32).max +// ); + +// assertEq(distributions.length, 1); +// assertEq(distributions[0].uniV3Pool, address(0x149e36E72726e0BceA5c59d40df2c43F60f5A22D)); +// assertEq(distributions[0].rewardToken, address(0xE0688A2FE90d0f93F17f273235031062a210d691)); +// assertEq(distributions[0].amount, 9700000000000000000000); +// assertEq(distributions[0].positionWrappers.length, 0); +// assertEq(distributions[0].wrapperTypes.length, 0); +// assertEq(distributions[0].propToken0, 2000); +// assertEq(distributions[0].propToken1, 5000); +// assertEq(distributions[0].propFees, 3000); +// assertEq(distributions[0].isOutOfRangeIncentivized, 0); +// assertEq(distributions[0].epochStart, 1681380000); +// 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" +// ); +// } +// } diff --git a/test/foundry/unit/Distributor.t.sol b/test/foundry/unit/Distributor.t.sol index 0124a11..b4b58dd 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())); } @@ -88,22 +88,22 @@ contract Test_Distributor_toggleOperator is DistributorCreatorTest { } } -contract Test_Distributor_toggleOnlyOperatorCanClaim is DistributorCreatorTest { - function test_RevertWhen_NotTrusted() public { - vm.expectRevert(NotTrusted.selector); - distributor.toggleOnlyOperatorCanClaim(bob); - } +// contract Test_Distributor_toggleOnlyOperatorCanClaim is DistributorCreatorTest { +// function test_RevertWhen_NotTrusted() public { +// vm.expectRevert(NotTrusted.selector); +// distributor.toggleOnlyOperatorCanClaim(bob); +// } - function test_Success() public { - vm.prank(governor); - distributor.toggleOnlyOperatorCanClaim(bob); - assertEq(distributor.onlyOperatorCanClaim(bob), 1); +// function test_Success() public { +// vm.prank(governor); +// distributor.toggleOnlyOperatorCanClaim(bob); +// assertEq(distributor.onlyOperatorCanClaim(bob), 1); - vm.prank(bob); - distributor.toggleOnlyOperatorCanClaim(bob); - assertEq(distributor.onlyOperatorCanClaim(bob), 0); - } -} +// vm.prank(bob); +// distributor.toggleOnlyOperatorCanClaim(bob); +// assertEq(distributor.onlyOperatorCanClaim(bob), 0); +// } +// } contract Test_Distributor_recoverERC20 is DistributorCreatorTest { function test_RevertWhen_NotGovernor() public { @@ -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); @@ -368,32 +368,32 @@ 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")})); + // function test_RevertWhen_NotWhitelisted() public { + // vm.prank(governor); + // distributor.updateTree(MerkleTree({ merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH") })); - vm.warp(distributor.endOfDisputePeriod() + 1); + // vm.warp(distributor.endOfDisputePeriod() + 1); - vm.prank(bob); - distributor.toggleOnlyOperatorCanClaim(bob); + // vm.prank(bob); + // distributor.toggleOnlyOperatorCanClaim(bob); - bytes32[][] memory proofs = new bytes32[][](1); - address[] memory users = new address[](1); - address[] memory tokens = new address[](1); - uint256[] memory amounts = new uint256[](1); - proofs[0] = new bytes32[](1); - users[0] = bob; - tokens[0] = address(angle); - amounts[0] = 1e18; + // bytes32[][] memory proofs = new bytes32[][](1); + // address[] memory users = new address[](1); + // address[] memory tokens = new address[](1); + // uint256[] memory amounts = new uint256[](1); + // proofs[0] = new bytes32[](1); + // users[0] = bob; + // tokens[0] = address(angle); + // amounts[0] = 1e18; - vm.expectRevert(NotWhitelisted.selector); - vm.prank(alice); - distributor.claim(users, tokens, amounts, proofs); - } + // vm.expectRevert(NotWhitelisted.selector); + // vm.prank(alice); + // distributor.claim(users, tokens, amounts, proofs); + // } 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); diff --git a/utils/forwardUtils.js b/utils/forwardUtils.js new file mode 100755 index 0000000..d5679d2 --- /dev/null +++ b/utils/forwardUtils.js @@ -0,0 +1,21 @@ +const { exec } = require('child_process'); + +if (process.argv.length < 3) { + console.error('Please provide a chain input as an argument.'); + process.exit(1); +} + +const command = process.argv[2]; +const extraArgs = process.argv.slice(3).join(' '); + +exec(`bun run lib/utils/utils/${command}.js ${extraArgs}`, (error, stdout, stderr) => { + if (error) { + console.log(error); + process.exit(1); + } + if (stderr) { + console.log(stderr); + process.exit(1); + } + console.log(stdout); +}); diff --git a/yarn.lock b/yarn.lock index b268705..4803640 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,21 +7,24 @@ resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== -"@angleprotocol/sdk@2.7.0": - version "2.7.0" - resolved "https://npm.pkg.github.com/download/@angleprotocol/sdk/2.7.0/1c2a1d32c4ca3792c2325d86d4830aed03f18442#1c2a1d32c4ca3792c2325d86d4830aed03f18442" - integrity sha512-CjLSxubhuRw68DP4u6Fpa/GnluaxB86FmAIC2W+ysF5o38nMP/TcHbWDK/WyM85QorZWT5AdzyS/fggyZrAUOA== +"@angleprotocol/sdk@^v2.28.19": + version "2.33.34" + resolved "https://npm.pkg.github.com/download/@angleprotocol/sdk/2.33.34/0268b1d3f9ac2b37d3a7eb267aa077f24fa36426#0268b1d3f9ac2b37d3a7eb267aa077f24fa36426" + integrity sha512-GPVN8azvCgqT8F/rGp5d0m4uJ8ZFO7wKwfOFgLEK2bM5V0UziQBWX/2sbLKQNUzEcNVklRqnK/nNufFDrPqxwg== dependencies: "@apollo/client" "^3.7.17" "@typechain/ethers-v5" "^10.0.0" - "@types/lodash" "^4.14.180" + bun-types "^1.1.27" + class-transformer "^0.5.1" + class-validator "^0.14.1" + cross-fetch "^4.0.0" ethers "^5.6.4" graphql "^15.7.1" graphql-request "^3.6.1" jsbi "^4.3.0" keccak256 "^1.0.6" - lodash "^4.17.21" merkletreejs "^0.3.10" + reflect-metadata "^0.2.2" tiny-invariant "^1.1.0" typechain "^8.3.2" @@ -2165,11 +2168,6 @@ dependencies: keyv "*" -"@types/lodash@^4.14.180": - version "4.14.181" - resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.181.tgz" - integrity sha512-n3tyKthHJbkiWhDZs3DkhkCzt2MexYHXlX0td5iMplyfwketaOeKboEVBqzceH7juqvEg3q5oUoBFxSLu7zFag== - "@types/lru-cache@^5.1.0": version "5.1.1" resolved "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.1.tgz" @@ -2205,6 +2203,13 @@ resolved "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz" integrity sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw== +"@types/node@~20.12.8": + version "20.12.14" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.14.tgz#0c5cf7ef26aedfd64b0539bba9380ed1f57dcc77" + integrity sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg== + dependencies: + undici-types "~5.26.4" + "@types/pbkdf2@^3.0.0": version "3.1.0" resolved "https://registry.npmjs.org/@types/pbkdf2/-/pbkdf2-3.1.0.tgz" @@ -2249,6 +2254,18 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.4.tgz#0a41252ad431c473158b22f9bfb9a63df7541cff" integrity sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ== +"@types/validator@^13.11.8": + version "13.12.2" + resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.12.2.tgz#760329e756e18a4aab82fc502b51ebdfebbe49f5" + integrity sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA== + +"@types/ws@~8.5.10": + version "8.5.13" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.13.tgz#6414c280875e2691d0d1e080b05addbf5cb91e20" + integrity sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA== + dependencies: + "@types/node" "*" + "@types/yargs-parser@*": version "21.0.0" resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz" @@ -3144,6 +3161,14 @@ builtins@^5.0.1: dependencies: semver "^7.0.0" +bun-types@^1.1.27: + version "1.1.34" + resolved "https://registry.yarnpkg.com/bun-types/-/bun-types-1.1.34.tgz#cf0e1dc5aa8875573a3acb09bead0f23bab5aca2" + integrity sha512-br5QygTEL/TwB4uQOb96Ky22j4Gq2WxWH/8Oqv20fk5HagwKXo/akB+LiYgSfzexCt6kkcUaVm+bKiPl71xPvw== + dependencies: + "@types/node" "~20.12.8" + "@types/ws" "~8.5.10" + busboy@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" @@ -3460,6 +3485,20 @@ class-is@^1.1.0: resolved "https://registry.npmjs.org/class-is/-/class-is-1.1.0.tgz" integrity sha512-rhjH9AG1fvabIDoGRVH587413LPjTZgmDF9fOFCbFJQV4yuocX1mHxxvXI4g3cGwbVY9wAYIoKlg1N79frJKQw== +class-transformer@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.5.1.tgz#24147d5dffd2a6cea930a3250a677addf96ab336" + integrity sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw== + +class-validator@^0.14.1: + version "0.14.1" + resolved "https://registry.yarnpkg.com/class-validator/-/class-validator-0.14.1.tgz#ff2411ed8134e9d76acfeb14872884448be98110" + integrity sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ== + dependencies: + "@types/validator" "^13.11.8" + libphonenumber-js "^1.10.53" + validator "^13.9.0" + classic-level@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/classic-level/-/classic-level-1.2.0.tgz" @@ -3790,6 +3829,13 @@ cross-fetch@^3.0.6, cross-fetch@^3.1.4: dependencies: node-fetch "2.6.7" +cross-fetch@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983" + integrity sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g== + dependencies: + node-fetch "^2.6.12" + cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz" @@ -6990,6 +7036,11 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +libphonenumber-js@^1.10.53: + version "1.11.14" + resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.11.14.tgz#d753524fd30e6433834a1464baf7efed4a06b593" + integrity sha512-sexvAfwcW1Lqws4zFp8heAtAEXbEDnvkYCEGzvOoMgZR7JhXo/IkE9MkkGACgBed5fWqh3ShBGnJBdDnU9N8EQ== + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" @@ -7702,6 +7753,13 @@ node-fetch@2.6.7, node-fetch@^2.6.0: dependencies: whatwg-url "^5.0.0" +node-fetch@^2.6.12: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + node-gyp-build@^4.2.0, node-gyp-build@^4.3.0: version "4.3.0" resolved "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.3.0.tgz" @@ -8574,6 +8632,11 @@ reduce-flatten@^2.0.0: resolved "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz" integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w== +reflect-metadata@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz#400c845b6cba87a21f2c65c4aeb158f4fa4d9c5b" + integrity sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q== + regenerator-runtime@^0.13.4: version "0.13.9" resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz" @@ -10140,6 +10203,11 @@ underscore@^1.8.3: resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.6.tgz#04786a1f589dc6c09f761fc5f45b89e935136441" integrity sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + undici@^5.14.0: version "5.18.0" resolved "https://registry.yarnpkg.com/undici/-/undici-5.18.0.tgz#e88a77a74d991a30701e9a6751e4193a26fabda9" @@ -10267,6 +10335,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +validator@^13.9.0: + version "13.12.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.12.0.tgz#7d78e76ba85504da3fee4fd1922b385914d4b35f" + integrity sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg== + varint@^5.0.0: version "5.0.2" resolved "https://registry.npmjs.org/varint/-/varint-5.0.2.tgz"