From fadf9b0801afd124752709c6672b6c09532ed59e Mon Sep 17 00:00:00 2001 From: andreivladbrg Date: Wed, 15 Jan 2025 19:14:55 +0200 Subject: [PATCH 01/11] feat: add airdrops dir refactor: restructure lockup dir refactor: update lockup to singleton version refactor: use latest deployments addresses build: install staging version in for all repos test: update the block number forked --- airdrops/AirstreamCreator.sol | 57 ++++++++ .../AirstreamCreator.t.sol | 6 +- bun.lockb | Bin 42986 -> 40444 bytes flow/FlowBatchable.sol | 2 +- flow/FlowBatchable.t.sol | 2 +- flow/FlowStreamCreator.sol | 6 +- flow/FlowStreamCreator.t.sol | 2 +- flow/FlowStreamManager.sol | 2 +- foundry.toml | 10 +- .../{periphery => }/BatchLDStreamCreator.sol | 25 ++-- .../{periphery => }/BatchLLStreamCreator.sol | 30 ++-- .../{periphery => }/BatchLTStreamCreator.sol | 25 ++-- .../{periphery => }/BatchStreamCreator.t.sol | 8 +- .../{core => }/LockupDynamicCurvesCreator.sol | 130 ++++++++---------- .../LockupDynamicCurvesCreator.t.sol | 47 +++---- .../{core => }/LockupDynamicStreamCreator.sol | 26 ++-- .../{core => }/LockupLinearStreamCreator.sol | 25 ++-- lockup/{core => }/LockupStreamCreator.t.sol | 8 +- .../LockupTranchedStreamCreator.sol | 23 ++-- lockup/{core => }/RecipientHooks.sol | 2 +- lockup/{core => }/StakeSablierNFT.sol | 23 ++-- lockup/{core => }/StreamManagement.sol | 6 +- .../{core => }/StreamManagementWithHook.sol | 27 ++-- .../{core => }/StreamManagementWithHook.t.sol | 20 +-- lockup/periphery/AirstreamCreator.sol | 53 ------- .../StakeSablierNFT.t.sol | 43 +++--- .../claim-rewards/claimRewards.t.sol | 0 .../claim-rewards/claimRewards.tree | 0 .../stake-sablier-nft-test/stake/stake.t.sol | 0 .../stake-sablier-nft-test/stake/stake.tree | 0 .../unstake/unstake.t.sol | 2 +- .../unstake/unstake.tree | 0 package.json | 12 +- remappings.txt | 4 +- 34 files changed, 314 insertions(+), 312 deletions(-) create mode 100644 airdrops/AirstreamCreator.sol rename {lockup/periphery => airdrops}/AirstreamCreator.t.sol (86%) rename lockup/{periphery => }/BatchLDStreamCreator.sol (80%) rename lockup/{periphery => }/BatchLLStreamCreator.sol (72%) rename lockup/{periphery => }/BatchLTStreamCreator.sol (79%) rename lockup/{periphery => }/BatchStreamCreator.t.sol (93%) rename lockup/{core => }/LockupDynamicCurvesCreator.sol (71%) rename lockup/{core => }/LockupDynamicCurvesCreator.t.sol (78%) rename lockup/{core => }/LockupDynamicStreamCreator.sol (71%) rename lockup/{core => }/LockupLinearStreamCreator.sol (68%) rename lockup/{core => }/LockupStreamCreator.t.sol (91%) rename lockup/{core => }/LockupTranchedStreamCreator.sol (65%) rename lockup/{core => }/RecipientHooks.sol (96%) rename lockup/{core => }/StakeSablierNFT.sol (95%) rename lockup/{core => }/StreamManagement.sol (94%) rename lockup/{core => }/StreamManagementWithHook.sol (88%) rename lockup/{core => }/StreamManagementWithHook.t.sol (85%) delete mode 100644 lockup/periphery/AirstreamCreator.sol rename lockup/{core => }/tests/stake-sablier-nft-test/StakeSablierNFT.t.sol (80%) rename lockup/{core => }/tests/stake-sablier-nft-test/claim-rewards/claimRewards.t.sol (100%) rename lockup/{core => }/tests/stake-sablier-nft-test/claim-rewards/claimRewards.tree (100%) rename lockup/{core => }/tests/stake-sablier-nft-test/stake/stake.t.sol (100%) rename lockup/{core => }/tests/stake-sablier-nft-test/stake/stake.tree (100%) rename lockup/{core => }/tests/stake-sablier-nft-test/unstake/unstake.t.sol (96%) rename lockup/{core => }/tests/stake-sablier-nft-test/unstake/unstake.tree (100%) diff --git a/airdrops/AirstreamCreator.sol b/airdrops/AirstreamCreator.sol new file mode 100644 index 0000000..0314bfb --- /dev/null +++ b/airdrops/AirstreamCreator.sol @@ -0,0 +1,57 @@ +// 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 { ISablierMerkleLL } from "@sablier/airdrops/src/interfaces/ISablierMerkleLL.sol"; +import { ISablierMerkleFactory } from "@sablier/airdrops/src/interfaces/ISablierMerkleFactory.sol"; +import { MerkleBase, MerkleLL } from "@sablier/airdrops/src/types/DataTypes.sol"; + +/// @notice Example of how to create an Airstream campaign with Lockup Linear. +/// @dev This code is referenced in the docs: https://docs.sablier.com/contracts/v2/guides/create-airstream +contract AirstreamCreator { + // Sepolia addresses + IERC20 public constant DAI = IERC20(0x68194a729C2450ad26072b3D33ADaCbcef39D574); + // See https://docs.sablier.com/contracts/v2/deployments for all deployments + ISablierLockup public constant LOCKUP = ISablierLockup(0xC2Da366fD67423b500cDF4712BdB41d0995b0794); + ISablierMerkleFactory public constant FACTORY = ISablierMerkleFactory(0x4ECd5A688b0365e61c1a764E8BF96A7C5dF5d35F); + + /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. + function createLLAirstream() public returns (ISablierMerkleLL merkleLL) { + // Declare the base parameters for the MerkleBase. + MerkleBase.ConstructorParams memory baseParams; + + 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 for the campaign + baseParams.shape = "A custom stream shape"; // Unique campaign name for the campaign + + // 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 too. + uint256 recipientCount = 10_000; + + // Deploy the Airstream contract. This contract will be completely owned by the campaign admin. Recipient will + // interact with the MerkleLL contract to claim their airdrop. + merkleLL = FACTORY.createMerkleLL({ + baseParams: baseParams, + lockup: LOCKUP, + cancelable: false, + transferable: true, + schedule: MerkleLL.Schedule({ + startTime: 0, // i.e. block.timestamp + startPercentage: ud2x18(0.01e18), + cliffDuration: 30 days, + cliffPercentage: ud2x18(0.01e18), + totalDuration: 90 days + }), + aggregateAmount: aggregateAmount, + recipientCount: recipientCount + }); + } +} diff --git a/lockup/periphery/AirstreamCreator.t.sol b/airdrops/AirstreamCreator.t.sol similarity index 86% rename from lockup/periphery/AirstreamCreator.t.sol rename to airdrops/AirstreamCreator.t.sol index 98544c9..8fc6896 100644 --- a/lockup/periphery/AirstreamCreator.t.sol +++ b/airdrops/AirstreamCreator.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3-0-or-later pragma solidity >=0.8.22; -import { ISablierV2MerkleLL } from "@sablier/v2-periphery/src/interfaces/ISablierV2MerkleLL.sol"; +import { ISablierMerkleLL } from "@sablier/airdrops/src/interfaces/ISablierMerkleLL.sol"; import { Test } from "forge-std/src/Test.sol"; import { AirstreamCreator } from "./AirstreamCreator.sol"; @@ -14,7 +14,7 @@ contract AirstreamCreatorTest is Test { function setUp() public { // Fork Ethereum Sepolia - vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 6_246_059 }); + vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 7_497_776 }); // Deploy the airstream creator airstreamCreator = new AirstreamCreator(); @@ -29,7 +29,7 @@ contract AirstreamCreatorTest is Test { // Tests creating the airstream campaign. function test_CreateLLAirstream() public { - ISablierV2MerkleLL merkleLL = airstreamCreator.createLLAirstream(); + ISablierMerkleLL merkleLL = airstreamCreator.createLLAirstream(); // Assert the merkleLL contract was created with correct params assertEq(address(0xBeeF), merkleLL.admin(), "admin"); diff --git a/bun.lockb b/bun.lockb index aa37a6f16e83688203013aee72efebb88128b735..e90a2ab45e9429916c83d68fd39eda75a4bc2b4d 100755 GIT binary patch delta 9116 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 delta 10631 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 diff --git a/flow/FlowBatchable.sol b/flow/FlowBatchable.sol index ce6d3a2..e159816 100644 --- a/flow/FlowBatchable.sol +++ b/flow/FlowBatchable.sol @@ -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. diff --git a/flow/FlowBatchable.t.sol b/flow/FlowBatchable.t.sol index 1dd9a87..05c112b 100644 --- a/flow/FlowBatchable.t.sol +++ b/flow/FlowBatchable.t.sol @@ -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(); diff --git a/flow/FlowStreamCreator.sol b/flow/FlowStreamCreator.sol index 5aed573..1c3966a 100644 --- a/flow/FlowStreamCreator.sol +++ b/flow/FlowStreamCreator.sol @@ -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) { @@ -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 @@ -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 diff --git a/flow/FlowStreamCreator.t.sol b/flow/FlowStreamCreator.t.sol index 7bbb475..c361b07 100644 --- a/flow/FlowStreamCreator.t.sol +++ b/flow/FlowStreamCreator.t.sol @@ -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(); diff --git a/flow/FlowStreamManager.sol b/flow/FlowStreamManager.sol index 9cbc598..a162cf3 100644 --- a/flow/FlowStreamManager.sol +++ b/flow/FlowStreamManager.sol @@ -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) }); diff --git a/foundry.toml b/foundry.toml index a71c655..d1e4edb 100644 --- a/foundry.toml +++ b/foundry.toml @@ -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" diff --git a/lockup/periphery/BatchLDStreamCreator.sol b/lockup/BatchLDStreamCreator.sol similarity index 80% rename from lockup/periphery/BatchLDStreamCreator.sol rename to lockup/BatchLDStreamCreator.sol index 4893db8..72bbe64 100644 --- a/lockup/periphery/BatchLDStreamCreator.sol +++ b/lockup/BatchLDStreamCreator.sol @@ -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); + 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 @@ -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 @@ -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 @@ -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); } } diff --git a/lockup/periphery/BatchLLStreamCreator.sol b/lockup/BatchLLStreamCreator.sol similarity index 72% rename from lockup/periphery/BatchLLStreamCreator.sol rename to lockup/BatchLLStreamCreator.sol index 3e1813e..a88905b 100644 --- a/lockup/periphery/BatchLLStreamCreator.sol +++ b/lockup/BatchLLStreamCreator.sol @@ -3,26 +3,24 @@ pragma solidity >=0.8.22; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ud60x18 } from "@prb/math/src/UD60x18.sol"; -import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; -import { Broker, LockupLinear } 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 { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; +import { Broker, Lockup, LockupLinear } from "@sablier/lockup/src/types/DataTypes.sol"; +import { ISablierBatchLockup } from "@sablier/lockup/src/interfaces/ISablierBatchLockup.sol"; +import { BatchLockup } from "@sablier/lockup/src/types/DataTypes.sol"; contract BatchLLStreamCreator { // Sepolia addresses IERC20 public constant DAI = IERC20(0x68194a729C2450ad26072b3D33ADaCbcef39D574); // See https://docs.sablier.com/contracts/v2/deployments for all deployments - ISablierV2LockupLinear public constant LOCKUP_LINEAR = - ISablierV2LockupLinear(0x3E435560fd0a03ddF70694b35b673C25c65aBB6C); - ISablierV2BatchLockup public constant BATCH_LOCKUP = - ISablierV2BatchLockup(0x04A9c14b7a000640419aD5515Db4eF4172C00E31); + 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 @@ -33,26 +31,26 @@ contract BatchLLStreamCreator { // Declare the first stream in the batch BatchLockup.CreateWithDurationsLL 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 stream0.durations = LockupLinear.Durations({ - cliff: 4 weeks, // Assets will be unlocked only after 4 weeks + cliff: 4 weeks, // Tokens will be unlocked only after 4 weeks total: 52 weeks // Setting a total duration of ~1 year }); stream0.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined // Declare the second stream in the batch BatchLockup.CreateWithDurationsLL 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 stream1.durations = LockupLinear.Durations({ - cliff: 1 weeks, // Assets will be unlocked only after 1 week + cliff: 1 weeks, // Tokens will be unlocked only after 1 week total: 26 weeks // Setting a total duration of ~6 months }); stream1.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined @@ -62,6 +60,6 @@ contract BatchLLStreamCreator { batch[0] = stream0; batch[1] = stream1; - streamIds = BATCH_LOCKUP.createWithDurationsLL(LOCKUP_LINEAR, DAI, batch); + streamIds = BATCH_LOCKUP.createWithDurationsLL(LOCKUP, DAI, batch); } } diff --git a/lockup/periphery/BatchLTStreamCreator.sol b/lockup/BatchLTStreamCreator.sol similarity index 79% rename from lockup/periphery/BatchLTStreamCreator.sol rename to lockup/BatchLTStreamCreator.sol index 1194e45..0b562b1 100644 --- a/lockup/periphery/BatchLTStreamCreator.sol +++ b/lockup/BatchLTStreamCreator.sol @@ -3,26 +3,23 @@ pragma solidity >=0.8.22; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ud60x18 } from "@prb/math/src/UD60x18.sol"; -import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; -import { Broker, LockupTranched } 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, LockupTranched } from "@sablier/lockup/src/types/DataTypes.sol"; contract BatchLTStreamCreator { // Sepolia addresses IERC20 public constant DAI = IERC20(0x68194a729C2450ad26072b3D33ADaCbcef39D574); // See https://docs.sablier.com/contracts/v2/deployments for all deployments - ISablierV2LockupTranched public constant LOCKUP_TRANCHED = - ISablierV2LockupTranched(0x3a1beA13A8C24c0EA2b8fAE91E4b2762A59D7aF5); - ISablierV2BatchLockup public constant BATCH_LOCKUP = - ISablierV2BatchLockup(0x04A9c14b7a000640419aD5515Db4eF4172C00E31); + 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 @@ -33,8 +30,8 @@ contract BatchLTStreamCreator { // Declare the first stream in the batch BatchLockup.CreateWithTimestampsLT 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 @@ -53,8 +50,8 @@ contract BatchLTStreamCreator { // Declare the second stream in the batch BatchLockup.CreateWithTimestampsLT 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 @@ -76,6 +73,6 @@ contract BatchLTStreamCreator { batch[0] = stream0; batch[1] = stream1; - streamIds = BATCH_LOCKUP.createWithTimestampsLT(LOCKUP_TRANCHED, DAI, batch); + streamIds = BATCH_LOCKUP.createWithTimestampsLT(LOCKUP, DAI, batch); } } diff --git a/lockup/periphery/BatchStreamCreator.t.sol b/lockup/BatchStreamCreator.t.sol similarity index 93% rename from lockup/periphery/BatchStreamCreator.t.sol rename to lockup/BatchStreamCreator.t.sol index 7dae210..2d80aaf 100644 --- a/lockup/periphery/BatchStreamCreator.t.sol +++ b/lockup/BatchStreamCreator.t.sol @@ -17,7 +17,7 @@ contract BatchStreamCreatorTest is Test { function setUp() public { // Fork Ethereum Sepolia - vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 6_240_755 }); + vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 7_497_776 }); // Deploy the stream creators dynamicCreator = new BatchLDStreamCreator(); @@ -46,7 +46,7 @@ contract BatchStreamCreatorTest is Test { // Tests that creating streams works by checking the stream ids function test_BatchLockupDynamicStreamCreator() public { - uint256 nextStreamId = dynamicCreator.LOCKUP_DYNAMIC().nextStreamId(); + uint256 nextStreamId = dynamicCreator.LOCKUP().nextStreamId(); uint256[] memory actualStreamIds = dynamicCreator.batchCreateStreams({ perStreamAmount: 1337e18 }); uint256[] memory expectedStreamIds = new uint256[](2); expectedStreamIds[0] = nextStreamId; @@ -60,7 +60,7 @@ contract BatchStreamCreatorTest is Test { // Tests that creating streams works by checking the stream ids function test_BatchLockupLinearStreamCreator() public { - uint256 nextStreamId = linearCreator.LOCKUP_LINEAR().nextStreamId(); + uint256 nextStreamId = linearCreator.LOCKUP().nextStreamId(); uint256[] memory actualStreamIds = linearCreator.batchCreateStreams({ perStreamAmount: 1337e18 }); uint256[] memory expectedStreamIds = new uint256[](2); expectedStreamIds[0] = nextStreamId; @@ -74,7 +74,7 @@ contract BatchStreamCreatorTest is Test { // Tests that creating streams works by checking the stream ids function test_BatchLockupTranchedStreamCreator() public { - uint256 nextStreamId = tranchedCreator.LOCKUP_TRANCHED().nextStreamId(); + uint256 nextStreamId = tranchedCreator.LOCKUP().nextStreamId(); uint256[] memory actualStreamIds = tranchedCreator.batchCreateStreams({ perStreamAmount: 1337e18 }); uint256[] memory expectedStreamIds = new uint256[](2); expectedStreamIds[0] = nextStreamId; diff --git a/lockup/core/LockupDynamicCurvesCreator.sol b/lockup/LockupDynamicCurvesCreator.sol similarity index 71% rename from lockup/core/LockupDynamicCurvesCreator.sol rename to lockup/LockupDynamicCurvesCreator.sol index fec10c9..4560778 100644 --- a/lockup/core/LockupDynamicCurvesCreator.sol +++ b/lockup/LockupDynamicCurvesCreator.sol @@ -4,8 +4,8 @@ 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 { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; +import { Broker, Lockup, LockupDynamic } from "@sablier/lockup/src/types/DataTypes.sol"; /// @notice Examples of how to create Lockup Dynamic streams with different curve shapes. /// @dev A visualization of the curve shapes can be found in the docs: @@ -14,8 +14,7 @@ import { Broker, LockupDynamic } from "@sablier/v2-core/src/types/DataTypes.sol" contract LockupDynamicCurvesCreator { // Sepolia addresses IERC20 public constant DAI = IERC20(0x68194a729C2450ad26072b3D33ADaCbcef39D574); - ISablierV2LockupDynamic public constant LOCKUP_DYNAMIC = - ISablierV2LockupDynamic(0x73BB6dD3f5828d60F8b3dBc8798EB10fbA2c5636); + ISablierLockup public constant LOCKUP = ISablierLockup(0xC2Da366fD67423b500cDF4712BdB41d0995b0794); /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. function createStream_Exponential() external returns (uint256 streamId) { @@ -26,30 +25,30 @@ contract LockupDynamicCurvesCreator { DAI.transferFrom(msg.sender, address(this), totalAmount); // Approve the Sablier contract to spend DAI - DAI.approve(address(LOCKUP_DYNAMIC), totalAmount); + DAI.approve(address(LOCKUP), totalAmount); // Declare the params struct - LockupDynamic.CreateWithDurations memory params; + Lockup.CreateWithDurations memory params; // Declare the function parameters params.sender = msg.sender; // The sender will be able to cancel the stream - params.recipient = address(0xCAFE); // The recipient of the streamed assets + params.recipient = address(0xCAFE); // The recipient of the streamed tokens params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees - params.asset = DAI; // The streaming asset + params.token = DAI; // The streaming token params.cancelable = true; // Whether the stream will be cancelable or not params.transferable = true; // Whether the stream will be transferable or not params.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined // Declare a single-size segment to match the curve shape - params.segments = new LockupDynamic.SegmentWithDuration[](1); - params.segments[0] = LockupDynamic.SegmentWithDuration({ + LockupDynamic.SegmentWithDuration[] memory segments = new LockupDynamic.SegmentWithDuration[](1); + segments[0] = LockupDynamic.SegmentWithDuration({ amount: uint128(totalAmount), duration: 100 days, exponent: ud2x18(6e18) }); // Create the LockupDynamic stream - streamId = LOCKUP_DYNAMIC.createWithDurations(params); + streamId = LOCKUP.createWithDurationsLD(params, segments); } function createStream_ExponentialCliff() external returns (uint256 streamId) { @@ -60,30 +59,28 @@ contract LockupDynamicCurvesCreator { DAI.transferFrom(msg.sender, address(this), totalAmount); // Approve the Sablier contract to spend DAI - DAI.approve(address(LOCKUP_DYNAMIC), totalAmount); + DAI.approve(address(LOCKUP), totalAmount); // Declare the params struct - LockupDynamic.CreateWithDurations memory params; + Lockup.CreateWithDurations memory params; // Declare the function parameters params.sender = msg.sender; // The sender will be able to cancel the stream - params.recipient = address(0xCAFE); // The recipient of the streamed assets + params.recipient = address(0xCAFE); // The recipient of the streamed tokens params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees - params.asset = DAI; // The streaming asset + params.token = DAI; // The streaming token params.cancelable = true; // Whether the stream will be cancelable or not params.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined // Declare a three-size segment to match the curve shape - params.segments = new LockupDynamic.SegmentWithDuration[](3); - params.segments[0] = + LockupDynamic.SegmentWithDuration[] memory segments = new LockupDynamic.SegmentWithDuration[](3); + segments[0] = LockupDynamic.SegmentWithDuration({ amount: 0, duration: 50 days - 1 seconds, exponent: ud2x18(1e18) }); - params.segments[1] = - LockupDynamic.SegmentWithDuration({ amount: 20e18, duration: 1 seconds, exponent: ud2x18(1e18) }); - params.segments[2] = - LockupDynamic.SegmentWithDuration({ amount: 80e18, duration: 50 days, exponent: ud2x18(6e18) }); + segments[1] = LockupDynamic.SegmentWithDuration({ amount: 20e18, duration: 1 seconds, exponent: ud2x18(1e18) }); + segments[2] = LockupDynamic.SegmentWithDuration({ amount: 80e18, duration: 50 days, exponent: ud2x18(6e18) }); // Create the LockupDynamic stream - streamId = LOCKUP_DYNAMIC.createWithDurations(params); + streamId = LOCKUP.createWithDurationsLD(params, segments); } function createStream_UnlockInSteps() external returns (uint256 streamId) { @@ -94,38 +91,38 @@ contract LockupDynamicCurvesCreator { DAI.transferFrom(msg.sender, address(this), totalAmount); // Approve the Sablier contract to spend DAI - DAI.approve(address(LOCKUP_DYNAMIC), totalAmount); + DAI.approve(address(LOCKUP), totalAmount); // Declare the params struct - LockupDynamic.CreateWithDurations memory params; + Lockup.CreateWithDurations memory params; // Declare the function parameters params.sender = msg.sender; // The sender will be able to cancel the stream - params.recipient = address(0xCAFE); // The recipient of the streamed assets + params.recipient = address(0xCAFE); // The recipient of the streamed tokens params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees - params.asset = DAI; // The streaming asset + params.token = DAI; // The streaming token params.cancelable = true; // Whether the stream will be cancelable or not params.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined // Declare a eight-size segment to match the curve shape uint256 segmentSize = 8; - params.segments = new LockupDynamic.SegmentWithDuration[](segmentSize); + LockupDynamic.SegmentWithDuration[] memory segments = new LockupDynamic.SegmentWithDuration[](segmentSize); // The even segments are empty and are spaced ~25 days apart for (uint256 i = 0; i < segmentSize; i += 2) { - params.segments[i] = + segments[i] = LockupDynamic.SegmentWithDuration({ amount: 0, duration: 25 days - 1 seconds, exponent: ud2x18(1e18) }); } // The odd segments are filled and have a delta of 1 second uint128 unlockAmount = totalAmount / uint128(segmentSize / 2); for (uint256 i = 1; i < segmentSize; i += 2) { - params.segments[i] = + segments[i] = LockupDynamic.SegmentWithDuration({ amount: unlockAmount, duration: 1 seconds, exponent: ud2x18(1e18) }); } // Create the LockupDynamic stream - streamId = LOCKUP_DYNAMIC.createWithDurations(params); + streamId = LOCKUP.createWithDurationsLD(params, segments); } function createStream_MonthlyUnlocks() external returns (uint256 streamId) { @@ -136,38 +133,38 @@ contract LockupDynamicCurvesCreator { DAI.transferFrom(msg.sender, address(this), totalAmount); // Approve the Sablier contract to spend DAI - DAI.approve(address(LOCKUP_DYNAMIC), totalAmount); + DAI.approve(address(LOCKUP), totalAmount); // Declare the params struct - LockupDynamic.CreateWithDurations memory params; + Lockup.CreateWithDurations memory params; // Declare the function parameters params.sender = msg.sender; // The sender will be able to cancel the stream - params.recipient = address(0xCAFE); // The recipient of the streamed assets + params.recipient = address(0xCAFE); // The recipient of the streamed tokens params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees - params.asset = DAI; // The streaming asset + params.token = DAI; // The streaming token params.cancelable = true; // Whether the stream will be cancelable or not params.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined // Declare a twenty four size segment to match the curve shape uint256 segmentSize = 24; - params.segments = new LockupDynamic.SegmentWithDuration[](segmentSize); + LockupDynamic.SegmentWithDuration[] memory segments = new LockupDynamic.SegmentWithDuration[](segmentSize); // The even segments are empty and are spaced 30 days apart (~one month) for (uint256 i = 0; i < segmentSize; i += 2) { - params.segments[i] = + segments[i] = LockupDynamic.SegmentWithDuration({ amount: 0, duration: 30 days - 1 seconds, exponent: ud2x18(1e18) }); } // The odd segments are filled and have a delta of 1 second uint128 unlockAmount = totalAmount / uint128(segmentSize / 2); for (uint256 i = 1; i < segmentSize; i += 2) { - params.segments[i] = + segments[i] = LockupDynamic.SegmentWithDuration({ amount: unlockAmount, duration: 1 seconds, exponent: ud2x18(1e18) }); } // Create the LockupDynamic stream - streamId = LOCKUP_DYNAMIC.createWithDurations(params); + streamId = LOCKUP.createWithDurationsLD(params, segments); } function createStream_Timelock() external returns (uint256 streamId) { @@ -178,28 +175,27 @@ contract LockupDynamicCurvesCreator { DAI.transferFrom(msg.sender, address(this), totalAmount); // Approve the Sablier contract to spend DAI - DAI.approve(address(LOCKUP_DYNAMIC), totalAmount); + DAI.approve(address(LOCKUP), totalAmount); // Declare the params struct - LockupDynamic.CreateWithDurations memory params; + Lockup.CreateWithDurations memory params; // Declare the function parameters params.sender = msg.sender; // The sender will be able to cancel the stream - params.recipient = address(0xCAFE); // The recipient of the streamed assets + params.recipient = address(0xCAFE); // The recipient of the streamed tokens params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees - params.asset = DAI; // The streaming asset + params.token = DAI; // The streaming token params.cancelable = true; // Whether the stream will be cancelable or not params.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined // Declare a two-size segment to match the curve shape - params.segments = new LockupDynamic.SegmentWithDuration[](2); - params.segments[0] = + LockupDynamic.SegmentWithDuration[] memory segments = new LockupDynamic.SegmentWithDuration[](2); + segments[0] = LockupDynamic.SegmentWithDuration({ amount: 0, duration: 90 days - 1 seconds, exponent: ud2x18(1e18) }); - params.segments[1] = - LockupDynamic.SegmentWithDuration({ amount: 100e18, duration: 1 seconds, exponent: ud2x18(1e18) }); + segments[1] = LockupDynamic.SegmentWithDuration({ amount: 100e18, duration: 1 seconds, exponent: ud2x18(1e18) }); // Create the LockupDynamic stream - streamId = LOCKUP_DYNAMIC.createWithDurations(params); + streamId = LOCKUP.createWithDurationsLD(params, segments); } function createStream_UnlockLinear() external returns (uint256 streamId) { @@ -210,28 +206,27 @@ contract LockupDynamicCurvesCreator { DAI.transferFrom(msg.sender, address(this), totalAmount); // Approve the Sablier contract to spend DAI - DAI.approve(address(LOCKUP_DYNAMIC), totalAmount); + DAI.approve(address(LOCKUP), totalAmount); // Declare the params struct - LockupDynamic.CreateWithDurations memory params; + Lockup.CreateWithDurations memory params; // Declare the function parameters params.sender = msg.sender; // The sender will be able to cancel the stream - params.recipient = address(0xCAFE); // The recipient of the streamed assets + params.recipient = address(0xCAFE); // The recipient of the streamed tokens params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees - params.asset = DAI; // The streaming asset + params.token = DAI; // The streaming token params.cancelable = true; // Whether the stream will be cancelable or not params.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined // Declare a two-size segment to match the curve shape - params.segments = new LockupDynamic.SegmentWithDuration[](2); - params.segments[0] = - LockupDynamic.SegmentWithDuration({ amount: 25e18, duration: 1 seconds, exponent: ud2x18(1e18) }); - params.segments[1] = + LockupDynamic.SegmentWithDuration[] memory segments = new LockupDynamic.SegmentWithDuration[](2); + segments[0] = LockupDynamic.SegmentWithDuration({ amount: 25e18, duration: 1 seconds, exponent: ud2x18(1e18) }); + segments[1] = LockupDynamic.SegmentWithDuration({ amount: 75e18, duration: 100 days - 1 days, exponent: ud2x18(1e18) }); // Create the LockupDynamic stream - streamId = LOCKUP_DYNAMIC.createWithDurations(params); + streamId = LOCKUP.createWithDurationsLD(params, segments); } function createStream_UnlockCliffLinear() external returns (uint256 streamId) { @@ -242,31 +237,28 @@ contract LockupDynamicCurvesCreator { DAI.transferFrom(msg.sender, address(this), totalAmount); // Approve the Sablier contract to spend DAI - DAI.approve(address(LOCKUP_DYNAMIC), totalAmount); + DAI.approve(address(LOCKUP), totalAmount); // Declare the params struct - LockupDynamic.CreateWithDurations memory params; + Lockup.CreateWithDurations memory params; // Declare the function parameters params.sender = msg.sender; // The sender will be able to cancel the stream - params.recipient = address(0xCAFE); // The recipient of the streamed assets + params.recipient = address(0xCAFE); // The recipient of the streamed tokens params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees - params.asset = DAI; // The streaming asset + params.token = DAI; // The streaming token params.cancelable = true; // Whether the stream will be cancelable or not params.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined // Declare a four-size segment to match the curve shape - params.segments = new LockupDynamic.SegmentWithDuration[](4); - params.segments[0] = - LockupDynamic.SegmentWithDuration({ amount: 25e18, duration: 1 seconds, exponent: ud2x18(1e18) }); - params.segments[1] = + LockupDynamic.SegmentWithDuration[] memory segments = new LockupDynamic.SegmentWithDuration[](4); + segments[0] = LockupDynamic.SegmentWithDuration({ amount: 25e18, duration: 1 seconds, exponent: ud2x18(1e18) }); + segments[1] = LockupDynamic.SegmentWithDuration({ amount: 0, duration: 50 days - 1 seconds, exponent: ud2x18(1e18) }); - params.segments[2] = - LockupDynamic.SegmentWithDuration({ amount: 25e18, duration: 1 seconds, exponent: ud2x18(1e18) }); - params.segments[3] = - LockupDynamic.SegmentWithDuration({ amount: 50e18, duration: 50 days, exponent: ud2x18(1e18) }); + segments[2] = LockupDynamic.SegmentWithDuration({ amount: 25e18, duration: 1 seconds, exponent: ud2x18(1e18) }); + segments[3] = LockupDynamic.SegmentWithDuration({ amount: 50e18, duration: 50 days, exponent: ud2x18(1e18) }); // Create the LockupDynamic stream - streamId = LOCKUP_DYNAMIC.createWithDurations(params); + streamId = LOCKUP.createWithDurationsLD(params, segments); } } diff --git a/lockup/core/LockupDynamicCurvesCreator.t.sol b/lockup/LockupDynamicCurvesCreator.t.sol similarity index 78% rename from lockup/core/LockupDynamicCurvesCreator.t.sol rename to lockup/LockupDynamicCurvesCreator.t.sol index f5416a0..66ebbaa 100644 --- a/lockup/core/LockupDynamicCurvesCreator.t.sol +++ b/lockup/LockupDynamicCurvesCreator.t.sol @@ -1,15 +1,12 @@ // SPDX-License-Identifier: GPL-3-0-or-later pragma solidity >=0.8.22; -import { ISablierV2LockupDynamic } from "@sablier/v2-core/src/interfaces/ISablierV2LockupDynamic.sol"; +import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; import { Test } from "forge-std/src/Test.sol"; import { LockupDynamicCurvesCreator } from "./LockupDynamicCurvesCreator.sol"; contract LockupDynamicCurvesCreatorTest is Test { - ISablierV2LockupDynamic public constant LOCKUP_DYNAMIC = - ISablierV2LockupDynamic(0x73BB6dD3f5828d60F8b3dBc8798EB10fbA2c5636); - // Test contracts LockupDynamicCurvesCreator internal creator; @@ -17,7 +14,7 @@ contract LockupDynamicCurvesCreatorTest is Test { function setUp() public { // Fork Ethereum Sepolia - vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 6_240_816 }); + vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 7_497_776 }); // Deploy the stream creator creator = new LockupDynamicCurvesCreator(); @@ -37,7 +34,7 @@ contract LockupDynamicCurvesCreatorTest is Test { } function test_CreateStream_Exponential() public { - uint256 expectedStreamId = creator.LOCKUP_DYNAMIC().nextStreamId(); + uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); uint256 actualStreamId = creator.createStream_Exponential(); // Assert that the stream has been created. @@ -46,13 +43,13 @@ contract LockupDynamicCurvesCreatorTest is Test { // Warp 50 days into the future, i.e. half way of the stream duration. vm.warp({ newTimestamp: block.timestamp + 50 days }); - uint128 actualStreamedAmount = creator.LOCKUP_DYNAMIC().streamedAmountOf(actualStreamId); + uint128 actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); uint128 expectedStreamedAmount = 1.5625e18; // 0.5^{6} * 100 + 0 assertEq(actualStreamedAmount, expectedStreamedAmount); } function test_CreateStream_ExponentialCliff() public { - uint256 expectedStreamId = creator.LOCKUP_DYNAMIC().nextStreamId(); + uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); uint256 actualStreamId = creator.createStream_ExponentialCliff(); // Assert that the stream has been created. @@ -63,20 +60,20 @@ contract LockupDynamicCurvesCreatorTest is Test { // Warp 50 days into the future, i.e. half way of the stream duration (unlock moment). vm.warp({ newTimestamp: blockTimestamp + 50 days }); - uint128 actualStreamedAmount = creator.LOCKUP_DYNAMIC().streamedAmountOf(actualStreamId); + uint128 actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); uint128 expectedStreamedAmount = 20e18; assertEq(actualStreamedAmount, expectedStreamedAmount); // Warp 75 days into the future, i.e. half way of the stream's last segment. vm.warp({ newTimestamp: blockTimestamp + 75 days }); - actualStreamedAmount = creator.LOCKUP_DYNAMIC().streamedAmountOf(actualStreamId); + actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); expectedStreamedAmount = 21.25e18; // 0.5^{6} * 80 + 20 assertEq(actualStreamedAmount, expectedStreamedAmount); } function test_CreateStream_UnlockInSteps() public { - uint256 expectedStreamId = creator.LOCKUP_DYNAMIC().nextStreamId(); + uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); uint256 actualStreamId = creator.createStream_UnlockInSteps(); // Assert that the stream has been created. @@ -87,7 +84,7 @@ contract LockupDynamicCurvesCreatorTest is Test { for (uint256 i = 0; i < 4; ++i) { vm.warp({ newTimestamp: block.timestamp + 25 days - 1 seconds }); - actualStreamedAmount = creator.LOCKUP_DYNAMIC().streamedAmountOf(actualStreamId); + actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); assertEq(actualStreamedAmount, expectedStreamedAmount); expectedStreamedAmount += 25e18; vm.warp({ newTimestamp: block.timestamp + 1 seconds }); @@ -95,7 +92,7 @@ contract LockupDynamicCurvesCreatorTest is Test { } function test_CreateStream_MonthlyUnlocks() public { - uint256 expectedStreamId = creator.LOCKUP_DYNAMIC().nextStreamId(); + uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); uint256 actualStreamId = creator.createStream_MonthlyUnlocks(); // Assert that the stream has been created. @@ -106,7 +103,7 @@ contract LockupDynamicCurvesCreatorTest is Test { for (uint256 i = 0; i < 12; ++i) { vm.warp({ newTimestamp: block.timestamp + 30 days - 1 seconds }); - actualStreamedAmount = creator.LOCKUP_DYNAMIC().streamedAmountOf(actualStreamId); + actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); assertEq(actualStreamedAmount, expectedStreamedAmount); expectedStreamedAmount += 10e18; @@ -115,7 +112,7 @@ contract LockupDynamicCurvesCreatorTest is Test { } function test_CreateStream_Timelock() external { - uint256 expectedStreamId = creator.LOCKUP_DYNAMIC().nextStreamId(); + uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); uint256 actualStreamId = creator.createStream_Timelock(); // Assert that the stream has been created. @@ -126,20 +123,20 @@ contract LockupDynamicCurvesCreatorTest is Test { // Warp 90 days - 1 second into the future, i.e. exactly 1 second before unlock. vm.warp({ newTimestamp: blockTimestamp + 90 days - 1 seconds }); - uint128 actualStreamedAmount = creator.LOCKUP_DYNAMIC().streamedAmountOf(actualStreamId); + uint128 actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); uint128 expectedStreamedAmount = 0; assertEq(actualStreamedAmount, expectedStreamedAmount); // Warp 90 days into the future, i.e. the unlock moment. vm.warp({ newTimestamp: blockTimestamp + 90 days }); - actualStreamedAmount = creator.LOCKUP_DYNAMIC().streamedAmountOf(actualStreamId); + actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); expectedStreamedAmount = 100e18; assertEq(actualStreamedAmount, expectedStreamedAmount); } function test_CreateStream_UnlockLinear() external { - uint256 expectedStreamId = creator.LOCKUP_DYNAMIC().nextStreamId(); + uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); uint256 actualStreamId = creator.createStream_UnlockLinear(); // Assert that the stream has been created. @@ -150,7 +147,7 @@ contract LockupDynamicCurvesCreatorTest is Test { // Warp 1 second into the future, i.e. the initial unlock. vm.warp({ newTimestamp: blockTimestamp + 1 seconds }); - uint128 actualStreamedAmount = creator.LOCKUP_DYNAMIC().streamedAmountOf(actualStreamId); + uint128 actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); uint128 expectedStreamedAmount = 25e18; assertEq(actualStreamedAmount, expectedStreamedAmount); @@ -160,13 +157,13 @@ contract LockupDynamicCurvesCreatorTest is Test { // total duration of segment: 8639999 seconds (100 days - 1 second) // amount to be stream in the current segment: 75e18 - actualStreamedAmount = creator.LOCKUP_DYNAMIC().streamedAmountOf(actualStreamId); + actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); expectedStreamedAmount = 62.87878787878787875e18; // (0.500000057870377068)^{1} * 75 + 25 assertEq(actualStreamedAmount, expectedStreamedAmount); } function test_CreateStream_UnlockCliffLinear() external { - uint256 expectedStreamId = creator.LOCKUP_DYNAMIC().nextStreamId(); + uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); uint256 actualStreamId = creator.createStream_UnlockCliffLinear(); // Assert that the stream has been created. @@ -177,7 +174,7 @@ contract LockupDynamicCurvesCreatorTest is Test { // Warp 1 second into the future, i.e. the initial unlock. vm.warp({ newTimestamp: blockTimestamp + 1 seconds }); - uint128 actualStreamedAmount = creator.LOCKUP_DYNAMIC().streamedAmountOf(actualStreamId); + uint128 actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); uint128 expectedStreamedAmount = 25e18; assertEq(actualStreamedAmount, expectedStreamedAmount); @@ -185,21 +182,21 @@ contract LockupDynamicCurvesCreatorTest is Test { vm.warp({ newTimestamp: blockTimestamp + 50 days }); // Assert that the streamed amount has remained the same. - actualStreamedAmount = creator.LOCKUP_DYNAMIC().streamedAmountOf(actualStreamId); + actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); assertEq(actualStreamedAmount, expectedStreamedAmount); // Warp 50 days plus a second into the future, i.e. after the cliff unlock. vm.warp({ newTimestamp: blockTimestamp + 50 days + 1 seconds }); // Assert that the streamed amount has increased by the cliff amount. - actualStreamedAmount = creator.LOCKUP_DYNAMIC().streamedAmountOf(actualStreamId); + actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); expectedStreamedAmount = 50e18; assertEq(actualStreamedAmount, expectedStreamedAmount); // Warp 75 days plus a second into the future. vm.warp({ newTimestamp: blockTimestamp + 75 days + 1 }); - actualStreamedAmount = creator.LOCKUP_DYNAMIC().streamedAmountOf(actualStreamId); + actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); expectedStreamedAmount = 75e18; // (0.50)^{1} * 50 + 50 assertEq(actualStreamedAmount, expectedStreamedAmount); } diff --git a/lockup/core/LockupDynamicStreamCreator.sol b/lockup/LockupDynamicStreamCreator.sol similarity index 71% rename from lockup/core/LockupDynamicStreamCreator.sol rename to lockup/LockupDynamicStreamCreator.sol index 07fc81c..c7ea672 100644 --- a/lockup/core/LockupDynamicStreamCreator.sol +++ b/lockup/LockupDynamicStreamCreator.sol @@ -4,16 +4,15 @@ 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 { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; +import { Broker, Lockup, LockupDynamic } from "@sablier/lockup/src/types/DataTypes.sol"; /// @notice Example of how to create a Lockup Dynamic stream. /// @dev This code is referenced in the docs: https://docs.sablier.com/contracts/v2/guides/create-stream/lockup-dynamic contract LockupDynamicStreamCreator { // sepolia addresses IERC20 public constant DAI = IERC20(0x68194a729C2450ad26072b3D33ADaCbcef39D574); - ISablierV2LockupDynamic public constant LOCKUP_DYNAMIC = - ISablierV2LockupDynamic(0x73BB6dD3f5828d60F8b3dBc8798EB10fbA2c5636); + ISablierLockup public constant LOCKUP = ISablierLockup(0xC2Da366fD67423b500cDF4712BdB41d0995b0794); /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. function createStream(uint128 amount0, uint128 amount1) public returns (uint256 streamId) { @@ -24,28 +23,29 @@ contract LockupDynamicStreamCreator { DAI.transferFrom(msg.sender, address(this), totalAmount); // Approve the Sablier contract to spend DAI - DAI.approve(address(LOCKUP_DYNAMIC), totalAmount); + DAI.approve(address(LOCKUP), totalAmount); // Declare the params struct - LockupDynamic.CreateWithTimestamps memory params; + Lockup.CreateWithTimestamps memory params; // Declare the function parameters params.sender = msg.sender; // The sender will be able to cancel the stream - params.recipient = address(0xCAFE); // The recipient of the streamed assets + params.recipient = address(0xCAFE); // The recipient of the streamed tokens params.totalAmount = uint128(totalAmount); // Total amount is the amount inclusive of all fees - params.asset = DAI; // The streaming asset + params.token = DAI; // The streaming token params.cancelable = true; // Whether the stream will be cancelable or not params.transferable = true; // Whether the stream will be transferable or not - params.startTime = uint40(block.timestamp + 100 seconds); + params.timestamps.start = uint40(block.timestamp + 100 seconds); + params.timestamps.end = uint40(block.timestamp + 52 weeks); // Declare some dummy segments - params.segments = new LockupDynamic.Segment[](2); - params.segments[0] = LockupDynamic.Segment({ + LockupDynamic.Segment[] memory segments = new LockupDynamic.Segment[](2); + segments[0] = LockupDynamic.Segment({ amount: amount0, exponent: ud2x18(1e18), timestamp: uint40(block.timestamp + 4 weeks) }); - params.segments[1] = ( + segments[1] = ( LockupDynamic.Segment({ amount: amount1, exponent: ud2x18(3.14e18), @@ -56,6 +56,6 @@ contract LockupDynamicStreamCreator { params.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined // Create the LockupDynamic stream - streamId = LOCKUP_DYNAMIC.createWithTimestamps(params); + streamId = LOCKUP.createWithTimestampsLD(params, segments); } } diff --git a/lockup/core/LockupLinearStreamCreator.sol b/lockup/LockupLinearStreamCreator.sol similarity index 68% rename from lockup/core/LockupLinearStreamCreator.sol rename to lockup/LockupLinearStreamCreator.sol index e0a89bb..36d4c9b 100644 --- a/lockup/core/LockupLinearStreamCreator.sol +++ b/lockup/LockupLinearStreamCreator.sol @@ -3,16 +3,15 @@ pragma solidity >=0.8.22; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ud60x18 } from "@prb/math/src/UD60x18.sol"; -import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; -import { Broker, LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; +import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; +import { Broker, Lockup, LockupLinear } from "@sablier/lockup/src/types/DataTypes.sol"; /// @notice Example of how to create a Lockup Linear stream. /// @dev This code is referenced in the docs: https://docs.sablier.com/contracts/v2/guides/create-stream/lockup-linear contract LockupLinearStreamCreator { // sepolia addresses IERC20 public constant DAI = IERC20(0x68194a729C2450ad26072b3D33ADaCbcef39D574); - ISablierV2LockupLinear public constant LOCKUP_LINEAR = - ISablierV2LockupLinear(0x3E435560fd0a03ddF70694b35b673C25c65aBB6C); + ISablierLockup public constant LOCKUP = ISablierLockup(0xC2Da366fD67423b500cDF4712BdB41d0995b0794); /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. function createStream(uint128 totalAmount) public returns (uint256 streamId) { @@ -20,25 +19,27 @@ contract LockupLinearStreamCreator { DAI.transferFrom(msg.sender, address(this), totalAmount); // Approve the Sablier contract to spend DAI - DAI.approve(address(LOCKUP_LINEAR), totalAmount); + DAI.approve(address(LOCKUP), totalAmount); // Declare the params struct - LockupLinear.CreateWithDurations memory params; + Lockup.CreateWithDurations memory params; // Declare the function parameters params.sender = msg.sender; // The sender will be able to cancel the stream - params.recipient = address(0xCAFE); // The recipient of the streamed assets + params.recipient = address(0xCAFE); // The recipient of the streamed tokens params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees - params.asset = DAI; // The streaming asset + params.token = DAI; // The streaming token params.cancelable = true; // Whether the stream will be cancelable or not params.transferable = true; // Whether the stream will be transferable or not - params.durations = LockupLinear.Durations({ - cliff: 4 weeks, // Assets will be unlocked only after 4 weeks + params.broker = Broker(address(0), ud60x18(0)); // Optional parameter for charging a fee + + LockupLinear.UnlockAmounts memory unlockAmounts = LockupLinear.UnlockAmounts({ start: 0, cliff: 0 }); + LockupLinear.Durations memory durations = LockupLinear.Durations({ + cliff: 0, // Setting a cliff of 0 total: 52 weeks // Setting a total duration of ~1 year }); - params.broker = Broker(address(0), ud60x18(0)); // Optional parameter for charging a fee // Create the LockupLinear stream using a function that sets the start time to `block.timestamp` - streamId = LOCKUP_LINEAR.createWithDurations(params); + streamId = LOCKUP.createWithDurationsLL(params, unlockAmounts, durations); } } diff --git a/lockup/core/LockupStreamCreator.t.sol b/lockup/LockupStreamCreator.t.sol similarity index 91% rename from lockup/core/LockupStreamCreator.t.sol rename to lockup/LockupStreamCreator.t.sol index eb1c0d7..64fe218 100644 --- a/lockup/core/LockupStreamCreator.t.sol +++ b/lockup/LockupStreamCreator.t.sol @@ -17,7 +17,7 @@ contract LockupStreamCreatorTest is Test { function setUp() public { // Fork Ethereum Sepolia - vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 6_240_816 }); + vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 7_497_776 }); // Deploy the stream creators dynamicCreator = new LockupDynamicStreamCreator(); @@ -45,14 +45,14 @@ contract LockupStreamCreatorTest is Test { uint128 amount0 = 1337e18 / 2; uint128 amount1 = 1337e18 - amount0; - uint256 expectedStreamId = dynamicCreator.LOCKUP_DYNAMIC().nextStreamId(); + uint256 expectedStreamId = dynamicCreator.LOCKUP().nextStreamId(); uint256 actualStreamId = dynamicCreator.createStream(amount0, amount1); assertEq(actualStreamId, expectedStreamId); } // Tests that creating streams works by checking the stream ids function test_LockupLinearStreamCreator() public { - uint256 expectedStreamId = linearCreator.LOCKUP_LINEAR().nextStreamId(); + uint256 expectedStreamId = linearCreator.LOCKUP().nextStreamId(); uint256 actualStreamId = linearCreator.createStream({ totalAmount: 1337e18 }); assertEq(actualStreamId, expectedStreamId); } @@ -62,7 +62,7 @@ contract LockupStreamCreatorTest is Test { uint128 amount0 = 1337e18 / 2; uint128 amount1 = 1337e18 - amount0; - uint256 expectedStreamId = tranchedCreator.LOCKUP_TRANCHED().nextStreamId(); + uint256 expectedStreamId = tranchedCreator.LOCKUP().nextStreamId(); uint256 actualStreamId = tranchedCreator.createStream(amount0, amount1); assertEq(actualStreamId, expectedStreamId); } diff --git a/lockup/core/LockupTranchedStreamCreator.sol b/lockup/LockupTranchedStreamCreator.sol similarity index 65% rename from lockup/core/LockupTranchedStreamCreator.sol rename to lockup/LockupTranchedStreamCreator.sol index fcd0397..8e8ca0a 100644 --- a/lockup/core/LockupTranchedStreamCreator.sol +++ b/lockup/LockupTranchedStreamCreator.sol @@ -3,16 +3,15 @@ pragma solidity >=0.8.22; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ud60x18 } from "@prb/math/src/UD60x18.sol"; -import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; -import { Broker, LockupTranched } from "@sablier/v2-core/src/types/DataTypes.sol"; +import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; +import { Broker, Lockup, LockupTranched } from "@sablier/lockup/src/types/DataTypes.sol"; /// @notice Example of how to create a Lockup Tranched stream. /// @dev This code is referenced in the docs: https://docs.sablier.com/contracts/v2/guides/create-stream/lockup-tranched contract LockupTranchedStreamCreator { // sepolia addresses IERC20 public constant DAI = IERC20(0x68194a729C2450ad26072b3D33ADaCbcef39D574); - ISablierV2LockupTranched public constant LOCKUP_TRANCHED = - ISablierV2LockupTranched(0x3a1beA13A8C24c0EA2b8fAE91E4b2762A59D7aF5); + ISablierLockup public constant LOCKUP = ISablierLockup(0xC2Da366fD67423b500cDF4712BdB41d0995b0794); /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. function createStream(uint128 amount0, uint128 amount1) public returns (uint256 streamId) { @@ -23,27 +22,27 @@ contract LockupTranchedStreamCreator { DAI.transferFrom(msg.sender, address(this), totalAmount); // Approve the Sablier contract to spend DAI - DAI.approve(address(LOCKUP_TRANCHED), totalAmount); + DAI.approve(address(LOCKUP), totalAmount); // Declare the params struct - LockupTranched.CreateWithDurations memory params; + Lockup.CreateWithDurations memory params; // Declare the function parameters params.sender = msg.sender; // The sender will be able to cancel the stream - params.recipient = address(0xCAFE); // The recipient of the streamed assets + params.recipient = address(0xCAFE); // The recipient of the streamed tokens params.totalAmount = uint128(totalAmount); // Total amount is the amount inclusive of all fees - params.asset = DAI; // The streaming asset + params.token = DAI; // The streaming token params.cancelable = true; // Whether the stream will be cancelable or not params.transferable = true; // Whether the stream will be transferable or not // Declare some dummy tranches - params.tranches = new LockupTranched.TrancheWithDuration[](2); - params.tranches[0] = LockupTranched.TrancheWithDuration({ amount: amount0, duration: uint40(4 weeks) }); - params.tranches[1] = (LockupTranched.TrancheWithDuration({ amount: amount1, duration: uint40(6 weeks) })); + LockupTranched.TrancheWithDuration[] memory tranches = new LockupTranched.TrancheWithDuration[](2); + tranches[0] = LockupTranched.TrancheWithDuration({ amount: amount0, duration: uint40(4 weeks) }); + tranches[1] = (LockupTranched.TrancheWithDuration({ amount: amount1, duration: uint40(6 weeks) })); params.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined // Create the LockupTranched stream - streamId = LOCKUP_TRANCHED.createWithDurations(params); + streamId = LOCKUP.createWithDurationsLT(params, tranches); } } diff --git a/lockup/core/RecipientHooks.sol b/lockup/RecipientHooks.sol similarity index 96% rename from lockup/core/RecipientHooks.sol rename to lockup/RecipientHooks.sol index bbfdd02..b7f9d2c 100644 --- a/lockup/core/RecipientHooks.sol +++ b/lockup/RecipientHooks.sol @@ -2,7 +2,7 @@ pragma solidity >=0.8.22; import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; -import { ISablierLockupRecipient } from "@sablier/v2-core/src/interfaces/ISablierLockupRecipient.sol"; +import { ISablierLockupRecipient } from "@sablier/lockup/src/interfaces/ISablierLockupRecipient.sol"; contract RecipientHooks is ISablierLockupRecipient { error CallerNotSablierContract(address caller, address sablierLockup); diff --git a/lockup/core/StakeSablierNFT.sol b/lockup/StakeSablierNFT.sol similarity index 95% rename from lockup/core/StakeSablierNFT.sol rename to lockup/StakeSablierNFT.sol index a240d58..d29bffa 100644 --- a/lockup/core/StakeSablierNFT.sol +++ b/lockup/StakeSablierNFT.sol @@ -5,9 +5,9 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { ERC721Holder } from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; -import { Adminable } from "@sablier/v2-core/src/abstracts/Adminable.sol"; -import { ISablierLockupRecipient } from "@sablier/v2-core/src/interfaces/ISablierLockupRecipient.sol"; -import { ISablierV2Lockup } from "@sablier/v2-core/src/interfaces/ISablierV2Lockup.sol"; +import { Adminable } from "@sablier/lockup/src/abstracts/Adminable.sol"; +import { ISablierLockupRecipient } from "@sablier/lockup/src/interfaces/ISablierLockupRecipient.sol"; +import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; /// @title StakeSablierNFT /// @@ -72,7 +72,7 @@ contract StakeSablierNFT is Adminable, ERC721Holder, ISablierLockupRecipient { /// @dev This should be the Sablier Lockup contract. /// - If you used Lockup Linear, you should use the LockupLinear contract address. /// - If you used Lockup Dynamic, you should use the LockupDynamic contract address. - ISablierV2Lockup public sablierLockup; + ISablierLockup public sablierLockup; /// @dev The staked stream IDs mapped by user addresses. mapping(address account => uint256 streamId) public stakedStreams; @@ -113,8 +113,13 @@ contract StakeSablierNFT is Adminable, ERC721Holder, ISablierLockupRecipient { /// @param initialAdmin The address of the initial contract admin. /// @param rewardERC20Token_ The address of the ERC-20 token used for rewards. /// @param sablierLockup_ The address of the ERC-721 Contract. - constructor(address initialAdmin, IERC20 rewardERC20Token_, ISablierV2Lockup sablierLockup_) { - admin = initialAdmin; + constructor( + address initialAdmin, + IERC20 rewardERC20Token_, + ISablierLockup sablierLockup_ + ) + Adminable(initialAdmin) + { rewardERC20Token = rewardERC20Token_; sablierLockup = sablierLockup_; } @@ -237,13 +242,13 @@ contract StakeSablierNFT is Adminable, ERC721Holder, ISablierLockupRecipient { return ISablierLockupRecipient.onSablierLockupWithdraw.selector; } - /// @notice Stake a Sablier NFT with specified base asset. + /// @notice Stake a Sablier NFT with specified base token. /// @dev The `msg.sender` must approve the staking contract to spend the Sablier NFT before calling this function. /// One user can only stake one NFT at a time. /// @param streamId The stream ID of the Sablier NFT to be staked. function stake(uint256 streamId) external updateReward(msg.sender) { - // Check: the Sablier NFT is streaming the staking asset. - if (sablierLockup.getAsset(streamId) != rewardERC20Token) { + // Check: the Sablier NFT is streaming the staking token. + if (sablierLockup.getUnderlyingToken(streamId) != rewardERC20Token) { revert DifferentStreamingToken(streamId, rewardERC20Token); } diff --git a/lockup/core/StreamManagement.sol b/lockup/StreamManagement.sol similarity index 94% rename from lockup/core/StreamManagement.sol rename to lockup/StreamManagement.sol index b6aac7c..99f0636 100644 --- a/lockup/core/StreamManagement.sol +++ b/lockup/StreamManagement.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity >=0.8.22; -import { ISablierV2Lockup } from "@sablier/v2-core/src/interfaces/ISablierV2Lockup.sol"; +import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; /// @notice Examples of how to manage Sablier streams after they have been created. /// @dev This code is referenced in the docs: https://docs.sablier.com/contracts/v2/guides/stream-management/setup contract StreamManagement { - ISablierV2Lockup public immutable sablier; + ISablierLockup public immutable sablier; - constructor(ISablierV2Lockup sablier_) { + constructor(ISablierLockup sablier_) { sablier = sablier_; } diff --git a/lockup/core/StreamManagementWithHook.sol b/lockup/StreamManagementWithHook.sol similarity index 88% rename from lockup/core/StreamManagementWithHook.sol rename to lockup/StreamManagementWithHook.sol index 727e3f5..ce03c89 100644 --- a/lockup/core/StreamManagementWithHook.sol +++ b/lockup/StreamManagementWithHook.sol @@ -5,9 +5,9 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import { ud60x18 } from "@prb/math/src/UD60x18.sol"; -import { ISablierLockupRecipient } from "@sablier/v2-core/src/interfaces/ISablierLockupRecipient.sol"; -import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; -import { Broker, LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; +import { ISablierLockupRecipient } from "@sablier/lockup/src/interfaces/ISablierLockupRecipient.sol"; +import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; +import { Broker, Lockup, LockupLinear } from "@sablier/lockup/src/types/DataTypes.sol"; /// @notice Example of creating Sablier streams and managing them on behalf of users with some withdrawal restrictions /// powered by Sablier hooks. @@ -19,7 +19,7 @@ contract StreamManagementWithHook is ISablierLockupRecipient { error CallerNotThisContract(); error Unauthorized(); - ISablierV2LockupLinear public immutable SABLIER; + ISablierLockup public immutable SABLIER; IERC20 public immutable TOKEN; /// @dev Stream IDs mapped to their beneficiaries. @@ -35,11 +35,10 @@ contract StreamManagementWithHook is ISablierLockupRecipient { } /// @dev Constructor will set the address of the lockup contract and ERC20 token. - constructor(ISablierV2LockupLinear sablier_, IERC20 token_) { + constructor(ISablierLockup sablier_, IERC20 token_) { SABLIER = sablier_; TOKEN = token_; } - /*////////////////////////////////////////////////////////////////////////// CREATE //////////////////////////////////////////////////////////////////////////*/ @@ -60,7 +59,7 @@ contract StreamManagementWithHook is ISablierLockupRecipient { TOKEN.transferFrom(msg.sender, address(this), totalAmount); TOKEN.approve(address(SABLIER), totalAmount); - LockupLinear.CreateWithDurations memory params; + Lockup.CreateWithDurations memory params; params.transferable = false; params.cancelable = true; // Set `this` as the recipient of the Stream. Only `this` will be able to call the "withdraw" function. @@ -68,15 +67,17 @@ contract StreamManagementWithHook is ISablierLockupRecipient { // Set `this` as the sender of the Stream. Only `this` will be able to call the "cancel" function params.sender = address(this); params.totalAmount = totalAmount; - params.asset = TOKEN; - params.durations = LockupLinear.Durations({ - cliff: 4 weeks, // Assets unlocked after 4 weeks - total: 52 weeks // Total duration of 1 year - }); + params.token = TOKEN; params.broker = Broker(address(0), ud60x18(0)); // No broker fee + LockupLinear.UnlockAmounts memory unlockAmounts = LockupLinear.UnlockAmounts({ start: 0, cliff: 0 }); + LockupLinear.Durations memory durations = LockupLinear.Durations({ + cliff: 0, // Setting a cliff of 0 + total: 52 weeks // Setting a total duration of ~1 year + }); + // Create the stream. - streamId = SABLIER.createWithDurations(params); + streamId = SABLIER.createWithDurationsLL(params, unlockAmounts, durations); // Set the `beneficiary` . streamBeneficiaries[streamId] = beneficiary; diff --git a/lockup/core/StreamManagementWithHook.t.sol b/lockup/StreamManagementWithHook.t.sol similarity index 85% rename from lockup/core/StreamManagementWithHook.t.sol rename to lockup/StreamManagementWithHook.t.sol index 25cf8c5..dde955f 100644 --- a/lockup/core/StreamManagementWithHook.t.sol +++ b/lockup/StreamManagementWithHook.t.sol @@ -1,10 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity >=0.8.22; -import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; -import { ISablierV2NFTDescriptor } from "@sablier/v2-core/src/interfaces/ISablierV2NFTDescriptor.sol"; -import { SablierV2LockupLinear } from "@sablier/v2-core/src/SablierV2LockupLinear.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; +import { ILockupNFTDescriptor } from "@sablier/lockup/src/interfaces/ILockupNFTDescriptor.sol"; +import { SablierLockup } from "@sablier/lockup/src/SablierLockup.sol"; + import { Test } from "forge-std/src/Test.sol"; import { StreamManagementWithHook } from "./StreamManagementWithHook.sol"; @@ -16,7 +17,7 @@ contract MockERC20 is ERC20 { contract StreamManagementWithHookTest is Test { StreamManagementWithHook internal streamManager; - ISablierV2LockupLinear internal sablierLockup; + ISablierLockup internal sablierLockup; ERC20 internal token; uint128 internal amount = 10e18; @@ -27,6 +28,8 @@ contract StreamManagementWithHookTest is Test { address internal sablierAdmin; function setUp() public { + vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 7_497_776 }); + // Create a test users alice = makeAddr("Alice"); bob = makeAddr("Bob"); @@ -36,9 +39,10 @@ contract StreamManagementWithHookTest is Test { token = new MockERC20(bob); // Deploy Sablier Lockup Linear contract - sablierLockup = new SablierV2LockupLinear( + sablierLockup = new SablierLockup( sablierAdmin, - ISablierV2NFTDescriptor(address(0)) // Irrelevant for test purposes + ILockupNFTDescriptor(address(0)), // Irrelevant for test purposes + 500 // the MAX_COUNT ); // Deploy StreamManagementWithHook contract @@ -65,10 +69,10 @@ contract StreamManagementWithHookTest is Test { // Check balances assertEq(token.balanceOf(alice), 0); assertEq(token.balanceOf(bob), 1_000_000e18 - amount); - assertEq(token.balanceOf(address(sablierLockup)), amount); + assertEq(token.balanceOf(address(streamManager.SABLIER())), amount); // Check stream details are correct - assertEq(address(sablierLockup.getAsset(streamId)), address(token)); + assertEq(address(sablierLockup.getUnderlyingToken(streamId)), address(token)); assertEq(sablierLockup.getRecipient(streamId), address(streamManager)); assertEq(sablierLockup.getDepositedAmount(streamId), amount); assertEq(sablierLockup.isCancelable(streamId), true); diff --git a/lockup/periphery/AirstreamCreator.sol b/lockup/periphery/AirstreamCreator.sol deleted file mode 100644 index ecb1a5a..0000000 --- a/lockup/periphery/AirstreamCreator.sol +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.22; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; -import { LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; -import { ISablierV2MerkleLL } from "@sablier/v2-periphery/src/interfaces/ISablierV2MerkleLL.sol"; -import { ISablierV2MerkleLockupFactory } from "@sablier/v2-periphery/src/interfaces/ISablierV2MerkleLockupFactory.sol"; -import { MerkleLockup } from "@sablier/v2-periphery/src/types/DataTypes.sol"; - -/// @notice Example of how to create an Airstream campaign with Lockup Linear. -/// @dev This code is referenced in the docs: https://docs.sablier.com/contracts/v2/guides/create-airstream -contract AirstreamCreator { - // Sepolia addresses - IERC20 public constant DAI = IERC20(0x68194a729C2450ad26072b3D33ADaCbcef39D574); - // See https://docs.sablier.com/contracts/v2/deployments for all deployments - ISablierV2LockupLinear public constant LOCKUP_LINEAR = - ISablierV2LockupLinear(0x3E435560fd0a03ddF70694b35b673C25c65aBB6C); - ISablierV2MerkleLockupFactory public constant FACTORY = - ISablierV2MerkleLockupFactory(0x56E9180A8d2C35c99F2F8a1A5Ab8aBe79E876E8c); - - /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. - function createLLAirstream() public returns (ISablierV2MerkleLL merkleLL) { - // Declare the base parameters for the MerkleLockup. - MerkleLockup.ConstructorParams memory baseParams; - - baseParams.asset = DAI; - baseParams.cancelable = true; // Whether the stream will be cancelable or not after it has been claimed - 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.name = "My First Campaign"; // Unique name for the campaign - baseParams.transferable = true; // Whether the stream will be transferable or not - - // Stream duration as required by the lockup linear. - LockupLinear.Durations memory streamDurations; - streamDurations = LockupLinear.Durations({ - cliff: 4 weeks, // Assets will be unlocked only after 4 weeks of claiming - total: 52 weeks // Setting a total duration of ~1 year - }); - - // The total amount of assets you want to airdrop to your users. - uint256 aggregateAmount = 100_000_000e18; - - // The total number of addresses you want to airdrop your tokens too. - uint256 recipientCount = 10_000; - - // Deploy the Airstream contract. This contract will be completely owned by the campaign admin. Recipient will - // interact with the MerkleLL contract to claim their airdrop. - merkleLL = FACTORY.createMerkleLL(baseParams, LOCKUP_LINEAR, streamDurations, aggregateAmount, recipientCount); - } -} diff --git a/lockup/core/tests/stake-sablier-nft-test/StakeSablierNFT.t.sol b/lockup/tests/stake-sablier-nft-test/StakeSablierNFT.t.sol similarity index 80% rename from lockup/core/tests/stake-sablier-nft-test/StakeSablierNFT.t.sol rename to lockup/tests/stake-sablier-nft-test/StakeSablierNFT.t.sol index 33300d8..ab3e5cb 100644 --- a/lockup/core/tests/stake-sablier-nft-test/StakeSablierNFT.t.sol +++ b/lockup/tests/stake-sablier-nft-test/StakeSablierNFT.t.sol @@ -3,8 +3,8 @@ pragma solidity >=0.8.19; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ud60x18 } from "@prb/math/src/UD60x18.sol"; -import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; -import { Broker, LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; +import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; +import { Broker, Lockup, LockupLinear } from "@sablier/lockup/src/types/DataTypes.sol"; import { Test } from "forge-std/src/Test.sol"; import { StakeSablierNFT } from "../../StakeSablierNFT.sol"; @@ -47,8 +47,7 @@ abstract contract StakeSablierNFT_Fork_Test is Test { IERC20 public constant USDC = IERC20(0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238); // Get the latest deployment address from the docs: https://docs.sablier.com/contracts/v2/deployments. - ISablierV2LockupLinear internal constant SABLIER = - ISablierV2LockupLinear(0x3E435560fd0a03ddF70694b35b673C25c65aBB6C); + ISablierLockup internal constant SABLIER = ISablierLockup(0xC2Da366fD67423b500cDF4712BdB41d0995b0794); // Set a stream ID to stake. uint256 internal stakingStreamId = 2; @@ -66,8 +65,8 @@ abstract contract StakeSablierNFT_Fork_Test is Test { Users internal users; function setUp() public { - // Fork Ethereum Mainnet. - vm.createSelectFork({ blockNumber: 6_239_031, urlOrAlias: "sepolia" }); + // Fork Ethereum Sepolia + vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 7_497_776 }); // Create users. users.admin = makeAddr("admin"); @@ -95,9 +94,9 @@ abstract contract StakeSablierNFT_Fork_Test is Test { stakingContract.startStakingPeriod(10_000e18, 1 weeks); // Stake some streams. - _createAndStakeStreamBy({ recipient: users.alice, asset: DAI, stake: true }); - _createAndStakeStreamBy({ recipient: users.bob, asset: USDC, stake: false }); - _createAndStakeStreamBy({ recipient: users.joe, asset: DAI, stake: false }); + _createAndStakeStreamBy({ recipient: users.alice, token: DAI, stake: true }); + _createAndStakeStreamBy({ recipient: users.bob, token: USDC, stake: false }); + _createAndStakeStreamBy({ recipient: users.joe, token: DAI, stake: false }); // Make the stream owner the `msg.sender` in all the subsequent calls. resetPrank({ msgSender: users.joe.addr }); @@ -112,37 +111,39 @@ abstract contract StakeSablierNFT_Fork_Test is Test { vm.startPrank(msgSender); } - function _createLockupLinearStreams(address recipient, IERC20 asset) private returns (uint256 streamId) { - deal({ token: address(asset), to: users.admin, give: AMOUNT_IN_STREAM }); + function _createLockupLinearStreams(address recipient, IERC20 token) private returns (uint256 streamId) { + deal({ token: address(token), to: users.admin, give: AMOUNT_IN_STREAM }); resetPrank({ msgSender: users.admin }); - asset.approve(address(SABLIER), type(uint256).max); + token.approve(address(SABLIER), type(uint256).max); // Declare the params struct - LockupLinear.CreateWithDurations memory params; + Lockup.CreateWithDurations memory params; // Declare the function parameters params.sender = users.admin; // The sender will be able to cancel the stream - params.recipient = recipient; // The recipient of the streamed assets + params.recipient = recipient; // The recipient of the streamed tokens params.totalAmount = uint128(AMOUNT_IN_STREAM); // Total amount is the amount inclusive of all fees - params.asset = asset; // The streaming asset + params.token = token; // The streaming token params.cancelable = true; // Whether the stream will be cancelable or not params.transferable = true; // Whether the stream will be transferable or not - params.durations = LockupLinear.Durations({ - cliff: 4 weeks, // Assets will be unlocked only after 4 weeks + params.broker = Broker(address(0), ud60x18(0)); // Optional parameter for charging a fee + + LockupLinear.UnlockAmounts memory unlockAmounts = LockupLinear.UnlockAmounts({ start: 0, cliff: 0 }); + LockupLinear.Durations memory durations = LockupLinear.Durations({ + cliff: 0, // Setting a cliff of 0 total: 52 weeks // Setting a total duration of ~1 year }); - params.broker = Broker(address(0), ud60x18(0)); // Optional parameter for charging a fee // Create the Sablier stream using a function that sets the start time to `block.timestamp` - streamId = SABLIER.createWithDurations(params); + streamId = SABLIER.createWithDurationsLL(params, unlockAmounts, durations); } - function _createAndStakeStreamBy(StreamOwner storage recipient, IERC20 asset, bool stake) private { + function _createAndStakeStreamBy(StreamOwner storage recipient, IERC20 token, bool stake) private { resetPrank({ msgSender: users.admin }); - uint256 streamId = _createLockupLinearStreams(recipient.addr, asset); + uint256 streamId = _createLockupLinearStreams(recipient.addr, token); recipient.streamId = streamId; // Make the stream owner the `msg.sender` in all the subsequent calls. diff --git a/lockup/core/tests/stake-sablier-nft-test/claim-rewards/claimRewards.t.sol b/lockup/tests/stake-sablier-nft-test/claim-rewards/claimRewards.t.sol similarity index 100% rename from lockup/core/tests/stake-sablier-nft-test/claim-rewards/claimRewards.t.sol rename to lockup/tests/stake-sablier-nft-test/claim-rewards/claimRewards.t.sol diff --git a/lockup/core/tests/stake-sablier-nft-test/claim-rewards/claimRewards.tree b/lockup/tests/stake-sablier-nft-test/claim-rewards/claimRewards.tree similarity index 100% rename from lockup/core/tests/stake-sablier-nft-test/claim-rewards/claimRewards.tree rename to lockup/tests/stake-sablier-nft-test/claim-rewards/claimRewards.tree diff --git a/lockup/core/tests/stake-sablier-nft-test/stake/stake.t.sol b/lockup/tests/stake-sablier-nft-test/stake/stake.t.sol similarity index 100% rename from lockup/core/tests/stake-sablier-nft-test/stake/stake.t.sol rename to lockup/tests/stake-sablier-nft-test/stake/stake.t.sol diff --git a/lockup/core/tests/stake-sablier-nft-test/stake/stake.tree b/lockup/tests/stake-sablier-nft-test/stake/stake.tree similarity index 100% rename from lockup/core/tests/stake-sablier-nft-test/stake/stake.tree rename to lockup/tests/stake-sablier-nft-test/stake/stake.tree diff --git a/lockup/core/tests/stake-sablier-nft-test/unstake/unstake.t.sol b/lockup/tests/stake-sablier-nft-test/unstake/unstake.t.sol similarity index 96% rename from lockup/core/tests/stake-sablier-nft-test/unstake/unstake.t.sol rename to lockup/tests/stake-sablier-nft-test/unstake/unstake.t.sol index d05c4f7..f242f30 100644 --- a/lockup/core/tests/stake-sablier-nft-test/unstake/unstake.t.sol +++ b/lockup/tests/stake-sablier-nft-test/unstake/unstake.t.sol @@ -29,7 +29,7 @@ contract Unstake_Test is StakeSablierNFT_Fork_Test { // Assert: NFT has been transferred. assertEq(SABLIER.ownerOf(users.joe.streamId), users.joe.addr); - // Assert: `stakedAssets` and `stakedStreamId` have been deleted from storage. + // Assert: `stakedTokens` and `stakedStreamId` have been deleted from storage. assertEq(stakingContract.stakedUsers(users.joe.streamId), address(0)); assertEq(stakingContract.stakedStreams(users.joe.addr), 0); diff --git a/lockup/core/tests/stake-sablier-nft-test/unstake/unstake.tree b/lockup/tests/stake-sablier-nft-test/unstake/unstake.tree similarity index 100% rename from lockup/core/tests/stake-sablier-nft-test/unstake/unstake.tree rename to lockup/tests/stake-sablier-nft-test/unstake/unstake.tree diff --git a/package.json b/package.json index 83d526c..aeba0d7 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,9 @@ "dependencies": { "@openzeppelin/contracts": "5.0.2", "@prb/math": "4.1.0", - "@sablier/flow": "^1.0.0", - "@sablier/v2-core": "1.2.0", - "@sablier/v2-periphery": "1.2.0" + "@sablier/airdrops": "github:sablier-labs/airdrops#main", + "@sablier/flow": "github:sablier-labs/flow#staging", + "@sablier/v2-core": "github:sablier-labs/v2-core#staging" }, "homepage": "https://github.com/sablier-labs/examples#readme", "keywords": [ @@ -41,7 +41,8 @@ "private": true, "repository": "github.com:sablier-labs/examples", "scripts": { - "build": "bun run build:flow && bun run build:lockup", + "build": "bun run build:airdrops && bun run build:flow && bun run build:lockup", + "build:airdrops": "FOUNDRY_PROFILE=airdrops forge build", "build:flow": "FOUNDRY_PROFILE=flow forge build", "build:lockup": "FOUNDRY_PROFILE=lockup forge build", "clean": "rm -rf cache out", @@ -49,7 +50,8 @@ "lint:sol": "forge fmt . && bun solhint \"{flow,lockup}/**/*.sol\"", "prettier:check": "prettier --check \"**/*.{md,yml}\"", "prettier:write": "prettier --write \"**/*.{md,yml}\"", - "test": "bun run test:flow && bun run test:lockup", + "test": "bun run test:airdrops && bun run test:flow && bun run test:lockup", + "test:airdrops": "FOUNDRY_PROFILE=airdrops forge test", "test:flow": "FOUNDRY_PROFILE=flow forge test", "test:lockup": "FOUNDRY_PROFILE=lockup forge test" } diff --git a/remappings.txt b/remappings.txt index 8331950..e56b57a 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,6 +1,6 @@ @openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ @prb/math/=node_modules/@prb/math/ +@sablier/airdrops/=node_modules/@sablier/airdrops/ @sablier/flow/=node_modules/@sablier/flow/ -@sablier/v2-core/=node_modules/@sablier/v2-core/ -@sablier/v2-periphery/=node_modules/@sablier/v2-periphery/ +@sablier/lockup/=node_modules/@sablier/lockup/ forge-std/=node_modules/forge-std/ From 8c7e69c3a8f30132ba0a0cb01c503b58ed6fd45b Mon Sep 17 00:00:00 2001 From: andreivladbrg Date: Thu, 16 Jan 2025 00:32:09 +0200 Subject: [PATCH 02/11] chore: update docs links --- airdrops/AirstreamCreator.sol | 4 ++-- lockup/BatchLDStreamCreator.sol | 2 +- lockup/BatchLLStreamCreator.sol | 2 +- lockup/BatchLTStreamCreator.sol | 2 +- lockup/LockupDynamicStreamCreator.sol | 3 ++- lockup/LockupLinearStreamCreator.sol | 5 +++-- lockup/LockupTranchedStreamCreator.sol | 3 ++- lockup/StreamManagement.sol | 2 +- lockup/tests/stake-sablier-nft-test/StakeSablierNFT.t.sol | 2 +- 9 files changed, 14 insertions(+), 11 deletions(-) diff --git a/airdrops/AirstreamCreator.sol b/airdrops/AirstreamCreator.sol index 0314bfb..d74b238 100644 --- a/airdrops/AirstreamCreator.sol +++ b/airdrops/AirstreamCreator.sol @@ -9,11 +9,11 @@ import { ISablierMerkleFactory } from "@sablier/airdrops/src/interfaces/ISablier import { MerkleBase, MerkleLL } from "@sablier/airdrops/src/types/DataTypes.sol"; /// @notice Example of how to create an Airstream campaign with Lockup Linear. -/// @dev This code is referenced in the docs: https://docs.sablier.com/contracts/v2/guides/create-airstream +/// @dev This code is referenced in the docs: https://docs.sablier.com/guides/lockup/examples/create-airstream contract AirstreamCreator { // Sepolia addresses IERC20 public constant DAI = IERC20(0x68194a729C2450ad26072b3D33ADaCbcef39D574); - // See https://docs.sablier.com/contracts/v2/deployments for all deployments + // See https://docs.sablier.com/guides/lockup/deployments for all deployments ISablierLockup public constant LOCKUP = ISablierLockup(0xC2Da366fD67423b500cDF4712BdB41d0995b0794); ISablierMerkleFactory public constant FACTORY = ISablierMerkleFactory(0x4ECd5A688b0365e61c1a764E8BF96A7C5dF5d35F); diff --git a/lockup/BatchLDStreamCreator.sol b/lockup/BatchLDStreamCreator.sol index 72bbe64..433c111 100644 --- a/lockup/BatchLDStreamCreator.sol +++ b/lockup/BatchLDStreamCreator.sol @@ -11,7 +11,7 @@ import { BatchLockup, Broker, LockupDynamic } from "@sablier/lockup/src/types/Da contract BatchLDStreamCreator { // Sepolia addresses IERC20 public constant DAI = IERC20(0x68194a729C2450ad26072b3D33ADaCbcef39D574); - // See https://docs.sablier.com/contracts/v2/deployments for all deployments + // See https://docs.sablier.com/guides/lockup/deployments for all deployments ISablierLockup public constant LOCKUP = ISablierLockup(0xC2Da366fD67423b500cDF4712BdB41d0995b0794); ISablierBatchLockup public constant BATCH_LOCKUP = ISablierBatchLockup(0xd4294579236eE290668c8FdaE9403c4F00D914f0); diff --git a/lockup/BatchLLStreamCreator.sol b/lockup/BatchLLStreamCreator.sol index a88905b..7e1ba02 100644 --- a/lockup/BatchLLStreamCreator.sol +++ b/lockup/BatchLLStreamCreator.sol @@ -11,7 +11,7 @@ import { BatchLockup } from "@sablier/lockup/src/types/DataTypes.sol"; contract BatchLLStreamCreator { // Sepolia addresses IERC20 public constant DAI = IERC20(0x68194a729C2450ad26072b3D33ADaCbcef39D574); - // See https://docs.sablier.com/contracts/v2/deployments for all deployments + // See https://docs.sablier.com/guides/lockup/deployments for all deployments ISablierLockup public constant LOCKUP = ISablierLockup(0xC2Da366fD67423b500cDF4712BdB41d0995b0794); ISablierBatchLockup public constant BATCH_LOCKUP = ISablierBatchLockup(0xd4294579236eE290668c8FdaE9403c4F00D914f0); diff --git a/lockup/BatchLTStreamCreator.sol b/lockup/BatchLTStreamCreator.sol index 0b562b1..af523dd 100644 --- a/lockup/BatchLTStreamCreator.sol +++ b/lockup/BatchLTStreamCreator.sol @@ -10,7 +10,7 @@ import { BatchLockup, Broker, LockupTranched } from "@sablier/lockup/src/types/D contract BatchLTStreamCreator { // Sepolia addresses IERC20 public constant DAI = IERC20(0x68194a729C2450ad26072b3D33ADaCbcef39D574); - // See https://docs.sablier.com/contracts/v2/deployments for all deployments + // See https://docs.sablier.com/guides/lockup/deployments for all deployments ISablierLockup public constant LOCKUP = ISablierLockup(0xC2Da366fD67423b500cDF4712BdB41d0995b0794); ISablierBatchLockup public constant BATCH_LOCKUP = ISablierBatchLockup(0xd4294579236eE290668c8FdaE9403c4F00D914f0); diff --git a/lockup/LockupDynamicStreamCreator.sol b/lockup/LockupDynamicStreamCreator.sol index c7ea672..ffe1584 100644 --- a/lockup/LockupDynamicStreamCreator.sol +++ b/lockup/LockupDynamicStreamCreator.sol @@ -8,7 +8,8 @@ import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.so import { Broker, Lockup, LockupDynamic } from "@sablier/lockup/src/types/DataTypes.sol"; /// @notice Example of how to create a Lockup Dynamic stream. -/// @dev This code is referenced in the docs: https://docs.sablier.com/contracts/v2/guides/create-stream/lockup-dynamic +/// @dev This code is referenced in the docs: +/// https://docs.sablier.com/guides/lockup/examples/create-stream/lockup-dynamic contract LockupDynamicStreamCreator { // sepolia addresses IERC20 public constant DAI = IERC20(0x68194a729C2450ad26072b3D33ADaCbcef39D574); diff --git a/lockup/LockupLinearStreamCreator.sol b/lockup/LockupLinearStreamCreator.sol index 36d4c9b..48e5a45 100644 --- a/lockup/LockupLinearStreamCreator.sol +++ b/lockup/LockupLinearStreamCreator.sol @@ -7,9 +7,10 @@ import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.so import { Broker, Lockup, LockupLinear } from "@sablier/lockup/src/types/DataTypes.sol"; /// @notice Example of how to create a Lockup Linear stream. -/// @dev This code is referenced in the docs: https://docs.sablier.com/contracts/v2/guides/create-stream/lockup-linear +/// @dev This code is referenced in the docs: +/// https://docs.sablier.com/guides/lockup/examples/create-stream/lockup-linear contract LockupLinearStreamCreator { - // sepolia addresses + // Sepolia addresses IERC20 public constant DAI = IERC20(0x68194a729C2450ad26072b3D33ADaCbcef39D574); ISablierLockup public constant LOCKUP = ISablierLockup(0xC2Da366fD67423b500cDF4712BdB41d0995b0794); diff --git a/lockup/LockupTranchedStreamCreator.sol b/lockup/LockupTranchedStreamCreator.sol index 8e8ca0a..d9103e4 100644 --- a/lockup/LockupTranchedStreamCreator.sol +++ b/lockup/LockupTranchedStreamCreator.sol @@ -7,7 +7,8 @@ import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.so import { Broker, Lockup, LockupTranched } from "@sablier/lockup/src/types/DataTypes.sol"; /// @notice Example of how to create a Lockup Tranched stream. -/// @dev This code is referenced in the docs: https://docs.sablier.com/contracts/v2/guides/create-stream/lockup-tranched +/// @dev This code is referenced in the docs: +/// https://docs.sablier.com/guides/lockup/examples/create-stream/lockup-tranched contract LockupTranchedStreamCreator { // sepolia addresses IERC20 public constant DAI = IERC20(0x68194a729C2450ad26072b3D33ADaCbcef39D574); diff --git a/lockup/StreamManagement.sol b/lockup/StreamManagement.sol index 99f0636..9440558 100644 --- a/lockup/StreamManagement.sol +++ b/lockup/StreamManagement.sol @@ -4,7 +4,7 @@ pragma solidity >=0.8.22; import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; /// @notice Examples of how to manage Sablier streams after they have been created. -/// @dev This code is referenced in the docs: https://docs.sablier.com/contracts/v2/guides/stream-management/setup +/// @dev This code is referenced in the docs: https://docs.sablier.com/guides/lockup/examples/stream-management/setup contract StreamManagement { ISablierLockup public immutable sablier; diff --git a/lockup/tests/stake-sablier-nft-test/StakeSablierNFT.t.sol b/lockup/tests/stake-sablier-nft-test/StakeSablierNFT.t.sol index ab3e5cb..26b5d1c 100644 --- a/lockup/tests/stake-sablier-nft-test/StakeSablierNFT.t.sol +++ b/lockup/tests/stake-sablier-nft-test/StakeSablierNFT.t.sol @@ -46,7 +46,7 @@ abstract contract StakeSablierNFT_Fork_Test is Test { IERC20 public constant DAI = IERC20(0x776b6fC2eD15D6Bb5Fc32e0c89DE68683118c62A); IERC20 public constant USDC = IERC20(0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238); - // Get the latest deployment address from the docs: https://docs.sablier.com/contracts/v2/deployments. + // Get the latest deployment address from the docs: https://docs.sablier.com/guides/lockup/deployments. ISablierLockup internal constant SABLIER = ISablierLockup(0xC2Da366fD67423b500cDF4712BdB41d0995b0794); // Set a stream ID to stake. From 8ea3f6b3c1a9be585d9a4baa4e9f8a166156a12f Mon Sep 17 00:00:00 2001 From: Andrei Vlad Birgaoanu Date: Thu, 16 Jan 2025 16:49:29 +0200 Subject: [PATCH 03/11] feat: add examples for all campaigns refactor: rename airstream to merkle --- airdrops/AirstreamCreator.sol | 57 --------------- airdrops/AirstreamCreator.t.sol | 40 ----------- airdrops/MerkleCreator.sol | 121 ++++++++++++++++++++++++++++++++ airdrops/MerkleCreator.t.sol | 66 +++++++++++++++++ bun.lockb | Bin 40444 -> 40084 bytes package.json | 6 +- 6 files changed, 190 insertions(+), 100 deletions(-) delete mode 100644 airdrops/AirstreamCreator.sol delete mode 100644 airdrops/AirstreamCreator.t.sol create mode 100644 airdrops/MerkleCreator.sol create mode 100644 airdrops/MerkleCreator.t.sol diff --git a/airdrops/AirstreamCreator.sol b/airdrops/AirstreamCreator.sol deleted file mode 100644 index d74b238..0000000 --- a/airdrops/AirstreamCreator.sol +++ /dev/null @@ -1,57 +0,0 @@ -// 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 { ISablierMerkleLL } from "@sablier/airdrops/src/interfaces/ISablierMerkleLL.sol"; -import { ISablierMerkleFactory } from "@sablier/airdrops/src/interfaces/ISablierMerkleFactory.sol"; -import { MerkleBase, MerkleLL } from "@sablier/airdrops/src/types/DataTypes.sol"; - -/// @notice Example of how to create an Airstream campaign with Lockup Linear. -/// @dev This code is referenced in the docs: https://docs.sablier.com/guides/lockup/examples/create-airstream -contract AirstreamCreator { - // Sepolia addresses - IERC20 public constant DAI = IERC20(0x68194a729C2450ad26072b3D33ADaCbcef39D574); - // See https://docs.sablier.com/guides/lockup/deployments for all deployments - ISablierLockup public constant LOCKUP = ISablierLockup(0xC2Da366fD67423b500cDF4712BdB41d0995b0794); - ISablierMerkleFactory public constant FACTORY = ISablierMerkleFactory(0x4ECd5A688b0365e61c1a764E8BF96A7C5dF5d35F); - - /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. - function createLLAirstream() public returns (ISablierMerkleLL merkleLL) { - // Declare the base parameters for the MerkleBase. - MerkleBase.ConstructorParams memory baseParams; - - 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 for the campaign - baseParams.shape = "A custom stream shape"; // Unique campaign name for the campaign - - // 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 too. - uint256 recipientCount = 10_000; - - // Deploy the Airstream contract. This contract will be completely owned by the campaign admin. Recipient will - // interact with the MerkleLL contract to claim their airdrop. - merkleLL = FACTORY.createMerkleLL({ - baseParams: baseParams, - lockup: LOCKUP, - cancelable: false, - transferable: true, - schedule: MerkleLL.Schedule({ - startTime: 0, // i.e. block.timestamp - startPercentage: ud2x18(0.01e18), - cliffDuration: 30 days, - cliffPercentage: ud2x18(0.01e18), - totalDuration: 90 days - }), - aggregateAmount: aggregateAmount, - recipientCount: recipientCount - }); - } -} diff --git a/airdrops/AirstreamCreator.t.sol b/airdrops/AirstreamCreator.t.sol deleted file mode 100644 index 8fc6896..0000000 --- a/airdrops/AirstreamCreator.t.sol +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: GPL-3-0-or-later -pragma solidity >=0.8.22; - -import { ISablierMerkleLL } from "@sablier/airdrops/src/interfaces/ISablierMerkleLL.sol"; -import { Test } from "forge-std/src/Test.sol"; - -import { AirstreamCreator } from "./AirstreamCreator.sol"; - -contract AirstreamCreatorTest is Test { - // Test contracts - AirstreamCreator internal airstreamCreator; - - address internal user; - - function setUp() public { - // Fork Ethereum Sepolia - vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 7_497_776 }); - - // Deploy the airstream creator - airstreamCreator = new AirstreamCreator(); - - // 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 }); - } - - // Tests creating the airstream campaign. - function test_CreateLLAirstream() public { - ISablierMerkleLL merkleLL = airstreamCreator.createLLAirstream(); - - // Assert the merkleLL contract was created with correct params - assertEq(address(0xBeeF), merkleLL.admin(), "admin"); - assertEq( - 0x4e07408562bedb8b60ce05c1decfe3ad16b722309875f562c03d02d7aaacb123, merkleLL.MERKLE_ROOT(), "merkle-root" - ); - } -} diff --git a/airdrops/MerkleCreator.sol b/airdrops/MerkleCreator.sol new file mode 100644 index 0000000..c13323e --- /dev/null +++ b/airdrops/MerkleCreator.sol @@ -0,0 +1,121 @@ +// 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/lockup/examples/create-airstream +contract MerkleCreator { + // Sepolia addresses + IERC20 public constant DAI = IERC20(0x68194a729C2450ad26072b3D33ADaCbcef39D574); + // See https://docs.sablier.com/guides/lockup/deployments for all deployments + ISablierLockup public constant LOCKUP = ISablierLockup(0xC2Da366fD67423b500cDF4712BdB41d0995b0794); + ISablierMerkleFactory public constant FACTORY = ISablierMerkleFactory(0x4ECd5A688b0365e61c1a764E8BF96A7C5dF5d35F); + + function createMerkleInstant() public virtual returns (ISablierMerkleInstant merkleInstant) { + // Declare the base parameters for the MerkleBase. + MerkleBase.ConstructorParams memory baseParams; + + // Declare 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 for the campaign + baseParams.shape = "A custom stream shape"; // Unique campaign name for the campaign + + // 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 too. + uint256 recipientCount = 10_000; + + // Deploy the MerkleInstant contract. + merkleInstant = FACTORY.createMerkleInstant(baseParams, aggregateAmount, recipientCount); + } + + function createMerkleLL() public returns (ISablierMerkleLL merkleLL) { + // Declare the base parameters for the MerkleBase. + MerkleBase.ConstructorParams memory baseParams; + + // Declare 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 for the campaign + baseParams.shape = "A custom stream shape"; // Unique campaign name for the campaign + + // 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 too. + uint256 recipientCount = 10_000; + + // Deploy the MerkleLL campaign contract. This contract will be completely owned by the campaign admin. Recipient + // will interact with the MerkleLL contract to claim their airdrop. + merkleLL = FACTORY.createMerkleLL({ + baseParams: baseParams, + lockup: LOCKUP, + cancelable: false, + transferable: true, + schedule: MerkleLL.Schedule({ + startTime: 0, // i.e. block.timestamp + startPercentage: ud2x18(0.01e18), + cliffDuration: 30 days, + cliffPercentage: ud2x18(0.01e18), + totalDuration: 90 days + }), + aggregateAmount: aggregateAmount, + recipientCount: recipientCount + }); + } + + function createMerkleLT() public returns (ISablierMerkleLT merkleLT) { + // Prepare the constructor parameters. + MerkleBase.ConstructorParams memory baseParams; + + // Declare 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 for the campaign + baseParams.shape = "A custom stream shape"; // Unique campaign name for the campaign + + // 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 too. + uint256 recipientCount = 10_000; + + // Deploy the MerkleLT contract. + merkleLT = FACTORY.createMerkleLT({ + baseParams: baseParams, + lockup: LOCKUP, + cancelable: true, + transferable: true, + streamStartTime: 0, // i.e. block.timestamp + tranchesWithPercentages: tranchesWithPercentages, + aggregateAmount: aggregateAmount, + recipientCount: recipientCount + }); + } +} diff --git a/airdrops/MerkleCreator.t.sol b/airdrops/MerkleCreator.t.sol new file mode 100644 index 0000000..e63b7a9 --- /dev/null +++ b/airdrops/MerkleCreator.t.sol @@ -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 contracts + 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 MerkleLL 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" + ); + } +} diff --git a/bun.lockb b/bun.lockb index e90a2ab45e9429916c83d68fd39eda75a4bc2b4d..d17ad9f06cb6982964016d72ed8d90fc55ad2cdd 100755 GIT binary patch delta 6487 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 delta 6517 zcmeHMdstNGnLpnEG6xy(g2E9L6cLFqz`!t!19QL|;sI}sj+baMzyKoygABr;0;8?Q zq_xK9jj28x;*(wyV`9_PG|8CQtWTbOQrjlAm-X^&ZDRZEZZxaCBxysoiTiucnQ_gt z+5WMA?H@h*JMa0u-|fA8-#OoWJQ_9q^;Od&)}re*FYc*1wef{+vF`lyU*FgD^y}4U zAHTTzE6#(q2iLU!av(4!=~v*eCwH`_%OH~E3wyhJ1HRBzDAFYyxDY7>6AS1lzo!PDu)bkETJ92`yU|gyfDcK(f9TqhvwOhnxoa0VEre1IhLM zJ$~JQ5h$UGw8r}BYt_|~AC35xj_QV zW&^H*bEtnBlc}hb4gN98+0%Hv_rW@_-XDjW^Q!rBJOwT@u2aILRG z^59JfAv;kDITO;?*B9(7@rCOZqyDT|`8Du4D8B^B?cRpuen!y`+f)qCS>{5rt!E%n zJ}0s{W)pR%^Q+^C%&p@+x=!Jw#aSxqOP|Xc5Kd`yl z(Z2SyIk7iE`46<^0S0U(t-(H@6zcbO_Ena7Aqz@@-R>k*e9QPaK zlvOd0hHxLIDDH32IPL}HOi|@sSTSx`Y^M4Yhlo-Xy4%pzK$k@4QXH}iYlm4P*O0#s zRtIL%>%UFWiK?t%l`PVAs4slvOjX5`G?c2ECM46DRJVL@vLvlW1)(?kHaRV-_%#jT zUP@7mDz};?X(7rDBgV*?rpmvBZV7bBdYgQTrm12tjpP0sa;B^DIwW|l-sibwhx{~H zlfhCQ;xiggS4Ay3C#m894dMP0MJK6Af5POdqq8Y4VI${cRo;zUtwbvmJDT)kuxhHE z>@t}w^vPtms3YeTRi2Jz(11!wB&JuJL6*l`$Ugw%_7n8bOvZ#}ixY?~S_ig5uUQX^ zj#G51Di>nKc%Vz9b7qG;0>)7ROLNF?gK=vk%u_JkIizOYvU|bms5Zrw^eCQ|@RRBC zWIB`LHf2nqGt=DiDlAJ*BgRIO4uY|jdhFzX2IJ_$fhi7C`cyhI-7U9cd2${Foty5E zp8_+gz?7tSxlA9t#pM7Q75r(Ai0@q^I%JLbd5GS;mWit%q${4!H@8EtS}J@i0YmR8ztX z`Xt9K`)3$kVKC(6J77GtG0Zz)95TdsqC>96yMvig76#*dp&jD?1{k+9X2PdnF0d3X z6V)_6OO?ZT!Eh*$A=x3n2FB*;eVIQ5WAo_R`ZD@MmQ3Ths^}nRo+^J2Z=m~7ge)XG zOsTWzOrBf5Z%%eO<(j#D)#py2bBO02m&P>DmncszTB;#E;P;RZZmgB1geGcte zl}k+pWuy3B@~fFY9nyS&ue&8yy;P!W4axP9di}av3TV)|ezD|wNT`0XxTGS+x zDSD5GIu}@I<^oN~tILAE!14D ziEKJn?xE#23w;VUlNxOv`WWn#O%u5^4tAo#LR%^{F`J&L@X&@z3t1{Pkxv^dJ!H09 z=qy+P$#xID0@iKUL?K1NI;-Gcl_rYFUj_dh@DFSODGvAtHtf(u37rQESHr(*4I90o zYWU}be_-WQ4JY?V=hfN={nfa8u(YE z2^Sr!fq%8|53H6NYvCW*saj2_G!Ayc4gcI4b`8(C;hzfsR81_VjVk=BgMVO4Nv?x` zVBK|^XrL%qXFdF@*RWUj*TcU>@DEHQWfA-X8(yS|W;zcRUJU;hYho1*Erx$f;2+o; zDp~^nzz#0a#Fyv_*ny?+Z>c8M(L+n&-!k|I<{{@Y_y;z&OcNXFI@r+$_}8F`&2+2* z{w;@pU|XnhIs5}VwOkYT(>U0PM)=pL2`@d<2>({VzZIHjrHw1#p9cTHd?ah|53F0$ zu#bp>bvD7jCQbOs-vs}fQ|YHoP1u8pq9(e&{t&&}oI33W-o?Ce;8%M5hxxZoiy?~zFHr2|z3TFB@ z3X*pS_X2!v1Gr2I@J*7Q>!u$D=1^kST&nD<8J!DN0Wc51K3M;-?1lU?@D*SL;7>h& z(0YOGKpWr#+JO#$jp5HL?=g7eQUojjiUIy0@g^k#>;iTJ{4wkYcJRhw01y22WZQYO zQ43(FCOH5nU;`=uE5N&)VSvAuL%>dezogs99>^d45}w%Wb-*$}1-w8Dz#ECp05*Ek z7GNvDn-1QfEC(6^{IZbhfO=pN@Bpv`SPXE4s{s#iIrh>2Ll6X4O3jW6n>&ww5|}1) z#>_x6T@ScMC*a8hB*Y}=f&<3^wJX52S`#bPy+BE>}DP? z8z=zsfjPijpb(e`aCSMf{LERt2jI+e$T&nCc21m|=M86*v&UKFsliz+1$ZCJ+2)4a zh_hVrF=SYFfO#B0GM{axu->Gi93AXcAk?%dGe(wb9}2 ziU`uP+dD*k}BFLLWhj9}R92kN?ZEzZ~yHi5(?K?jXGtQmW!dg}<}aOXUO6!DGxz2LGW%_G z{Q-^jE2d1DUg>YjZpCuJ>WEydtDBeEd*}sJt1_wuDR+lrkDn=ixGs17KfXSaqFacL zuLmmBvzFHR(*$5@;X)9V9@ zJ$`)nPcwQiedJb_{CD|tSnxcPIu^QD{wnm>LAex$1B=yy}uX(RPtqv0V%T%&Icb%;^A zJyek$KNRh>-~Uu;@y#Q;>v{l>Q1ebD=Lx(hF&`qw3nIk}_W4>eguAxV>a^OWSy{Aq zr!6~vit2j!o4>Zr9zTZ;Y*yHdHx~VDr(&$Hj3_Q7QCFMC&G|-X!dSJJVvhF6)6)& z5N2ExMku_y?w+z3Z5uPx_~KQ&^UCu={++&HdoiB80e?qNuswy|+udpBlhwKq{?33u zJCw$cTRZ&yojY0<#?&PNZ%e3@4{Gycr?hlnxF|XP{L1jfxfM2VtJB$fcY~66XC>v{ eHb;fcPDh6GQ(}MVux<;{o;^9IFOR%7^S=Nr(D~s2 diff --git a/package.json b/package.json index aeba0d7..10c09bf 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,9 @@ "dependencies": { "@openzeppelin/contracts": "5.0.2", "@prb/math": "4.1.0", - "@sablier/airdrops": "github:sablier-labs/airdrops#main", - "@sablier/flow": "github:sablier-labs/flow#staging", - "@sablier/v2-core": "github:sablier-labs/v2-core#staging" + "@sablier/airdrops": "github:sablier-labs/airdrops#1ad7325", + "@sablier/flow": "github:sablier-labs/flow#acbb14c", + "@sablier/v2-core": "github:sablier-labs/v2-core#076eba9" }, "homepage": "https://github.com/sablier-labs/examples#readme", "keywords": [ From 0fb4f379fba2986d53f7144ee70926eb66ef8b5c Mon Sep 17 00:00:00 2001 From: Andrei Vlad Birgaoanu Date: Thu, 16 Jan 2025 19:13:29 +0200 Subject: [PATCH 04/11] feat: add linear curves creator feat: add tranched curves creator refactor: move from dynamy to linear and tranched the correct shapes --- lockup/LockupDynamicCurvesCreator.sol | 181 +----------------- lockup/LockupDynamicCurvesCreator.t.sol | 158 +++++----------- lockup/LockupDynamicStreamCreator.sol | 2 +- lockup/LockupLinearCurvesCreator.sol | 219 ++++++++++++++++++++++ lockup/LockupLinearCurvesCreator.t.sol | 222 +++++++++++++++++++++++ lockup/LockupTranchedCurvesCreator.sol | 116 ++++++++++++ lockup/LockupTranchedCurvesCreator.t.sol | 97 ++++++++++ lockup/LockupTranchedStreamCreator.sol | 2 +- 8 files changed, 704 insertions(+), 293 deletions(-) create mode 100644 lockup/LockupLinearCurvesCreator.sol create mode 100644 lockup/LockupLinearCurvesCreator.t.sol create mode 100644 lockup/LockupTranchedCurvesCreator.sol create mode 100644 lockup/LockupTranchedCurvesCreator.t.sol diff --git a/lockup/LockupDynamicCurvesCreator.sol b/lockup/LockupDynamicCurvesCreator.sol index 4560778..4c73dcf 100644 --- a/lockup/LockupDynamicCurvesCreator.sol +++ b/lockup/LockupDynamicCurvesCreator.sol @@ -9,7 +9,7 @@ import { Broker, Lockup, LockupDynamic } from "@sablier/lockup/src/types/DataTyp /// @notice Examples of how to create Lockup Dynamic streams with different curve shapes. /// @dev A visualization of the curve shapes can be found in the docs: -/// https://docs.sablier.com/concepts/protocol/stream-types#lockup-dynamic +/// https://docs.sablier.com/concepts/lockup/stream-shapeslockup-dynamic /// Visualizing the curves while reviewing this code is recommended. The X axis will be assumed to represent "days". contract LockupDynamicCurvesCreator { // Sepolia addresses @@ -82,183 +82,4 @@ contract LockupDynamicCurvesCreator { // Create the LockupDynamic stream streamId = LOCKUP.createWithDurationsLD(params, segments); } - - function createStream_UnlockInSteps() external returns (uint256 streamId) { - // Declare the total amount as 100 DAI - uint128 totalAmount = 100e18; - - // Transfer the provided amount of DAI tokens to this contract - DAI.transferFrom(msg.sender, address(this), totalAmount); - - // Approve the Sablier contract to spend DAI - DAI.approve(address(LOCKUP), totalAmount); - - // Declare the params struct - Lockup.CreateWithDurations memory params; - - // Declare the function parameters - params.sender = msg.sender; // The sender will be able to cancel the stream - params.recipient = address(0xCAFE); // The recipient of the streamed tokens - params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees - params.token = DAI; // The streaming token - params.cancelable = true; // Whether the stream will be cancelable or not - params.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined - - // Declare a eight-size segment to match the curve shape - uint256 segmentSize = 8; - LockupDynamic.SegmentWithDuration[] memory segments = new LockupDynamic.SegmentWithDuration[](segmentSize); - - // The even segments are empty and are spaced ~25 days apart - for (uint256 i = 0; i < segmentSize; i += 2) { - segments[i] = - LockupDynamic.SegmentWithDuration({ amount: 0, duration: 25 days - 1 seconds, exponent: ud2x18(1e18) }); - } - - // The odd segments are filled and have a delta of 1 second - uint128 unlockAmount = totalAmount / uint128(segmentSize / 2); - for (uint256 i = 1; i < segmentSize; i += 2) { - segments[i] = - LockupDynamic.SegmentWithDuration({ amount: unlockAmount, duration: 1 seconds, exponent: ud2x18(1e18) }); - } - - // Create the LockupDynamic stream - streamId = LOCKUP.createWithDurationsLD(params, segments); - } - - function createStream_MonthlyUnlocks() external returns (uint256 streamId) { - // Declare the total amount as 120 DAI - uint128 totalAmount = 120e18; - - // Transfer the provided amount of DAI tokens to this contract - DAI.transferFrom(msg.sender, address(this), totalAmount); - - // Approve the Sablier contract to spend DAI - DAI.approve(address(LOCKUP), totalAmount); - - // Declare the params struct - Lockup.CreateWithDurations memory params; - - // Declare the function parameters - params.sender = msg.sender; // The sender will be able to cancel the stream - params.recipient = address(0xCAFE); // The recipient of the streamed tokens - params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees - params.token = DAI; // The streaming token - params.cancelable = true; // Whether the stream will be cancelable or not - params.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined - - // Declare a twenty four size segment to match the curve shape - uint256 segmentSize = 24; - LockupDynamic.SegmentWithDuration[] memory segments = new LockupDynamic.SegmentWithDuration[](segmentSize); - - // The even segments are empty and are spaced 30 days apart (~one month) - for (uint256 i = 0; i < segmentSize; i += 2) { - segments[i] = - LockupDynamic.SegmentWithDuration({ amount: 0, duration: 30 days - 1 seconds, exponent: ud2x18(1e18) }); - } - - // The odd segments are filled and have a delta of 1 second - uint128 unlockAmount = totalAmount / uint128(segmentSize / 2); - for (uint256 i = 1; i < segmentSize; i += 2) { - segments[i] = - LockupDynamic.SegmentWithDuration({ amount: unlockAmount, duration: 1 seconds, exponent: ud2x18(1e18) }); - } - - // Create the LockupDynamic stream - streamId = LOCKUP.createWithDurationsLD(params, segments); - } - - function createStream_Timelock() external returns (uint256 streamId) { - // Declare the total amount as 100 DAI - uint128 totalAmount = 100e18; - - // Transfer the provided amount of DAI tokens to this contract - DAI.transferFrom(msg.sender, address(this), totalAmount); - - // Approve the Sablier contract to spend DAI - DAI.approve(address(LOCKUP), totalAmount); - - // Declare the params struct - Lockup.CreateWithDurations memory params; - - // Declare the function parameters - params.sender = msg.sender; // The sender will be able to cancel the stream - params.recipient = address(0xCAFE); // The recipient of the streamed tokens - params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees - params.token = DAI; // The streaming token - params.cancelable = true; // Whether the stream will be cancelable or not - params.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined - - // Declare a two-size segment to match the curve shape - LockupDynamic.SegmentWithDuration[] memory segments = new LockupDynamic.SegmentWithDuration[](2); - segments[0] = - LockupDynamic.SegmentWithDuration({ amount: 0, duration: 90 days - 1 seconds, exponent: ud2x18(1e18) }); - segments[1] = LockupDynamic.SegmentWithDuration({ amount: 100e18, duration: 1 seconds, exponent: ud2x18(1e18) }); - - // Create the LockupDynamic stream - streamId = LOCKUP.createWithDurationsLD(params, segments); - } - - function createStream_UnlockLinear() external returns (uint256 streamId) { - // Declare the total amount as 100 DAI - uint128 totalAmount = 100e18; - - // Transfer the provided amount of DAI tokens to this contract - DAI.transferFrom(msg.sender, address(this), totalAmount); - - // Approve the Sablier contract to spend DAI - DAI.approve(address(LOCKUP), totalAmount); - - // Declare the params struct - Lockup.CreateWithDurations memory params; - - // Declare the function parameters - params.sender = msg.sender; // The sender will be able to cancel the stream - params.recipient = address(0xCAFE); // The recipient of the streamed tokens - params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees - params.token = DAI; // The streaming token - params.cancelable = true; // Whether the stream will be cancelable or not - params.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined - - // Declare a two-size segment to match the curve shape - LockupDynamic.SegmentWithDuration[] memory segments = new LockupDynamic.SegmentWithDuration[](2); - segments[0] = LockupDynamic.SegmentWithDuration({ amount: 25e18, duration: 1 seconds, exponent: ud2x18(1e18) }); - segments[1] = - LockupDynamic.SegmentWithDuration({ amount: 75e18, duration: 100 days - 1 days, exponent: ud2x18(1e18) }); - - // Create the LockupDynamic stream - streamId = LOCKUP.createWithDurationsLD(params, segments); - } - - function createStream_UnlockCliffLinear() external returns (uint256 streamId) { - // Declare the total amount as 100 DAI - uint128 totalAmount = 100e18; - - // Transfer the provided amount of DAI tokens to this contract - DAI.transferFrom(msg.sender, address(this), totalAmount); - - // Approve the Sablier contract to spend DAI - DAI.approve(address(LOCKUP), totalAmount); - - // Declare the params struct - Lockup.CreateWithDurations memory params; - - // Declare the function parameters - params.sender = msg.sender; // The sender will be able to cancel the stream - params.recipient = address(0xCAFE); // The recipient of the streamed tokens - params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees - params.token = DAI; // The streaming token - params.cancelable = true; // Whether the stream will be cancelable or not - params.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined - - // Declare a four-size segment to match the curve shape - LockupDynamic.SegmentWithDuration[] memory segments = new LockupDynamic.SegmentWithDuration[](4); - segments[0] = LockupDynamic.SegmentWithDuration({ amount: 25e18, duration: 1 seconds, exponent: ud2x18(1e18) }); - segments[1] = - LockupDynamic.SegmentWithDuration({ amount: 0, duration: 50 days - 1 seconds, exponent: ud2x18(1e18) }); - segments[2] = LockupDynamic.SegmentWithDuration({ amount: 25e18, duration: 1 seconds, exponent: ud2x18(1e18) }); - segments[3] = LockupDynamic.SegmentWithDuration({ amount: 50e18, duration: 50 days, exponent: ud2x18(1e18) }); - - // Create the LockupDynamic stream - streamId = LOCKUP.createWithDurationsLD(params, segments); - } } diff --git a/lockup/LockupDynamicCurvesCreator.t.sol b/lockup/LockupDynamicCurvesCreator.t.sol index 66ebbaa..d77e332 100644 --- a/lockup/LockupDynamicCurvesCreator.t.sol +++ b/lockup/LockupDynamicCurvesCreator.t.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-3-0-or-later pragma solidity >=0.8.22; -import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; import { Test } from "forge-std/src/Test.sol"; import { LockupDynamicCurvesCreator } from "./LockupDynamicCurvesCreator.sol"; @@ -72,132 +71,69 @@ contract LockupDynamicCurvesCreatorTest is Test { assertEq(actualStreamedAmount, expectedStreamedAmount); } - function test_CreateStream_UnlockInSteps() public { - uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); - uint256 actualStreamId = creator.createStream_UnlockInSteps(); - - // Assert that the stream has been created. - assertEq(actualStreamId, expectedStreamId); - - uint256 actualStreamedAmount; - uint256 expectedStreamedAmount; - - for (uint256 i = 0; i < 4; ++i) { - vm.warp({ newTimestamp: block.timestamp + 25 days - 1 seconds }); - actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); - assertEq(actualStreamedAmount, expectedStreamedAmount); - expectedStreamedAmount += 25e18; - vm.warp({ newTimestamp: block.timestamp + 1 seconds }); - } - } - - function test_CreateStream_MonthlyUnlocks() public { - uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); - uint256 actualStreamId = creator.createStream_MonthlyUnlocks(); - - // Assert that the stream has been created. - assertEq(actualStreamId, expectedStreamId); - - uint256 actualStreamedAmount; - uint256 expectedStreamedAmount; - - for (uint256 i = 0; i < 12; ++i) { - vm.warp({ newTimestamp: block.timestamp + 30 days - 1 seconds }); - actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); - - assertEq(actualStreamedAmount, expectedStreamedAmount); - expectedStreamedAmount += 10e18; - vm.warp({ newTimestamp: block.timestamp + 1 seconds }); - } - } - - function test_CreateStream_Timelock() external { - uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); - uint256 actualStreamId = creator.createStream_Timelock(); - - // Assert that the stream has been created. - assertEq(actualStreamId, expectedStreamId); - - uint256 blockTimestamp = block.timestamp; - - // Warp 90 days - 1 second into the future, i.e. exactly 1 second before unlock. - vm.warp({ newTimestamp: blockTimestamp + 90 days - 1 seconds }); - - uint128 actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); - uint128 expectedStreamedAmount = 0; - assertEq(actualStreamedAmount, expectedStreamedAmount); - - // Warp 90 days into the future, i.e. the unlock moment. - vm.warp({ newTimestamp: blockTimestamp + 90 days }); + // function test_CreateStream_UnlockLinear() external { + // uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); + // uint256 actualStreamId = creator.createStream_UnlockLinear(); - actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); - expectedStreamedAmount = 100e18; - assertEq(actualStreamedAmount, expectedStreamedAmount); - } - - function test_CreateStream_UnlockLinear() external { - uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); - uint256 actualStreamId = creator.createStream_UnlockLinear(); - - // Assert that the stream has been created. - assertEq(actualStreamId, expectedStreamId); + // // Assert that the stream has been created. + // assertEq(actualStreamId, expectedStreamId); - uint256 blockTimestamp = block.timestamp; + // uint256 blockTimestamp = block.timestamp; - // Warp 1 second into the future, i.e. the initial unlock. - vm.warp({ newTimestamp: blockTimestamp + 1 seconds }); + // // Warp 1 second into the future, i.e. the initial unlock. + // vm.warp({ newTimestamp: blockTimestamp + 1 seconds }); - uint128 actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); - uint128 expectedStreamedAmount = 25e18; - assertEq(actualStreamedAmount, expectedStreamedAmount); + // uint128 actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); + // uint128 expectedStreamedAmount = 25e18; + // assertEq(actualStreamedAmount, expectedStreamedAmount); - // Warp 50 days into the second segment (4320000 seconds). - vm.warp({ newTimestamp: blockTimestamp + 50 days + 1 seconds }); + // // Warp 50 days into the second segment (4320000 seconds). + // vm.warp({ newTimestamp: blockTimestamp + 50 days + 1 seconds }); - // total duration of segment: 8639999 seconds (100 days - 1 second) - // amount to be stream in the current segment: 75e18 + // // total duration of segment: 8639999 seconds (100 days - 1 second) + // // amount to be stream in the current segment: 75e18 - actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); - expectedStreamedAmount = 62.87878787878787875e18; // (0.500000057870377068)^{1} * 75 + 25 - assertEq(actualStreamedAmount, expectedStreamedAmount); - } + // actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); + // expectedStreamedAmount = 62.87878787878787875e18; // (0.500000057870377068)^{1} * 75 + 25 + // assertEq(actualStreamedAmount, expectedStreamedAmount); + // } - function test_CreateStream_UnlockCliffLinear() external { - uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); - uint256 actualStreamId = creator.createStream_UnlockCliffLinear(); + // function test_CreateStream_UnlockCliffLinear() external { + // uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); + // uint256 actualStreamId = creator.createStream_UnlockCliffLinear(); - // Assert that the stream has been created. - assertEq(actualStreamId, expectedStreamId); + // // Assert that the stream has been created. + // assertEq(actualStreamId, expectedStreamId); - uint256 blockTimestamp = block.timestamp; + // uint256 blockTimestamp = block.timestamp; - // Warp 1 second into the future, i.e. the initial unlock. - vm.warp({ newTimestamp: blockTimestamp + 1 seconds }); + // // Warp 1 second into the future, i.e. the initial unlock. + // vm.warp({ newTimestamp: blockTimestamp + 1 seconds }); - uint128 actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); - uint128 expectedStreamedAmount = 25e18; - assertEq(actualStreamedAmount, expectedStreamedAmount); + // uint128 actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); + // uint128 expectedStreamedAmount = 25e18; + // assertEq(actualStreamedAmount, expectedStreamedAmount); - // Warp 50 days into the future. - vm.warp({ newTimestamp: blockTimestamp + 50 days }); + // // Warp 50 days into the future. + // vm.warp({ newTimestamp: blockTimestamp + 50 days }); - // Assert that the streamed amount has remained the same. - actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); - assertEq(actualStreamedAmount, expectedStreamedAmount); + // // Assert that the streamed amount has remained the same. + // actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); + // assertEq(actualStreamedAmount, expectedStreamedAmount); - // Warp 50 days plus a second into the future, i.e. after the cliff unlock. - vm.warp({ newTimestamp: blockTimestamp + 50 days + 1 seconds }); + // // Warp 50 days plus a second into the future, i.e. after the cliff unlock. + // vm.warp({ newTimestamp: blockTimestamp + 50 days + 1 seconds }); - // Assert that the streamed amount has increased by the cliff amount. - actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); - expectedStreamedAmount = 50e18; - assertEq(actualStreamedAmount, expectedStreamedAmount); + // // Assert that the streamed amount has increased by the cliff amount. + // actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); + // expectedStreamedAmount = 50e18; + // assertEq(actualStreamedAmount, expectedStreamedAmount); - // Warp 75 days plus a second into the future. - vm.warp({ newTimestamp: blockTimestamp + 75 days + 1 }); + // // Warp 75 days plus a second into the future. + // vm.warp({ newTimestamp: blockTimestamp + 75 days + 1 }); - actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); - expectedStreamedAmount = 75e18; // (0.50)^{1} * 50 + 50 - assertEq(actualStreamedAmount, expectedStreamedAmount); - } + // actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); + // expectedStreamedAmount = 75e18; // (0.50)^{1} * 50 + 50 + // assertEq(actualStreamedAmount, expectedStreamedAmount); + // } } diff --git a/lockup/LockupDynamicStreamCreator.sol b/lockup/LockupDynamicStreamCreator.sol index ffe1584..a9151c6 100644 --- a/lockup/LockupDynamicStreamCreator.sol +++ b/lockup/LockupDynamicStreamCreator.sol @@ -11,7 +11,7 @@ import { Broker, Lockup, LockupDynamic } from "@sablier/lockup/src/types/DataTyp /// @dev This code is referenced in the docs: /// https://docs.sablier.com/guides/lockup/examples/create-stream/lockup-dynamic contract LockupDynamicStreamCreator { - // sepolia addresses + // Sepolia addresses IERC20 public constant DAI = IERC20(0x68194a729C2450ad26072b3D33ADaCbcef39D574); ISablierLockup public constant LOCKUP = ISablierLockup(0xC2Da366fD67423b500cDF4712BdB41d0995b0794); diff --git a/lockup/LockupLinearCurvesCreator.sol b/lockup/LockupLinearCurvesCreator.sol new file mode 100644 index 0000000..989e8d9 --- /dev/null +++ b/lockup/LockupLinearCurvesCreator.sol @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ud60x18 } from "@prb/math/src/UD60x18.sol"; +import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; +import { Broker, Lockup, LockupLinear } from "@sablier/lockup/src/types/DataTypes.sol"; + +/// @notice Examples of how to create Lockup Linear streams with different curve shapes. +/// @dev A visualization of the curve shapes can be found in the docs: +/// https://docs.sablier.com/concepts/lockup/stream-shapeslockup-linear +/// Visualizing the curves while reviewing this code is recommended. The X axis will be assumed to represent "days". +contract LockupLinearCurvesCreator { + // Sepolia addresses + IERC20 public constant DAI = IERC20(0x68194a729C2450ad26072b3D33ADaCbcef39D574); + ISablierLockup public constant LOCKUP = ISablierLockup(0xC2Da366fD67423b500cDF4712BdB41d0995b0794); + + /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. + function createStream_Linear() public returns (uint256 streamId) { + // Declare the total amount as 100 DAI + uint128 totalAmount = 100e18; + + // Transfer the provided amount of DAI tokens to this contract + DAI.transferFrom(msg.sender, address(this), totalAmount); + + // Approve the Sablier contract to spend DAI + DAI.approve(address(LOCKUP), totalAmount); + + // Declare the params struct + Lockup.CreateWithDurations memory params; + + // Declare the function parameters + params.sender = msg.sender; // The sender will be able to cancel the stream + params.recipient = address(0xCAFE); // The recipient of the streamed tokens + params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees + params.token = DAI; // The streaming token + params.cancelable = true; // Whether the stream will be cancelable or not + params.transferable = true; // Whether the stream will be transferable or not + params.broker = Broker(address(0), ud60x18(0)); // Optional parameter for charging a fee + + LockupLinear.UnlockAmounts memory unlockAmounts = LockupLinear.UnlockAmounts({ start: 0, cliff: 0 }); + LockupLinear.Durations memory durations = LockupLinear.Durations({ + cliff: 0, // Setting a cliff of 0 + total: 100 days // Setting a total duration of 100 days + }); + + // Create the LockupLinear stream using a function that sets the start time to `block.timestamp` + streamId = LOCKUP.createWithDurationsLL(params, unlockAmounts, durations); + } + + /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. + function createStream_Cliff() public returns (uint256 streamId) { + // Declare the total amount as 100 DAI + uint128 totalAmount = 100e18; + + // Transfer the provided amount of DAI tokens to this contract + DAI.transferFrom(msg.sender, address(this), totalAmount); + + // Approve the Sablier contract to spend DAI + DAI.approve(address(LOCKUP), totalAmount); + + // Declare the params struct + Lockup.CreateWithDurations memory params; + + // Declare the function parameters + params.sender = msg.sender; // The sender will be able to cancel the stream + params.recipient = address(0xCAFE); // The recipient of the streamed tokens + params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees + params.token = DAI; // The streaming token + params.cancelable = true; // Whether the stream will be cancelable or not + params.transferable = true; // Whether the stream will be transferable or not + params.broker = Broker(address(0), ud60x18(0)); // Optional parameter for charging a fee + + // Setting a cliff unlock amount of 25 DAI + LockupLinear.UnlockAmounts memory unlockAmounts = LockupLinear.UnlockAmounts({ start: 0, cliff: 25e18 }); + LockupLinear.Durations memory durations = LockupLinear.Durations({ + cliff: 25 days, // Setting a cliff of 25 days + total: 100 days // Setting a total duration of 100 days + }); + + // Create the LockupLinear stream using a function that sets the start time to `block.timestamp` + streamId = LOCKUP.createWithDurationsLL(params, unlockAmounts, durations); + } + + /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. + function createStream_UnlockLinear() public returns (uint256 streamId) { + // Declare the total amount as 100 DAI + uint128 totalAmount = 100e18; + + // Transfer the provided amount of DAI tokens to this contract + DAI.transferFrom(msg.sender, address(this), totalAmount); + + // Approve the Sablier contract to spend DAI + DAI.approve(address(LOCKUP), totalAmount); + + // Declare the params struct + Lockup.CreateWithDurations memory params; + + // Declare the function parameters + params.sender = msg.sender; // The sender will be able to cancel the stream + params.recipient = address(0xCAFE); // The recipient of the streamed tokens + params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees + params.token = DAI; // The streaming token + params.cancelable = true; // Whether the stream will be cancelable or not + params.transferable = true; // Whether the stream will be transferable or not + params.broker = Broker(address(0), ud60x18(0)); // Optional parameter for charging a fee + + // Setting an initial unlock amount of 25 DAI + LockupLinear.UnlockAmounts memory unlockAmounts = LockupLinear.UnlockAmounts({ start: 25e18, cliff: 0 }); + LockupLinear.Durations memory durations = LockupLinear.Durations({ + cliff: 0, // Setting a cliff of 0 + total: 100 days // Setting a total duration of 100 days + }); + + // Create the LockupLinear stream using a function that sets the start time to `block.timestamp` + streamId = LOCKUP.createWithDurationsLL(params, unlockAmounts, durations); + } + + /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. + function createStream_UnlockCliff() public returns (uint256 streamId) { + // Declare the total amount as 100 DAI + uint128 totalAmount = 100e18; + + // Transfer the provided amount of DAI tokens to this contract + DAI.transferFrom(msg.sender, address(this), totalAmount); + + // Approve the Sablier contract to spend DAI + DAI.approve(address(LOCKUP), totalAmount); + + // Declare the params struct + Lockup.CreateWithDurations memory params; + + // Declare the function parameters + params.sender = msg.sender; // The sender will be able to cancel the stream + params.recipient = address(0xCAFE); // The recipient of the streamed tokens + params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees + params.token = DAI; // The streaming token + params.cancelable = true; // Whether the stream will be cancelable or not + params.transferable = true; // Whether the stream will be transferable or not + params.broker = Broker(address(0), ud60x18(0)); // Optional parameter for charging a fee + + // Setting an initial and a cliff unlock amount of 25 DAI + LockupLinear.UnlockAmounts memory unlockAmounts = LockupLinear.UnlockAmounts({ start: 25e18, cliff: 25e18 }); + LockupLinear.Durations memory durations = LockupLinear.Durations({ + cliff: 50 days, // Setting a cliff of 50 days + total: 100 days // Setting a total duration of 100 days + }); + + // Create the LockupLinear stream using a function that sets the start time to `block.timestamp` + streamId = LOCKUP.createWithDurationsLL(params, unlockAmounts, durations); + } + + /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. + function createStream_ConstantLinear() public returns (uint256 streamId) { + // Declare the total amount as 100 DAI + uint128 totalAmount = 100e18; + + // Transfer the provided amount of DAI tokens to this contract + DAI.transferFrom(msg.sender, address(this), totalAmount); + + // Approve the Sablier contract to spend DAI + DAI.approve(address(LOCKUP), totalAmount); + + // Declare the params struct + Lockup.CreateWithDurations memory params; + + // Declare the function parameters + params.sender = msg.sender; // The sender will be able to cancel the stream + params.recipient = address(0xCAFE); // The recipient of the streamed tokens + params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees + params.token = DAI; // The streaming token + params.cancelable = true; // Whether the stream will be cancelable or not + params.transferable = true; // Whether the stream will be transferable or not + params.broker = Broker(address(0), ud60x18(0)); // Optional parameter for charging a fee + + LockupLinear.UnlockAmounts memory unlockAmounts = LockupLinear.UnlockAmounts({ start: 0, cliff: 0 }); + LockupLinear.Durations memory durations = LockupLinear.Durations({ + cliff: 25 days, // Setting a cliff of 25 days + total: 100 days // Setting a total duration of 100 days + }); + + // Create the LockupLinear stream using a function that sets the start time to `block.timestamp` + streamId = LOCKUP.createWithDurationsLL(params, unlockAmounts, durations); + } + + /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. + function createStream_UnlockConstant() public returns (uint256 streamId) { + // Declare the total amount as 100 DAI + uint128 totalAmount = 100e18; + + // Transfer the provided amount of DAI tokens to this contract + DAI.transferFrom(msg.sender, address(this), totalAmount); + + // Approve the Sablier contract to spend DAI + DAI.approve(address(LOCKUP), totalAmount); + + // Declare the params struct + Lockup.CreateWithDurations memory params; + + // Declare the function parameters + params.sender = msg.sender; // The sender will be able to cancel the stream + params.recipient = address(0xCAFE); // The recipient of the streamed tokens + params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees + params.token = DAI; // The streaming token + params.cancelable = true; // Whether the stream will be cancelable or not + params.transferable = true; // Whether the stream will be transferable or not + params.broker = Broker(address(0), ud60x18(0)); // Optional parameter for charging a fee + + // Setting an initial unlock amount of 25 DAI + LockupLinear.UnlockAmounts memory unlockAmounts = LockupLinear.UnlockAmounts({ start: 25e18, cliff: 0 }); + LockupLinear.Durations memory durations = LockupLinear.Durations({ + cliff: 25 days, // Setting a cliff of 25 days + total: 100 days // Setting a total duration of 100 days + }); + + // Create the LockupLinear stream using a function that sets the start time to `block.timestamp` + streamId = LOCKUP.createWithDurationsLL(params, unlockAmounts, durations); + } +} diff --git a/lockup/LockupLinearCurvesCreator.t.sol b/lockup/LockupLinearCurvesCreator.t.sol new file mode 100644 index 0000000..c2462e1 --- /dev/null +++ b/lockup/LockupLinearCurvesCreator.t.sol @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: GPL-3-0-or-later +pragma solidity >=0.8.22; + +import { Test } from "forge-std/src/Test.sol"; + +import { LockupLinearCurvesCreator } from "./LockupLinearCurvesCreator.sol"; + +contract LockupLinearCurvesCreatorTest is Test { + // Test contracts + LockupLinearCurvesCreator internal creator; + + address internal user; + + function setUp() public { + // Fork Ethereum Sepolia + vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 7_497_776 }); + + // Deploy the stream creator + creator = new LockupLinearCurvesCreator(); + + // Create a test user + user = payable(makeAddr("User")); + vm.deal({ account: user, newBalance: 1 ether }); + + // Mint some DAI tokens to the test user, which will be pulled by the creator contract + deal({ token: address(creator.DAI()), to: user, give: 1337e18 }); + + // Make the test user the `msg.sender` in all following calls + vm.startPrank({ msgSender: user }); + + // Approve the creator contract to pull DAI tokens from the test user + creator.DAI().approve({ spender: address(creator), value: 1337e18 }); + } + + function test_CreateStream_Linear() external { + uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); + uint256 actualStreamId = creator.createStream_Linear(); + + // Assert that the stream has been created. + assertEq(actualStreamId, expectedStreamId, "streamId"); + + uint256 blockTimestamp = block.timestamp; + + // Assert that the amount is zero at start. + uint128 actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); + uint128 expectedStreamedAmount = 0; + assertEq(actualStreamedAmount, expectedStreamedAmount); + + // Warp 20 days into the future. + vm.warp({ newTimestamp: blockTimestamp + 20 days }); + + // Assert that the streamed amount has linearly increased. + actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); + expectedStreamedAmount = 20e18; + assertEq(actualStreamedAmount, expectedStreamedAmount); + + // Warp 50 days into the future. + vm.warp({ newTimestamp: blockTimestamp + 50 days }); + + // Assert that the streamed amount has linearly increased. + actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); + expectedStreamedAmount = 50e18; + assertEq(actualStreamedAmount, expectedStreamedAmount); + } + + function test_CreateStream_Cliff() external { + uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); + uint256 actualStreamId = creator.createStream_Cliff(); + + // Assert that the stream has been created. + assertEq(actualStreamId, expectedStreamId); + + uint256 blockTimestamp = block.timestamp; + + // Warp a second before the cliff. + vm.warp({ newTimestamp: blockTimestamp + 25 days - 1 seconds }); + + uint128 actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); + uint128 expectedStreamedAmount = 0; + assertEq(actualStreamedAmount, expectedStreamedAmount); + + // Warp to the cliff time. + vm.warp({ newTimestamp: blockTimestamp + 25 days }); + + actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); + expectedStreamedAmount = 25e18; + assertEq(actualStreamedAmount, expectedStreamedAmount, "cliff unlock amount"); + + // Warp to the halfway point of the stream duration. + vm.warp({ newTimestamp: blockTimestamp + 50 days }); + + // Assert that the streamed amount has linearly increased. + actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); + expectedStreamedAmount = 50e18; + assertApproxEqAbs(actualStreamedAmount, expectedStreamedAmount, 100, "cliff unlock amount + linear streaming"); + } + + function test_CreateStream_UnlockLinear() external { + uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); + uint256 actualStreamId = creator.createStream_UnlockLinear(); + + // Assert that the stream has been created. + assertEq(actualStreamId, expectedStreamId); + + uint256 blockTimestamp = block.timestamp; + + // Warp 1 second into the future, i.e. the initial unlock. + vm.warp({ newTimestamp: blockTimestamp }); + + uint128 actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); + uint128 expectedStreamedAmount = 25e18; + assertEq(actualStreamedAmount, expectedStreamedAmount); + + // Warp to the halfway point of the stream duration. + vm.warp({ newTimestamp: blockTimestamp + 50 days }); + + actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); + expectedStreamedAmount = 62.5e18; // 0.50 * 75 + 25 + assertEq(actualStreamedAmount, expectedStreamedAmount); + } + + function test_CreateStream_UnlockCliff() external { + uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); + uint256 actualStreamId = creator.createStream_UnlockCliff(); + + // Assert that the stream has been created. + assertEq(actualStreamId, expectedStreamId); + + uint256 blockTimestamp = block.timestamp; + + // Assert that the streamed amount is the initial unlock amount. + uint128 actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); + uint128 expectedStreamedAmount = 25e18; + assertEq(actualStreamedAmount, expectedStreamedAmount, "initial unlock"); + + // Warp 1 second before the cliff time. + vm.warp({ newTimestamp: blockTimestamp + 50 days - 1 }); + + // Assert that the streamed amount has remained the same. + actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); + assertEq(actualStreamedAmount, expectedStreamedAmount, "1 second before cliff"); + + // Warp to the cliff time. + vm.warp({ newTimestamp: blockTimestamp + 50 days }); + + // Assert that the streamed amount has unlocked the cliff amount. + actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); + expectedStreamedAmount = 50e18; + assertEq(actualStreamedAmount, expectedStreamedAmount, "cliff unlock amount"); + + // Warp 75 days into the future. + vm.warp({ newTimestamp: blockTimestamp + 75 days }); + + // Assert that the streamed amount has increased linearly after cliff. + actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); + expectedStreamedAmount = 75e18; + assertApproxEqAbs(actualStreamedAmount, expectedStreamedAmount, 100, "linear streaming after cliff"); + } + + function test_CreateStream_ConstantLinear() external { + uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); + uint256 actualStreamId = creator.createStream_ConstantLinear(); + + // Assert that the stream has been created. + assertEq(actualStreamId, expectedStreamId); + + uint256 blockTimestamp = block.timestamp; + + // Warp 1 second into the future. + vm.warp({ newTimestamp: blockTimestamp + 1 seconds }); + + // Assert that the streamed amount is zero. + uint128 actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); + uint128 expectedStreamedAmount = 0; + assertEq(actualStreamedAmount, expectedStreamedAmount, "not zero"); + + // Warp 1 second before the cliff time. + vm.warp({ newTimestamp: blockTimestamp + 25 days - 1 }); + + // Assert that the streamed amount has remained the same. + actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); + assertEq(actualStreamedAmount, expectedStreamedAmount, "1 second before cliff"); + + // Warp to 62.5 days into the future. + vm.warp({ newTimestamp: blockTimestamp + 62.5 days }); + + // Assert that the streamed amount has linearly increased. + actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); + expectedStreamedAmount = 50e18; + assertEq(actualStreamedAmount, expectedStreamedAmount, "linear streaming"); + } + + function test_CreateStream_UnlockConstant() external { + uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); + uint256 actualStreamId = creator.createStream_UnlockConstant(); + + // Assert that the stream has been created. + assertEq(actualStreamId, expectedStreamId); + + uint256 blockTimestamp = block.timestamp; + + // Assert that the streamed amount is zero. + uint128 actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); + uint128 expectedStreamedAmount = 25e18; + assertEq(actualStreamedAmount, expectedStreamedAmount, "initial unlock"); + + // Warp 1 second before the cliff time. + vm.warp({ newTimestamp: blockTimestamp + 25 days - 1 }); + + // Assert that the streamed amount has remained the same. + actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); + assertEq(actualStreamedAmount, expectedStreamedAmount, "1 second before cliff"); + + // Warp to 75 days into the future. + vm.warp({ newTimestamp: blockTimestamp + 62.5 days }); + + // Assert that the streamed amount has linearly increased. + actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); + expectedStreamedAmount = 62.5e18; + assertEq(actualStreamedAmount, expectedStreamedAmount, "linear streaming"); + } +} diff --git a/lockup/LockupTranchedCurvesCreator.sol b/lockup/LockupTranchedCurvesCreator.sol new file mode 100644 index 0000000..a4e2628 --- /dev/null +++ b/lockup/LockupTranchedCurvesCreator.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ud60x18 } from "@prb/math/src/UD60x18.sol"; +import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; +import { Broker, Lockup, LockupTranched } from "@sablier/lockup/src/types/DataTypes.sol"; + +/// @notice Examples of how to create Lockup Linear streams with different curve shapes. +/// @dev A visualization of the curve shapes can be found in the docs: +/// https://docs.sablier.com/concepts/lockup/stream-shapeslockup-tranched +/// Visualizing the curves while reviewing this code is recommended. The X axis will be assumed to represent "days". +contract LockupTranchedCurvesCreator { + // Sepolia addresses + IERC20 public constant DAI = IERC20(0x68194a729C2450ad26072b3D33ADaCbcef39D574); + ISablierLockup public constant LOCKUP = ISablierLockup(0xC2Da366fD67423b500cDF4712BdB41d0995b0794); + + function createStream_UnlockInSteps() external returns (uint256 streamId) { + // Declare the total amount as 100 DAI + uint128 totalAmount = 100e18; + + // Transfer the provided amount of DAI tokens to this contract + DAI.transferFrom(msg.sender, address(this), totalAmount); + + // Approve the Sablier contract to spend DAI + DAI.approve(address(LOCKUP), totalAmount); + + // Declare the params struct + Lockup.CreateWithDurations memory params; + + // Declare the function parameters + params.sender = msg.sender; // The sender will be able to cancel the stream + params.recipient = address(0xCAFE); // The recipient of the streamed tokens + params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees + params.token = DAI; // The streaming token + params.cancelable = true; // Whether the stream will be cancelable or not + params.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined + + // Declare a four-size tranche to match the curve shape + uint256 trancheSize = 4; + LockupTranched.TrancheWithDuration[] memory tranches = new LockupTranched.TrancheWithDuration[](trancheSize); + + // The tranches are filled with the same amount and are spaced 25 days apart + uint128 unlockAmount = uint128(totalAmount / trancheSize); + for (uint256 i = 0; i < trancheSize; ++i) { + tranches[i] = LockupTranched.TrancheWithDuration({ amount: unlockAmount, duration: 25 days }); + } + + // Create the LockupTranche stream + streamId = LOCKUP.createWithDurationsLT(params, tranches); + } + + function createStream_MonthlyUnlocks() external returns (uint256 streamId) { + // Declare the total amount as 120 DAI + uint128 totalAmount = 120e18; + + // Transfer the provided amount of DAI tokens to this contract + DAI.transferFrom(msg.sender, address(this), totalAmount); + + // Approve the Sablier contract to spend DAI + DAI.approve(address(LOCKUP), totalAmount); + + // Declare the params struct + Lockup.CreateWithDurations memory params; + + // Declare the function parameters + params.sender = msg.sender; // The sender will be able to cancel the stream + params.recipient = address(0xCAFE); // The recipient of the streamed tokens + params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees + params.token = DAI; // The streaming token + params.cancelable = true; // Whether the stream will be cancelable or not + params.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined + + // Declare a twenty four size tranche to match the curve shape + uint256 trancheSize = 12; + LockupTranched.TrancheWithDuration[] memory tranches = new LockupTranched.TrancheWithDuration[](trancheSize); + + // The tranches are spaced 30 days apart (~one month) + uint128 unlockAmount = uint128(totalAmount / trancheSize); + for (uint256 i = 0; i < trancheSize; ++i) { + tranches[i] = LockupTranched.TrancheWithDuration({ amount: unlockAmount, duration: 30 days }); + } + + // Create the LockupTranche stream + streamId = LOCKUP.createWithDurationsLT(params, tranches); + } + + function createStream_Timelock() external returns (uint256 streamId) { + // Declare the total amount as 100 DAI + uint128 totalAmount = 100e18; + + // Transfer the provided amount of DAI tokens to this contract + DAI.transferFrom(msg.sender, address(this), totalAmount); + + // Approve the Sablier contract to spend DAI + DAI.approve(address(LOCKUP), totalAmount); + + // Declare the params struct + Lockup.CreateWithDurations memory params; + + // Declare the function parameters + params.sender = msg.sender; // The sender will be able to cancel the stream + params.recipient = address(0xCAFE); // The recipient of the streamed tokens + params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees + params.token = DAI; // The streaming token + params.cancelable = true; // Whether the stream will be cancelable or not + params.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined + + // Declare a two-size tranche to match the curve shape + LockupTranched.TrancheWithDuration[] memory tranches = new LockupTranched.TrancheWithDuration[](1); + tranches[0] = LockupTranched.TrancheWithDuration({ amount: 100e18, duration: 90 days }); + + // Create the LockupTranche stream + streamId = LOCKUP.createWithDurationsLT(params, tranches); + } +} diff --git a/lockup/LockupTranchedCurvesCreator.t.sol b/lockup/LockupTranchedCurvesCreator.t.sol new file mode 100644 index 0000000..a074e8c --- /dev/null +++ b/lockup/LockupTranchedCurvesCreator.t.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-3-0-or-later +pragma solidity >=0.8.22; + +import { Test } from "forge-std/src/Test.sol"; + +import { LockupTranchedCurvesCreator } from "./LockupTranchedCurvesCreator.sol"; + +contract LockupTranchedCurvesCreatorTest is Test { + // Test contracts + LockupTranchedCurvesCreator internal creator; + + address internal user; + + function setUp() public { + // Fork Ethereum Sepolia + vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 7_497_776 }); + + // Deploy the stream creator + creator = new LockupTranchedCurvesCreator(); + + // Create a test user + user = payable(makeAddr("User")); + vm.deal({ account: user, newBalance: 1 ether }); + + // Mint some DAI tokens to the test user, which will be pulled by the creator contract + deal({ token: address(creator.DAI()), to: user, give: 1337e18 }); + + // Make the test user the `msg.sender` in all following calls + vm.startPrank({ msgSender: user }); + + // Approve the creator contract to pull DAI tokens from the test user + creator.DAI().approve({ spender: address(creator), value: 1337e18 }); + } + + function test_CreateStream_UnlockInSteps() public { + uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); + uint256 actualStreamId = creator.createStream_UnlockInSteps(); + + // Assert that the stream has been created. + assertEq(actualStreamId, expectedStreamId); + + uint256 actualStreamedAmount; + uint256 expectedStreamedAmount; + + for (uint256 i = 0; i < 4; ++i) { + vm.warp({ newTimestamp: block.timestamp + 25 days - 1 seconds }); + actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); + assertEq(actualStreamedAmount, expectedStreamedAmount); + expectedStreamedAmount += 25e18; + vm.warp({ newTimestamp: block.timestamp + 1 seconds }); + } + } + + function test_CreateStream_MonthlyUnlocks() public { + uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); + uint256 actualStreamId = creator.createStream_MonthlyUnlocks(); + + // Assert that the stream has been created. + assertEq(actualStreamId, expectedStreamId); + + uint256 actualStreamedAmount; + uint256 expectedStreamedAmount; + + for (uint256 i = 0; i < 12; ++i) { + vm.warp({ newTimestamp: block.timestamp + 30 days - 1 seconds }); + actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); + + assertEq(actualStreamedAmount, expectedStreamedAmount); + expectedStreamedAmount += 10e18; + vm.warp({ newTimestamp: block.timestamp + 1 seconds }); + } + } + + function test_CreateStream_Timelock() external { + uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); + uint256 actualStreamId = creator.createStream_Timelock(); + + // Assert that the stream has been created. + assertEq(actualStreamId, expectedStreamId); + + uint256 blockTimestamp = block.timestamp; + + // Warp 90 days - 1 second into the future, i.e. exactly 1 second before unlock. + vm.warp({ newTimestamp: blockTimestamp + 90 days - 1 seconds }); + + uint128 actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); + uint128 expectedStreamedAmount = 0; + assertEq(actualStreamedAmount, expectedStreamedAmount); + + // Warp 90 days into the future, i.e. the unlock moment. + vm.warp({ newTimestamp: blockTimestamp + 90 days }); + + actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); + expectedStreamedAmount = 100e18; + assertEq(actualStreamedAmount, expectedStreamedAmount); + } +} diff --git a/lockup/LockupTranchedStreamCreator.sol b/lockup/LockupTranchedStreamCreator.sol index d9103e4..a0c21ee 100644 --- a/lockup/LockupTranchedStreamCreator.sol +++ b/lockup/LockupTranchedStreamCreator.sol @@ -10,7 +10,7 @@ import { Broker, Lockup, LockupTranched } from "@sablier/lockup/src/types/DataTy /// @dev This code is referenced in the docs: /// https://docs.sablier.com/guides/lockup/examples/create-stream/lockup-tranched contract LockupTranchedStreamCreator { - // sepolia addresses + // Sepolia addresses IERC20 public constant DAI = IERC20(0x68194a729C2450ad26072b3D33ADaCbcef39D574); ISablierLockup public constant LOCKUP = ISablierLockup(0xC2Da366fD67423b500cDF4712BdB41d0995b0794); From 6b505bb521b3b59b2ada5c1dd433892f837c9601 Mon Sep 17 00:00:00 2001 From: Andrei Vlad Birgaoanu Date: Thu, 16 Jan 2025 19:21:42 +0200 Subject: [PATCH 05/11] remove commented lines --- lockup/LockupDynamicCurvesCreator.t.sol | 66 ------------------------- 1 file changed, 66 deletions(-) diff --git a/lockup/LockupDynamicCurvesCreator.t.sol b/lockup/LockupDynamicCurvesCreator.t.sol index d77e332..9d0e578 100644 --- a/lockup/LockupDynamicCurvesCreator.t.sol +++ b/lockup/LockupDynamicCurvesCreator.t.sol @@ -70,70 +70,4 @@ contract LockupDynamicCurvesCreatorTest is Test { expectedStreamedAmount = 21.25e18; // 0.5^{6} * 80 + 20 assertEq(actualStreamedAmount, expectedStreamedAmount); } - - // function test_CreateStream_UnlockLinear() external { - // uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); - // uint256 actualStreamId = creator.createStream_UnlockLinear(); - - // // Assert that the stream has been created. - // assertEq(actualStreamId, expectedStreamId); - - // uint256 blockTimestamp = block.timestamp; - - // // Warp 1 second into the future, i.e. the initial unlock. - // vm.warp({ newTimestamp: blockTimestamp + 1 seconds }); - - // uint128 actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); - // uint128 expectedStreamedAmount = 25e18; - // assertEq(actualStreamedAmount, expectedStreamedAmount); - - // // Warp 50 days into the second segment (4320000 seconds). - // vm.warp({ newTimestamp: blockTimestamp + 50 days + 1 seconds }); - - // // total duration of segment: 8639999 seconds (100 days - 1 second) - // // amount to be stream in the current segment: 75e18 - - // actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); - // expectedStreamedAmount = 62.87878787878787875e18; // (0.500000057870377068)^{1} * 75 + 25 - // assertEq(actualStreamedAmount, expectedStreamedAmount); - // } - - // function test_CreateStream_UnlockCliffLinear() external { - // uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); - // uint256 actualStreamId = creator.createStream_UnlockCliffLinear(); - - // // Assert that the stream has been created. - // assertEq(actualStreamId, expectedStreamId); - - // uint256 blockTimestamp = block.timestamp; - - // // Warp 1 second into the future, i.e. the initial unlock. - // vm.warp({ newTimestamp: blockTimestamp + 1 seconds }); - - // uint128 actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); - // uint128 expectedStreamedAmount = 25e18; - // assertEq(actualStreamedAmount, expectedStreamedAmount); - - // // Warp 50 days into the future. - // vm.warp({ newTimestamp: blockTimestamp + 50 days }); - - // // Assert that the streamed amount has remained the same. - // actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); - // assertEq(actualStreamedAmount, expectedStreamedAmount); - - // // Warp 50 days plus a second into the future, i.e. after the cliff unlock. - // vm.warp({ newTimestamp: blockTimestamp + 50 days + 1 seconds }); - - // // Assert that the streamed amount has increased by the cliff amount. - // actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); - // expectedStreamedAmount = 50e18; - // assertEq(actualStreamedAmount, expectedStreamedAmount); - - // // Warp 75 days plus a second into the future. - // vm.warp({ newTimestamp: blockTimestamp + 75 days + 1 }); - - // actualStreamedAmount = creator.LOCKUP().streamedAmountOf(actualStreamId); - // expectedStreamedAmount = 75e18; // (0.50)^{1} * 50 + 50 - // assertEq(actualStreamedAmount, expectedStreamedAmount); - // } } From 3982ddeaa1d2d12789b98291dd0901fba3e5d1af Mon Sep 17 00:00:00 2001 From: smol-ninja Date: Mon, 20 Jan 2025 18:29:27 +0000 Subject: [PATCH 06/11] style: fix solhint warnings docs: polish comments --- .solhint.json | 2 +- airdrops/MerkleCreator.sol | 41 ++++++++++++++------------ airdrops/MerkleCreator.t.sol | 4 +-- lockup/BatchLLStreamCreator.sol | 2 +- lockup/LockupDynamicCurvesCreator.sol | 11 +++---- lockup/LockupLinearCurvesCreator.sol | 27 +++++++++-------- lockup/LockupTranchedCurvesCreator.sol | 14 ++++----- package.json | 2 +- 8 files changed, 54 insertions(+), 49 deletions(-) diff --git a/.solhint.json b/.solhint.json index 7620360..9e29cfe 100644 --- a/.solhint.json +++ b/.solhint.json @@ -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", diff --git a/airdrops/MerkleCreator.sol b/airdrops/MerkleCreator.sol index c13323e..cce00be 100644 --- a/airdrops/MerkleCreator.sol +++ b/airdrops/MerkleCreator.sol @@ -15,54 +15,56 @@ import { MerkleBase, MerkleLL, MerkleLT } from "@sablier/airdrops/src/types/Data contract MerkleCreator { // Sepolia addresses IERC20 public constant DAI = IERC20(0x68194a729C2450ad26072b3D33ADaCbcef39D574); + // See https://docs.sablier.com/guides/lockup/deployments for all deployments - ISablierLockup public constant LOCKUP = ISablierLockup(0xC2Da366fD67423b500cDF4712BdB41d0995b0794); ISablierMerkleFactory public constant FACTORY = ISablierMerkleFactory(0x4ECd5A688b0365e61c1a764E8BF96A7C5dF5d35F); + ISablierLockup public constant LOCKUP = ISablierLockup(0xC2Da366fD67423b500cDF4712BdB41d0995b0794); function createMerkleInstant() public virtual returns (ISablierMerkleInstant merkleInstant) { - // Declare the base parameters for the MerkleBase. + // Declare the constructor parameter of MerkleBase. MerkleBase.ConstructorParams memory baseParams; - // Declare the base parameters. + // 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 for the campaign - baseParams.shape = "A custom stream shape"; // Unique campaign name for the campaign + 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 too. + // The total number of addresses you want to airdrop your tokens to. uint256 recipientCount = 10_000; - // Deploy the MerkleInstant contract. + // 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 base parameters for the MerkleBase. + // Declare the constructor parameter of MerkleBase. MerkleBase.ConstructorParams memory baseParams; - // Declare the base parameters. + // 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 for the campaign - baseParams.shape = "A custom stream shape"; // Unique campaign name for the campaign + 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 too. + // The total number of addresses you want to airdrop your tokens to. uint256 recipientCount = 10_000; - // Deploy the MerkleLL campaign contract. This contract will be completely owned by the campaign admin. Recipient - // will interact with the MerkleLL contract to claim their airdrop. + // 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, @@ -84,14 +86,14 @@ contract MerkleCreator { // Prepare the constructor parameters. MerkleBase.ConstructorParams memory baseParams; - // Declare the base parameters. + // 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 for the campaign - baseParams.shape = "A custom stream shape"; // Unique campaign name for the campaign + 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); @@ -103,10 +105,11 @@ contract MerkleCreator { // 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 too. + // The total number of addresses you want to airdrop your tokens to. uint256 recipientCount = 10_000; - // Deploy the MerkleLT contract. + // 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, diff --git a/airdrops/MerkleCreator.t.sol b/airdrops/MerkleCreator.t.sol index e63b7a9..9251411 100644 --- a/airdrops/MerkleCreator.t.sol +++ b/airdrops/MerkleCreator.t.sol @@ -9,7 +9,7 @@ import { Test } from "forge-std/src/Test.sol"; import { MerkleCreator } from "./MerkleCreator.sol"; contract MerkleCreatorTest is Test { - // Test contracts + // Test contract MerkleCreator internal merkleCreator; address internal user; @@ -29,7 +29,7 @@ contract MerkleCreatorTest is Test { vm.startPrank({ msgSender: user }); } - // Test creating the MerkleLL campaign. + // Test creating the MerkleInstant campaign. function test_CreateMerkleInstant() public { ISablierMerkleInstant merkleInstant = merkleCreator.createMerkleInstant(); diff --git a/lockup/BatchLLStreamCreator.sol b/lockup/BatchLLStreamCreator.sol index 7e1ba02..3b5095c 100644 --- a/lockup/BatchLLStreamCreator.sol +++ b/lockup/BatchLLStreamCreator.sol @@ -4,7 +4,7 @@ pragma solidity >=0.8.22; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ud60x18 } from "@prb/math/src/UD60x18.sol"; import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol"; -import { Broker, Lockup, LockupLinear } from "@sablier/lockup/src/types/DataTypes.sol"; +import { Broker, LockupLinear } from "@sablier/lockup/src/types/DataTypes.sol"; import { ISablierBatchLockup } from "@sablier/lockup/src/interfaces/ISablierBatchLockup.sol"; import { BatchLockup } from "@sablier/lockup/src/types/DataTypes.sol"; diff --git a/lockup/LockupDynamicCurvesCreator.sol b/lockup/LockupDynamicCurvesCreator.sol index 4c73dcf..d87ff95 100644 --- a/lockup/LockupDynamicCurvesCreator.sol +++ b/lockup/LockupDynamicCurvesCreator.sol @@ -9,7 +9,7 @@ import { Broker, Lockup, LockupDynamic } from "@sablier/lockup/src/types/DataTyp /// @notice Examples of how to create Lockup Dynamic streams with different curve shapes. /// @dev A visualization of the curve shapes can be found in the docs: -/// https://docs.sablier.com/concepts/lockup/stream-shapeslockup-dynamic +/// https://docs.sablier.com/concepts/lockup/stream-shapes#lockup-dynamic /// Visualizing the curves while reviewing this code is recommended. The X axis will be assumed to represent "days". contract LockupDynamicCurvesCreator { // Sepolia addresses @@ -37,7 +37,7 @@ contract LockupDynamicCurvesCreator { params.token = DAI; // The streaming token params.cancelable = true; // Whether the stream will be cancelable or not params.transferable = true; // Whether the stream will be transferable or not - params.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined + params.broker = Broker(address(0), ud60x18(0)); // Optional broker fee // Declare a single-size segment to match the curve shape LockupDynamic.SegmentWithDuration[] memory segments = new LockupDynamic.SegmentWithDuration[](1); @@ -47,10 +47,11 @@ contract LockupDynamicCurvesCreator { exponent: ud2x18(6e18) }); - // Create the LockupDynamic stream + // Create the Lockup stream using dynamic model with exponential shape streamId = LOCKUP.createWithDurationsLD(params, segments); } + /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. function createStream_ExponentialCliff() external returns (uint256 streamId) { // Declare the total amount as 100 DAI uint128 totalAmount = 100e18; @@ -70,7 +71,7 @@ contract LockupDynamicCurvesCreator { params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees params.token = DAI; // The streaming token params.cancelable = true; // Whether the stream will be cancelable or not - params.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined + params.broker = Broker(address(0), ud60x18(0)); // Optional broker fee // Declare a three-size segment to match the curve shape LockupDynamic.SegmentWithDuration[] memory segments = new LockupDynamic.SegmentWithDuration[](3); @@ -79,7 +80,7 @@ contract LockupDynamicCurvesCreator { segments[1] = LockupDynamic.SegmentWithDuration({ amount: 20e18, duration: 1 seconds, exponent: ud2x18(1e18) }); segments[2] = LockupDynamic.SegmentWithDuration({ amount: 80e18, duration: 50 days, exponent: ud2x18(6e18) }); - // Create the LockupDynamic stream + // Create the Lockup stream using dynamic model with exponential cliff shape streamId = LOCKUP.createWithDurationsLD(params, segments); } } diff --git a/lockup/LockupLinearCurvesCreator.sol b/lockup/LockupLinearCurvesCreator.sol index 989e8d9..53a9d1c 100644 --- a/lockup/LockupLinearCurvesCreator.sol +++ b/lockup/LockupLinearCurvesCreator.sol @@ -8,7 +8,7 @@ import { Broker, Lockup, LockupLinear } from "@sablier/lockup/src/types/DataType /// @notice Examples of how to create Lockup Linear streams with different curve shapes. /// @dev A visualization of the curve shapes can be found in the docs: -/// https://docs.sablier.com/concepts/lockup/stream-shapeslockup-linear +/// https://docs.sablier.com/concepts/lockup/stream-shapes#lockup-linear /// Visualizing the curves while reviewing this code is recommended. The X axis will be assumed to represent "days". contract LockupLinearCurvesCreator { // Sepolia addresses @@ -36,7 +36,7 @@ contract LockupLinearCurvesCreator { params.token = DAI; // The streaming token params.cancelable = true; // Whether the stream will be cancelable or not params.transferable = true; // Whether the stream will be transferable or not - params.broker = Broker(address(0), ud60x18(0)); // Optional parameter for charging a fee + params.broker = Broker(address(0), ud60x18(0)); // Optional broker fee LockupLinear.UnlockAmounts memory unlockAmounts = LockupLinear.UnlockAmounts({ start: 0, cliff: 0 }); LockupLinear.Durations memory durations = LockupLinear.Durations({ @@ -44,7 +44,7 @@ contract LockupLinearCurvesCreator { total: 100 days // Setting a total duration of 100 days }); - // Create the LockupLinear stream using a function that sets the start time to `block.timestamp` + // Create the Lockup stream with Linear shape, no cliff and start time as `block.timestamp` streamId = LOCKUP.createWithDurationsLL(params, unlockAmounts, durations); } @@ -69,7 +69,7 @@ contract LockupLinearCurvesCreator { params.token = DAI; // The streaming token params.cancelable = true; // Whether the stream will be cancelable or not params.transferable = true; // Whether the stream will be transferable or not - params.broker = Broker(address(0), ud60x18(0)); // Optional parameter for charging a fee + params.broker = Broker(address(0), ud60x18(0)); // Optional broker fee // Setting a cliff unlock amount of 25 DAI LockupLinear.UnlockAmounts memory unlockAmounts = LockupLinear.UnlockAmounts({ start: 0, cliff: 25e18 }); @@ -78,7 +78,7 @@ contract LockupLinearCurvesCreator { total: 100 days // Setting a total duration of 100 days }); - // Create the LockupLinear stream using a function that sets the start time to `block.timestamp` + // Create the Lockup stream with Linear shape, a cliff and start time as `block.timestamp` streamId = LOCKUP.createWithDurationsLL(params, unlockAmounts, durations); } @@ -103,7 +103,7 @@ contract LockupLinearCurvesCreator { params.token = DAI; // The streaming token params.cancelable = true; // Whether the stream will be cancelable or not params.transferable = true; // Whether the stream will be transferable or not - params.broker = Broker(address(0), ud60x18(0)); // Optional parameter for charging a fee + params.broker = Broker(address(0), ud60x18(0)); // Optional broker fee // Setting an initial unlock amount of 25 DAI LockupLinear.UnlockAmounts memory unlockAmounts = LockupLinear.UnlockAmounts({ start: 25e18, cliff: 0 }); @@ -112,7 +112,7 @@ contract LockupLinearCurvesCreator { total: 100 days // Setting a total duration of 100 days }); - // Create the LockupLinear stream using a function that sets the start time to `block.timestamp` + // Create the Lockup stream with Linear shape, an initial unlock, no cliff and start time as `block.timestamp` streamId = LOCKUP.createWithDurationsLL(params, unlockAmounts, durations); } @@ -137,7 +137,7 @@ contract LockupLinearCurvesCreator { params.token = DAI; // The streaming token params.cancelable = true; // Whether the stream will be cancelable or not params.transferable = true; // Whether the stream will be transferable or not - params.broker = Broker(address(0), ud60x18(0)); // Optional parameter for charging a fee + params.broker = Broker(address(0), ud60x18(0)); // Optional broker fee // Setting an initial and a cliff unlock amount of 25 DAI LockupLinear.UnlockAmounts memory unlockAmounts = LockupLinear.UnlockAmounts({ start: 25e18, cliff: 25e18 }); @@ -146,7 +146,7 @@ contract LockupLinearCurvesCreator { total: 100 days // Setting a total duration of 100 days }); - // Create the LockupLinear stream using a function that sets the start time to `block.timestamp` + // Create the Lockup stream with Linear shape, an initial unlock, a cliff and start time as `block.timestamp` streamId = LOCKUP.createWithDurationsLL(params, unlockAmounts, durations); } @@ -171,7 +171,7 @@ contract LockupLinearCurvesCreator { params.token = DAI; // The streaming token params.cancelable = true; // Whether the stream will be cancelable or not params.transferable = true; // Whether the stream will be transferable or not - params.broker = Broker(address(0), ud60x18(0)); // Optional parameter for charging a fee + params.broker = Broker(address(0), ud60x18(0)); // Optional broker fee LockupLinear.UnlockAmounts memory unlockAmounts = LockupLinear.UnlockAmounts({ start: 0, cliff: 0 }); LockupLinear.Durations memory durations = LockupLinear.Durations({ @@ -179,7 +179,7 @@ contract LockupLinearCurvesCreator { total: 100 days // Setting a total duration of 100 days }); - // Create the LockupLinear stream using a function that sets the start time to `block.timestamp` + // Create the Lockup stream with Linear shape, zero unlock until cliff and start time as `block.timestamp` streamId = LOCKUP.createWithDurationsLL(params, unlockAmounts, durations); } @@ -204,7 +204,7 @@ contract LockupLinearCurvesCreator { params.token = DAI; // The streaming token params.cancelable = true; // Whether the stream will be cancelable or not params.transferable = true; // Whether the stream will be transferable or not - params.broker = Broker(address(0), ud60x18(0)); // Optional parameter for charging a fee + params.broker = Broker(address(0), ud60x18(0)); // Optional broker fee // Setting an initial unlock amount of 25 DAI LockupLinear.UnlockAmounts memory unlockAmounts = LockupLinear.UnlockAmounts({ start: 25e18, cliff: 0 }); @@ -213,7 +213,8 @@ contract LockupLinearCurvesCreator { total: 100 days // Setting a total duration of 100 days }); - // Create the LockupLinear stream using a function that sets the start time to `block.timestamp` + // Create the Lockup stream with Linear shape, an initial unlock followed by zero unlock until cliff and start + // time as `block.timestamp` streamId = LOCKUP.createWithDurationsLL(params, unlockAmounts, durations); } } diff --git a/lockup/LockupTranchedCurvesCreator.sol b/lockup/LockupTranchedCurvesCreator.sol index a4e2628..ae97371 100644 --- a/lockup/LockupTranchedCurvesCreator.sol +++ b/lockup/LockupTranchedCurvesCreator.sol @@ -8,7 +8,7 @@ import { Broker, Lockup, LockupTranched } from "@sablier/lockup/src/types/DataTy /// @notice Examples of how to create Lockup Linear streams with different curve shapes. /// @dev A visualization of the curve shapes can be found in the docs: -/// https://docs.sablier.com/concepts/lockup/stream-shapeslockup-tranched +/// https://docs.sablier.com/concepts/lockup/stream-shapes#lockup-tranched /// Visualizing the curves while reviewing this code is recommended. The X axis will be assumed to represent "days". contract LockupTranchedCurvesCreator { // Sepolia addresses @@ -34,7 +34,7 @@ contract LockupTranchedCurvesCreator { params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees params.token = DAI; // The streaming token params.cancelable = true; // Whether the stream will be cancelable or not - params.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined + params.broker = Broker(address(0), ud60x18(0)); // Optional broker fee // Declare a four-size tranche to match the curve shape uint256 trancheSize = 4; @@ -46,7 +46,7 @@ contract LockupTranchedCurvesCreator { tranches[i] = LockupTranched.TrancheWithDuration({ amount: unlockAmount, duration: 25 days }); } - // Create the LockupTranche stream + // Create the Lockup stream using tranche model with periodic unlocks in step streamId = LOCKUP.createWithDurationsLT(params, tranches); } @@ -69,7 +69,7 @@ contract LockupTranchedCurvesCreator { params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees params.token = DAI; // The streaming token params.cancelable = true; // Whether the stream will be cancelable or not - params.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined + params.broker = Broker(address(0), ud60x18(0)); // Optional broker fee // Declare a twenty four size tranche to match the curve shape uint256 trancheSize = 12; @@ -81,7 +81,7 @@ contract LockupTranchedCurvesCreator { tranches[i] = LockupTranched.TrancheWithDuration({ amount: unlockAmount, duration: 30 days }); } - // Create the LockupTranche stream + // Create the Lockup stream using tranche model with web2 style monthly unlocks streamId = LOCKUP.createWithDurationsLT(params, tranches); } @@ -104,13 +104,13 @@ contract LockupTranchedCurvesCreator { params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees params.token = DAI; // The streaming token params.cancelable = true; // Whether the stream will be cancelable or not - params.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined + params.broker = Broker(address(0), ud60x18(0)); // Optional broker fee // Declare a two-size tranche to match the curve shape LockupTranched.TrancheWithDuration[] memory tranches = new LockupTranched.TrancheWithDuration[](1); tranches[0] = LockupTranched.TrancheWithDuration({ amount: 100e18, duration: 90 days }); - // Create the LockupTranche stream + // Create the Lockup stream using tranche model with full unlock only at the end streamId = LOCKUP.createWithDurationsLT(params, tranches); } } diff --git a/package.json b/package.json index 10c09bf..ae5b038 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "build:lockup": "FOUNDRY_PROFILE=lockup forge build", "clean": "rm -rf cache out", "lint": "bun run lint:sol && bun run prettier:check", - "lint:sol": "forge fmt . && bun solhint \"{flow,lockup}/**/*.sol\"", + "lint:sol": "forge fmt . && bun solhint \"{airdrops,flow,lockup}/**/*.sol\"", "prettier:check": "prettier --check \"**/*.{md,yml}\"", "prettier:write": "prettier --write \"**/*.{md,yml}\"", "test": "bun run test:airdrops && bun run test:flow && bun run test:lockup", From 5328ac37df387e53061d39782cb563b0946e6755 Mon Sep 17 00:00:00 2001 From: Andrei Vlad Birgaoanu Date: Tue, 21 Jan 2025 17:59:42 +0200 Subject: [PATCH 07/11] refactor: rename functions --- airdrops/MerkleCreator.sol | 17 ++++++++++------- lockup/LockupLinearCurvesCreator.sol | 10 +++++----- lockup/LockupLinearCurvesCreator.t.sol | 20 ++++++++++---------- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/airdrops/MerkleCreator.sol b/airdrops/MerkleCreator.sol index cce00be..b62294d 100644 --- a/airdrops/MerkleCreator.sol +++ b/airdrops/MerkleCreator.sol @@ -63,6 +63,15 @@ contract MerkleCreator { // 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({ @@ -70,13 +79,7 @@ contract MerkleCreator { lockup: LOCKUP, cancelable: false, transferable: true, - schedule: MerkleLL.Schedule({ - startTime: 0, // i.e. block.timestamp - startPercentage: ud2x18(0.01e18), - cliffDuration: 30 days, - cliffPercentage: ud2x18(0.01e18), - totalDuration: 90 days - }), + schedule: schedule, aggregateAmount: aggregateAmount, recipientCount: recipientCount }); diff --git a/lockup/LockupLinearCurvesCreator.sol b/lockup/LockupLinearCurvesCreator.sol index 53a9d1c..a79d1ab 100644 --- a/lockup/LockupLinearCurvesCreator.sol +++ b/lockup/LockupLinearCurvesCreator.sol @@ -49,7 +49,7 @@ contract LockupLinearCurvesCreator { } /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. - function createStream_Cliff() public returns (uint256 streamId) { + function createStream_CliffUnlock() public returns (uint256 streamId) { // Declare the total amount as 100 DAI uint128 totalAmount = 100e18; @@ -83,7 +83,7 @@ contract LockupLinearCurvesCreator { } /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. - function createStream_UnlockLinear() public returns (uint256 streamId) { + function createStream_InitialUnlock() public returns (uint256 streamId) { // Declare the total amount as 100 DAI uint128 totalAmount = 100e18; @@ -117,7 +117,7 @@ contract LockupLinearCurvesCreator { } /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. - function createStream_UnlockCliff() public returns (uint256 streamId) { + function createStream_InitialCliffUnlock() public returns (uint256 streamId) { // Declare the total amount as 100 DAI uint128 totalAmount = 100e18; @@ -151,7 +151,7 @@ contract LockupLinearCurvesCreator { } /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. - function createStream_ConstantLinear() public returns (uint256 streamId) { + function createStream_ConstantCliff() public returns (uint256 streamId) { // Declare the total amount as 100 DAI uint128 totalAmount = 100e18; @@ -184,7 +184,7 @@ contract LockupLinearCurvesCreator { } /// @dev For this function to work, the sender must have approved this dummy contract to spend DAI. - function createStream_UnlockConstant() public returns (uint256 streamId) { + function createStream_InitialUnlockConstantCliff() public returns (uint256 streamId) { // Declare the total amount as 100 DAI uint128 totalAmount = 100e18; diff --git a/lockup/LockupLinearCurvesCreator.t.sol b/lockup/LockupLinearCurvesCreator.t.sol index c2462e1..70f0bae 100644 --- a/lockup/LockupLinearCurvesCreator.t.sol +++ b/lockup/LockupLinearCurvesCreator.t.sol @@ -63,9 +63,9 @@ contract LockupLinearCurvesCreatorTest is Test { assertEq(actualStreamedAmount, expectedStreamedAmount); } - function test_CreateStream_Cliff() external { + function test_CreateStream_CliffUnlock() external { uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); - uint256 actualStreamId = creator.createStream_Cliff(); + uint256 actualStreamId = creator.createStream_CliffUnlock(); // Assert that the stream has been created. assertEq(actualStreamId, expectedStreamId); @@ -95,9 +95,9 @@ contract LockupLinearCurvesCreatorTest is Test { assertApproxEqAbs(actualStreamedAmount, expectedStreamedAmount, 100, "cliff unlock amount + linear streaming"); } - function test_CreateStream_UnlockLinear() external { + function test_CreateStream_InitialUnlock() external { uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); - uint256 actualStreamId = creator.createStream_UnlockLinear(); + uint256 actualStreamId = creator.createStream_InitialUnlock(); // Assert that the stream has been created. assertEq(actualStreamId, expectedStreamId); @@ -119,9 +119,9 @@ contract LockupLinearCurvesCreatorTest is Test { assertEq(actualStreamedAmount, expectedStreamedAmount); } - function test_CreateStream_UnlockCliff() external { + function test_CreateStream_InitialCliffUnlock() external { uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); - uint256 actualStreamId = creator.createStream_UnlockCliff(); + uint256 actualStreamId = creator.createStream_InitialCliffUnlock(); // Assert that the stream has been created. assertEq(actualStreamId, expectedStreamId); @@ -157,9 +157,9 @@ contract LockupLinearCurvesCreatorTest is Test { assertApproxEqAbs(actualStreamedAmount, expectedStreamedAmount, 100, "linear streaming after cliff"); } - function test_CreateStream_ConstantLinear() external { + function test_CreateStream_ConstantCliff() external { uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); - uint256 actualStreamId = creator.createStream_ConstantLinear(); + uint256 actualStreamId = creator.createStream_ConstantCliff(); // Assert that the stream has been created. assertEq(actualStreamId, expectedStreamId); @@ -190,9 +190,9 @@ contract LockupLinearCurvesCreatorTest is Test { assertEq(actualStreamedAmount, expectedStreamedAmount, "linear streaming"); } - function test_CreateStream_UnlockConstant() external { + function test_CreateStream_InitialUnlockConstantCliff() external { uint256 expectedStreamId = creator.LOCKUP().nextStreamId(); - uint256 actualStreamId = creator.createStream_UnlockConstant(); + uint256 actualStreamId = creator.createStream_InitialUnlockConstantCliff(); // Assert that the stream has been created. assertEq(actualStreamId, expectedStreamId); From 7fc2ce84677e3c3bd2120d1b883b33bdc16e847c Mon Sep 17 00:00:00 2001 From: smol-ninja Date: Sun, 26 Jan 2025 19:27:13 +0000 Subject: [PATCH 08/11] fix docs --- airdrops/MerkleCreator.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airdrops/MerkleCreator.sol b/airdrops/MerkleCreator.sol index b62294d..c3a37b6 100644 --- a/airdrops/MerkleCreator.sol +++ b/airdrops/MerkleCreator.sol @@ -11,7 +11,7 @@ import { ISablierMerkleFactory } from "@sablier/airdrops/src/interfaces/ISablier 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/lockup/examples/create-airstream +/// @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); From 77f1a650d32ac1a004f891b320b13e89bdb8f252 Mon Sep 17 00:00:00 2001 From: smol-ninja Date: Mon, 27 Jan 2025 09:45:41 +0000 Subject: [PATCH 09/11] remove reference of stream type from staking contract --- lockup/StakeSablierNFT.sol | 3 --- 1 file changed, 3 deletions(-) diff --git a/lockup/StakeSablierNFT.sol b/lockup/StakeSablierNFT.sol index d29bffa..b8ecfa7 100644 --- a/lockup/StakeSablierNFT.sol +++ b/lockup/StakeSablierNFT.sol @@ -21,7 +21,6 @@ import { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.so /// https://github.com/Synthetixio/synthetix/blob/develop/contracts/StakingRewards.sol. /// /// Assumptions: -/// - The staking contract supports only one type of stream at a time, either Lockup Dynamic or Lockup Linear. /// - The Sablier NFT must be transferable because staking requires transferring the NFT to the staking contract. /// - This staking contract assumes that one user can only stake one NFT at a time. contract StakeSablierNFT is Adminable, ERC721Holder, ISablierLockupRecipient { @@ -70,8 +69,6 @@ contract StakeSablierNFT is Adminable, ERC721Holder, ISablierLockupRecipient { uint256 public rewardsDuration; /// @dev This should be the Sablier Lockup contract. - /// - If you used Lockup Linear, you should use the LockupLinear contract address. - /// - If you used Lockup Dynamic, you should use the LockupDynamic contract address. ISablierLockup public sablierLockup; /// @dev The staked stream IDs mapped by user addresses. From 0062f75a48ebf36bab00985cca22806432395e87 Mon Sep 17 00:00:00 2001 From: Andrei Vlad Birgaoanu Date: Thu, 30 Jan 2025 02:18:43 +0200 Subject: [PATCH 10/11] build: bump sablier deps --- bun.lockb | Bin 40084 -> 39958 bytes package.json | 8 ++++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bun.lockb b/bun.lockb index d17ad9f06cb6982964016d72ed8d90fc55ad2cdd..643fb79dbe4364c2368599bdd066b7eb3cdf535d 100755 GIT binary patch delta 5724 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 delta 5937 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 F{{W5+mAe1{ diff --git a/package.json b/package.json index ae5b038..2f8c097 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "examples", - "description": "Solidity examples for on-chain interaction with Sablier V2", + "description": "Solidity examples for on-chain interaction with Sablier Protocols", "version": "1.0.0", "author": { "name": "Sablier Labs Ltd", @@ -17,9 +17,9 @@ "dependencies": { "@openzeppelin/contracts": "5.0.2", "@prb/math": "4.1.0", - "@sablier/airdrops": "github:sablier-labs/airdrops#1ad7325", - "@sablier/flow": "github:sablier-labs/flow#acbb14c", - "@sablier/v2-core": "github:sablier-labs/v2-core#076eba9" + "@sablier/airdrops": "1.3.0", + "@sablier/flow": "1.1.0", + "@sablier/lockup": "2.0.0" }, "homepage": "https://github.com/sablier-labs/examples#readme", "keywords": [ From dc85aa63f1b17c0a2615b16b5648c6abfacefd01 Mon Sep 17 00:00:00 2001 From: Andrei Vlad Birgaoanu Date: Sun, 2 Feb 2025 16:58:43 +0200 Subject: [PATCH 11/11] add unlock amounts in batch LL --- lockup/BatchLLStreamCreator.sol | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lockup/BatchLLStreamCreator.sol b/lockup/BatchLLStreamCreator.sol index 3b5095c..48b747b 100644 --- a/lockup/BatchLLStreamCreator.sol +++ b/lockup/BatchLLStreamCreator.sol @@ -37,9 +37,13 @@ contract BatchLLStreamCreator { stream0.cancelable = true; // Whether the stream will be cancelable or not stream0.transferable = false; // Whether the recipient can transfer the NFT or not stream0.durations = LockupLinear.Durations({ - cliff: 4 weeks, // Tokens will be unlocked only after 4 weeks + cliff: 4 weeks, // Tokens will start streaming continuously after 4 weeks total: 52 weeks // Setting a total duration of ~1 year }); + stream0.unlockAmounts = LockupLinear.UnlockAmounts({ + start: 0, // Whether the stream will unlock a certain amount of tokens at the start time + cliff: 0 // Whether the stream will unlock a certain amount of tokens at the cliff time + }); stream0.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined // Declare the second stream in the batch @@ -50,9 +54,13 @@ contract BatchLLStreamCreator { stream1.cancelable = false; // Whether the stream will be cancelable or not stream1.transferable = false; // Whether the recipient can transfer the NFT or not stream1.durations = LockupLinear.Durations({ - cliff: 1 weeks, // Tokens will be unlocked only after 1 week + cliff: 1 weeks, // Tokens will start streaming continuously after 4 weeks total: 26 weeks // Setting a total duration of ~6 months }); + stream1.unlockAmounts = LockupLinear.UnlockAmounts({ + start: 0, // Whether the stream will unlock a certain amount of tokens at the start time + cliff: 0 // Whether the stream will unlock a certain amount of tokens at the start time + }); stream1.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined // Fill the batch param