Skip to content

Commit

Permalink
feat: implement chainlink oracle
Browse files Browse the repository at this point in the history
refactor: change the minimum fee to USD price instead of wei

build: set ffi true
test: add a python script to fetch prices
test: add fork tests for the price feeds
test: add a mock price feed contract
test: add a contract to store the addresses

test: update tests accordingly
  • Loading branch information
andreivladbrg committed Feb 24, 2025
1 parent a1b2be0 commit 78a74f4
Show file tree
Hide file tree
Showing 65 changed files with 563 additions and 165 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,15 @@ jobs:
needs: ["lint", "build"]
secrets:
MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}
ARBITRUM_RPC_URL: ${{ secrets.ARBITRUM_RPC_URL }}
AVALANCHE_RPC_URL: ${{ secrets.AVALANCHE_RPC_URL }}
OPTIMISM_RPC_URL: ${{ secrets.OPTIMISM_RPC_URL }}
POLYGON_RPC_URL: ${{ secrets.POLYGON_RPC_URL }}
uses: "sablier-labs/reusable-workflows/.github/workflows/forge-test.yml@main"
with:
foundry-fuzz-runs: 20
foundry-profile: "test-optimized"
# install-python: true
match-path: "tests/fork/**/*.sol"
name: "Fork tests"

Expand Down
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
auto_detect_solc = false
bytecode_hash = "none"
evm_version = "shanghai"
ffi = true
fs_permissions = [
{ access = "read", path = "./out-optimized" },
{ access = "read", path = "package.json" },
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"url": "https://github.com/sablier-labs/airdrops/issues"
},
"dependencies": {
"@chainlink/contracts": "^1.3.0",
"@openzeppelin/contracts": "5.0.2",
"@prb/math": "4.1.0",
"@sablier/lockup": "github:sablier-labs/lockup#723a956",
Expand Down
1 change: 1 addition & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@chainlink/contracts/=node_modules/@chainlink/contracts/
@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/
@prb/math/=node_modules/@prb/math/
@sablier/evm-utils/=node_modules/@sablier/evm-utils/
Expand Down
3 changes: 2 additions & 1 deletion script/CreateMerkleInstant.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ contract CreateMerkleInstant is BaseScript {
/// @dev Deploy via Forge.
function run() public broadcast returns (ISablierMerkleInstant merkleInstant) {
// TODO: Load deployed addresses from Ethereum mainnet.
SablierMerkleFactoryInstant merkleFactory = new SablierMerkleFactoryInstant(DEFAULT_SABLIER_ADMIN, 0);
SablierMerkleFactoryInstant merkleFactory =
new SablierMerkleFactoryInstant(DEFAULT_SABLIER_ADMIN, address(0), 0);

// Prepare the constructor parameters.
MerkleInstant.ConstructorParams memory params;
Expand Down
2 changes: 1 addition & 1 deletion script/CreateMerkleLL.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ contract CreateMerkleLL is BaseScript {
/// @dev Deploy via Forge.
function run() public broadcast returns (ISablierMerkleLL merkleLL) {
// TODO: Load deployed addresses from Ethereum mainnet.
SablierMerkleFactoryLL merkleFactory = new SablierMerkleFactoryLL(DEFAULT_SABLIER_ADMIN, 0);
SablierMerkleFactoryLL merkleFactory = new SablierMerkleFactoryLL(DEFAULT_SABLIER_ADMIN, address(0), 0);

// Prepare the constructor parameters.
MerkleLL.ConstructorParams memory params;
Expand Down
2 changes: 1 addition & 1 deletion script/CreateMerkleLT.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ contract CreateMerkleLT is BaseScript {
/// @dev Deploy via Forge.
function run() public broadcast returns (ISablierMerkleLT merkleLT) {
// TODO: Load deployed addresses from Ethereum mainnet.
SablierMerkleFactoryLT merkleFactory = new SablierMerkleFactoryLT(DEFAULT_SABLIER_ADMIN, 0);
SablierMerkleFactoryLT merkleFactory = new SablierMerkleFactoryLT(DEFAULT_SABLIER_ADMIN, address(0), 0);

// Prepare the constructor parameters.
MerkleLT.ConstructorParams memory params;
Expand Down
2 changes: 1 addition & 1 deletion script/CreateMerkleVCA.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ contract CreateMerkleVCA is BaseScript {
/// @dev Deploy via Forge.
function run() public broadcast returns (ISablierMerkleVCA merkleVCA) {
// TODO: Load deployed addresses from Ethereum mainnet.
SablierMerkleFactoryVCA merkleFactory = new SablierMerkleFactoryVCA(DEFAULT_SABLIER_ADMIN, 0);
SablierMerkleFactoryVCA merkleFactory = new SablierMerkleFactoryVCA(DEFAULT_SABLIER_ADMIN, address(0), 0);

// Prepare the constructor parameters.
MerkleVCA.ConstructorParams memory params;
Expand Down
21 changes: 15 additions & 6 deletions script/DeployDeterministicMerkleFactories.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import { SablierMerkleFactoryInstant } from "../src/SablierMerkleFactoryInstant.
import { SablierMerkleFactoryLL } from "../src/SablierMerkleFactoryLL.sol";
import { SablierMerkleFactoryLT } from "../src/SablierMerkleFactoryLT.sol";
import { SablierMerkleFactoryVCA } from "../src/SablierMerkleFactoryVCA.sol";
import { ChainlinkPriceFeedAddresses } from "../src/tests/ChainlinkPriceFeedAddresses.sol";
import { InitialMinimumFees } from "../src/tests/InitialMinimumFees.sol";

/// @notice Deploys Merkle factory contracts at deterministic address.
///
/// @dev Reverts if any contract has already been deployed.
contract DeployDeterministicMerkleFactories is BaseScript {
contract DeployDeterministicMerkleFactories is BaseScript, ChainlinkPriceFeedAddresses, InitialMinimumFees {
/// @dev Deploy via Forge.
function run(uint256 initialMinimumFee)
function run()
public
broadcast
returns (
Expand All @@ -23,9 +25,16 @@ contract DeployDeterministicMerkleFactories is BaseScript {
SablierMerkleFactoryVCA merkleFactoryVCA
)
{
merkleFactoryInstant = new SablierMerkleFactoryInstant{ salt: SALT }(protocolAdmin(), initialMinimumFee);
merkleFactoryLL = new SablierMerkleFactoryLL{ salt: SALT }(protocolAdmin(), initialMinimumFee);
merkleFactoryLT = new SablierMerkleFactoryLT{ salt: SALT }(protocolAdmin(), initialMinimumFee);
merkleFactoryVCA = new SablierMerkleFactoryVCA{ salt: SALT }(protocolAdmin(), initialMinimumFee);
address initialAdmin = protocolAdmin();
address initialChainlinkPriceFeed = getPriceFeedAddress();
uint256 initialMinimumFee = getMinimumFee();
merkleFactoryInstant =
new SablierMerkleFactoryInstant{ salt: SALT }(initialAdmin, initialChainlinkPriceFeed, initialMinimumFee);
merkleFactoryLL =
new SablierMerkleFactoryLL{ salt: SALT }(initialAdmin, initialChainlinkPriceFeed, initialMinimumFee);
merkleFactoryLT =
new SablierMerkleFactoryLT{ salt: SALT }(initialAdmin, initialChainlinkPriceFeed, initialMinimumFee);
merkleFactoryVCA =
new SablierMerkleFactoryVCA{ salt: SALT }(initialAdmin, initialChainlinkPriceFeed, initialMinimumFee);
}
}
18 changes: 12 additions & 6 deletions script/DeployMerkleFactories.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import { SablierMerkleFactoryInstant } from "../src/SablierMerkleFactoryInstant.
import { SablierMerkleFactoryLL } from "../src/SablierMerkleFactoryLL.sol";
import { SablierMerkleFactoryLT } from "../src/SablierMerkleFactoryLT.sol";
import { SablierMerkleFactoryVCA } from "../src/SablierMerkleFactoryVCA.sol";
import { ChainlinkPriceFeedAddresses } from "../src/tests/ChainlinkPriceFeedAddresses.sol";
import { InitialMinimumFees } from "../src/tests/InitialMinimumFees.sol";

/// @notice Deploys Merkle factory contracts.
contract DeployMerkleFactories is BaseScript {
contract DeployMerkleFactories is BaseScript, ChainlinkPriceFeedAddresses, InitialMinimumFees {
/// @dev Deploy via Forge.
function run(uint256 initialMinimumFee)
function run()
public
broadcast
returns (
Expand All @@ -21,9 +23,13 @@ contract DeployMerkleFactories is BaseScript {
SablierMerkleFactoryVCA merkleFactoryVCA
)
{
merkleFactoryInstant = new SablierMerkleFactoryInstant(protocolAdmin(), initialMinimumFee);
merkleFactoryLL = new SablierMerkleFactoryLL(protocolAdmin(), initialMinimumFee);
merkleFactoryLT = new SablierMerkleFactoryLT(protocolAdmin(), initialMinimumFee);
merkleFactoryVCA = new SablierMerkleFactoryVCA(protocolAdmin(), initialMinimumFee);
address initialAdmin = protocolAdmin();
address initialChainlinkPriceFeed = getPriceFeedAddress();
uint256 initialMinimumFee = getMinimumFee();
merkleFactoryInstant =
new SablierMerkleFactoryInstant(initialAdmin, initialChainlinkPriceFeed, initialMinimumFee);
merkleFactoryLL = new SablierMerkleFactoryLL(initialAdmin, initialChainlinkPriceFeed, initialMinimumFee);
merkleFactoryLT = new SablierMerkleFactoryLT(initialAdmin, initialChainlinkPriceFeed, initialMinimumFee);
merkleFactoryVCA = new SablierMerkleFactoryVCA(initialAdmin, initialChainlinkPriceFeed, initialMinimumFee);
}
}
4 changes: 3 additions & 1 deletion src/SablierMerkleFactoryInstant.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ contract SablierMerkleFactoryInstant is ISablierMerkleFactoryInstant, SablierMer
//////////////////////////////////////////////////////////////////////////*/

/// @param initialAdmin The address of the initial contract admin.
/// @param initialChainlinkPriceFeed The initial Chainlink price feed contract.
/// @param initialMinimumFee The initial minimum fee charged for claiming an airdrop.
constructor(
address initialAdmin,
address initialChainlinkPriceFeed,
uint256 initialMinimumFee
)
SablierMerkleFactoryBase(initialAdmin, initialMinimumFee)
SablierMerkleFactoryBase(initialAdmin, initialChainlinkPriceFeed, initialMinimumFee)
{ }

/*//////////////////////////////////////////////////////////////////////////
Expand Down
4 changes: 3 additions & 1 deletion src/SablierMerkleFactoryLL.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ contract SablierMerkleFactoryLL is ISablierMerkleFactoryLL, SablierMerkleFactory
//////////////////////////////////////////////////////////////////////////*/

/// @param initialAdmin The address of the initial contract admin.
/// @param initialChainlinkPriceFeed The initial Chainlink price feed contract.
/// @param initialMinimumFee The initial minimum fee charged for claiming an airdrop.
constructor(
address initialAdmin,
address initialChainlinkPriceFeed,
uint256 initialMinimumFee
)
SablierMerkleFactoryBase(initialAdmin, initialMinimumFee)
SablierMerkleFactoryBase(initialAdmin, initialChainlinkPriceFeed, initialMinimumFee)
{ }

/*//////////////////////////////////////////////////////////////////////////
Expand Down
5 changes: 4 additions & 1 deletion src/SablierMerkleFactoryLT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity >=0.8.22;

import { uUNIT } from "@prb/math/src/UD2x18.sol";

import { SablierMerkleFactoryBase } from "./abstracts/SablierMerkleFactoryBase.sol";
import { ISablierMerkleFactoryLT } from "./interfaces/ISablierMerkleFactoryLT.sol";
import { ISablierMerkleLT } from "./interfaces/ISablierMerkleLT.sol";
Expand All @@ -16,12 +17,14 @@ contract SablierMerkleFactoryLT is ISablierMerkleFactoryLT, SablierMerkleFactory
//////////////////////////////////////////////////////////////////////////*/

/// @param initialAdmin The address of the initial contract admin.
/// @param initialChainlinkPriceFeed The initial Chainlink price feed contract.
/// @param initialMinimumFee The initial minimum fee charged for claiming an airdrop.
constructor(
address initialAdmin,
address initialChainlinkPriceFeed,
uint256 initialMinimumFee
)
SablierMerkleFactoryBase(initialAdmin, initialMinimumFee)
SablierMerkleFactoryBase(initialAdmin, initialChainlinkPriceFeed, initialMinimumFee)
{ }

/*//////////////////////////////////////////////////////////////////////////
Expand Down
4 changes: 3 additions & 1 deletion src/SablierMerkleFactoryVCA.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ contract SablierMerkleFactoryVCA is ISablierMerkleFactoryVCA, SablierMerkleFacto
//////////////////////////////////////////////////////////////////////////*/

/// @param initialAdmin The address of the initial contract admin.
/// @param initialChainlinkPriceFeed The initial Chainlink price feed contract.
/// @param initialMinimumFee The initial minimum fee charged for claiming an airdrop.
constructor(
address initialAdmin,
address initialChainlinkPriceFeed,
uint256 initialMinimumFee
)
SablierMerkleFactoryBase(initialAdmin, initialMinimumFee)
SablierMerkleFactoryBase(initialAdmin, initialChainlinkPriceFeed, initialMinimumFee)
{ }

/*//////////////////////////////////////////////////////////////////////////
Expand Down
47 changes: 40 additions & 7 deletions src/abstracts/SablierMerkleBase.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.8.22;

import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { MerkleProof } from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
Expand All @@ -24,6 +25,9 @@ abstract contract SablierMerkleBase is
STATE VARIABLES
//////////////////////////////////////////////////////////////////////////*/

/// @inheritdoc ISablierMerkleBase
address public immutable override CHAINLINK_PRICE_FEED;

/// @inheritdoc ISablierMerkleBase
uint40 public immutable override EXPIRATION;

Expand Down Expand Up @@ -67,19 +71,25 @@ abstract contract SablierMerkleBase is
)
Adminable(initialAdmin)
{
campaignName = _campaignName;
EXPIRATION = expiration;
FACTORY = msg.sender;
MINIMUM_FEE = ISablierMerkleFactoryBase(FACTORY).getFee(campaignCreator);
CHAINLINK_PRICE_FEED = ISablierMerkleFactoryBase(FACTORY).chainlinkPriceFeed();
EXPIRATION = expiration;
MERKLE_ROOT = merkleRoot;
MINIMUM_FEE = ISablierMerkleFactoryBase(FACTORY).getFee(campaignCreator);
TOKEN = token;
campaignName = _campaignName;
ipfsCID = _ipfsCID;
}

/*//////////////////////////////////////////////////////////////////////////
USER-FACING CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/

/// @inheritdoc ISablierMerkleBase
function calculateMinimumFeeInWei() external view returns (uint256) {
return _calculateMinimumFeeInWei();
}

/// @inheritdoc ISablierMerkleBase
function getFirstClaimTime() external view override returns (uint40) {
return _firstClaimTime;
Expand Down Expand Up @@ -115,9 +125,12 @@ abstract contract SablierMerkleBase is
revert Errors.SablierMerkleBase_CampaignExpired({ blockTimestamp: block.timestamp, expiration: EXPIRATION });
}

// Calculate the minimum fee in wei.
uint256 minimumFeeInWei = _calculateMinimumFeeInWei();

// Check: `msg.value` is not less than the minimum fee.
if (msg.value < MINIMUM_FEE) {
revert Errors.SablierMerkleBase_InsufficientFeePayment(msg.value, MINIMUM_FEE);
if (msg.value < minimumFeeInWei) {
revert Errors.SablierMerkleBase_InsufficientFeePayment(msg.value, minimumFeeInWei);
}

// Check: the index has not been claimed.
Expand Down Expand Up @@ -167,8 +180,8 @@ abstract contract SablierMerkleBase is
/// @inheritdoc ISablierMerkleBase
function collectFees(address factoryAdmin) external override returns (uint256 feeAmount) {
// Check: the caller is the FACTORY.
if (msg.sender != FACTORY) {
revert Errors.SablierMerkleBase_CallerNotFactory(FACTORY, msg.sender);
if (msg.sender != address(FACTORY)) {
revert Errors.SablierMerkleBase_CallerNotFactory(address(FACTORY), msg.sender);
}

feeAmount = address(this).balance;
Expand All @@ -186,6 +199,26 @@ abstract contract SablierMerkleBase is
INTERNAL CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/

/// @dev See the documentation for the user-facing functions that call this internal function.
function _calculateMinimumFeeInWei() internal view returns (uint256) {
// If the Chainlink price feed is not set, return 0.
if (CHAINLINK_PRICE_FEED == address(0)) {
return 0;
}

// If the minimum fee is 0, return 0.
if (MINIMUM_FEE == 0) {
return 0;
}

// Q: should we do a low-level call here instead?
(, int256 price,,,) = AggregatorV3Interface(CHAINLINK_PRICE_FEED).latestRoundData();
// Q: should we check the price is greater than 0 ? If yes, should we revert?

// Calculate the minimum fee in wei.
return 1e18 * MINIMUM_FEE / uint256(price);
}

/// @notice Returns a flag indicating whether the grace period has passed.
/// @dev The grace period is 7 days after the first claim.
function _hasGracePeriodPassed() internal view returns (bool) {
Expand Down
29 changes: 25 additions & 4 deletions src/abstracts/SablierMerkleFactoryBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ abstract contract SablierMerkleFactoryBase is
STATE VARIABLES
//////////////////////////////////////////////////////////////////////////*/

/// @inheritdoc ISablierMerkleFactoryBase
address public override chainlinkPriceFeed;

/// @inheritdoc ISablierMerkleFactoryBase
uint256 public override minimumFee;

Expand All @@ -28,8 +31,16 @@ abstract contract SablierMerkleFactoryBase is
//////////////////////////////////////////////////////////////////////////*/

/// @param initialAdmin The address of the initial contract admin.
/// @param initialChainlinkPriceFeed The initial Chainlink price feed contract address.
/// @param initialMinimumFee The initial minimum fee charged for claiming an airdrop.
constructor(address initialAdmin, uint256 initialMinimumFee) Adminable(initialAdmin) {
constructor(
address initialAdmin,
address initialChainlinkPriceFeed,
uint256 initialMinimumFee
)
Adminable(initialAdmin)
{
chainlinkPriceFeed = initialChainlinkPriceFeed;
minimumFee = initialMinimumFee;
}

Expand Down Expand Up @@ -68,6 +79,15 @@ abstract contract SablierMerkleFactoryBase is
emit ResetCustomFee({ admin: msg.sender, campaignCreator: campaignCreator });
}

/// @inheritdoc ISablierMerkleFactoryBase
function setChainlinkPriceFeed(address newChainlinkPriceFeed) external override onlyAdmin {
// Effect: update the Chainlink price feed.
chainlinkPriceFeed = newChainlinkPriceFeed;

// Log the update.
emit SetChainlinkPriceFeed({ admin: msg.sender, chainlinkPriceFeed: newChainlinkPriceFeed });
}

/// @inheritdoc ISablierMerkleFactoryBase
function setCustomFee(address campaignCreator, uint256 newFee) external override onlyAdmin {
MerkleFactory.CustomFee storage customFeeByUser = _customFees[campaignCreator];
Expand All @@ -85,11 +105,12 @@ abstract contract SablierMerkleFactoryBase is
}

/// @inheritdoc ISablierMerkleFactoryBase
function setMinimumFee(uint256 newFee) external override onlyAdmin {
function setMinimumFee(uint256 newMinimumFee) external override onlyAdmin {
// Effect: update the minimum fee.
minimumFee = newFee;
minimumFee = newMinimumFee;

emit SetMinimumFee({ admin: msg.sender, minimumFee: newFee });
// Log the update.
emit SetMinimumFee({ admin: msg.sender, minimumFee: newMinimumFee });
}

/*//////////////////////////////////////////////////////////////////////////
Expand Down
Loading

0 comments on commit 78a74f4

Please sign in to comment.