-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
test: integration and fork tests for MerkleLockupLD
- Loading branch information
1 parent
b4f7b9b
commit 8471bd4
Showing
28 changed files
with
1,121 additions
and
80 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }); | ||
} | ||
} | ||
} |
Oops, something went wrong.