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 de15007
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 19 deletions.
77 changes: 68 additions & 9 deletions src/periphery/SablierMerkleFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ 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";

Expand All @@ -17,7 +18,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,6 +58,18 @@ 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);
}

/*//////////////////////////////////////////////////////////////////////////
USER-FACING NON-CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
Expand All @@ -58,12 +92,18 @@ contract SablierMerkleFactory is ISablierMerkleFactory {
baseParams.initialAdmin,
abi.encode(baseParams.ipfsCID),
baseParams.merkleRoot,
bytes32(abi.encodePacked(baseParams.name))
bytes32(abi.encodePacked(baseParams.name)),
admin,
sablierFee
)
);

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

// Log the creation of the MerkleInstant contract, including some metadata that is not stored on-chain.
emit CreateMerkleInstant(merkleInstant, baseParams, aggregateAmount, recipientCount);
Expand Down Expand Up @@ -95,12 +135,22 @@ contract SablierMerkleFactory is ISablierMerkleFactory {
lockupLinear,
cancelable,
transferable,
abi.encode(schedule)
abi.encode(schedule),
admin,
sablierFee
)
);

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

// Log the creation of the MerkleLL contract, including some metadata that is not stored on-chain.
emit CreateMerkleLL(
Expand Down Expand Up @@ -183,13 +233,22 @@ contract SablierMerkleFactory is ISablierMerkleFactory {
cancelable,
transferable,
streamStartTime,
abi.encode(tranchesWithPercentages)
abi.encode(tranchesWithPercentages),
admin,
sablierFee
)
);

// Deploy the MerkleLT contract with CREATE2.
merkleLT = new SablierMerkleLT{ salt: salt }(
baseParams, lockupTranched, cancelable, transferable, streamStartTime, tranchesWithPercentages
);
merkleLT = new SablierMerkleLT{ salt: salt }({
baseParams: baseParams,
lockupTranched: lockupTranched,
cancelable: cancelable,
transferable: transferable,
streamStartTime: streamStartTime,
tranchesWithPercentages: tranchesWithPercentages,
sablierAdmin: admin,
sablierFee: sablierFee
});
}
}
8 changes: 7 additions & 1 deletion src/periphery/SablierMerkleInstant.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ 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,
address sablierAdmin,
uint256 sablierFee
)
SablierMerkleBase(baseParams, sablierAdmin, sablierFee)
{ }

/*//////////////////////////////////////////////////////////////////////////
INTERNAL NON-CONSTANT FUNCTIONS
Expand Down
6 changes: 4 additions & 2 deletions src/periphery/SablierMerkleLL.sol
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,11 @@ contract SablierMerkleLL is
ISablierLockupLinear lockupLinear,
bool cancelable,
bool transferable,
MerkleLL.Schedule memory schedule_
MerkleLL.Schedule memory schedule_,
address sablierAdmin,
uint256 sablierFee
)
SablierMerkleBase(baseParams)
SablierMerkleBase(baseParams, sablierAdmin, sablierFee)
{
CANCELABLE = cancelable;
LOCKUP_LINEAR = lockupLinear;
Expand Down
6 changes: 4 additions & 2 deletions src/periphery/SablierMerkleLT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,11 @@ contract SablierMerkleLT is
bool cancelable,
bool transferable,
uint40 streamStartTime,
MerkleLT.TrancheWithPercentage[] memory tranchesWithPercentages
MerkleLT.TrancheWithPercentage[] memory tranchesWithPercentages,
address sablierAdmin,
uint256 sablierFee
)
SablierMerkleBase(baseParams)
SablierMerkleBase(baseParams, sablierAdmin, sablierFee)
{
CANCELABLE = cancelable;
LOCKUP_TRANCHED = lockupTranched;
Expand Down
42 changes: 40 additions & 2 deletions src/periphery/abstracts/SablierMerkleBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ abstract contract SablierMerkleBase is
/// @dev The name of the campaign stored as bytes32.
bytes32 internal immutable NAME;

/// @inheritdoc ISablierMerkleBase
address public immutable SABLIER;

/// @inheritdoc ISablierMerkleBase
uint256 public immutable SABLIER_FEE;

/// @inheritdoc ISablierMerkleBase
string public ipfsCID;

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

/// @dev Constructs the contract by initializing the immutable state variables.
constructor(MerkleBase.ConstructorParams memory params) {
constructor(MerkleBase.ConstructorParams memory params, address sablierAdmin, 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 });
}

admin = params.initialAdmin;
ASSET = params.asset;
EXPIRATION = params.expiration;
ipfsCID = params.ipfsCID;
MERKLE_ROOT = params.merkleRoot;
NAME = bytes32(abi.encodePacked(params.name));
SABLIER = sablierAdmin;
SABLIER_FEE = sablierFee;
}

/*//////////////////////////////////////////////////////////////////////////
Expand All @@ -89,6 +96,31 @@ abstract contract SablierMerkleBase is
return string(abi.encodePacked(NAME));
}

/*//////////////////////////////////////////////////////////////////////////
SABLIER-FACING NON-CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/

/// @inheritdoc ISablierMerkleBase
function withdrawFees(address payable to) external {
// Check: the caller is the Sablier admin.
if (msg.sender != SABLIER) {
revert Errors.CallerNotSablier(SABLIER, msg.sender);
}

uint256 feesAccrued = address(this).balance;

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

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

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

/*//////////////////////////////////////////////////////////////////////////
USER-FACING NON-CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
Expand All @@ -101,13 +133,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
25 changes: 23 additions & 2 deletions src/periphery/interfaces/ISablierMerkleBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IAdminable } from "../../core/interfaces/IAdminable.sol";

/// @title ISablierMerkleBase
/// @dev This is the base interface for Merkle Lockups and Merkle Instant.
/// @dev This is the base interface for Merklefunction claim Lockups and Merkle Instant.
interface ISablierMerkleBase is IAdminable {
/*//////////////////////////////////////////////////////////////////////////
EVENTS
Expand All @@ -15,6 +15,9 @@ interface ISablierMerkleBase is IAdminable {
/// @notice Emitted when the admin claws back the unclaimed tokens.
event Clawback(address indexed admin, address indexed to, uint128 amount);

/// @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 @@ -31,6 +34,12 @@ interface ISablierMerkleBase is IAdminable {
/// @dev This is an immutable state variable.
function MERKLE_ROOT() external returns (bytes32);

/// @notice Retrieves the address of the Sablier admin.
function SABLIER() external view returns (address);

/// @notice Retrieves the minimum fee required to claim Airstream, paid in native token.
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 +70,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 +91,15 @@ 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 the native token to the provided address. If the reciever is a contract, it must be
/// able to receive native tokens.
///
/// Requirements:
/// - The caller must be the Sablier admin.
///
/// @param to The address to receive the Sablier fees.
function withdrawFees(address payable to) external;
}
22 changes: 21 additions & 1 deletion src/periphery/interfaces/ISablierMerkleFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity >=0.8.22;
import { ISablierLockupLinear } from "../../core/interfaces/ISablierLockupLinear.sol";
import { ISablierLockupTranched } from "../../core/interfaces/ISablierLockupTranched.sol";

import { IAdminable } from "../../core/interfaces/IAdminable.sol";
import { MerkleBase, MerkleLL, MerkleLT } from "../types/DataTypes.sol";
import { ISablierMerkleInstant } from "./ISablierMerkleInstant.sol";
import { ISablierMerkleLL } from "./ISablierMerkleLL.sol";
Expand All @@ -16,7 +17,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 +56,9 @@ interface ISablierMerkleFactory {
uint256 recipientCount
);

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

/*//////////////////////////////////////////////////////////////////////////
CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
Expand All @@ -68,6 +72,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 native token during `claim`.
function sablierFee() external view returns (uint256);

/*//////////////////////////////////////////////////////////////////////////
NON-CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -135,4 +143,16 @@ 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 sablier admin.
///
/// @param fee The new fee to be set.
function setSablierFee(uint256 fee) external;
}
Loading

0 comments on commit de15007

Please sign in to comment.