Skip to content

Commit

Permalink
feat: check if the chainlink price feed return zero price
Browse files Browse the repository at this point in the history
test: update tests accordingly
  • Loading branch information
andreivladbrg committed Feb 25, 2025
1 parent 93e0a30 commit 940d734
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 13 deletions.
3 changes: 1 addition & 2 deletions src/abstracts/SablierMerkleBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,8 @@ abstract contract SablierMerkleBase is
return 0;
}

// Q: should we do a low-level call here instead?
// Retrieve the latest price from the Chainlink price feed.
(, 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 * minimumFee / uint256(price);
Expand Down
34 changes: 28 additions & 6 deletions src/abstracts/SablierMerkleFactoryBase.sol
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.8.22;

import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
import { Adminable } from "@sablier/evm-utils/src/Adminable.sol";

import { ISablierMerkleBase } from "../interfaces/ISablierMerkleBase.sol";
import { ISablierMerkleFactoryBase } from "../interfaces/ISablierMerkleFactoryBase.sol";
import { Errors } from "../libraries/Errors.sol";
import { MerkleFactory } from "../types/DataTypes.sol";

/// @title SablierMerkleFactoryBase
Expand All @@ -30,6 +32,7 @@ abstract contract SablierMerkleFactoryBase is
CONSTRUCTOR
//////////////////////////////////////////////////////////////////////////*/

/// @dev Emits a {SetChainlinkPriceFeed} event.
/// @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.
Expand All @@ -40,8 +43,8 @@ abstract contract SablierMerkleFactoryBase is
)
Adminable(initialAdmin)
{
chainlinkPriceFeed = initialChainlinkPriceFeed;
minimumFee = initialMinimumFee;
_setChainlinkPriceFeed(initialChainlinkPriceFeed, initialAdmin);
}

/*//////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -81,11 +84,7 @@ abstract contract SablierMerkleFactoryBase is

/// @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 });
_setChainlinkPriceFeed(newChainlinkPriceFeed, msg.sender);
}

/// @inheritdoc ISablierMerkleFactoryBase
Expand Down Expand Up @@ -113,6 +112,29 @@ abstract contract SablierMerkleFactoryBase is
emit SetMinimumFee({ admin: msg.sender, minimumFee: newMinimumFee });
}

/*//////////////////////////////////////////////////////////////////////////
PRIVATE NON-CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/

/// @dev See the documentation for the user-facing functions that call this internal function.
function _setChainlinkPriceFeed(address newChainlinkPriceFeed, address admin) private {
// If the Chainlink address is not zero, verify that the price feed is valid.
if (newChainlinkPriceFeed != address(0)) {
(, int256 price,,,) = AggregatorV3Interface(newChainlinkPriceFeed).latestRoundData();

// Check: the price is not zero.
if (price == 0) {
revert Errors.IncorrectChainlinkPriceFeed(newChainlinkPriceFeed);
}
}

// Effect: update the Chainlink price feed.
chainlinkPriceFeed = newChainlinkPriceFeed;

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

/*//////////////////////////////////////////////////////////////////////////
INTERNAL CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
Expand Down
1 change: 1 addition & 0 deletions src/interfaces/ISablierMerkleFactoryBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ interface ISablierMerkleFactoryBase is IAdminable {
///
/// Requirements:
/// - `msg.sender` must be the admin.
/// - If `newChainlinkPriceFeed` is not the zero address, it must return a price greater than zero.
///
/// @param newChainlinkPriceFeed The new Chainlink price feed contract.
function setChainlinkPriceFeed(address newChainlinkPriceFeed) external;
Expand Down
7 changes: 7 additions & 0 deletions src/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ library Errors {
/// @notice Thrown when trying to claim the same stream more than once.
error SablierMerkleBase_StreamClaimed(uint256 index);

/*//////////////////////////////////////////////////////////////////////////
SABLIER-MERKLE-FACTORY-BASE
//////////////////////////////////////////////////////////////////////////*/

/// @notice Thrown when trying to an invalid Chainlink price feed contract address.
error IncorrectChainlinkPriceFeed(address chainlinkPriceFeed);

/*//////////////////////////////////////////////////////////////////////////
SABLIER-MERKLE-LT
//////////////////////////////////////////////////////////////////////////*/
Expand Down
15 changes: 14 additions & 1 deletion src/tests/ChainlinkPriceFeedMock.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.22;

/// @notice A mock Chainlink price feed contract that returns a constant price of $3000 for 1 ETH.
/// @notice A mock Chainlink price feed contract that returns a constant price of $3000 for 1 native token.
contract ChainlinkPriceFeedMock {
int256 private constant THREE_THOUSAND = 3000e8; // Chainlink format price (8 decimals)

Expand All @@ -13,3 +13,16 @@ contract ChainlinkPriceFeedMock {
return (0, THREE_THOUSAND, 0, 0, 0);
}
}

/// @notice A mock Chainlink price feed contract that returns a price of $0.
contract ChainlinkPriceFeedMock_Zero {
int256 private constant ZERO = 0;

function latestRoundData()
external
pure
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
{
return (0, ZERO, 0, 0, 0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ pragma solidity >=0.8.22 <0.9.0;

import { Errors as EvmUtilsErrors } from "@sablier/evm-utils/src/libraries/Errors.sol";

import { Integration_Test } from "./../../../../Integration.t.sol";
import { ISablierMerkleFactoryBase } from "src/interfaces/ISablierMerkleFactoryBase.sol";
import { Errors } from "src/libraries/Errors.sol";
import { ChainlinkPriceFeedMock, ChainlinkPriceFeedMock_Zero } from "src/tests/ChainlinkPriceFeedMock.sol";

import { Integration_Test } from "../../../../Integration.t.sol";

abstract contract SetChainlinkPriceFeed_Integration_Test is Integration_Test {
function test_RevertWhen_CallerNotAdmin() external {
Expand All @@ -14,8 +18,45 @@ abstract contract SetChainlinkPriceFeed_Integration_Test is Integration_Test {

function test_WhenCallerAdmin() external {
resetPrank({ msgSender: users.admin });
assertEq(merkleFactoryBase.chainlinkPriceFeed(), address(chainlinkPriceFeed), "price feed before");
}

function test_WhenNewPriceFeedZeroAddress() external whenCallerAdmin {
resetPrank({ msgSender: users.admin });

assertNotEq(merkleFactoryBase.chainlinkPriceFeed(), address(0), "price feed before");

vm.expectEmit({ emitter: address(merkleFactoryBase) });
emit ISablierMerkleFactoryBase.SetChainlinkPriceFeed(users.admin, address(0));
merkleFactoryBase.setChainlinkPriceFeed(address(0));

assertEq(merkleFactoryBase.chainlinkPriceFeed(), address(0), "price feed after");
}

modifier whenNewPriceFeedNotZeroAddress() {
_;
}

function test_RevertWhen_NewPriceFeedReturnsZeroPrice() external whenCallerAdmin whenNewPriceFeedNotZeroAddress {
ChainlinkPriceFeedMock_Zero newChainlinkPriceFeed = new ChainlinkPriceFeedMock_Zero();
resetPrank({ msgSender: users.admin });
vm.expectRevert(
abi.encodeWithSelector(Errors.IncorrectChainlinkPriceFeed.selector, address(newChainlinkPriceFeed))
);
merkleFactoryBase.setChainlinkPriceFeed(address(newChainlinkPriceFeed));
}

function test_WhenNewPriceFeedReturnsNonZeroPrice() external whenCallerAdmin whenNewPriceFeedNotZeroAddress {
// Deploy a new Chainlink price feed contract that returns a constant price of $3000 for 1 native token.
ChainlinkPriceFeedMock newChainlinkPriceFeed = new ChainlinkPriceFeedMock();

resetPrank({ msgSender: users.admin });

assertNotEq(merkleFactoryBase.chainlinkPriceFeed(), address(newChainlinkPriceFeed), "price feed before");

vm.expectEmit({ emitter: address(merkleFactoryBase) });
emit ISablierMerkleFactoryBase.SetChainlinkPriceFeed(users.admin, address(newChainlinkPriceFeed));
merkleFactoryBase.setChainlinkPriceFeed(address(newChainlinkPriceFeed));

assertEq(merkleFactoryBase.chainlinkPriceFeed(), address(newChainlinkPriceFeed), "price feed after");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,12 @@ SetChainlinkPriceFeed_Integration_Test
├── when caller not admin
│ └── it should revert
└── when caller admin
├── it should set the price feed
└── it should emit a {SetChainlinkPriceFeed} event
├── when new price feed zero address
│ ├── it should set the price feed
│ └── it should emit a {SetChainlinkPriceFeed} event
└── when new price feed not zero address
├── when new price feed returns zero price
│ └── it should revert
└── when new price feed returns non zero price
├── it should set the price feed
└── it should emit a {SetChainlinkPriceFeed} event

0 comments on commit 940d734

Please sign in to comment.