Skip to content

Commit

Permalink
test: integration and fork tests for MerkleLockupLD
Browse files Browse the repository at this point in the history
  • Loading branch information
smol-ninja committed Feb 6, 2024
1 parent b4f7b9b commit 8471bd4
Show file tree
Hide file tree
Showing 28 changed files with 1,121 additions and 80 deletions.
4 changes: 2 additions & 2 deletions src/interfaces/ISablierV2MerkleLockupFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
56 changes: 55 additions & 1 deletion test/Base.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;

/*//////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -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" });
Expand Down Expand Up @@ -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(
Expand Down
3 changes: 3 additions & 0 deletions test/fork/assets/USDC.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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) { }
3 changes: 3 additions & 0 deletions test/fork/assets/USDT.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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) { }
231 changes: 231 additions & 0 deletions test/fork/merkle-lockup/MerkleLockupLD.t.sol
Original file line number Diff line number Diff line change
@@ -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 });
}
}
}
Loading

0 comments on commit 8471bd4

Please sign in to comment.