Skip to content

Commit

Permalink
feat: v1 override and reallocation
Browse files Browse the repository at this point in the history
  • Loading branch information
GuillaumeNervoXS committed Nov 21, 2024
1 parent 2b5df12 commit 46f4580
Show file tree
Hide file tree
Showing 13 changed files with 1,656 additions and 198 deletions.
55 changes: 41 additions & 14 deletions contracts/DistributionCreator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ import { Distributor } from "./Distributor.sol";
/// @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 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 more global name to describe any reward program on top of Merkl
Expand Down Expand Up @@ -130,7 +130,13 @@ contract DistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable {
mapping(bytes32 => CampaignParameters) public campaignOverrides;

/// @notice Maps a campaignId to the block numbers at which it's been updated
mapping(bytes32 => uint256[]) public campaignOverridesBlocks;
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
Expand Down Expand Up @@ -282,12 +288,18 @@ contract DistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable {
newCampaign.rewardToken != _campaign.rewardToken ||
newCampaign.amount != _campaign.amount ||
newCampaign.startTimestamp != _campaign.startTimestamp ||
// TODO we may want to be able to override the duration
// but we need to make sure that rewards/s is not an invariant in the engine
newCampaign.duration != _campaign.duration
// 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;
campaignOverridesBlocks[_campaignId].push(block.number);
campaignOverridesTimestamp[_campaignId].push(block.timestamp);
emit CampaignOverride(_campaignId, newCampaign);
}

Expand All @@ -299,7 +311,8 @@ contract DistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable {
/// @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) revert InvalidOverride();
if (_campaign.creator != msg.sender || block.timestamp < _campaign.startTimestamp + _campaign.duration)
revert InvalidOverride();

uint256 fromsLength = froms.length;
address[] memory successfullFrom = new address[](fromsLength);
Expand All @@ -308,6 +321,8 @@ contract DistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable {
(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++;
}
}
Expand Down Expand Up @@ -400,6 +415,14 @@ contract DistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable {
return _getCampaignsBetween(start, end, 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];
}

/*//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
GOVERNANCE FUNCTIONS
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -507,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();
Expand Down Expand Up @@ -581,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) {
Expand All @@ -597,7 +625,6 @@ contract DistributionCreator is UUPSHelper, ReentrancyGuardUpgradeable {
distributionAmount - distributionAmountMinusFees
);
}
IERC20(rewardToken).safeTransferFrom(msg.sender, distributor, distributionAmountMinusFees);
}

/// @notice Internal version of the `sign` function
Expand Down
24 changes: 20 additions & 4 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ gas_reports = ["*"]
via_ir = true
optimizer_runs=100

# solc_version = '0.8.17'
solc_version = '0.8.25'

ffi = true

Expand All @@ -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"}
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}"}
37 changes: 37 additions & 0 deletions helpers/fork.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#! /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 "Forking $uri"
anvil --fork-url $uri
}

main
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,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",
Expand All @@ -34,6 +35,10 @@
"hardhat:deploy": "hardhat deploy --network linea --tags",
"hardhat:test": "hardhat test",
"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 0x19c41f6607b2c0e80e84baadaf886b17565f278e 1000000000000000000",
"fork:advanceTime": "cast rpc evm_increaseTime 3600 && cast rpc anvil_mine",
"license": "hardhat prepend-spdx-license",
"node:fork": "FORK=true hardhat node",
"size": "yarn hardhat:compile && hardhat size-contracts",
Expand All @@ -52,7 +57,7 @@
"url": "https://github.com/AngleProtocol/merkl-contracts/issues"
},
"devDependencies": {
"@angleprotocol/sdk": "2.7.0",
"@angleprotocol/sdk": "^v2.28.19",
"@ethersproject/abi": "^5.7.0",
"@ethersproject/providers": "^5.7.1",
"@nomicfoundation/hardhat-chai-matchers": "^1.0.3",
Expand Down Expand Up @@ -118,4 +123,4 @@
"solc": "0.8.12",
"yargs": "^17.5.1"
}
}
}
62 changes: 62 additions & 0 deletions scripts/foundry/CreateCampaign.s.sol
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading

0 comments on commit 46f4580

Please sign in to comment.