From 8471bd43a9a47313a332f32da09c668d441decc0 Mon Sep 17 00:00:00 2001 From: smol-ninja Date: Tue, 6 Feb 2024 16:27:36 +0530 Subject: [PATCH] test: integration and fork tests for MerkleLockupLD --- .../ISablierV2MerkleLockupFactory.sol | 4 +- test/Base.t.sol | 56 ++++- test/fork/assets/USDC.t.sol | 3 + test/fork/assets/USDT.t.sol | 3 + test/fork/merkle-lockup/MerkleLockupLD.t.sol | 231 ++++++++++++++++++ test/fork/merkle-lockup/MerkleLockupLL.t.sol | 22 +- .../merkle-lockup/MerkleLockup.t.sol | 72 +++++- .../createMerkleLockupLD.t.sol | 92 +++++++ .../createMerkleLockupLD.tree | 9 + .../createMerkleLockupLL.t.sol | 6 +- .../merkle-lockup/ld/claim/claim.t.sol | 175 +++++++++++++ .../merkle-lockup/ld/claim/claim.tree | 21 ++ .../merkle-lockup/ld/clawback/clawback.t.sol | 58 +++++ .../merkle-lockup/ld/clawback/clawback.tree | 9 + .../ld/constructor/constructor.t.sol | 75 ++++++ .../ld/has-claimed/hasClaimed.t.sol | 32 +++ .../ld/has-claimed/hasClaimed.tree | 8 + .../ld/has-expired/hasExpired.t.sol | 35 +++ .../ld/has-expired/hasExpired.tree | 10 + .../merkle-lockup/ll/claim/claim.t.sol | 27 +- .../merkle-lockup/ll/clawback/clawback.t.sol | 2 +- .../ll/constructor/constructor.t.sol | 4 +- .../ll/has-claimed/hasClaimed.t.sol | 2 +- .../ll/has-expired/hasExpired.t.sol | 2 +- test/utils/Defaults.sol | 100 +++++--- test/utils/Events.sol | 13 + test/utils/MerkleBuilder.sol | 62 ++++- test/utils/MerkleBuilder.t.sol | 68 +++++- 28 files changed, 1121 insertions(+), 80 deletions(-) create mode 100644 test/fork/merkle-lockup/MerkleLockupLD.t.sol create mode 100644 test/integration/merkle-lockup/factory/create-merkle-lockup-ld/createMerkleLockupLD.t.sol create mode 100644 test/integration/merkle-lockup/factory/create-merkle-lockup-ld/createMerkleLockupLD.tree create mode 100644 test/integration/merkle-lockup/ld/claim/claim.t.sol create mode 100644 test/integration/merkle-lockup/ld/claim/claim.tree create mode 100644 test/integration/merkle-lockup/ld/clawback/clawback.t.sol create mode 100644 test/integration/merkle-lockup/ld/clawback/clawback.tree create mode 100644 test/integration/merkle-lockup/ld/constructor/constructor.t.sol create mode 100644 test/integration/merkle-lockup/ld/has-claimed/hasClaimed.t.sol create mode 100644 test/integration/merkle-lockup/ld/has-claimed/hasClaimed.tree create mode 100644 test/integration/merkle-lockup/ld/has-expired/hasExpired.t.sol create mode 100644 test/integration/merkle-lockup/ld/has-expired/hasExpired.tree diff --git a/src/interfaces/ISablierV2MerkleLockupFactory.sol b/src/interfaces/ISablierV2MerkleLockupFactory.sol index ce61c677..5c335203 100644 --- a/src/interfaces/ISablierV2MerkleLockupFactory.sol +++ b/src/interfaces/ISablierV2MerkleLockupFactory.sol @@ -49,7 +49,7 @@ interface ISablierV2MerkleLockupFactory { /// @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 merkleLockupLD The address of the newly created Merkle Lockup contract. + /// @return merkleLockupLD The address of the newly created Merkle Lockup Dynamic contract. function createMerkleLockupLD( MerkleLockup.ConstructorParams memory baseParams, ISablierV2LockupDynamic lockupDynamic, @@ -69,7 +69,7 @@ interface ISablierV2MerkleLockupFactory { /// @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 merkleLockupLL The address of the newly created Merkle Lockup contract. + /// @return merkleLockupLL The address of the newly created Merkle Lockup Linear contract. function createMerkleLockupLL( MerkleLockup.ConstructorParams memory baseParams, ISablierV2LockupLinear lockupLinear, diff --git a/test/Base.t.sol b/test/Base.t.sol index 84909168..b6471617 100644 --- a/test/Base.t.sol +++ b/test/Base.t.sol @@ -15,9 +15,11 @@ import { Utils as V2CoreUtils } from "@sablier/v2-core/test/utils/Utils.sol"; import { ISablierV2Batch } from "src/interfaces/ISablierV2Batch.sol"; import { ISablierV2MerkleLockupFactory } from "src/interfaces/ISablierV2MerkleLockupFactory.sol"; +import { ISablierV2MerkleLockupLD } from "src/interfaces/ISablierV2MerkleLockupLD.sol"; import { ISablierV2MerkleLockupLL } from "src/interfaces/ISablierV2MerkleLockupLL.sol"; import { SablierV2Batch } from "src/SablierV2Batch.sol"; import { SablierV2MerkleLockupFactory } from "src/SablierV2MerkleLockupFactory.sol"; +import { SablierV2MerkleLockupLD } from "src/SablierV2MerkleLockupLD.sol"; import { SablierV2MerkleLockupLL } from "src/SablierV2MerkleLockupLL.sol"; import { Defaults } from "./utils/Defaults.sol"; @@ -45,6 +47,7 @@ abstract contract Base_Test is DeployOptimized, Events, Merkle, V2CoreAssertions ISablierV2LockupDynamic internal lockupDynamic; ISablierV2LockupLinear internal lockupLinear; ISablierV2MerkleLockupFactory internal merkleLockupFactory; + ISablierV2MerkleLockupLD internal merkleLockupLD; ISablierV2MerkleLockupLL internal merkleLockupLL; /*////////////////////////////////////////////////////////////////////////// @@ -100,6 +103,7 @@ abstract contract Base_Test is DeployOptimized, Events, Merkle, V2CoreAssertions function labelContracts() internal { vm.label({ account: address(asset), newLabel: IERC20Metadata(address(asset)).symbol() }); vm.label({ account: address(merkleLockupFactory), newLabel: "MerkleLockupFactory" }); + vm.label({ account: address(merkleLockupLD), newLabel: "MerkleLockupLD" }); vm.label({ account: address(merkleLockupLL), newLabel: "MerkleLockupLL" }); vm.label({ account: address(defaults), newLabel: "Defaults" }); vm.label({ account: address(comptroller), newLabel: "Comptroller" }); @@ -247,7 +251,57 @@ abstract contract Base_Test is DeployOptimized, Events, Merkle, V2CoreAssertions } /*////////////////////////////////////////////////////////////////////////// - MERKLE-LOCKUP + MERKLE-LOCKUP-LD + //////////////////////////////////////////////////////////////////////////*/ + + function computeMerkleLockupLDAddress( + address admin, + bytes32 merkleRoot, + uint40 expiration + ) + internal + returns (address) + { + bytes32 salt = keccak256( + abi.encodePacked( + admin, + asset, + defaults.NAME_BYTES32(), + merkleRoot, + expiration, + defaults.CANCELABLE(), + defaults.TRANSFERABLE(), + lockupDynamic + ) + ); + bytes32 creationBytecodeHash = keccak256(getMerkleLockupLDBytecode(admin, merkleRoot, expiration)); + return computeCreate2Address({ + salt: salt, + initcodeHash: creationBytecodeHash, + deployer: address(merkleLockupFactory) + }); + } + + function getMerkleLockupLDBytecode( + address admin, + bytes32 merkleRoot, + uint40 expiration + ) + internal + returns (bytes memory) + { + bytes memory constructorArgs = abi.encode(defaults.baseParams(admin, merkleRoot, expiration), lockupDynamic); + if (!isTestOptimizedProfile()) { + return bytes.concat(type(SablierV2MerkleLockupLD).creationCode, constructorArgs); + } else { + return bytes.concat( + vm.getCode("out-optimized/SablierV2MerkleLockupLD.sol/SablierV2MerkleLockupLD.json"), constructorArgs + ); + } + } + + /*////////////////////////////////////////////////////////////////////////// + MERKLE-LOCKUP-LL //////////////////////////////////////////////////////////////////////////*/ function computeMerkleLockupLLAddress( diff --git a/test/fork/assets/USDC.t.sol b/test/fork/assets/USDC.t.sol index 1d58e000..31699097 100644 --- a/test/fork/assets/USDC.t.sol +++ b/test/fork/assets/USDC.t.sol @@ -5,6 +5,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { CreateWithTimestamps_LockupDynamic_Batch_Fork_Test } from "../batch/createWithTimestampsLD.t.sol"; import { CreateWithTimestamps_LockupLinear_Batch_Fork_Test } from "../batch/createWithTimestampsLL.t.sol"; +import { MerkleLockupLD_Fork_Test } from "../merkle-lockup/MerkleLockupLD.t.sol"; import { MerkleLockupLL_Fork_Test } from "../merkle-lockup/MerkleLockupLL.t.sol"; IERC20 constant usdc = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); @@ -17,4 +18,6 @@ contract USDC_CreateWithTimestamps_LockupLinear_Batch_Fork_Test is CreateWithTimestamps_LockupLinear_Batch_Fork_Test(usdc) { } +contract USDC_MerkleLockupLD_Fork_Test is MerkleLockupLD_Fork_Test(usdc) { } + contract USDC_MerkleLockupLL_Fork_Test is MerkleLockupLL_Fork_Test(usdc) { } diff --git a/test/fork/assets/USDT.t.sol b/test/fork/assets/USDT.t.sol index 5be49fce..4517eae2 100644 --- a/test/fork/assets/USDT.t.sol +++ b/test/fork/assets/USDT.t.sol @@ -5,6 +5,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { CreateWithTimestamps_LockupDynamic_Batch_Fork_Test } from "../batch/createWithTimestampsLD.t.sol"; import { CreateWithTimestamps_LockupLinear_Batch_Fork_Test } from "../batch/createWithTimestampsLL.t.sol"; +import { MerkleLockupLD_Fork_Test } from "../merkle-lockup/MerkleLockupLD.t.sol"; import { MerkleLockupLL_Fork_Test } from "../merkle-lockup/MerkleLockupLL.t.sol"; IERC20 constant usdt = IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7); @@ -17,4 +18,6 @@ contract USDT_CreateWithTimestamps_LockupLinear_Batch_Fork_Test is CreateWithTimestamps_LockupLinear_Batch_Fork_Test(usdt) { } +contract USDT_MerkleLockupLD_Fork_Test is MerkleLockupLD_Fork_Test(usdt) { } + contract USDT_MerkleLockupLL_Fork_Test is MerkleLockupLL_Fork_Test(usdt) { } diff --git a/test/fork/merkle-lockup/MerkleLockupLD.t.sol b/test/fork/merkle-lockup/MerkleLockupLD.t.sol new file mode 100644 index 00000000..84904e0c --- /dev/null +++ b/test/fork/merkle-lockup/MerkleLockupLD.t.sol @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { Arrays } from "@openzeppelin/contracts/utils/Arrays.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { Lockup, LockupDynamic } from "@sablier/v2-core/src/types/DataTypes.sol"; + +import { ISablierV2MerkleLockupLD } from "src/interfaces/ISablierV2MerkleLockupLD.sol"; +import { MerkleLockup } from "src/types/DataTypes.sol"; + +import { MerkleBuilder } from "../../utils/MerkleBuilder.sol"; +import { Fork_Test } from "../Fork.t.sol"; + +abstract contract MerkleLockupLD_Fork_Test is Fork_Test { + using MerkleBuilder for uint256[]; + + constructor(IERC20 asset_) Fork_Test(asset_) { } + + function setUp() public virtual override { + Fork_Test.setUp(); + } + + /// @dev Encapsulates the data needed to compute a Merkle tree leaf. + struct LeafData { + uint256 index; + uint256 recipientSeed; + LockupDynamic.SegmentWithDuration[] segments; + } + + struct Params { + address admin; + uint40 expiration; + LeafData[] leafData; + uint256 posBeforeSort; + } + + struct Vars { + uint256 actualStreamId; + LockupDynamic.Stream actualStream; + uint128[] amounts; + uint256 aggregateAmount; + uint128 clawbackAmount; + address expectedLockupLD; + MerkleLockup.ConstructorParams baseParams; + LockupDynamic.Stream expectedStream; + uint256 expectedStreamId; + uint256[] indexes; + uint256 leafPos; + uint256 leafToClaim; + ISablierV2MerkleLockupLD merkleLockupLD; + bytes32 merkleRoot; + address[] recipients; + uint256 recipientsCount; + uint40[] streamDurations; + LockupDynamic.SegmentWithDuration[][] segments; + LockupDynamic.Segment[][] segmentsWithTimestamps; + } + + // We need the leaves as a storage variable so that we can use OpenZeppelin's {Arrays.findUpperBound}. + uint256[] public leaves; + + function testForkFuzz_MerkleLockupLD(Params memory params) external { + vm.assume(params.admin != address(0) && params.admin != users.admin); + vm.assume(params.expiration == 0 || params.expiration > block.timestamp); + vm.assume(params.leafData.length > 1); + params.posBeforeSort = _bound(params.posBeforeSort, 0, params.leafData.length - 1); + assumeNoBlacklisted({ token: address(asset), addr: params.admin }); + + /*////////////////////////////////////////////////////////////////////////// + CREATE + //////////////////////////////////////////////////////////////////////////*/ + + Vars memory vars; + + vars.recipientsCount = params.leafData.length; + vars.amounts = new uint128[](vars.recipientsCount); + vars.indexes = new uint256[](vars.recipientsCount); + vars.recipients = new address[](vars.recipientsCount); + vars.segments = new LockupDynamic.SegmentWithDuration[][](vars.recipientsCount); + vars.segmentsWithTimestamps = new LockupDynamic.Segment[][](vars.recipientsCount); + vars.streamDurations = new uint40[](vars.recipientsCount); + + for (uint256 i = 0; i < vars.recipientsCount; ++i) { + vm.assume( + params.leafData[i].segments.length > 0 + && params.leafData[i].segments.length < defaults.MAX_SEGMENT_COUNT() + ); + vars.indexes[i] = params.leafData[i].index; + + // Bound each leaf's segment duration to avoid overflows. + fuzzSegmentDurations(params.leafData[i].segments); + + uint256 segmentCount = params.leafData[i].segments.length; + for (uint256 j = 0; j < segmentCount; ++j) { + // Bound each leaf's segment amount so that `aggregateAmount` does not overflow. + params.leafData[i].segments[j].amount = uint128( + _bound( + params.leafData[i].segments[j].amount, + 1, + MAX_UINT128 / (vars.recipientsCount * segmentCount) - 1 + ) + ); + + vars.amounts[i] += params.leafData[i].segments[j].amount; + vars.streamDurations[i] += params.leafData[i].segments[j].duration; + } + + vars.aggregateAmount += vars.amounts[i]; + + // Avoid zero recipient addresses. + uint256 boundedRecipientSeed = _bound(params.leafData[i].recipientSeed, 1, type(uint160).max); + vars.recipients[i] = address(uint160(boundedRecipientSeed)); + + vars.segments[i] = params.leafData[i].segments; + vars.segmentsWithTimestamps[i] = getSegmentsWithTimestamps(params.leafData[i].segments); + } + + leaves = new uint256[](vars.recipientsCount); + leaves = MerkleBuilder.computeLeavesLD(vars.indexes, vars.recipients, vars.segments); + + // Sort the leaves in ascending order to match the production environment. + MerkleBuilder.sortLeaves(leaves); + vars.merkleRoot = getRoot(leaves.toBytes32()); + + vars.expectedLockupLD = computeMerkleLockupLDAddress(params.admin, vars.merkleRoot, params.expiration); + + vars.baseParams = + defaults.baseParams({ admin: params.admin, merkleRoot: vars.merkleRoot, expiration: params.expiration }); + + vm.expectEmit({ emitter: address(merkleLockupFactory) }); + emit CreateMerkleLockupLD({ + merkleLockupLD: ISablierV2MerkleLockupLD(vars.expectedLockupLD), + baseParams: vars.baseParams, + lockupDynamic: lockupDynamic, + ipfsCID: defaults.IPFS_CID(), + aggregateAmount: vars.aggregateAmount, + recipientsCount: vars.recipientsCount + }); + + vars.merkleLockupLD = merkleLockupFactory.createMerkleLockupLD({ + baseParams: vars.baseParams, + lockupDynamic: lockupDynamic, + ipfsCID: defaults.IPFS_CID(), + aggregateAmount: vars.aggregateAmount, + recipientsCount: vars.recipientsCount + }); + + // Fund the Merkle Lockup contract. + deal({ token: address(asset), to: address(vars.merkleLockupLD), give: vars.aggregateAmount }); + + assertGt(address(vars.merkleLockupLD).code.length, 0, "MerkleLockupLD contract not created"); + assertEq( + address(vars.merkleLockupLD), + vars.expectedLockupLD, + "MerkleLockupLD contract does not match computed address" + ); + + /*////////////////////////////////////////////////////////////////////////// + CLAIM + //////////////////////////////////////////////////////////////////////////*/ + + assertFalse(vars.merkleLockupLD.hasClaimed(vars.indexes[params.posBeforeSort])); + + vars.leafToClaim = MerkleBuilder.computeLeafLD( + vars.indexes[params.posBeforeSort], + vars.recipients[params.posBeforeSort], + vars.segments[params.posBeforeSort] + ); + vars.leafPos = Arrays.findUpperBound(leaves, vars.leafToClaim); + + vars.expectedStreamId = lockupDynamic.nextStreamId(); + emit Claim( + vars.indexes[params.posBeforeSort], + vars.recipients[params.posBeforeSort], + vars.amounts[params.posBeforeSort], + vars.expectedStreamId + ); + vars.actualStreamId = vars.merkleLockupLD.claim({ + index: vars.indexes[params.posBeforeSort], + recipient: vars.recipients[params.posBeforeSort], + segments: vars.segments[params.posBeforeSort], + merkleProof: getProof(leaves.toBytes32(), vars.leafPos) + }); + + vars.actualStream = lockupDynamic.getStream(vars.actualStreamId); + vars.expectedStream = LockupDynamic.Stream({ + amounts: Lockup.Amounts({ deposited: vars.amounts[params.posBeforeSort], refunded: 0, withdrawn: 0 }), + asset: asset, + endTime: uint40(block.timestamp) + vars.streamDurations[params.posBeforeSort], + isCancelable: defaults.CANCELABLE(), + isDepleted: false, + isStream: true, + isTransferable: defaults.TRANSFERABLE(), + segments: vars.segmentsWithTimestamps[params.posBeforeSort], + sender: params.admin, + startTime: uint40(block.timestamp), + wasCanceled: false + }); + + assertTrue(vars.merkleLockupLD.hasClaimed(vars.indexes[params.posBeforeSort])); + assertEq(vars.actualStreamId, vars.expectedStreamId); + assertEq(vars.actualStream.amounts.deposited, vars.expectedStream.amounts.deposited); + assertEq(vars.actualStream.amounts.refunded, vars.expectedStream.amounts.refunded); + assertEq(vars.actualStream.amounts.withdrawn, vars.expectedStream.amounts.withdrawn); + assertEq(vars.actualStream.asset, vars.expectedStream.asset); + assertEq(vars.actualStream.endTime, vars.expectedStream.endTime); + assertEq(vars.actualStream.isCancelable, vars.expectedStream.isCancelable); + assertEq(vars.actualStream.isDepleted, vars.expectedStream.isDepleted); + assertEq(vars.actualStream.isStream, vars.expectedStream.isStream); + assertEq(vars.actualStream.isTransferable, vars.expectedStream.isTransferable); + assertEq(vars.actualStream.segments, vars.expectedStream.segments); + assertEq(vars.actualStream.sender, vars.expectedStream.sender); + assertEq(vars.actualStream.startTime, vars.expectedStream.startTime); + assertEq(vars.actualStream.wasCanceled, vars.expectedStream.wasCanceled); + + /*////////////////////////////////////////////////////////////////////////// + CLAWBACK + //////////////////////////////////////////////////////////////////////////*/ + + if (params.expiration > 0) { + vars.clawbackAmount = uint128(asset.balanceOf(address(vars.merkleLockupLD))); + vm.warp({ timestamp: uint256(params.expiration) + 1 seconds }); + + changePrank({ msgSender: params.admin }); + expectCallToTransfer({ to: params.admin, amount: vars.clawbackAmount }); + vm.expectEmit({ emitter: address(vars.merkleLockupLD) }); + emit Clawback({ to: params.admin, admin: params.admin, amount: vars.clawbackAmount }); + vars.merkleLockupLD.clawback({ to: params.admin, amount: vars.clawbackAmount }); + } + } +} diff --git a/test/fork/merkle-lockup/MerkleLockupLL.t.sol b/test/fork/merkle-lockup/MerkleLockupLL.t.sol index 7b5c6882..a25779ad 100644 --- a/test/fork/merkle-lockup/MerkleLockupLL.t.sol +++ b/test/fork/merkle-lockup/MerkleLockupLL.t.sol @@ -76,7 +76,9 @@ abstract contract MerkleLockupLL_Fork_Test is Fork_Test { vars.indexes[i] = params.leafData[i].index; // Bound each leaf amount so that `aggregateAmount` does not overflow. - vars.amounts[i] = uint128(_bound(params.leafData[i].amount, 1, MAX_UINT256 / vars.recipientsCount - 1)); + params.leafData[i].amount = + uint128(_bound(params.leafData[i].amount, 1, MAX_UINT256 / vars.recipientsCount - 1)); + vars.amounts[i] = params.leafData[i].amount; vars.aggregateAmount += params.leafData[i].amount; // Avoid zero recipient addresses. @@ -85,7 +87,7 @@ abstract contract MerkleLockupLL_Fork_Test is Fork_Test { } leaves = new uint256[](vars.recipientsCount); - leaves = MerkleBuilder.computeLeaves(vars.indexes, vars.recipients, vars.amounts); + leaves = MerkleBuilder.computeLeavesLL(vars.indexes, vars.recipients, vars.amounts); // Sort the leaves in ascending order to match the production environment. MerkleBuilder.sortLeaves(leaves); @@ -132,7 +134,7 @@ abstract contract MerkleLockupLL_Fork_Test is Fork_Test { assertFalse(vars.merkleLockupLL.hasClaimed(vars.indexes[params.posBeforeSort])); - vars.leafToClaim = MerkleBuilder.computeLeaf( + vars.leafToClaim = MerkleBuilder.computeLeafLL( vars.indexes[params.posBeforeSort], vars.recipients[params.posBeforeSort], vars.amounts[params.posBeforeSort] @@ -170,7 +172,19 @@ abstract contract MerkleLockupLL_Fork_Test is Fork_Test { assertTrue(vars.merkleLockupLL.hasClaimed(vars.indexes[params.posBeforeSort])); assertEq(vars.actualStreamId, vars.expectedStreamId); - assertEq(vars.actualStream, vars.expectedStream); + assertEq(vars.actualStream.amounts.deposited, vars.expectedStream.amounts.deposited); + assertEq(vars.actualStream.amounts.refunded, vars.expectedStream.amounts.refunded); + assertEq(vars.actualStream.amounts.withdrawn, vars.expectedStream.amounts.withdrawn); + assertEq(vars.actualStream.asset, vars.expectedStream.asset); + assertEq(vars.actualStream.cliffTime, vars.expectedStream.cliffTime); + assertEq(vars.actualStream.endTime, vars.expectedStream.endTime); + assertEq(vars.actualStream.isCancelable, vars.expectedStream.isCancelable); + assertEq(vars.actualStream.isDepleted, vars.expectedStream.isDepleted); + assertEq(vars.actualStream.isStream, vars.expectedStream.isStream); + assertEq(vars.actualStream.isTransferable, vars.expectedStream.isTransferable); + assertEq(vars.actualStream.sender, vars.expectedStream.sender); + assertEq(vars.actualStream.startTime, vars.expectedStream.startTime); + assertEq(vars.actualStream.wasCanceled, vars.expectedStream.wasCanceled); /*////////////////////////////////////////////////////////////////////////// CLAWBACK diff --git a/test/integration/merkle-lockup/MerkleLockup.t.sol b/test/integration/merkle-lockup/MerkleLockup.t.sol index 16a2c43b..e0c7757c 100644 --- a/test/integration/merkle-lockup/MerkleLockup.t.sol +++ b/test/integration/merkle-lockup/MerkleLockup.t.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22; +import { ISablierV2MerkleLockupLD } from "src/interfaces/ISablierV2MerkleLockupLD.sol"; import { ISablierV2MerkleLockupLL } from "src/interfaces/ISablierV2MerkleLockupLL.sol"; import { Integration_Test } from "../Integration.t.sol"; @@ -9,32 +10,89 @@ abstract contract MerkleLockup_Integration_Test is Integration_Test { function setUp() public virtual override { Integration_Test.setUp(); - // Create the default Merkle Lockup. + // Create the default Merkle Lockup contracts. merkleLockupLL = createMerkleLockupLL(); + merkleLockupLD = createMerkleLockupLD(); - // Fund the Merkle Lockup contract. + // Fund the Merkle Lockup contracts. deal({ token: address(asset), to: address(merkleLockupLL), give: defaults.AGGREGATE_AMOUNT() }); + deal({ token: address(asset), to: address(merkleLockupLD), give: defaults.AGGREGATE_AMOUNT() }); } + /*////////////////////////////////////////////////////////////////////////// + MERKLE-LOCKUP-LD + //////////////////////////////////////////////////////////////////////////*/ + + function claimLD() internal returns (uint256) { + return merkleLockupLD.claim({ + index: defaults.INDEX1(), + recipient: users.recipient1, + segments: defaults.segmentsWithDurations(), + merkleProof: defaults.index1ProofLD() + }); + } + + function computeMerkleLockupLDAddress() internal returns (address) { + return computeMerkleLockupLDAddress(users.admin, defaults.MERKLE_ROOT_LD(), defaults.EXPIRATION()); + } + + function computeMerkleLockupLDAddress(address admin) internal returns (address) { + return computeMerkleLockupLDAddress(admin, defaults.MERKLE_ROOT_LD(), defaults.EXPIRATION()); + } + + function computeMerkleLockupLDAddress(address admin, uint40 expiration) internal returns (address) { + return computeMerkleLockupLDAddress(admin, defaults.MERKLE_ROOT_LD(), expiration); + } + + function computeMerkleLockupLDAddress(address admin, bytes32 merkleRoot) internal returns (address) { + return computeMerkleLockupLDAddress(admin, merkleRoot, defaults.EXPIRATION()); + } + + function createMerkleLockupLD() internal returns (ISablierV2MerkleLockupLD) { + return createMerkleLockupLD(users.admin, defaults.EXPIRATION()); + } + + function createMerkleLockupLD(address admin) internal returns (ISablierV2MerkleLockupLD) { + return createMerkleLockupLD(admin, defaults.EXPIRATION()); + } + + function createMerkleLockupLD(uint40 expiration) internal returns (ISablierV2MerkleLockupLD) { + return createMerkleLockupLD(users.admin, expiration); + } + + function createMerkleLockupLD(address admin, uint40 expiration) internal returns (ISablierV2MerkleLockupLD) { + return merkleLockupFactory.createMerkleLockupLD({ + baseParams: defaults.baseParams(admin, defaults.MERKLE_ROOT_LD(), expiration), + lockupDynamic: lockupDynamic, + ipfsCID: defaults.IPFS_CID(), + aggregateAmount: defaults.AGGREGATE_AMOUNT(), + recipientsCount: defaults.RECIPIENTS_COUNT() + }); + } + + /*////////////////////////////////////////////////////////////////////////// + MERKLE-LOCKUP-LL + //////////////////////////////////////////////////////////////////////////*/ + function claimLL() internal returns (uint256) { return merkleLockupLL.claim({ index: defaults.INDEX1(), recipient: users.recipient1, amount: defaults.CLAIM_AMOUNT(), - merkleProof: defaults.index1Proof() + merkleProof: defaults.index1ProofLL() }); } function computeMerkleLockupLLAddress() internal returns (address) { - return computeMerkleLockupLLAddress(users.admin, defaults.MERKLE_ROOT(), defaults.EXPIRATION()); + return computeMerkleLockupLLAddress(users.admin, defaults.MERKLE_ROOT_LL(), defaults.EXPIRATION()); } function computeMerkleLockupLLAddress(address admin) internal returns (address) { - return computeMerkleLockupLLAddress(admin, defaults.MERKLE_ROOT(), defaults.EXPIRATION()); + return computeMerkleLockupLLAddress(admin, defaults.MERKLE_ROOT_LL(), defaults.EXPIRATION()); } function computeMerkleLockupLLAddress(address admin, uint40 expiration) internal returns (address) { - return computeMerkleLockupLLAddress(admin, defaults.MERKLE_ROOT(), expiration); + return computeMerkleLockupLLAddress(admin, defaults.MERKLE_ROOT_LL(), expiration); } function computeMerkleLockupLLAddress(address admin, bytes32 merkleRoot) internal returns (address) { @@ -55,7 +113,7 @@ abstract contract MerkleLockup_Integration_Test is Integration_Test { function createMerkleLockupLL(address admin, uint40 expiration) internal returns (ISablierV2MerkleLockupLL) { return merkleLockupFactory.createMerkleLockupLL({ - baseParams: defaults.baseParams(admin, defaults.MERKLE_ROOT(), expiration), + baseParams: defaults.baseParams(admin, defaults.MERKLE_ROOT_LL(), expiration), lockupLinear: lockupLinear, streamDurations: defaults.durations(), ipfsCID: defaults.IPFS_CID(), diff --git a/test/integration/merkle-lockup/factory/create-merkle-lockup-ld/createMerkleLockupLD.t.sol b/test/integration/merkle-lockup/factory/create-merkle-lockup-ld/createMerkleLockupLD.t.sol new file mode 100644 index 00000000..d202d240 --- /dev/null +++ b/test/integration/merkle-lockup/factory/create-merkle-lockup-ld/createMerkleLockupLD.t.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { Errors } from "src/libraries/Errors.sol"; +import { ISablierV2MerkleLockupLD } from "src/interfaces/ISablierV2MerkleLockupLD.sol"; +import { MerkleLockup } from "src/types/DataTypes.sol"; + +import { MerkleLockup_Integration_Test } from "../../MerkleLockup.t.sol"; + +contract CreateMerkleLockupLD_Integration_Test is MerkleLockup_Integration_Test { + function setUp() public override { + MerkleLockup_Integration_Test.setUp(); + } + + function test_RevertWhen_CampaignNameTooLong() external { + MerkleLockup.ConstructorParams memory baseParams = defaults.baseParamsLD(); + 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.SablierV2MerkleLockup_CampaignNameTooLong.selector, bytes(baseParams.name).length, 32 + ) + ); + + merkleLockupFactory.createMerkleLockupLD({ + baseParams: baseParams, + lockupDynamic: lockupDynamic, + ipfsCID: ipfsCID, + aggregateAmount: aggregateAmount, + recipientsCount: recipientsCount + }); + } + + modifier whenCampaignNameIsNotTooLong() { + _; + } + + /// @dev This test works because a default Merkle Lockup contract is deployed in {Integration_Test.setUp} + function test_RevertGiven_AlreadyCreated() external whenCampaignNameIsNotTooLong { + MerkleLockup.ConstructorParams memory baseParams = defaults.baseParamsLD(); + string memory ipfsCID = defaults.IPFS_CID(); + uint256 aggregateAmount = defaults.AGGREGATE_AMOUNT(); + uint256 recipientsCount = defaults.RECIPIENTS_COUNT(); + + vm.expectRevert(); + merkleLockupFactory.createMerkleLockupLD({ + baseParams: baseParams, + lockupDynamic: lockupDynamic, + ipfsCID: ipfsCID, + aggregateAmount: aggregateAmount, + recipientsCount: recipientsCount + }); + } + + modifier givenNotAlreadyCreated() { + _; + } + + function testFuzz_CreateMerkleLockupLD( + address admin, + uint40 expiration + ) + external + whenCampaignNameIsNotTooLong + givenNotAlreadyCreated + { + vm.assume(admin != users.admin); + address expectedLockupLD = computeMerkleLockupLDAddress(admin, expiration); + + MerkleLockup.ConstructorParams memory baseParams = + defaults.baseParams({ admin: admin, merkleRoot: defaults.MERKLE_ROOT_LD(), expiration: expiration }); + + vm.expectEmit({ emitter: address(merkleLockupFactory) }); + emit CreateMerkleLockupLD({ + merkleLockupLD: ISablierV2MerkleLockupLD(expectedLockupLD), + baseParams: baseParams, + lockupDynamic: lockupDynamic, + ipfsCID: defaults.IPFS_CID(), + aggregateAmount: defaults.AGGREGATE_AMOUNT(), + recipientsCount: defaults.RECIPIENTS_COUNT() + }); + + address actualLockupLD = address(createMerkleLockupLD(admin, expiration)); + + assertGt(actualLockupLD.code.length, 0, "MerkleLockupLD contract not created"); + assertEq(actualLockupLD, expectedLockupLD, "MerkleLockupLD contract does not match computed address"); + } +} diff --git a/test/integration/merkle-lockup/factory/create-merkle-lockup-ld/createMerkleLockupLD.tree b/test/integration/merkle-lockup/factory/create-merkle-lockup-ld/createMerkleLockupLD.tree new file mode 100644 index 00000000..6e36d45e --- /dev/null +++ b/test/integration/merkle-lockup/factory/create-merkle-lockup-ld/createMerkleLockupLD.tree @@ -0,0 +1,9 @@ +createMerkleLockupLD.t.sol +├── when the campaign name is too long +│ └── it should revert +└── when the campaign name is not too long + ├── given the campaign has been already created + │ └── it should revert + └── given the campaign has not been already created + ├── it should create the campaign + └── it should emit a {CreateMerkleLockupLD} event diff --git a/test/integration/merkle-lockup/factory/create-merkle-lockup-ll/createMerkleLockupLL.t.sol b/test/integration/merkle-lockup/factory/create-merkle-lockup-ll/createMerkleLockupLL.t.sol index 60f35400..f5f51973 100644 --- a/test/integration/merkle-lockup/factory/create-merkle-lockup-ll/createMerkleLockupLL.t.sol +++ b/test/integration/merkle-lockup/factory/create-merkle-lockup-ll/createMerkleLockupLL.t.sol @@ -15,7 +15,7 @@ contract CreateMerkleLockupLL_Integration_Test is MerkleLockup_Integration_Test } function test_RevertWhen_CampaignNameTooLong() external { - MerkleLockup.ConstructorParams memory baseParams = defaults.baseParams(); + MerkleLockup.ConstructorParams memory baseParams = defaults.baseParamsLL(); LockupLinear.Durations memory streamDurations = defaults.durations(); string memory ipfsCID = defaults.IPFS_CID(); uint256 aggregateAmount = defaults.AGGREGATE_AMOUNT(); @@ -45,7 +45,7 @@ contract CreateMerkleLockupLL_Integration_Test is MerkleLockup_Integration_Test /// @dev This test works because a default Merkle Lockup contract is deployed in {Integration_Test.setUp} function test_RevertGiven_AlreadyCreated() external whenCampaignNameIsNotTooLong { - MerkleLockup.ConstructorParams memory baseParams = defaults.baseParams(); + MerkleLockup.ConstructorParams memory baseParams = defaults.baseParamsLL(); LockupLinear.Durations memory streamDurations = defaults.durations(); string memory ipfsCID = defaults.IPFS_CID(); uint256 aggregateAmount = defaults.AGGREGATE_AMOUNT(); @@ -78,7 +78,7 @@ contract CreateMerkleLockupLL_Integration_Test is MerkleLockup_Integration_Test address expectedLockupLL = computeMerkleLockupLLAddress(admin, expiration); MerkleLockup.ConstructorParams memory baseParams = - defaults.baseParams({ admin: admin, merkleRoot: defaults.MERKLE_ROOT(), expiration: expiration }); + defaults.baseParams({ admin: admin, merkleRoot: defaults.MERKLE_ROOT_LL(), expiration: expiration }); vm.expectEmit({ emitter: address(merkleLockupFactory) }); emit CreateMerkleLockupLL({ diff --git a/test/integration/merkle-lockup/ld/claim/claim.t.sol b/test/integration/merkle-lockup/ld/claim/claim.t.sol new file mode 100644 index 00000000..9c4b48b8 --- /dev/null +++ b/test/integration/merkle-lockup/ld/claim/claim.t.sol @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { Lockup, LockupDynamic } from "@sablier/v2-core/src/types/DataTypes.sol"; + +import { Errors } from "src/libraries/Errors.sol"; + +import { MerkleLockup_Integration_Test } from "../../MerkleLockup.t.sol"; + +contract ClaimLD_Integration_Test is MerkleLockup_Integration_Test { + function setUp() public virtual override { + MerkleLockup_Integration_Test.setUp(); + } + + function test_RevertGiven_CampaignExpired() external { + uint40 expiration = defaults.EXPIRATION(); + uint256 warpTime = expiration + 1 seconds; + LockupDynamic.SegmentWithDuration[] memory segments = defaults.segmentsWithDurations(); + bytes32[] memory merkleProof; + vm.warp({ timestamp: warpTime }); + vm.expectRevert( + abi.encodeWithSelector(Errors.SablierV2MerkleLockup_CampaignExpired.selector, warpTime, expiration) + ); + merkleLockupLD.claim({ index: 1, recipient: users.recipient1, segments: segments, merkleProof: merkleProof }); + } + + modifier givenCampaignNotExpired() { + _; + } + + function test_RevertGiven_AlreadyClaimed() external givenCampaignNotExpired { + claimLD(); + uint256 index1 = defaults.INDEX1(); + LockupDynamic.SegmentWithDuration[] memory segments = defaults.segmentsWithDurations(); + bytes32[] memory merkleProof = defaults.index1ProofLD(); + vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2MerkleLockup_StreamClaimed.selector, index1)); + merkleLockupLD.claim({ index: index1, recipient: users.recipient1, segments: segments, merkleProof: merkleProof }); + } + + modifier givenNotClaimed() { + _; + } + + modifier givenNotIncludedInMerkleTree() { + _; + } + + function test_RevertWhen_InvalidIndex() + external + givenCampaignNotExpired + givenNotClaimed + givenNotIncludedInMerkleTree + { + uint256 invalidIndex = 1337; + LockupDynamic.SegmentWithDuration[] memory segments = defaults.segmentsWithDurations(); + bytes32[] memory merkleProof = defaults.index1ProofLD(); + vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2MerkleLockup_InvalidProof.selector)); + merkleLockupLD.claim({ + index: invalidIndex, + recipient: users.recipient1, + segments: segments, + merkleProof: merkleProof + }); + } + + function test_RevertWhen_InvalidRecipient() + external + givenCampaignNotExpired + givenNotClaimed + givenNotIncludedInMerkleTree + { + uint256 index1 = defaults.INDEX1(); + address invalidRecipient = address(1337); + LockupDynamic.SegmentWithDuration[] memory segments = defaults.segmentsWithDurations(); + bytes32[] memory merkleProof = defaults.index1ProofLD(); + vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2MerkleLockup_InvalidProof.selector)); + merkleLockupLD.claim({ index: index1, recipient: invalidRecipient, segments: segments, merkleProof: merkleProof }); + } + + function test_RevertWhen_InvalidSegments() + external + givenCampaignNotExpired + givenNotClaimed + givenNotIncludedInMerkleTree + { + uint256 index1 = defaults.INDEX1(); + LockupDynamic.SegmentWithDuration[] memory segmentsWithInvalidAmounts = + defaults.segmentsWithDurations(1000, 8000); + bytes32[] memory merkleProof = defaults.index1ProofLD(); + vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2MerkleLockup_InvalidProof.selector)); + merkleLockupLD.claim({ + index: index1, + recipient: users.recipient1, + segments: segmentsWithInvalidAmounts, + merkleProof: merkleProof + }); + } + + function test_RevertWhen_InvalidMerkleProof() + external + givenCampaignNotExpired + givenNotClaimed + givenNotIncludedInMerkleTree + { + uint256 index1 = defaults.INDEX1(); + LockupDynamic.SegmentWithDuration[] memory segments = defaults.segmentsWithDurations(); + bytes32[] memory invalidMerkleProof = defaults.index2ProofLD(); + vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2MerkleLockup_InvalidProof.selector)); + merkleLockupLD.claim({ + index: index1, + recipient: users.recipient1, + segments: segments, + merkleProof: invalidMerkleProof + }); + } + + modifier givenIncludedInMerkleTree() { + _; + } + + modifier givenProtocolFeeZero() { + _; + } + + function test_Claim() + external + givenCampaignNotExpired + givenNotClaimed + givenIncludedInMerkleTree + givenProtocolFeeZero + { + uint256 expectedStreamId = lockupDynamic.nextStreamId(); + + vm.expectEmit({ emitter: address(merkleLockupLD) }); + emit Claim(defaults.INDEX1(), users.recipient1, defaults.CLAIM_AMOUNT(), expectedStreamId); + uint256 actualStreamId = claimLD(); + + LockupDynamic.Stream memory actualStream = lockupDynamic.getStream(actualStreamId); + LockupDynamic.Stream memory expectedStream = LockupDynamic.Stream({ + amounts: Lockup.Amounts({ deposited: defaults.CLAIM_AMOUNT(), refunded: 0, withdrawn: 0 }), + asset: asset, + endTime: uint40(block.timestamp) + defaults.TOTAL_DURATION(), + isCancelable: defaults.CANCELABLE(), + isDepleted: false, + isStream: true, + isTransferable: defaults.TRANSFERABLE(), + segments: defaults.segments(), + sender: users.admin, + startTime: uint40(block.timestamp), + wasCanceled: false + }); + + assertTrue(merkleLockupLD.hasClaimed(defaults.INDEX1()), "not claimed"); + assertEq(actualStreamId, expectedStreamId, "invalid stream id"); + + assertEq(actualStream.amounts.deposited, expectedStream.amounts.deposited); + assertEq(actualStream.amounts.refunded, expectedStream.amounts.refunded); + assertEq(actualStream.amounts.withdrawn, expectedStream.amounts.withdrawn); + assertEq(actualStream.asset, expectedStream.asset); + assertEq(actualStream.endTime, expectedStream.endTime); + assertEq(actualStream.isCancelable, expectedStream.isCancelable); + assertEq(actualStream.isDepleted, expectedStream.isDepleted); + assertEq(actualStream.isStream, expectedStream.isStream); + assertEq(actualStream.isTransferable, expectedStream.isTransferable); + assertEq(actualStream.segments[0].amount, expectedStream.segments[0].amount); + assertEq(actualStream.segments[0].exponent, expectedStream.segments[0].exponent); + assertEq(actualStream.segments[0].timestamp, expectedStream.segments[0].timestamp); + assertEq(actualStream.segments[1].amount, expectedStream.segments[1].amount); + assertEq(actualStream.segments[1].exponent, expectedStream.segments[1].exponent); + assertEq(actualStream.segments[1].timestamp, expectedStream.segments[1].timestamp); + assertEq(actualStream.sender, expectedStream.sender); + assertEq(actualStream.startTime, expectedStream.startTime); + assertEq(actualStream.wasCanceled, expectedStream.wasCanceled); + } +} diff --git a/test/integration/merkle-lockup/ld/claim/claim.tree b/test/integration/merkle-lockup/ld/claim/claim.tree new file mode 100644 index 00000000..b9932a2d --- /dev/null +++ b/test/integration/merkle-lockup/ld/claim/claim.tree @@ -0,0 +1,21 @@ +claim.t.sol +├── given the campaign has expired +│ └── it should revert +└── given the campaign has not expired + ├── given the recipient has claimed + │ └── it should revert + └── given the recipient has not claimed + ├── given the claim is not included in the Merkle tree + │ ├── when the index is not valid + │ │ └── it should revert + │ ├── when the recipient address is not valid + │ │ └── it should revert + │ ├── when the segments are not valid + │ │ └── it should revert + │ └── when the Merkle proof is not valid + │ └── it should revert + └── given the claim is included in the Merkle tree + └── given the protocol fee is not greater than zero + ├── it should mark the index as claimed + ├── it should create a LockupLinear stream + └── it should emit a {Claim} event diff --git a/test/integration/merkle-lockup/ld/clawback/clawback.t.sol b/test/integration/merkle-lockup/ld/clawback/clawback.t.sol new file mode 100644 index 00000000..79d6b23e --- /dev/null +++ b/test/integration/merkle-lockup/ld/clawback/clawback.t.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { Errors as V2CoreErrors } from "@sablier/v2-core/src/libraries/Errors.sol"; + +import { Errors } from "src/libraries/Errors.sol"; + +import { MerkleLockup_Integration_Test } from "../../MerkleLockup.t.sol"; + +contract ClawbackLD_Integration_Test is MerkleLockup_Integration_Test { + function setUp() public virtual override { + MerkleLockup_Integration_Test.setUp(); + } + + function test_RevertWhen_CallerNotAdmin() external { + changePrank({ msgSender: users.eve }); + vm.expectRevert(abi.encodeWithSelector(V2CoreErrors.CallerNotAdmin.selector, users.admin, users.eve)); + merkleLockupLD.clawback({ to: users.eve, amount: 1 }); + } + + modifier whenCallerAdmin() { + changePrank({ msgSender: users.admin }); + _; + } + + function test_RevertGiven_CampaignNotExpired() external whenCallerAdmin { + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierV2MerkleLockup_CampaignNotExpired.selector, block.timestamp, defaults.EXPIRATION() + ) + ); + merkleLockupLD.clawback({ to: users.admin, amount: 1 }); + } + + modifier givenCampaignExpired() { + // Make a claim to have a different contract balance. + claimLD(); + vm.warp({ timestamp: defaults.EXPIRATION() + 1 seconds }); + _; + } + + function test_Clawback() external whenCallerAdmin givenCampaignExpired { + test_Clawback(users.admin); + } + + function testFuzz_Clawback(address to) external whenCallerAdmin givenCampaignExpired { + vm.assume(to != address(0)); + test_Clawback(to); + } + + function test_Clawback(address to) internal { + uint128 clawbackAmount = uint128(asset.balanceOf(address(merkleLockupLD))); + expectCallToTransfer({ to: to, amount: clawbackAmount }); + vm.expectEmit({ emitter: address(merkleLockupLD) }); + emit Clawback({ admin: users.admin, to: to, amount: clawbackAmount }); + merkleLockupLD.clawback({ to: to, amount: clawbackAmount }); + } +} diff --git a/test/integration/merkle-lockup/ld/clawback/clawback.tree b/test/integration/merkle-lockup/ld/clawback/clawback.tree new file mode 100644 index 00000000..feef74bc --- /dev/null +++ b/test/integration/merkle-lockup/ld/clawback/clawback.tree @@ -0,0 +1,9 @@ +clawback.t.sol +├── when the caller is not the admin +│ └── it should revert +└── when the caller is the admin + ├── given the campaign has not expired + │ └── it should revert + └── given the campaign has expired + ├── it should perform the ERC-20 transfer + └── it should emit a {Clawback} event diff --git a/test/integration/merkle-lockup/ld/constructor/constructor.t.sol b/test/integration/merkle-lockup/ld/constructor/constructor.t.sol new file mode 100644 index 00000000..71a154c5 --- /dev/null +++ b/test/integration/merkle-lockup/ld/constructor/constructor.t.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { LockupDynamic } from "@sablier/v2-core/src/types/DataTypes.sol"; + +import { SablierV2MerkleLockupLD } from "src/SablierV2MerkleLockupLD.sol"; + +import { MerkleLockup_Integration_Test } from "../../MerkleLockup.t.sol"; + +contract Constructor_MerkleLockupLD_Integration_Test is MerkleLockup_Integration_Test { + /// @dev Needed to prevent "Stack too deep" error + struct Vars { + address actualAdmin; + uint256 actualAllowance; + address actualAsset; + string actualName; + bool actualCancelable; + bool actualTransferable; + uint40 actualExpiration; + address actualLockupDynamic; + bytes32 actualMerkleRoot; + address expectedAdmin; + uint256 expectedAllowance; + address expectedAsset; + bytes32 expectedName; + bool expectedCancelable; + bool expectedTransferable; + uint40 expectedExpiration; + address expectedLockupDynamic; + bytes32 expectedMerkleRoot; + } + + function test_Constructor() external { + SablierV2MerkleLockupLD constructedLockupLD = + new SablierV2MerkleLockupLD(defaults.baseParamsLD(), lockupDynamic); + + Vars memory vars; + + vars.actualAdmin = constructedLockupLD.admin(); + vars.expectedAdmin = users.admin; + assertEq(vars.actualAdmin, vars.expectedAdmin, "admin"); + + vars.actualAsset = address(constructedLockupLD.ASSET()); + vars.expectedAsset = address(asset); + assertEq(vars.actualAsset, vars.expectedAsset, "asset"); + + vars.actualName = constructedLockupLD.name(); + vars.expectedName = defaults.NAME_BYTES32(); + assertEq(bytes32(abi.encodePacked(vars.actualName)), vars.expectedName, "name"); + + vars.actualMerkleRoot = constructedLockupLD.MERKLE_ROOT(); + vars.expectedMerkleRoot = defaults.MERKLE_ROOT_LD(); + assertEq(vars.actualMerkleRoot, vars.expectedMerkleRoot, "merkleRoot"); + + vars.actualCancelable = constructedLockupLD.CANCELABLE(); + vars.expectedCancelable = defaults.CANCELABLE(); + assertEq(vars.actualCancelable, vars.expectedCancelable, "cancelable"); + + vars.actualTransferable = constructedLockupLD.TRANSFERABLE(); + vars.expectedTransferable = defaults.TRANSFERABLE(); + assertEq(vars.actualTransferable, vars.expectedTransferable, "transferable"); + + vars.actualExpiration = constructedLockupLD.EXPIRATION(); + vars.expectedExpiration = defaults.EXPIRATION(); + assertEq(vars.actualExpiration, vars.expectedExpiration, "expiration"); + + vars.actualLockupDynamic = address(constructedLockupLD.LOCKUP_DYNAMIC()); + vars.expectedLockupDynamic = address(lockupDynamic); + assertEq(vars.actualLockupDynamic, vars.expectedLockupDynamic, "lockupDynamic"); + + vars.actualAllowance = asset.allowance(address(constructedLockupLD), address(lockupDynamic)); + vars.expectedAllowance = MAX_UINT256; + assertEq(vars.actualAllowance, vars.expectedAllowance, "allowance"); + } +} diff --git a/test/integration/merkle-lockup/ld/has-claimed/hasClaimed.t.sol b/test/integration/merkle-lockup/ld/has-claimed/hasClaimed.t.sol new file mode 100644 index 00000000..c2accfbc --- /dev/null +++ b/test/integration/merkle-lockup/ld/has-claimed/hasClaimed.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { MerkleLockup_Integration_Test } from "../../MerkleLockup.t.sol"; + +contract HasClaimedLD_Integration_Test is MerkleLockup_Integration_Test { + function setUp() public virtual override { + MerkleLockup_Integration_Test.setUp(); + } + + function test_HasClaimed_IndexNotInTree() external { + uint256 indexNotInTree = 1337e18; + assertFalse(merkleLockupLD.hasClaimed(indexNotInTree), "claimed"); + } + + modifier whenIndexInTree() { + _; + } + + function test_HasClaimed_NotClaimed() external whenIndexInTree { + assertFalse(merkleLockupLD.hasClaimed(defaults.INDEX1()), "claimed"); + } + + modifier givenRecipientHasClaimed() { + claimLD(); + _; + } + + function test_HasClaimed() external whenIndexInTree givenRecipientHasClaimed { + assertTrue(merkleLockupLD.hasClaimed(defaults.INDEX1()), "not claimed"); + } +} diff --git a/test/integration/merkle-lockup/ld/has-claimed/hasClaimed.tree b/test/integration/merkle-lockup/ld/has-claimed/hasClaimed.tree new file mode 100644 index 00000000..dcbcafe7 --- /dev/null +++ b/test/integration/merkle-lockup/ld/has-claimed/hasClaimed.tree @@ -0,0 +1,8 @@ +hasClaimed.t.sol +├── when the index is not in the Merkle tree +│ └── it should return false +└── when the index is in the Merkle tree + ├── given the recipient has not claimed + │ └── it should return false + └── given the recipient has claimed + └── it should return true diff --git a/test/integration/merkle-lockup/ld/has-expired/hasExpired.t.sol b/test/integration/merkle-lockup/ld/has-expired/hasExpired.t.sol new file mode 100644 index 00000000..423e2df9 --- /dev/null +++ b/test/integration/merkle-lockup/ld/has-expired/hasExpired.t.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { ISablierV2MerkleLockupLD } from "src/interfaces/ISablierV2MerkleLockupLD.sol"; + +import { MerkleLockup_Integration_Test } from "../../MerkleLockup.t.sol"; + +contract HasExpiredLD_Integration_Test is MerkleLockup_Integration_Test { + function setUp() public virtual override { + MerkleLockup_Integration_Test.setUp(); + } + + function test_HasExpired_ExpirationZero() external { + ISablierV2MerkleLockupLD testLockup = createMerkleLockupLD({ expiration: 0 }); + assertFalse(testLockup.hasExpired(), "campaign expired"); + } + + modifier whenExpirationNotZero() { + _; + } + + function test_HasExpired_ExpirationLessThanCurrentTime() external whenExpirationNotZero { + assertFalse(merkleLockupLD.hasExpired(), "campaign expired"); + } + + function test_HasExpired_ExpirationEqualToCurrentTime() external whenExpirationNotZero { + vm.warp({ timestamp: defaults.EXPIRATION() }); + assertTrue(merkleLockupLD.hasExpired(), "campaign not expired"); + } + + function test_HasExpired_ExpirationGreaterThanCurrentTime() external whenExpirationNotZero { + vm.warp({ timestamp: defaults.EXPIRATION() + 1 seconds }); + assertTrue(merkleLockupLD.hasExpired(), "campaign not expired"); + } +} diff --git a/test/integration/merkle-lockup/ld/has-expired/hasExpired.tree b/test/integration/merkle-lockup/ld/has-expired/hasExpired.tree new file mode 100644 index 00000000..18fd5766 --- /dev/null +++ b/test/integration/merkle-lockup/ld/has-expired/hasExpired.tree @@ -0,0 +1,10 @@ +hasExpired.t.sol +├── when the expiration is zero +│ └── it should return false +└── when the expiration is not zero + ├── when the expiration is less than current time + │ └── it should return false + ├── when the expiration is equal to the current time + │ └── it should return true + └── when the expiration is greater than current time + └── it should return true diff --git a/test/integration/merkle-lockup/ll/claim/claim.t.sol b/test/integration/merkle-lockup/ll/claim/claim.t.sol index c9543ce2..4f238eb3 100644 --- a/test/integration/merkle-lockup/ll/claim/claim.t.sol +++ b/test/integration/merkle-lockup/ll/claim/claim.t.sol @@ -8,7 +8,7 @@ import { Errors } from "src/libraries/Errors.sol"; import { MerkleLockup_Integration_Test } from "../../MerkleLockup.t.sol"; -contract Claim_Integration_Test is MerkleLockup_Integration_Test { +contract ClaimLL_Integration_Test is MerkleLockup_Integration_Test { function setUp() public virtual override { MerkleLockup_Integration_Test.setUp(); } @@ -32,7 +32,7 @@ contract Claim_Integration_Test is MerkleLockup_Integration_Test { claimLL(); uint256 index1 = defaults.INDEX1(); uint128 amount = defaults.CLAIM_AMOUNT(); - bytes32[] memory merkleProof = defaults.index1Proof(); + bytes32[] memory merkleProof = defaults.index1ProofLL(); vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2MerkleLockup_StreamClaimed.selector, index1)); merkleLockupLL.claim(index1, users.recipient1, amount, merkleProof); } @@ -53,7 +53,7 @@ contract Claim_Integration_Test is MerkleLockup_Integration_Test { { uint256 invalidIndex = 1337; uint128 amount = defaults.CLAIM_AMOUNT(); - bytes32[] memory merkleProof = defaults.index1Proof(); + bytes32[] memory merkleProof = defaults.index1ProofLL(); vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2MerkleLockup_InvalidProof.selector)); merkleLockupLL.claim(invalidIndex, users.recipient1, amount, merkleProof); } @@ -67,7 +67,7 @@ contract Claim_Integration_Test is MerkleLockup_Integration_Test { uint256 index1 = defaults.INDEX1(); address invalidRecipient = address(1337); uint128 amount = defaults.CLAIM_AMOUNT(); - bytes32[] memory merkleProof = defaults.index1Proof(); + bytes32[] memory merkleProof = defaults.index1ProofLL(); vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2MerkleLockup_InvalidProof.selector)); merkleLockupLL.claim(index1, invalidRecipient, amount, merkleProof); } @@ -80,7 +80,7 @@ contract Claim_Integration_Test is MerkleLockup_Integration_Test { { uint256 index1 = defaults.INDEX1(); uint128 invalidAmount = 1337; - bytes32[] memory merkleProof = defaults.index1Proof(); + bytes32[] memory merkleProof = defaults.index1ProofLL(); vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2MerkleLockup_InvalidProof.selector)); merkleLockupLL.claim(index1, users.recipient1, invalidAmount, merkleProof); } @@ -93,7 +93,7 @@ contract Claim_Integration_Test is MerkleLockup_Integration_Test { { uint256 index1 = defaults.INDEX1(); uint128 amount = defaults.CLAIM_AMOUNT(); - bytes32[] memory invalidMerkleProof = defaults.index2Proof(); + bytes32[] memory invalidMerkleProof = defaults.index2ProofLL(); vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2MerkleLockup_InvalidProof.selector)); merkleLockupLL.claim(index1, users.recipient1, amount, invalidMerkleProof); } @@ -153,6 +153,19 @@ contract Claim_Integration_Test is MerkleLockup_Integration_Test { assertTrue(merkleLockupLL.hasClaimed(defaults.INDEX1()), "not claimed"); assertEq(actualStreamId, expectedStreamId, "invalid stream id"); - assertEq(actualStream, expectedStream); + + assertEq(actualStream.amounts.deposited, expectedStream.amounts.deposited); + assertEq(actualStream.amounts.refunded, expectedStream.amounts.refunded); + assertEq(actualStream.amounts.withdrawn, expectedStream.amounts.withdrawn); + assertEq(actualStream.asset, expectedStream.asset); + assertEq(actualStream.cliffTime, expectedStream.cliffTime); + assertEq(actualStream.endTime, expectedStream.endTime); + assertEq(actualStream.isCancelable, expectedStream.isCancelable); + assertEq(actualStream.isDepleted, expectedStream.isDepleted); + assertEq(actualStream.isStream, expectedStream.isStream); + assertEq(actualStream.isTransferable, expectedStream.isTransferable); + assertEq(actualStream.sender, expectedStream.sender); + assertEq(actualStream.startTime, expectedStream.startTime); + assertEq(actualStream.wasCanceled, expectedStream.wasCanceled); } } diff --git a/test/integration/merkle-lockup/ll/clawback/clawback.t.sol b/test/integration/merkle-lockup/ll/clawback/clawback.t.sol index 1ea18bed..25686e5b 100644 --- a/test/integration/merkle-lockup/ll/clawback/clawback.t.sol +++ b/test/integration/merkle-lockup/ll/clawback/clawback.t.sol @@ -7,7 +7,7 @@ import { Errors } from "src/libraries/Errors.sol"; import { MerkleLockup_Integration_Test } from "../../MerkleLockup.t.sol"; -contract Clawback_Integration_Test is MerkleLockup_Integration_Test { +contract ClawbackLL_Integration_Test is MerkleLockup_Integration_Test { function setUp() public virtual override { MerkleLockup_Integration_Test.setUp(); } diff --git a/test/integration/merkle-lockup/ll/constructor/constructor.t.sol b/test/integration/merkle-lockup/ll/constructor/constructor.t.sol index 2d96f94c..42d875d6 100644 --- a/test/integration/merkle-lockup/ll/constructor/constructor.t.sol +++ b/test/integration/merkle-lockup/ll/constructor/constructor.t.sol @@ -34,7 +34,7 @@ contract Constructor_MerkleLockupLL_Integration_Test is MerkleLockup_Integration function test_Constructor() external { SablierV2MerkleLockupLL constructedLockupLL = - new SablierV2MerkleLockupLL(defaults.baseParams(), lockupLinear, defaults.durations()); + new SablierV2MerkleLockupLL(defaults.baseParamsLL(), lockupLinear, defaults.durations()); Vars memory vars; @@ -51,7 +51,7 @@ contract Constructor_MerkleLockupLL_Integration_Test is MerkleLockup_Integration assertEq(bytes32(abi.encodePacked(vars.actualName)), vars.expectedName, "name"); vars.actualMerkleRoot = constructedLockupLL.MERKLE_ROOT(); - vars.expectedMerkleRoot = defaults.MERKLE_ROOT(); + vars.expectedMerkleRoot = defaults.MERKLE_ROOT_LL(); assertEq(vars.actualMerkleRoot, vars.expectedMerkleRoot, "merkleRoot"); vars.actualCancelable = constructedLockupLL.CANCELABLE(); diff --git a/test/integration/merkle-lockup/ll/has-claimed/hasClaimed.t.sol b/test/integration/merkle-lockup/ll/has-claimed/hasClaimed.t.sol index e7aafaef..8eda2629 100644 --- a/test/integration/merkle-lockup/ll/has-claimed/hasClaimed.t.sol +++ b/test/integration/merkle-lockup/ll/has-claimed/hasClaimed.t.sol @@ -3,7 +3,7 @@ pragma solidity >=0.8.22 <0.9.0; import { MerkleLockup_Integration_Test } from "../../MerkleLockup.t.sol"; -contract HasClaimed_Integration_Test is MerkleLockup_Integration_Test { +contract HasClaimedLL_Integration_Test is MerkleLockup_Integration_Test { function setUp() public virtual override { MerkleLockup_Integration_Test.setUp(); } diff --git a/test/integration/merkle-lockup/ll/has-expired/hasExpired.t.sol b/test/integration/merkle-lockup/ll/has-expired/hasExpired.t.sol index c185e38f..6ce8a601 100644 --- a/test/integration/merkle-lockup/ll/has-expired/hasExpired.t.sol +++ b/test/integration/merkle-lockup/ll/has-expired/hasExpired.t.sol @@ -5,7 +5,7 @@ import { ISablierV2MerkleLockupLL } from "src/interfaces/ISablierV2MerkleLockupL import { MerkleLockup_Integration_Test } from "../../MerkleLockup.t.sol"; -contract HasExpired_Integration_Test is MerkleLockup_Integration_Test { +contract HasExpiredLL_Integration_Test is MerkleLockup_Integration_Test { function setUp() public virtual override { MerkleLockup_Integration_Test.setUp(); } diff --git a/test/utils/Defaults.sol b/test/utils/Defaults.sol index 3d951f30..44ec18bb 100644 --- a/test/utils/Defaults.sol +++ b/test/utils/Defaults.sol @@ -29,7 +29,7 @@ contract Defaults is Merkle { uint40 public immutable CLIFF_TIME; uint40 public immutable END_TIME; uint256 public constant ETHER_AMOUNT = 10_000 ether; - uint256 public constant MAX_SEGMENT_COUNT = 1000; + uint256 public constant MAX_SEGMENT_COUNT = 300; uint128 public constant PER_STREAM_AMOUNT = 10_000e18; UD60x18 public constant PROTOCOL_FEE = UD60x18.wrap(0); uint128 public constant REFUND_AMOUNT = 7500e18; // deposit - cliff amount @@ -53,8 +53,10 @@ contract Defaults is Merkle { string public constant IPFS_CID = "QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR"; uint256 public constant RECIPIENTS_COUNT = 4; bool public constant TRANSFERABLE = false; - uint256[] public LEAVES = new uint256[](RECIPIENTS_COUNT); - bytes32 public immutable MERKLE_ROOT; + uint256[] public LEAVES_LD = new uint256[](RECIPIENTS_COUNT); + uint256[] public LEAVES_LL = new uint256[](RECIPIENTS_COUNT); + bytes32 public immutable MERKLE_ROOT_LD; + bytes32 public immutable MERKLE_ROOT_LL; string public constant NAME = "Airdrop Campaign"; bytes32 public constant NAME_BYTES32 = bytes32(abi.encodePacked("Airdrop Campaign")); @@ -79,45 +81,77 @@ contract Defaults is Merkle { END_TIME = START_TIME + TOTAL_DURATION; EXPIRATION = uint40(block.timestamp) + 12 weeks; - // Initialize the Merkle tree. - LEAVES[0] = MerkleBuilder.computeLeaf(INDEX1, users.recipient1, CLAIM_AMOUNT); - LEAVES[1] = MerkleBuilder.computeLeaf(INDEX2, users.recipient2, CLAIM_AMOUNT); - LEAVES[2] = MerkleBuilder.computeLeaf(INDEX3, users.recipient3, CLAIM_AMOUNT); - LEAVES[3] = MerkleBuilder.computeLeaf(INDEX4, users.recipient4, CLAIM_AMOUNT); - MerkleBuilder.sortLeaves(LEAVES); - MERKLE_ROOT = getRoot(LEAVES.toBytes32()); + // Initialize the Merkle tree for Lockup Dynamic streams. + LEAVES_LD[0] = MerkleBuilder.computeLeafLD(INDEX1, users.recipient1, segmentsWithDurations()); + LEAVES_LD[1] = MerkleBuilder.computeLeafLD(INDEX2, users.recipient2, segmentsWithDurations()); + LEAVES_LD[2] = MerkleBuilder.computeLeafLD(INDEX3, users.recipient3, segmentsWithDurations()); + LEAVES_LD[3] = MerkleBuilder.computeLeafLD(INDEX4, users.recipient4, segmentsWithDurations()); + MerkleBuilder.sortLeaves(LEAVES_LD); + MERKLE_ROOT_LD = getRoot(LEAVES_LD.toBytes32()); + + // Initialize the Merkle tree for Lockup Linear streams. + LEAVES_LL[0] = MerkleBuilder.computeLeafLL(INDEX1, users.recipient1, CLAIM_AMOUNT); + LEAVES_LL[1] = MerkleBuilder.computeLeafLL(INDEX2, users.recipient2, CLAIM_AMOUNT); + LEAVES_LL[2] = MerkleBuilder.computeLeafLL(INDEX3, users.recipient3, CLAIM_AMOUNT); + LEAVES_LL[3] = MerkleBuilder.computeLeafLL(INDEX4, users.recipient4, CLAIM_AMOUNT); + MerkleBuilder.sortLeaves(LEAVES_LL); + MERKLE_ROOT_LL = getRoot(LEAVES_LL.toBytes32()); } /*////////////////////////////////////////////////////////////////////////// MERKLE-LOCKUP //////////////////////////////////////////////////////////////////////////*/ - function index1Proof() public view returns (bytes32[] memory) { - uint256 leaf = MerkleBuilder.computeLeaf(INDEX1, users.recipient1, CLAIM_AMOUNT); - uint256 pos = Arrays.findUpperBound(LEAVES, leaf); - return getProof(LEAVES.toBytes32(), pos); + function index1ProofLD() public view returns (bytes32[] memory) { + return indexProofLD(INDEX1, users.recipient1); } - function index2Proof() public view returns (bytes32[] memory) { - uint256 leaf = MerkleBuilder.computeLeaf(INDEX2, users.recipient2, CLAIM_AMOUNT); - uint256 pos = Arrays.findUpperBound(LEAVES, leaf); - return getProof(LEAVES.toBytes32(), pos); + function index1ProofLL() public view returns (bytes32[] memory) { + return indexProofLL(INDEX1, users.recipient1); } - function index3Proof() public view returns (bytes32[] memory) { - uint256 leaf = MerkleBuilder.computeLeaf(INDEX3, users.recipient3, CLAIM_AMOUNT); - uint256 pos = Arrays.findUpperBound(LEAVES, leaf); - return getProof(LEAVES.toBytes32(), pos); + function index2ProofLD() public view returns (bytes32[] memory) { + return indexProofLD(INDEX2, users.recipient2); } - function index4Proof() public view returns (bytes32[] memory) { - uint256 leaf = MerkleBuilder.computeLeaf(INDEX4, users.recipient4, CLAIM_AMOUNT); - uint256 pos = Arrays.findUpperBound(LEAVES, leaf); - return getProof(LEAVES.toBytes32(), pos); + function index2ProofLL() public view returns (bytes32[] memory) { + return indexProofLL(INDEX2, users.recipient2); } - function baseParams() public view returns (MerkleLockup.ConstructorParams memory) { - return baseParams(users.admin, MERKLE_ROOT, EXPIRATION); + function index3ProofLD() public view returns (bytes32[] memory) { + return indexProofLD(INDEX3, users.recipient3); + } + + function index3ProofLL() public view returns (bytes32[] memory) { + return indexProofLL(INDEX3, users.recipient3); + } + + function index4ProofLD() public view returns (bytes32[] memory) { + return indexProofLD(INDEX4, users.recipient4); + } + + function index4ProofLL() public view returns (bytes32[] memory) { + return indexProofLL(INDEX4, users.recipient4); + } + + function indexProofLD(uint256 index, address recipient) internal view returns (bytes32[] memory) { + uint256 leaf = MerkleBuilder.computeLeafLD(index, recipient, segmentsWithDurations()); + uint256 pos = Arrays.findUpperBound(LEAVES_LD, leaf); + return getProof(LEAVES_LD.toBytes32(), pos); + } + + function indexProofLL(uint256 index, address recipient) internal view returns (bytes32[] memory) { + uint256 leaf = MerkleBuilder.computeLeafLL(index, recipient, CLAIM_AMOUNT); + uint256 pos = Arrays.findUpperBound(LEAVES_LL, leaf); + return getProof(LEAVES_LL.toBytes32(), pos); + } + + function baseParamsLD() public view returns (MerkleLockup.ConstructorParams memory) { + return baseParams(users.admin, MERKLE_ROOT_LD, EXPIRATION); + } + + function baseParamsLL() public view returns (MerkleLockup.ConstructorParams memory) { + return baseParams(users.admin, MERKLE_ROOT_LL, EXPIRATION); } function baseParams( @@ -196,22 +230,18 @@ contract Defaults is Merkle { }); } - function dynamicRange() public view returns (LockupDynamic.Range memory) { - return LockupDynamic.Range({ start: START_TIME, end: END_TIME }); - } - /// @dev Returns a batch of `LockupDynamic.Segment` parameters. - function segments() private view returns (LockupDynamic.Segment[] memory segments_) { + function segments() public view returns (LockupDynamic.Segment[] memory segments_) { segments_ = new LockupDynamic.Segment[](2); segments_[0] = LockupDynamic.Segment({ amount: 2500e18, exponent: ud2x18(3.14e18), - timestamp: START_TIME + CLIFF_DURATION + timestamp: uint40(block.timestamp) + CLIFF_DURATION }); segments_[1] = LockupDynamic.Segment({ amount: 7500e18, exponent: ud2x18(3.14e18), - timestamp: START_TIME + TOTAL_DURATION + timestamp: uint40(block.timestamp) + TOTAL_DURATION }); } diff --git a/test/utils/Events.sol b/test/utils/Events.sol index f54ee425..efd28c3b 100644 --- a/test/utils/Events.sol +++ b/test/utils/Events.sol @@ -2,15 +2,28 @@ pragma solidity >=0.8.22; import { LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; +import { ISablierV2LockupDynamic } from "@sablier/v2-core/src/interfaces/ISablierV2LockupDynamic.sol"; import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; +import { ISablierV2MerkleLockupLD } from "src/interfaces/ISablierV2MerkleLockupLD.sol"; import { ISablierV2MerkleLockupLL } from "src/interfaces/ISablierV2MerkleLockupLL.sol"; import { MerkleLockup } 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 CreateMerkleLockupLD( + ISablierV2MerkleLockupLD indexed merkleLockupLD, + MerkleLockup.ConstructorParams indexed baseParams, + ISablierV2LockupDynamic lockupDynamic, + string ipfsCID, + uint256 aggregateAmount, + uint256 recipientsCount + ); + event CreateMerkleLockupLL( ISablierV2MerkleLockupLL indexed merkleLockupLL, MerkleLockup.ConstructorParams indexed baseParams, diff --git a/test/utils/MerkleBuilder.sol b/test/utils/MerkleBuilder.sol index ad6bb0c2..9f7bccd4 100644 --- a/test/utils/MerkleBuilder.sol +++ b/test/utils/MerkleBuilder.sol @@ -2,33 +2,79 @@ // solhint-disable reason-string pragma solidity >=0.8.22; +import { LockupDynamic } from "@sablier/v2-core/src/types/DataTypes.sol"; import { LibSort } from "solady/src/utils/LibSort.sol"; /// @dev A helper library for building Merkle leaves, roots, and proofs. library MerkleBuilder { - /// @dev Function that double hashes the data needed for a Merkle tree leaf. - function computeLeaf(uint256 index, address recipient, uint128 amount) internal pure returns (uint256 leaf) { + /*////////////////////////////////////////////////////////////////////////// + MERKLE-LEAF-LD + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Function that double hashes the data needed for a Merkle tree leaf in Lockup Dynamic. + function computeLeafLD( + uint256 index, + address recipient, + LockupDynamic.SegmentWithDuration[] memory segments + ) + internal + pure + returns (uint256 leaf) + { + leaf = uint256(keccak256(bytes.concat(keccak256(abi.encode(index, recipient, segments))))); + } + + /// @dev A batch function for `computeLeafLD`. + function computeLeavesLD( + uint256[] memory indexes, + address[] memory recipients, + LockupDynamic.SegmentWithDuration[][] memory segments + ) + internal + pure + returns (uint256[] memory leaves) + { + uint256 count = indexes.length; + require( + count == recipients.length && count == segments.length, "Merkle leaves arrays must have the same length" + ); + leaves = new uint256[](count); + for (uint256 i = 0; i < count; ++i) { + leaves[i] = computeLeafLD(indexes[i], recipients[i], segments[i]); + } + } + + /*////////////////////////////////////////////////////////////////////////// + MERKLE-LEAF-LL + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Function that double hashes the data needed for a Merkle tree leaf in Lockup Linear. + function computeLeafLL(uint256 index, address recipient, uint128 amount) internal pure returns (uint256 leaf) { leaf = uint256(keccak256(bytes.concat(keccak256(abi.encode(index, recipient, amount))))); } - /// @dev A batch function for `computeLeaf`. - function computeLeaves( + /// @dev A batch function for `computeLeafLL`. + function computeLeavesLL( uint256[] memory indexes, - address[] memory recipient, - uint128[] memory amount + address[] memory recipients, + uint128[] memory amounts ) internal pure returns (uint256[] memory leaves) { uint256 count = indexes.length; - require(count == recipient.length && count == amount.length, "Merkle leaves arrays must have the same length"); + require(count == recipients.length && count == amounts.length, "Merkle leaves arrays must have the same length"); leaves = new uint256[](count); for (uint256 i = 0; i < count; ++i) { - leaves[i] = computeLeaf(indexes[i], recipient[i], amount[i]); + leaves[i] = computeLeafLL(indexes[i], recipients[i], amounts[i]); } } + /*////////////////////////////////////////////////////////////////////////// + HELPERS + //////////////////////////////////////////////////////////////////////////*/ + /// @dev Function that convert a storage array to memory and sorts it in ascending order. We need this /// because `LibSort` does not support storage arrays. function sortLeaves(uint256[] storage leaves) internal { diff --git a/test/utils/MerkleBuilder.t.sol b/test/utils/MerkleBuilder.t.sol index 9a92b4ee..bfffb054 100644 --- a/test/utils/MerkleBuilder.t.sol +++ b/test/utils/MerkleBuilder.t.sol @@ -3,24 +3,76 @@ pragma solidity >=0.8.22; import { PRBTest } from "@prb/test/src/PRBTest.sol"; import { StdUtils } from "forge-std/src/StdUtils.sol"; +import { LockupDynamic } from "@sablier/v2-core/src/types/DataTypes.sol"; import { MerkleBuilder } from "./MerkleBuilder.sol"; contract MerkleBuilder_Test is PRBTest, StdUtils { - function testFuzz_ComputeLeaf(uint256 index, address recipient, uint128 amount) external { - uint256 actualLeaf = MerkleBuilder.computeLeaf(index, recipient, amount); + /*////////////////////////////////////////////////////////////////////////// + MERKLE-LEAF-LD + //////////////////////////////////////////////////////////////////////////*/ + + function testFuzz_ComputeLeafLD( + uint256 index, + address recipient, + LockupDynamic.SegmentWithDuration[] memory segments + ) + external + { + uint256 actualLeaf = MerkleBuilder.computeLeafLD(index, recipient, segments); + uint256 expectedLeaf = uint256(keccak256(bytes.concat(keccak256(abi.encode(index, recipient, segments))))); + assertEq(actualLeaf, expectedLeaf, "computeLeafLD"); + } + + /// @dev We declare this struct so that we will not need cheatcodes in the `computeLeavesLD` test. + struct LeavesParamsLD { + uint256 indexes; + address recipients; + LockupDynamic.SegmentWithDuration[] segments; + } + + function testFuzz_ComputeLeavesLD(LeavesParamsLD[] memory params) external { + uint256 count = params.length; + + uint256[] memory indexes = new uint256[](count); + address[] memory recipients = new address[](count); + LockupDynamic.SegmentWithDuration[][] memory segments = new LockupDynamic.SegmentWithDuration[][](count); + for (uint256 i = 0; i < count; ++i) { + indexes[i] = params[i].indexes; + recipients[i] = params[i].recipients; + segments[i] = params[i].segments; + } + + uint256[] memory actualLeaves = new uint256[](count); + actualLeaves = MerkleBuilder.computeLeavesLD(indexes, recipients, segments); + + uint256[] memory expectedLeaves = new uint256[](count); + for (uint256 i = 0; i < count; ++i) { + expectedLeaves[i] = + uint256(keccak256(bytes.concat(keccak256(abi.encode(indexes[i], recipients[i], segments[i]))))); + } + + assertEq(actualLeaves, expectedLeaves, "computeLeavesLD"); + } + + /*////////////////////////////////////////////////////////////////////////// + MERKLE-LEAF-LL + //////////////////////////////////////////////////////////////////////////*/ + + function testFuzz_ComputeLeafLL(uint256 index, address recipient, uint128 amount) external { + uint256 actualLeaf = MerkleBuilder.computeLeafLL(index, recipient, amount); uint256 expectedLeaf = uint256(keccak256(bytes.concat(keccak256(abi.encode(index, recipient, amount))))); - assertEq(actualLeaf, expectedLeaf, "computeLeaf"); + assertEq(actualLeaf, expectedLeaf, "computeLeafLL"); } - /// @dev We declare this struct so that we will not need cheatcodes in the `computeLeaves` test. - struct LeavesParams { + /// @dev We declare this struct so that we will not need cheatcodes in the `computeLeavesLL` test. + struct LeavesParamsLL { uint256 indexes; address recipients; uint128 amounts; } - function testFuzz_ComputeLeaves(LeavesParams[] memory params) external { + function testFuzz_ComputeLeavesLL(LeavesParamsLL[] memory params) external { uint256 count = params.length; uint256[] memory indexes = new uint256[](count); @@ -33,7 +85,7 @@ contract MerkleBuilder_Test is PRBTest, StdUtils { } uint256[] memory actualLeaves = new uint256[](count); - actualLeaves = MerkleBuilder.computeLeaves(indexes, recipients, amounts); + actualLeaves = MerkleBuilder.computeLeavesLL(indexes, recipients, amounts); uint256[] memory expectedLeaves = new uint256[](count); for (uint256 i = 0; i < count; ++i) { @@ -41,6 +93,6 @@ contract MerkleBuilder_Test is PRBTest, StdUtils { uint256(keccak256(bytes.concat(keccak256(abi.encode(indexes[i], recipients[i], amounts[i]))))); } - assertEq(actualLeaves, expectedLeaves, "computeLeaves"); + assertEq(actualLeaves, expectedLeaves, "computeLeavesLL"); } }