From 817f6fba9bfa5b67cc998db9acd24a12ee1b35c5 Mon Sep 17 00:00:00 2001 From: smol-ninja Date: Tue, 30 Jan 2024 18:29:36 +0530 Subject: [PATCH] feat: add `name`, introduces struct `ConstructorParams` (#262) * feat: add `name` string parameter in MerkleStreamer contract constructor refactor: use struct `CreateWithLockupLinear` in Merkle streamer constructor * test: name should not exceed 32 bytes * refactor: use an struct for MerkleStreamer constructor parameters * test: rename defaults function * refactor: rename custom error chore: small rewording changes refactor: update import * refactor: add constructor params in event test: update tests accordingly chore: remove unused imports * refactor: use internal for NAME * refactor: rename params to baseParams when referring to ConstructorParams struct * build: update bun lockfile --------- Co-authored-by: andreivladbrg Co-authored-by: Paul Razvan Berg --- bun.lockb | Bin 43329 -> 43329 bytes script/CreateMerkleStreamerLL.s.sol | 17 ++--- src/SablierV2MerkleStreamerFactory.sol | 47 ++++--------- src/SablierV2MerkleStreamerLL.sol | 12 ++-- src/abstracts/SablierV2MerkleStreamer.sol | 43 ++++++++---- src/interfaces/ISablierV2MerkleStreamer.sol | 3 + .../ISablierV2MerkleStreamerFactory.sol | 28 ++------ src/libraries/Errors.sol | 3 + src/types/DataTypes.sol | 21 ++++++ test/Base.t.sol | 19 ++--- .../merkle-streamer/MerkleStreamerLL.t.sol | 22 +++--- .../merkle-streamer/MerkleStreamer.t.sol | 7 +- .../createMerkleStreamerLL.t.sol | 66 +++++++++++++----- .../createMerkleStreamerLL.tree | 13 ++-- .../ll/constructor/constructor.t.sol | 18 +++-- test/utils/Defaults.sol | 28 +++++++- test/utils/Events.sol | 13 ++-- 17 files changed, 196 insertions(+), 164 deletions(-) diff --git a/bun.lockb b/bun.lockb index 2ba27d49b45464e71bbc676d57ea0a703b1d0c64..c0526ba6953ea0e9ad57efbb655de3d65d5c4c78 100755 GIT binary patch delta 66 zcmV-I0KNag(gMNK0+22szhWX+#y+AdA`^;hIz1?nVb5;tNXa{89TMX#n73$au};T_ Y2Vym1I5;$9lTnBqlaQzcvpk3eA)yxb%7 delta 66 zcmV-I0KNag(gMNK0+22s->6=>+;t2hmB7-@PIP+blEdLn^x8wkt7P~Mltxy8u};T_ Y2W4S4W-vK7lTnBqlaQzcvpk3eA%LeJrvLx| diff --git a/script/CreateMerkleStreamerLL.s.sol b/script/CreateMerkleStreamerLL.s.sol index fb8612c0..be5cd898 100644 --- a/script/CreateMerkleStreamerLL.s.sol +++ b/script/CreateMerkleStreamerLL.s.sol @@ -1,24 +1,20 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity >=0.8.22 <0.9.0; -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 { BaseScript } from "./Base.s.sol"; import { ISablierV2MerkleStreamerFactory } from "../src/interfaces/ISablierV2MerkleStreamerFactory.sol"; import { ISablierV2MerkleStreamerLL } from "../src/interfaces/ISablierV2MerkleStreamerLL.sol"; +import { MerkleStreamer } from "../src/types/DataTypes.sol"; contract CreateMerkleStreamerLL is BaseScript { struct Params { - address initialAdmin; + MerkleStreamer.ConstructorParams baseParams; ISablierV2LockupLinear lockupLinear; - IERC20 asset; - bytes32 merkleRoot; - uint40 expiration; LockupLinear.Durations streamDurations; - bool cancelable; - bool transferable; string ipfsCID; uint256 campaignTotalAmount; uint256 recipientsCount; @@ -33,14 +29,9 @@ contract CreateMerkleStreamerLL is BaseScript { returns (ISablierV2MerkleStreamerLL merkleStreamerLL) { merkleStreamerLL = merkleStreamerFactory.createMerkleStreamerLL( - params.initialAdmin, + params.baseParams, params.lockupLinear, - params.asset, - params.merkleRoot, - params.expiration, params.streamDurations, - params.cancelable, - params.transferable, params.ipfsCID, params.campaignTotalAmount, params.recipientsCount diff --git a/src/SablierV2MerkleStreamerFactory.sol b/src/SablierV2MerkleStreamerFactory.sol index 9efdd7a5..7c3097a0 100644 --- a/src/SablierV2MerkleStreamerFactory.sol +++ b/src/SablierV2MerkleStreamerFactory.sol @@ -1,13 +1,13 @@ // 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 { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; +import { SablierV2MerkleStreamerLL } from "./SablierV2MerkleStreamerLL.sol"; import { ISablierV2MerkleStreamerFactory } from "./interfaces/ISablierV2MerkleStreamerFactory.sol"; import { ISablierV2MerkleStreamerLL } from "./interfaces/ISablierV2MerkleStreamerLL.sol"; -import { SablierV2MerkleStreamerLL } from "./SablierV2MerkleStreamerLL.sol"; +import { MerkleStreamer } from "./types/DataTypes.sol"; /// @title SablierV2MerkleStreamerFactory /// @notice See the documentation in {ISablierV2MerkleStreamerFactory}. @@ -18,14 +18,9 @@ contract SablierV2MerkleStreamerFactory is ISablierV2MerkleStreamerFactory { /// @notice inheritdoc ISablierV2MerkleStreamerFactory function createMerkleStreamerLL( - address initialAdmin, + MerkleStreamer.ConstructorParams memory baseParams, ISablierV2LockupLinear lockupLinear, - IERC20 asset, - bytes32 merkleRoot, - uint40 expiration, LockupLinear.Durations memory streamDurations, - bool cancelable, - bool transferable, string memory ipfsCID, uint256 aggregateAmount, uint256 recipientsCount @@ -36,36 +31,24 @@ contract SablierV2MerkleStreamerFactory is ISablierV2MerkleStreamerFactory { // Hash the parameters to generate a salt. bytes32 salt = keccak256( abi.encodePacked( - initialAdmin, + baseParams.initialAdmin, + baseParams.asset, + bytes32(abi.encodePacked(baseParams.name)), + baseParams.merkleRoot, + baseParams.expiration, + baseParams.cancelable, + baseParams.transferable, lockupLinear, - asset, - merkleRoot, - expiration, - abi.encode(streamDurations), - cancelable, - transferable + abi.encode(streamDurations) ) ); // Deploy the Merkle streamer with CREATE2. - merkleStreamerLL = new SablierV2MerkleStreamerLL{ salt: salt }( - initialAdmin, lockupLinear, asset, merkleRoot, expiration, streamDurations, cancelable, transferable - ); + merkleStreamerLL = new SablierV2MerkleStreamerLL{ salt: salt }(baseParams, lockupLinear, streamDurations); - // Log the creation of the Merkle streamer, including some metadata that is not stored on-chain. + // Using a different function to emit the event to avoid stack too deep error. emit CreateMerkleStreamerLL( - merkleStreamerLL, - initialAdmin, - lockupLinear, - asset, - merkleRoot, - expiration, - streamDurations, - cancelable, - transferable, - ipfsCID, - aggregateAmount, - recipientsCount + merkleStreamerLL, baseParams, lockupLinear, streamDurations, ipfsCID, aggregateAmount, recipientsCount ); } } diff --git a/src/SablierV2MerkleStreamerLL.sol b/src/SablierV2MerkleStreamerLL.sol index a3a451c0..ca9fb690 100644 --- a/src/SablierV2MerkleStreamerLL.sol +++ b/src/SablierV2MerkleStreamerLL.sol @@ -10,6 +10,7 @@ import { ud } from "@prb/math/src/UD60x18.sol"; import { SablierV2MerkleStreamer } from "./abstracts/SablierV2MerkleStreamer.sol"; import { ISablierV2MerkleStreamerLL } from "./interfaces/ISablierV2MerkleStreamerLL.sol"; +import { MerkleStreamer } from "./types/DataTypes.sol"; /// @title SablierV2MerkleStreamerLL /// @notice See the documentation in {ISablierV2MerkleStreamerLL}. @@ -41,16 +42,11 @@ contract SablierV2MerkleStreamerLL is /// @dev Constructs the contract by initializing the immutable state variables, and max approving the Sablier /// contract. constructor( - address initialAdmin, + MerkleStreamer.ConstructorParams memory baseParams, ISablierV2LockupLinear lockupLinear, - IERC20 asset, - bytes32 merkleRoot, - uint40 expiration, - LockupLinear.Durations memory streamDurations_, - bool cancelable, - bool transferable + LockupLinear.Durations memory streamDurations_ ) - SablierV2MerkleStreamer(initialAdmin, asset, merkleRoot, expiration, cancelable, transferable) + SablierV2MerkleStreamer(baseParams) { LOCKUP_LINEAR = lockupLinear; streamDurations = streamDurations_; diff --git a/src/abstracts/SablierV2MerkleStreamer.sol b/src/abstracts/SablierV2MerkleStreamer.sol index 75b50101..0fafbfa4 100644 --- a/src/abstracts/SablierV2MerkleStreamer.sol +++ b/src/abstracts/SablierV2MerkleStreamer.sol @@ -8,6 +8,7 @@ import { BitMaps } from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; import { Adminable } from "@sablier/v2-core/src/abstracts/Adminable.sol"; import { ISablierV2MerkleStreamer } from "../interfaces/ISablierV2MerkleStreamer.sol"; +import { MerkleStreamer } from "../types/DataTypes.sol"; import { Errors } from "../libraries/Errors.sol"; /// @title SablierV2MerkleStreamer @@ -38,6 +39,13 @@ abstract contract SablierV2MerkleStreamer is /// @inheritdoc ISablierV2MerkleStreamer bool public immutable override TRANSFERABLE; + /*////////////////////////////////////////////////////////////////////////// + INTERNAL CONSTANT + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev The name of the campaign stored as bytes32. + bytes32 internal immutable NAME; + /*////////////////////////////////////////////////////////////////////////// INTERNAL STORAGE //////////////////////////////////////////////////////////////////////////*/ @@ -50,20 +58,22 @@ abstract contract SablierV2MerkleStreamer is //////////////////////////////////////////////////////////////////////////*/ /// @dev Constructs the contract by initializing the immutable state variables. - constructor( - address initialAdmin, - IERC20 asset, - bytes32 merkleRoot, - uint40 expiration, - bool cancelable, - bool transferable - ) { - admin = initialAdmin; - ASSET = asset; - MERKLE_ROOT = merkleRoot; - EXPIRATION = expiration; - CANCELABLE = cancelable; - TRANSFERABLE = transferable; + constructor(MerkleStreamer.ConstructorParams memory params) { + // Checks: the campaign name is not greater than 32 bytes + if (bytes(params.name).length > 32) { + revert Errors.SablierV2MerkleStreamer_CampaignNameTooLong({ + nameLength: bytes(params.name).length, + maxLength: 32 + }); + } + + admin = params.initialAdmin; + ASSET = params.asset; + CANCELABLE = params.cancelable; + EXPIRATION = params.expiration; + MERKLE_ROOT = params.merkleRoot; + NAME = bytes32(abi.encodePacked(params.name)); + TRANSFERABLE = params.transferable; } /*////////////////////////////////////////////////////////////////////////// @@ -80,6 +90,11 @@ abstract contract SablierV2MerkleStreamer is return EXPIRATION > 0 && EXPIRATION <= block.timestamp; } + /// @inheritdoc ISablierV2MerkleStreamer + function name() external view override returns (string memory) { + return string(abi.encodePacked(NAME)); + } + /*////////////////////////////////////////////////////////////////////////// USER-FACING NON-CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ diff --git a/src/interfaces/ISablierV2MerkleStreamer.sol b/src/interfaces/ISablierV2MerkleStreamer.sol index 94c5d7de..0079e149 100644 --- a/src/interfaces/ISablierV2MerkleStreamer.sol +++ b/src/interfaces/ISablierV2MerkleStreamer.sol @@ -38,6 +38,9 @@ interface ISablierV2MerkleStreamer is IAdminable { /// @dev This is an immutable state variable. function EXPIRATION() external returns (uint40); + /// @notice Retrieves the name of the campaign. + function name() external returns (string memory); + /// @notice Returns a flag indicating whether a claim has been made for a given index. /// @dev Uses a bitmap to save gas. /// @param index The index of the recipient to check. diff --git a/src/interfaces/ISablierV2MerkleStreamerFactory.sol b/src/interfaces/ISablierV2MerkleStreamerFactory.sol index 504db123..46deefaa 100644 --- a/src/interfaces/ISablierV2MerkleStreamerFactory.sol +++ b/src/interfaces/ISablierV2MerkleStreamerFactory.sol @@ -1,11 +1,11 @@ // 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 { ISablierV2MerkleStreamerLL } from "./ISablierV2MerkleStreamerLL.sol"; +import { MerkleStreamer } from "../types/DataTypes.sol"; /// @title ISablierV2MerkleStreamerFactory /// @notice Deploys new Lockup Linear Merkle streamers via CREATE2. @@ -16,15 +16,10 @@ interface ISablierV2MerkleStreamerFactory { /// @notice Emitted when a Sablier V2 Lockup Linear Merkle streamer is created. event CreateMerkleStreamerLL( - ISablierV2MerkleStreamerLL merkleStreamer, - address indexed admin, - ISablierV2LockupLinear indexed lockupLinear, - IERC20 indexed asset, - bytes32 merkleRoot, - uint40 expiration, + ISablierV2MerkleStreamerLL indexed merkleStreamerLL, + MerkleStreamer.ConstructorParams indexed baseParams, + ISablierV2LockupLinear lockupLinear, LockupLinear.Durations streamDurations, - bool cancelable, - bool transferable, string ipfsCID, uint256 aggregateAmount, uint256 recipientsCount @@ -36,27 +31,18 @@ interface ISablierV2MerkleStreamerFactory { /// @notice Creates a new Merkle streamer that uses Lockup Linear. /// @dev Emits a {CreateMerkleStreamerLL} event. - /// @param initialAdmin The initial admin of the Merkle streamer contract. + /// @param baseParams Struct encapsulating the {SablierV2MerkleStreamer} parameters, which are documented in + /// {DataTypes}. /// @param lockupLinear The address of the {SablierV2LockupLinear} contract. - /// @param asset The address of the streamed ERC-20 asset. - /// @param merkleRoot The Merkle root of the claim data. - /// @param expiration The expiration of the streaming campaign, as a Unix timestamp. /// @param streamDurations The durations for each stream due to the recipient. - /// @param cancelable Indicates if each stream will be cancelable. - /// @param transferable Indicates if each stream NFT will be transferable. /// @param ipfsCID Metadata parameter emitted for indexing purposes. /// @param aggregateAmount Total amount of ERC-20 assets to be streamed to all recipients. /// @param recipientsCount Total number of recipients eligible to claim. /// @return merkleStreamerLL The address of the newly created Merkle streamer contract. function createMerkleStreamerLL( - address initialAdmin, + MerkleStreamer.ConstructorParams memory baseParams, ISablierV2LockupLinear lockupLinear, - IERC20 asset, - bytes32 merkleRoot, - uint40 expiration, LockupLinear.Durations memory streamDurations, - bool cancelable, - bool transferable, string memory ipfsCID, uint256 aggregateAmount, uint256 recipientsCount diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index 382e716a..1c343584 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -17,6 +17,9 @@ library Errors { /// @notice Thrown when trying to claim after the campaign has expired. error SablierV2MerkleStreamer_CampaignExpired(uint256 currentTime, uint40 expiration); + /// @notice Thrown when trying to create a campaign with a name that is too long. + error SablierV2MerkleStreamer_CampaignNameTooLong(uint256 nameLength, uint256 maxLength); + /// @notice Thrown when trying to clawback when the campaign has not expired. error SablierV2MerkleStreamer_CampaignNotExpired(uint256 currentTime, uint40 expiration); diff --git a/src/types/DataTypes.sol b/src/types/DataTypes.sol index ca2d63ec..02549065 100644 --- a/src/types/DataTypes.sol +++ b/src/types/DataTypes.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity >=0.8.22; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ISablierV2Lockup } from "@sablier/v2-core/src/interfaces/ISablierV2Lockup.sol"; import { Broker, LockupDynamic, LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; @@ -60,3 +61,23 @@ library Batch { Broker broker; } } + +library MerkleStreamer { + /// @notice Struct encapsulating the base constructor parameter of a {SablierV2MerkleStreamer} contract. + /// @param initialAdmin The initial admin of the Merkle streamer contract. + /// @param asset The address of the streamed ERC-20 asset. + /// @param name The name of the Merkle streamer contract. + /// @param merkleRoot The Merkle root of the claim data. + /// @param expiration The expiration of the streaming campaign, as a Unix timestamp. + /// @param cancelable Indicates if each stream will be cancelable. + /// @param transferable Indicates if each stream NFT will be transferable. + struct ConstructorParams { + address initialAdmin; + IERC20 asset; + string name; + bytes32 merkleRoot; + uint40 expiration; + bool cancelable; + bool transferable; + } +} diff --git a/test/Base.t.sol b/test/Base.t.sol index 164b60c5..792277e6 100644 --- a/test/Base.t.sol +++ b/test/Base.t.sol @@ -261,13 +261,14 @@ abstract contract Base_Test is DeployOptimized, Events, Merkle, V2CoreAssertions bytes32 salt = keccak256( abi.encodePacked( admin, - lockupLinear, asset, + defaults.NAME_BYTES32(), merkleRoot, expiration, - abi.encode(defaults.durations()), defaults.CANCELABLE(), - defaults.TRANSFERABLE() + defaults.TRANSFERABLE(), + lockupLinear, + abi.encode(defaults.durations()) ) ); bytes32 creationBytecodeHash = keccak256(getMerkleStreamerLLBytecode(admin, merkleRoot, expiration)); @@ -286,16 +287,8 @@ abstract contract Base_Test is DeployOptimized, Events, Merkle, V2CoreAssertions internal returns (bytes memory) { - bytes memory constructorArgs = abi.encode( - admin, - lockupLinear, - asset, - merkleRoot, - expiration, - defaults.durations(), - defaults.CANCELABLE(), - defaults.TRANSFERABLE() - ); + bytes memory constructorArgs = + abi.encode(defaults.baseParams(admin, merkleRoot, expiration), lockupLinear, defaults.durations()); if (!isTestOptimizedProfile()) { return bytes.concat(type(SablierV2MerkleStreamerLL).creationCode, constructorArgs); } else { diff --git a/test/fork/merkle-streamer/MerkleStreamerLL.t.sol b/test/fork/merkle-streamer/MerkleStreamerLL.t.sol index 7e436931..c2741211 100644 --- a/test/fork/merkle-streamer/MerkleStreamerLL.t.sol +++ b/test/fork/merkle-streamer/MerkleStreamerLL.t.sol @@ -6,6 +6,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { Lockup, LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; import { ISablierV2MerkleStreamerLL } from "src/interfaces/ISablierV2MerkleStreamerLL.sol"; +import { MerkleStreamer } from "src/types/DataTypes.sol"; import { MerkleBuilder } from "../../utils/MerkleBuilder.sol"; import { Fork_Test } from "../Fork.t.sol"; @@ -40,6 +41,7 @@ abstract contract MerkleStreamerLL_Fork_Test is Fork_Test { uint256 aggregateAmount; uint128 clawbackAmount; address expectedStreamerLL; + MerkleStreamer.ConstructorParams baseParams; LockupLinear.Stream expectedStream; uint256 expectedStreamId; uint256[] indexes; @@ -92,31 +94,25 @@ abstract contract MerkleStreamerLL_Fork_Test is Fork_Test { vars.merkleRoot = getRoot(leaves.toBytes32()); vars.expectedStreamerLL = computeMerkleStreamerLLAddress(params.admin, vars.merkleRoot, params.expiration); + + vars.baseParams = + defaults.baseParams({ admin: params.admin, merkleRoot: vars.merkleRoot, expiration: params.expiration }); + vm.expectEmit({ emitter: address(merkleStreamerFactory) }); emit CreateMerkleStreamerLL({ - merkleStreamer: ISablierV2MerkleStreamerLL(vars.expectedStreamerLL), - admin: params.admin, + merkleStreamerLL: ISablierV2MerkleStreamerLL(vars.expectedStreamerLL), + baseParams: vars.baseParams, lockupLinear: lockupLinear, - asset: asset, - merkleRoot: vars.merkleRoot, - expiration: params.expiration, streamDurations: defaults.durations(), - cancelable: defaults.CANCELABLE(), - transferable: defaults.TRANSFERABLE(), ipfsCID: defaults.IPFS_CID(), aggregateAmount: vars.aggregateAmount, recipientsCount: vars.recipientsCount }); vars.merkleStreamerLL = merkleStreamerFactory.createMerkleStreamerLL({ - initialAdmin: params.admin, + baseParams: vars.baseParams, lockupLinear: lockupLinear, - asset: asset, - merkleRoot: vars.merkleRoot, - expiration: params.expiration, streamDurations: defaults.durations(), - cancelable: defaults.CANCELABLE(), - transferable: defaults.TRANSFERABLE(), ipfsCID: defaults.IPFS_CID(), aggregateAmount: vars.aggregateAmount, recipientsCount: vars.recipientsCount diff --git a/test/integration/merkle-streamer/MerkleStreamer.t.sol b/test/integration/merkle-streamer/MerkleStreamer.t.sol index 3f244cef..63adb001 100644 --- a/test/integration/merkle-streamer/MerkleStreamer.t.sol +++ b/test/integration/merkle-streamer/MerkleStreamer.t.sol @@ -55,13 +55,8 @@ abstract contract MerkleStreamer_Integration_Test is Integration_Test { function createMerkleStreamerLL(address admin, uint40 expiration) internal returns (ISablierV2MerkleStreamerLL) { return merkleStreamerFactory.createMerkleStreamerLL({ - initialAdmin: admin, + baseParams: defaults.baseParams(admin, defaults.MERKLE_ROOT(), expiration), lockupLinear: lockupLinear, - asset: asset, - merkleRoot: defaults.MERKLE_ROOT(), - expiration: expiration, - cancelable: defaults.CANCELABLE(), - transferable: defaults.TRANSFERABLE(), streamDurations: defaults.durations(), ipfsCID: defaults.IPFS_CID(), aggregateAmount: defaults.AGGREGATE_AMOUNT(), diff --git a/test/integration/merkle-streamer/factory/create-merkle-streamer-ll/createMerkleStreamerLL.t.sol b/test/integration/merkle-streamer/factory/create-merkle-streamer-ll/createMerkleStreamerLL.t.sol index 0c17b93a..936cc02f 100644 --- a/test/integration/merkle-streamer/factory/create-merkle-streamer-ll/createMerkleStreamerLL.t.sol +++ b/test/integration/merkle-streamer/factory/create-merkle-streamer-ll/createMerkleStreamerLL.t.sol @@ -3,7 +3,9 @@ pragma solidity >=0.8.22 <0.9.0; import { LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; +import { Errors } from "src/libraries/Errors.sol"; import { ISablierV2MerkleStreamerLL } from "src/interfaces/ISablierV2MerkleStreamerLL.sol"; +import { MerkleStreamer } from "src/types/DataTypes.sol"; import { MerkleStreamer_Integration_Test } from "../../MerkleStreamer.t.sol"; @@ -12,12 +14,38 @@ contract CreateMerkleStreamerLL_Integration_Test is MerkleStreamer_Integration_T MerkleStreamer_Integration_Test.setUp(); } + function test_RevertWhen_CampaignNameTooLong() external { + MerkleStreamer.ConstructorParams memory baseParams = defaults.baseParams(); + LockupLinear.Durations memory streamDurations = defaults.durations(); + string memory ipfsCID = defaults.IPFS_CID(); + uint256 aggregateAmount = defaults.AGGREGATE_AMOUNT(); + uint256 recipientsCount = defaults.RECIPIENTS_COUNT(); + + baseParams.name = "this string is longer than 32 characters"; + + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierV2MerkleStreamer_CampaignNameTooLong.selector, bytes(baseParams.name).length, 32 + ) + ); + + merkleStreamerFactory.createMerkleStreamerLL({ + baseParams: baseParams, + lockupLinear: lockupLinear, + streamDurations: streamDurations, + ipfsCID: ipfsCID, + aggregateAmount: aggregateAmount, + recipientsCount: recipientsCount + }); + } + + modifier whenCampaignNameIsNotTooLong() { + _; + } + /// @dev This test works because a default Merkle streamer is deployed in {Integration_Test.setUp} - function test_RevertGiven_AlreadyDeployed() external { - bytes32 merkleRoot = defaults.MERKLE_ROOT(); - uint40 expiration = defaults.EXPIRATION(); - bool cancelable = defaults.CANCELABLE(); - bool transferable = defaults.TRANSFERABLE(); + function test_RevertGiven_AlreadyDeployed() external whenCampaignNameIsNotTooLong { + MerkleStreamer.ConstructorParams memory baseParams = defaults.baseParams(); LockupLinear.Durations memory streamDurations = defaults.durations(); string memory ipfsCID = defaults.IPFS_CID(); uint256 aggregateAmount = defaults.AGGREGATE_AMOUNT(); @@ -25,13 +53,8 @@ contract CreateMerkleStreamerLL_Integration_Test is MerkleStreamer_Integration_T vm.expectRevert(); merkleStreamerFactory.createMerkleStreamerLL({ - initialAdmin: users.admin, + baseParams: baseParams, lockupLinear: lockupLinear, - asset: asset, - merkleRoot: merkleRoot, - expiration: expiration, - cancelable: cancelable, - transferable: transferable, streamDurations: streamDurations, ipfsCID: ipfsCID, aggregateAmount: aggregateAmount, @@ -43,21 +66,26 @@ contract CreateMerkleStreamerLL_Integration_Test is MerkleStreamer_Integration_T _; } - function testFuzz_CreateMerkleStreamerLL(address admin, uint40 expiration) external givenNotAlreadyDeployed { + function testFuzz_CreateMerkleStreamerLL( + address admin, + uint40 expiration + ) + external + givenNotAlreadyDeployed + whenCampaignNameIsNotTooLong + { vm.assume(admin != users.admin); address expectedStreamerLL = computeMerkleStreamerLLAddress(admin, expiration); + MerkleStreamer.ConstructorParams memory baseParams = + defaults.baseParams({ admin: admin, merkleRoot: defaults.MERKLE_ROOT(), expiration: expiration }); + vm.expectEmit({ emitter: address(merkleStreamerFactory) }); emit CreateMerkleStreamerLL({ - merkleStreamer: ISablierV2MerkleStreamerLL(expectedStreamerLL), - admin: admin, + merkleStreamerLL: ISablierV2MerkleStreamerLL(expectedStreamerLL), + baseParams: baseParams, lockupLinear: lockupLinear, - asset: asset, - merkleRoot: defaults.MERKLE_ROOT(), - expiration: expiration, streamDurations: defaults.durations(), - cancelable: defaults.CANCELABLE(), - transferable: defaults.TRANSFERABLE(), ipfsCID: defaults.IPFS_CID(), aggregateAmount: defaults.AGGREGATE_AMOUNT(), recipientsCount: defaults.RECIPIENTS_COUNT() diff --git a/test/integration/merkle-streamer/factory/create-merkle-streamer-ll/createMerkleStreamerLL.tree b/test/integration/merkle-streamer/factory/create-merkle-streamer-ll/createMerkleStreamerLL.tree index 48eeaea2..0ca44455 100644 --- a/test/integration/merkle-streamer/factory/create-merkle-streamer-ll/createMerkleStreamerLL.tree +++ b/test/integration/merkle-streamer/factory/create-merkle-streamer-ll/createMerkleStreamerLL.tree @@ -1,6 +1,9 @@ createMerkleStreamerLL.t.sol -├── given the Merkle streamer has been deployed with CREATE2 -│ └── it should revert -└── given the Merkle streamer has not been deployed - ├── it should deploy the Merkle streamer - └── it should emit an {CreateMerkleStreamerLL} event +├── when the campaign name is too long +│ └── it should revert +└── when the campaign name is not too long + ├── given the Merkle streamer has been deployed with CREATE2 + │ └── it should revert + └── given the Merkle streamer has not been deployed + ├── it should deploy the Merkle streamer + └── it should emit a {CreateMerkleStreamerLL} event diff --git a/test/integration/merkle-streamer/ll/constructor/constructor.t.sol b/test/integration/merkle-streamer/ll/constructor/constructor.t.sol index 0837554e..2f0d3ea2 100644 --- a/test/integration/merkle-streamer/ll/constructor/constructor.t.sol +++ b/test/integration/merkle-streamer/ll/constructor/constructor.t.sol @@ -13,6 +13,7 @@ contract Constructor_MerkleStreamerLL_Integration_Test is MerkleStreamer_Integra address actualAdmin; uint256 actualAllowance; address actualAsset; + string actualName; bool actualCancelable; bool actualTransferable; LockupLinear.Durations actualDurations; @@ -22,6 +23,7 @@ contract Constructor_MerkleStreamerLL_Integration_Test is MerkleStreamer_Integra address expectedAdmin; uint256 expectedAllowance; address expectedAsset; + bytes32 expectedName; bool expectedCancelable; bool expectedTransferable; LockupLinear.Durations expectedDurations; @@ -31,16 +33,8 @@ contract Constructor_MerkleStreamerLL_Integration_Test is MerkleStreamer_Integra } function test_Constructor() external { - SablierV2MerkleStreamerLL constructedStreamerLL = new SablierV2MerkleStreamerLL( - users.admin, - lockupLinear, - asset, - defaults.MERKLE_ROOT(), - defaults.EXPIRATION(), - defaults.durations(), - defaults.CANCELABLE(), - defaults.TRANSFERABLE() - ); + SablierV2MerkleStreamerLL constructedStreamerLL = + new SablierV2MerkleStreamerLL(defaults.baseParams(), lockupLinear, defaults.durations()); Vars memory vars; @@ -52,6 +46,10 @@ contract Constructor_MerkleStreamerLL_Integration_Test is MerkleStreamer_Integra vars.expectedAsset = address(asset); assertEq(vars.actualAsset, vars.expectedAsset, "asset"); + vars.actualName = constructedStreamerLL.name(); + vars.expectedName = defaults.NAME_BYTES32(); + assertEq(bytes32(abi.encodePacked(vars.actualName)), vars.expectedName, "name"); + vars.actualMerkleRoot = constructedStreamerLL.MERKLE_ROOT(); vars.expectedMerkleRoot = defaults.MERKLE_ROOT(); assertEq(vars.actualMerkleRoot, vars.expectedMerkleRoot, "merkleRoot"); diff --git a/test/utils/Defaults.sol b/test/utils/Defaults.sol index 66297b86..bb9e395f 100644 --- a/test/utils/Defaults.sol +++ b/test/utils/Defaults.sol @@ -7,7 +7,7 @@ import { ud2x18 } from "@prb/math/src/UD2x18.sol"; import { UD60x18 } from "@prb/math/src/UD60x18.sol"; import { Broker, LockupDynamic, LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; -import { Batch } from "src/types/DataTypes.sol"; +import { Batch, MerkleStreamer } from "src/types/DataTypes.sol"; import { ArrayBuilder } from "./ArrayBuilder.sol"; import { BatchBuilder } from "./BatchBuilder.sol"; @@ -55,6 +55,8 @@ contract Defaults is Merkle { bool public constant TRANSFERABLE = false; uint256[] public LEAVES = new uint256[](RECIPIENTS_COUNT); bytes32 public immutable MERKLE_ROOT; + string public constant NAME = "Airdrop Campaign"; + bytes32 public constant NAME_BYTES32 = bytes32(abi.encodePacked("Airdrop Campaign")); /*////////////////////////////////////////////////////////////////////////// VARIABLES @@ -114,6 +116,30 @@ contract Defaults is Merkle { return getProof(LEAVES.toBytes32(), pos); } + function baseParams() public view returns (MerkleStreamer.ConstructorParams memory) { + return baseParams(users.admin, MERKLE_ROOT, EXPIRATION); + } + + function baseParams( + address admin, + bytes32 merkleRoot, + uint40 expiration + ) + public + view + returns (MerkleStreamer.ConstructorParams memory) + { + return MerkleStreamer.ConstructorParams({ + initialAdmin: admin, + asset: asset, + name: NAME, + merkleRoot: merkleRoot, + expiration: expiration, + cancelable: CANCELABLE, + transferable: TRANSFERABLE + }); + } + /*////////////////////////////////////////////////////////////////////////// SABLIER-V2-LOCKUP //////////////////////////////////////////////////////////////////////////*/ diff --git a/test/utils/Events.sol b/test/utils/Events.sol index 5977a16c..9a8c2a96 100644 --- a/test/utils/Events.sol +++ b/test/utils/Events.sol @@ -1,26 +1,21 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity >=0.8.22; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; import { ISablierV2MerkleStreamerLL } from "src/interfaces/ISablierV2MerkleStreamerLL.sol"; +import { MerkleStreamer } from "src/types/DataTypes.sol"; /// @notice Abstract contract containing all the events emitted by the protocol. abstract contract Events { event Claim(uint256 index, address indexed recipient, uint128 amount, uint256 indexed streamId); event Clawback(address indexed admin, address indexed to, uint128 amount); event CreateMerkleStreamerLL( - ISablierV2MerkleStreamerLL merkleStreamer, - address indexed admin, - ISablierV2LockupLinear indexed lockupLinear, - IERC20 indexed asset, - bytes32 merkleRoot, - uint40 expiration, + ISablierV2MerkleStreamerLL indexed merkleStreamerLL, + MerkleStreamer.ConstructorParams indexed baseParams, + ISablierV2LockupLinear lockupLinear, LockupLinear.Durations streamDurations, - bool cancelable, - bool transferable, string ipfsCID, uint256 aggregateAmount, uint256 recipientsCount