Skip to content

Commit

Permalink
feat: receive ether in claim function
Browse files Browse the repository at this point in the history
  • Loading branch information
smol-ninja committed Sep 9, 2024
1 parent 945c414 commit 945477a
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 15 deletions.
61 changes: 54 additions & 7 deletions src/periphery/SablierMerkleFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ pragma solidity >=0.8.22;

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

import { Adminable } from "../core/abstracts/Adminable.sol";
import { ISablierLockupLinear } from "../core/interfaces/ISablierLockupLinear.sol";
import { ISablierLockupTranched } from "../core/interfaces/ISablierLockupTranched.sol";

import { ISablierMerkleBase } from "./interfaces/ISablierMerkleBase.sol";
import { ISablierMerkleFactory } from "./interfaces/ISablierMerkleFactory.sol";
import { ISablierMerkleInstant } from "./interfaces/ISablierMerkleInstant.sol";
import { ISablierMerkleLL } from "./interfaces/ISablierMerkleLL.sol";
Expand All @@ -17,7 +19,28 @@ import { MerkleBase, MerkleLL, MerkleLT } from "./types/DataTypes.sol";

/// @title SablierMerkleFactory
/// @notice See the documentation in {ISablierMerkleFactory}.
contract SablierMerkleFactory is ISablierMerkleFactory {
contract SablierMerkleFactory is
ISablierMerkleFactory, // 2 inherited components
Adminable // 1 inherited component
{
/*//////////////////////////////////////////////////////////////////////////
STATE VARIABLES
//////////////////////////////////////////////////////////////////////////*/

/// @inheritdoc ISablierMerkleFactory
uint256 public sablierFee;

/*//////////////////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////////////////*/

/// @dev Emits a {TransferAdmin} event.
/// @param initialAdmin The address of the initial contract admin.
constructor(address initialAdmin) {
admin = initialAdmin;
emit TransferAdmin({ oldAdmin: address(0), newAdmin: initialAdmin });
}

/*//////////////////////////////////////////////////////////////////////////
USER-FACING CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
Expand All @@ -36,11 +59,34 @@ contract SablierMerkleFactory is ISablierMerkleFactory {
return totalPercentage == uUNIT;
}

/*//////////////////////////////////////////////////////////////////////////
ADMIN-FACING NON-CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/

/// @inheritdoc ISablierMerkleFactory
function setSablierFee(uint256 fee) external onlyAdmin {
// Effect: update the Sablier fee.
sablierFee = fee;

emit SetSablierFee(msg.sender, fee);
}

/// @inheritdoc ISablierMerkleFactory
function withdrawFees(address payable to, ISablierMerkleBase merkleLockup) external onlyAdmin {
uint256 feesAccrued = address(merkleLockup).balance;

// Effect: call `withdrawFees` on the MerkleLockup contract.
merkleLockup.withdrawFees(to, feesAccrued);

// Log the withdrawal.
emit WithdrawSablierFees(msg.sender, to, feesAccrued);
}

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

/// @notice inheritdoc ISablierMerkleFactory
/// @inheritdoc ISablierMerkleFactory
function createMerkleInstant(
MerkleBase.ConstructorParams memory baseParams,
uint256 aggregateAmount,
Expand All @@ -63,13 +109,13 @@ contract SablierMerkleFactory is ISablierMerkleFactory {
);

// Deploy the MerkleInstant contract with CREATE2.
merkleInstant = new SablierMerkleInstant{ salt: salt }(baseParams);
merkleInstant = new SablierMerkleInstant{ salt: salt }(baseParams, sablierFee);

// Log the creation of the MerkleInstant contract, including some metadata that is not stored on-chain.
emit CreateMerkleInstant(merkleInstant, baseParams, aggregateAmount, recipientCount);
}

/// @notice inheritdoc ISablierMerkleFactory
/// @inheritdoc ISablierMerkleFactory
function createMerkleLL(
MerkleBase.ConstructorParams memory baseParams,
ISablierLockupLinear lockupLinear,
Expand Down Expand Up @@ -100,15 +146,16 @@ contract SablierMerkleFactory is ISablierMerkleFactory {
);

// Deploy the MerkleLL contract with CREATE2.
merkleLL = new SablierMerkleLL{ salt: salt }(baseParams, lockupLinear, cancelable, transferable, schedule);
merkleLL =
new SablierMerkleLL{ salt: salt }(baseParams, lockupLinear, cancelable, transferable, schedule, sablierFee);

// Log the creation of the MerkleLL contract, including some metadata that is not stored on-chain.
emit CreateMerkleLL(
merkleLL, baseParams, lockupLinear, cancelable, transferable, schedule, aggregateAmount, recipientCount
);
}

/// @notice inheritdoc ISablierMerkleFactory
/// @inheritdoc ISablierMerkleFactory
function createMerkleLT(
MerkleBase.ConstructorParams memory baseParams,
ISablierLockupTranched lockupTranched,
Expand Down Expand Up @@ -189,7 +236,7 @@ contract SablierMerkleFactory is ISablierMerkleFactory {

// Deploy the MerkleLT contract with CREATE2.
merkleLT = new SablierMerkleLT{ salt: salt }(
baseParams, lockupTranched, cancelable, transferable, streamStartTime, tranchesWithPercentages
baseParams, lockupTranched, cancelable, transferable, streamStartTime, tranchesWithPercentages, sablierFee
);
}
}
7 changes: 6 additions & 1 deletion src/periphery/SablierMerkleInstant.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ contract SablierMerkleInstant is
//////////////////////////////////////////////////////////////////////////*/

/// @dev Constructs the contract by initializing the immutable state variables.
constructor(MerkleBase.ConstructorParams memory baseParams) SablierMerkleBase(baseParams) { }
constructor(
MerkleBase.ConstructorParams memory baseParams,
uint256 sablierFee
)
SablierMerkleBase(baseParams, sablierFee)
{ }

/*//////////////////////////////////////////////////////////////////////////
INTERNAL NON-CONSTANT FUNCTIONS
Expand Down
5 changes: 3 additions & 2 deletions src/periphery/SablierMerkleLL.sol
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,10 @@ contract SablierMerkleLL is
ISablierLockupLinear lockupLinear,
bool cancelable,
bool transferable,
MerkleLL.Schedule memory schedule_
MerkleLL.Schedule memory schedule_,
uint256 sablierFee
)
SablierMerkleBase(baseParams)
SablierMerkleBase(baseParams, sablierFee)
{
CANCELABLE = cancelable;
LOCKUP_LINEAR = lockupLinear;
Expand Down
5 changes: 3 additions & 2 deletions src/periphery/SablierMerkleLT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,10 @@ contract SablierMerkleLT is
bool cancelable,
bool transferable,
uint40 streamStartTime,
MerkleLT.TrancheWithPercentage[] memory tranchesWithPercentages
MerkleLT.TrancheWithPercentage[] memory tranchesWithPercentages,
uint256 sablierFee
)
SablierMerkleBase(baseParams)
SablierMerkleBase(baseParams, sablierFee)
{
CANCELABLE = cancelable;
LOCKUP_TRANCHED = lockupTranched;
Expand Down
32 changes: 31 additions & 1 deletion src/periphery/abstracts/SablierMerkleBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,18 @@ abstract contract SablierMerkleBase is
/// @inheritdoc ISablierMerkleBase
uint40 public immutable override EXPIRATION;

/// @inheritdoc ISablierMerkleBase
address public immutable FACTORY;

/// @inheritdoc ISablierMerkleBase
bytes32 public immutable override MERKLE_ROOT;

/// @dev The name of the campaign stored as bytes32.
bytes32 internal immutable NAME;

/// @inheritdoc ISablierMerkleBase
uint256 public immutable SABLIER_FEE;

/// @inheritdoc ISablierMerkleBase
string public ipfsCID;

Expand All @@ -51,7 +57,7 @@ abstract contract SablierMerkleBase is
//////////////////////////////////////////////////////////////////////////*/

/// @dev Constructs the contract by initializing the immutable state variables.
constructor(MerkleBase.ConstructorParams memory params) {
constructor(MerkleBase.ConstructorParams memory params, uint256 sablierFee) {
// Check: the campaign name is not greater than 32 bytes
if (bytes(params.name).length > 32) {
revert Errors.SablierMerkleBase_CampaignNameTooLong({ nameLength: bytes(params.name).length, maxLength: 32 });
Expand All @@ -63,6 +69,8 @@ abstract contract SablierMerkleBase is
ipfsCID = params.ipfsCID;
MERKLE_ROOT = params.merkleRoot;
NAME = bytes32(abi.encodePacked(params.name));
FACTORY = msg.sender;
SABLIER_FEE = sablierFee;
}

/*//////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -101,13 +109,19 @@ abstract contract SablierMerkleBase is
bytes32[] calldata merkleProof
)
external
payable
override
{
// Check: the campaign has not expired.
if (hasExpired()) {
revert Errors.SablierMerkleBase_CampaignExpired({ blockTimestamp: block.timestamp, expiration: EXPIRATION });
}

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

// Check: the index has not been claimed.
if (_claimedBitMap.get(index)) {
revert Errors.SablierMerkleBase_StreamClaimed(index);
Expand Down Expand Up @@ -152,6 +166,22 @@ abstract contract SablierMerkleBase is
emit Clawback(admin, to, amount);
}

/// @inheritdoc ISablierMerkleBase
function withdrawFees(address payable to, uint256 feeAmount) external {
// Check: the caller is the factory.
if (msg.sender != FACTORY) {
revert Errors.SablierMerkleBase_CallerNotFactory(FACTORY, msg.sender);
}

// Effect: transfer the fees to the provided address.
(bool success,) = to.call{ value: feeAmount }("");

// Revert if the call failed.
if (!success) {
revert Errors.SablierMerkleBase_FeeWithdrawFailed(to, feeAmount);
}
}

/*//////////////////////////////////////////////////////////////////////////
INTERNAL CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
Expand Down
21 changes: 20 additions & 1 deletion src/periphery/interfaces/ISablierMerkleBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,16 @@ interface ISablierMerkleBase is IAdminable {
/// @dev This is an immutable state variable.
function EXPIRATION() external returns (uint40);

/// @notice Retrieves the address of the factory contract.
function FACTORY() external view returns (address);

/// @notice The root of the Merkle tree used to validate the proofs of inclusion.
/// @dev This is an immutable state variable.
function MERKLE_ROOT() external returns (bytes32);

/// @notice Retrieves the minimum fee required to claim Airstream, paid in ETH.
function SABLIER_FEE() external view returns (uint256);

/// @notice Returns the timestamp when the first claim is made.
function getFirstClaimTime() external view returns (uint40);

Expand Down Expand Up @@ -61,12 +67,13 @@ interface ISablierMerkleBase is IAdminable {
/// - The campaign must not have expired.
/// - The stream must not have been claimed already.
/// - The Merkle proof must be valid.
/// - The `msg.value` must not be less than `SABLIER_FEE`.
///
/// @param index The index of the recipient in the Merkle tree.
/// @param recipient The address of the airdrop recipient.
/// @param amount The amount of ERC-20 assets to be transferred to the recipient.
/// @param merkleProof The proof of inclusion in the Merkle tree.
function claim(uint256 index, address recipient, uint128 amount, bytes32[] calldata merkleProof) external;
function claim(uint256 index, address recipient, uint128 amount, bytes32[] calldata merkleProof) external payable;

/// @notice Claws back the unclaimed tokens from the campaign.
///
Expand All @@ -81,4 +88,16 @@ interface ISablierMerkleBase is IAdminable {
/// @param to The address to receive the tokens.
/// @param amount The amount of tokens to claw back.
function clawback(address to, uint128 amount) external;

/// @notice Withdraws the Sablier fees accrued to the provided address.
///
/// @dev This function transfers ETH to the provided address. If the receiver is a contract, it must be able to
/// receive ETH.
///
/// Requirements:
/// - The caller must be the Factory contract.
///
/// @param to The address to receive the Sablier fees.
/// @param feeAmount The fee amount to withdraw from the contract.
function withdrawFees(address payable to, uint256 feeAmount) external;
}
39 changes: 38 additions & 1 deletion src/periphery/interfaces/ISablierMerkleFactory.sol
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.8.22;

import { IAdminable } from "../../core/interfaces/IAdminable.sol";
import { ISablierLockupLinear } from "../../core/interfaces/ISablierLockupLinear.sol";
import { ISablierLockupTranched } from "../../core/interfaces/ISablierLockupTranched.sol";

import { ISablierMerkleBase } from "../interfaces/ISablierMerkleBase.sol";
import { MerkleBase, MerkleLL, MerkleLT } from "../types/DataTypes.sol";
import { ISablierMerkleInstant } from "./ISablierMerkleInstant.sol";
import { ISablierMerkleLL } from "./ISablierMerkleLL.sol";
Expand All @@ -16,7 +18,7 @@ import { ISablierMerkleLT } from "./ISablierMerkleLT.sol";
/// enables instant airdrops where tokens are unlocked and distributed immediately. See the Sablier docs for more
/// guidance: https://docs.sablier.com
/// @dev Deploys Merkle Lockup and Merkle Instant campaigns with CREATE2.
interface ISablierMerkleFactory {
interface ISablierMerkleFactory is IAdminable {
/*//////////////////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -55,6 +57,12 @@ interface ISablierMerkleFactory {
uint256 recipientCount
);

/// @notice Emitted when the Sablier fee is set by the admin.
event SetSablierFee(address indexed admin, uint256 sablierFee);

/// @notice Emitted when the sablier fees are claimed by the sablier admin.
event WithdrawSablierFees(address indexed admin, address indexed to, uint256 sablierFees);

/*//////////////////////////////////////////////////////////////////////////
CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
Expand All @@ -68,6 +76,10 @@ interface ISablierMerkleFactory {
pure
returns (bool result);

/// @notice Retrieves the sablier fee required to claim an airstream.
/// @dev A minimum of this fee must be paid in ETH during `claim`.
function sablierFee() external view returns (uint256);

/*//////////////////////////////////////////////////////////////////////////
NON-CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -135,4 +147,29 @@ interface ISablierMerkleFactory {
)
external
returns (ISablierMerkleLT merkleLT);

/// @notice Sets the Sablier fee for claiming an airstream.
/// @dev Emits a {SetSablierFee} event.
///
/// Notes:
/// - The new fee will only be applied to the future campaigns.
///
/// Requiurements:
/// - The caller must be the admin.
///
/// @param fee The new fee to be set.
function setSablierFee(uint256 fee) external;

/// @notice Withdraws the Sablier fees accrued on `merkleLockup` to the provided address.
/// @dev Emits a {WithdrawSablierFees} event.
///
/// Notes:
/// - This function transfers ETH to the provided address. If the receiver is a contract, it must be able to receive
/// ETH.
///
/// Requirements:
/// - The caller must be the admin.
///
/// @param to The address to receive the Sablier fees.
function withdrawFees(address payable to, ISablierMerkleBase merkleLockup) external;
}
9 changes: 9 additions & 0 deletions src/periphery/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ library Errors {
SABLIER-MERKLE-BASE
//////////////////////////////////////////////////////////////////////////*/

/// @notice Thrown when caller is not the factory contract.
error SablierMerkleBase_CallerNotFactory(address factory, address caller);

/// @notice Thrown when trying to claim after the campaign has expired.
error SablierMerkleBase_CampaignExpired(uint256 blockTimestamp, uint40 expiration);

Expand All @@ -24,6 +27,12 @@ library Errors {
/// not expired.
error SablierMerkleBase_ClawbackNotAllowed(uint256 blockTimestamp, uint40 expiration, uint40 firstClaimTime);

/// @notice Thrown if the Sablier fees withdraw failed.
error SablierMerkleBase_FeeWithdrawFailed(address to, uint256 amount);

/// @notice Thrown when trying to claim with an insufficient fee payment.
error SablierMerkleBase_InsufficientFeePayment(uint256 feePaid, uint256 sablierFee);

/// @notice Thrown when trying to claim with an invalid Merkle proof.
error SablierMerkleBase_InvalidProof();

Expand Down

0 comments on commit 945477a

Please sign in to comment.