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: new campaign to facilitate instant distribution of assets #380

Closed
wants to merge 1 commit 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
58 changes: 58 additions & 0 deletions src/SablierMerkleInstant.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.8.22;

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

import { SablierV2MerkleLockup } from "./abstracts/SablierV2MerkleLockup.sol";
import { ISablierMerkleInstant } from "./interfaces/ISablierMerkleInstant.sol";
import { MerkleLockup } from "./types/DataTypes.sol";

/// @title SablierMerkleInstant
/// @notice See the documentation in {ISablierMerkleInstant}.
contract SablierMerkleInstant is
ISablierMerkleInstant, // 2 inherited components
SablierV2MerkleLockup // 4 inherited components
{
using BitMaps for BitMaps.BitMap;
using SafeERC20 for IERC20;

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

/// @dev Constructs the contract by initializing the immutable state variables.
constructor(MerkleLockup.ConstructorParams memory baseParams) SablierV2MerkleLockup(baseParams) { }

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

/// @inheritdoc ISablierMerkleInstant
function claim(
uint256 index,
address recipient,
uint128 amount,
bytes32[] calldata merkleProof
)
external
override
{
// Generate the Merkle tree leaf by hashing the corresponding parameters. Hashing twice prevents second
// preimage attacks.
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(index, recipient, amount))));

// Check: validate the function.
_checkClaim(index, leaf, merkleProof);

// Effect: mark the index as claimed.
_claimedBitMap.set(index);

// Interaction: withdraw the assets to the recipient.
ASSET.safeTransfer(recipient, amount);

// Log the claim.
emit Claim(index, recipient, amount);
}
}
8 changes: 8 additions & 0 deletions src/SablierV2MerkleLL.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,18 @@ contract SablierV2MerkleLL is
STATE VARIABLES
//////////////////////////////////////////////////////////////////////////*/

/// @inheritdoc ISablierV2MerkleLL
bool public immutable override CANCELABLE;

/// @inheritdoc ISablierV2MerkleLL
ISablierV2LockupLinear public immutable override LOCKUP_LINEAR;

/// @inheritdoc ISablierV2MerkleLL
LockupLinear.Durations public override streamDurations;

/// @inheritdoc ISablierV2MerkleLL
bool public immutable override TRANSFERABLE;

/*//////////////////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////////////////*/
Expand All @@ -44,8 +50,10 @@ contract SablierV2MerkleLL is
)
SablierV2MerkleLockup(baseParams)
{
CANCELABLE = baseParams.cancelable;
LOCKUP_LINEAR = lockupLinear;
streamDurations = streamDurations_;
TRANSFERABLE = baseParams.transferable;

// Max approve the Sablier contract to spend funds from the MerkleLockup contract.
ASSET.forceApprove(address(LOCKUP_LINEAR), type(uint256).max);
Expand Down
9 changes: 9 additions & 0 deletions src/SablierV2MerkleLT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,18 @@ contract SablierV2MerkleLT is
STATE VARIABLES
//////////////////////////////////////////////////////////////////////////*/

/// @inheritdoc ISablierV2MerkleLT
bool public immutable override CANCELABLE;

/// @inheritdoc ISablierV2MerkleLT
ISablierV2LockupTranched public immutable override LOCKUP_TRANCHED;

/// @inheritdoc ISablierV2MerkleLT
uint64 public immutable override TOTAL_PERCENTAGE;

/// @inheritdoc ISablierV2MerkleLT
bool public immutable override TRANSFERABLE;

/// @dev The tranches with their respective unlock percentages and durations.
MerkleLT.TrancheWithPercentage[] internal _tranchesWithPercentages;

Expand All @@ -49,6 +55,7 @@ contract SablierV2MerkleLT is
)
SablierV2MerkleLockup(baseParams)
{
CANCELABLE = baseParams.cancelable;
LOCKUP_TRANCHED = lockupTranched;

uint256 count = tranchesWithPercentages.length;
Expand All @@ -62,6 +69,8 @@ contract SablierV2MerkleLT is
}
TOTAL_PERCENTAGE = totalPercentage;

TRANSFERABLE = baseParams.transferable;

// Max approve the Sablier contract to spend funds from the MerkleLockup contract.
ASSET.forceApprove(address(LOCKUP_TRANCHED), type(uint256).max);
}
Expand Down
31 changes: 31 additions & 0 deletions src/SablierV2MerkleLockupFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablier
import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol";
import { LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol";

import { ISablierMerkleInstant } from "./interfaces/ISablierMerkleInstant.sol";
import { ISablierV2MerkleLL } from "./interfaces/ISablierV2MerkleLL.sol";
import { ISablierV2MerkleLockupFactory } from "./interfaces/ISablierV2MerkleLockupFactory.sol";
import { ISablierV2MerkleLT } from "./interfaces/ISablierV2MerkleLT.sol";
import { SablierMerkleInstant } from "./SablierMerkleInstant.sol";
import { SablierV2MerkleLL } from "./SablierV2MerkleLL.sol";
import { SablierV2MerkleLT } from "./SablierV2MerkleLT.sol";
import { MerkleLockup, MerkleLT } from "./types/DataTypes.sol";
Expand Down Expand Up @@ -38,6 +40,35 @@ contract SablierV2MerkleLockupFactory is ISablierV2MerkleLockupFactory {
USER-FACING NON-CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/

/// @notice inheritdoc ISablierV2MerkleLockupFactory
function createMerkleInstant(
MerkleLockup.ConstructorParams memory baseParams,
uint256 aggregateAmount,
uint256 recipientCount
)
external
returns (ISablierMerkleInstant merkleInstant)
{
// Hash the parameters to generate a salt.
bytes32 salt = keccak256(
abi.encodePacked(
msg.sender,
baseParams.asset,
baseParams.expiration,
baseParams.initialAdmin,
abi.encode(baseParams.ipfsCID),
baseParams.merkleRoot,
bytes32(abi.encodePacked(baseParams.name))
)
);

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

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

/// @notice inheritdoc ISablierV2MerkleLockupFactory
function createMerkleLL(
MerkleLockup.ConstructorParams memory baseParams,
Expand Down
8 changes: 0 additions & 8 deletions src/abstracts/SablierV2MerkleLockup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@ abstract contract SablierV2MerkleLockup is
/// @inheritdoc ISablierV2MerkleLockup
IERC20 public immutable override ASSET;

/// @inheritdoc ISablierV2MerkleLockup
bool public immutable override CANCELABLE;

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

Expand All @@ -39,9 +36,6 @@ abstract contract SablierV2MerkleLockup is
/// @dev The name of the campaign stored as bytes32.
bytes32 internal immutable NAME;

/// @inheritdoc ISablierV2MerkleLockup
bool public immutable override TRANSFERABLE;

/// @inheritdoc ISablierV2MerkleLockup
string public ipfsCID;

Expand All @@ -67,12 +61,10 @@ abstract contract SablierV2MerkleLockup is

admin = params.initialAdmin;
ASSET = params.asset;
CANCELABLE = params.cancelable;
EXPIRATION = params.expiration;
ipfsCID = params.ipfsCID;
MERKLE_ROOT = params.merkleRoot;
NAME = bytes32(abi.encodePacked(params.name));
TRANSFERABLE = params.transferable;
}

/*//////////////////////////////////////////////////////////////////////////
Expand Down
34 changes: 34 additions & 0 deletions src/interfaces/ISablierMerkleInstant.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.8.22;

import { ISablierV2MerkleLockup } from "./ISablierV2MerkleLockup.sol";

/// @title ISablierMerkleInstant
/// @notice MerkleLockup campaign that facilitates instant distribution of assets.
interface ISablierMerkleInstant is ISablierV2MerkleLockup {
/*//////////////////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////////////////*/

/// @notice Emitted when a recipient claims an instant airdrop.
event Claim(uint256 index, address indexed recipient, uint128 amount);

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

/// @notice Makes the claim and transfer assets to the recipient.
///
/// @dev Emits a {Claim} event.
///
/// Requirements:
/// - The campaign must not have expired.
/// - The stream must not have been claimed already.
/// - The Merkle proof must be valid.
///
/// @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;
}
15 changes: 15 additions & 0 deletions src/interfaces/ISablierV2MerkleLL.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,31 @@ import { ISablierV2MerkleLockup } from "./ISablierV2MerkleLockup.sol";
/// @title ISablierV2MerkleLL
/// @notice MerkleLockup campaign that creates LockupLinear streams.
interface ISablierV2MerkleLL is ISablierV2MerkleLockup {
/*//////////////////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////////////////*/

/// @notice Emitted when a recipient claims a stream.
event Claim(uint256 index, address indexed recipient, uint128 amount, uint256 indexed streamId);

/*//////////////////////////////////////////////////////////////////////////
CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/

/// @notice A flag indicating whether the streams can be canceled.
/// @dev This is an immutable state variable.
function CANCELABLE() external returns (bool);

/// @notice The address of the {SablierV2LockupLinear} contract.
function LOCKUP_LINEAR() external view returns (ISablierV2LockupLinear);

/// @notice The total streaming duration of each stream.
function streamDurations() external view returns (uint40 cliff, uint40 duration);

/// @notice A flag indicating whether the stream NFTs are transferable.
/// @dev This is an immutable state variable.
function TRANSFERABLE() external returns (bool);

/*//////////////////////////////////////////////////////////////////////////
NON-CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
Expand Down
15 changes: 15 additions & 0 deletions src/interfaces/ISablierV2MerkleLT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,21 @@ import { MerkleLT } from "../types/DataTypes.sol";
/// @title ISablierV2MerkleLT
/// @notice MerkleLockup campaign that creates LockupTranched streams.
interface ISablierV2MerkleLT is ISablierV2MerkleLockup {
/*//////////////////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////////////////*/

/// @notice Emitted when a recipient claims a stream.
event Claim(uint256 index, address indexed recipient, uint128 amount, uint256 indexed streamId);

/*//////////////////////////////////////////////////////////////////////////
CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/

/// @notice A flag indicating whether the streams can be canceled.
/// @dev This is an immutable state variable.
function CANCELABLE() external returns (bool);

/// @notice Retrieves the tranches with their respective unlock percentages and durations.
function getTranchesWithPercentages() external view returns (MerkleLT.TrancheWithPercentage[] memory);

Expand All @@ -22,6 +33,10 @@ interface ISablierV2MerkleLT is ISablierV2MerkleLockup {
/// @notice The total percentage of the tranches.
function TOTAL_PERCENTAGE() external view returns (uint64);

/// @notice A flag indicating whether the stream NFTs are transferable.
/// @dev This is an immutable state variable.
function TRANSFERABLE() external returns (bool);

/*//////////////////////////////////////////////////////////////////////////
NON-CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
Expand Down
11 changes: 0 additions & 11 deletions src/interfaces/ISablierV2MerkleLockup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ interface ISablierV2MerkleLockup is IAdminable {
EVENTS
//////////////////////////////////////////////////////////////////////////*/

/// @notice Emitted when a recipient claims a stream.
event Claim(uint256 index, address indexed recipient, uint128 amount, uint256 indexed streamId);

/// @notice Emitted when the admin claws back the unclaimed tokens.
event Clawback(address indexed admin, address indexed to, uint128 amount);

Expand All @@ -28,10 +25,6 @@ interface ISablierV2MerkleLockup is IAdminable {
/// @dev This is an immutable state variable.
function ASSET() external returns (IERC20);

/// @notice A flag indicating whether the streams can be canceled.
/// @dev This is an immutable state variable.
function CANCELABLE() external returns (bool);

/// @notice The cut-off point for the campaign, as a Unix timestamp. A value of zero means there is no expiration.
/// @dev This is an immutable state variable.
function EXPIRATION() external returns (uint40);
Expand All @@ -57,10 +50,6 @@ interface ISablierV2MerkleLockup is IAdminable {
/// @notice Retrieves the name of the campaign.
function name() external returns (string memory);

/// @notice A flag indicating whether the stream NFTs are transferable.
/// @dev This is an immutable state variable.
function TRANSFERABLE() external returns (bool);

/*//////////////////////////////////////////////////////////////////////////
NON-CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
Expand Down
23 changes: 23 additions & 0 deletions src/interfaces/ISablierV2MerkleLockupFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablier
import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol";
import { LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol";

import { ISablierMerkleInstant } from "./ISablierMerkleInstant.sol";
import { ISablierV2MerkleLL } from "./ISablierV2MerkleLL.sol";
import { ISablierV2MerkleLT } from "./ISablierV2MerkleLT.sol";
import { MerkleLockup, MerkleLT } from "../types/DataTypes.sol";
Expand All @@ -16,6 +17,14 @@ interface ISablierV2MerkleLockupFactory {
EVENTS
//////////////////////////////////////////////////////////////////////////*/

/// @notice Emitted when a {SablierV2MerkleInstant} campaign is created.
event CreateMerkleInstant(
ISablierMerkleInstant indexed merkleInstant,
MerkleLockup.ConstructorParams baseParams,
uint256 aggregateAmount,
uint256 recipientCount
);

/// @notice Emitted when a {SablierV2MerkleLL} campaign is created.
event CreateMerkleLL(
ISablierV2MerkleLL indexed merkleLL,
Expand Down Expand Up @@ -54,6 +63,20 @@ interface ISablierV2MerkleLockupFactory {
NON-CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/

/// @notice Creates a new MerkleLockup campaign for instant distribution of assets.
/// @dev Emits a {CreateMerkleInstant} event.
/// @param baseParams Struct encapsulating the {SablierV2MerkleLockup} parameters, which are documented in
/// {DataTypes}.
/// @param aggregateAmount The total amount of ERC-20 assets to be distributed to all recipients.
/// @param recipientCount The total number of recipients who are eligible to claim.
function createMerkleInstant(
MerkleLockup.ConstructorParams memory baseParams,
uint256 aggregateAmount,
uint256 recipientCount
)
external
returns (ISablierMerkleInstant merkleLL);

/// @notice Creates a new MerkleLockup campaign with a LockupLinear distribution.
/// @dev Emits a {CreateMerkleLL} event.
/// @param baseParams Struct encapsulating the {SablierV2MerkleLockup} parameters, which are documented in
Expand Down
6 changes: 4 additions & 2 deletions src/types/DataTypes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,15 @@ library BatchLockup {
library MerkleLockup {
/// @notice Struct encapsulating the base constructor parameters of a MerkleLockup campaign.
/// @param asset The contract address of the ERC-20 asset to be distributed.
/// @param cancelable Indicates if the stream will be cancelable after claiming.
/// @param cancelable Indicates if the stream will be cancelable after claiming. This does not apply to
/// {SablierMerkleInstant}.
/// @param expiration The expiration of the campaign, as a Unix timestamp.
/// @param initialAdmin The initial admin of the MerkleLockup campaign.
/// @param ipfsCID The content identifier for indexing the contract on IPFS.
/// @param merkleRoot The Merkle root of the claim data.
/// @param name The name of the campaign.
/// @param transferable Indicates if the stream will be transferable after claiming.
/// @param transferable Indicates if the stream will be transferable after claiming. This only applies to
/// {SablierMerkleInstant}.
struct ConstructorParams {
IERC20 asset;
bool cancelable;
Expand Down
Loading