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

refactor: update repo for the new iteration #43

Merged
merged 11 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from 10 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
2 changes: 1 addition & 1 deletion .solhint.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"rules": {
"code-complexity": ["error", 8],
"compiler-version": ["error", ">=0.8.13"],
"contract-name-camelcase": "off",
"contract-name-capwords": "off",
"func-name-mixedcase": "off",
"func-visibility": ["error", { "ignoreConstructors": true }],
"gas-custom-errors": "off",
Expand Down
127 changes: 127 additions & 0 deletions airdrops/MerkleCreator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.8.22;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { ud2x18 } from "@prb/math/src/UD2x18.sol";
import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol";
import { ISablierMerkleInstant } from "@sablier/airdrops/src/interfaces/ISablierMerkleInstant.sol";
import { ISablierMerkleLL } from "@sablier/airdrops/src/interfaces/ISablierMerkleLL.sol";
import { ISablierMerkleLT } from "@sablier/airdrops/src/interfaces/ISablierMerkleLT.sol";
import { ISablierMerkleFactory } from "@sablier/airdrops/src/interfaces/ISablierMerkleFactory.sol";
import { MerkleBase, MerkleLL, MerkleLT } from "@sablier/airdrops/src/types/DataTypes.sol";

/// @notice Example of how to create Merkle airdrop campaigns.
/// @dev This code is referenced in the docs: https://docs.sablier.com/guides/airdrops/examples/create-campaign
contract MerkleCreator {
// Sepolia addresses
IERC20 public constant DAI = IERC20(0x68194a729C2450ad26072b3D33ADaCbcef39D574);

// See https://docs.sablier.com/guides/lockup/deployments for all deployments
ISablierMerkleFactory public constant FACTORY = ISablierMerkleFactory(0x4ECd5A688b0365e61c1a764E8BF96A7C5dF5d35F);
ISablierLockup public constant LOCKUP = ISablierLockup(0xC2Da366fD67423b500cDF4712BdB41d0995b0794);

function createMerkleInstant() public virtual returns (ISablierMerkleInstant merkleInstant) {
// Declare the constructor parameter of MerkleBase.
MerkleBase.ConstructorParams memory baseParams;

// Set the base parameters.
baseParams.token = DAI;
baseParams.expiration = uint40(block.timestamp + 12 weeks); // The expiration of the campaign
baseParams.initialAdmin = address(0xBeeF); // Admin of the merkle lockup contract
baseParams.ipfsCID = "QmT5NvUtoM5nWFfrQdVrFtvGfKFmG7AHE8P34isapyhCxX"; // IPFS hash of the campaign metadata
baseParams.merkleRoot = 0x4e07408562bedb8b60ce05c1decfe3ad16b722309875f562c03d02d7aaacb123;
baseParams.campaignName = "My First Campaign"; // Unique campaign name
baseParams.shape = "A custom stream shape"; // Stream shape name for visualization in the UI

// The total amount of tokens you want to airdrop to your users.
uint256 aggregateAmount = 100_000_000e18;

// The total number of addresses you want to airdrop your tokens to.
uint256 recipientCount = 10_000;

// Deploy the MerkleInstant campaign contract. The deployed contract will be completely owned by the campaign
// admin. Recipients will interact with the deployed contract to claim their airdrop.
merkleInstant = FACTORY.createMerkleInstant(baseParams, aggregateAmount, recipientCount);
}

function createMerkleLL() public returns (ISablierMerkleLL merkleLL) {
// Declare the constructor parameter of MerkleBase.
MerkleBase.ConstructorParams memory baseParams;

// Set the base parameters.
baseParams.token = DAI;
baseParams.expiration = uint40(block.timestamp + 12 weeks); // The expiration of the campaign
baseParams.initialAdmin = address(0xBeeF); // Admin of the merkle lockup contract
baseParams.ipfsCID = "QmT5NvUtoM5nWFfrQdVrFtvGfKFmG7AHE8P34isapyhCxX"; // IPFS hash of the campaign metadata
baseParams.merkleRoot = 0x4e07408562bedb8b60ce05c1decfe3ad16b722309875f562c03d02d7aaacb123;
baseParams.campaignName = "My First Campaign"; // Unique campaign name
baseParams.shape = "A custom stream shape"; // Stream shape name for visualization in the UI

// The total amount of tokens you want to airdrop to your users.
uint256 aggregateAmount = 100_000_000e18;

// The total number of addresses you want to airdrop your tokens to.
uint256 recipientCount = 10_000;

// Set the schedule of the stream that will be created from this campaign.
MerkleLL.Schedule memory schedule = MerkleLL.Schedule({
startTime: 0, // i.e. block.timestamp
startPercentage: ud2x18(0.01e18),
cliffDuration: 30 days,
cliffPercentage: ud2x18(0.01e18),
totalDuration: 90 days
});

// Deploy the MerkleLL campaign contract. The deployed contract will be completely owned by the campaign admin.
// Recipients will interact with the deployed contract to claim their airdrop.
merkleLL = FACTORY.createMerkleLL({
baseParams: baseParams,
lockup: LOCKUP,
cancelable: false,
transferable: true,
schedule: schedule,
aggregateAmount: aggregateAmount,
recipientCount: recipientCount
});
}

function createMerkleLT() public returns (ISablierMerkleLT merkleLT) {
// Prepare the constructor parameters.
MerkleBase.ConstructorParams memory baseParams;

// Set the base parameters.
baseParams.token = DAI;
baseParams.expiration = uint40(block.timestamp + 12 weeks); // The expiration of the campaign
baseParams.initialAdmin = address(0xBeeF); // Admin of the merkle lockup contract
baseParams.ipfsCID = "QmT5NvUtoM5nWFfrQdVrFtvGfKFmG7AHE8P34isapyhCxX"; // IPFS hash of the campaign metadata
baseParams.merkleRoot = 0x4e07408562bedb8b60ce05c1decfe3ad16b722309875f562c03d02d7aaacb123;
baseParams.campaignName = "My First Campaign"; // Unique campaign name
baseParams.shape = "A custom stream shape"; // Stream shape name for visualization in the UI

// The tranches with their unlock percentages and durations.
MerkleLT.TrancheWithPercentage[] memory tranchesWithPercentages = new MerkleLT.TrancheWithPercentage[](2);
tranchesWithPercentages[0] =
MerkleLT.TrancheWithPercentage({ unlockPercentage: ud2x18(0.5e18), duration: 30 days });
tranchesWithPercentages[1] =
MerkleLT.TrancheWithPercentage({ unlockPercentage: ud2x18(0.5e18), duration: 60 days });

// The total amount of tokens you want to airdrop to your users.
uint256 aggregateAmount = 100_000_000e18;

// The total number of addresses you want to airdrop your tokens to.
uint256 recipientCount = 10_000;

// Deploy the MerkleLT campaign contract. The deployed contract will be completely owned by the campaign admin.
// Recipients will interact with the deployed contract to claim their airdrop.
merkleLT = FACTORY.createMerkleLT({
baseParams: baseParams,
lockup: LOCKUP,
cancelable: true,
transferable: true,
streamStartTime: 0, // i.e. block.timestamp
tranchesWithPercentages: tranchesWithPercentages,
aggregateAmount: aggregateAmount,
recipientCount: recipientCount
});
}
}
66 changes: 66 additions & 0 deletions airdrops/MerkleCreator.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: GPL-3-0-or-later
pragma solidity >=0.8.22;

import { ISablierMerkleInstant } from "@sablier/airdrops/src/interfaces/ISablierMerkleInstant.sol";
import { ISablierMerkleLL } from "@sablier/airdrops/src/interfaces/ISablierMerkleLL.sol";
import { ISablierMerkleLT } from "@sablier/airdrops/src/interfaces/ISablierMerkleLT.sol";
import { Test } from "forge-std/src/Test.sol";

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

contract MerkleCreatorTest is Test {
// Test contract
MerkleCreator internal merkleCreator;

address internal user;

function setUp() public {
// Fork Ethereum Sepolia
vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 7_497_776 });

// Deploy the Merkle creator
merkleCreator = new MerkleCreator();

// Create a test user
user = payable(makeAddr("User"));
vm.deal({ account: user, newBalance: 1 ether });

// Make the test user the `msg.sender` in all following calls
vm.startPrank({ msgSender: user });
}

// Test creating the MerkleInstant campaign.
function test_CreateMerkleInstant() public {
ISablierMerkleInstant merkleInstant = merkleCreator.createMerkleInstant();

// Assert the merkleLL contract was created with correct params
assertEq(address(0xBeeF), merkleInstant.admin(), "admin");
assertEq(
0x4e07408562bedb8b60ce05c1decfe3ad16b722309875f562c03d02d7aaacb123,
merkleInstant.MERKLE_ROOT(),
"merkle-root"
);
}

// Test creating the MerkleLL campaign.
function test_CreateMerkleLL() public {
ISablierMerkleLL merkleLL = merkleCreator.createMerkleLL();

// Assert the merkleLL contract was created with correct params
assertEq(address(0xBeeF), merkleLL.admin(), "admin");
assertEq(
0x4e07408562bedb8b60ce05c1decfe3ad16b722309875f562c03d02d7aaacb123, merkleLL.MERKLE_ROOT(), "merkle-root"
);
}

// Test creating the MerkleLT campaign.
function test_CreateMerkleLT() public {
ISablierMerkleLT merkleLT = merkleCreator.createMerkleLT();

// Assert the merkleLT contract was created with correct params
assertEq(address(0xBeeF), merkleLT.admin(), "admin");
assertEq(
0x4e07408562bedb8b60ce05c1decfe3ad16b722309875f562c03d02d7aaacb123, merkleLT.MERKLE_ROOT(), "merkle-root"
);
}
}
Binary file modified bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion flow/FlowBatchable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Broker, ISablierFlow } from "@sablier/flow/src/interfaces/ISablierFlow.
contract FlowBatchable {
// Sepolia addresses
IERC20 public constant USDC = IERC20(0xf08A50178dfcDe18524640EA6618a1f965821715);
ISablierFlow public constant FLOW = ISablierFlow(0x5Ae8c13f6Ae094887322012425b34b0919097d8A);
ISablierFlow public constant FLOW = ISablierFlow(0x52ab22e769E31564E17D524b683264B65662A014);

/// @dev A function to adjust the rate per second and deposit into a stream in a single transaction.
/// Note: The streamId's sender must be this contract, otherwise, the call will fail due to no authorization.
Expand Down
2 changes: 1 addition & 1 deletion flow/FlowBatchable.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ contract FlowBatchable_Test is Test {

function setUp() external {
// Fork Ethereum Sepolia
vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 7_250_564 });
vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 7_497_776 });

// Deploy the batchable contract
batchable = new FlowBatchable();
Expand Down
6 changes: 3 additions & 3 deletions flow/FlowStreamCreator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { FlowUtilities } from "./FlowUtilities.sol";
contract FlowStreamCreator {
// Sepolia addresses
IERC20 public constant USDC = IERC20(0xf08A50178dfcDe18524640EA6618a1f965821715);
ISablierFlow public constant FLOW = ISablierFlow(0x5Ae8c13f6Ae094887322012425b34b0919097d8A);
ISablierFlow public constant FLOW = ISablierFlow(0x52ab22e769E31564E17D524b683264B65662A014);

// Create a stream that sends 1000 USDC per month.
function createStream_1K_PerMonth() external returns (uint256 streamId) {
Expand All @@ -19,7 +19,7 @@ contract FlowStreamCreator {

streamId = FLOW.create({
sender: msg.sender, // The sender will be able to manage the stream
recipient: address(0xCAFE), // The recipient of the streamed assets
recipient: address(0xCAFE), // The recipient of the streamed tokens
ratePerSecond: ratePerSecond, // The rate per second equivalent to 1000 USDC per month
token: USDC, // The token to be streamed
transferable: true // Whether the stream will be transferable or not
Expand All @@ -33,7 +33,7 @@ contract FlowStreamCreator {

streamId = FLOW.create({
sender: msg.sender, // The sender will be able to manage the stream
recipient: address(0xCAFE), // The recipient of the streamed assets
recipient: address(0xCAFE), // The recipient of the streamed tokens
ratePerSecond: ratePerSecond, // The rate per second equivalent to 1,000,00 USDC per year
token: USDC, // The token to be streamed
transferable: true // Whether the stream will be transferable or not
Expand Down
2 changes: 1 addition & 1 deletion flow/FlowStreamCreator.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ contract FlowStreamCreator_Test is Test {

function setUp() external {
// Fork Ethereum Sepolia
vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 7_250_564 });
vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 7_497_776 });

// Deploy the FlowStreamCreator contract
streamCreator = new FlowStreamCreator();
Expand Down
2 changes: 1 addition & 1 deletion flow/FlowStreamManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Broker, ISablierFlow } from "@sablier/flow/src/interfaces/ISablierFlow.

contract FlowStreamManager {
// Sepolia address
ISablierFlow public constant FLOW = ISablierFlow(0x5Ae8c13f6Ae094887322012425b34b0919097d8A);
ISablierFlow public constant FLOW = ISablierFlow(0x52ab22e769E31564E17D524b683264B65662A014);

function adjustRatePerSecond(uint256 streamId) external {
FLOW.adjustRatePerSecond({ streamId: streamId, newRatePerSecond: ud21x18(0.0001e18) });
Expand Down
10 changes: 7 additions & 3 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,18 @@ optimizer_runs = 5000
out = "out"
solc = "0.8.26"

[profile.lockup]
src = "lockup"
test = "lockup"
[profile.airdrops]
src = "airdrops"
test = "airdrops"

[profile.flow]
src = "flow"
test = "flow"

[profile.lockup]
src = "lockup"
test = "lockup"

[fmt]
bracket_spacing = true
int_types = "long"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,23 @@ pragma solidity >=0.8.22;
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { ud2x18 } from "@prb/math/src/UD2x18.sol";
import { ud60x18 } from "@prb/math/src/UD60x18.sol";
import { ISablierV2LockupDynamic } from "@sablier/v2-core/src/interfaces/ISablierV2LockupDynamic.sol";
import { Broker, LockupDynamic } from "@sablier/v2-core/src/types/DataTypes.sol";
import { ISablierV2BatchLockup } from "@sablier/v2-periphery/src/interfaces/ISablierV2BatchLockup.sol";
import { BatchLockup } from "@sablier/v2-periphery/src/types/DataTypes.sol";
import { ISablierBatchLockup } from "@sablier/lockup/src/interfaces/ISablierBatchLockup.sol";
import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol";
import { BatchLockup, Broker, LockupDynamic } from "@sablier/lockup/src/types/DataTypes.sol";

contract BatchLDStreamCreator {
// Sepolia addresses
IERC20 public constant DAI = IERC20(0x68194a729C2450ad26072b3D33ADaCbcef39D574);
// See https://docs.sablier.com/contracts/v2/deployments for all deployments
ISablierV2LockupDynamic public constant LOCKUP_DYNAMIC =
ISablierV2LockupDynamic(0x73BB6dD3f5828d60F8b3dBc8798EB10fbA2c5636);
ISablierV2BatchLockup public constant BATCH_LOCKUP =
ISablierV2BatchLockup(0x04A9c14b7a000640419aD5515Db4eF4172C00E31);
// See https://docs.sablier.com/guides/lockup/deployments for all deployments
ISablierLockup public constant LOCKUP = ISablierLockup(0xC2Da366fD67423b500cDF4712BdB41d0995b0794);
ISablierBatchLockup public constant BATCH_LOCKUP = ISablierBatchLockup(0xd4294579236eE290668c8FdaE9403c4F00D914f0);

/// @dev For this function to work, the sender must have approved this dummy contract to spend DAI.
function batchCreateStreams(uint128 perStreamAmount) public returns (uint256[] memory streamIds) {
// Create a batch of two streams
uint256 batchSize = 2;

// Calculate the combined amount of DAI assets to transfer to this contract
// Calculate the combined amount of DAI tokens to transfer to this contract
uint256 transferAmount = perStreamAmount * batchSize;

// Transfer the provided amount of DAI tokens to this contract
Expand All @@ -34,8 +31,8 @@ contract BatchLDStreamCreator {

// Declare the first stream in the batch
BatchLockup.CreateWithTimestampsLD memory stream0;
stream0.sender = address(0xABCD); // The sender to stream the assets, he will be able to cancel the stream
stream0.recipient = address(0xCAFE); // The recipient of the streamed assets
stream0.sender = address(0xABCD); // The sender to stream the tokens, he will be able to cancel the stream
stream0.recipient = address(0xCAFE); // The recipient of the streamed tokens
stream0.totalAmount = perStreamAmount; // The total amount of each stream, inclusive of all fees
stream0.cancelable = true; // Whether the stream will be cancelable or not
stream0.transferable = false; // Whether the recipient can transfer the NFT or not
Expand All @@ -59,8 +56,8 @@ contract BatchLDStreamCreator {

// Declare the second stream in the batch
BatchLockup.CreateWithTimestampsLD memory stream1;
stream1.sender = address(0xABCD); // The sender to stream the assets, he will be able to cancel the stream
stream1.recipient = address(0xBEEF); // The recipient of the streamed assets
stream1.sender = address(0xABCD); // The sender to stream the tokens, he will be able to cancel the stream
stream1.recipient = address(0xBEEF); // The recipient of the streamed tokens
stream1.totalAmount = perStreamAmount; // The total amount of each stream, inclusive of all fees
stream1.cancelable = false; // Whether the stream will be cancelable or not
stream1.transferable = false; // Whether the recipient can transfer the NFT or not
Expand All @@ -87,6 +84,6 @@ contract BatchLDStreamCreator {
batch[0] = stream0;
batch[1] = stream1;

streamIds = BATCH_LOCKUP.createWithTimestampsLD(LOCKUP_DYNAMIC, DAI, batch);
streamIds = BATCH_LOCKUP.createWithTimestampsLD(LOCKUP, DAI, batch);
}
}
Loading