Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: merklV2 poc #47

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@angleprotocol:registry=https://npm.pkg.github.com
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"editor.defaultFormatter": "JuanBlanco.solidity"
},
"editor.codeActionsOnSave": {
"source.fixAll": true
"source.fixAll": "explicit"
},
"solidity.compileUsingRemoteVersion": "v0.8.17+commit.8df45f5f"
}
843 changes: 378 additions & 465 deletions contracts/DistributionCreator.sol

Large diffs are not rendered by default.

22 changes: 14 additions & 8 deletions contracts/Distributor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ struct Claim {
}

/// @title Distributor
/// @notice Allows LPs on AMMs with concentrated liquidity to claim the rewards that were distributed to them
/// @notice Allows to claim rewards distributed to them through Merkl
/// @author Angle Labs. Inc
contract Distributor is UUPSHelper {
using SafeERC20 for IERC20;
Expand Down Expand Up @@ -130,6 +130,12 @@ contract Distributor 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))
Expand All @@ -147,7 +153,7 @@ contract Distributor is UUPSHelper {
}

/// @inheritdoc UUPSUpgradeable
function _authorizeUpgrade(address) internal view override onlyGuardianUpgrader(core) {}
function _authorizeUpgrade(address) internal view override onlyGovernorUpgrader(core) {}

// =============================== MAIN FUNCTION ===============================

Expand Down Expand Up @@ -205,7 +211,7 @@ contract Distributor 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);
Expand All @@ -218,7 +224,7 @@ contract Distributor 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;
Expand Down Expand Up @@ -278,26 +284,26 @@ contract Distributor 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);
Expand Down
307 changes: 261 additions & 46 deletions contracts/deprecated/OldDistributionCreator.sol

Large diffs are not rendered by default.

68 changes: 43 additions & 25 deletions contracts/deprecated/OldDistributor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@

pragma solidity ^0.8.17;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/math/SafeCast.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";

import "../utils/UUPSHelper.sol";

Expand All @@ -54,14 +54,18 @@ struct MerkleTree {
struct Claim {
uint208 amount;
uint48 timestamp;
bytes32 merkleRoot;
}

/// @title OldDistributor
/// @title Distributor
/// @notice Allows LPs on AMMs with concentrated liquidity to claim the rewards that were distributed to them
/// @author Angle Labs. Inc
contract OldDistributor is UUPSHelper {
using SafeERC20 for IERC20;

/// @notice Epoch duration
uint32 internal constant _EPOCH_DURATION = 3600;

// ================================= VARIABLES =================================

/// @notice Tree of claimable tokens through this contract
Expand All @@ -80,10 +84,10 @@ contract OldDistributor is UUPSHelper {
/// @dev Used to store if there is an ongoing dispute
address public disputer;

/// @notice Last time the `tree` was updated
uint48 public lastTreeUpdate;
/// @notice When the current tree will become valid
uint48 public endOfDisputePeriod;

/// @notice Time before which a change in a tree becomes effective
/// @notice Time after which a change in a tree becomes effective, in EPOCH_DURATION
uint48 public disputePeriod;

/// @notice Amount to deposit to freeze the roots update
Expand All @@ -105,16 +109,17 @@ contract OldDistributor is UUPSHelper {

// =================================== EVENTS ==================================

event Claimed(address user, address token, uint256 amount);
event Claimed(address indexed user, address indexed token, uint256 amount);
event DisputeAmountUpdated(uint256 _disputeAmount);
event Disputed(string reason);
event DisputePeriodUpdated(uint48 _disputePeriod);
event DisputeTokenUpdated(address indexed _disputeToken);
event DisputeAmountUpdated(uint256 _disputeAmount);
event DisputeResolved(bool valid);
event OperatorClaimingToggled(address user, bool isEnabled);
event OperatorToggled(address user, address operator, bool isWhitelisted);
event DisputeTokenUpdated(address indexed _disputeToken);
event OperatorClaimingToggled(address indexed user, bool isEnabled);
event OperatorToggled(address indexed user, address indexed operator, bool isWhitelisted);
event Recovered(address indexed token, address indexed to, uint256 amount);
event TreeUpdated(bytes32 merkleRoot, bytes32 ipfsHash);
event Revoked(); // With this event an indexer could maintain a table (timestamp, merkleRootUpdate)
event TreeUpdated(bytes32 merkleRoot, bytes32 ipfsHash, uint48 endOfDisputePeriod);
event TrustedToggled(address indexed eoa, bool trust);

// ================================= MODIFIERS =================================
Expand Down Expand Up @@ -181,7 +186,7 @@ contract OldDistributor is UUPSHelper {

// Closing reentrancy gate here
uint256 toSend = amount - claimed[user][token].amount;
claimed[user][token] = Claim(SafeCast.toUint208(amount), uint48(block.timestamp));
claimed[user][token] = Claim(SafeCast.toUint208(amount), uint48(block.timestamp), getMerkleRoot());

IERC20(token).safeTransfer(user, toSend);
emit Claimed(user, token, toSend);
Expand All @@ -193,7 +198,7 @@ contract OldDistributor is UUPSHelper {

/// @notice Returns the MerkleRoot that is currently live for the contract
function getMerkleRoot() public view returns (bytes32) {
if (block.timestamp - lastTreeUpdate >= disputePeriod) return tree.merkleRoot;
if (block.timestamp >= endOfDisputePeriod && disputer == address(0)) return tree.merkleRoot;
else return lastTree.merkleRoot;
}

Expand All @@ -212,21 +217,24 @@ contract OldDistributor is UUPSHelper {
disputer != address(0) ||
// A trusted address cannot update a tree right after a precedent tree update otherwise it can de facto
// validate a tree which has not passed the dispute period
((canUpdateMerkleRoot[msg.sender] != 1 || block.timestamp - lastTreeUpdate < disputePeriod) &&
((canUpdateMerkleRoot[msg.sender] != 1 || block.timestamp < endOfDisputePeriod) &&
!core.isGovernorOrGuardian(msg.sender))
) revert NotTrusted();
MerkleTree memory _lastTree = tree;
tree = _tree;
lastTree = _lastTree;
lastTreeUpdate = uint48(block.timestamp);
emit TreeUpdated(_tree.merkleRoot, _tree.ipfsHash);

uint48 _endOfPeriod = _endOfDisputePeriod(uint48(block.timestamp));
endOfDisputePeriod = _endOfPeriod;
emit TreeUpdated(_tree.merkleRoot, _tree.ipfsHash, _endOfPeriod);
}

/// @notice Freezes the Merkle tree update until the dispute is resolved
/// @dev Requires a deposit of `disputeToken` that'll be slashed if the dispute is not accepted
/// @dev It is only possible to create a dispute for `disputePeriod` after each tree update
/// @dev It is only possible to create a dispute within `disputePeriod` after each tree update
function disputeTree(string memory reason) external {
if (block.timestamp - lastTreeUpdate >= disputePeriod) revert InvalidDispute();
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);
Expand All @@ -242,7 +250,7 @@ contract OldDistributor is UUPSHelper {
_revokeTree();
} else {
IERC20(disputeToken).safeTransfer(msg.sender, disputeAmount);
lastTreeUpdate = uint48(block.timestamp);
endOfDisputePeriod = _endOfDisputePeriod(uint48(block.timestamp));
}
disputer = address(0);
emit DisputeResolved(valid);
Expand Down Expand Up @@ -275,9 +283,8 @@ contract OldDistributor is UUPSHelper {
emit Recovered(tokenAddress, to, amountToRecover);
}

/// @notice Sets the dispute period before which a tree update becomes effective
/// @notice Sets the dispute period after which a tree update becomes effective
function setDisputePeriod(uint48 _disputePeriod) external onlyGovernorOrGuardian {
if (_disputePeriod > block.timestamp) revert InvalidParam();
disputePeriod = uint48(_disputePeriod);
emit DisputePeriodUpdated(_disputePeriod);
}
Expand All @@ -301,9 +308,20 @@ contract OldDistributor is UUPSHelper {
/// @notice Fallback to the last version of the tree
function _revokeTree() internal {
MerkleTree memory _tree = lastTree;
lastTreeUpdate = 0;
endOfDisputePeriod = 0;
tree = _tree;
emit TreeUpdated(_tree.merkleRoot, _tree.ipfsHash);
emit Revoked();
emit TreeUpdated(
_tree.merkleRoot,
_tree.ipfsHash,
(uint48(block.timestamp) / _EPOCH_DURATION) * (_EPOCH_DURATION) // Last hour
);
}

/// @notice Returns the end of the dispute period
/// @dev treeUpdate is rounded up to next hour and then `disputePeriod` hours are added
function _endOfDisputePeriod(uint48 treeUpdate) internal view returns (uint48) {
return ((treeUpdate - 1) / _EPOCH_DURATION + 1 + disputePeriod) * (_EPOCH_DURATION);
}

/// @notice Checks the validity of a proof
Expand Down
29 changes: 29 additions & 0 deletions contracts/struct/CampaignParameters.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: BUSL-1.1

pragma solidity ^0.8.17;

struct CampaignParameters {
// POPULATED ONCE CREATED

// ID of the campaign. This can be left as a null bytes32 when creating campaigns
// on Merkl.
bytes32 campaignId;
// CHOSEN BY CAMPAIGN CREATOR

// Address of the campaign creator, if marked as address(0), it will be overriden with the
// address of the `msg.sender` creating the campaign
address creator;
// Address of the token used as a reward
address rewardToken;
// Amount of `rewardToken` to distribute across all the epochs
// Amount distributed per epoch is `amount/numEpoch`
uint256 amount;
// Type of campaign
uint32 campaignType;
// Timestamp at which the campaign should start
uint32 startTimestamp;
// Duration of the campaign in seconds. Has to be a multiple of EPOCH = 3600
uint32 duration;
// Extra data to pass to specify the campaign
bytes campaignData;
}
6 changes: 6 additions & 0 deletions contracts/utils/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

pragma solidity ^0.8.17;

error CampaignDoesNotExist();
error CampaignAlreadyExists();
error CampaignDurationBelowHour();
error CampaignRewardTokenNotWhitelisted();
error CampaignRewardTooLow();
error CampaignSouldStartInFuture();
error InvalidDispute();
error InvalidLengths();
error InvalidParam();
Expand Down
7 changes: 4 additions & 3 deletions deploy/0_distributor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ const func: DeployFunction = async ({ deployments, ethers, network }) => {
const { deployer } = await ethers.getNamedSigners();

let core: string;
core = '0xC16B81Af351BA9e64C1a069E3Ab18c244A1E3049';
// Base - CoreBorrowTest
core = '0xA6a505Eeb4e1e93052c48126f2d42ef6694A651D';
/*
if (!network.live) {
// If we're in mainnet fork, we're using the `CoreBorrow` address from mainnet
Expand Down Expand Up @@ -40,14 +41,14 @@ const func: DeployFunction = async ({ deployments, ethers, network }) => {

console.log('Now deploying the Proxy');

await deploy('Distributor', {
await deploy('TestDistributor', {
contract: 'ERC1967Proxy',
from: deployer.address,
args: [implementationAddress, '0x'],
log: !argv.ci,
});

const distributor = (await deployments.get('Distributor')).address;
const distributor = (await deployments.get('TestDistributor')).address;
console.log(`Successfully deployed contract at the address ${distributor}`);
console.log('Initializing the contract');
const contract = new ethers.Contract(distributor, Distributor__factory.createInterface(), deployer) as Distributor;
Expand Down
47 changes: 24 additions & 23 deletions deploy/1_distributionCreator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,46 +18,47 @@ const func: DeployFunction = async ({ deployments, ethers, network }) => {
// Otherwise, we're using the proxy admin address from the desired network
core = registry(network.config.chainId as ChainId)?.Merkl?.CoreMerkl!;
}
core = '0xA6a505Eeb4e1e93052c48126f2d42ef6694A651D';

console.log(deployer.address);

console.log('Now deploying DistributionCreator');
console.log('Starting with the implementation');
console.log('deployer ', await deployer.getBalance());
await deploy('DistributionCreator_Implementation_7', {
await deploy('TestDistributionCreator_Implementation', {
contract: 'DistributionCreator',
from: deployer.address,
log: !argv.ci,
});

const implementationAddress = (await ethers.getContract('DistributionCreator_Implementation_7')).address;
const implementationAddress = (await ethers.getContract('TestDistributionCreator_Implementation')).address;

console.log(`Successfully deployed the implementation for DistributionCreator at ${implementationAddress}`);
console.log('');

// const distributor = (await deployments.get('Distributor')).address;
// console.log('Now deploying the Proxy');
const distributor = (await deployments.get('Distributor')).address;
console.log('Now deploying the Proxy');

// await deploy('DistributionCreator', {
// contract: 'ERC1967Proxy',
// from: deployer.address,
// args: [implementationAddress, '0x'],
// log: !argv.ci,
// });
await deploy('TestDistributionCreator', {
contract: 'ERC1967Proxy',
from: deployer.address,
args: [implementationAddress, '0x'],
log: !argv.ci,
});

// const manager = (await deployments.get('DistributionCreator')).address;
// console.log(`Successfully deployed contract at the address ${manager}`);
// console.log('Initializing the contract');
// const contract = new ethers.Contract(
// manager,
// DistributionCreator__factory.createInterface(),
// deployer,
// ) as DistributionCreator;
const manager = (await deployments.get('TestDistributionCreator')).address;
console.log(`Successfully deployed contract at the address ${manager}`);
console.log('Initializing the contract');
const contract = new ethers.Contract(
manager,
DistributionCreator__factory.createInterface(),
deployer,
) as DistributionCreator;

// await (await contract.connect(deployer).initialize(core, distributor, parseAmount.gwei('0.03'))).wait();
// console.log('Contract successfully initialized');
// console.log('');
// console.log(await contract.core());
await (await contract.connect(deployer).initialize(core, distributor, parseAmount.gwei('0.03'))).wait();
console.log('Contract successfully initialized');
console.log('');
console.log(await contract.core());

/* Once good some functions need to be called to have everything setup.

Expand All @@ -76,5 +77,5 @@ const func: DeployFunction = async ({ deployments, ethers, network }) => {
};

func.tags = ['distributionCreator'];
func.dependencies = [];
func.dependencies = ['distributor'];
export default func;
Loading