diff --git a/.codecov.yml b/.codecov.yml index 5cf31dcd..9e7248e0 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -5,5 +5,6 @@ coverage: status: patch: off ignore: + - "precompiles" - "script" - "test" diff --git a/.env.example b/.env.example index 5c5efc6e..d0f0d17e 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ export API_KEY_INFURA="YOUR_API_KEY_INFURA" +export EOA="YOUR_EOA_ADDRESS" export FOUNDRY_PROFILE="lite" export MNEMONIC="YOUR_MNEMONIC" export RPC_URL_MAINNET="YOUR_RPC_URL_MAINNET" diff --git a/.gas-snapshot b/.gas-snapshot deleted file mode 100644 index d44f0f21..00000000 --- a/.gas-snapshot +++ /dev/null @@ -1,33 +0,0 @@ -Claim_Integration_Test:test_Claim() (gas: 278831) -Claim_Integration_Test:test_RevertGiven_AlreadyClaimed() (gas: 266771) -Claim_Integration_Test:test_RevertGiven_CampaignExpired() (gas: 17765) -Claim_Integration_Test:test_RevertGiven_ProtocolFeeNotZero() (gas: 84689) -Clawback_Integration_Test:testFuzz_Clawback(address) (runs: 20, μ: 316927, ~: 316927) -Clawback_Integration_Test:testFuzz_Clawback_CampaignNotExpired(address) (runs: 20, μ: 88771, ~: 88771) -Clawback_Integration_Test:test_Clawback() (gas: 272109) -Clawback_Integration_Test:test_RevertGiven_CampaignNotExpired() (gas: 30440) -Constructor_MerkleStreamerLL_Integration_Test:test_Constructor() (gas: 1219550) -CreateMerkleStreamerLL_Integration_Test:testFuzz_CreateMerkleStreamerLL(address,uint40) (runs: 20, μ: 1139893, ~: 1139893) -CreateMerkleStreamerLL_Integration_Test:test_RevertGiven_AlreadyDeployed() (gas: 8937393460516730625) -CreateWithDeltas_Integration_Test:test_BatchCreateWithDeltas() (gas: 2070549) -CreateWithDurations_Integration_Test:test_BatchCreateWithDurations() (gas: 1334021) -CreateWithMilestones_Integration_Test:test_BatchCreateWithMilestones() (gas: 2063959) -CreateWithRange_Integration_Test:test_CreateWithRange() (gas: 1336190) -HasClaimed_Integration_Test:test_HasClaimed() (gas: 251894) -HasClaimed_Integration_Test:test_HasClaimed_IndexNotInTree() (gas: 8011) -HasClaimed_Integration_Test:test_HasClaimed_NotClaimed() (gas: 13268) -HasExpired_Integration_Test:test_HasExpired_ExpirationEqualToCurrentTime() (gas: 13971) -HasExpired_Integration_Test:test_HasExpired_ExpirationGreaterThanCurrentTime() (gas: 14065) -HasExpired_Integration_Test:test_HasExpired_ExpirationLessThanCurrentTime() (gas: 5760) -HasExpired_Integration_Test:test_HasExpired_ExpirationZero() (gas: 1081876) -MerkleBuilder_Test:testFuzz_ComputeLeaf(uint256,address,uint128) (runs: 20, μ: 1176, ~: 1176) -MerkleBuilder_Test:testFuzz_ComputeLeaves((uint256,address,uint128)[]) (runs: 20, μ: 353773, ~: 397124) -Precompiles_Test:test_DeployBatch() (gas: 2794484) -Precompiles_Test:test_DeployMerkleStreamerFactory() (gas: 3124723) -Precompiles_Test:test_DeployPeriphery() (gas: 5913539) -USDC_CreateWithMilestones_Batch_Fork_Test:testForkFuzz_CreateWithMilestones((uint128,address,address,uint128,uint40,(uint128,uint64,uint40)[])) (runs: 20, μ: 34894028, ~: 21953046) -USDC_CreateWithRange_Batch_Fork_Test:testForkFuzz_CreateWithRange((uint128,(uint40,uint40,uint40),address,address,uint128)) (runs: 20, μ: 1223264, ~: 1038250) -USDC_MerkleStreamerLL_Fork_Test:testForkFuzz_MerkleStreamerLL((address,uint40,(uint256,uint256,uint128)[],uint256)) (runs: 20, μ: 4359652, ~: 3909187) -USDT_CreateWithMilestones_Batch_Fork_Test:testForkFuzz_CreateWithMilestones((uint128,address,address,uint128,uint40,(uint128,uint64,uint40)[])) (runs: 20, μ: 34894028, ~: 21953046) -USDT_CreateWithRange_Batch_Fork_Test:testForkFuzz_CreateWithRange((uint128,(uint40,uint40,uint40),address,address,uint128)) (runs: 20, μ: 1223264, ~: 1038250) -USDT_MerkleStreamerLL_Fork_Test:testForkFuzz_MerkleStreamerLL((address,uint40,(uint256,uint256,uint128)[],uint256)) (runs: 20, μ: 4359652, ~: 3909187) \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bf6570d..b5817704 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,25 @@ jobs: match-path: "test/integration/**/*.sol" name: "Integration tests" + test-utils: + needs: ["lint", "build"] + uses: "sablier-labs/reusable-workflows/.github/workflows/forge-test.yml@main" + with: + foundry-profile: "test-optimized" + match-path: "test/utils/**/*.sol" + name: "Utils tests" + + test-fork: + needs: ["lint", "build"] + secrets: + RPC_URL_MAINNET: ${{ secrets.RPC_URL_MAINNET }} + uses: "sablier-labs/reusable-workflows/.github/workflows/forge-test.yml@main" + with: + foundry-fuzz-runs: 20 + foundry-profile: "test-optimized" + match-path: "test/fork/**/*.sol" + name: "Fork tests" + coverage: needs: ["lint", "build"] secrets: diff --git a/.github/workflows/create-merkle-streamer-ll.yml b/.github/workflows/create-merkle-streamer-ll.yml deleted file mode 100644 index 324b68be..00000000 --- a/.github/workflows/create-merkle-streamer-ll.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: "Create Merkle Streamer LockupLinear" - -env: - API_KEY_INFURA: ${{ secrets.API_KEY_INFURA }} - FOUNDRY_PROFILE: "optimized" - MNEMONIC: ${{ secrets.EVM_MNEMONIC }} - RPC_URL_MAINNET: ${{ secrets.RPC_URL_MAINNET }} - -on: - workflow_dispatch: - inputs: - params: - description: "Parameters needed for the script, as comma-separated tupples." - required: true - chain: - default: "sepolia" - description: "Chain name as defined in the Foundry config." - required: false - -jobs: - create-merkle-streamer-ll: - runs-on: "ubuntu-latest" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v4" - - - name: "Install Foundry" - uses: "foundry-rs/foundry-toolchain@v1" - - - name: "Create a Merkle streamer contract that uses Sablier V2 Lockup Linear" - run: >- - forge script script/CreateMerkleStreamerLL.s.sol - --broadcast - --rpc-url "${{ inputs.chain }}" - --sig "run(address,(address,address,address,bytes32,uint40,(uint40,uint40),bool,bool,string,uint256,uint256))" - -vvvv - "${{ inputs.params }}" - - - name: "Add workflow summary" - run: | - echo "## Result" >> $GITHUB_STEP_SUMMARY - echo "✅ Done" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/multibuild.yml b/.github/workflows/multibuild.yml index f8490a37..7bee0f5c 100644 --- a/.github/workflows/multibuild.yml +++ b/.github/workflows/multibuild.yml @@ -21,6 +21,6 @@ jobs: - name: "Check that V2 Periphery can be built with multiple Solidity versions" uses: "PaulRBerg/foundry-multibuild@v1" with: - min: "0.8.19" - max: "0.8.23" + min: "0.8.22" + max: "0.8.26" skip-test: "true" diff --git a/.solhint.json b/.solhint.json index 35ba4419..2e1b56a5 100644 --- a/.solhint.json +++ b/.solhint.json @@ -3,12 +3,12 @@ "rules": { "avoid-low-level-calls": "off", "code-complexity": ["error", 8], - "compiler-version": ["error", ">=0.8.19"], + "compiler-version": ["error", ">=0.8.22"], "contract-name-camelcase": "off", "const-name-snakecase": "off", - "custom-errors": "off", "func-name-mixedcase": "off", "func-visibility": ["error", { "ignoreConstructors": true }], + "gas-custom-errors": "off", "max-line-length": ["error", 123], "named-parameters-mapping": "warn", "no-empty-blocks": "off", diff --git a/.vscode/settings.json b/.vscode/settings.json index fa100fc5..3844d3ee 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,5 +9,8 @@ ".gas-snapshot": "julia" }, "editor.formatOnSave": true, + "search.exclude": { + "**/node_modules": true + }, "solidity.formatter": "forge" } diff --git a/CHANGELOG.md b/CHANGELOG.md index fa7d7922..ff8c097e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Common Changelog](https://common-changelog.org). +[1.2.0]: https://github.com/sablier-labs/v2-periphery/compare/v1.1.1...v1.2.0 [1.1.1]: https://github.com/sablier-labs/v2-periphery/compare/v1.1.0...v1.1.1 [1.1.0]: https://github.com/sablier-labs/v2-periphery/compare/v1.0.3...v1.1.0 [1.0.3]: https://github.com/sablier-labs/v2-periphery/compare/v1.0.2...v1.0.3 @@ -11,6 +12,31 @@ The format is based on [Common Changelog](https://common-changelog.org). [1.0.1]: https://github.com/sablier-labs/v2-periphery/compare/v1.0.0...v1.0.1 [1.0.0]: https://github.com/sablier-labs/v2-periphery/releases/tag/v1.0.0 +## [1.2.0] - 2024-07-04 + +### Changed + +- Bump dependencies ([#283](https://github.com/sablier-labs/v2-periphery/pull/283), + [#351](https://github.com/sablier-labs/v2-periphery/pull/351), + [#363](https://github.com/sablier-labs/v2-periphery/pull/363)) +- Rename `Batch` to `BatchLockup` ([#322](https://github.com/sablier-labs/v2-periphery/pull/322)) +- Rename `MerkleStreamer` to `MerkleLockup` ([#268](https://github.com/sablier-labs/v2-periphery/pull/268)) +- Refactor `Range` to `Timestamps` ([#335](https://github.com/sablier-labs/v2-periphery/pull/335)) +- Switch to Bun ([#249](https://github.com/sablier-labs/v2-periphery/pull/249)) +- Use Solidity v0.8.26 ([#351](https://github.com/sablier-labs/v2-periphery/pull/351)) + +### Added + +- And `BatchLockup` support for `LockupTranched` ([#300](https://github.com/sablier-labs/v2-periphery/pull/300)) +- Add grace period mechanism for `clawback` function ([#340](https://github.com/sablier-labs/v2-periphery/pull/340)) +- Add `MerkleLockup` support for `LockupTranched` ([#297](https://github.com/sablier-labs/v2-periphery/pull/297), + [#357](https://github.com/sablier-labs/v2-periphery/pull/357)) +- Add `precompiles` in the NPM release ([#302](https://github.com/sablier-labs/v2-periphery/pull/302)) + +### Removed + +- **Breaking**: Remove protocol fee check in `MerkleLL` ([#257](https://github.com/sablier-labs/v2-periphery/pull/257)) + ## [1.1.1] - 2023-12-20 ### Changed diff --git a/SECURITY.md b/SECURITY.md index 70c9e3ba..f751d39a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -31,8 +31,8 @@ The Program does NOT cover the following: Vulnerabilities contingent upon the occurrence of any of the following also are outside the scope of this Program: -- Front-end bugs -- DDOS attacks +- Front-end bugs (clickjacking etc.) +- DDoS attacks - Spamming - Phishing - Social engineering attacks @@ -47,6 +47,8 @@ vulnerability, it must adhere to these assumptions as well: - [All assumptions](https://github.com/sablier-labs/v2-core/blob/main/SECURITY.md) in Sablier V2 Core apply to Sablier V2 Periphery as well. +- In `SablierV2MerkleLT`, the tranche unlock percentages and the durations will be same for all airdrop claimers. +- In `SablierV2MerkleLockupFactory`, there should be no need to create two Merkle campaigns with identical parameters. ### Rewards diff --git a/benchmark/BatchLockup.Gas.t.sol b/benchmark/BatchLockup.Gas.t.sol new file mode 100644 index 00000000..6ac28654 --- /dev/null +++ b/benchmark/BatchLockup.Gas.t.sol @@ -0,0 +1,272 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22; + +import { ud2x18 } from "@prb/math/src/UD2x18.sol"; +import { LockupDynamic, LockupLinear, LockupTranched } from "@sablier/v2-core/src/types/DataTypes.sol"; + +import { BatchLockup } from "../src/types/DataTypes.sol"; +import { BatchLockupBuilder } from "../test/utils/BatchLockupBuilder.sol"; +import { Benchmark_Test } from "./Benchmark.t.sol"; + +/// @notice Tests used to benchmark {BatchLockup}. +/// @dev This contract creates a Markdown file with the gas usage of each function. +contract BatchLockup_Gas_Test is Benchmark_Test { + /*////////////////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////////////////*/ + + uint128 internal constant AMOUNT_PER_ITEM = 10e18; + uint8[5] internal batches = [5, 10, 20, 30, 50]; + uint8[5] internal counts = [24, 24, 24, 24, 12]; + + /*////////////////////////////////////////////////////////////////////////// + TEST FUNCTION + //////////////////////////////////////////////////////////////////////////*/ + + function testGas_Implementations() external { + // Set the file path. + benchmarkResultsFile = string.concat(benchmarkResults, "SablierV2BatchLockup.md"); + + // Create the file if it doesn't exist, otherwise overwrite it. + vm.writeFile({ + path: benchmarkResultsFile, + data: string.concat( + "# Benchmarks for BatchLockup\n\n", + "| Function | Lockup Type | Segments/Tranches | Batch Size | Gas Usage |\n", + "| --- | --- | --- | --- | --- |\n" + ) + }); + + for (uint256 i; i < batches.length; ++i) { + // Benchmark the batch create functions for Lockup Linear. + gasCreateWithDurationsLL(batches[i]); + gasCreateWithTimestampsLL(batches[i]); + + // Benchmark the batch create functions for Lockup Dynamic. + gasCreateWithDurationsLD({ batchSize: batches[i], segmentsCount: counts[i] }); + gasCreateWithTimestampsLD({ batchSize: batches[i], segmentsCount: counts[i] }); + + // Benchmark the batch create functions for Lockup Tranched. + gasCreateWithDurationsLT({ batchSize: batches[i], tranchesCount: counts[i] }); + gasCreateWithTimestampsLT({ batchSize: batches[i], tranchesCount: counts[i] }); + } + } + + /*////////////////////////////////////////////////////////////////////////// + GAS BENCHMARKS FOR BATCH FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + function gasCreateWithDurationsLD(uint256 batchSize, uint256 segmentsCount) internal { + BatchLockup.CreateWithDurationsLD[] memory params = BatchLockupBuilder.fillBatch({ + params: defaults.createWithDurationsLD({ + asset_: dai, + totalAmount_: uint128(AMOUNT_PER_ITEM * segmentsCount), + segments_: _generateSegmentsWithDuration(segmentsCount) + }), + batchSize: batchSize + }); + + uint256 initialGas = gasleft(); + batchLockup.createWithDurationsLD(lockupDynamic, dai, params); + string memory gasUsed = vm.toString(initialGas - gasleft()); + + contentToAppend = string.concat( + "| `createWithDurationsLD` | Lockup Dynamic |", + vm.toString(segmentsCount), + " |", + vm.toString(batchSize), + " | ", + gasUsed, + " |" + ); + + // Append the content to the file. + appendToFile(benchmarkResultsFile, contentToAppend); + } + + function gasCreateWithTimestampsLD(uint256 batchSize, uint256 segmentsCount) internal { + BatchLockup.CreateWithTimestampsLD[] memory params = BatchLockupBuilder.fillBatch({ + params: defaults.createWithTimestampsLD({ + asset_: dai, + totalAmount_: uint128(AMOUNT_PER_ITEM * segmentsCount), + segments_: _generateSegments(segmentsCount) + }), + batchSize: batchSize + }); + + uint256 initialGas = gasleft(); + batchLockup.createWithTimestampsLD(lockupDynamic, dai, params); + string memory gasUsed = vm.toString(initialGas - gasleft()); + + contentToAppend = string.concat( + "| `createWithTimestampsLD` | Lockup Dynamic |", + vm.toString(segmentsCount), + " |", + vm.toString(batchSize), + " | ", + gasUsed, + " |" + ); + + // Append the data to the file + appendToFile(benchmarkResultsFile, contentToAppend); + } + + function gasCreateWithDurationsLL(uint256 batchSize) internal { + BatchLockup.CreateWithDurationsLL[] memory params = + BatchLockupBuilder.fillBatch({ params: defaults.createWithDurationsLL(dai), batchSize: batchSize }); + + uint256 initialGas = gasleft(); + batchLockup.createWithDurationsLL(lockupLinear, dai, params); + string memory gasUsed = vm.toString(initialGas - gasleft()); + + contentToAppend = string.concat( + "| `createWithDurationsLL` | Lockup Linear | N/A |", vm.toString(batchSize), " | ", gasUsed, " |" + ); + + // Append the content to the file. + appendToFile(benchmarkResultsFile, contentToAppend); + } + + function gasCreateWithTimestampsLL(uint256 batchSize) internal { + BatchLockup.CreateWithTimestampsLL[] memory params = + BatchLockupBuilder.fillBatch({ params: defaults.createWithTimestampsLL(dai), batchSize: batchSize }); + + uint256 initialGas = gasleft(); + batchLockup.createWithTimestampsLL(lockupLinear, dai, params); + string memory gasUsed = vm.toString(initialGas - gasleft()); + + contentToAppend = string.concat( + "| `createWithTimestampsLL` | Lockup Linear | N/A |", vm.toString(batchSize), " | ", gasUsed, " |" + ); + + // Append the data to the file + appendToFile(benchmarkResultsFile, contentToAppend); + } + + function gasCreateWithDurationsLT(uint256 batchSize, uint256 tranchesCount) internal { + BatchLockup.CreateWithDurationsLT[] memory params = BatchLockupBuilder.fillBatch({ + params: defaults.createWithDurationsLT({ + asset_: dai, + totalAmount_: uint128(AMOUNT_PER_ITEM * tranchesCount), + tranches_: _generateTranchesWithDuration(tranchesCount) + }), + batchSize: batchSize + }); + + uint256 initialGas = gasleft(); + batchLockup.createWithDurationsLT(lockupTranched, dai, params); + string memory gasUsed = vm.toString(initialGas - gasleft()); + + contentToAppend = string.concat( + "| `createWithDurationsLT` | Lockup Tranched |", + vm.toString(tranchesCount), + " |", + vm.toString(batchSize), + " | ", + gasUsed, + " |" + ); + + // Append the content to the file. + appendToFile(benchmarkResultsFile, contentToAppend); + } + + function gasCreateWithTimestampsLT(uint256 batchSize, uint256 tranchesCount) internal { + BatchLockup.CreateWithTimestampsLT[] memory params = BatchLockupBuilder.fillBatch({ + params: defaults.createWithTimestampsLT({ + asset_: dai, + totalAmount_: uint128(AMOUNT_PER_ITEM * tranchesCount), + tranches_: _generateTranches(tranchesCount) + }), + batchSize: batchSize + }); + + uint256 initialGas = gasleft(); + batchLockup.createWithTimestampsLT(lockupTranched, dai, params); + string memory gasUsed = vm.toString(initialGas - gasleft()); + + contentToAppend = string.concat( + "| `createWithTimestampsLT` | Lockup Tranched |", + vm.toString(tranchesCount), + " |", + vm.toString(batchSize), + " | ", + gasUsed, + " |" + ); + + // Append the data to the file + appendToFile(benchmarkResultsFile, contentToAppend); + } + + /*////////////////////////////////////////////////////////////////////////// + HELPERS + //////////////////////////////////////////////////////////////////////////*/ + + function _generateSegments(uint256 segmentsCount) private view returns (LockupDynamic.Segment[] memory) { + LockupDynamic.Segment[] memory segments = new LockupDynamic.Segment[](segmentsCount); + + // Populate segments. + for (uint256 i = 0; i < segmentsCount; ++i) { + segments[i] = LockupDynamic.Segment({ + amount: AMOUNT_PER_ITEM, + exponent: ud2x18(0.5e18), + timestamp: getBlockTimestamp() + uint40(defaults.CLIFF_DURATION() * (1 + i)) + }); + } + + return segments; + } + + function _generateSegmentsWithDuration(uint256 segmentsCount) + private + view + returns (LockupDynamic.SegmentWithDuration[] memory) + { + LockupDynamic.SegmentWithDuration[] memory segments = new LockupDynamic.SegmentWithDuration[](segmentsCount); + + // Populate segments. + for (uint256 i; i < segmentsCount; ++i) { + segments[i] = LockupDynamic.SegmentWithDuration({ + amount: AMOUNT_PER_ITEM, + exponent: ud2x18(0.5e18), + duration: defaults.CLIFF_DURATION() + }); + } + + return segments; + } + + function _generateTranches(uint256 tranchesCount) private view returns (LockupTranched.Tranche[] memory) { + LockupTranched.Tranche[] memory tranches = new LockupTranched.Tranche[](tranchesCount); + + // Populate tranches. + for (uint256 i = 0; i < tranchesCount; ++i) { + tranches[i] = ( + LockupTranched.Tranche({ + amount: AMOUNT_PER_ITEM, + timestamp: getBlockTimestamp() + uint40(defaults.CLIFF_DURATION() * (1 + i)) + }) + ); + } + + return tranches; + } + + function _generateTranchesWithDuration(uint256 tranchesCount) + private + view + returns (LockupTranched.TrancheWithDuration[] memory) + { + LockupTranched.TrancheWithDuration[] memory tranches = new LockupTranched.TrancheWithDuration[](tranchesCount); + + // Populate tranches. + for (uint256 i; i < tranchesCount; ++i) { + tranches[i] = + LockupTranched.TrancheWithDuration({ amount: AMOUNT_PER_ITEM, duration: defaults.CLIFF_DURATION() }); + } + + return tranches; + } +} diff --git a/benchmark/Benchmark.t.sol b/benchmark/Benchmark.t.sol new file mode 100644 index 00000000..b03d8e50 --- /dev/null +++ b/benchmark/Benchmark.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22; + +import { Integration_Test } from "../test/integration/Integration.t.sol"; + +/// @notice Benchmark contract with common logic needed by all tests. +abstract contract Benchmark_Test is Integration_Test { + /*////////////////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev The directory where the benchmark files are stored. + string internal benchmarkResults = "benchmark/results/"; + + /// @dev The path to the file where the benchmark results are stored. + string internal benchmarkResultsFile; + + /// @dev A variable used to store the content to append to the results file. + string internal contentToAppend; + + /*////////////////////////////////////////////////////////////////////////// + SET-UP FUNCTION + //////////////////////////////////////////////////////////////////////////*/ + + function setUp() public override { + super.setUp(); + + deal({ token: address(dai), to: users.alice, give: type(uint256).max }); + resetPrank({ msgSender: users.alice }); + + // Create the first streams in each Lockup contract to initialize all the variables. + _createFewStreams(); + } + + /*////////////////////////////////////////////////////////////////////////// + HELPERS + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Append a line to the file at given path. + function appendToFile(string memory path, string memory line) internal { + vm.writeLine({ path: path, data: line }); + } + + /// @dev Internal function to creates a few streams in each Lockup contract. + function _createFewStreams() private { + approveContract({ asset_: dai, from: users.alice, spender: address(lockupDynamic) }); + approveContract({ asset_: dai, from: users.alice, spender: address(lockupLinear) }); + approveContract({ asset_: dai, from: users.alice, spender: address(lockupTranched) }); + for (uint128 i = 0; i < 100; ++i) { + lockupDynamic.createWithTimestamps(defaults.createWithTimestampsLD()); + lockupLinear.createWithTimestamps(defaults.createWithTimestampsLL()); + lockupTranched.createWithTimestamps(defaults.createWithTimestampsLT()); + } + } +} diff --git a/benchmark/results/SablierV2BatchLockup.md b/benchmark/results/SablierV2BatchLockup.md new file mode 100644 index 00000000..799b0eec --- /dev/null +++ b/benchmark/results/SablierV2BatchLockup.md @@ -0,0 +1,34 @@ +# Benchmarks for BatchLockup + +| Function | Lockup Type | Segments/Tranches | Batch Size | Gas Usage | +| ------------------------ | --------------- | ----------------- | ---------- | --------- | +| `createWithDurationsLL` | Lockup Linear | N/A | 5 | 771013 | +| `createWithTimestampsLL` | Lockup Linear | N/A | 5 | 732772 | +| `createWithDurationsLD` | Lockup Dynamic | 24 | 5 | 3951599 | +| `createWithTimestampsLD` | Lockup Dynamic | 24 | 5 | 3815274 | +| `createWithDurationsLT` | Lockup Tranched | 24 | 5 | 3862651 | +| `createWithTimestampsLT` | Lockup Tranched | 24 | 5 | 3744523 | +| `createWithDurationsLL` | Lockup Linear | N/A | 10 | 1417180 | +| `createWithTimestampsLL` | Lockup Linear | N/A | 10 | 1414247 | +| `createWithDurationsLD` | Lockup Dynamic | 24 | 10 | 7819165 | +| `createWithTimestampsLD` | Lockup Dynamic | 24 | 10 | 7585616 | +| `createWithDurationsLT` | Lockup Tranched | 24 | 10 | 7632114 | +| `createWithTimestampsLT` | Lockup Tranched | 24 | 10 | 7444115 | +| `createWithDurationsLL` | Lockup Linear | N/A | 20 | 2783510 | +| `createWithTimestampsLL` | Lockup Linear | N/A | 20 | 2779081 | +| `createWithDurationsLD` | Lockup Dynamic | 24 | 20 | 15617207 | +| `createWithTimestampsLD` | Lockup Dynamic | 24 | 20 | 15131248 | +| `createWithDurationsLT` | Lockup Tranched | 24 | 20 | 15211892 | +| `createWithTimestampsLT` | Lockup Tranched | 24 | 20 | 14846363 | +| `createWithDurationsLL` | Lockup Linear | N/A | 30 | 4143337 | +| `createWithTimestampsLL` | Lockup Linear | N/A | 30 | 4148585 | +| `createWithDurationsLD` | Lockup Dynamic | 24 | 30 | 23460912 | +| `createWithTimestampsLD` | Lockup Dynamic | 24 | 30 | 22697560 | +| `createWithDurationsLT` | Lockup Tranched | 24 | 30 | 22794686 | +| `createWithTimestampsLT` | Lockup Tranched | 24 | 30 | 22267335 | +| `createWithDurationsLL` | Lockup Linear | N/A | 50 | 6871104 | +| `createWithTimestampsLL` | Lockup Linear | N/A | 50 | 6893797 | +| `createWithDurationsLD` | Lockup Dynamic | 12 | 50 | 22990726 | +| `createWithTimestampsLD` | Lockup Dynamic | 12 | 50 | 22355943 | +| `createWithDurationsLT` | Lockup Tranched | 12 | 50 | 22413554 | +| `createWithTimestampsLT` | Lockup Tranched | 12 | 50 | 22006169 | diff --git a/bun.lockb b/bun.lockb index a84c0889..0089db56 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/foundry.toml b/foundry.toml index 560e0ca3..bb3e8ab8 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,21 +1,27 @@ [profile.default] auto_detect_solc = false - block_timestamp = 1_682_899_200 # May 1, 2023 at 00:00 GMT + block_timestamp = 1_714_518_000 # May 1, 2024 at 00:00 GMT bytecode_hash = "none" - evm_version = "paris" - ffi = true - fs_permissions = [{ access = "read", path = "out-optimized" }] + evm_version = "shanghai" + fs_permissions = [ + { access = "read", path = "./out-optimized" }, + { access = "read", path = "package.json"}, + { access = "read-write", path = "./benchmark/results"}, + { access = "read-write", path = "./cache" } + ] + gas_limit = 9223372036854775807 gas_reports = [ - "SablierV2Batch", - "SablierV2MerkleStreamerFactory", - "SablierV2MerkleStreamerLL", + "SablierV2BatchLockup", + "SablierV2MerkleLL", + "SablierV2MerkleLockupFactory", + "SablierV2MerkleLT", ] optimizer = true optimizer_runs = 10_000 out = "out" script = "script" sender = "0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38" - solc = "0.8.23" + solc = "0.8.26" src = "src" test = "test" @@ -23,6 +29,10 @@ max_test_rejects = 1_000_000 # Number of times `vm.assume` can fail runs = 20 +# Run only the code inside benchmark directory +[profile.benchmark] + test = "benchmark" + # Speed up compilation and tests during development [profile.lite] optimizer = false @@ -35,7 +45,6 @@ # Test the optimized contracts without re-compiling them [profile.test-optimized] - ffi = true src = "test" [doc] @@ -56,8 +65,10 @@ [rpc_endpoints] arbitrum = "https://arbitrum-mainnet.infura.io/v3/${API_KEY_INFURA}" avalanche = "https://avalanche-mainnet.infura.io/v3/${API_KEY_INFURA}" - bnb_smart_chain = "https://bsc-dataseed.binance.org" - gnosis_chain = "https://rpc.gnosischain.com" + base = "https://mainnet.base.org" + bnb = "https://bsc-dataseed.binance.org" + ethereum = "${RPC_URL_MAINNET}" + gnosis = "https://rpc.gnosischain.com" localhost = "http://localhost:8545" mainnet = "${RPC_URL_MAINNET}" optimism = "https://optimism-mainnet.infura.io/v3/${API_KEY_INFURA}" diff --git a/package.json b/package.json index b7bad1e6..88cca733 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@sablier/v2-periphery", "description": "Peripheral smart contracts for interacting with Sablier V2", - "version": "1.1.1", + "version": "1.2.0", "author": { "name": "Sablier Labs Ltd", "url": "https://sablier.com" @@ -10,25 +10,27 @@ "url": "https://github.com/sablier-labs/v2-periphery/issues" }, "dependencies": { - "@openzeppelin/contracts": "4.9.2", - "@prb/math": "4.0.2", - "@sablier/v2-core": "1.1.2" + "@openzeppelin/contracts": "5.0.2", + "@prb/math": "4.0.3", + "@sablier/v2-core": "1.2.0" }, "devDependencies": { - "@prb/test": "0.6.4", - "forge-std": "github:foundry-rs/forge-std#v1.5.6", - "prettier": "^2.8.8", - "solady": "0.0.129", - "solhint": "^4.0.0" + "forge-std": "github:foundry-rs/forge-std#v1.8.2", + "prettier": "^3.3.2", + "solady": "0.0.208", + "solhint": "^5.0.1" }, "files": [ "artifacts", + "precompiles", "src", "test/utils", "CHANGELOG.md" ], "homepage": "https://github.com/sablier-labs/v2-periphery#readme", "keywords": [ + "airdrops", + "airstreams", "asset-distribution", "asset-streaming", "blockchain", @@ -49,21 +51,19 @@ "web3" ], "peerDependencies": { - "@sablier/v2-core": "1.1.2" + "@sablier/v2-core": "1.2.0" }, "publishConfig": { "access": "public" }, "repository": "github:sablier-labs/v2-periphery", "scripts": { + "benchmark": "bun run build:optimized && FOUNDRY_PROFILE=benchmark forge test --mt testGas && bun run prettier:write", "build": "forge build", "build:optimized": "FOUNDRY_PROFILE=optimized forge build", "clean": "rm -rf artifacts broadcast cache docs out-optimized out", - "gas:report": "forge test --gas-report --no-match-test \"test(Fuzz)?_RevertWhen_\\w{1,}?\"", - "gas:snapshot": "forge snapshot --no-match-test \"test(Fuzz)?_RevertWhen_\\w{1,}?\"", - "gas:snapshot:optimized": "bun run build:optimized && FOUNDRY_PROFILE=test-optimized forge snapshot --no-match-test \"test(Fork)?(Fuzz)?_RevertWhen_\\w{1,}?\"", "lint": "bun run lint:sol && bun run prettier:check", - "lint:sol": "forge fmt --check && bun solhint \"{script,src,test}/**/*.sol\"", + "lint:sol": "forge fmt --check && bun solhint \"{precompiles,script,src,test}/**/*.sol\"", "prepack": "bun install && bash ./shell/prepare-artifacts.sh", "prettier:check": "prettier --check \"**/*.{json,md,yml}\"", "prettier:write": "prettier --write \"**/*.{json,md,yml}\"", diff --git a/precompiles/Precompiles.sol b/precompiles/Precompiles.sol new file mode 100644 index 00000000..738c5213 --- /dev/null +++ b/precompiles/Precompiles.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable max-line-length,no-inline-assembly,reason-string +pragma solidity >=0.8.22; + +import { Precompiles as V2CorePrecompiles } from "@sablier/v2-core/precompiles/Precompiles.sol"; +import { ISablierV2LockupDynamic } from "@sablier/v2-core/src/interfaces/ISablierV2LockupDynamic.sol"; +import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; +import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; +import { ISablierV2NFTDescriptor } from "@sablier/v2-core/src/interfaces/ISablierV2NFTDescriptor.sol"; + +import { ISablierV2BatchLockup } from "../src/interfaces/ISablierV2BatchLockup.sol"; +import { ISablierV2MerkleLockupFactory } from "../src/interfaces/ISablierV2MerkleLockupFactory.sol"; + +contract Precompiles { + /*////////////////////////////////////////////////////////////////////////// + BYTECODES + //////////////////////////////////////////////////////////////////////////*/ + + bytes public constant BYTECODE_BATCH_LOCKUP = + hex"60808060405234601557611e0a908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c806337266dd3146111a357806349a32c4014610e3e578063606ef87514610b025780639e743f29146107a6578063a514f83e1461040c5763f7ca34eb1461005b575f80fd5b3461033c5761006936611512565b91909282156103e4575f905f5b8481106103b057506001600160a01b036100939116918383611a7a565b61009c83611758565b926001600160a01b035f9316925b8181106100c357604051806100bf8782611587565b0390f35b6100ce818388611a17565b6100d7906117a7565b90826100e482828a611a17565b6020016100f0906117a7565b6100fb83838b611a17565b60400161010790611643565b9389610114858583611a17565b606001610120906117bb565b61012b868684611a17565b608001610137906117bb565b610142878785611a17565b60a00161014e90611a57565b918761015b818987611a17565b60c0810161016891611926565b98610174929196611a17565b60e0019360405195610185876116c6565b6001600160a01b0316865260208601966001600160a01b0316875260408601996fffffffffffffffffffffffffffffffff168a5260608601978d895260808701921515835260a08701931515845260c087019464ffffffffff16855236906101ec9261197a565b9360e08601948552366101fe916118c8565b966101008601978852604051998a977f31df3d480000000000000000000000000000000000000000000000000000000089526004890160209052610164890197516001600160a01b031660248a0152516001600160a01b03166044890152516fffffffffffffffffffffffffffffffff166064880152516001600160a01b0316608487015251151560a486015251151560c48501525164ffffffffff1660e48401525190610104830161014090528151809152610184830191602001905f905b808210610353575050925180516001600160a01b03166101248401526020908101516101448401529250819003815f885af18015610348575f90610312575b6001925061030b8288611901565b52016100aa565b506020823d8211610340575b8161032b602093836116ff565b8101031261033c57600191516102fd565b5f80fd5b3d915061031e565b6040513d5f823e3d90fd5b919493509160206060826103a1600194895164ffffffffff604080926fffffffffffffffffffffffffffffffff815116855267ffffffffffffffff6020820151166020860152015116910152565b019501920186939492916102be565b916001906fffffffffffffffffffffffffffffffff6103db60406103d5878a8c611a17565b01611643565b16019201610076565b7ff8bf106c000000000000000000000000000000000000000000000000000000005f5260045ffd5b3461033c5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033c576104436115c0565b61044b6114cb565b906044359167ffffffffffffffff831161033c573660238401121561033c5782600401359267ffffffffffffffff841161033c57602481019060243691610140870201011161033c5783156103e45790915f9190825b85811061077457506001600160a01b036104be9116928484611a7a565b6104c784611758565b926001600160a01b03165f5b8581106104e857604051806100bf8782611587565b6104fb6104f6828886611a69565b6117a7565b90610512602061050c838a88611a69565b016117a7565b878561052460406103d5868585611a69565b61053a6060610534878686611a69565b016117bb565b906101006105648761055d8188610557608061053484848d611a69565b98611a69565b968c611a69565b01906001600160a01b036040519861057b8a611660565b1688526001600160a01b0360208901961686526fffffffffffffffffffffffffffffffff6040890191168152606088019189835260808901931515845260a08901941515855260607fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60873603011261033c57604051956105fa876116e3565b61060660a08201611839565b875261061460c08201611839565b602088015260e00161062590611839565b604087015260c089019586523661063b916118c8565b9560e08901968752604051987f53b15727000000000000000000000000000000000000000000000000000000008a52516001600160a01b031660048a0152516001600160a01b03166024890152516fffffffffffffffffffffffffffffffff166044880152516001600160a01b03166064870152511515608486015251151560a485015251805164ffffffffff1660c4850152602081015164ffffffffff1660e48501526040015164ffffffffff1661010484015251610124830161071291602080916001600160a01b0381511684520151910152565b8180865a925f61016492602095f18015610348575f90610742575b6001925061073b8288611901565b52016104d3565b506020823d821161076c575b8161075b602093836116ff565b8101031261033c576001915161072d565b3d915061074e565b93926001906fffffffffffffffffffffffffffffffff61079a60406103d5898b89611a69565b160194019392936104a1565b3461033c576107b436611512565b91909282156103e4575f905f5b848110610ad457506001600160a01b036107de9116918383611a7a565b6107e783611758565b926001600160a01b035f9316925b81811061080a57604051806100bf8782611587565b610815818388611a17565b61081e906117a7565b908261082b82828a611a17565b602001610837906117a7565b61084283838b611a17565b60400161084e90611643565b938961085b858583611a17565b606001610867906117bb565b610872868684611a17565b60800161087e906117bb565b610889878785611a17565b60a00161089590611a57565b91876108a2818987611a17565b60c081016108af916117c8565b986108bb929196611a17565b60e00193604051956108cc876116c6565b6001600160a01b0316865260208601966001600160a01b0316875260408601996fffffffffffffffffffffffffffffffff168a5260608601978d895260808701921515835260a08701931515845260c087019464ffffffffff16855236906109339261184b565b9360e0860194855236610945916118c8565b966101008601978852604051998a977f32fbe22b0000000000000000000000000000000000000000000000000000000089526004890160209052610164890197516001600160a01b031660248a0152516001600160a01b03166044890152516fffffffffffffffffffffffffffffffff166064880152516001600160a01b0316608487015251151560a486015251151560c48501525164ffffffffff1660e48401525190610104830161014090528151809152610184830191602001905f905b808210610a8b575050925180516001600160a01b03166101248401526020908101516101448401529250819003815f885af18015610348575f90610a59575b60019250610a528288611901565b52016107f5565b506020823d8211610a83575b81610a72602093836116ff565b8101031261033c5760019151610a44565b3d9150610a65565b91949350916020604082610ac5600194895164ffffffffff602080926fffffffffffffffffffffffffffffffff8151168552015116910152565b01950192018693949291610a05565b916001906fffffffffffffffffffffffffffffffff610af960406103d5878a8c611a17565b160192016107c1565b3461033c57610b1036611512565b91909282156103e4575f905f5b848110610e1057506001600160a01b03610b3a9116918383611a7a565b610b4383611758565b926001600160a01b035f9316925b818110610b6657604051806100bf8782611587565b610b718183886115d6565b610b7a906117a7565b9082610b8782828a6115d6565b602001610b93906117a7565b610b9e83838b6115d6565b604001610baa90611643565b9389610bb78585836115d6565b606001610bc3906117bb565b610bce8686846115d6565b608001610bda906117bb565b9086610be78188866115d6565b60a08101610bf491611926565b97610c009291956115d6565b60c0019260405194610c1186611660565b6001600160a01b0316855260208501956001600160a01b0316865260408501986fffffffffffffffffffffffffffffffff16895260608501968c885260808601921515835260a0860193151584523690610c6a9261197a565b9260c0850193845236610c7c916118c8565b9560e085019687526040519889967f54c022920000000000000000000000000000000000000000000000000000000088526004880160209052610144880196516001600160a01b03166024890152516001600160a01b03166044880152516fffffffffffffffffffffffffffffffff166064870152516001600160a01b0316608486015251151560a485015251151560c4840152519060e4830161012090528151809152610164830191602001905f905b808210610db3575050925180516001600160a01b03166101048401526020908101516101248401529250819003815f885af18015610348575f90610d81575b60019250610d7a8288611901565b5201610b51565b506020823d8211610dab575b81610d9a602093836116ff565b8101031261033c5760019151610d6c565b3d9150610d8d565b91949350916020606082610e01600194895164ffffffffff604080926fffffffffffffffffffffffffffffffff815116855267ffffffffffffffff6020820151166020860152015116910152565b01950192018693949291610d2d565b916001906fffffffffffffffffffffffffffffffff610e3560406103d5878a8c6115d6565b16019201610b1d565b3461033c5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033c57610e756115c0565b610e7d6114cb565b906044359167ffffffffffffffff831161033c573660238401121561033c5782600401359267ffffffffffffffff841161033c57602481019060243691610120870201011161033c5783156103e45790915f9190825b85811061117157506001600160a01b03610ef09116928484611a7a565b610ef984611758565b926001600160a01b03165f5b858110610f1a57604051806100bf8782611587565b610f286104f6828886611915565b90610f39602061050c838a88611915565b8785610f4b60406103d5868585611915565b610f5b6060610534878686611915565b9060e0610f8487610f7d8188610f77608061053484848d611915565b98611915565b968c611915565b01906001600160a01b0360405198610f9b8a611660565b1688526001600160a01b0360208901961686526fffffffffffffffffffffffffffffffff6040890191168152606088019189835260808901931515845260a08901941515855260407fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60873603011261033c576040519561101a876116aa565b61102660a08201611839565b875260c00161103490611839565b602087015260c089019586523661104a916118c8565b9560e08901968752604051987fab167ccc000000000000000000000000000000000000000000000000000000008a52516001600160a01b031660048a0152516001600160a01b03166024890152516fffffffffffffffffffffffffffffffff166044880152516001600160a01b03166064870152511515608486015251151560a485015251805164ffffffffff1660c48501526020015164ffffffffff1660e484015251610104830161110f91602080916001600160a01b0381511684520151910152565b8180865a925f61014492602095f18015610348575f9061113f575b600192506111388288611901565b5201610f05565b506020823d8211611169575b81611158602093836116ff565b8101031261033c576001915161112a565b3d915061114b565b93926001906fffffffffffffffffffffffffffffffff61119760406103d5898b89611915565b16019401939293610ed3565b3461033c576111b136611512565b91909282156103e4575f905f5b84811061149d57506001600160a01b036111db9116918383611a7a565b6111e483611758565b926001600160a01b035f9316925b81811061120757604051806100bf8782611587565b6112128183886115d6565b61121b906117a7565b908261122882828a6115d6565b602001611234906117a7565b61123f83838b6115d6565b60400161124b90611643565b93896112588585836115d6565b606001611264906117bb565b61126f8686846115d6565b60800161127b906117bb565b90866112888188866115d6565b60a08101611295916117c8565b976112a19291956115d6565b60c00192604051946112b286611660565b6001600160a01b0316855260208501956001600160a01b0316865260408501986fffffffffffffffffffffffffffffffff16895260608501968c885260808601921515835260a086019315158452369061130b9261184b565b9260c085019384523661131d916118c8565b9560e085019687526040519889967f897f362b0000000000000000000000000000000000000000000000000000000088526004880160209052610144880196516001600160a01b03166024890152516001600160a01b03166044880152516fffffffffffffffffffffffffffffffff166064870152516001600160a01b0316608486015251151560a485015251151560c4840152519060e4830161012090528151809152610164830191602001905f905b808210611454575050925180516001600160a01b03166101048401526020908101516101248401529250819003815f885af18015610348575f90611422575b6001925061141b8288611901565b52016111f2565b506020823d821161144c575b8161143b602093836116ff565b8101031261033c576001915161140d565b3d915061142e565b9194935091602060408261148e600194895164ffffffffff602080926fffffffffffffffffffffffffffffffff8151168552015116910152565b019501920186939492916113ce565b916001906fffffffffffffffffffffffffffffffff6114c260406103d5878a8c6115d6565b160192016111be565b602435906001600160a01b038216820361033c57565b9181601f8401121561033c5782359167ffffffffffffffff831161033c576020808501948460051b01011161033c57565b60607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc82011261033c576004356001600160a01b038116810361033c57916024356001600160a01b038116810361033c57916044359067ffffffffffffffff821161033c57611583916004016114e1565b9091565b60206040818301928281528451809452019201905f5b8181106115aa5750505090565b825184526020938401939092019160010161159d565b600435906001600160a01b038216820361033c57565b91908110156116165760051b810135907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff018136030182121561033c570190565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b356fffffffffffffffffffffffffffffffff8116810361033c5790565b610100810190811067ffffffffffffffff82111761167d57604052565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b6040810190811067ffffffffffffffff82111761167d57604052565b610120810190811067ffffffffffffffff82111761167d57604052565b6060810190811067ffffffffffffffff82111761167d57604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff82111761167d57604052565b67ffffffffffffffff811161167d5760051b60200190565b9061176282611740565b61176f60405191826116ff565b8281527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe061179d8294611740565b0190602036910137565b356001600160a01b038116810361033c5790565b35801515810361033c5790565b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe18136030182121561033c570180359067ffffffffffffffff821161033c57602001918160061b3603831361033c57565b35906fffffffffffffffffffffffffffffffff8216820361033c57565b359064ffffffffff8216820361033c57565b92919261185782611740565b9361186560405195866116ff565b602085848152019260061b82019181831161033c57925b8284106118895750505050565b60408483031261033c57602060409182516118a3816116aa565b6118ac8761181c565b81526118b9838801611839565b8382015281520193019261187c565b919082604091031261033c576040516118e0816116aa565b809280356001600160a01b038116810361033c578252602090810135910152565b80518210156116165760209160051b010190565b919081101561161657610120020190565b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe18136030182121561033c570180359067ffffffffffffffff821161033c5760200191606082023603831361033c57565b92919261198682611740565b9361199460405195866116ff565b606060208685815201930282019181831161033c57925b8284106119b85750505050565b60608483031261033c57604051906119cf826116e3565b6119d88561181c565b825260208501359067ffffffffffffffff8216820361033c5782602092836060950152611a0760408801611839565b60408201528152019301926119ab565b91908110156116165760051b810135907ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffee18136030182121561033c570190565b3564ffffffffff8116810361033c5790565b919081101561161657610140020190565b9190611acf6040517f23b872dd00000000000000000000000000000000000000000000000000000000602082015233602482015230604482015283606482015260648152611ac96084826116ff565b82611c8f565b6001600160a01b0381166001600160a01b03604051947fdd62ed3e0000000000000000000000000000000000000000000000000000000086523060048701521693846024820152602081604481855afa80156103485784915f91611c42575b5010611b3b575b50505050565b5f806040519460208601907f095ea7b3000000000000000000000000000000000000000000000000000000008252876024880152604487015260448652611b836064876116ff565b85519082855af190611b93611d14565b82611c10575b5081611c05575b5015611bad575b80611b35565b611bf8611bfd93604051907f095ea7b300000000000000000000000000000000000000000000000000000000602083015260248201525f604482015260448152611ac96064826116ff565b611c8f565b5f8080611ba7565b90503b15155f611ba0565b80519192508115918215611c28575b5050905f611b99565b611c3b9250602080918301019101611c77565b5f80611c1f565b9150506020813d602011611c6f575b81611c5e602093836116ff565b8101031261033c578390515f611b2e565b3d9150611c51565b9081602091031261033c5751801515810361033c5790565b5f806001600160a01b03611cb893169360208151910182865af1611cb1611d14565b9083611d71565b8051908115159182611cf9575b5050611cce5750565b7f5274afe7000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b611d0c9250602080918301019101611c77565b155f80611cc5565b3d15611d6c573d9067ffffffffffffffff821161167d5760405191611d6160207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f84011601846116ff565b82523d5f602084013e565b606090565b90611dae5750805115611d8657805190602001fd5b7f1425ea42000000000000000000000000000000000000000000000000000000005f5260045ffd5b81511580611df4575b611dbf575090565b6001600160a01b03907f9996b315000000000000000000000000000000000000000000000000000000005f521660045260245ffd5b50803b15611db756fea164736f6c634300081a000a"; + bytes public constant BYTECODE_MERKLE_LOCKUP_FACTORY = + hex"608080604052346015576141d4908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c80631e323876146105285780634d7c0f111461041e5763769bed201461003a575f80fd5b3461041a5760c07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261041a5760043567ffffffffffffffff811161041a57610089903690600401610a26565b6024359073ffffffffffffffffffffffffffffffffffffffff82169182810361041a5760407fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffbc36011261041a57604051906100e382610972565b60443564ffffffffff8116810361041a57825260643564ffffffffff8116810361041a57602083015282519060208401511515836040860151926060870151608088015191604051806020810194602086526040820161014291610b48565b03601f1981018252610154908261098e565b60a08a01519360c08b015160405181819251908160208401916020019161017a92610b27565b81010380825261018d906020018261098e565b61019690610b6d565b9060e08c0151151592604051956020870198896101c59164ffffffffff60208092828151168552015116910152565b604087526101d460608861098e565b6040519a8b9a60208c019d8e3360601b905260601b7fffffffffffffffffffffffffffffffffffffffff0000000000000000000000001660348d015260f81b60488c015260d81b7fffffffffff0000000000000000000000000000000000000000000000000000001660498b015260601b7fffffffffffffffffffffffffffffffffffffffff00000000000000000000000016604e8a015251908160628a0161027c92610b27565b8701946062860152608285015260f81b60a284015260601b7fffffffffffffffffffffffffffffffffffffffff0000000000000000000000001660a383015251918260b783016102cb92610b27565b0160620103605501601f19810182526102e4908261098e565b51902060405161174880820182811067ffffffffffffffff8211176103ed578291610c9f83396080815261034160406103206080840189610bfe565b92896020820152018664ffffffffff60208092828151168552015116910152565b03905ff59283156103e2576103a56103c7937f2ba0fe49588281dbb122dd3b7f3e2b3396338f70dbe3c62bf3e3888b4ba7ffb89273ffffffffffffffffffffffffffffffffffffffff6020971695869560405194859460c0865260c0860190610bfe565b9289850152604084019064ffffffffff60208092828151168552015116910152565b608435608083015260a43560a08301520390a2604051908152f35b6040513d5f823e3d90fd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b5f80fd5b3461041a5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261041a5760043567ffffffffffffffff811161041a573660238201121561041a5780600401359067ffffffffffffffff821161041a573660248360061b8301011161041a575f90815b838310156105095760248360061b830101359067ffffffffffffffff821680920361041a5767ffffffffffffffff160167ffffffffffffffff81116104dc57600190920191610492565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b602090670de0b6b3a764000067ffffffffffffffff6040519216148152f35b3461041a5760a07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261041a5760043567ffffffffffffffff811161041a57610577903690600401610a26565b60243573ffffffffffffffffffffffffffffffffffffffff81169081810361041a576044359067ffffffffffffffff821161041a573660238301121561041a57816004013567ffffffffffffffff81116103ed57604051926105df60208360051b018561098e565b8184526024602085019260061b8201019036821161041a57602401915b818310610925575050505f9082515f905b8082106108cd5750508451906020860151151584604088015192606089015160808a015191604051806020810194602086526040820161064c91610b48565b03601f198101825261065e908261098e565b8b60a08101519460c082015160405181819251908160208401916020019161068592610b27565b810103808252610698906020018261098e565b6106a190610b6d565b9160e001511515926040519586602081019960208b52604082016106c491610bae565b03601f19810188526106d6908861098e565b6040519a8b9a60208c019d8e3360601b905260601b7fffffffffffffffffffffffffffffffffffffffff0000000000000000000000001660348d015260f81b60488c015260d81b7fffffffffff0000000000000000000000000000000000000000000000000000001660498b015260601b7fffffffffffffffffffffffffffffffffffffffff00000000000000000000000016604e8a015251908160628a0161077e92610b27565b8701946062860152608285015260f81b60a284015260601b7fffffffffffffffffffffffffffffffffffffffff0000000000000000000000001660a383015251918260b783016107cd92610b27565b0160620103605501601f19810182526107e6908261098e565b519020604051611de18082019082821067ffffffffffffffff8311176103ed578291610837916123e7843960608152610822606082018a610bfe565b90886020820152604081830391015286610bae565b03905ff580156103e2576020947ffe44018cf74992b2720702385a1728bd329dd136e4f651203176c81c12710a8b926108ac73ffffffffffffffffffffffffffffffffffffffff61089a941696879660405195869560c0875260c0870190610bfe565b918a8601528482036040860152610bae565b906060830152606435608083015260843560a08301520390a2604051908152f35b909284518410156108f85760019064ffffffffff6020808760051b890101510151160193019061060d565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b60408336031261041a576040519061093c82610972565b83359067ffffffffffffffff8216820361041a5782602092604094526109638387016109be565b838201528152019201916105fc565b6040810190811067ffffffffffffffff8211176103ed57604052565b90601f601f19910116810190811067ffffffffffffffff8211176103ed57604052565b3590811515820361041a57565b359064ffffffffff8216820361041a57565b81601f8201121561041a5780359067ffffffffffffffff82116103ed5760405192610a056020601f19601f860116018561098e565b8284526020838301011161041a57815f926020809301838601378301015290565b9190916101008184031261041a5760405190610100820182811067ffffffffffffffff8211176103ed576040528193813573ffffffffffffffffffffffffffffffffffffffff8116810361041a578352610a82602083016109b1565b6020840152610a93604083016109be565b6040840152606082013573ffffffffffffffffffffffffffffffffffffffff8116810361041a576060840152608082013567ffffffffffffffff811161041a5781610adf9184016109d0565b608084015260a082013560a084015260c08201359067ffffffffffffffff821161041a5782610b1760e09492610b22948694016109d0565b60c0860152016109b1565b910152565b5f5b838110610b385750505f910152565b8181015183820152602001610b29565b90601f19601f602093610b6681518092818752878088019101610b27565b0116010190565b602081519101519060208110610b81575090565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9060200360031b1b1690565b90602080835192838152019201905f5b818110610bcb5750505090565b8251805167ffffffffffffffff16855260209081015164ffffffffff168186015260409094019390920191600101610bbe565b9073ffffffffffffffffffffffffffffffffffffffff825116815260208201511515602082015264ffffffffff604083015116604082015273ffffffffffffffffffffffffffffffffffffffff606083015116606082015260e080610c93610c7760808601516101006080870152610100860190610b48565b60a086015160a086015260c086015185820360c0870152610b48565b93015115159101529056fe610160806040523461042857611748803803809161001d8285610560565b833981019080820390608082126104285780516001600160401b03811161042857810190610100828503126104285760405161010081016001600160401b038111828210176105355760405282516001600160a01b038116810361042857815261008960208401610583565b6020820190815261009c60408501610590565b60408301908152606085015194906001600160a01b0386168603610428576060840195865260808201516001600160401b03811161042857886100e09184016105de565b6080850190815260a08381015190860190815260c084015190999192916001600160401b0382116104285761011c60e09161012a9387016105de565b9460c0880195865201610583565b9360e0860194855260208701519560018060a01b038716998a880361042857604090603f1901126104285760408051989089016001600160401b0381118a8210176105355761018d9160609160405261018560408201610590565b8b5201610590565b9860208901998a52855151602081116105495750515f80546001600160a01b0319166001600160a01b0392831617905590511660805251151560a0525164ffffffffff1660c0525180519097906001600160401b03811161053557600154600181811c9116801561052b575b602082101461051757601f81116104b4575b506020601f821160011461044857819064ffffffffff98999a5f9261043d575b50508160011b915f199060031b1c1916176001555b5160e052516040516102736020828161026281830196878151938492016105bd565b81010301601f198101835282610560565b519051906020811061042c575b50610100525115156101205261014052511669ffffffffff0000000000600454925160281b169160018060501b031916171760045560018060a01b0360805116604051905f806020840163095ea7b360e01b815285602486015281196044860152604485526102f0606486610560565b84519082855af16102ff610623565b816103f1575b50806103e7575b156103a2575b60405161102490816107248239608051818181610481015281816106d60152610c20015260a0518181816107140152610b11015260c05181818161015f01528181610a6801528181610d8a0152610f49015260e0518181816102d3015261062201526101005181610e2801526101205181818161073e0152610ad50152610140518181816101af01526108870152f35b6103da6103df936040519063095ea7b360e01b602083015260248201525f6044820152604481526103d4606482610560565b82610652565b610652565b5f8080610312565b50803b151561030c565b8051801592508215610406575b50505f610305565b81925090602091810103126104285760206104219101610583565b5f806103fe565b5f80fd5b5f199060200360031b1b165f610280565b015190505f8061022b565b601f1982169960015f52815f209a5f5b81811061049c57509164ffffffffff999a9b91846001959410610484575b505050811b01600155610240565b01515f1960f88460031b161c191690555f8080610476565b838301518d556001909c019b60209384019301610458565b60015f527fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6601f830160051c8101916020841061050d575b601f0160051c01905b818110610502575061020b565b5f81556001016104f5565b90915081906104ec565b634e487b7160e01b5f52602260045260245ffd5b90607f16906101f9565b634e487b7160e01b5f52604160045260245ffd5b63a52d539b60e01b5f52600452602060245260445ffd5b601f909101601f19168101906001600160401b0382119082101761053557604052565b5190811515820361042857565b519064ffffffffff8216820361042857565b6001600160401b03811161053557601f01601f191660200190565b5f5b8381106105ce5750505f910152565b81810151838201526020016105bf565b81601f820112156104285780516105f4816105a2565b926106026040519485610560565b818452602082840101116104285761062091602080850191016105bd565b90565b3d1561064d573d90610634826105a2565b916106426040519384610560565b82523d5f602084013e565b606090565b5f8061067a9260018060a01b03169360208151910182865af1610673610623565b90836106c5565b80519081151591826106a2575b50506106905750565b635274afe760e01b5f5260045260245ffd5b81925090602091810103126104285760206106bd9101610583565b155f80610687565b906106e957508051156106da57805190602001fd5b630a12f52160e11b5f5260045ffd5b8151158061071a575b6106fa575090565b639996b31560e01b5f9081526001600160a01b0391909116600452602490fd5b50803b156106f256fe6080806040526004361015610012575f80fd5b5f3560e01c90816306fdde0314610e12575080631686c90914610b3657806316c3549d14610afa5780631bfd681414610abe5780633bfe03a814610a905780633f31ae3f146104a55780634800d97f1461045557806349fc73dd1461031a5780634e390d3e146102f657806351e75e8b146102bc57806375829def146101ed57806390e64d13146101d35780639e93e57714610183578063bb4b573414610142578063ce516507146101025763f851a440146100cc575f80fd5b346100fe575f6003193601126100fe57602073ffffffffffffffffffffffffffffffffffffffff5f5416604051908152f35b5f80fd5b346100fe5760206003193601126100fe57602061013860043560ff6001918060081c5f526002602052161b60405f205416151590565b6040519015158152f35b346100fe575f6003193601126100fe57602060405164ffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b346100fe575f6003193601126100fe57602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b346100fe575f6003193601126100fe576020610138610f41565b346100fe5760206003193601126100fe57610206610ec1565b5f5473ffffffffffffffffffffffffffffffffffffffff811633810361028d575073ffffffffffffffffffffffffffffffffffffffff7fffffffffffffffffffffffff0000000000000000000000000000000000000000921691829116175f55337fbdd36143ee09de60bdefca70680e0f71189b2ed7acee364b53917ad433fdaf805f80a3005b7fc6cce6a4000000000000000000000000000000000000000000000000000000005f526004523360245260445ffd5b346100fe575f6003193601126100fe5760206040517f00000000000000000000000000000000000000000000000000000000000000008152f35b346100fe575f6003193601126100fe57602064ffffffffff60035416604051908152f35b346100fe575f6003193601126100fe576040515f6001548060011c9060018116801561044b575b60208310811461041e578285529081156103dc575060011461037e575b61037a8361036e81850382610f00565b60405191829182610e5b565b0390f35b91905060015f527fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6915f905b8082106103c25750909150810160200161036e61035e565b9192600181602092548385880101520191019092916103aa565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660208086019190915291151560051b8401909101915061036e905061035e565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b91607f1691610341565b346100fe575f6003193601126100fe57602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b346100fe5760806003193601126100fe5760043560243573ffffffffffffffffffffffffffffffffffffffff81168091036100fe57604435916fffffffffffffffffffffffffffffffff83168093036100fe576064359067ffffffffffffffff82116100fe57366023830112156100fe57816004013567ffffffffffffffff81116100fe578060051b92602484820101903682116100fe57604051602081019085825287604082015288606082015260608152610563608082610f00565b519020604051602081019182526020815261057f604082610f00565b5190209261058b610f41565b610a39576105b08560ff6001918060081c5f526002602052161b60405f205416151590565b610a0d576105c46020604051970187610f00565b8552602401602085015b8282106109fd57505050935f945b835186101561061e5760208660051b85010151908181105f1461060d575f52602052600160405f205b9501946105dc565b905f52602052600160405f20610605565b84907f0000000000000000000000000000000000000000000000000000000000000000036109d55760035464ffffffffff8116156109a1575b508260081c5f52600260205260405f20600160ff85161b815417905573ffffffffffffffffffffffffffffffffffffffff5f54169160405161069881610ee4565b5f81525f602082015260405193610100850185811067ffffffffffffffff8211176109745760405284526020840183815260408501838152606086017f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff16815260808701907f00000000000000000000000000000000000000000000000000000000000000001515825260a08801927f0000000000000000000000000000000000000000000000000000000000000000151584526040519461076e86610ee4565b60045464ffffffffff8116875260281c64ffffffffff16602087015260c08a0195865260e08a01968752604051997fab167ccc000000000000000000000000000000000000000000000000000000008b525173ffffffffffffffffffffffffffffffffffffffff1660048b01525173ffffffffffffffffffffffffffffffffffffffff1660248a0152516fffffffffffffffffffffffffffffffff1660448901525173ffffffffffffffffffffffffffffffffffffffff166064880152511515608487015251151560a486015251805164ffffffffff1660c48601526020015164ffffffffff1660e485015251805173ffffffffffffffffffffffffffffffffffffffff166101048501526020015161012484015282807f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff165a925f61014492602095f1928315610969575f93610911575b50907f28b58397e03322f670d6b223cc863f8c148e368b8b615412e6798a641a22842d60406020958594825191825287820152a3604051908152f35b939250906020843d602011610961575b8161092e60209383610f00565b810103126100fe5792519192907f28b58397e03322f670d6b223cc863f8c148e368b8b615412e6798a641a22842d6108d5565b3d9150610921565b6040513d5f823e3d90fd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b7fffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000164264ffffffffff161760035583610657565b7f0fa7d73c000000000000000000000000000000000000000000000000000000005f5260045ffd5b81358152602091820191016105ce565b847f712b37a3000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b7f442b1841000000000000000000000000000000000000000000000000000000005f524260045264ffffffffff7f00000000000000000000000000000000000000000000000000000000000000001660245260445ffd5b346100fe575f6003193601126100fe57604060045464ffffffffff825191818116835260281c166020820152f35b346100fe575f6003193601126100fe5760206040517f000000000000000000000000000000000000000000000000000000000000000015158152f35b346100fe575f6003193601126100fe5760206040517f000000000000000000000000000000000000000000000000000000000000000015158152f35b346100fe5760406003193601126100fe57610b4f610ec1565b6024356fffffffffffffffffffffffffffffffff81168091036100fe5773ffffffffffffffffffffffffffffffffffffffff5f541633810361028d575064ffffffffff6003541680151580610dc4575b80610db5575b610d5b57506040515f8073ffffffffffffffffffffffffffffffffffffffff60208401957fa9059cbb000000000000000000000000000000000000000000000000000000008752169485602485015284604485015260448452610c09606485610f00565b73ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001693519082855af13d15610d4f573d67ffffffffffffffff811161097457610ca79160405191610c9760207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8401160184610f00565b82523d5f602084013e5b83610f7e565b8051908115159182610d2b575b5050610d0057507f2e9d425ba8b27655048400b366d7b6a1f7180ebdb088e06bb7389704860ffe1f602073ffffffffffffffffffffffffffffffffffffffff5f541692604051908152a3005b7f5274afe7000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b81925090602091810103126100fe57602001518015908115036100fe578480610cb4565b610ca790606090610ca1565b7f92b66697000000000000000000000000000000000000000000000000000000005f524260045264ffffffffff7f00000000000000000000000000000000000000000000000000000000000000001660245260445260645ffd5b50610dbe610f41565b15610ba5565b5062093a80810164ffffffffff8111610de55764ffffffffff164211610b9f565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b346100fe575f6003193601126100fe5761037a907f000000000000000000000000000000000000000000000000000000000000000060208201526020815261036e604082610f00565b919091602081528251928360208301525f5b848110610eab5750507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f845f6040809697860101520116010190565b8060208092840101516040828601015201610e6d565b6004359073ffffffffffffffffffffffffffffffffffffffff821682036100fe57565b6040810190811067ffffffffffffffff82111761097457604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff82111761097457604052565b64ffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168015159081610f76575090565b905042101590565b90610fbb5750805115610f9357805190602001fd5b7f1425ea42000000000000000000000000000000000000000000000000000000005f5260045ffd5b8151158061100e575b610fcc575090565b73ffffffffffffffffffffffffffffffffffffffff907f9996b315000000000000000000000000000000000000000000000000000000005f521660045260245ffd5b50803b15610fc456fea164736f6c634300081a000a610180806040523461044857611de1803803809161001d82856106ab565b83398101906060818303126104485780516001600160401b03811161044857810161010081840312610448576040519061010082016001600160401b038111838210176105025760405280516001600160a01b0381168103610448578252610087602082016106ce565b916020810192835261009b604083016106db565b60408201908152606083015193906001600160a01b0385168503610448576060830194855260808401516001600160401b03811161044857876100df918601610729565b916080840192835260a08501519360a0810194855260c086015160018060401b0381116104485760e06101178b610125938a01610729565b9760c08401988952016106ce565b60e082019081526020890151989097906001600160a01b038a168a03610448576040810151906001600160401b03821161044857018a601f82011215610448578051906001600160401b0382116105025760209b8c6040519d8e61018e828760051b01826106ab565b858152019360061b8301019181831161044857602001925b82841061064f5750505050865151602081116106385750515f80546001600160a01b0319166001600160a01b0392831617905590511660805251151560a0525164ffffffffff1660c052518051906001600160401b0382116105025760015490600182811c9216801561062e575b602083101461061a5781601f8493116105ac575b50602090601f8311600114610546575f9261053b575b50508160011b915f199060031b1c1916176001555b5160e05251604051610286602082816102758183019687815193849201610708565b81010301601f1981018352826106ab565b519051906020811061052a575b506101005251151561012052610140528051905f915f915b81831061044c57836101605260018060a01b036080511660018060a01b03610140511690604051905f806020840163095ea7b360e01b815285602486015281196044860152604485526102ff6064866106ab565b84519082855af161030e610782565b81610411575b5080610407575b156103c2575b60405161155e908161088382396080518181816105d10152818161095d0152611061015260a0518181816109870152610f52015260c0518181816102bb01528181610eac015281816111cb015261135d015260e05181818161042301526107b40152610100518161123c0152610120518181816109c20152610f160152610140518181816101390152610ac20152610160518181816102ff01526106980152f35b6103fa6103ff936040519063095ea7b360e01b602083015260248201525f6044820152604481526103f46064826106ab565b826107b1565b6107b1565b808080610321565b50803b151561031b565b8051801592508215610426575b505084610314565b819250906020918101031261044857602061044191016106ce565b848061041e565b5f80fd5b91929091906001600160401b03610463858461076e565b5151166001600160401b03918216019081116105165792610484818361076e565b519060045491680100000000000000008310156105025760018301806004558310156104ee5760019260045f5260205f200190838060401b038151166cffffffffff00000000000000006020845493015160401b1691858060681b031916171790550191906102ab565b634e487b7160e01b5f52603260045260245ffd5b634e487b7160e01b5f52604160045260245ffd5b634e487b7160e01b5f52601160045260245ffd5b5f199060200360031b1b165f610293565b015190505f8061023e565b60015f9081528281209350601f198516905b818110610594575090846001959493921061057c575b505050811b01600155610253565b01515f1960f88460031b161c191690555f808061056e565b92936020600181928786015181550195019301610558565b60015f529091507fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6601f840160051c81019160208510610610575b90601f859493920160051c01905b8181106106025750610228565b5f81558493506001016105f5565b90915081906105e7565b634e487b7160e01b5f52602260045260245ffd5b91607f1691610214565b63a52d539b60e01b5f52600452602060245260445ffd5b6040848303126104485760408051919082016001600160401b03811183821017610502576040528451906001600160401b038216820361044857826020926040945261069c8388016106db565b838201528152019301926101a6565b601f909101601f19168101906001600160401b0382119082101761050257604052565b5190811515820361044857565b519064ffffffffff8216820361044857565b6001600160401b03811161050257601f01601f191660200190565b5f5b8381106107195750505f910152565b818101518382015260200161070a565b81601f8201121561044857805161073f816106ed565b9261074d60405194856106ab565b818452602082840101116104485761076b9160208085019101610708565b90565b80518210156104ee5760209160051b010190565b3d156107ac573d90610793826106ed565b916107a160405193846106ab565b82523d5f602084013e565b606090565b5f806107d99260018060a01b03169360208151910182865af16107d2610782565b9083610824565b8051908115159182610801575b50506107ef5750565b635274afe760e01b5f5260045260245ffd5b819250906020918101031261044857602061081c91016106ce565b155f806107e6565b90610848575080511561083957805190602001fd5b630a12f52160e11b5f5260045ffd5b81511580610879575b610859575090565b639996b31560e01b5f9081526001600160a01b0391909116600452602490fd5b50803b1561085156fe6080806040526004361015610012575f80fd5b5f3560e01c90816306fdde0314611226575080631686c90914610f7757806316c3549d14610f3b5780631bfd681414610eff5780633f31ae3f146105f55780634800d97f146105a557806349fc73dd1461046a5780634e390d3e1461044657806351e75e8b1461040c57806375829def1461033d57806390e64d1314610323578063936c63d9146102df578063bb4b57341461029e578063bf4ed03f1461019d578063ce5165071461015d578063da7924681461010d5763f851a440146100d7575f80fd5b34610109575f60031936011261010957602073ffffffffffffffffffffffffffffffffffffffff5f5416604051908152f35b5f80fd5b34610109575f60031936011261010957602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b3461010957602060031936011261010957602061019360043560ff6001918060081c5f526002602052161b60405f205416151590565b6040519015158152f35b34610109575f600319360112610109576004546101b981611392565b906101c76040519283611314565b80825260045f9081526020830191907f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b835b838310610261578486604051918291602083019060208452518091526040830191905f5b81811061022b575050500390f35b8251805167ffffffffffffffff16855260209081015164ffffffffff16818601528695506040909401939092019160010161021d565b600160208192604051610273816112f8565b64ffffffffff865467ffffffffffffffff8116835260401c16838201528152019201920191906101f9565b34610109575f60031936011261010957602060405164ffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b34610109575f60031936011261010957602060405167ffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b34610109575f600319360112610109576020610193611355565b34610109576020600319360112610109576103566112d5565b5f5473ffffffffffffffffffffffffffffffffffffffff81163381036103dd575073ffffffffffffffffffffffffffffffffffffffff7fffffffffffffffffffffffff0000000000000000000000000000000000000000921691829116175f55337fbdd36143ee09de60bdefca70680e0f71189b2ed7acee364b53917ad433fdaf805f80a3005b7fc6cce6a4000000000000000000000000000000000000000000000000000000005f526004523360245260445ffd5b34610109575f6003193601126101095760206040517f00000000000000000000000000000000000000000000000000000000000000008152f35b34610109575f60031936011261010957602064ffffffffff60035416604051908152f35b34610109575f600319360112610109576040515f6001548060011c9060018116801561059b575b60208310811461056e5782855290811561052c57506001146104ce575b6104ca836104be81850382611314565b6040519182918261126f565b0390f35b91905060015f527fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6915f905b808210610512575090915081016020016104be6104ae565b9192600181602092548385880101520191019092916104fa565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660208086019190915291151560051b840190910191506104be90506104ae565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b91607f1691610491565b34610109575f60031936011261010957602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b34610109576080600319360112610109576004356024359073ffffffffffffffffffffffffffffffffffffffff821680920361010957604435916fffffffffffffffffffffffffffffffff831691828403610109576064359367ffffffffffffffff851161010957366023860112156101095784600401359467ffffffffffffffff86116101095760248660051b8201013681116101095767ffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016670de0b6b3a76400008103610ed457506040516020810190858252866040820152876060820152606081526106ee608082611314565b519020604051602081019182526020815261070a604082611314565b51902091610716611355565b610e7d5761073b8560ff6001918060081c5f526002602052161b60405f205416151590565b610e515761074888611392565b97610756604051998a611314565b8852602401602088015b828210610e4157505050925f935b86518510156107b05761078185886113aa565b51908181101561079f575f52602052600160405f205b94019361076e565b905f52602052600160405f20610797565b85907f000000000000000000000000000000000000000000000000000000000000000003610e195760035464ffffffffff811615610de5575b50600454926107f784611392565b936108056040519586611314565b80855260045f9081527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b602087015b838310610da857505050505f845161084b81611392565b956108596040519788611314565b8187527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe061088683611392565b015f5b818110610d855750505f905b828210610c7c5750506fffffffffffffffffffffffffffffffff8216848111610c4f578411610bfc575b5050508360081c5f52600260205260405f20600160ff86161b815417905573ffffffffffffffffffffffffffffffffffffffff5f541692604051610902816112f8565b5f81525f60208201526040519161010083019583871067ffffffffffffffff881117610bcf5773ffffffffffffffffffffffffffffffffffffffff9660409492939452815260208101918583526040820185815260608301887f00000000000000000000000000000000000000000000000000000000000000001681528860808501917f0000000000000000000000000000000000000000000000000000000000000000151583526fffffffffffffffffffffffffffffffff60a08701947f00000000000000000000000000000000000000000000000000000000000000001515865260c0880196875260e08801998a526040519c8d997f897f362b000000000000000000000000000000000000000000000000000000008b52602060048c0152816101448c019a511660248c0152511660448a0152511660648801525116608486015251151560a485015251151560c4840152519061012060e48401528151809152602061016484019201905f5b818110610b91575050508190602080945173ffffffffffffffffffffffffffffffffffffffff815116610104850152015161012483015203815f73ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165af1928315610b86575f93610b2e575b50907f28b58397e03322f670d6b223cc863f8c148e368b8b615412e6798a641a22842d60406020958594825191825287820152a3604051908152f35b939250906020843d602011610b7e575b81610b4b60209383611314565b810103126101095792519192907f28b58397e03322f670d6b223cc863f8c148e368b8b615412e6798a641a22842d610af2565b3d9150610b3e565b6040513d5f823e3d90fd5b825180516fffffffffffffffffffffffffffffffff16855260209081015164ffffffffff168186015289955060409094019390920191600101610a71565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b6fffffffffffffffffffffffffffffffff91610c3b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff849301886113aa565b5193031681835116011690528480806108bf565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52600160045260245ffd5b9092610c9d67ffffffffffffffff610c9486856113aa565b515116876113eb565b6fffffffffffffffffffffffffffffffff8111610d5a576fffffffffffffffffffffffffffffffff8091169164ffffffffff6020610cdb88876113aa565b5101511660405190610cec826112f8565b8482526020820152610cfe878c6113aa565b52610d09868b6113aa565b5016016fffffffffffffffffffffffffffffffff8111610d2d579260010190610895565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b7f4916adce000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b602090604051610d94816112f8565b5f81525f8382015282828c01015201610889565b600160208192604051610dba816112f8565b64ffffffffff865467ffffffffffffffff8116835260401c1683820152815201920192019190610834565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000164264ffffffffff1617600355846107e9565b7f0fa7d73c000000000000000000000000000000000000000000000000000000005f5260045ffd5b8135815260209182019101610760565b847f712b37a3000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b7f442b1841000000000000000000000000000000000000000000000000000000005f524260045264ffffffffff7f00000000000000000000000000000000000000000000000000000000000000001660245260445ffd5b7f4557880f000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b34610109575f6003193601126101095760206040517f000000000000000000000000000000000000000000000000000000000000000015158152f35b34610109575f6003193601126101095760206040517f000000000000000000000000000000000000000000000000000000000000000015158152f35b3461010957604060031936011261010957610f906112d5565b6024356fffffffffffffffffffffffffffffffff81168091036101095773ffffffffffffffffffffffffffffffffffffffff5f54163381036103dd575064ffffffffff6003541680151580611205575b806111f6575b61119c57506040515f8073ffffffffffffffffffffffffffffffffffffffff60208401957fa9059cbb00000000000000000000000000000000000000000000000000000000875216948560248501528460448501526044845261104a606485611314565b73ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001693519082855af13d15611190573d67ffffffffffffffff8111610bcf576110e891604051916110d860207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8401160184611314565b82523d5f602084013e5b836114b8565b805190811515918261116c575b505061114157507f2e9d425ba8b27655048400b366d7b6a1f7180ebdb088e06bb7389704860ffe1f602073ffffffffffffffffffffffffffffffffffffffff5f541692604051908152a3005b7f5274afe7000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b819250906020918101031261010957602001518015908115036101095784806110f5565b6110e8906060906110e2565b7f92b66697000000000000000000000000000000000000000000000000000000005f524260045264ffffffffff7f00000000000000000000000000000000000000000000000000000000000000001660245260445260645ffd5b506111ff611355565b15610fe6565b5062093a80810164ffffffffff8111610d2d5764ffffffffff164211610fe0565b34610109575f600319360112610109576104ca907f00000000000000000000000000000000000000000000000000000000000000006020820152602081526104be604082611314565b919091602081528251928360208301525f5b8481106112bf5750507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f845f6040809697860101520116010190565b8060208092840101516040828601015201611281565b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361010957565b6040810190811067ffffffffffffffff821117610bcf57604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff821117610bcf57604052565b64ffffffffff7f000000000000000000000000000000000000000000000000000000000000000016801515908161138a575090565b905042101590565b67ffffffffffffffff8111610bcf5760051b60200190565b80518210156113be5760209160051b010190565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b9190917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff838209838202918280831092039180830392146114a757670de0b6b3a7640000821015611477577faccb18165bd6fe31ae1cf318dc5b51eee0e1ba569b88cd74c1773b91fac106699394670de0b6b3a7640000910990828211900360ee1b910360121c170290565b84907f5173648d000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b5050670de0b6b3a764000090049150565b906114f557508051156114cd57805190602001fd5b7f1425ea42000000000000000000000000000000000000000000000000000000005f5260045ffd5b81511580611548575b611506575090565b73ffffffffffffffffffffffffffffffffffffffff907f9996b315000000000000000000000000000000000000000000000000000000005f521660045260245ffd5b50803b156114fe56fea164736f6c634300081a000aa164736f6c634300081a000a"; + + /*////////////////////////////////////////////////////////////////////////// + DEPLOYERS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Deploys {SablierV2BatchLockup} from precompiled bytecode. + function deployBatchLockup() public returns (ISablierV2BatchLockup batchLockup) { + bytes memory creationBytecode = BYTECODE_BATCH_LOCKUP; + assembly { + batchLockup := create(0, add(creationBytecode, 0x20), mload(creationBytecode)) + } + require( + address(batchLockup) != address(0), "Sablier V2 Precompiles: deployment failed for BatchLockup contract" + ); + } + + /// @notice Deploys {SablierV2MerkleLockupFactory} from precompiled bytecode. + function deployMerkleLockupFactory() public returns (ISablierV2MerkleLockupFactory factory) { + bytes memory creationBytecode = BYTECODE_MERKLE_LOCKUP_FACTORY; + assembly { + factory := create(0, add(creationBytecode, 0x20), mload(creationBytecode)) + } + require( + address(factory) != address(0), "Sablier V2 Precompiles: deployment failed for MerkleLockupFactory contract" + ); + } + + /// @notice Deploys all V2 Periphery contracts in the following order: + /// + /// 1. {SablierV2BatchLockup} + /// 2. {SablierV2MerkleLockupFactory} + function deployPeriphery() + public + returns (ISablierV2BatchLockup batchLockup, ISablierV2MerkleLockupFactory merkleLockupFactory) + { + batchLockup = deployBatchLockup(); + merkleLockupFactory = deployMerkleLockupFactory(); + } + + /// @notice Deploys the entire Sablier V2 Protocol from precompiled bytecode. + /// + /// 1. {SablierV2NFTDescriptor} + /// 2. {SablierV2LockupDynamic} + /// 3. {SablierV2LockupLinear} + /// 4. {SablierV2LockupTranched} + /// 5. {SablierV2BatchLockup} + /// 6. {SablierV2MerkleLockupFactory} + function deployProtocol(address initialAdmin) + public + returns ( + ISablierV2LockupDynamic lockupDynamic, + ISablierV2LockupLinear lockupLinear, + ISablierV2LockupTranched lockupTranched, + ISablierV2NFTDescriptor nftDescriptor, + ISablierV2BatchLockup batchLockup, + ISablierV2MerkleLockupFactory merkleLockupFactory + ) + { + // Deploy V2 Core. + (lockupDynamic, lockupLinear, lockupTranched, nftDescriptor) = new V2CorePrecompiles().deployCore(initialAdmin); + + // Deploy V2 Periphery. + (batchLockup, merkleLockupFactory) = deployPeriphery(); + } +} diff --git a/remappings.txt b/remappings.txt index e5aca86c..7089bf38 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,6 +1,5 @@ @openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ @prb/math/=node_modules/@prb/math/ -@prb/test/=node_modules/@prb/test/ @sablier/v2-core/=node_modules/@sablier/v2-core/ forge-std/=node_modules/forge-std/ -solady/=node_modules/solady/ \ No newline at end of file +solady/=node_modules/solady/ diff --git a/script/Base.s.sol b/script/Base.s.sol index 8e710a6a..ef9c4cea 100644 --- a/script/Base.s.sol +++ b/script/Base.s.sol @@ -1,14 +1,16 @@ // SPDX-License-Identifier: GPL-3.0-or-later // solhint-disable no-console -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { console2 } from "forge-std/src/console2.sol"; import { Script } from "forge-std/src/Script.sol"; +import { stdJson } from "forge-std/src/StdJson.sol"; contract BaseScript is Script { using Strings for uint256; + using stdJson for string; /// @dev Included to enable compilation of the script without a $MNEMONIC environment variable. string internal constant TEST_MNEMONIC = "test test test test test test test test test test test junk"; @@ -19,18 +21,18 @@ contract BaseScript is Script { /// @dev The address of the transaction broadcaster. address internal broadcaster; - /// @dev Used to derive the broadcaster's address if $ETH_FROM is not defined. + /// @dev Used to derive the broadcaster's address if $EOA is not defined. string internal mnemonic; /// @dev Initializes the transaction broadcaster like this: /// - /// - If $ETH_FROM is defined, use it. + /// - If $EOA is defined, use it. /// - Otherwise, derive the broadcaster address from $MNEMONIC. /// - If $MNEMONIC is not defined, default to a test mnemonic. /// - /// The use case for $ETH_FROM is to specify the broadcaster key and its address via the command line. + /// The use case for $EOA is to specify the broadcaster key and its address via the command line. constructor() { - address from = vm.envOr({ name: "ETH_FROM", defaultValue: address(0) }); + address from = vm.envOr({ name: "EOA", defaultValue: address(0) }); if (from != address(0)) { broadcaster = from; } else { @@ -50,18 +52,11 @@ contract BaseScript is Script { /// /// Notes: /// - The salt format is "ChainID , Version ". - /// - The version is obtained from `package.json` using the `ffi` cheatcode: - /// https://book.getfoundry.sh/cheatcodes/ffi - /// - Requires `jq` CLI tool installed: https://jqlang.github.io/jq/ - function constructCreate2Salt() public returns (bytes32) { + /// - The version is obtained from `package.json`. + function constructCreate2Salt() public view returns (bytes32) { string memory chainId = block.chainid.toString(); - string[] memory inputs = new string[](4); - inputs[0] = "jq"; - inputs[1] = "-r"; - inputs[2] = ".version"; - inputs[3] = "./package.json"; - bytes memory result = vm.ffi(inputs); - string memory version = string(result); + string memory json = vm.readFile("package.json"); + string memory version = json.readString(".version"); string memory create2Salt = string.concat("ChainID ", chainId, ", Version ", version); console2.log("The CREATE2 salt is \"%s\"", create2Salt); return bytes32(abi.encodePacked(create2Salt)); diff --git a/script/CreateMerkleLL.s.sol b/script/CreateMerkleLL.s.sol new file mode 100644 index 00000000..daf788f7 --- /dev/null +++ b/script/CreateMerkleLL.s.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22 <0.9.0; + +import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; +import { LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; + +import { BaseScript } from "./Base.s.sol"; + +import { ISablierV2MerkleLL } from "../src/interfaces/ISablierV2MerkleLL.sol"; +import { ISablierV2MerkleLockupFactory } from "../src/interfaces/ISablierV2MerkleLockupFactory.sol"; +import { MerkleLockup } from "../src/types/DataTypes.sol"; + +contract CreateMerkleLL is BaseScript { + struct Params { + MerkleLockup.ConstructorParams baseParams; + ISablierV2LockupLinear lockupLinear; + LockupLinear.Durations streamDurations; + uint256 campaignTotalAmount; + uint256 recipientCount; + } + + /// @dev Deploy via Forge. + function run( + ISablierV2MerkleLockupFactory merkleLockupFactory, + Params calldata params + ) + public + virtual + broadcast + returns (ISablierV2MerkleLL merkleLL) + { + merkleLL = merkleLockupFactory.createMerkleLL( + params.baseParams, + params.lockupLinear, + params.streamDurations, + params.campaignTotalAmount, + params.recipientCount + ); + } +} diff --git a/script/CreateMerkleLT.s.sol b/script/CreateMerkleLT.s.sol new file mode 100644 index 00000000..c32c3eea --- /dev/null +++ b/script/CreateMerkleLT.s.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22 <0.9.0; + +import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; + +import { BaseScript } from "./Base.s.sol"; + +import { ISablierV2MerkleLockupFactory } from "../src/interfaces/ISablierV2MerkleLockupFactory.sol"; +import { ISablierV2MerkleLT } from "../src/interfaces/ISablierV2MerkleLT.sol"; +import { MerkleLockup, MerkleLT } from "../src/types/DataTypes.sol"; + +contract CreateMerkleLT is BaseScript { + struct Params { + MerkleLockup.ConstructorParams baseParams; + ISablierV2LockupTranched lockupTranched; + MerkleLT.TrancheWithPercentage[] tranchesWithPercentages; + uint256 campaignTotalAmount; + uint256 recipientCount; + } + + /// @dev Deploy via Forge. + function run( + ISablierV2MerkleLockupFactory merkleLockupFactory, + Params calldata params + ) + public + virtual + broadcast + returns (ISablierV2MerkleLT merkleLT) + { + merkleLT = merkleLockupFactory.createMerkleLT( + params.baseParams, + params.lockupTranched, + params.tranchesWithPercentages, + params.campaignTotalAmount, + params.recipientCount + ); + } +} diff --git a/script/CreateMerkleStreamerLL.s.sol b/script/CreateMerkleStreamerLL.s.sol deleted file mode 100644 index 860807b4..00000000 --- a/script/CreateMerkleStreamerLL.s.sol +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19 <0.9.0; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; -import { LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; -import { BaseScript } from "./Base.s.sol"; - -import { ISablierV2MerkleStreamerFactory } from "../src/interfaces/ISablierV2MerkleStreamerFactory.sol"; -import { ISablierV2MerkleStreamerLL } from "../src/interfaces/ISablierV2MerkleStreamerLL.sol"; - -contract CreateMerkleStreamerLL is BaseScript { - struct Params { - address initialAdmin; - ISablierV2LockupLinear lockupLinear; - IERC20 asset; - bytes32 merkleRoot; - uint40 expiration; - LockupLinear.Durations streamDurations; - bool cancelable; - bool transferable; - string ipfsCID; - uint256 campaignTotalAmount; - uint256 recipientsCount; - } - - function run( - ISablierV2MerkleStreamerFactory merkleStreamerFactory, - Params calldata params - ) - public - broadcast - returns (ISablierV2MerkleStreamerLL merkleStreamerLL) - { - merkleStreamerLL = merkleStreamerFactory.createMerkleStreamerLL( - params.initialAdmin, - params.lockupLinear, - params.asset, - params.merkleRoot, - params.expiration, - params.streamDurations, - params.cancelable, - params.transferable, - params.ipfsCID, - params.campaignTotalAmount, - params.recipientsCount - ); - } -} diff --git a/script/DeployBatchLockup.t.sol b/script/DeployBatchLockup.t.sol new file mode 100644 index 00000000..03bfc7d1 --- /dev/null +++ b/script/DeployBatchLockup.t.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22 <0.9.0; + +import { BaseScript } from "./Base.s.sol"; + +import { SablierV2BatchLockup } from "../src/SablierV2BatchLockup.sol"; + +contract DeployBatchLockup is BaseScript { + /// @dev Deploy via Forge. + function run() public virtual broadcast returns (SablierV2BatchLockup batchLockup) { + batchLockup = new SablierV2BatchLockup(); + } +} diff --git a/script/DeployDeterministicBatch.s.sol b/script/DeployDeterministicBatch.s.sol deleted file mode 100644 index 8ea71b36..00000000 --- a/script/DeployDeterministicBatch.s.sol +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19 <0.9.0; - -import { BaseScript } from "./Base.s.sol"; - -import { SablierV2Batch } from "../src/SablierV2Batch.sol"; - -/// @notice Deploys {SablierV2Batch} at a deterministic address across chains. -/// @dev Reverts if the contract has already been deployed. -contract DeployDeterministicBatch is BaseScript { - function run() public virtual broadcast returns (SablierV2Batch batch) { - bytes32 salt = constructCreate2Salt(); - batch = new SablierV2Batch{ salt: salt }(); - } -} diff --git a/script/DeployDeterministicBatchLockup.s.sol b/script/DeployDeterministicBatchLockup.s.sol new file mode 100644 index 00000000..e214fc07 --- /dev/null +++ b/script/DeployDeterministicBatchLockup.s.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22 <0.9.0; + +import { BaseScript } from "./Base.s.sol"; + +import { SablierV2BatchLockup } from "../src/SablierV2BatchLockup.sol"; + +/// @notice Deploys {SablierV2BatchLockup} at a deterministic address across chains. +/// @dev Reverts if the contract has already been deployed. +contract DeployDeterministicBatchLockup is BaseScript { + /// @dev Deploy via Forge. + function run() public virtual broadcast returns (SablierV2BatchLockup batchLockup) { + bytes32 salt = constructCreate2Salt(); + batchLockup = new SablierV2BatchLockup{ salt: salt }(); + } +} diff --git a/script/DeployDeterministicPeriphery.s.sol b/script/DeployDeterministicPeriphery.s.sol index e58ce3cc..e4bc8c63 100644 --- a/script/DeployDeterministicPeriphery.s.sol +++ b/script/DeployDeterministicPeriphery.s.sol @@ -1,26 +1,27 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { BaseScript } from "./Base.s.sol"; -import { SablierV2Batch } from "../src/SablierV2Batch.sol"; -import { SablierV2MerkleStreamerFactory } from "../src/SablierV2MerkleStreamerFactory.sol"; +import { SablierV2BatchLockup } from "../src/SablierV2BatchLockup.sol"; +import { SablierV2MerkleLockupFactory } from "../src/SablierV2MerkleLockupFactory.sol"; /// @notice Deploys all V2 Periphery contracts at deterministic addresses across chains, in the following order: /// -/// 1. {SablierV2Batch} -/// 2. {SablierV2MerkleStreamerFactory} +/// 1. {SablierV2BatchLockup} +/// 2. {SablierV2MerkleLockupFactory} /// /// @dev Reverts if any contract has already been deployed. contract DeployDeterministicPeriphery is BaseScript { + /// @dev Deploy via Forge. function run() public virtual broadcast - returns (SablierV2Batch batch, SablierV2MerkleStreamerFactory merkleStreamerFactory) + returns (SablierV2BatchLockup batchLockup, SablierV2MerkleLockupFactory merkleLockupFactory) { bytes32 salt = constructCreate2Salt(); - batch = new SablierV2Batch{ salt: salt }(); - merkleStreamerFactory = new SablierV2MerkleStreamerFactory{ salt: salt }(); + batchLockup = new SablierV2BatchLockup{ salt: salt }(); + merkleLockupFactory = new SablierV2MerkleLockupFactory{ salt: salt }(); } } diff --git a/script/DeployMerkleLockupFactory.s.sol b/script/DeployMerkleLockupFactory.s.sol new file mode 100644 index 00000000..4b1c5de9 --- /dev/null +++ b/script/DeployMerkleLockupFactory.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22 <0.9.0; + +import { BaseScript } from "./Base.s.sol"; + +import { SablierV2MerkleLockupFactory } from "../src/SablierV2MerkleLockupFactory.sol"; + +contract DeployMerkleLockupFactory is BaseScript { + /// @dev Deploy via Forge. + function run() public virtual broadcast returns (SablierV2MerkleLockupFactory merkleLockupFactory) { + merkleLockupFactory = new SablierV2MerkleLockupFactory(); + } +} diff --git a/script/DeployMerkleStreamerFactory.s.sol b/script/DeployMerkleStreamerFactory.s.sol deleted file mode 100644 index 7fa01b27..00000000 --- a/script/DeployMerkleStreamerFactory.s.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19 <0.9.0; - -import { BaseScript } from "./Base.s.sol"; - -import { SablierV2MerkleStreamerFactory } from "../src/SablierV2MerkleStreamerFactory.sol"; - -contract DeployMerkleStreamerFactory is BaseScript { - function run() public broadcast returns (SablierV2MerkleStreamerFactory merkleStreamerFactory) { - merkleStreamerFactory = new SablierV2MerkleStreamerFactory(); - } -} diff --git a/script/DeployPeriphery.s.sol b/script/DeployPeriphery.s.sol index 40390bc9..a51f4b9c 100644 --- a/script/DeployPeriphery.s.sol +++ b/script/DeployPeriphery.s.sol @@ -1,22 +1,24 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { BaseScript } from "./Base.s.sol"; -import { SablierV2MerkleStreamerFactory } from "../src/SablierV2MerkleStreamerFactory.sol"; -import { SablierV2Batch } from "../src/SablierV2Batch.sol"; +import { SablierV2MerkleLockupFactory } from "../src/SablierV2MerkleLockupFactory.sol"; +import { SablierV2BatchLockup } from "../src/SablierV2BatchLockup.sol"; /// @notice Deploys all V2 Periphery contract in the following order: /// -/// 1. {SablierV2Batch} -/// 2. {SablierV2MerkleStreamerFactory} +/// 1. {SablierV2BatchLockup} +/// 2. {SablierV2MerkleLockupFactory} contract DeployPeriphery is BaseScript { + /// @dev Deploy via Forge. function run() public + virtual broadcast - returns (SablierV2Batch batch, SablierV2MerkleStreamerFactory merkleStreamerFactory) + returns (SablierV2BatchLockup batchLockup, SablierV2MerkleLockupFactory merkleLockupFactory) { - batch = new SablierV2Batch(); - merkleStreamerFactory = new SablierV2MerkleStreamerFactory(); + batchLockup = new SablierV2BatchLockup(); + merkleLockupFactory = new SablierV2MerkleLockupFactory(); } } diff --git a/script/DeployProtocol.s.sol b/script/DeployProtocol.s.sol index c1c0ced4..1976faeb 100644 --- a/script/DeployProtocol.s.sol +++ b/script/DeployProtocol.s.sol @@ -1,40 +1,43 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; -import { SablierV2Comptroller } from "@sablier/v2-core/src/SablierV2Comptroller.sol"; import { SablierV2LockupDynamic } from "@sablier/v2-core/src/SablierV2LockupDynamic.sol"; import { SablierV2LockupLinear } from "@sablier/v2-core/src/SablierV2LockupLinear.sol"; +import { SablierV2LockupTranched } from "@sablier/v2-core/src/SablierV2LockupTranched.sol"; import { SablierV2NFTDescriptor } from "@sablier/v2-core/src/SablierV2NFTDescriptor.sol"; import { BaseScript } from "./Base.s.sol"; -import { SablierV2MerkleStreamerFactory } from "../src/SablierV2MerkleStreamerFactory.sol"; -import { SablierV2Batch } from "../src/SablierV2Batch.sol"; +import { SablierV2MerkleLockupFactory } from "../src/SablierV2MerkleLockupFactory.sol"; +import { SablierV2BatchLockup } from "../src/SablierV2BatchLockup.sol"; /// @notice Deploys the Sablier V2 Protocol. contract DeployProtocol is BaseScript { + /// @dev Deploy via Forge. function run( address initialAdmin, - uint256 maxSegmentCount + uint256 maxSegmentCount, + uint256 maxTrancheCount ) public virtual broadcast returns ( - SablierV2Comptroller comptroller, SablierV2LockupDynamic lockupDynamic, SablierV2LockupLinear lockupLinear, + SablierV2LockupTranched lockupTranched, SablierV2NFTDescriptor nftDescriptor, - SablierV2Batch batch, - SablierV2MerkleStreamerFactory merkleStreamerFactory + SablierV2BatchLockup batchLockup, + SablierV2MerkleLockupFactory merkleLockupFactory ) { // Deploy V2 Core. - comptroller = new SablierV2Comptroller(initialAdmin); nftDescriptor = new SablierV2NFTDescriptor(); - lockupDynamic = new SablierV2LockupDynamic(initialAdmin, comptroller, nftDescriptor, maxSegmentCount); - lockupLinear = new SablierV2LockupLinear(initialAdmin, comptroller, nftDescriptor); + lockupDynamic = new SablierV2LockupDynamic(initialAdmin, nftDescriptor, maxSegmentCount); + lockupLinear = new SablierV2LockupLinear(initialAdmin, nftDescriptor); + lockupTranched = new SablierV2LockupTranched(initialAdmin, nftDescriptor, maxTrancheCount); - batch = new SablierV2Batch(); - merkleStreamerFactory = new SablierV2MerkleStreamerFactory(); + // Deploy V2 Periphery. + batchLockup = new SablierV2BatchLockup(); + merkleLockupFactory = new SablierV2MerkleLockupFactory(); } } diff --git a/shell/prepare-artifacts.sh b/shell/prepare-artifacts.sh index e417fdd2..74affdbb 100755 --- a/shell/prepare-artifacts.sh +++ b/shell/prepare-artifacts.sh @@ -24,14 +24,16 @@ mkdir $artifacts \ FOUNDRY_PROFILE=optimized forge build # Copy the production artifacts -cp out-optimized/SablierV2Batch.sol/SablierV2Batch.json $artifacts -cp out-optimized/SablierV2MerkleStreamerFactory.sol/SablierV2MerkleStreamerFactory.json $artifacts -cp out-optimized/SablierV2MerkleStreamerLL.sol/SablierV2MerkleStreamerLL.json $artifacts +cp out-optimized/SablierV2BatchLockup.sol/SablierV2BatchLockup.json $artifacts +cp out-optimized/SablierV2MerkleLL.sol/SablierV2MerkleLL.json $artifacts +cp out-optimized/SablierV2MerkleLockupFactory.sol/SablierV2MerkleLockupFactory.json $artifacts +cp out-optimized/SablierV2MerkleLT.sol/SablierV2MerkleLT.json $artifacts interfaces=./artifacts/interfaces -cp out-optimized/ISablierV2Batch.sol/ISablierV2Batch.json $interfaces -cp out-optimized/ISablierV2MerkleStreamerFactory.sol/ISablierV2MerkleStreamerFactory.json $interfaces -cp out-optimized/ISablierV2MerkleStreamerLL.sol/ISablierV2MerkleStreamerLL.json $interfaces +cp out-optimized/ISablierV2BatchLockup.sol/ISablierV2BatchLockup.json $interfaces +cp out-optimized/ISablierV2MerkleLL.sol/ISablierV2MerkleLL.json $interfaces +cp out-optimized/ISablierV2MerkleLockupFactory.sol/ISablierV2MerkleLockupFactory.json $interfaces +cp out-optimized/ISablierV2MerkleLT.sol/ISablierV2MerkleLT.json $interfaces erc20=./artifacts/interfaces/erc20 cp out-optimized/IERC20.sol/IERC20.json $erc20 diff --git a/shell/update-precompiles.sh b/shell/update-precompiles.sh index b2a9602a..ef98115f 100755 --- a/shell/update-precompiles.sh +++ b/shell/update-precompiles.sh @@ -12,18 +12,18 @@ set -euo pipefail FOUNDRY_PROFILE=optimized forge build # Retrieve the raw bytecodes, removing the "0x" prefix -batch=$(cat out-optimized/SablierV2Batch.sol/SablierV2Batch.json | jq -r '.bytecode.object' | cut -c 3-) -merkle_streamer_factory=$(cat out-optimized/SablierV2MerkleStreamerFactory.sol/SablierV2MerkleStreamerFactory.json | jq -r '.bytecode.object' | cut -c 3-) +batch_lockup=$(cat out-optimized/SablierV2BatchLockup.sol/SablierV2BatchLockup.json | jq -r '.bytecode.object' | cut -c 3-) +merkle_lockup_factory=$(cat out-optimized/SablierV2MerkleLockupFactory.sol/SablierV2MerkleLockupFactory.json | jq -r '.bytecode.object' | cut -c 3-) -precompiles_path="test/utils/Precompiles.sol" +precompiles_path="precompiles/Precompiles.sol" if [ ! -f $precompiles_path ]; then echo "Precompiles file does not exist" exit 1 fi # Replace the current bytecodes -sd "(BYTECODE_BATCH =)[^;]+;" "\$1 hex\"$batch\";" $precompiles_path -sd "(BYTECODE_MERKLE_STREAMER_FACTORY =)[^;]+;" "\$1 hex\"$merkle_streamer_factory\";" $precompiles_path +sd "(BYTECODE_BATCH_LOCKUP =)[^;]+;" "\$1 hex\"$batch_lockup\";" $precompiles_path +sd "(BYTECODE_MERKLE_LOCKUP_FACTORY =)[^;]+;" "\$1 hex\"$merkle_lockup_factory\";" $precompiles_path # Reformat the code with Forge forge fmt $precompiles_path diff --git a/src/SablierV2Batch.sol b/src/SablierV2BatchLockup.sol similarity index 52% rename from src/SablierV2Batch.sol rename to src/SablierV2BatchLockup.sol index c8569cc2..70f6bc13 100644 --- a/src/SablierV2Batch.sol +++ b/src/SablierV2BatchLockup.sol @@ -1,30 +1,132 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; import { ISablierV2LockupDynamic } from "@sablier/v2-core/src/interfaces/ISablierV2LockupDynamic.sol"; -import { LockupDynamic, LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; +import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; +import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; +import { LockupDynamic, LockupLinear, LockupTranched } from "@sablier/v2-core/src/types/DataTypes.sol"; -import { ISablierV2Batch } from "./interfaces/ISablierV2Batch.sol"; +import { ISablierV2BatchLockup } from "./interfaces/ISablierV2BatchLockup.sol"; import { Errors } from "./libraries/Errors.sol"; -import { Batch } from "./types/DataTypes.sol"; +import { BatchLockup } from "./types/DataTypes.sol"; -/// @title SablierV2Batch -/// @notice See the documentation in {ISablierV2Batch}. -contract SablierV2Batch is ISablierV2Batch { +/// @title SablierV2BatchLockup +/// @notice See the documentation in {ISablierV2BatchLockup}. +contract SablierV2BatchLockup is ISablierV2BatchLockup { using SafeERC20 for IERC20; + /*////////////////////////////////////////////////////////////////////////// + SABLIER-V2-LOCKUP-DYNAMIC + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc ISablierV2BatchLockup + function createWithDurationsLD( + ISablierV2LockupDynamic lockupDynamic, + IERC20 asset, + BatchLockup.CreateWithDurationsLD[] calldata batch + ) + external + override + returns (uint256[] memory streamIds) + { + // Check that the batch size is not zero. + uint256 batchSize = batch.length; + if (batchSize == 0) { + revert Errors.SablierV2BatchLockup_BatchSizeZero(); + } + + // Calculate the sum of all of stream amounts. It is safe to use unchecked addition because one of the create + // transactions will revert if there is overflow. + uint256 i; + uint256 transferAmount; + for (i = 0; i < batchSize; ++i) { + unchecked { + transferAmount += batch[i].totalAmount; + } + } + + // Perform the ERC-20 transfer and approve {SablierV2LockupDynamic} to spend the amount of assets. + _handleTransfer(address(lockupDynamic), asset, transferAmount); + + // Create a stream for each element in the parameter array. + streamIds = new uint256[](batchSize); + for (i = 0; i < batchSize; ++i) { + // Create the stream. + streamIds[i] = lockupDynamic.createWithDurations( + LockupDynamic.CreateWithDurations({ + sender: batch[i].sender, + recipient: batch[i].recipient, + totalAmount: batch[i].totalAmount, + asset: asset, + cancelable: batch[i].cancelable, + transferable: batch[i].transferable, + segments: batch[i].segments, + broker: batch[i].broker + }) + ); + } + } + + /// @inheritdoc ISablierV2BatchLockup + function createWithTimestampsLD( + ISablierV2LockupDynamic lockupDynamic, + IERC20 asset, + BatchLockup.CreateWithTimestampsLD[] calldata batch + ) + external + override + returns (uint256[] memory streamIds) + { + // Check that the batch size is not zero. + uint256 batchSize = batch.length; + if (batchSize == 0) { + revert Errors.SablierV2BatchLockup_BatchSizeZero(); + } + + // Calculate the sum of all of stream amounts. It is safe to use unchecked addition because one of the create + // transactions will revert if there is overflow. + uint256 i; + uint256 transferAmount; + for (i = 0; i < batchSize; ++i) { + unchecked { + transferAmount += batch[i].totalAmount; + } + } + + // Perform the ERC-20 transfer and approve {SablierV2LockupDynamic} to spend the amount of assets. + _handleTransfer(address(lockupDynamic), asset, transferAmount); + + // Create a stream for each element in the parameter array. + streamIds = new uint256[](batchSize); + for (i = 0; i < batchSize; ++i) { + // Create the stream. + streamIds[i] = lockupDynamic.createWithTimestamps( + LockupDynamic.CreateWithTimestamps({ + sender: batch[i].sender, + recipient: batch[i].recipient, + totalAmount: batch[i].totalAmount, + asset: asset, + cancelable: batch[i].cancelable, + transferable: batch[i].transferable, + startTime: batch[i].startTime, + segments: batch[i].segments, + broker: batch[i].broker + }) + ); + } + } + /*////////////////////////////////////////////////////////////////////////// SABLIER-V2-LOCKUP-LINEAR //////////////////////////////////////////////////////////////////////////*/ - /// @inheritdoc ISablierV2Batch - function createWithDurations( + /// @inheritdoc ISablierV2BatchLockup + function createWithDurationsLL( ISablierV2LockupLinear lockupLinear, IERC20 asset, - Batch.CreateWithDurations[] calldata batch + BatchLockup.CreateWithDurationsLL[] calldata batch ) external override @@ -33,52 +135,46 @@ contract SablierV2Batch is ISablierV2Batch { // Check that the batch size is not zero. uint256 batchSize = batch.length; if (batchSize == 0) { - revert Errors.SablierV2Batch_BatchSizeZero(); + revert Errors.SablierV2BatchLockup_BatchSizeZero(); } // Calculate the sum of all of stream amounts. It is safe to use unchecked addition because one of the create // transactions will revert if there is overflow. uint256 i; uint256 transferAmount; - for (i = 0; i < batchSize;) { + for (i = 0; i < batchSize; ++i) { unchecked { transferAmount += batch[i].totalAmount; - i += 1; } } - // Transfers the assets to the batch and approves the Sablier contract to spend them. + // Perform the ERC-20 transfer and approve {SablierV2LockupLinear} to spend the amount of assets. _handleTransfer(address(lockupLinear), asset, transferAmount); // Create a stream for each element in the parameter array. streamIds = new uint256[](batchSize); - for (i = 0; i < batchSize;) { + for (i = 0; i < batchSize; ++i) { // Create the stream. streamIds[i] = lockupLinear.createWithDurations( LockupLinear.CreateWithDurations({ + sender: batch[i].sender, + recipient: batch[i].recipient, + totalAmount: batch[i].totalAmount, asset: asset, - broker: batch[i].broker, cancelable: batch[i].cancelable, + transferable: batch[i].transferable, durations: batch[i].durations, - recipient: batch[i].recipient, - sender: batch[i].sender, - totalAmount: batch[i].totalAmount, - transferable: batch[i].transferable + broker: batch[i].broker }) ); - - // Increment the for loop iterator. - unchecked { - i += 1; - } } } - /// @inheritdoc ISablierV2Batch - function createWithRange( + /// @inheritdoc ISablierV2BatchLockup + function createWithTimestampsLL( ISablierV2LockupLinear lockupLinear, IERC20 asset, - Batch.CreateWithRange[] calldata batch + BatchLockup.CreateWithTimestampsLL[] calldata batch ) external override @@ -87,56 +183,50 @@ contract SablierV2Batch is ISablierV2Batch { // Check that the batch is not empty. uint256 batchSize = batch.length; if (batchSize == 0) { - revert Errors.SablierV2Batch_BatchSizeZero(); + revert Errors.SablierV2BatchLockup_BatchSizeZero(); } // Calculate the sum of all of stream amounts. It is safe to use unchecked addition because one of the create // transactions will revert if there is overflow. uint256 i; uint256 transferAmount; - for (i = 0; i < batchSize;) { + for (i = 0; i < batchSize; ++i) { unchecked { transferAmount += batch[i].totalAmount; - i += 1; } } - // Transfers the assets to the batch and approve the Sablier contract to spend them. + // Perform the ERC-20 transfer and approve {SablierV2LockupLinear} to spend the amount of assets. _handleTransfer(address(lockupLinear), asset, transferAmount); // Create a stream for each element in the parameter array. streamIds = new uint256[](batchSize); - for (i = 0; i < batchSize;) { + for (i = 0; i < batchSize; ++i) { // Create the stream. - streamIds[i] = lockupLinear.createWithRange( - LockupLinear.CreateWithRange({ - asset: asset, - broker: batch[i].broker, - cancelable: batch[i].cancelable, - range: batch[i].range, - recipient: batch[i].recipient, + streamIds[i] = lockupLinear.createWithTimestamps( + LockupLinear.CreateWithTimestamps({ sender: batch[i].sender, + recipient: batch[i].recipient, totalAmount: batch[i].totalAmount, - transferable: batch[i].transferable + asset: asset, + cancelable: batch[i].cancelable, + transferable: batch[i].transferable, + timestamps: batch[i].timestamps, + broker: batch[i].broker }) ); - - // Increment the for loop iterator. - unchecked { - i += 1; - } } } /*////////////////////////////////////////////////////////////////////////// - SABLIER-V2-LOCKUP-DYNAMIC + SABLIER-V2-LOCKUP-TRANCHED //////////////////////////////////////////////////////////////////////////*/ - /// @inheritdoc ISablierV2Batch - function createWithDeltas( - ISablierV2LockupDynamic lockupDynamic, + /// @inheritdoc ISablierV2BatchLockup + function createWithDurationsLT( + ISablierV2LockupTranched lockupTranched, IERC20 asset, - Batch.CreateWithDeltas[] calldata batch + BatchLockup.CreateWithDurationsLT[] calldata batch ) external override @@ -145,52 +235,46 @@ contract SablierV2Batch is ISablierV2Batch { // Check that the batch size is not zero. uint256 batchSize = batch.length; if (batchSize == 0) { - revert Errors.SablierV2Batch_BatchSizeZero(); + revert Errors.SablierV2BatchLockup_BatchSizeZero(); } // Calculate the sum of all of stream amounts. It is safe to use unchecked addition because one of the create // transactions will revert if there is overflow. uint256 i; uint256 transferAmount; - for (i = 0; i < batchSize;) { + for (i = 0; i < batchSize; ++i) { unchecked { transferAmount += batch[i].totalAmount; - i += 1; } } - // Perform the ERC-20 transfer and approve {SablierV2LockupDynamic} to spend the amount of assets. - _handleTransfer(address(lockupDynamic), asset, transferAmount); + // Perform the ERC-20 transfer and approve {SablierV2LockupTranched} to spend the amount of assets. + _handleTransfer(address(lockupTranched), asset, transferAmount); // Create a stream for each element in the parameter array. streamIds = new uint256[](batchSize); - for (i = 0; i < batchSize;) { + for (i = 0; i < batchSize; ++i) { // Create the stream. - streamIds[i] = lockupDynamic.createWithDeltas( - LockupDynamic.CreateWithDeltas({ - asset: asset, - broker: batch[i].broker, - cancelable: batch[i].cancelable, - recipient: batch[i].recipient, - segments: batch[i].segments, + streamIds[i] = lockupTranched.createWithDurations( + LockupTranched.CreateWithDurations({ sender: batch[i].sender, + recipient: batch[i].recipient, totalAmount: batch[i].totalAmount, - transferable: batch[i].transferable + asset: asset, + cancelable: batch[i].cancelable, + transferable: batch[i].transferable, + tranches: batch[i].tranches, + broker: batch[i].broker }) ); - - // Increment the for loop iterator. - unchecked { - i += 1; - } } } - /// @inheritdoc ISablierV2Batch - function createWithMilestones( - ISablierV2LockupDynamic lockupDynamic, + /// @inheritdoc ISablierV2BatchLockup + function createWithTimestampsLT( + ISablierV2LockupTranched lockupTranched, IERC20 asset, - Batch.CreateWithMilestones[] calldata batch + BatchLockup.CreateWithTimestampsLT[] calldata batch ) external override @@ -199,45 +283,39 @@ contract SablierV2Batch is ISablierV2Batch { // Check that the batch size is not zero. uint256 batchSize = batch.length; if (batchSize == 0) { - revert Errors.SablierV2Batch_BatchSizeZero(); + revert Errors.SablierV2BatchLockup_BatchSizeZero(); } // Calculate the sum of all of stream amounts. It is safe to use unchecked addition because one of the create // transactions will revert if there is overflow. uint256 i; uint256 transferAmount; - for (i = 0; i < batchSize;) { + for (i = 0; i < batchSize; ++i) { unchecked { transferAmount += batch[i].totalAmount; - i += 1; } } - // Perform the ERC-20 transfer and approve {SablierV2LockupDynamic} to spend the amount of assets. - _handleTransfer(address(lockupDynamic), asset, transferAmount); + // Perform the ERC-20 transfer and approve {SablierV2LockupTranched} to spend the amount of assets. + _handleTransfer(address(lockupTranched), asset, transferAmount); // Create a stream for each element in the parameter array. streamIds = new uint256[](batchSize); - for (i = 0; i < batchSize;) { + for (i = 0; i < batchSize; ++i) { // Create the stream. - streamIds[i] = lockupDynamic.createWithMilestones( - LockupDynamic.CreateWithMilestones({ + streamIds[i] = lockupTranched.createWithTimestamps( + LockupTranched.CreateWithTimestamps({ + sender: batch[i].sender, + recipient: batch[i].recipient, + totalAmount: batch[i].totalAmount, asset: asset, - broker: batch[i].broker, cancelable: batch[i].cancelable, - recipient: batch[i].recipient, - segments: batch[i].segments, - sender: batch[i].sender, + transferable: batch[i].transferable, startTime: batch[i].startTime, - totalAmount: batch[i].totalAmount, - transferable: batch[i].transferable + tranches: batch[i].tranches, + broker: batch[i].broker }) ); - - // Increment the for loop iterator. - unchecked { - i += 1; - } } } @@ -245,7 +323,7 @@ contract SablierV2Batch is ISablierV2Batch { HELPER FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @dev Helper function to approve a Sablier contract to spend funds from the batch. If the current allowance + /// @dev Helper function to approve a Sablier contract to spend funds from the batchLockup. If the current allowance /// is insufficient, this function approves Sablier to spend the exact `amount`. /// The {SafeERC20.forceApprove} function is used to handle special ERC-20 assets (e.g. USDT) that require the /// current allowance to be zero before setting it to a non-zero value. @@ -256,9 +334,10 @@ contract SablierV2Batch is ISablierV2Batch { } } - /// @dev Helper function to transfer assets from the caller to the batch contract and approve the Sablier contract. + /// @dev Helper function to transfer assets from the caller to the batchLockup contract and approve the Sablier + /// contract. function _handleTransfer(address sablierContract, IERC20 asset, uint256 amount) internal { - // Transfer the assets to the batch contract. + // Transfer the assets to the batchLockup contract. asset.safeTransferFrom({ from: msg.sender, to: address(this), value: amount }); // Approve the Sablier contract to spend funds. diff --git a/src/SablierV2MerkleStreamerLL.sol b/src/SablierV2MerkleLL.sol similarity index 66% rename from src/SablierV2MerkleStreamerLL.sol rename to src/SablierV2MerkleLL.sol index fbfd99e8..fd9d41f6 100644 --- a/src/SablierV2MerkleStreamerLL.sol +++ b/src/SablierV2MerkleLL.sol @@ -1,37 +1,34 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; import { BitMaps } from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { ud } from "@prb/math/src/UD60x18.sol"; import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; import { Broker, LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; -import { ud } from "@prb/math/src/UD60x18.sol"; -import { SablierV2MerkleStreamer } from "./abstracts/SablierV2MerkleStreamer.sol"; -import { ISablierV2MerkleStreamerLL } from "./interfaces/ISablierV2MerkleStreamerLL.sol"; +import { SablierV2MerkleLockup } from "./abstracts/SablierV2MerkleLockup.sol"; +import { ISablierV2MerkleLL } from "./interfaces/ISablierV2MerkleLL.sol"; +import { MerkleLockup } from "./types/DataTypes.sol"; -/// @title SablierV2MerkleStreamerLL -/// @notice See the documentation in {ISablierV2MerkleStreamerLL}. -contract SablierV2MerkleStreamerLL is - ISablierV2MerkleStreamerLL, // 2 inherited components - SablierV2MerkleStreamer // 4 inherited components +/// @title SablierV2MerkleLL +/// @notice See the documentation in {ISablierV2MerkleLL}. +contract SablierV2MerkleLL is + ISablierV2MerkleLL, // 2 inherited components + SablierV2MerkleLockup // 4 inherited components { using BitMaps for BitMaps.BitMap; using SafeERC20 for IERC20; /*////////////////////////////////////////////////////////////////////////// - USER-FACING CONSTANTS + STATE VARIABLES //////////////////////////////////////////////////////////////////////////*/ - /// @inheritdoc ISablierV2MerkleStreamerLL + /// @inheritdoc ISablierV2MerkleLL ISablierV2LockupLinear public immutable override LOCKUP_LINEAR; - /*////////////////////////////////////////////////////////////////////////// - USER-FACING STORAGE - //////////////////////////////////////////////////////////////////////////*/ - - /// @inheritdoc ISablierV2MerkleStreamerLL + /// @inheritdoc ISablierV2MerkleLL LockupLinear.Durations public override streamDurations; /*////////////////////////////////////////////////////////////////////////// @@ -41,21 +38,16 @@ contract SablierV2MerkleStreamerLL is /// @dev Constructs the contract by initializing the immutable state variables, and max approving the Sablier /// contract. constructor( - address initialAdmin, + MerkleLockup.ConstructorParams memory baseParams, ISablierV2LockupLinear lockupLinear, - IERC20 asset, - bytes32 merkleRoot, - uint40 expiration, - LockupLinear.Durations memory streamDurations_, - bool cancelable, - bool transferable + LockupLinear.Durations memory streamDurations_ ) - SablierV2MerkleStreamer(initialAdmin, asset, lockupLinear, merkleRoot, expiration, cancelable, transferable) + SablierV2MerkleLockup(baseParams) { LOCKUP_LINEAR = lockupLinear; streamDurations = streamDurations_; - // Max approve the Sablier contract to spend funds from the Merkle streamer. + // Max approve the Sablier contract to spend funds from the MerkleLockup contract. ASSET.forceApprove(address(LOCKUP_LINEAR), type(uint256).max); } @@ -63,7 +55,7 @@ contract SablierV2MerkleStreamerLL is USER-FACING NON-CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @inheritdoc ISablierV2MerkleStreamerLL + /// @inheritdoc ISablierV2MerkleLL function claim( uint256 index, address recipient, @@ -78,23 +70,23 @@ contract SablierV2MerkleStreamerLL is // preimage attacks. bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(index, recipient, amount)))); - // Checks: validate the function. + // Check: validate the function. _checkClaim(index, leaf, merkleProof); - // Effects: mark the index as claimed. + // Effect: mark the index as claimed. _claimedBitMap.set(index); - // Interactions: create the stream via {SablierV2LockupLinear}. + // Interaction: create the stream via {SablierV2LockupLinear}. streamId = LOCKUP_LINEAR.createWithDurations( LockupLinear.CreateWithDurations({ + sender: admin, + recipient: recipient, + totalAmount: amount, asset: ASSET, - broker: Broker({ account: address(0), fee: ud(0) }), cancelable: CANCELABLE, + transferable: TRANSFERABLE, durations: streamDurations, - recipient: recipient, - sender: admin, - totalAmount: amount, - transferable: TRANSFERABLE + broker: Broker({ account: address(0), fee: ud(0) }) }) ); diff --git a/src/SablierV2MerkleLT.sol b/src/SablierV2MerkleLT.sol new file mode 100644 index 00000000..286614e4 --- /dev/null +++ b/src/SablierV2MerkleLT.sol @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { BitMaps } from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { uUNIT } from "@prb/math/src/UD2x18.sol"; +import { UD60x18, ud60x18, ZERO } from "@prb/math/src/UD60x18.sol"; +import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; +import { Broker, LockupTranched } from "@sablier/v2-core/src/types/DataTypes.sol"; + +import { SablierV2MerkleLockup } from "./abstracts/SablierV2MerkleLockup.sol"; +import { ISablierV2MerkleLT } from "./interfaces/ISablierV2MerkleLT.sol"; +import { Errors } from "./libraries/Errors.sol"; +import { MerkleLockup, MerkleLT } from "./types/DataTypes.sol"; + +/// @title SablierV2MerkleLT +/// @notice See the documentation in {ISablierV2MerkleLT}. +contract SablierV2MerkleLT is + ISablierV2MerkleLT, // 2 inherited components + SablierV2MerkleLockup // 4 inherited components +{ + using BitMaps for BitMaps.BitMap; + using SafeERC20 for IERC20; + + /*////////////////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc ISablierV2MerkleLT + ISablierV2LockupTranched public immutable override LOCKUP_TRANCHED; + + /// @inheritdoc ISablierV2MerkleLT + uint64 public immutable override TOTAL_PERCENTAGE; + + /// @dev The tranches with their respective unlock percentages and durations. + MerkleLT.TrancheWithPercentage[] internal _tranchesWithPercentages; + + /*////////////////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Constructs the contract by initializing the immutable state variables, and max approving the Sablier + /// contract. + constructor( + MerkleLockup.ConstructorParams memory baseParams, + ISablierV2LockupTranched lockupTranched, + MerkleLT.TrancheWithPercentage[] memory tranchesWithPercentages + ) + SablierV2MerkleLockup(baseParams) + { + LOCKUP_TRANCHED = lockupTranched; + + uint256 count = tranchesWithPercentages.length; + + // Calculate the total percentage of the tranches and save them in the contract state. + uint64 totalPercentage; + for (uint256 i = 0; i < count; ++i) { + uint64 percentage = tranchesWithPercentages[i].unlockPercentage.unwrap(); + totalPercentage += percentage; + _tranchesWithPercentages.push(tranchesWithPercentages[i]); + } + TOTAL_PERCENTAGE = totalPercentage; + + // Max approve the Sablier contract to spend funds from the MerkleLockup contract. + ASSET.forceApprove(address(LOCKUP_TRANCHED), type(uint256).max); + } + + /*////////////////////////////////////////////////////////////////////////// + USER-FACING CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc ISablierV2MerkleLT + function getTranchesWithPercentages() external view override returns (MerkleLT.TrancheWithPercentage[] memory) { + return _tranchesWithPercentages; + } + + /*////////////////////////////////////////////////////////////////////////// + USER-FACING NON-CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc ISablierV2MerkleLT + function claim( + uint256 index, + address recipient, + uint128 amount, + bytes32[] calldata merkleProof + ) + external + override + returns (uint256 streamId) + { + // Check: the sum of percentages equals 100%. + if (TOTAL_PERCENTAGE != uUNIT) { + revert Errors.SablierV2MerkleLT_TotalPercentageNotOneHundred(TOTAL_PERCENTAGE); + } + + // Generate the Merkle tree leaf by hashing the corresponding parameters. Hashing twice prevents second + // preimage attacks. + bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(index, recipient, amount)))); + + // Check: validate the function. + _checkClaim(index, leaf, merkleProof); + + // Calculate the tranches based on the unlock percentages. + LockupTranched.TrancheWithDuration[] memory tranches = _calculateTranches(amount); + + // Effect: mark the index as claimed. + _claimedBitMap.set(index); + + // Interaction: create the stream via {SablierV2LockupTranched}. + streamId = LOCKUP_TRANCHED.createWithDurations( + LockupTranched.CreateWithDurations({ + sender: admin, + recipient: recipient, + totalAmount: amount, + asset: ASSET, + cancelable: CANCELABLE, + transferable: TRANSFERABLE, + tranches: tranches, + broker: Broker({ account: address(0), fee: ZERO }) + }) + ); + + // Log the claim. + emit Claim(index, recipient, amount, streamId); + } + + /*////////////////////////////////////////////////////////////////////////// + INTERNAL CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Calculates the tranches based on the claim amount and the unlock percentages for each tranche. + function _calculateTranches(uint128 claimAmount) + internal + view + returns (LockupTranched.TrancheWithDuration[] memory tranches) + { + // Load the tranches in memory (to save gas). + MerkleLT.TrancheWithPercentage[] memory tranchesWithPercentages = _tranchesWithPercentages; + + // Declare the variables needed for calculation. + uint128 calculatedAmountsSum; + UD60x18 claimAmountUD = ud60x18(claimAmount); + uint256 trancheCount = tranchesWithPercentages.length; + tranches = new LockupTranched.TrancheWithDuration[](trancheCount); + + // Iterate over each tranche to calculate its unlock amount. + for (uint256 i = 0; i < trancheCount; ++i) { + // Convert the tranche's percentage from the `UD2x18` to the `UD60x18` type. + UD60x18 percentage = (tranchesWithPercentages[i].unlockPercentage).intoUD60x18(); + + // Calculate the tranche's amount by multiplying the claim amount by the unlock percentage. + uint128 calculatedAmount = claimAmountUD.mul(percentage).intoUint128(); + + // Create the tranche with duration. + tranches[i] = LockupTranched.TrancheWithDuration({ + amount: calculatedAmount, + duration: tranchesWithPercentages[i].duration + }); + + // Add the calculated tranche amount. + calculatedAmountsSum += calculatedAmount; + } + + // It should never be the case that the sum of the calculated amounts is greater than the claim amount because + // PRBMath always rounds down. + assert(calculatedAmountsSum <= claimAmount); + + // Since there can be rounding errors, the last tranche amount needs to be adjusted to ensure the sum of all + // tranche amounts equals the claim amount. + if (calculatedAmountsSum < claimAmount) { + unchecked { + tranches[trancheCount - 1].amount += claimAmount - calculatedAmountsSum; + } + } + } +} diff --git a/src/SablierV2MerkleLockupFactory.sol b/src/SablierV2MerkleLockupFactory.sol new file mode 100644 index 00000000..35c5f12a --- /dev/null +++ b/src/SablierV2MerkleLockupFactory.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { uUNIT } from "@prb/math/src/UD2x18.sol"; +import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; +import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; +import { LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; + +import { ISablierV2MerkleLL } from "./interfaces/ISablierV2MerkleLL.sol"; +import { ISablierV2MerkleLockupFactory } from "./interfaces/ISablierV2MerkleLockupFactory.sol"; +import { ISablierV2MerkleLT } from "./interfaces/ISablierV2MerkleLT.sol"; +import { SablierV2MerkleLL } from "./SablierV2MerkleLL.sol"; +import { SablierV2MerkleLT } from "./SablierV2MerkleLT.sol"; +import { MerkleLockup, MerkleLT } from "./types/DataTypes.sol"; + +/// @title SablierV2MerkleLockupFactory +/// @notice See the documentation in {ISablierV2MerkleLockupFactory}. +contract SablierV2MerkleLockupFactory is ISablierV2MerkleLockupFactory { + /*////////////////////////////////////////////////////////////////////////// + USER-FACING CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc ISablierV2MerkleLockupFactory + function isPercentagesSum100(MerkleLT.TrancheWithPercentage[] calldata tranches) + external + pure + override + returns (bool result) + { + uint64 totalPercentage; + for (uint256 i = 0; i < tranches.length; ++i) { + totalPercentage += tranches[i].unlockPercentage.unwrap(); + } + return totalPercentage == uUNIT; + } + + /*////////////////////////////////////////////////////////////////////////// + USER-FACING NON-CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice inheritdoc ISablierV2MerkleLockupFactory + function createMerkleLL( + MerkleLockup.ConstructorParams memory baseParams, + ISablierV2LockupLinear lockupLinear, + LockupLinear.Durations memory streamDurations, + uint256 aggregateAmount, + uint256 recipientCount + ) + external + returns (ISablierV2MerkleLL merkleLL) + { + // Hash the parameters to generate a salt. + bytes32 salt = keccak256( + abi.encodePacked( + msg.sender, + baseParams.asset, + baseParams.cancelable, + baseParams.expiration, + baseParams.initialAdmin, + abi.encode(baseParams.ipfsCID), + baseParams.merkleRoot, + bytes32(abi.encodePacked(baseParams.name)), + baseParams.transferable, + lockupLinear, + abi.encode(streamDurations) + ) + ); + + // Deploy the MerkleLockup contract with CREATE2. + merkleLL = new SablierV2MerkleLL{ salt: salt }(baseParams, lockupLinear, streamDurations); + + // Log the creation of the MerkleLockup contract, including some metadata that is not stored on-chain. + emit CreateMerkleLL(merkleLL, baseParams, lockupLinear, streamDurations, aggregateAmount, recipientCount); + } + + /// @notice inheritdoc ISablierV2MerkleLockupFactory + function createMerkleLT( + MerkleLockup.ConstructorParams memory baseParams, + ISablierV2LockupTranched lockupTranched, + MerkleLT.TrancheWithPercentage[] memory tranchesWithPercentages, + uint256 aggregateAmount, + uint256 recipientCount + ) + external + returns (ISablierV2MerkleLT merkleLT) + { + uint256 totalDuration; + + // Need a separate scope to prevent the stack too deep error. + { + // Calculate the sum of percentages and durations across all tranches. + uint256 count = tranchesWithPercentages.length; + for (uint256 i = 0; i < count; ++i) { + unchecked { + // Safe to use `unchecked` because its only used in the event. + totalDuration += tranchesWithPercentages[i].duration; + } + } + } + + // Hash the parameters to generate a salt. + bytes32 salt = keccak256( + abi.encodePacked( + msg.sender, + baseParams.asset, + baseParams.cancelable, + baseParams.expiration, + baseParams.initialAdmin, + abi.encode(baseParams.ipfsCID), + baseParams.merkleRoot, + bytes32(abi.encodePacked(baseParams.name)), + baseParams.transferable, + lockupTranched, + abi.encode(tranchesWithPercentages) + ) + ); + + // Deploy the MerkleLockup contract with CREATE2. + merkleLT = new SablierV2MerkleLT{ salt: salt }(baseParams, lockupTranched, tranchesWithPercentages); + + // Log the creation of the MerkleLockup contract, including some metadata that is not stored on-chain. + emit CreateMerkleLT( + merkleLT, + baseParams, + lockupTranched, + tranchesWithPercentages, + totalDuration, + aggregateAmount, + recipientCount + ); + } +} diff --git a/src/SablierV2MerkleStreamerFactory.sol b/src/SablierV2MerkleStreamerFactory.sol deleted file mode 100644 index e9f14b1f..00000000 --- a/src/SablierV2MerkleStreamerFactory.sol +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; -import { LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; - -import { ISablierV2MerkleStreamerFactory } from "./interfaces/ISablierV2MerkleStreamerFactory.sol"; -import { ISablierV2MerkleStreamerLL } from "./interfaces/ISablierV2MerkleStreamerLL.sol"; -import { SablierV2MerkleStreamerLL } from "./SablierV2MerkleStreamerLL.sol"; - -/// @title SablierV2MerkleStreamerFactory -/// @notice See the documentation in {ISablierV2MerkleStreamerFactory}. -contract SablierV2MerkleStreamerFactory is ISablierV2MerkleStreamerFactory { - /*////////////////////////////////////////////////////////////////////////// - USER-FACING NON-CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice inheritdoc ISablierV2MerkleStreamerFactory - function createMerkleStreamerLL( - address initialAdmin, - ISablierV2LockupLinear lockupLinear, - IERC20 asset, - bytes32 merkleRoot, - uint40 expiration, - LockupLinear.Durations memory streamDurations, - bool cancelable, - bool transferable, - string memory ipfsCID, - uint256 aggregateAmount, - uint256 recipientsCount - ) - external - returns (ISablierV2MerkleStreamerLL merkleStreamerLL) - { - // Hash the parameters to generate a salt. - bytes32 salt = keccak256( - abi.encodePacked( - initialAdmin, - lockupLinear, - asset, - merkleRoot, - expiration, - abi.encode(streamDurations), - cancelable, - transferable - ) - ); - - // Deploy the Merkle streamer with CREATE2. - merkleStreamerLL = new SablierV2MerkleStreamerLL{ salt: salt }( - initialAdmin, lockupLinear, asset, merkleRoot, expiration, streamDurations, cancelable, transferable - ); - - // Log the creation of the Merkle streamer, including some metadata that is not stored on-chain. - emit CreateMerkleStreamerLL( - merkleStreamerLL, - initialAdmin, - lockupLinear, - asset, - merkleRoot, - expiration, - streamDurations, - cancelable, - transferable, - ipfsCID, - aggregateAmount, - recipientsCount - ); - } -} diff --git a/src/abstracts/SablierV2MerkleLockup.sol b/src/abstracts/SablierV2MerkleLockup.sol new file mode 100644 index 00000000..b342c56f --- /dev/null +++ b/src/abstracts/SablierV2MerkleLockup.sol @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { MerkleProof } from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; +import { BitMaps } from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; +import { Adminable } from "@sablier/v2-core/src/abstracts/Adminable.sol"; + +import { ISablierV2MerkleLockup } from "../interfaces/ISablierV2MerkleLockup.sol"; +import { MerkleLockup } from "../types/DataTypes.sol"; +import { Errors } from "../libraries/Errors.sol"; + +/// @title SablierV2MerkleLockup +/// @notice See the documentation in {ISablierV2MerkleLockup}. +abstract contract SablierV2MerkleLockup is + ISablierV2MerkleLockup, // 2 inherited component + Adminable // 1 inherited component +{ + using BitMaps for BitMaps.BitMap; + using SafeERC20 for IERC20; + + /*////////////////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc ISablierV2MerkleLockup + IERC20 public immutable override ASSET; + + /// @inheritdoc ISablierV2MerkleLockup + bool public immutable override CANCELABLE; + + /// @inheritdoc ISablierV2MerkleLockup + uint40 public immutable override EXPIRATION; + + /// @inheritdoc ISablierV2MerkleLockup + bytes32 public immutable override MERKLE_ROOT; + + /// @dev The name of the campaign stored as bytes32. + bytes32 internal immutable NAME; + + /// @inheritdoc ISablierV2MerkleLockup + bool public immutable override TRANSFERABLE; + + /// @inheritdoc ISablierV2MerkleLockup + string public ipfsCID; + + /// @dev Packed booleans that record the history of claims. + BitMaps.BitMap internal _claimedBitMap; + + /// @dev The timestamp when the first claim is made. + uint40 internal _firstClaimTime; + + /*////////////////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Constructs the contract by initializing the immutable state variables. + constructor(MerkleLockup.ConstructorParams memory params) { + // Check: the campaign name is not greater than 32 bytes + if (bytes(params.name).length > 32) { + revert Errors.SablierV2MerkleLockup_CampaignNameTooLong({ + nameLength: bytes(params.name).length, + maxLength: 32 + }); + } + + admin = params.initialAdmin; + ASSET = params.asset; + CANCELABLE = params.cancelable; + EXPIRATION = params.expiration; + ipfsCID = params.ipfsCID; + MERKLE_ROOT = params.merkleRoot; + NAME = bytes32(abi.encodePacked(params.name)); + TRANSFERABLE = params.transferable; + } + + /*////////////////////////////////////////////////////////////////////////// + USER-FACING CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc ISablierV2MerkleLockup + function getFirstClaimTime() external view override returns (uint40) { + return _firstClaimTime; + } + + /// @inheritdoc ISablierV2MerkleLockup + function hasClaimed(uint256 index) public view override returns (bool) { + return _claimedBitMap.get(index); + } + + /// @inheritdoc ISablierV2MerkleLockup + function hasExpired() public view override returns (bool) { + return EXPIRATION > 0 && EXPIRATION <= block.timestamp; + } + + /// @inheritdoc ISablierV2MerkleLockup + function name() external view override returns (string memory) { + return string(abi.encodePacked(NAME)); + } + + /*////////////////////////////////////////////////////////////////////////// + USER-FACING NON-CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc ISablierV2MerkleLockup + function clawback(address to, uint128 amount) external override onlyAdmin { + // Check: current timestamp is over the grace period and the campaign has not expired. + if (_hasGracePeriodPassed() && !hasExpired()) { + revert Errors.SablierV2MerkleLockup_ClawbackNotAllowed({ + blockTimestamp: block.timestamp, + expiration: EXPIRATION, + firstClaimTime: _firstClaimTime + }); + } + + // Effect: transfer the tokens to the provided address. + ASSET.safeTransfer(to, amount); + + // Log the clawback. + emit Clawback(admin, to, amount); + } + + /*////////////////////////////////////////////////////////////////////////// + INTERNAL CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Returns a flag indicating whether the grace period has passed. + /// @dev The grace period is 7 days after the first claim. + function _hasGracePeriodPassed() internal view returns (bool) { + return _firstClaimTime > 0 && block.timestamp > _firstClaimTime + 7 days; + } + + /*////////////////////////////////////////////////////////////////////////// + INTERNAL NON-CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Validates the parameters of the `claim` function, which is implemented by child contracts. + function _checkClaim(uint256 index, bytes32 leaf, bytes32[] calldata merkleProof) internal { + // Check: the campaign has not expired. + if (hasExpired()) { + revert Errors.SablierV2MerkleLockup_CampaignExpired({ + blockTimestamp: block.timestamp, + expiration: EXPIRATION + }); + } + + // Check: the index has not been claimed. + if (_claimedBitMap.get(index)) { + revert Errors.SablierV2MerkleLockup_StreamClaimed(index); + } + + // Check: the input claim is included in the Merkle tree. + if (!MerkleProof.verify(merkleProof, MERKLE_ROOT, leaf)) { + revert Errors.SablierV2MerkleLockup_InvalidProof(); + } + + // Effect: set the `_firstClaimTime` if its zero. + if (_firstClaimTime == 0) { + _firstClaimTime = uint40(block.timestamp); + } + } +} diff --git a/src/abstracts/SablierV2MerkleStreamer.sol b/src/abstracts/SablierV2MerkleStreamer.sol deleted file mode 100644 index 9591a6af..00000000 --- a/src/abstracts/SablierV2MerkleStreamer.sol +++ /dev/null @@ -1,148 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { MerkleProof } from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; -import { BitMaps } from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; -import { Adminable } from "@sablier/v2-core/src/abstracts/Adminable.sol"; -import { ISablierV2Lockup } from "@sablier/v2-core/src/interfaces/ISablierV2Lockup.sol"; -import { UD60x18, ud } from "@prb/math/src/UD60x18.sol"; - -import { ISablierV2MerkleStreamer } from "../interfaces/ISablierV2MerkleStreamer.sol"; -import { Errors } from "../libraries/Errors.sol"; - -/// @title SablierV2MerkleStreamer -/// @notice See the documentation in {ISablierV2MerkleStreamer}. -abstract contract SablierV2MerkleStreamer is - ISablierV2MerkleStreamer, // 2 inherited component - Adminable // 1 inherited component -{ - using BitMaps for BitMaps.BitMap; - using SafeERC20 for IERC20; - - /*////////////////////////////////////////////////////////////////////////// - USER-FACING CONSTANTS - //////////////////////////////////////////////////////////////////////////*/ - - /// @inheritdoc ISablierV2MerkleStreamer - IERC20 public immutable override ASSET; - - /// @inheritdoc ISablierV2MerkleStreamer - bool public immutable override CANCELABLE; - - /// @inheritdoc ISablierV2MerkleStreamer - uint40 public immutable override EXPIRATION; - - /// @inheritdoc ISablierV2MerkleStreamer - ISablierV2Lockup public immutable override LOCKUP; - - /// @inheritdoc ISablierV2MerkleStreamer - bytes32 public immutable override MERKLE_ROOT; - - /// @inheritdoc ISablierV2MerkleStreamer - bool public immutable override TRANSFERABLE; - - /*////////////////////////////////////////////////////////////////////////// - INTERNAL STORAGE - //////////////////////////////////////////////////////////////////////////*/ - - /// @dev Packed booleans that record the history of claims. - BitMaps.BitMap internal _claimedBitMap; - - /*////////////////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////////////////*/ - - /// @dev Constructs the contract by initializing the immutable state variables. - constructor( - address initialAdmin, - IERC20 asset, - ISablierV2Lockup lockup, - bytes32 merkleRoot, - uint40 expiration, - bool cancelable, - bool transferable - ) { - admin = initialAdmin; - ASSET = asset; - LOCKUP = lockup; - MERKLE_ROOT = merkleRoot; - EXPIRATION = expiration; - CANCELABLE = cancelable; - TRANSFERABLE = transferable; - } - - /*////////////////////////////////////////////////////////////////////////// - USER-FACING CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @inheritdoc ISablierV2MerkleStreamer - function hasClaimed(uint256 index) public view override returns (bool) { - return _claimedBitMap.get(index); - } - - /// @inheritdoc ISablierV2MerkleStreamer - function hasExpired() public view override returns (bool) { - return EXPIRATION > 0 && EXPIRATION <= block.timestamp; - } - - /*////////////////////////////////////////////////////////////////////////// - USER-FACING NON-CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @inheritdoc ISablierV2MerkleStreamer - function clawback(address to, uint128 amount) external override onlyAdmin { - // Safe Interactions: query the protocol fee. This is safe because it's a known Sablier contract that does - // not call other unknown contracts. - UD60x18 protocolFee = LOCKUP.comptroller().protocolFees(ASSET); - - // Checks: the campaign is not expired and the protocol fee is zero. - if (!hasExpired() && !protocolFee.gt(ud(0))) { - revert Errors.SablierV2MerkleStreamer_CampaignNotExpired({ - currentTime: block.timestamp, - expiration: EXPIRATION - }); - } - - // Effects: transfer the tokens to the provided address. - ASSET.safeTransfer(to, amount); - - // Log the clawback. - emit Clawback(admin, to, amount); - } - - /*////////////////////////////////////////////////////////////////////////// - INTERNAL CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @dev Validates the parameters of the `claim` function, which is implemented by child contracts. - function _checkClaim(uint256 index, bytes32 leaf, bytes32[] calldata merkleProof) internal view { - // Checks: the campaign has not expired. - if (hasExpired()) { - revert Errors.SablierV2MerkleStreamer_CampaignExpired({ - currentTime: block.timestamp, - expiration: EXPIRATION - }); - } - - // Checks: the index has not been claimed. - if (_claimedBitMap.get(index)) { - revert Errors.SablierV2MerkleStreamer_StreamClaimed(index); - } - - // Checks: the input claim is included in the Merkle tree. - if (!MerkleProof.verify(merkleProof, MERKLE_ROOT, leaf)) { - revert Errors.SablierV2MerkleStreamer_InvalidProof(); - } - - // Safe Interactions: query the protocol fee. This is safe because it's a known Sablier contract that does - // not call other unknown contracts. - UD60x18 protocolFee = LOCKUP.comptroller().protocolFees(ASSET); - - // Checks: the protocol fee is zero. - if (protocolFee.gt(ud(0))) { - revert Errors.SablierV2MerkleStreamer_ProtocolFeeNotZero(); - } - } -} diff --git a/src/interfaces/ISablierV2Batch.sol b/src/interfaces/ISablierV2Batch.sol deleted file mode 100644 index 1d24f273..00000000 --- a/src/interfaces/ISablierV2Batch.sol +++ /dev/null @@ -1,96 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { ISablierV2LockupDynamic } from "@sablier/v2-core/src/interfaces/ISablierV2LockupDynamic.sol"; -import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; - -import { Batch } from "../types/DataTypes.sol"; - -/// @title ISablierV2Batch -/// @notice Helper to batch create Sablier V2 Lockup streams. -interface ISablierV2Batch { - /*////////////////////////////////////////////////////////////////////////// - SABLIER-V2-LOCKUP-LINEAR - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Creates a batch of Lockup Linear streams using `createWithDurations`. - /// - /// @dev Requirements: - /// - There must be at least one element in `batch`. - /// - All requirements from {ISablierV2LockupLinear.createWithDurations} must be met for each stream. - /// - /// @param lockupLinear The address of the {SablierV2LockupLinear} contract. - /// @param asset The contract address of the ERC-20 asset used for streaming. - /// @param batch An array of structs, each encapsulating a subset of the parameters of - /// {SablierV2LockupLinear.createWithDurations}. - /// @return streamIds The ids of the newly created streams. - function createWithDurations( - ISablierV2LockupLinear lockupLinear, - IERC20 asset, - Batch.CreateWithDurations[] calldata batch - ) - external - returns (uint256[] memory streamIds); - - /// @notice Creates a batch of Lockup Linear streams using `createWithRange`. - /// - /// @dev Requirements: - /// - There must be at least one element in `batch`. - /// - All requirements from {ISablierV2LockupLinear.createWithRange} must be met for each stream. - /// - /// @param lockupLinear The address of the {SablierV2LockupLinear} contract. - /// @param asset The contract address of the ERC-20 asset used for streaming. - /// @param batch An array of structs, each encapsulating a subset of the parameters of - /// {SablierV2LockupLinear.createWithRange}. - /// @return streamIds The ids of the newly created streams. - function createWithRange( - ISablierV2LockupLinear lockupLinear, - IERC20 asset, - Batch.CreateWithRange[] calldata batch - ) - external - returns (uint256[] memory streamIds); - - /*////////////////////////////////////////////////////////////////////////// - SABLIER-V2-LOCKUP-DYNAMIC - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Creates a batch of Lockup Dynamic streams using `createWithDeltas`. - /// - /// @dev Requirements: - /// - There must be at least one element in `batch`. - /// - All requirements from {ISablierV2LockupDynamic.createWithDeltas} must be met for each stream. - /// - /// @param lockupDynamic The address of the {SablierV2LockupDynamic} contract. - /// @param asset The contract address of the ERC-20 asset used for streaming. - /// @param batch An array of structs, each encapsulating a subset of the parameters of - /// {SablierV2LockupDynamic.createWithDeltas}. - /// @return streamIds The ids of the newly created streams. - function createWithDeltas( - ISablierV2LockupDynamic lockupDynamic, - IERC20 asset, - Batch.CreateWithDeltas[] calldata batch - ) - external - returns (uint256[] memory streamIds); - - /// @notice Creates a batch of Lockup Dynamic streams using `createWithMilestones`. - /// - /// @dev Requirements: - /// - There must be at least one element in `batch`. - /// - All requirements from {ISablierV2LockupDynamic.createWithMilestones} must be met for each stream. - /// - /// @param lockupDynamic The address of the {SablierV2LockupDynamic} contract. - /// @param asset The contract address of the ERC-20 asset used for streaming. - /// @param batch An array of structs, each encapsulating a subset of the parameters of - /// {SablierV2LockupDynamic.createWithMilestones}. - /// @return streamIds The ids of the newly created streams. - function createWithMilestones( - ISablierV2LockupDynamic lockupDynamic, - IERC20 asset, - Batch.CreateWithMilestones[] calldata batch - ) - external - returns (uint256[] memory streamIds); -} diff --git a/src/interfaces/ISablierV2BatchLockup.sol b/src/interfaces/ISablierV2BatchLockup.sol new file mode 100644 index 00000000..4460fca3 --- /dev/null +++ b/src/interfaces/ISablierV2BatchLockup.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ISablierV2LockupDynamic } from "@sablier/v2-core/src/interfaces/ISablierV2LockupDynamic.sol"; +import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; +import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; + +import { BatchLockup } from "../types/DataTypes.sol"; + +/// @title ISablierV2BatchLockup +/// @notice Helper to batch create Sablier V2 Lockup streams. +interface ISablierV2BatchLockup { + /*////////////////////////////////////////////////////////////////////////// + SABLIER-V2-LOCKUP-LINEAR + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Creates a batch of LockupLinear streams using `createWithDurations`. + /// + /// @dev Requirements: + /// - There must be at least one element in `batch`. + /// - All requirements from {ISablierV2LockupLinear.createWithDurations} must be met for each stream. + /// + /// @param lockupLinear The address of the {SablierV2LockupLinear} contract. + /// @param asset The contract address of the ERC-20 asset to be distributed. + /// @param batch An array of structs, each encapsulating a subset of the parameters of + /// {SablierV2LockupLinear.createWithDurations}. + /// @return streamIds The ids of the newly created streams. + function createWithDurationsLL( + ISablierV2LockupLinear lockupLinear, + IERC20 asset, + BatchLockup.CreateWithDurationsLL[] calldata batch + ) + external + returns (uint256[] memory streamIds); + + /// @notice Creates a batch of LockupLinear streams using `createWithTimestamps`. + /// + /// @dev Requirements: + /// - There must be at least one element in `batch`. + /// - All requirements from {ISablierV2LockupLinear.createWithTimestamps} must be met for each stream. + /// + /// @param lockupLinear The address of the {SablierV2LockupLinear} contract. + /// @param asset The contract address of the ERC-20 asset to be distributed. + /// @param batch An array of structs, each encapsulating a subset of the parameters of + /// {SablierV2LockupLinear.createWithTimestamps}. + /// @return streamIds The ids of the newly created streams. + function createWithTimestampsLL( + ISablierV2LockupLinear lockupLinear, + IERC20 asset, + BatchLockup.CreateWithTimestampsLL[] calldata batch + ) + external + returns (uint256[] memory streamIds); + + /*////////////////////////////////////////////////////////////////////////// + SABLIER-V2-LOCKUP-DYNAMIC + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Creates a batch of Lockup Dynamic streams using `createWithDurations`. + /// + /// @dev Requirements: + /// - There must be at least one element in `batch`. + /// - All requirements from {ISablierV2LockupDynamic.createWithDurations} must be met for each stream. + /// + /// @param lockupDynamic The address of the {SablierV2LockupDynamic} contract. + /// @param asset The contract address of the ERC-20 asset to be distributed. + /// @param batch An array of structs, each encapsulating a subset of the parameters of + /// {SablierV2LockupDynamic.createWithDurations}. + /// @return streamIds The ids of the newly created streams. + function createWithDurationsLD( + ISablierV2LockupDynamic lockupDynamic, + IERC20 asset, + BatchLockup.CreateWithDurationsLD[] calldata batch + ) + external + returns (uint256[] memory streamIds); + + /// @notice Creates a batch of Lockup Dynamic streams using `createWithTimestamps`. + /// + /// @dev Requirements: + /// - There must be at least one element in `batch`. + /// - All requirements from {ISablierV2LockupDynamic.createWithTimestamps} must be met for each stream. + /// + /// @param lockupDynamic The address of the {SablierV2LockupDynamic} contract. + /// @param asset The contract address of the ERC-20 asset to be distributed. + /// @param batch An array of structs, each encapsulating a subset of the parameters of + /// {SablierV2LockupDynamic.createWithTimestamps}. + /// @return streamIds The ids of the newly created streams. + function createWithTimestampsLD( + ISablierV2LockupDynamic lockupDynamic, + IERC20 asset, + BatchLockup.CreateWithTimestampsLD[] calldata batch + ) + external + returns (uint256[] memory streamIds); + + /*////////////////////////////////////////////////////////////////////////// + SABLIER-V2-LOCKUP-TRANCHED + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Creates a batch of LockupTranched streams using `createWithDurations`. + /// + /// @dev Requirements: + /// - There must be at least one element in `batch`. + /// - All requirements from {ISablierV2LockupTranched.createWithDurations} must be met for each stream. + /// + /// @param lockupTranched The address of the {SablierV2LockupTranched} contract. + /// @param asset The contract address of the ERC-20 asset to be distributed. + /// @param batch An array of structs, each encapsulating a subset of the parameters of + /// {SablierV2LockupTranched.createWithDurations}. + /// @return streamIds The ids of the newly created streams. + function createWithDurationsLT( + ISablierV2LockupTranched lockupTranched, + IERC20 asset, + BatchLockup.CreateWithDurationsLT[] calldata batch + ) + external + returns (uint256[] memory streamIds); + + /// @notice Creates a batch of LockupTranched streams using `createWithTimestamps`. + /// + /// @dev Requirements: + /// - There must be at least one element in `batch`. + /// - All requirements from {ISablierV2LockupTranched.createWithTimestamps} must be met for each stream. + /// + /// @param lockupTranched The address of the {SablierV2LockupTranched} contract. + /// @param asset The contract address of the ERC-20 asset to be distributed. + /// @param batch An array of structs, each encapsulating a subset of the parameters of + /// {SablierV2LockupTranched.createWithTimestamps}. + /// @return streamIds The ids of the newly created streams. + function createWithTimestampsLT( + ISablierV2LockupTranched lockupTranched, + IERC20 asset, + BatchLockup.CreateWithTimestampsLT[] calldata batch + ) + external + returns (uint256[] memory streamIds); +} diff --git a/src/interfaces/ISablierV2MerkleStreamerLL.sol b/src/interfaces/ISablierV2MerkleLL.sol similarity index 73% rename from src/interfaces/ISablierV2MerkleStreamerLL.sol rename to src/interfaces/ISablierV2MerkleLL.sol index 794ad944..98e72f99 100644 --- a/src/interfaces/ISablierV2MerkleStreamerLL.sol +++ b/src/interfaces/ISablierV2MerkleLL.sol @@ -1,13 +1,13 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; -import { ISablierV2MerkleStreamer } from "./ISablierV2MerkleStreamer.sol"; +import { ISablierV2MerkleLockup } from "./ISablierV2MerkleLockup.sol"; -/// @title ISablierV2MerkleStreamerLL -/// @notice Merkle streamer that creates Lockup Linear streams. -interface ISablierV2MerkleStreamerLL is ISablierV2MerkleStreamer { +/// @title ISablierV2MerkleLL +/// @notice MerkleLockup campaign that creates LockupLinear streams. +interface ISablierV2MerkleLL is ISablierV2MerkleLockup { /*////////////////////////////////////////////////////////////////////////// CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ @@ -22,20 +22,20 @@ interface ISablierV2MerkleStreamerLL is ISablierV2MerkleStreamer { NON-CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @notice Makes the claim by creating a Lockup Linear stream to the recipient. + /// @notice Makes the claim by creating a LockupLinear stream to the recipient. A stream NFT is minted to the + /// recipient. /// /// @dev Emits a {Claim} event. /// /// Requirements: /// - The campaign must not have expired. /// - The stream must not have been claimed already. - /// - The protocol fee must be zero. /// - The Merkle proof must be valid. /// /// @param index The index of the recipient in the Merkle tree. /// @param recipient The address of the stream holder. - /// @param amount The amount of tokens to be streamed. - /// @param merkleProof The Merkle proof of inclusion in the stream. + /// @param amount The amount of ERC-20 assets to be distributed via the claimed stream. + /// @param merkleProof The proof of inclusion in the Merkle tree. /// @return streamId The id of the newly created stream. function claim( uint256 index, diff --git a/src/interfaces/ISablierV2MerkleLT.sol b/src/interfaces/ISablierV2MerkleLT.sol new file mode 100644 index 00000000..d64a082f --- /dev/null +++ b/src/interfaces/ISablierV2MerkleLT.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; + +import { ISablierV2MerkleLockup } from "./ISablierV2MerkleLockup.sol"; +import { MerkleLT } from "./../types/DataTypes.sol"; + +/// @title ISablierV2MerkleLT +/// @notice MerkleLockup campaign that creates LockupTranched streams. +interface ISablierV2MerkleLT is ISablierV2MerkleLockup { + /*////////////////////////////////////////////////////////////////////////// + CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Retrieves the tranches with their respective unlock percentages and durations. + function getTranchesWithPercentages() external view returns (MerkleLT.TrancheWithPercentage[] memory); + + /// @notice The address of the {SablierV2LockupTranched} contract. + function LOCKUP_TRANCHED() external view returns (ISablierV2LockupTranched); + + /// @notice The total percentage of the tranches. + function TOTAL_PERCENTAGE() external view returns (uint64); + + /*////////////////////////////////////////////////////////////////////////// + NON-CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Makes the claim by creating a LockupTranched stream to the recipient. A stream NFT is minted to the + /// recipient. + /// + /// @dev Emits a {Claim} event. + /// + /// Requirements: + /// - The campaign must not have expired. + /// - The stream must not have been claimed already. + /// - The Merkle proof must be valid. + /// - TOTAL_PERCENTAGE must be equal to 100%. + /// + /// @param index The index of the recipient in the Merkle tree. + /// @param recipient The address of the stream holder. + /// @param amount The amount of ERC-20 assets to be distributed via the claimed stream. + /// @param merkleProof The proof of inclusion in the Merkle tree. + /// @return streamId The id of the newly created stream. + function claim( + uint256 index, + address recipient, + uint128 amount, + bytes32[] calldata merkleProof + ) + external + returns (uint256 streamId); +} diff --git a/src/interfaces/ISablierV2MerkleStreamer.sol b/src/interfaces/ISablierV2MerkleLockup.sol similarity index 66% rename from src/interfaces/ISablierV2MerkleStreamer.sol rename to src/interfaces/ISablierV2MerkleLockup.sol index 42dc7a4d..73576eb3 100644 --- a/src/interfaces/ISablierV2MerkleStreamer.sol +++ b/src/interfaces/ISablierV2MerkleLockup.sol @@ -1,17 +1,15 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IAdminable } from "@sablier/v2-core/src/interfaces/IAdminable.sol"; -import { ISablierV2Lockup } from "@sablier/v2-core/src/interfaces/ISablierV2Lockup.sol"; - -/// @title ISablierV2MerkleStreamer -/// @notice A contract that lets user claim Sablier streams using Merkle proofs. An interesting use case for -/// MerkleStream is airstreams, which is a portmanteau of "airdrop" and "stream". This is an airdrop model where the -/// tokens are distributed over time, as opposed to all at once. -/// @dev This is the base interface for MerkleStreamer contracts. See the Sablier docs for more guidance on how -/// streaming works: https://docs.sablier.com/. -interface ISablierV2MerkleStreamer is IAdminable { + +/// @title ISablierV2MerkleLockup +/// @notice A contract that lets user claim Sablier streams using Merkle proofs. A popular use case for MerkleLockup +/// is airstreams: a portmanteau of "airdrop" and "stream". This is an airdrop model where the tokens are distributed +/// over time, as opposed to all at once. +/// @dev This is the base interface for MerkleLockup. See the Sablier docs for more guidance: https://docs.sablier.com +interface ISablierV2MerkleLockup is IAdminable { /*////////////////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////////////////*/ @@ -26,7 +24,7 @@ interface ISablierV2MerkleStreamer is IAdminable { CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @notice The streamed ERC-20 asset. + /// @notice The ERC-20 asset to distribute. /// @dev This is an immutable state variable. function ASSET() external returns (IERC20); @@ -34,26 +32,31 @@ interface ISablierV2MerkleStreamer is IAdminable { /// @dev This is an immutable state variable. function CANCELABLE() external returns (bool); - /// @notice The cut-off point for the Merkle streamer, as a Unix timestamp. A value of zero means there - /// is no expiration. + /// @notice The cut-off point for the campaign, as a Unix timestamp. A value of zero means there is no expiration. /// @dev This is an immutable state variable. function EXPIRATION() external returns (uint40); + /// @notice Returns the timestamp when the first claim is made. + function getFirstClaimTime() external view returns (uint40); + /// @notice Returns a flag indicating whether a claim has been made for a given index. /// @dev Uses a bitmap to save gas. /// @param index The index of the recipient to check. function hasClaimed(uint256 index) external returns (bool); - /// @notice Returns a flag indicating whether the Merkle streamer has expired. + /// @notice Returns a flag indicating whether the campaign has expired. function hasExpired() external view returns (bool); - /// @notice The address of the {SablierV2Lockup} contract. - function LOCKUP() external returns (ISablierV2Lockup); + /// @notice The content identifier for indexing the campaign on IPFS. + function ipfsCID() external view returns (string memory); - /// @notice The root of the Merkle tree used to validate the claims. + /// @notice The root of the Merkle tree used to validate the proofs of inclusion. /// @dev This is an immutable state variable. function MERKLE_ROOT() external returns (bytes32); + /// @notice Retrieves the name of the campaign. + function name() external returns (string memory); + /// @notice A flag indicating whether the stream NFTs are transferable. /// @dev This is an immutable state variable. function TRANSFERABLE() external returns (bool); @@ -62,16 +65,15 @@ interface ISablierV2MerkleStreamer is IAdminable { NON-CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @notice Claws back the unclaimed tokens from the Merkle streamer. + /// @notice Claws back the unclaimed tokens from the campaign. /// /// @dev Emits a {Clawback} event. /// - /// Notes: - /// - If the protocol is not zero, the expiration check is not made. - /// /// Requirements: /// - The caller must be the admin. - /// - The campaign must either be expired or not have an expiration. + /// - No claim must be made, OR + /// The current timestamp must not exceed 7 days after the first claim, OR + /// The campaign must be expired. /// /// @param to The address to receive the tokens. /// @param amount The amount of tokens to claw back. diff --git a/src/interfaces/ISablierV2MerkleLockupFactory.sol b/src/interfaces/ISablierV2MerkleLockupFactory.sol new file mode 100644 index 00000000..8574492f --- /dev/null +++ b/src/interfaces/ISablierV2MerkleLockupFactory.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; +import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; +import { LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; + +import { ISablierV2MerkleLL } from "./ISablierV2MerkleLL.sol"; +import { ISablierV2MerkleLT } from "./ISablierV2MerkleLT.sol"; +import { MerkleLockup, MerkleLT } from "../types/DataTypes.sol"; + +/// @title ISablierV2MerkleLockupFactory +/// @notice Deploys MerkleLockup campaigns with CREATE2. +interface ISablierV2MerkleLockupFactory { + /*////////////////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when a {SablierV2MerkleLL} campaign is created. + event CreateMerkleLL( + ISablierV2MerkleLL indexed merkleLL, + MerkleLockup.ConstructorParams baseParams, + ISablierV2LockupLinear lockupLinear, + LockupLinear.Durations streamDurations, + uint256 aggregateAmount, + uint256 recipientCount + ); + + /// @notice Emitted when a {SablierV2MerkleLT} campaign is created. + event CreateMerkleLT( + ISablierV2MerkleLT indexed merkleLT, + MerkleLockup.ConstructorParams baseParams, + ISablierV2LockupTranched lockupTranched, + MerkleLT.TrancheWithPercentage[] tranchesWithPercentages, + uint256 totalDuration, + uint256 aggregateAmount, + uint256 recipientCount + ); + + /*////////////////////////////////////////////////////////////////////////// + CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Verifies if the sum of percentages in `tranches` equals 100% , i.e. 1e18. + /// @dev Reverts if the sum of percentages overflows. + /// @param tranches The tranches with their respective unlock percentages. + /// @return result True if the sum of percentages equals 100%, otherwise false. + function isPercentagesSum100(MerkleLT.TrancheWithPercentage[] calldata tranches) + external + pure + returns (bool result); + + /*////////////////////////////////////////////////////////////////////////// + NON-CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Creates a new MerkleLockup campaign with a LockupLinear distribution. + /// @dev Emits a {CreateMerkleLL} event. + /// @param baseParams Struct encapsulating the {SablierV2MerkleLockup} parameters, which are documented in + /// {DataTypes}. + /// @param lockupLinear The address of the {SablierV2LockupLinear} contract. + /// @param streamDurations The durations for each stream. + /// @param aggregateAmount The total amount of ERC-20 assets to be distributed to all recipients. + /// @param recipientCount The total number of recipients who are eligible to claim. + /// @return merkleLL The address of the newly created MerkleLockup contract. + function createMerkleLL( + MerkleLockup.ConstructorParams memory baseParams, + ISablierV2LockupLinear lockupLinear, + LockupLinear.Durations memory streamDurations, + uint256 aggregateAmount, + uint256 recipientCount + ) + external + returns (ISablierV2MerkleLL merkleLL); + + /// @notice Creates a new MerkleLockup campaign with a LockupTranched distribution. + /// @dev Emits a {CreateMerkleLT} event. + /// + /// @param baseParams Struct encapsulating the {SablierV2MerkleLockup} parameters, which are documented in + /// {DataTypes}. + /// @param lockupTranched The address of the {SablierV2LockupTranched} contract. + /// @param tranchesWithPercentages The tranches with their respective unlock percentages. + /// @param aggregateAmount The total amount of ERC-20 assets to be distributed to all recipients. + /// @param recipientCount The total number of recipients who are eligible to claim. + /// @return merkleLT The address of the newly created MerkleLockup contract. + function createMerkleLT( + MerkleLockup.ConstructorParams memory baseParams, + ISablierV2LockupTranched lockupTranched, + MerkleLT.TrancheWithPercentage[] memory tranchesWithPercentages, + uint256 aggregateAmount, + uint256 recipientCount + ) + external + returns (ISablierV2MerkleLT merkleLT); +} diff --git a/src/interfaces/ISablierV2MerkleStreamerFactory.sol b/src/interfaces/ISablierV2MerkleStreamerFactory.sol deleted file mode 100644 index 958ca0e8..00000000 --- a/src/interfaces/ISablierV2MerkleStreamerFactory.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; -import { LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; - -import { ISablierV2MerkleStreamerLL } from "./ISablierV2MerkleStreamerLL.sol"; - -/// @title ISablierV2MerkleStreamerFactory -/// @notice Deploys new Lockup Linear Merkle streamers via CREATE2. -interface ISablierV2MerkleStreamerFactory { - /*////////////////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Emitted when a Sablier V2 Lockup Linear Merkle streamer is created. - event CreateMerkleStreamerLL( - ISablierV2MerkleStreamerLL merkleStreamer, - address indexed admin, - ISablierV2LockupLinear indexed lockupLinear, - IERC20 indexed asset, - bytes32 merkleRoot, - uint40 expiration, - LockupLinear.Durations streamDurations, - bool cancelable, - bool transferable, - string ipfsCID, - uint256 aggregateAmount, - uint256 recipientsCount - ); - - /*////////////////////////////////////////////////////////////////////////// - NON-CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Creates a new Merkle streamer that uses Lockup Linear. - /// @dev Emits a {CreateMerkleStreamerLL} event. - /// @param initialAdmin The initial admin of the Merkle streamer contract. - /// @param lockupLinear The address of the {SablierV2LockupLinear} contract. - /// @param asset The address of the streamed ERC-20 asset. - /// @param merkleRoot The Merkle root of the claim data. - /// @param expiration The expiration of the streaming campaign, as a Unix timestamp. - /// @param streamDurations The durations for each stream due to the recipient. - /// @param cancelable Indicates if each stream will be cancelable. - /// @param transferable Indicates if each stream NFT will be transferable. - /// @param ipfsCID Metadata parameter emitted for indexing purposes. - /// @param aggregateAmount Total amount of ERC-20 assets to be streamed to all recipients. - /// @param recipientsCount Total number of recipients eligible to claim. - /// @return merkleStreamerLL The address of the newly created Merkle streamer contract. - function createMerkleStreamerLL( - address initialAdmin, - ISablierV2LockupLinear lockupLinear, - IERC20 asset, - bytes32 merkleRoot, - uint40 expiration, - LockupLinear.Durations memory streamDurations, - bool cancelable, - bool transferable, - string memory ipfsCID, - uint256 aggregateAmount, - uint256 recipientsCount - ) - external - returns (ISablierV2MerkleStreamerLL merkleStreamerLL); -} diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index 93308ed9..ecd1b2e1 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -1,31 +1,39 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; /// @title Errors /// @notice Library containing all custom errors the protocol may revert with. library Errors { /*////////////////////////////////////////////////////////////////////////// - SABLIER-V2-BATCH + SABLIER-V2-BATCH-LOCKUP //////////////////////////////////////////////////////////////////////////*/ - error SablierV2Batch_BatchSizeZero(); + error SablierV2BatchLockup_BatchSizeZero(); /*////////////////////////////////////////////////////////////////////////// - SABLIER-V2-MERKLE-STREAMER + SABLIER-V2-MERKLE-LOCKUP //////////////////////////////////////////////////////////////////////////*/ /// @notice Thrown when trying to claim after the campaign has expired. - error SablierV2MerkleStreamer_CampaignExpired(uint256 currentTime, uint40 expiration); + error SablierV2MerkleLockup_CampaignExpired(uint256 blockTimestamp, uint40 expiration); - /// @notice Thrown when trying to clawback when the campaign has not expired. - error SablierV2MerkleStreamer_CampaignNotExpired(uint256 currentTime, uint40 expiration); + /// @notice Thrown when trying to create a campaign with a name that is too long. + error SablierV2MerkleLockup_CampaignNameTooLong(uint256 nameLength, uint256 maxLength); - /// @notice Thrown when trying to claim with an invalid Merkle proof. - error SablierV2MerkleStreamer_InvalidProof(); + /// @notice Thrown when trying to clawback when the current timestamp is over the grace period and the campaign has + /// not expired. + error SablierV2MerkleLockup_ClawbackNotAllowed(uint256 blockTimestamp, uint40 expiration, uint40 firstClaimTime); - /// @notice Thrown when trying to claim when the protocol fee is not zero. - error SablierV2MerkleStreamer_ProtocolFeeNotZero(); + /// @notice Thrown when trying to claim with an invalid Merkle proof. + error SablierV2MerkleLockup_InvalidProof(); /// @notice Thrown when trying to claim the same stream more than once. - error SablierV2MerkleStreamer_StreamClaimed(uint256 index); + error SablierV2MerkleLockup_StreamClaimed(uint256 index); + + /*////////////////////////////////////////////////////////////////////////// + SABLIER-V2-MERKLE-LT + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Thrown when trying to claim from an LT campaign with tranches' unlock percentages not adding up to 100%. + error SablierV2MerkleLT_TotalPercentageNotOneHundred(uint64 totalPercentage); } diff --git a/src/types/DataTypes.sol b/src/types/DataTypes.sol index 315886a4..11b9eeb0 100644 --- a/src/types/DataTypes.sol +++ b/src/types/DataTypes.sol @@ -1,30 +1,33 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { UD2x18 } from "@prb/math/src/UD2x18.sol"; import { ISablierV2Lockup } from "@sablier/v2-core/src/interfaces/ISablierV2Lockup.sol"; -import { Broker, LockupDynamic, LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; +import { Broker, LockupDynamic, LockupLinear, LockupTranched } from "@sablier/v2-core/src/types/DataTypes.sol"; -library Batch { +library BatchLockup { /// @notice A struct encapsulating the lockup contract's address and the stream ids to cancel. struct CancelMultiple { ISablierV2Lockup lockup; uint256[] streamIds; } - /// @notice A struct encapsulating all parameters of {SablierV2LockupDynamic.createWithDelta} except for the asset. - struct CreateWithDeltas { + /// @notice A struct encapsulating all parameters of {SablierV2LockupDynamic.createWithDurations} except for the + /// asset. + struct CreateWithDurationsLD { address sender; address recipient; uint128 totalAmount; bool cancelable; bool transferable; - LockupDynamic.SegmentWithDelta[] segments; + LockupDynamic.SegmentWithDuration[] segments; Broker broker; } /// @notice A struct encapsulating all parameters of {SablierV2LockupLinear.createWithDurations} except for the /// asset. - struct CreateWithDurations { + struct CreateWithDurationsLL { address sender; address recipient; uint128 totalAmount; @@ -34,27 +37,89 @@ library Batch { Broker broker; } - /// @notice A struct encapsulating all parameters of {SablierV2LockupDynamic.createWithMilestones} except for the + /// @notice A struct encapsulating all parameters of {SablierV2LockupTranched.createWithDurations} except for the /// asset. - struct CreateWithMilestones { + struct CreateWithDurationsLT { + address sender; + address recipient; + uint128 totalAmount; + bool cancelable; + bool transferable; + LockupTranched.TrancheWithDuration[] tranches; + Broker broker; + } + + /// @notice A struct encapsulating all parameters of {SablierV2LockupDynamic.createWithTimestamps} except for the + /// asset. + struct CreateWithTimestampsLD { address sender; address recipient; uint128 totalAmount; - uint40 startTime; bool cancelable; bool transferable; + uint40 startTime; LockupDynamic.Segment[] segments; Broker broker; } - /// @notice A struct encapsulating all parameters of {SablierV2LockupLinear.createWithRange} except for the asset. - struct CreateWithRange { + /// @notice A struct encapsulating all parameters of {SablierV2LockupLinear.createWithTimestamps} except for the + /// asset. + struct CreateWithTimestampsLL { address sender; address recipient; uint128 totalAmount; bool cancelable; bool transferable; - LockupLinear.Range range; + LockupLinear.Timestamps timestamps; Broker broker; } + + /// @notice A struct encapsulating all parameters of {SablierV2LockupTranched.createWithTimestamps} except for the + /// asset. + struct CreateWithTimestampsLT { + address sender; + address recipient; + uint128 totalAmount; + bool cancelable; + bool transferable; + uint40 startTime; + LockupTranched.Tranche[] tranches; + Broker broker; + } +} + +library MerkleLockup { + /// @notice Struct encapsulating the base constructor parameters of a MerkleLockup campaign. + /// @param asset The contract address of the ERC-20 asset to be distributed. + /// @param cancelable Indicates if the stream will be cancelable after claiming. + /// @param expiration The expiration of the campaign, as a Unix timestamp. + /// @param initialAdmin The initial admin of the MerkleLockup campaign. + /// @param ipfsCID The content identifier for indexing the contract on IPFS. + /// @param merkleRoot The Merkle root of the claim data. + /// @param name The name of the campaign. + /// @param transferable Indicates if the stream will be transferable after claiming. + struct ConstructorParams { + IERC20 asset; + bool cancelable; + uint40 expiration; + address initialAdmin; + string ipfsCID; + bytes32 merkleRoot; + string name; + bool transferable; + } +} + +library MerkleLT { + /// @notice Struct encapsulating the unlock percentage and duration of a tranche. + /// @dev Since users may have different amounts allocated, this struct makes it possible to calculate the amounts + /// at claim time. An 18-decimal format is used to represent percentages: 100% = 1e18. For more information, see + /// the PRBMath documentation on UD2x18: https://github.com/PaulRBerg/prb-math + /// @param unlockPercentage The percentage designated to be unlocked in this tranche. + /// @param duration The time difference in seconds between this tranche and the previous one. + struct TrancheWithPercentage { + // slot 0 + UD2x18 unlockPercentage; + uint40 duration; + } } diff --git a/test/Base.t.sol b/test/Base.t.sol index 16d0ed2d..41d556a8 100644 --- a/test/Base.t.sol +++ b/test/Base.t.sol @@ -1,25 +1,29 @@ // SPDX-License-Identifier: UNLICENSED // solhint-disable max-states-count -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -import { ISablierV2Comptroller } from "@sablier/v2-core/src/interfaces/ISablierV2Comptroller.sol"; import { ISablierV2LockupDynamic } from "@sablier/v2-core/src/interfaces/ISablierV2LockupDynamic.sol"; import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; -import { LockupDynamic, LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; +import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; +import { LockupDynamic, LockupLinear, LockupTranched } from "@sablier/v2-core/src/types/DataTypes.sol"; import { Assertions as V2CoreAssertions } from "@sablier/v2-core/test/utils/Assertions.sol"; +import { Constants as V2CoreConstants } from "@sablier/v2-core/test/utils/Constants.sol"; import { Utils as V2CoreUtils } from "@sablier/v2-core/test/utils/Utils.sol"; -import { ISablierV2Batch } from "src/interfaces/ISablierV2Batch.sol"; -import { ISablierV2MerkleStreamerFactory } from "src/interfaces/ISablierV2MerkleStreamerFactory.sol"; -import { ISablierV2MerkleStreamerLL } from "src/interfaces/ISablierV2MerkleStreamerLL.sol"; -import { SablierV2Batch } from "src/SablierV2Batch.sol"; -import { SablierV2MerkleStreamerFactory } from "src/SablierV2MerkleStreamerFactory.sol"; -import { SablierV2MerkleStreamerLL } from "src/SablierV2MerkleStreamerLL.sol"; - +import { ISablierV2BatchLockup } from "src/interfaces/ISablierV2BatchLockup.sol"; +import { ISablierV2MerkleLL } from "src/interfaces/ISablierV2MerkleLL.sol"; +import { ISablierV2MerkleLockupFactory } from "src/interfaces/ISablierV2MerkleLockupFactory.sol"; +import { ISablierV2MerkleLT } from "src/interfaces/ISablierV2MerkleLT.sol"; +import { SablierV2BatchLockup } from "src/SablierV2BatchLockup.sol"; +import { SablierV2MerkleLL } from "src/SablierV2MerkleLL.sol"; +import { SablierV2MerkleLockupFactory } from "src/SablierV2MerkleLockupFactory.sol"; +import { SablierV2MerkleLT } from "src/SablierV2MerkleLT.sol"; + +import { ERC20Mock } from "./mocks/erc20/ERC20Mock.sol"; +import { Assertions } from "./utils/Assertions.sol"; import { Defaults } from "./utils/Defaults.sol"; import { DeployOptimized } from "./utils/DeployOptimized.sol"; import { Events } from "./utils/Events.sol"; @@ -27,7 +31,15 @@ import { Merkle } from "./utils/Murky.sol"; import { Users } from "./utils/Types.sol"; /// @notice Base test contract with common logic needed by all tests. -abstract contract Base_Test is DeployOptimized, Events, Merkle, V2CoreAssertions, V2CoreUtils { +abstract contract Base_Test is + Assertions, + DeployOptimized, + Events, + Merkle, + V2CoreConstants, + V2CoreAssertions, + V2CoreUtils +{ /*////////////////////////////////////////////////////////////////////////// VARIABLES //////////////////////////////////////////////////////////////////////////*/ @@ -38,14 +50,15 @@ abstract contract Base_Test is DeployOptimized, Events, Merkle, V2CoreAssertions TEST CONTRACTS //////////////////////////////////////////////////////////////////////////*/ - IERC20 internal asset; - ISablierV2Batch internal batch; - ISablierV2Comptroller internal comptroller; + ISablierV2BatchLockup internal batchLockup; + IERC20 internal dai; Defaults internal defaults; ISablierV2LockupDynamic internal lockupDynamic; ISablierV2LockupLinear internal lockupLinear; - ISablierV2MerkleStreamerFactory internal merkleStreamerFactory; - ISablierV2MerkleStreamerLL internal merkleStreamerLL; + ISablierV2LockupTranched internal lockupTranched; + ISablierV2MerkleLockupFactory internal merkleLockupFactory; + ISablierV2MerkleLL internal merkleLL; + ISablierV2MerkleLT internal merkleLT; /*////////////////////////////////////////////////////////////////////////// SET-UP FUNCTION @@ -53,7 +66,7 @@ abstract contract Base_Test is DeployOptimized, Events, Merkle, V2CoreAssertions function setUp() public virtual { // Deploy the default test asset. - asset = new ERC20("DAI Stablecoin", "DAI"); + dai = new ERC20Mock("DAI Stablecoin", "DAI"); // Create users for testing. users.alice = createUser("Alice"); @@ -71,81 +84,50 @@ abstract contract Base_Test is DeployOptimized, Events, Merkle, V2CoreAssertions HELPERS //////////////////////////////////////////////////////////////////////////*/ - /// @dev Approves relevant contracts to spend assets from some users. - function approveContracts() internal { - // Approve Batch to spend assets from Alice. - changePrank({ msgSender: users.alice }); - asset.approve({ spender: address(batch), amount: MAX_UINT256 }); + /// @dev Approve `spender` to spend assets from `from`. + function approveContract(IERC20 asset_, address from, address spender) internal { + resetPrank({ msgSender: from }); + (bool success,) = address(asset_).call(abi.encodeCall(IERC20.approve, (spender, MAX_UINT256))); + success; } /// @dev Generates a user, labels its address, and funds it with ETH. function createUser(string memory name) internal returns (address payable) { address user = makeAddr(name); vm.deal({ account: user, newBalance: 100_000 ether }); - deal({ token: address(asset), to: user, give: 1_000_000e18 }); + deal({ token: address(dai), to: user, give: 1_000_000e18 }); return payable(user); } /// @dev Conditionally deploy V2 Periphery normally or from an optimized source compiled with `--via-ir`. function deployPeripheryConditionally() internal { if (!isTestOptimizedProfile()) { - batch = new SablierV2Batch(); - merkleStreamerFactory = new SablierV2MerkleStreamerFactory(); + batchLockup = new SablierV2BatchLockup(); + merkleLockupFactory = new SablierV2MerkleLockupFactory(); } else { - (batch, merkleStreamerFactory) = deployOptimizedPeriphery(); + (batchLockup, merkleLockupFactory) = deployOptimizedPeriphery(); } } /// @dev Labels the most relevant contracts. - function labelContracts() internal { - vm.label({ account: address(asset), newLabel: IERC20Metadata(address(asset)).symbol() }); - vm.label({ account: address(merkleStreamerFactory), newLabel: "MerkleStreamerFactory" }); - vm.label({ account: address(merkleStreamerLL), newLabel: "MerkleStreamerLL" }); + function labelContracts(IERC20 asset_) internal { + vm.label({ account: address(asset_), newLabel: IERC20Metadata(address(asset_)).symbol() }); vm.label({ account: address(defaults), newLabel: "Defaults" }); - vm.label({ account: address(comptroller), newLabel: "Comptroller" }); vm.label({ account: address(lockupDynamic), newLabel: "LockupDynamic" }); vm.label({ account: address(lockupLinear), newLabel: "LockupLinear" }); + vm.label({ account: address(lockupTranched), newLabel: "LockupTranched" }); + vm.label({ account: address(merkleLL), newLabel: "MerkleLL" }); + vm.label({ account: address(merkleLockupFactory), newLabel: "MerkleLockupFactory" }); + vm.label({ account: address(merkleLT), newLabel: "MerkleLT" }); } /*////////////////////////////////////////////////////////////////////////// CALL EXPECTS //////////////////////////////////////////////////////////////////////////*/ - /// @dev Expects a call to {ISablierV2LockupDynamic.createWithDeltas}. - function expectCallToCreateWithDeltas(LockupDynamic.CreateWithDeltas memory params) internal { - vm.expectCall({ - callee: address(lockupDynamic), - data: abi.encodeCall(ISablierV2LockupDynamic.createWithDeltas, (params)) - }); - } - - /// @dev Expects a call to {ISablierV2LockupLinear.createWithDurations}. - function expectCallToCreateWithDurations(LockupLinear.CreateWithDurations memory params) internal { - vm.expectCall({ - callee: address(lockupLinear), - data: abi.encodeCall(ISablierV2LockupLinear.createWithDurations, (params)) - }); - } - - /// @dev Expects a call to {ISablierV2LockupDynamic.createWithMilestones}. - function expectCallToCreateWithMilestones(LockupDynamic.CreateWithMilestones memory params) internal { - vm.expectCall({ - callee: address(lockupDynamic), - data: abi.encodeCall(ISablierV2LockupDynamic.createWithMilestones, (params)) - }); - } - - /// @dev Expects a call to {ISablierV2LockupLinear.createWithRange}. - function expectCallToCreateWithRange(LockupLinear.CreateWithRange memory params) internal { - vm.expectCall({ - callee: address(lockupLinear), - data: abi.encodeCall(ISablierV2LockupLinear.createWithRange, (params)) - }); - } - /// @dev Expects a call to {IERC20.transfer}. function expectCallToTransfer(address to, uint256 amount) internal { - expectCallToTransfer(address(asset), to, amount); + expectCallToTransfer(address(dai), to, amount); } /// @dev Expects a call to {IERC20.transfer}. @@ -155,7 +137,7 @@ abstract contract Base_Test is DeployOptimized, Events, Merkle, V2CoreAssertions /// @dev Expects a call to {IERC20.transferFrom}. function expectCallToTransferFrom(address from, address to, uint256 amount) internal { - expectCallToTransferFrom(address(asset), from, to, amount); + expectCallToTransferFrom(address(dai), from, to, amount); } /// @dev Expects a call to {IERC20.transferFrom}. @@ -163,24 +145,24 @@ abstract contract Base_Test is DeployOptimized, Events, Merkle, V2CoreAssertions vm.expectCall({ callee: asset_, data: abi.encodeCall(IERC20.transferFrom, (from, to, amount)) }); } - /// @dev Expects multiple calls to {ISablierV2LockupDynamic.createWithDeltas}, each with the specified + /// @dev Expects multiple calls to {ISablierV2LockupDynamic.createWithDurations}, each with the specified /// `params`. - function expectMultipleCallsToCreateWithDeltas( + function expectMultipleCallsToCreateWithDurationsLD( uint64 count, - LockupDynamic.CreateWithDeltas memory params + LockupDynamic.CreateWithDurations memory params ) internal { vm.expectCall({ callee: address(lockupDynamic), count: count, - data: abi.encodeCall(ISablierV2LockupDynamic.createWithDeltas, (params)) + data: abi.encodeCall(ISablierV2LockupDynamic.createWithDurations, (params)) }); } - /// @dev Expects multiple calls to {ISablierV2LockupDynamic.createWithDurations}, each with the specified + /// @dev Expects multiple calls to {ISablierV2LockupLinear.createWithDurations}, each with the specified /// `params`. - function expectMultipleCallsToCreateWithDurations( + function expectMultipleCallsToCreateWithDurationsLL( uint64 count, LockupLinear.CreateWithDurations memory params ) @@ -193,39 +175,74 @@ abstract contract Base_Test is DeployOptimized, Events, Merkle, V2CoreAssertions }); } - /// @dev Expects multiple calls to {ISablierV2LockupDynamic.createWithMilestones}, each with the specified + /// @dev Expects multiple calls to {ISablierV2LockupTranched.createWithDurations}, each with the specified /// `params`. - function expectMultipleCallsToCreateWithMilestones( + function expectMultipleCallsToCreateWithDurationsLT( uint64 count, - LockupDynamic.CreateWithMilestones memory params + LockupTranched.CreateWithDurations memory params + ) + internal + { + vm.expectCall({ + callee: address(lockupTranched), + count: count, + data: abi.encodeCall(ISablierV2LockupTranched.createWithDurations, (params)) + }); + } + + /// @dev Expects multiple calls to {ISablierV2LockupDynamic.createWithTimestamps}, each with the specified + /// `params`. + function expectMultipleCallsToCreateWithTimestampsLD( + uint64 count, + LockupDynamic.CreateWithTimestamps memory params ) internal { vm.expectCall({ callee: address(lockupDynamic), count: count, - data: abi.encodeCall(ISablierV2LockupDynamic.createWithMilestones, (params)) + data: abi.encodeCall(ISablierV2LockupDynamic.createWithTimestamps, (params)) }); } - /// @dev Expects multiple calls to {ISablierV2LockupDynamic.createWithRange}, each with the specified + /// @dev Expects multiple calls to {ISablierV2LockupLinear.createWithTimestamps}, each with the specified /// `params`. - function expectMultipleCallsToCreateWithRange(uint64 count, LockupLinear.CreateWithRange memory params) internal { + function expectMultipleCallsToCreateWithTimestampsLL( + uint64 count, + LockupLinear.CreateWithTimestamps memory params + ) + internal + { vm.expectCall({ callee: address(lockupLinear), count: count, - data: abi.encodeCall(ISablierV2LockupLinear.createWithRange, (params)) + data: abi.encodeCall(ISablierV2LockupLinear.createWithTimestamps, (params)) + }); + } + + /// @dev Expects multiple calls to {ISablierV2LockupTranched.createWithTimestamps}, each with the specified + /// `params`. + function expectMultipleCallsToCreateWithTimestampsLT( + uint64 count, + LockupTranched.CreateWithTimestamps memory params + ) + internal + { + vm.expectCall({ + callee: address(lockupTranched), + count: count, + data: abi.encodeCall(ISablierV2LockupTranched.createWithTimestamps, (params)) }); } /// @dev Expects multiple calls to {IERC20.transfer}. function expectMultipleCallsToTransfer(uint64 count, address to, uint256 amount) internal { - vm.expectCall({ callee: address(asset), count: count, data: abi.encodeCall(IERC20.transfer, (to, amount)) }); + vm.expectCall({ callee: address(dai), count: count, data: abi.encodeCall(IERC20.transfer, (to, amount)) }); } /// @dev Expects multiple calls to {IERC20.transferFrom}. function expectMultipleCallsToTransferFrom(uint64 count, address from, address to, uint256 amount) internal { - expectMultipleCallsToTransferFrom(address(asset), count, from, to, amount); + expectMultipleCallsToTransferFrom(address(dai), count, from, to, amount); } /// @dev Expects multiple calls to {IERC20.transferFrom}. @@ -242,62 +259,139 @@ abstract contract Base_Test is DeployOptimized, Events, Merkle, V2CoreAssertions } /*////////////////////////////////////////////////////////////////////////// - MERKLE-STREAMER + MERKLE-LOCKUP //////////////////////////////////////////////////////////////////////////*/ - function computeMerkleStreamerLLAddress( + function computeMerkleLLAddress( address admin, bytes32 merkleRoot, uint40 expiration ) internal + view + returns (address) + { + return computeMerkleLLAddress(admin, dai, merkleRoot, expiration); + } + + function computeMerkleLLAddress( + address admin, + IERC20 asset_, + bytes32 merkleRoot, + uint40 expiration + ) + internal + view returns (address) { bytes32 salt = keccak256( abi.encodePacked( + users.alice, + address(asset_), + defaults.CANCELABLE(), + expiration, admin, - lockupLinear, - asset, + abi.encode(defaults.IPFS_CID()), merkleRoot, - expiration, - abi.encode(defaults.durations()), + defaults.NAME_BYTES32(), + defaults.TRANSFERABLE(), + lockupLinear, + abi.encode(defaults.durations()) + ) + ); + bytes32 creationBytecodeHash = keccak256(getMerkleLLBytecode(admin, asset_, merkleRoot, expiration)); + return vm.computeCreate2Address({ + salt: salt, + initCodeHash: creationBytecodeHash, + deployer: address(merkleLockupFactory) + }); + } + + function computeMerkleLTAddress( + address admin, + bytes32 merkleRoot, + uint40 expiration + ) + internal + view + returns (address) + { + return computeMerkleLTAddress(admin, dai, merkleRoot, expiration); + } + + function computeMerkleLTAddress( + address admin, + IERC20 asset_, + bytes32 merkleRoot, + uint40 expiration + ) + internal + view + returns (address) + { + bytes32 salt = keccak256( + abi.encodePacked( + users.alice, + address(asset_), defaults.CANCELABLE(), - defaults.TRANSFERABLE() + expiration, + admin, + abi.encode(defaults.IPFS_CID()), + merkleRoot, + defaults.NAME_BYTES32(), + defaults.TRANSFERABLE(), + lockupTranched, + abi.encode(defaults.tranchesWithPercentages()) ) ); - bytes32 creationBytecodeHash = keccak256(getMerkleStreamerLLBytecode(admin, merkleRoot, expiration)); - return computeCreate2Address({ + bytes32 creationBytecodeHash = keccak256(getMerkleLTBytecode(admin, asset_, merkleRoot, expiration)); + return vm.computeCreate2Address({ salt: salt, - initcodeHash: creationBytecodeHash, - deployer: address(merkleStreamerFactory) + initCodeHash: creationBytecodeHash, + deployer: address(merkleLockupFactory) }); } - function getMerkleStreamerLLBytecode( + function getMerkleLLBytecode( + address admin, + IERC20 asset_, + bytes32 merkleRoot, + uint40 expiration + ) + internal + view + returns (bytes memory) + { + bytes memory constructorArgs = + abi.encode(defaults.baseParams(admin, asset_, expiration, merkleRoot), lockupLinear, defaults.durations()); + if (!isTestOptimizedProfile()) { + return bytes.concat(type(SablierV2MerkleLL).creationCode, constructorArgs); + } else { + return + bytes.concat(vm.getCode("out-optimized/SablierV2MerkleLL.sol/SablierV2MerkleLL.json"), constructorArgs); + } + } + + function getMerkleLTBytecode( address admin, + IERC20 asset_, bytes32 merkleRoot, uint40 expiration ) internal + view returns (bytes memory) { bytes memory constructorArgs = abi.encode( - admin, - lockupLinear, - asset, - merkleRoot, - expiration, - defaults.durations(), - defaults.CANCELABLE(), - defaults.TRANSFERABLE() + defaults.baseParams(admin, asset_, expiration, merkleRoot), + lockupTranched, + defaults.tranchesWithPercentages() ); if (!isTestOptimizedProfile()) { - return bytes.concat(type(SablierV2MerkleStreamerLL).creationCode, constructorArgs); + return bytes.concat(type(SablierV2MerkleLT).creationCode, constructorArgs); } else { - return bytes.concat( - vm.getCode("out-optimized/SablierV2MerkleStreamerLL.sol/SablierV2MerkleStreamerLL.json"), - constructorArgs - ); + return + bytes.concat(vm.getCode("out-optimized/SablierV2MerkleLT.sol/SablierV2MerkleLT.json"), constructorArgs); } } } diff --git a/test/fork/Fork.t.sol b/test/fork/Fork.t.sol index fb2b3b59..8a165c7b 100644 --- a/test/fork/Fork.t.sol +++ b/test/fork/Fork.t.sol @@ -1,9 +1,11 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { Precompiles as V2CorePrecompiles } from "@sablier/v2-core/precompiles/Precompiles.sol"; import { ISablierV2LockupDynamic } from "@sablier/v2-core/src/interfaces/ISablierV2LockupDynamic.sol"; import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; +import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; import { Fuzzers as V2CoreFuzzers } from "@sablier/v2-core/test/utils/Fuzzers.sol"; @@ -12,12 +14,18 @@ import { Base_Test } from "../Base.t.sol"; /// @notice Common logic needed by all fork tests. abstract contract Fork_Test is Base_Test, V2CoreFuzzers { + /*////////////////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////////////////*/ + + IERC20 internal immutable FORK_ASSET; + /*////////////////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////////////////*/ - constructor(IERC20 asset_) { - asset = asset_; + constructor(IERC20 forkAsset) { + FORK_ASSET = forkAsset; } /*////////////////////////////////////////////////////////////////////////// @@ -32,20 +40,22 @@ abstract contract Fork_Test is Base_Test, V2CoreFuzzers { Base_Test.setUp(); // Load the external dependencies. - loadDependencies(); + // loadDependencies(); + // TODO: Remove this line once the V2 Core contracts are deployed on Mainnet. + deployDependencies(); // Deploy the defaults contract and allow it to access cheatcodes. - defaults = new Defaults(users, asset); + defaults = new Defaults({ users_: users, asset_: FORK_ASSET }); vm.allowCheatcodes(address(defaults)); // Deploy V2 Periphery. deployPeripheryConditionally(); // Label the contracts. - labelContracts(); + labelContracts(FORK_ASSET); - // Approve the relevant contracts. - approveContracts(); + // Approve the BatchLockup contract. + approveContract({ asset_: FORK_ASSET, from: users.alice, spender: address(batchLockup) }); } /*////////////////////////////////////////////////////////////////////////// @@ -61,15 +71,23 @@ abstract contract Fork_Test is Base_Test, V2CoreFuzzers { vm.assume(user != recipient); vm.assume(user != address(lockupDynamic) && recipient != address(lockupDynamic)); vm.assume(user != address(lockupLinear) && recipient != address(lockupLinear)); + vm.assume(user != address(lockupTranched) && recipient != address(lockupTranched)); // Avoid users blacklisted by USDC or USDT. - assumeNoBlacklisted(address(asset), user); - assumeNoBlacklisted(address(asset), recipient); + assumeNoBlacklisted(address(FORK_ASSET), user); + assumeNoBlacklisted(address(FORK_ASSET), recipient); } /// @dev Loads all dependencies pre-deployed on Mainnet. function loadDependencies() private { lockupDynamic = ISablierV2LockupDynamic(0x7CC7e125d83A581ff438608490Cc0f7bDff79127); lockupLinear = ISablierV2LockupLinear(0xAFb979d9afAd1aD27C5eFf4E27226E3AB9e5dCC9); + lockupTranched = ISablierV2LockupTranched(0xAFb979d9afAd1aD27C5eFf4E27226E3AB9e5dCC9); + } + + /// @dev Deploys the V2 Core dependencies. + // TODO: Remove this function once the V2 Core contracts are deployed on Mainnet. + function deployDependencies() private { + (lockupDynamic, lockupLinear, lockupTranched,) = new V2CorePrecompiles().deployCore(users.admin); } } diff --git a/test/fork/assets/USDC.t.sol b/test/fork/assets/USDC.t.sol index 6e038fe4..ca1467e0 100644 --- a/test/fork/assets/USDC.t.sol +++ b/test/fork/assets/USDC.t.sol @@ -1,16 +1,29 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { CreateWithMilestones_Batch_Fork_Test } from "../batch/createWithMilestones.t.sol"; -import { CreateWithRange_Batch_Fork_Test } from "../batch/createWithRange.t.sol"; -import { MerkleStreamerLL_Fork_Test } from "../merkle-streamer/MerkleStreamerLL.t.sol"; +import { CreateWithTimestamps_LockupDynamic_BatchLockup_Fork_Test } from "../batch-lockup/createWithTimestampsLD.t.sol"; +import { CreateWithTimestamps_LockupLinear_BatchLockup_Fork_Test } from "../batch-lockup/createWithTimestampsLL.t.sol"; +import { CreateWithTimestamps_LockupTranched_BatchLockup_Fork_Test } from "../batch-lockup/createWithTimestampsLT.t.sol"; +import { MerkleLL_Fork_Test } from "../merkle-lockup/MerkleLL.t.sol"; +import { MerkleLT_Fork_Test } from "../merkle-lockup/MerkleLT.t.sol"; +/// @dev An ERC-20 asset with 6 decimals. IERC20 constant usdc = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); -contract USDC_CreateWithMilestones_Batch_Fork_Test is CreateWithMilestones_Batch_Fork_Test(usdc) { } +contract USDC_CreateWithTimestamps_LockupDynamic_BatchLockup_Fork_Test is + CreateWithTimestamps_LockupDynamic_BatchLockup_Fork_Test(usdc) +{ } -contract USDC_CreateWithRange_Batch_Fork_Test is CreateWithRange_Batch_Fork_Test(usdc) { } +contract USDC_CreateWithTimestamps_LockupLinear_BatchLockup_Fork_Test is + CreateWithTimestamps_LockupLinear_BatchLockup_Fork_Test(usdc) +{ } -contract USDC_MerkleStreamerLL_Fork_Test is MerkleStreamerLL_Fork_Test(usdc) { } +contract USDC_CreateWithTimestamps_LockupTranched_BatchLockup_Fork_Test is + CreateWithTimestamps_LockupTranched_BatchLockup_Fork_Test(usdc) +{ } + +contract USDC_MerkleLL_Fork_Test is MerkleLL_Fork_Test(usdc) { } + +contract USDC_MerkleLT_Fork_Test is MerkleLT_Fork_Test(usdc) { } diff --git a/test/fork/assets/USDT.t.sol b/test/fork/assets/USDT.t.sol index 423168d0..bdc5ed85 100644 --- a/test/fork/assets/USDT.t.sol +++ b/test/fork/assets/USDT.t.sol @@ -1,16 +1,29 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { CreateWithMilestones_Batch_Fork_Test } from "../batch/createWithMilestones.t.sol"; -import { CreateWithRange_Batch_Fork_Test } from "../batch/createWithRange.t.sol"; -import { MerkleStreamerLL_Fork_Test } from "../merkle-streamer/MerkleStreamerLL.t.sol"; +import { CreateWithTimestamps_LockupDynamic_BatchLockup_Fork_Test } from "../batch-lockup/createWithTimestampsLD.t.sol"; +import { CreateWithTimestamps_LockupLinear_BatchLockup_Fork_Test } from "../batch-lockup/createWithTimestampsLL.t.sol"; +import { CreateWithTimestamps_LockupTranched_BatchLockup_Fork_Test } from "../batch-lockup/createWithTimestampsLT.t.sol"; +import { MerkleLL_Fork_Test } from "../merkle-lockup/MerkleLL.t.sol"; +import { MerkleLT_Fork_Test } from "../merkle-lockup/MerkleLT.t.sol"; +/// @dev An ERC-20 asset that suffers from the missing return value bug. IERC20 constant usdt = IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7); -contract USDT_CreateWithMilestones_Batch_Fork_Test is CreateWithMilestones_Batch_Fork_Test(usdt) { } +contract USDT_CreateWithTimestamps_LockupDynamic_BatchLockup_Fork_Test is + CreateWithTimestamps_LockupDynamic_BatchLockup_Fork_Test(usdt) +{ } -contract USDT_CreateWithRange_Batch_Fork_Test is CreateWithRange_Batch_Fork_Test(usdt) { } +contract USDT_CreateWithTimestamps_LockupLinear_BatchLockup_Fork_Test is + CreateWithTimestamps_LockupLinear_BatchLockup_Fork_Test(usdt) +{ } -contract USDT_MerkleStreamerLL_Fork_Test is MerkleStreamerLL_Fork_Test(usdt) { } +contract USDT_CreateWithTimestamps_LockupTranched_BatchLockup_Fork_Test is + CreateWithTimestamps_LockupTranched_BatchLockup_Fork_Test(usdt) +{ } + +contract USDT_MerkleLL_Fork_Test is MerkleLL_Fork_Test(usdt) { } + +contract USDT_MerkleLT_Fork_Test is MerkleLT_Fork_Test(usdt) { } diff --git a/test/fork/batch/createWithMilestones.t.sol b/test/fork/batch-lockup/createWithTimestampsLD.t.sol similarity index 55% rename from test/fork/batch/createWithMilestones.t.sol rename to test/fork/batch-lockup/createWithTimestampsLD.t.sol index d564ea8a..c36edc94 100644 --- a/test/fork/batch/createWithMilestones.t.sol +++ b/test/fork/batch-lockup/createWithTimestampsLD.t.sol @@ -1,28 +1,24 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { LockupDynamic } from "@sablier/v2-core/src/types/DataTypes.sol"; -import { Batch } from "src/types/DataTypes.sol"; +import { BatchLockup } from "src/types/DataTypes.sol"; -import { Fork_Test } from "../Fork.t.sol"; import { ArrayBuilder } from "../../utils/ArrayBuilder.sol"; -import { BatchBuilder } from "../../utils/BatchBuilder.sol"; +import { BatchLockupBuilder } from "../../utils/BatchLockupBuilder.sol"; +import { Fork_Test } from "../Fork.t.sol"; /// @dev Runs against multiple fork assets. -abstract contract CreateWithMilestones_Batch_Fork_Test is Fork_Test { +abstract contract CreateWithTimestamps_LockupDynamic_BatchLockup_Fork_Test is Fork_Test { constructor(IERC20 asset_) Fork_Test(asset_) { } function setUp() public virtual override { Fork_Test.setUp(); } - /*////////////////////////////////////////////////////////////////////////// - BATCH-CREATE-WITH-MILESTONES - //////////////////////////////////////////////////////////////////////////*/ - - struct CreateWithMilestonesParams { + struct CreateWithTimestampsParams { uint128 batchSize; address sender; address recipient; @@ -31,15 +27,14 @@ abstract contract CreateWithMilestones_Batch_Fork_Test is Fork_Test { LockupDynamic.Segment[] segments; } - function testForkFuzz_CreateWithMilestones(CreateWithMilestonesParams memory params) external { + function testForkFuzz_CreateWithTimestampsLD(CreateWithTimestampsParams memory params) external { vm.assume(params.segments.length != 0); params.batchSize = boundUint128(params.batchSize, 1, 20); params.startTime = boundUint40(params.startTime, getBlockTimestamp(), getBlockTimestamp() + 24 hours); - fuzzSegmentMilestones(params.segments, params.startTime); + fuzzSegmentTimestamps(params.segments, params.startTime); (params.perStreamAmount,) = fuzzDynamicStreamAmounts({ upperBound: MAX_UINT128 / params.batchSize, segments: params.segments, - protocolFee: defaults.PROTOCOL_FEE(), brokerFee: defaults.BROKER_FEE() }); @@ -48,39 +43,39 @@ abstract contract CreateWithMilestones_Batch_Fork_Test is Fork_Test { uint256 firstStreamId = lockupDynamic.nextStreamId(); uint128 totalTransferAmount = params.perStreamAmount * params.batchSize; - deal({ token: address(asset), to: params.sender, give: uint256(totalTransferAmount) }); - changePrank({ msgSender: params.sender }); - asset.approve({ spender: address(batch), amount: totalTransferAmount }); + deal({ token: address(FORK_ASSET), to: params.sender, give: uint256(totalTransferAmount) }); + approveContract({ asset_: FORK_ASSET, from: params.sender, spender: address(batchLockup) }); - LockupDynamic.CreateWithMilestones memory createWithMilestones = LockupDynamic.CreateWithMilestones({ - asset: asset, - broker: defaults.broker(), - cancelable: true, - recipient: params.recipient, - segments: params.segments, + LockupDynamic.CreateWithTimestamps memory createWithTimestamps = LockupDynamic.CreateWithTimestamps({ sender: params.sender, - startTime: params.startTime, + recipient: params.recipient, totalAmount: params.perStreamAmount, - transferable: true + asset: FORK_ASSET, + cancelable: true, + transferable: true, + startTime: params.startTime, + segments: params.segments, + broker: defaults.broker() }); - Batch.CreateWithMilestones[] memory batchParams = BatchBuilder.fillBatch(createWithMilestones, params.batchSize); + BatchLockup.CreateWithTimestampsLD[] memory batchParams = + BatchLockupBuilder.fillBatch(createWithTimestamps, params.batchSize); expectCallToTransferFrom({ - asset_: address(asset), + asset_: address(FORK_ASSET), from: params.sender, - to: address(batch), + to: address(batchLockup), amount: totalTransferAmount }); - expectMultipleCallsToCreateWithMilestones({ count: uint64(params.batchSize), params: createWithMilestones }); + expectMultipleCallsToCreateWithTimestampsLD({ count: uint64(params.batchSize), params: createWithTimestamps }); expectMultipleCallsToTransferFrom({ - asset_: address(asset), + asset_: address(FORK_ASSET), count: uint64(params.batchSize), - from: address(batch), + from: address(batchLockup), to: address(lockupDynamic), amount: params.perStreamAmount }); - uint256[] memory actualStreamIds = batch.createWithMilestones(lockupDynamic, asset, batchParams); + uint256[] memory actualStreamIds = batchLockup.createWithTimestampsLD(lockupDynamic, FORK_ASSET, batchParams); uint256[] memory expectedStreamIds = ArrayBuilder.fillStreamIds(firstStreamId, params.batchSize); assertEq(actualStreamIds, expectedStreamIds); } diff --git a/test/fork/batch-lockup/createWithTimestampsLL.t.sol b/test/fork/batch-lockup/createWithTimestampsLL.t.sol new file mode 100644 index 00000000..31896b2f --- /dev/null +++ b/test/fork/batch-lockup/createWithTimestampsLL.t.sol @@ -0,0 +1,81 @@ + // SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; + +import { BatchLockup } from "src/types/DataTypes.sol"; + +import { ArrayBuilder } from "../../utils/ArrayBuilder.sol"; +import { BatchLockupBuilder } from "../../utils/BatchLockupBuilder.sol"; +import { Fork_Test } from "../Fork.t.sol"; + +/// @dev Runs against multiple fork assets. +abstract contract CreateWithTimestamps_LockupLinear_BatchLockup_Fork_Test is Fork_Test { + constructor(IERC20 asset_) Fork_Test(asset_) { } + + function setUp() public virtual override { + Fork_Test.setUp(); + } + + struct CreateWithTimestampsParams { + uint128 batchSize; + LockupLinear.Timestamps timestamps; + address sender; + address recipient; + uint128 perStreamAmount; + } + + function testForkFuzz_CreateWithTimestampsLL(CreateWithTimestampsParams memory params) external { + params.batchSize = boundUint128(params.batchSize, 1, 20); + params.perStreamAmount = boundUint128(params.perStreamAmount, 1, MAX_UINT128 / params.batchSize); + params.timestamps.start = + boundUint40(params.timestamps.start, getBlockTimestamp(), getBlockTimestamp() + 24 hours); + params.timestamps.cliff = boundUint40( + params.timestamps.cliff, params.timestamps.start + 1 seconds, params.timestamps.start + 52 weeks + ); + params.timestamps.end = + boundUint40(params.timestamps.end, params.timestamps.cliff + 1 seconds, MAX_UNIX_TIMESTAMP); + + checkUsers(params.sender, params.recipient); + + uint256 firstStreamId = lockupLinear.nextStreamId(); + uint128 totalTransferAmount = params.perStreamAmount * params.batchSize; + + deal({ token: address(FORK_ASSET), to: params.sender, give: uint256(totalTransferAmount) }); + approveContract({ asset_: FORK_ASSET, from: params.sender, spender: address(batchLockup) }); + + LockupLinear.CreateWithTimestamps memory createParams = LockupLinear.CreateWithTimestamps({ + sender: params.sender, + recipient: params.recipient, + totalAmount: params.perStreamAmount, + asset: FORK_ASSET, + cancelable: true, + transferable: true, + timestamps: params.timestamps, + broker: defaults.broker() + }); + BatchLockup.CreateWithTimestampsLL[] memory batchParams = + BatchLockupBuilder.fillBatch(createParams, params.batchSize); + + // Asset flow: sender → batch → Sablier + expectCallToTransferFrom({ + asset_: address(FORK_ASSET), + from: params.sender, + to: address(batchLockup), + amount: totalTransferAmount + }); + expectMultipleCallsToCreateWithTimestampsLL({ count: uint64(params.batchSize), params: createParams }); + expectMultipleCallsToTransferFrom({ + asset_: address(FORK_ASSET), + count: uint64(params.batchSize), + from: address(batchLockup), + to: address(lockupLinear), + amount: params.perStreamAmount + }); + + uint256[] memory actualStreamIds = batchLockup.createWithTimestampsLL(lockupLinear, FORK_ASSET, batchParams); + uint256[] memory expectedStreamIds = ArrayBuilder.fillStreamIds(firstStreamId, params.batchSize); + assertEq(actualStreamIds, expectedStreamIds); + } +} diff --git a/test/fork/batch-lockup/createWithTimestampsLT.t.sol b/test/fork/batch-lockup/createWithTimestampsLT.t.sol new file mode 100644 index 00000000..30f758a8 --- /dev/null +++ b/test/fork/batch-lockup/createWithTimestampsLT.t.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { LockupTranched } from "@sablier/v2-core/src/types/DataTypes.sol"; + +import { BatchLockup } from "src/types/DataTypes.sol"; + +import { ArrayBuilder } from "../../utils/ArrayBuilder.sol"; +import { BatchLockupBuilder } from "../../utils/BatchLockupBuilder.sol"; +import { Fork_Test } from "../Fork.t.sol"; + +/// @dev Runs against multiple fork assets. +abstract contract CreateWithTimestamps_LockupTranched_BatchLockup_Fork_Test is Fork_Test { + constructor(IERC20 asset_) Fork_Test(asset_) { } + + function setUp() public virtual override { + Fork_Test.setUp(); + } + + struct CreateWithTimestampsParams { + uint128 batchSize; + address sender; + address recipient; + uint128 perStreamAmount; + uint40 startTime; + LockupTranched.Tranche[] tranches; + } + + function testForkFuzz_CreateWithTimestampsLT(CreateWithTimestampsParams memory params) external { + vm.assume(params.tranches.length != 0); + params.batchSize = boundUint128(params.batchSize, 1, 20); + params.startTime = boundUint40(params.startTime, getBlockTimestamp(), getBlockTimestamp() + 24 hours); + fuzzTrancheTimestamps(params.tranches, params.startTime); + (params.perStreamAmount,) = fuzzTranchedStreamAmounts({ + upperBound: MAX_UINT128 / params.batchSize, + tranches: params.tranches, + brokerFee: defaults.BROKER_FEE() + }); + + checkUsers(params.sender, params.recipient); + + uint256 firstStreamId = lockupTranched.nextStreamId(); + uint128 totalTransferAmount = params.perStreamAmount * params.batchSize; + + deal({ token: address(FORK_ASSET), to: params.sender, give: uint256(totalTransferAmount) }); + approveContract({ asset_: FORK_ASSET, from: params.sender, spender: address(batchLockup) }); + + LockupTranched.CreateWithTimestamps memory createWithTimestamps = LockupTranched.CreateWithTimestamps({ + sender: params.sender, + recipient: params.recipient, + totalAmount: params.perStreamAmount, + asset: FORK_ASSET, + cancelable: true, + transferable: true, + startTime: params.startTime, + tranches: params.tranches, + broker: defaults.broker() + }); + BatchLockup.CreateWithTimestampsLT[] memory batchParams = + BatchLockupBuilder.fillBatch(createWithTimestamps, params.batchSize); + + expectCallToTransferFrom({ + asset_: address(FORK_ASSET), + from: params.sender, + to: address(batchLockup), + amount: totalTransferAmount + }); + expectMultipleCallsToCreateWithTimestampsLT({ count: uint64(params.batchSize), params: createWithTimestamps }); + expectMultipleCallsToTransferFrom({ + asset_: address(FORK_ASSET), + count: uint64(params.batchSize), + from: address(batchLockup), + to: address(lockupTranched), + amount: params.perStreamAmount + }); + + uint256[] memory actualStreamIds = batchLockup.createWithTimestampsLT(lockupTranched, FORK_ASSET, batchParams); + uint256[] memory expectedStreamIds = ArrayBuilder.fillStreamIds(firstStreamId, params.batchSize); + assertEq(actualStreamIds, expectedStreamIds); + } +} diff --git a/test/fork/batch/createWithRange.t.sol b/test/fork/batch/createWithRange.t.sol deleted file mode 100644 index 13e825ff..00000000 --- a/test/fork/batch/createWithRange.t.sol +++ /dev/null @@ -1,81 +0,0 @@ - // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; - -import { Batch } from "src/types/DataTypes.sol"; - -import { Fork_Test } from "../Fork.t.sol"; -import { ArrayBuilder } from "../../utils/ArrayBuilder.sol"; -import { BatchBuilder } from "../../utils/BatchBuilder.sol"; - -/// @dev Runs against multiple fork assets. -abstract contract CreateWithRange_Batch_Fork_Test is Fork_Test { - constructor(IERC20 asset_) Fork_Test(asset_) { } - - function setUp() public virtual override { - Fork_Test.setUp(); - } - - /*////////////////////////////////////////////////////////////////////////// - BATCH-CREATE-WITH-RANGE - //////////////////////////////////////////////////////////////////////////*/ - - struct CreateWithRangeParams { - uint128 batchSize; - LockupLinear.Range range; - address sender; - address recipient; - uint128 perStreamAmount; - } - - function testForkFuzz_CreateWithRange(CreateWithRangeParams memory params) external { - params.batchSize = boundUint128(params.batchSize, 1, 20); - params.perStreamAmount = boundUint128(params.perStreamAmount, 1, MAX_UINT128 / params.batchSize); - params.range.start = boundUint40(params.range.start, getBlockTimestamp(), getBlockTimestamp() + 24 hours); - params.range.cliff = boundUint40(params.range.cliff, params.range.start, params.range.start + 52 weeks); - params.range.end = boundUint40(params.range.end, params.range.cliff + 1 seconds, MAX_UNIX_TIMESTAMP); - - checkUsers(params.sender, params.recipient); - - uint256 firstStreamId = lockupLinear.nextStreamId(); - uint128 totalTransferAmount = params.perStreamAmount * params.batchSize; - - deal({ token: address(asset), to: params.sender, give: uint256(totalTransferAmount) }); - changePrank({ msgSender: params.sender }); - asset.approve({ spender: address(batch), amount: totalTransferAmount }); - - LockupLinear.CreateWithRange memory createParams = LockupLinear.CreateWithRange({ - asset: asset, - broker: defaults.broker(), - cancelable: true, - recipient: params.recipient, - sender: params.sender, - range: params.range, - totalAmount: params.perStreamAmount, - transferable: true - }); - Batch.CreateWithRange[] memory batchParams = BatchBuilder.fillBatch(createParams, params.batchSize); - - // Asset flow: sender → batch → Sablier - expectCallToTransferFrom({ - asset_: address(asset), - from: params.sender, - to: address(batch), - amount: totalTransferAmount - }); - expectMultipleCallsToCreateWithRange({ count: uint64(params.batchSize), params: createParams }); - expectMultipleCallsToTransferFrom({ - asset_: address(asset), - count: uint64(params.batchSize), - from: address(batch), - to: address(lockupLinear), - amount: params.perStreamAmount - }); - - uint256[] memory actualStreamIds = batch.createWithRange(lockupLinear, asset, batchParams); - uint256[] memory expectedStreamIds = ArrayBuilder.fillStreamIds(firstStreamId, params.batchSize); - assertEq(actualStreamIds, expectedStreamIds); - } -} diff --git a/test/fork/merkle-streamer/MerkleStreamerLL.t.sol b/test/fork/merkle-lockup/MerkleLL.t.sol similarity index 56% rename from test/fork/merkle-streamer/MerkleStreamerLL.t.sol rename to test/fork/merkle-lockup/MerkleLL.t.sol index 9b176183..1cad3e13 100644 --- a/test/fork/merkle-streamer/MerkleStreamerLL.t.sol +++ b/test/fork/merkle-lockup/MerkleLL.t.sol @@ -1,16 +1,17 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +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, LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; -import { ISablierV2MerkleStreamerLL } from "src/interfaces/ISablierV2MerkleStreamerLL.sol"; +import { ISablierV2MerkleLL } from "src/interfaces/ISablierV2MerkleLL.sol"; +import { MerkleLockup } from "src/types/DataTypes.sol"; import { MerkleBuilder } from "../../utils/MerkleBuilder.sol"; import { Fork_Test } from "../Fork.t.sol"; -abstract contract MerkleStreamerLL_Fork_Test is Fork_Test { +abstract contract MerkleLL_Fork_Test is Fork_Test { using MerkleBuilder for uint256[]; constructor(IERC20 asset_) Fork_Test(asset_) { } @@ -34,109 +35,104 @@ abstract contract MerkleStreamerLL_Fork_Test is Fork_Test { } struct Vars { + LockupLinear.StreamLL actualStream; uint256 actualStreamId; - LockupLinear.Stream actualStream; - uint128[] amounts; uint256 aggregateAmount; + uint128[] amounts; + MerkleLockup.ConstructorParams baseParams; uint128 clawbackAmount; - address expectedStreamerLL; - LockupLinear.Stream expectedStream; + address expectedLL; + LockupLinear.StreamLL expectedStream; uint256 expectedStreamId; uint256[] indexes; uint256 leafPos; uint256 leafToClaim; - ISablierV2MerkleStreamerLL merkleStreamerLL; + ISablierV2MerkleLL merkleLL; bytes32 merkleRoot; address[] recipients; - uint256 recipientsCount; + uint256 recipientCount; } // We need the leaves as a storage variable so that we can use OpenZeppelin's {Arrays.findUpperBound}. uint256[] public leaves; - function testForkFuzz_MerkleStreamerLL(Params memory params) external { + function testForkFuzz_MerkleLL(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); + assumeNoBlacklisted({ token: address(FORK_ASSET), addr: params.admin }); params.posBeforeSort = _bound(params.posBeforeSort, 0, params.leafData.length - 1); - assumeNoBlacklisted({ token: address(asset), addr: params.admin }); + + // The expiration must be either zero or greater than the block timestamp. + if (params.expiration != 0) { + params.expiration = boundUint40(params.expiration, getBlockTimestamp() + 1 seconds, MAX_UNIX_TIMESTAMP); + } /*////////////////////////////////////////////////////////////////////////// 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); - for (uint256 i = 0; i < vars.recipientsCount; ++i) { + vars.recipientCount = params.leafData.length; + vars.amounts = new uint128[](vars.recipientCount); + vars.indexes = new uint256[](vars.recipientCount); + vars.recipients = new address[](vars.recipientCount); + for (uint256 i = 0; i < vars.recipientCount; ++i) { vars.indexes[i] = params.leafData[i].index; // Bound each leaf amount so that `aggregateAmount` does not overflow. - 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; + vars.amounts[i] = boundUint128(params.leafData[i].amount, 1, uint128(MAX_UINT128 / vars.recipientCount - 1)); + 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)); } - leaves = new uint256[](vars.recipientsCount); + leaves = new uint256[](vars.recipientCount); leaves = MerkleBuilder.computeLeaves(vars.indexes, vars.recipients, vars.amounts); // Sort the leaves in ascending order to match the production environment. MerkleBuilder.sortLeaves(leaves); vars.merkleRoot = getRoot(leaves.toBytes32()); - vars.expectedStreamerLL = computeMerkleStreamerLLAddress(params.admin, vars.merkleRoot, params.expiration); - vm.expectEmit({ emitter: address(merkleStreamerFactory) }); - emit CreateMerkleStreamerLL({ - merkleStreamer: ISablierV2MerkleStreamerLL(vars.expectedStreamerLL), + vars.expectedLL = computeMerkleLLAddress(params.admin, FORK_ASSET, vars.merkleRoot, params.expiration); + + vars.baseParams = defaults.baseParams({ admin: params.admin, - lockupLinear: lockupLinear, - asset: asset, + asset_: FORK_ASSET, merkleRoot: vars.merkleRoot, - expiration: params.expiration, + expiration: params.expiration + }); + + vm.expectEmit({ emitter: address(merkleLockupFactory) }); + emit CreateMerkleLL({ + merkleLL: ISablierV2MerkleLL(vars.expectedLL), + baseParams: vars.baseParams, + lockupLinear: lockupLinear, streamDurations: defaults.durations(), - cancelable: defaults.CANCELABLE(), - transferable: defaults.TRANSFERABLE(), - ipfsCID: defaults.IPFS_CID(), aggregateAmount: vars.aggregateAmount, - recipientsCount: vars.recipientsCount + recipientCount: vars.recipientCount }); - vars.merkleStreamerLL = merkleStreamerFactory.createMerkleStreamerLL({ - initialAdmin: params.admin, + vars.merkleLL = merkleLockupFactory.createMerkleLL({ + baseParams: vars.baseParams, lockupLinear: lockupLinear, - asset: asset, - merkleRoot: vars.merkleRoot, - expiration: params.expiration, streamDurations: defaults.durations(), - cancelable: defaults.CANCELABLE(), - transferable: defaults.TRANSFERABLE(), - ipfsCID: defaults.IPFS_CID(), aggregateAmount: vars.aggregateAmount, - recipientsCount: vars.recipientsCount + recipientCount: vars.recipientCount }); - // Fund the Merkle streamer. - deal({ token: address(asset), to: address(vars.merkleStreamerLL), give: vars.aggregateAmount }); + // Fund the MerkleLockup contract. + deal({ token: address(FORK_ASSET), to: address(vars.merkleLL), give: vars.aggregateAmount }); - assertGt(address(vars.merkleStreamerLL).code.length, 0, "MerkleStreamerLL contract not created"); - assertEq( - address(vars.merkleStreamerLL), - vars.expectedStreamerLL, - "MerkleStreamerLL contract does not match computed address" - ); + assertGt(address(vars.merkleLL).code.length, 0, "MerkleLL contract not created"); + assertEq(address(vars.merkleLL), vars.expectedLL, "MerkleLL contract does not match computed address"); /*////////////////////////////////////////////////////////////////////////// CLAIM //////////////////////////////////////////////////////////////////////////*/ - assertFalse(vars.merkleStreamerLL.hasClaimed(vars.indexes[params.posBeforeSort])); + assertFalse(vars.merkleLL.hasClaimed(vars.indexes[params.posBeforeSort])); vars.leafToClaim = MerkleBuilder.computeLeaf( vars.indexes[params.posBeforeSort], @@ -152,7 +148,7 @@ abstract contract MerkleStreamerLL_Fork_Test is Fork_Test { vars.amounts[params.posBeforeSort], vars.expectedStreamId ); - vars.actualStreamId = vars.merkleStreamerLL.claim({ + vars.actualStreamId = vars.merkleLL.claim({ index: vars.indexes[params.posBeforeSort], recipient: vars.recipients[params.posBeforeSort], amount: vars.amounts[params.posBeforeSort], @@ -160,21 +156,22 @@ abstract contract MerkleStreamerLL_Fork_Test is Fork_Test { }); vars.actualStream = lockupLinear.getStream(vars.actualStreamId); - vars.expectedStream = LockupLinear.Stream({ + vars.expectedStream = LockupLinear.StreamLL({ amounts: Lockup.Amounts({ deposited: vars.amounts[params.posBeforeSort], refunded: 0, withdrawn: 0 }), - asset: asset, - cliffTime: uint40(block.timestamp) + defaults.CLIFF_DURATION(), - endTime: uint40(block.timestamp) + defaults.TOTAL_DURATION(), + asset: FORK_ASSET, + cliffTime: getBlockTimestamp() + defaults.CLIFF_DURATION(), + endTime: getBlockTimestamp() + defaults.TOTAL_DURATION(), isCancelable: defaults.CANCELABLE(), isDepleted: false, isStream: true, isTransferable: defaults.TRANSFERABLE(), + recipient: vars.recipients[params.posBeforeSort], sender: params.admin, - startTime: uint40(block.timestamp), + startTime: getBlockTimestamp(), wasCanceled: false }); - assertTrue(vars.merkleStreamerLL.hasClaimed(vars.indexes[params.posBeforeSort])); + assertTrue(vars.merkleLL.hasClaimed(vars.indexes[params.posBeforeSort])); assertEq(vars.actualStreamId, vars.expectedStreamId); assertEq(vars.actualStream, vars.expectedStream); @@ -183,14 +180,14 @@ abstract contract MerkleStreamerLL_Fork_Test is Fork_Test { //////////////////////////////////////////////////////////////////////////*/ if (params.expiration > 0) { - vars.clawbackAmount = uint128(asset.balanceOf(address(vars.merkleStreamerLL))); - vm.warp({ timestamp: uint256(params.expiration) + 1 seconds }); + vars.clawbackAmount = uint128(FORK_ASSET.balanceOf(address(vars.merkleLL))); + vm.warp({ newTimestamp: uint256(params.expiration) + 1 seconds }); - changePrank({ msgSender: params.admin }); - expectCallToTransfer({ to: params.admin, amount: vars.clawbackAmount }); - vm.expectEmit({ emitter: address(vars.merkleStreamerLL) }); + resetPrank({ msgSender: params.admin }); + expectCallToTransfer({ asset_: address(FORK_ASSET), to: params.admin, amount: vars.clawbackAmount }); + vm.expectEmit({ emitter: address(vars.merkleLL) }); emit Clawback({ to: params.admin, admin: params.admin, amount: vars.clawbackAmount }); - vars.merkleStreamerLL.clawback({ to: params.admin, amount: vars.clawbackAmount }); + vars.merkleLL.clawback({ to: params.admin, amount: vars.clawbackAmount }); } } } diff --git a/test/fork/merkle-lockup/MerkleLT.t.sol b/test/fork/merkle-lockup/MerkleLT.t.sol new file mode 100644 index 00000000..ad741646 --- /dev/null +++ b/test/fork/merkle-lockup/MerkleLT.t.sol @@ -0,0 +1,195 @@ +// 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, LockupTranched } from "@sablier/v2-core/src/types/DataTypes.sol"; + +import { ISablierV2MerkleLT } from "src/interfaces/ISablierV2MerkleLT.sol"; +import { MerkleLockup } from "src/types/DataTypes.sol"; + +import { MerkleBuilder } from "../../utils/MerkleBuilder.sol"; +import { Fork_Test } from "../Fork.t.sol"; + +abstract contract MerkleLT_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; + uint128 amount; + } + + struct Params { + address admin; + uint40 expiration; + LeafData[] leafData; + uint256 posBeforeSort; + } + + struct Vars { + LockupTranched.StreamLT actualStream; + uint256 actualStreamId; + LockupTranched.Tranche[] actualTranches; + uint256 aggregateAmount; + uint128[] amounts; + MerkleLockup.ConstructorParams baseParams; + uint128 clawbackAmount; + address expectedLT; + LockupTranched.StreamLT expectedStream; + uint256 expectedStreamId; + uint256[] indexes; + uint256 leafPos; + uint256 leafToClaim; + ISablierV2MerkleLT merkleLT; + bytes32 merkleRoot; + address[] recipients; + uint256 recipientCount; + } + + // We need the leaves as a storage variable so that we can use OpenZeppelin's {Arrays.findUpperBound}. + uint256[] public leaves; + + function testForkFuzz_MerkleLT(Params memory params) external { + vm.assume(params.admin != address(0) && params.admin != users.admin); + vm.assume(params.leafData.length > 1); + assumeNoBlacklisted({ token: address(FORK_ASSET), addr: params.admin }); + params.posBeforeSort = _bound(params.posBeforeSort, 0, params.leafData.length - 1); + + // The expiration must be either zero or greater than the block timestamp. + if (params.expiration != 0) { + params.expiration = boundUint40(params.expiration, getBlockTimestamp() + 1 seconds, MAX_UNIX_TIMESTAMP); + } + + /*////////////////////////////////////////////////////////////////////////// + CREATE + //////////////////////////////////////////////////////////////////////////*/ + + Vars memory vars; + vars.recipientCount = params.leafData.length; + vars.amounts = new uint128[](vars.recipientCount); + vars.indexes = new uint256[](vars.recipientCount); + vars.recipients = new address[](vars.recipientCount); + for (uint256 i = 0; i < vars.recipientCount; ++i) { + vars.indexes[i] = params.leafData[i].index; + + // Bound each leaf amount so that `aggregateAmount` does not overflow. + vars.amounts[i] = boundUint128(params.leafData[i].amount, 1, uint128(MAX_UINT128 / vars.recipientCount - 1)); + 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)); + } + + leaves = new uint256[](vars.recipientCount); + leaves = MerkleBuilder.computeLeaves(vars.indexes, vars.recipients, vars.amounts); + + // Sort the leaves in ascending order to match the production environment. + MerkleBuilder.sortLeaves(leaves); + vars.merkleRoot = getRoot(leaves.toBytes32()); + + vars.expectedLT = computeMerkleLTAddress(params.admin, FORK_ASSET, vars.merkleRoot, params.expiration); + + vars.baseParams = defaults.baseParams({ + admin: params.admin, + asset_: FORK_ASSET, + merkleRoot: vars.merkleRoot, + expiration: params.expiration + }); + + vm.expectEmit({ emitter: address(merkleLockupFactory) }); + emit CreateMerkleLT({ + merkleLT: ISablierV2MerkleLT(vars.expectedLT), + baseParams: vars.baseParams, + lockupTranched: lockupTranched, + tranchesWithPercentages: defaults.tranchesWithPercentages(), + totalDuration: defaults.TOTAL_DURATION(), + aggregateAmount: vars.aggregateAmount, + recipientCount: vars.recipientCount + }); + + vars.merkleLT = merkleLockupFactory.createMerkleLT({ + baseParams: vars.baseParams, + lockupTranched: lockupTranched, + tranchesWithPercentages: defaults.tranchesWithPercentages(), + aggregateAmount: vars.aggregateAmount, + recipientCount: vars.recipientCount + }); + + // Fund the MerkleLockup contract. + deal({ token: address(FORK_ASSET), to: address(vars.merkleLT), give: vars.aggregateAmount }); + + assertGt(address(vars.merkleLT).code.length, 0, "MerkleLT contract not created"); + assertEq(address(vars.merkleLT), vars.expectedLT, "MerkleLT contract does not match computed address"); + + /*////////////////////////////////////////////////////////////////////////// + CLAIM + //////////////////////////////////////////////////////////////////////////*/ + + assertFalse(vars.merkleLT.hasClaimed(vars.indexes[params.posBeforeSort])); + + vars.leafToClaim = MerkleBuilder.computeLeaf( + vars.indexes[params.posBeforeSort], + vars.recipients[params.posBeforeSort], + vars.amounts[params.posBeforeSort] + ); + vars.leafPos = Arrays.findUpperBound(leaves, vars.leafToClaim); + + vars.expectedStreamId = lockupTranched.nextStreamId(); + emit Claim( + vars.indexes[params.posBeforeSort], + vars.recipients[params.posBeforeSort], + vars.amounts[params.posBeforeSort], + vars.expectedStreamId + ); + vars.actualStreamId = vars.merkleLT.claim({ + index: vars.indexes[params.posBeforeSort], + recipient: vars.recipients[params.posBeforeSort], + amount: vars.amounts[params.posBeforeSort], + merkleProof: getProof(leaves.toBytes32(), vars.leafPos) + }); + + vars.actualStream = lockupTranched.getStream(vars.actualStreamId); + vars.expectedStream = LockupTranched.StreamLT({ + amounts: Lockup.Amounts({ deposited: vars.amounts[params.posBeforeSort], refunded: 0, withdrawn: 0 }), + asset: FORK_ASSET, + endTime: getBlockTimestamp() + defaults.TOTAL_DURATION(), + isCancelable: defaults.CANCELABLE(), + isDepleted: false, + isStream: true, + isTransferable: defaults.TRANSFERABLE(), + recipient: vars.recipients[params.posBeforeSort], + sender: params.admin, + startTime: getBlockTimestamp(), + tranches: defaults.tranches({ totalAmount: vars.amounts[params.posBeforeSort] }), + wasCanceled: false + }); + + assertTrue(vars.merkleLT.hasClaimed(vars.indexes[params.posBeforeSort])); + assertEq(vars.actualStreamId, vars.expectedStreamId); + assertEq(vars.actualStream, vars.expectedStream); + + /*////////////////////////////////////////////////////////////////////////// + CLAWBACK + //////////////////////////////////////////////////////////////////////////*/ + + if (params.expiration > 0) { + vars.clawbackAmount = uint128(FORK_ASSET.balanceOf(address(vars.merkleLT))); + vm.warp({ newTimestamp: uint256(params.expiration) + 1 seconds }); + + resetPrank({ msgSender: params.admin }); + expectCallToTransfer({ asset_: address(FORK_ASSET), to: params.admin, amount: vars.clawbackAmount }); + vm.expectEmit({ emitter: address(vars.merkleLT) }); + emit Clawback({ to: params.admin, admin: params.admin, amount: vars.clawbackAmount }); + vars.merkleLT.clawback({ to: params.admin, amount: vars.clawbackAmount }); + } + } +} diff --git a/test/integration/Integration.t.sol b/test/integration/Integration.t.sol index 7d3c0728..f889e27f 100644 --- a/test/integration/Integration.t.sol +++ b/test/integration/Integration.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; -import { Precompiles as V2CorePrecompiles } from "@sablier/v2-core/test/utils/Precompiles.sol"; +import { Precompiles as V2CorePrecompiles } from "@sablier/v2-core/precompiles/Precompiles.sol"; import { Defaults } from "../utils/Defaults.sol"; import { Base_Test } from "../Base.t.sol"; @@ -20,16 +20,16 @@ abstract contract Integration_Test is Base_Test { deployDependencies(); // Deploy the defaults contract. - defaults = new Defaults(users, asset); + defaults = new Defaults({ users_: users, asset_: dai }); // Deploy V2 Periphery. deployPeripheryConditionally(); // Label the contracts. - labelContracts(); + labelContracts(dai); - // Approve the relevant contracts. - approveContracts(); + // Approve the BatchLockup contract. + approveContract({ asset_: dai, from: users.alice, spender: address(batchLockup) }); } /*////////////////////////////////////////////////////////////////////////// @@ -37,6 +37,6 @@ abstract contract Integration_Test is Base_Test { //////////////////////////////////////////////////////////////////////////*/ function deployDependencies() private { - (comptroller, lockupDynamic, lockupLinear,) = new V2CorePrecompiles().deployCore(users.admin); + (lockupDynamic, lockupLinear, lockupTranched,) = new V2CorePrecompiles().deployCore(users.admin); } } diff --git a/test/integration/batch-lockup/create-with-durations-ld/createWithDurationsLD.t.sol b/test/integration/batch-lockup/create-with-durations-ld/createWithDurationsLD.t.sol new file mode 100644 index 00000000..fdcd5924 --- /dev/null +++ b/test/integration/batch-lockup/create-with-durations-ld/createWithDurationsLD.t.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { Errors } from "src/libraries/Errors.sol"; +import { BatchLockup } from "src/types/DataTypes.sol"; + +import { Integration_Test } from "../../Integration.t.sol"; + +contract CreateWithDurationsLD_Integration_Test is Integration_Test { + function setUp() public virtual override { + Integration_Test.setUp(); + } + + function test_RevertWhen_BatchSizeZero() external { + BatchLockup.CreateWithDurationsLD[] memory batchParams = new BatchLockup.CreateWithDurationsLD[](0); + vm.expectRevert(Errors.SablierV2BatchLockup_BatchSizeZero.selector); + batchLockup.createWithDurationsLD(lockupDynamic, dai, batchParams); + } + + modifier whenBatchSizeNotZero() { + _; + } + + function test_BatchCreateWithDurations() external whenBatchSizeNotZero { + // Asset flow: Alice → batchLockup → Sablier + // Expect transfers from Alice to the batchLockup, and then from the batchLockup to the Sablier contract. + expectCallToTransferFrom({ + from: users.alice, + to: address(batchLockup), + amount: defaults.TOTAL_TRANSFER_AMOUNT() + }); + expectMultipleCallsToCreateWithDurationsLD({ + count: defaults.BATCH_SIZE(), + params: defaults.createWithDurationsLD() + }); + expectMultipleCallsToTransferFrom({ + count: defaults.BATCH_SIZE(), + from: address(batchLockup), + to: address(lockupDynamic), + amount: defaults.PER_STREAM_AMOUNT() + }); + + // Assert that the batch of streams has been created successfully. + uint256[] memory actualStreamIds = + batchLockup.createWithDurationsLD(lockupDynamic, dai, defaults.batchCreateWithDurationsLD()); + uint256[] memory expectedStreamIds = defaults.incrementalStreamIds(); + assertEq(actualStreamIds, expectedStreamIds, "stream ids mismatch"); + } +} diff --git a/test/integration/batch/create-with-durations/createWithDurations.tree b/test/integration/batch-lockup/create-with-durations-ld/createWithDurationsLD.tree similarity index 89% rename from test/integration/batch/create-with-durations/createWithDurations.tree rename to test/integration/batch-lockup/create-with-durations-ld/createWithDurationsLD.tree index 377110d8..782763df 100644 --- a/test/integration/batch/create-with-durations/createWithDurations.tree +++ b/test/integration/batch-lockup/create-with-durations-ld/createWithDurationsLD.tree @@ -1,4 +1,4 @@ -createWithDurations.t.sol +createWithDurationsLD.t.sol ├── when the batch size is zero │ └── it should revert └── when the batch size is not zero diff --git a/test/integration/batch-lockup/create-with-durations-ll/createWithDurationsLL.t.sol b/test/integration/batch-lockup/create-with-durations-ll/createWithDurationsLL.t.sol new file mode 100644 index 00000000..ae18ca04 --- /dev/null +++ b/test/integration/batch-lockup/create-with-durations-ll/createWithDurationsLL.t.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { Errors } from "src/libraries/Errors.sol"; +import { BatchLockup } from "src/types/DataTypes.sol"; + +import { Integration_Test } from "../../Integration.t.sol"; + +contract CreateWithDurationsLL_Integration_Test is Integration_Test { + function setUp() public virtual override { + Integration_Test.setUp(); + } + + function test_RevertWhen_BatchSizeZero() external { + BatchLockup.CreateWithDurationsLL[] memory batchParams = new BatchLockup.CreateWithDurationsLL[](0); + vm.expectRevert(Errors.SablierV2BatchLockup_BatchSizeZero.selector); + batchLockup.createWithDurationsLL(lockupLinear, dai, batchParams); + } + + modifier whenBatchSizeNotZero() { + _; + } + + function test_BatchCreateWithDurations() external whenBatchSizeNotZero { + // Asset flow: Alice → batchLockup → Sablier + // Expect transfers from Alice to the batchLockup, and then from the batchLockup to the Sablier contract. + expectCallToTransferFrom({ + from: users.alice, + to: address(batchLockup), + amount: defaults.TOTAL_TRANSFER_AMOUNT() + }); + expectMultipleCallsToCreateWithDurationsLL({ + count: defaults.BATCH_SIZE(), + params: defaults.createWithDurationsLL() + }); + expectMultipleCallsToTransferFrom({ + count: defaults.BATCH_SIZE(), + from: address(batchLockup), + to: address(lockupLinear), + amount: defaults.PER_STREAM_AMOUNT() + }); + + // Assert that the batch of streams has been created successfully. + uint256[] memory actualStreamIds = + batchLockup.createWithDurationsLL(lockupLinear, dai, defaults.batchCreateWithDurationsLL()); + uint256[] memory expectedStreamIds = defaults.incrementalStreamIds(); + assertEq(actualStreamIds, expectedStreamIds, "stream ids mismatch"); + } +} diff --git a/test/integration/batch/create-with-deltas/createWithDeltas.tree b/test/integration/batch-lockup/create-with-durations-ll/createWithDurationsLL.tree similarity index 64% rename from test/integration/batch/create-with-deltas/createWithDeltas.tree rename to test/integration/batch-lockup/create-with-durations-ll/createWithDurationsLL.tree index 37e9b52f..c0b50c87 100644 --- a/test/integration/batch/create-with-deltas/createWithDeltas.tree +++ b/test/integration/batch-lockup/create-with-durations-ll/createWithDurationsLL.tree @@ -1,6 +1,6 @@ -createWithDeltas.t.sol +createWithDurationsLL.t.sol ├── when the batch size is zero │ └── it should revert └── when the batch size is not zero - ├── it should create a batch of streams with deltas + ├── it should create a batch of streams with durations └── it should perform the ERC-20 transfers diff --git a/test/integration/batch-lockup/create-with-durations-lt/createWithDurationsLT.t.sol b/test/integration/batch-lockup/create-with-durations-lt/createWithDurationsLT.t.sol new file mode 100644 index 00000000..86fd7153 --- /dev/null +++ b/test/integration/batch-lockup/create-with-durations-lt/createWithDurationsLT.t.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { Errors } from "src/libraries/Errors.sol"; +import { BatchLockup } from "src/types/DataTypes.sol"; + +import { Integration_Test } from "../../Integration.t.sol"; + +contract CreateWithDurationsLT_Integration_Test is Integration_Test { + function setUp() public virtual override { + Integration_Test.setUp(); + } + + function test_RevertWhen_BatchSizeZero() external { + BatchLockup.CreateWithDurationsLT[] memory batchParams = new BatchLockup.CreateWithDurationsLT[](0); + vm.expectRevert(Errors.SablierV2BatchLockup_BatchSizeZero.selector); + batchLockup.createWithDurationsLT(lockupTranched, dai, batchParams); + } + + modifier whenBatchSizeNotZero() { + _; + } + + function test_BatchCreateWithDurations() external whenBatchSizeNotZero { + // Asset flow: Alice → batchLockup → Sablier + // Expect transfers from Alice to the batchLockup, and then from the batchLockup to the Sablier contract. + expectCallToTransferFrom({ + from: users.alice, + to: address(batchLockup), + amount: defaults.TOTAL_TRANSFER_AMOUNT() + }); + expectMultipleCallsToCreateWithDurationsLT({ + count: defaults.BATCH_SIZE(), + params: defaults.createWithDurationsLT() + }); + expectMultipleCallsToTransferFrom({ + count: defaults.BATCH_SIZE(), + from: address(batchLockup), + to: address(lockupTranched), + amount: defaults.PER_STREAM_AMOUNT() + }); + + // Assert that the batch of streams has been created successfully. + uint256[] memory actualStreamIds = + batchLockup.createWithDurationsLT(lockupTranched, dai, defaults.batchCreateWithDurationsLT()); + uint256[] memory expectedStreamIds = defaults.incrementalStreamIds(); + assertEq(actualStreamIds, expectedStreamIds, "stream ids mismatch"); + } +} diff --git a/test/integration/batch/create-with-milestones/createWithMilestones.tree b/test/integration/batch-lockup/create-with-durations-lt/createWithDurationsLT.tree similarity index 64% rename from test/integration/batch/create-with-milestones/createWithMilestones.tree rename to test/integration/batch-lockup/create-with-durations-lt/createWithDurationsLT.tree index 41e9e3f9..8e67caf2 100644 --- a/test/integration/batch/create-with-milestones/createWithMilestones.tree +++ b/test/integration/batch-lockup/create-with-durations-lt/createWithDurationsLT.tree @@ -1,6 +1,6 @@ -createWithMilestones.t.sol +createWithDurationsLT.t.sol ├── when the batch size is zero │ └── it should revert └── when the batch size is not zero - ├── it should create a batch of streams with milestones + ├── it should create a batch of streams with durations └── it should perform the ERC-20 transfers diff --git a/test/integration/batch-lockup/create-with-timestamps-ld/createWithTimestampsLD.t.sol b/test/integration/batch-lockup/create-with-timestamps-ld/createWithTimestampsLD.t.sol new file mode 100644 index 00000000..fd15616d --- /dev/null +++ b/test/integration/batch-lockup/create-with-timestamps-ld/createWithTimestampsLD.t.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { Errors } from "src/libraries/Errors.sol"; +import { BatchLockup } from "src/types/DataTypes.sol"; + +import { Integration_Test } from "../../Integration.t.sol"; + +contract CreateWithTimestampsLD_Integration_Test is Integration_Test { + function setUp() public virtual override { + Integration_Test.setUp(); + } + + function test_RevertWhen_BatchSizeZero() external { + BatchLockup.CreateWithTimestampsLD[] memory batchParams = new BatchLockup.CreateWithTimestampsLD[](0); + vm.expectRevert(Errors.SablierV2BatchLockup_BatchSizeZero.selector); + batchLockup.createWithTimestampsLD(lockupDynamic, dai, batchParams); + } + + modifier whenBatchSizeNotZero() { + _; + } + + function test_BatchCreateWithTimestamps() external whenBatchSizeNotZero { + // Asset flow: Alice → batchLockup → Sablier + // Expect transfers from Alice to the batchLockup, and then from the batchLockup to the Sablier contract. + expectCallToTransferFrom({ + from: users.alice, + to: address(batchLockup), + amount: defaults.TOTAL_TRANSFER_AMOUNT() + }); + expectMultipleCallsToCreateWithTimestampsLD({ + count: defaults.BATCH_SIZE(), + params: defaults.createWithTimestampsLD() + }); + expectMultipleCallsToTransferFrom({ + count: defaults.BATCH_SIZE(), + from: address(batchLockup), + to: address(lockupDynamic), + amount: defaults.PER_STREAM_AMOUNT() + }); + + // Assert that the batch of streams has been created successfully. + uint256[] memory actualStreamIds = + batchLockup.createWithTimestampsLD(lockupDynamic, dai, defaults.batchCreateWithTimestampsLD()); + uint256[] memory expectedStreamIds = defaults.incrementalStreamIds(); + assertEq(actualStreamIds, expectedStreamIds, "stream ids mismatch"); + } +} diff --git a/test/integration/batch/create-with-range/createWithRange.tree b/test/integration/batch-lockup/create-with-timestamps-ld/createWithTimestampsLD.tree similarity index 63% rename from test/integration/batch/create-with-range/createWithRange.tree rename to test/integration/batch-lockup/create-with-timestamps-ld/createWithTimestampsLD.tree index 886e5ff0..78d71fbd 100644 --- a/test/integration/batch/create-with-range/createWithRange.tree +++ b/test/integration/batch-lockup/create-with-timestamps-ld/createWithTimestampsLD.tree @@ -1,6 +1,6 @@ -createWithRange.t.sol +createWithTimestampsLD.t.sol ├── when the batch size is zero │ └── it should revert └── when the batch size is not zero - ├── it should create a batch of streams with range + ├── it should create a batch of streams with timestamps └── it should perform the ERC-20 transfers diff --git a/test/integration/batch-lockup/create-with-timestamps-ll/createWithTimestamps.t.sol b/test/integration/batch-lockup/create-with-timestamps-ll/createWithTimestamps.t.sol new file mode 100644 index 00000000..39153116 --- /dev/null +++ b/test/integration/batch-lockup/create-with-timestamps-ll/createWithTimestamps.t.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { Errors } from "src/libraries/Errors.sol"; +import { BatchLockup } from "src/types/DataTypes.sol"; + +import { Integration_Test } from "../../Integration.t.sol"; + +contract CreateWithTimestampsLL_Integration_Test is Integration_Test { + function setUp() public virtual override { + Integration_Test.setUp(); + } + + function test_RevertWhen_BatchSizeZero() external { + BatchLockup.CreateWithTimestampsLL[] memory batchParams = new BatchLockup.CreateWithTimestampsLL[](0); + vm.expectRevert(Errors.SablierV2BatchLockup_BatchSizeZero.selector); + batchLockup.createWithTimestampsLL(lockupLinear, dai, batchParams); + } + + modifier whenBatchSizeNotZero() { + _; + } + + function test_BatchCreateWithTimestamps() external whenBatchSizeNotZero { + // Asset flow: Alice → batchLockup → Sablier + // Expect transfers from Alice to the batchLockup, and then from the batchLockup to the Sablier contract. + expectCallToTransferFrom({ + from: users.alice, + to: address(batchLockup), + amount: defaults.TOTAL_TRANSFER_AMOUNT() + }); + expectMultipleCallsToCreateWithTimestampsLL({ + count: defaults.BATCH_SIZE(), + params: defaults.createWithTimestampsLL() + }); + expectMultipleCallsToTransferFrom({ + count: defaults.BATCH_SIZE(), + from: address(batchLockup), + to: address(lockupLinear), + amount: defaults.PER_STREAM_AMOUNT() + }); + + // Assert that the batch of streams has been created successfully. + uint256[] memory actualStreamIds = + batchLockup.createWithTimestampsLL(lockupLinear, dai, defaults.batchCreateWithTimestampsLL()); + uint256[] memory expectedStreamIds = defaults.incrementalStreamIds(); + assertEq(actualStreamIds, expectedStreamIds, "stream ids mismatch"); + } +} diff --git a/test/integration/batch-lockup/create-with-timestamps-ll/createWithTimestamps.tree b/test/integration/batch-lockup/create-with-timestamps-ll/createWithTimestamps.tree new file mode 100644 index 00000000..e9409127 --- /dev/null +++ b/test/integration/batch-lockup/create-with-timestamps-ll/createWithTimestamps.tree @@ -0,0 +1,6 @@ +createWithTimestampsLL.t.sol +├── when the batch size is zero +│ └── it should revert +└── when the batch size is not zero + ├── it should create a batch of streams with timestamps + └── it should perform the ERC-20 transfers diff --git a/test/integration/batch-lockup/create-with-timestamps-lt/createWithTimestampsLT.t.sol b/test/integration/batch-lockup/create-with-timestamps-lt/createWithTimestampsLT.t.sol new file mode 100644 index 00000000..21aa2eed --- /dev/null +++ b/test/integration/batch-lockup/create-with-timestamps-lt/createWithTimestampsLT.t.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { Errors } from "src/libraries/Errors.sol"; +import { BatchLockup } from "src/types/DataTypes.sol"; + +import { Integration_Test } from "../../Integration.t.sol"; + +contract CreateWithTimestampsLT_Integration_Test is Integration_Test { + function setUp() public virtual override { + Integration_Test.setUp(); + } + + function test_RevertWhen_BatchSizeZero() external { + BatchLockup.CreateWithTimestampsLT[] memory batchParams = new BatchLockup.CreateWithTimestampsLT[](0); + vm.expectRevert(Errors.SablierV2BatchLockup_BatchSizeZero.selector); + batchLockup.createWithTimestampsLT(lockupTranched, dai, batchParams); + } + + modifier whenBatchSizeNotZero() { + _; + } + + function test_BatchCreateWithTimestamps() external whenBatchSizeNotZero { + // Asset flow: Alice → batchLockup → Sablier + // Expect transfers from Alice to the batchLockup, and then from the batchLockup to the Sablier contract. + expectCallToTransferFrom({ + from: users.alice, + to: address(batchLockup), + amount: defaults.TOTAL_TRANSFER_AMOUNT() + }); + expectMultipleCallsToCreateWithTimestampsLT({ + count: defaults.BATCH_SIZE(), + params: defaults.createWithTimestampsLT() + }); + expectMultipleCallsToTransferFrom({ + count: defaults.BATCH_SIZE(), + from: address(batchLockup), + to: address(lockupTranched), + amount: defaults.PER_STREAM_AMOUNT() + }); + + // Assert that the batch of streams has been created successfully. + uint256[] memory actualStreamIds = + batchLockup.createWithTimestampsLT(lockupTranched, dai, defaults.batchCreateWithTimestampsLT()); + uint256[] memory expectedStreamIds = defaults.incrementalStreamIds(); + assertEq(actualStreamIds, expectedStreamIds, "stream ids mismatch"); + } +} diff --git a/test/integration/batch-lockup/create-with-timestamps-lt/createWithTimestampsLT.tree b/test/integration/batch-lockup/create-with-timestamps-lt/createWithTimestampsLT.tree new file mode 100644 index 00000000..66ab5a5a --- /dev/null +++ b/test/integration/batch-lockup/create-with-timestamps-lt/createWithTimestampsLT.tree @@ -0,0 +1,6 @@ +createWithTimestampsLT.t.sol +├── when the batch size is zero +│ └── it should revert +└── when the batch size is not zero + ├── it should create a batch of streams with timestamps + └── it should perform the ERC-20 transfers diff --git a/test/integration/batch/create-with-deltas/createWithDeltas.t.sol b/test/integration/batch/create-with-deltas/createWithDeltas.t.sol deleted file mode 100644 index 11c51e4b..00000000 --- a/test/integration/batch/create-with-deltas/createWithDeltas.t.sol +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { Errors } from "src/libraries/Errors.sol"; -import { Batch } from "src/types/DataTypes.sol"; - -import { Integration_Test } from "../../Integration.t.sol"; - -contract CreateWithDeltas_Integration_Test is Integration_Test { - function setUp() public virtual override { - Integration_Test.setUp(); - } - - function test_RevertWhen_BatchSizeZero() external { - Batch.CreateWithDeltas[] memory batchParams = new Batch.CreateWithDeltas[](0); - vm.expectRevert(Errors.SablierV2Batch_BatchSizeZero.selector); - batch.createWithDeltas(lockupDynamic, asset, batchParams); - } - - modifier whenBatchSizeNotZero() { - _; - } - - function test_BatchCreateWithDeltas() external whenBatchSizeNotZero { - // Asset flow: Alice → batch → Sablier - // Expect transfers from Alice to the batch, and then from the batch to the Sablier contract. - expectCallToTransferFrom({ from: users.alice, to: address(batch), amount: defaults.TOTAL_TRANSFER_AMOUNT() }); - expectMultipleCallsToCreateWithDeltas({ count: defaults.BATCH_SIZE(), params: defaults.createWithDeltas() }); - expectMultipleCallsToTransferFrom({ - count: defaults.BATCH_SIZE(), - from: address(batch), - to: address(lockupDynamic), - amount: defaults.PER_STREAM_AMOUNT() - }); - - // Assert that the batch of streams has been created successfully. - uint256[] memory actualStreamIds = - batch.createWithDeltas(lockupDynamic, asset, defaults.batchCreateWithDeltas()); - uint256[] memory expectedStreamIds = defaults.incrementalStreamIds(); - assertEq(actualStreamIds, expectedStreamIds, "stream ids mismatch"); - } -} diff --git a/test/integration/batch/create-with-durations/createWithDurations.t.sol b/test/integration/batch/create-with-durations/createWithDurations.t.sol deleted file mode 100644 index 2aea5448..00000000 --- a/test/integration/batch/create-with-durations/createWithDurations.t.sol +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { Errors } from "src/libraries/Errors.sol"; -import { Batch } from "src/types/DataTypes.sol"; - -import { Integration_Test } from "../../Integration.t.sol"; - -contract CreateWithDurations_Integration_Test is Integration_Test { - function setUp() public virtual override { - Integration_Test.setUp(); - } - - function test_RevertWhen_BatchSizeZero() external { - Batch.CreateWithDurations[] memory batchParams = new Batch.CreateWithDurations[](0); - vm.expectRevert(Errors.SablierV2Batch_BatchSizeZero.selector); - batch.createWithDurations(lockupLinear, asset, batchParams); - } - - modifier whenBatchSizeNotZero() { - _; - } - - function test_BatchCreateWithDurations() external whenBatchSizeNotZero { - // Asset flow: Alice → batch → Sablier - // Expect transfers from Alice to the batch, and then from the batch to the Sablier contract. - expectCallToTransferFrom({ from: users.alice, to: address(batch), amount: defaults.TOTAL_TRANSFER_AMOUNT() }); - expectMultipleCallsToCreateWithDurations({ count: defaults.BATCH_SIZE(), params: defaults.createWithDurations() }); - expectMultipleCallsToTransferFrom({ - count: defaults.BATCH_SIZE(), - from: address(batch), - to: address(lockupLinear), - amount: defaults.PER_STREAM_AMOUNT() - }); - - // Assert that the batch of streams has been created successfully. - uint256[] memory actualStreamIds = - batch.createWithDurations(lockupLinear, asset, defaults.batchCreateWithDurations()); - uint256[] memory expectedStreamIds = defaults.incrementalStreamIds(); - assertEq(actualStreamIds, expectedStreamIds, "stream ids mismatch"); - } -} diff --git a/test/integration/batch/create-with-milestones/createWithMilestones.t.sol b/test/integration/batch/create-with-milestones/createWithMilestones.t.sol deleted file mode 100644 index ca5b7383..00000000 --- a/test/integration/batch/create-with-milestones/createWithMilestones.t.sol +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { Errors } from "src/libraries/Errors.sol"; -import { Batch } from "src/types/DataTypes.sol"; - -import { Integration_Test } from "../../Integration.t.sol"; - -contract CreateWithMilestones_Integration_Test is Integration_Test { - function setUp() public virtual override { - Integration_Test.setUp(); - } - - function test_RevertWhen_BatchSizeZero() external { - Batch.CreateWithMilestones[] memory batchParams = new Batch.CreateWithMilestones[](0); - vm.expectRevert(Errors.SablierV2Batch_BatchSizeZero.selector); - batch.createWithMilestones(lockupDynamic, asset, batchParams); - } - - modifier whenBatchSizeNotZero() { - _; - } - - function test_BatchCreateWithMilestones() external whenBatchSizeNotZero { - // Asset flow: Alice → batch → Sablier - // Expect transfers from Alice to the batch, and then from the batch to the Sablier contract. - expectCallToTransferFrom({ from: users.alice, to: address(batch), amount: defaults.TOTAL_TRANSFER_AMOUNT() }); - expectMultipleCallsToCreateWithMilestones({ - count: defaults.BATCH_SIZE(), - params: defaults.createWithMilestones() - }); - expectMultipleCallsToTransferFrom({ - count: defaults.BATCH_SIZE(), - from: address(batch), - to: address(lockupDynamic), - amount: defaults.PER_STREAM_AMOUNT() - }); - - // Assert that the batch of streams has been created successfully. - uint256[] memory actualStreamIds = - batch.createWithMilestones(lockupDynamic, asset, defaults.batchCreateWithMilestones()); - uint256[] memory expectedStreamIds = defaults.incrementalStreamIds(); - assertEq(actualStreamIds, expectedStreamIds, "stream ids mismatch"); - } -} diff --git a/test/integration/batch/create-with-range/createWithRange.t.sol b/test/integration/batch/create-with-range/createWithRange.t.sol deleted file mode 100644 index 4f248bba..00000000 --- a/test/integration/batch/create-with-range/createWithRange.t.sol +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { Errors } from "src/libraries/Errors.sol"; -import { Batch } from "src/types/DataTypes.sol"; - -import { Integration_Test } from "../../Integration.t.sol"; - -contract CreateWithRange_Integration_Test is Integration_Test { - function setUp() public virtual override { - Integration_Test.setUp(); - } - - function test_RevertWhen_BatchSizeZero() external { - Batch.CreateWithRange[] memory batchParams = new Batch.CreateWithRange[](0); - vm.expectRevert(Errors.SablierV2Batch_BatchSizeZero.selector); - batch.createWithRange(lockupLinear, asset, batchParams); - } - - modifier whenBatchSizeNotZero() { - _; - } - - function test_CreateWithRange() external whenBatchSizeNotZero { - // Asset flow: Alice → batch → Sablier - // Expect transfers from Alice to the batch, and then from the batch to the Sablier contract. - expectCallToTransferFrom({ from: users.alice, to: address(batch), amount: defaults.TOTAL_TRANSFER_AMOUNT() }); - expectMultipleCallsToCreateWithRange({ count: defaults.BATCH_SIZE(), params: defaults.createWithRange() }); - expectMultipleCallsToTransferFrom({ - count: defaults.BATCH_SIZE(), - from: address(batch), - to: address(lockupLinear), - amount: defaults.PER_STREAM_AMOUNT() - }); - - // Assert that the batch of streams has been created successfully. - uint256[] memory actualStreamIds = batch.createWithRange(lockupLinear, asset, defaults.batchCreateWithRange()); - uint256[] memory expectedStreamIds = defaults.incrementalStreamIds(); - assertEq(actualStreamIds, expectedStreamIds, "stream ids mismatch"); - } -} diff --git a/test/integration/merkle-lockup/MerkleLockup.t.sol b/test/integration/merkle-lockup/MerkleLockup.t.sol new file mode 100644 index 00000000..ba55de28 --- /dev/null +++ b/test/integration/merkle-lockup/MerkleLockup.t.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22; + +import { ISablierV2MerkleLL } from "src/interfaces/ISablierV2MerkleLL.sol"; +import { ISablierV2MerkleLT } from "src/interfaces/ISablierV2MerkleLT.sol"; + +import { Integration_Test } from "../Integration.t.sol"; + +abstract contract MerkleLockup_Integration_Test is Integration_Test { + function setUp() public virtual override { + Integration_Test.setUp(); + + // Create the default MerkleLockup contracts. + merkleLL = createMerkleLL(); + merkleLT = createMerkleLT(); + + // Fund the MerkleLockup contracts. + deal({ token: address(dai), to: address(merkleLL), give: defaults.AGGREGATE_AMOUNT() }); + deal({ token: address(dai), to: address(merkleLT), give: defaults.AGGREGATE_AMOUNT() }); + } + + /*////////////////////////////////////////////////////////////////////////// + MERKLE-LL + //////////////////////////////////////////////////////////////////////////*/ + + function claimLL() internal returns (uint256) { + return merkleLL.claim({ + index: defaults.INDEX1(), + recipient: users.recipient1, + amount: defaults.CLAIM_AMOUNT(), + merkleProof: defaults.index1Proof() + }); + } + + function computeMerkleLLAddress() internal view returns (address) { + return computeMerkleLLAddress(users.admin, defaults.MERKLE_ROOT(), defaults.EXPIRATION()); + } + + function computeMerkleLLAddress(address admin) internal view returns (address) { + return computeMerkleLLAddress(admin, defaults.MERKLE_ROOT(), defaults.EXPIRATION()); + } + + function computeMerkleLLAddress(address admin, uint40 expiration) internal view returns (address) { + return computeMerkleLLAddress(admin, defaults.MERKLE_ROOT(), expiration); + } + + function computeMerkleLLAddress(address admin, bytes32 merkleRoot) internal view returns (address) { + return computeMerkleLLAddress(admin, merkleRoot, defaults.EXPIRATION()); + } + + function createMerkleLL() internal returns (ISablierV2MerkleLL) { + return createMerkleLL(users.admin, defaults.EXPIRATION()); + } + + function createMerkleLL(address admin) internal returns (ISablierV2MerkleLL) { + return createMerkleLL(admin, defaults.EXPIRATION()); + } + + function createMerkleLL(uint40 expiration) internal returns (ISablierV2MerkleLL) { + return createMerkleLL(users.admin, expiration); + } + + function createMerkleLL(address admin, uint40 expiration) internal returns (ISablierV2MerkleLL) { + return merkleLockupFactory.createMerkleLL({ + baseParams: defaults.baseParams(admin, dai, expiration, defaults.MERKLE_ROOT()), + lockupLinear: lockupLinear, + streamDurations: defaults.durations(), + aggregateAmount: defaults.AGGREGATE_AMOUNT(), + recipientCount: defaults.RECIPIENT_COUNT() + }); + } + + /*////////////////////////////////////////////////////////////////////////// + MERKLE-LT + //////////////////////////////////////////////////////////////////////////*/ + + function claimLT() internal returns (uint256) { + return merkleLT.claim({ + index: defaults.INDEX1(), + recipient: users.recipient1, + amount: defaults.CLAIM_AMOUNT(), + merkleProof: defaults.index1Proof() + }); + } + + function computeMerkleLTAddress() internal view returns (address) { + return computeMerkleLTAddress(users.admin, defaults.MERKLE_ROOT(), defaults.EXPIRATION()); + } + + function computeMerkleLTAddress(address admin) internal view returns (address) { + return computeMerkleLTAddress(admin, defaults.MERKLE_ROOT(), defaults.EXPIRATION()); + } + + function computeMerkleLTAddress(address admin, uint40 expiration) internal view returns (address) { + return computeMerkleLTAddress(admin, defaults.MERKLE_ROOT(), expiration); + } + + function computeMerkleLTAddress(address admin, bytes32 merkleRoot) internal view returns (address) { + return computeMerkleLTAddress(admin, merkleRoot, defaults.EXPIRATION()); + } + + function createMerkleLT() internal returns (ISablierV2MerkleLT) { + return createMerkleLT(users.admin, defaults.EXPIRATION()); + } + + function createMerkleLT(address admin) internal returns (ISablierV2MerkleLT) { + return createMerkleLT(admin, defaults.EXPIRATION()); + } + + function createMerkleLT(uint40 expiration) internal returns (ISablierV2MerkleLT) { + return createMerkleLT(users.admin, expiration); + } + + function createMerkleLT(address admin, uint40 expiration) internal returns (ISablierV2MerkleLT) { + return merkleLockupFactory.createMerkleLT({ + baseParams: defaults.baseParams(admin, dai, expiration, defaults.MERKLE_ROOT()), + lockupTranched: lockupTranched, + tranchesWithPercentages: defaults.tranchesWithPercentages(), + aggregateAmount: defaults.AGGREGATE_AMOUNT(), + recipientCount: defaults.RECIPIENT_COUNT() + }); + } +} diff --git a/test/integration/merkle-lockup/factory/create-merkle-ll/createMerkleLL.t.sol b/test/integration/merkle-lockup/factory/create-merkle-ll/createMerkleLL.t.sol new file mode 100644 index 00000000..a5088c74 --- /dev/null +++ b/test/integration/merkle-lockup/factory/create-merkle-ll/createMerkleLL.t.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; + +import { Errors } from "src/libraries/Errors.sol"; +import { ISablierV2MerkleLL } from "src/interfaces/ISablierV2MerkleLL.sol"; +import { MerkleLockup } from "src/types/DataTypes.sol"; + +import { MerkleLockup_Integration_Test } from "../../MerkleLockup.t.sol"; + +contract CreateMerkleLL_Integration_Test is MerkleLockup_Integration_Test { + function setUp() public override { + MerkleLockup_Integration_Test.setUp(); + + // Make alice the caller of createMerkleLT. + resetPrank(users.alice); + } + + function test_RevertWhen_CampaignNameTooLong() external { + MerkleLockup.ConstructorParams memory baseParams = defaults.baseParams(); + LockupLinear.Durations memory streamDurations = defaults.durations(); + uint256 aggregateAmount = defaults.AGGREGATE_AMOUNT(); + uint256 recipientCount = defaults.RECIPIENT_COUNT(); + + baseParams.name = "this string is longer than 32 characters"; + + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierV2MerkleLockup_CampaignNameTooLong.selector, bytes(baseParams.name).length, 32 + ) + ); + + merkleLockupFactory.createMerkleLL({ + baseParams: baseParams, + lockupLinear: lockupLinear, + streamDurations: streamDurations, + aggregateAmount: aggregateAmount, + recipientCount: recipientCount + }); + } + + modifier whenCampaignNameNotTooLong() { + _; + } + + /// @dev This test works because a default MerkleLockup contract is deployed in {Integration_Test.setUp} + function test_RevertGiven_CreatedAlready() external whenCampaignNameNotTooLong { + MerkleLockup.ConstructorParams memory baseParams = defaults.baseParams(); + LockupLinear.Durations memory streamDurations = defaults.durations(); + uint256 aggregateAmount = defaults.AGGREGATE_AMOUNT(); + uint256 recipientCount = defaults.RECIPIENT_COUNT(); + + // Expect a revert due to CREATE2. + vm.expectRevert(); + merkleLockupFactory.createMerkleLL({ + baseParams: baseParams, + lockupLinear: lockupLinear, + streamDurations: streamDurations, + aggregateAmount: aggregateAmount, + recipientCount: recipientCount + }); + } + + modifier givenNotCreatedAlready() { + _; + } + + function testFuzz_CreateMerkleLL( + address admin, + uint40 expiration + ) + external + whenCampaignNameNotTooLong + givenNotCreatedAlready + { + vm.assume(admin != users.admin); + address expectedLL = computeMerkleLLAddress(admin, expiration); + + MerkleLockup.ConstructorParams memory baseParams = defaults.baseParams({ + admin: admin, + asset_: dai, + merkleRoot: defaults.MERKLE_ROOT(), + expiration: expiration + }); + + vm.expectEmit({ emitter: address(merkleLockupFactory) }); + emit CreateMerkleLL({ + merkleLL: ISablierV2MerkleLL(expectedLL), + baseParams: baseParams, + lockupLinear: lockupLinear, + streamDurations: defaults.durations(), + aggregateAmount: defaults.AGGREGATE_AMOUNT(), + recipientCount: defaults.RECIPIENT_COUNT() + }); + + address actualLL = address(createMerkleLL(admin, expiration)); + assertGt(actualLL.code.length, 0, "MerkleLL contract not created"); + assertEq(actualLL, expectedLL, "MerkleLL contract does not match computed address"); + } +} diff --git a/test/integration/merkle-lockup/factory/create-merkle-ll/createMerkleLL.tree b/test/integration/merkle-lockup/factory/create-merkle-ll/createMerkleLL.tree new file mode 100644 index 00000000..81de5f39 --- /dev/null +++ b/test/integration/merkle-lockup/factory/create-merkle-ll/createMerkleLL.tree @@ -0,0 +1,9 @@ +createMerkleLL.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 created already + │ └── it should revert + └── given the campaign has not been created already + ├── it should create the campaign + └── it should emit a {CreateMerkleLL} event diff --git a/test/integration/merkle-lockup/factory/create-merkle-lt/createMerkleLT.t.sol b/test/integration/merkle-lockup/factory/create-merkle-lt/createMerkleLT.t.sol new file mode 100644 index 00000000..e7225abd --- /dev/null +++ b/test/integration/merkle-lockup/factory/create-merkle-lt/createMerkleLT.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 { ISablierV2MerkleLT } from "src/interfaces/ISablierV2MerkleLT.sol"; +import { MerkleLockup, MerkleLT } from "src/types/DataTypes.sol"; + +import { MerkleLockup_Integration_Test } from "../../MerkleLockup.t.sol"; + +contract CreateMerkleLT_Integration_Test is MerkleLockup_Integration_Test { + function setUp() public override { + MerkleLockup_Integration_Test.setUp(); + + // Make alice the caller of createMerkleLT. + resetPrank(users.alice); + } + + function test_RevertWhen_CampaignNameTooLong() external { + MerkleLockup.ConstructorParams memory baseParams = defaults.baseParams(); + MerkleLT.TrancheWithPercentage[] memory tranchesWithPercentages = defaults.tranchesWithPercentages(); + uint256 aggregateAmount = defaults.AGGREGATE_AMOUNT(); + uint256 recipientCount = defaults.RECIPIENT_COUNT(); + + baseParams.name = "this string is longer than 32 characters"; + + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierV2MerkleLockup_CampaignNameTooLong.selector, bytes(baseParams.name).length, 32 + ) + ); + + merkleLockupFactory.createMerkleLT( + baseParams, lockupTranched, tranchesWithPercentages, aggregateAmount, recipientCount + ); + } + + modifier whenCampaignNameNotTooLong() { + _; + } + + /// @dev This test works because a default MerkleLockup contract is deployed in {Integration_Test.setUp} + function test_RevertGiven_CreatedAlready() external whenCampaignNameNotTooLong { + MerkleLockup.ConstructorParams memory baseParams = defaults.baseParams(); + MerkleLT.TrancheWithPercentage[] memory tranchesWithPercentages = defaults.tranchesWithPercentages(); + uint256 aggregateAmount = defaults.AGGREGATE_AMOUNT(); + uint256 recipientCount = defaults.RECIPIENT_COUNT(); + + // Expect a revert due to CREATE2. + vm.expectRevert(); + merkleLockupFactory.createMerkleLT( + baseParams, lockupTranched, tranchesWithPercentages, aggregateAmount, recipientCount + ); + } + + modifier givenNotCreatedAlready() { + _; + } + + function testFuzz_CreateMerkleLT( + address admin, + uint40 expiration + ) + external + whenCampaignNameNotTooLong + givenNotCreatedAlready + { + vm.assume(admin != users.admin); + address expectedLT = computeMerkleLTAddress(admin, expiration); + + MerkleLockup.ConstructorParams memory baseParams = defaults.baseParams({ + admin: admin, + asset_: dai, + merkleRoot: defaults.MERKLE_ROOT(), + expiration: expiration + }); + + vm.expectEmit({ emitter: address(merkleLockupFactory) }); + emit CreateMerkleLT({ + merkleLT: ISablierV2MerkleLT(expectedLT), + baseParams: baseParams, + lockupTranched: lockupTranched, + tranchesWithPercentages: defaults.tranchesWithPercentages(), + totalDuration: defaults.TOTAL_DURATION(), + aggregateAmount: defaults.AGGREGATE_AMOUNT(), + recipientCount: defaults.RECIPIENT_COUNT() + }); + + address actualLT = address(createMerkleLT(admin, expiration)); + assertGt(actualLT.code.length, 0, "MerkleLT contract not created"); + assertEq(actualLT, expectedLT, "MerkleLT contract does not match computed address"); + } +} diff --git a/test/integration/merkle-lockup/factory/create-merkle-lt/createMerkleLT.tree b/test/integration/merkle-lockup/factory/create-merkle-lt/createMerkleLT.tree new file mode 100644 index 00000000..eda67426 --- /dev/null +++ b/test/integration/merkle-lockup/factory/create-merkle-lt/createMerkleLT.tree @@ -0,0 +1,9 @@ +createMerkleLT.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 created already + │ └── it should revert + └── given the campaign has not been created already + ├── it should create the campaign + └── it should emit a {CreateMerkleLT} event diff --git a/test/integration/merkle-lockup/factory/is-percentages-sum-100/isPercentagesSum100.t.sol b/test/integration/merkle-lockup/factory/is-percentages-sum-100/isPercentagesSum100.t.sol new file mode 100644 index 00000000..24f94da1 --- /dev/null +++ b/test/integration/merkle-lockup/factory/is-percentages-sum-100/isPercentagesSum100.t.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { MAX_UD2x18, ud2x18 } from "@prb/math/src/UD2x18.sol"; + +import { MerkleLT } from "src/types/DataTypes.sol"; + +import { MerkleLockup_Integration_Test } from "../../MerkleLockup.t.sol"; + +contract IsPercentagesSum100_Integration_Test is MerkleLockup_Integration_Test { + function test_RevertWhen_SumOverflow() public { + MerkleLT.TrancheWithPercentage[] memory tranches = defaults.tranchesWithPercentages(); + tranches[0].unlockPercentage = MAX_UD2x18; + + vm.expectRevert(); + merkleLockupFactory.isPercentagesSum100(tranches); + } + + modifier whenSumDoesNotOverflow() { + _; + } + + modifier whenTotalPercentageNotOneHundred() { + _; + } + + function test_TotalPercentageLessThanOneHundred() + external + view + whenSumDoesNotOverflow + whenTotalPercentageNotOneHundred + { + MerkleLT.TrancheWithPercentage[] memory tranchesWithPercentages = defaults.tranchesWithPercentages(); + tranchesWithPercentages[0].unlockPercentage = ud2x18(0.05e18); + tranchesWithPercentages[1].unlockPercentage = ud2x18(0.2e18); + + assertFalse(merkleLockupFactory.isPercentagesSum100(tranchesWithPercentages), "isPercentagesSum100"); + } + + function test_TotalPercentageGreaterThanOneHundred() + external + view + whenSumDoesNotOverflow + whenTotalPercentageNotOneHundred + { + MerkleLT.TrancheWithPercentage[] memory tranchesWithPercentages = defaults.tranchesWithPercentages(); + tranchesWithPercentages[0].unlockPercentage = ud2x18(0.5e18); + tranchesWithPercentages[1].unlockPercentage = ud2x18(0.6e18); + + assertFalse(merkleLockupFactory.isPercentagesSum100(tranchesWithPercentages), "isPercentagesSum100"); + } + + modifier whenTotalPercentageOneHundred() { + _; + } + + function test_IsPercentagesSum100() external view whenSumDoesNotOverflow whenTotalPercentageOneHundred { + assertTrue(merkleLockupFactory.isPercentagesSum100(defaults.tranchesWithPercentages()), "isPercentagesSum100"); + } +} diff --git a/test/integration/merkle-lockup/factory/is-percentages-sum-100/isPercentagesSum100.tree b/test/integration/merkle-lockup/factory/is-percentages-sum-100/isPercentagesSum100.tree new file mode 100644 index 00000000..d500edc0 --- /dev/null +++ b/test/integration/merkle-lockup/factory/is-percentages-sum-100/isPercentagesSum100.tree @@ -0,0 +1,11 @@ +isPercentagesSum100.t.sol +├── when the sum of the percentages overflows +│ └── it should revert +└── when the sum of the percentages does not overflow + ├── when the sum of the percentages does not equal 100% + │ ├── when the sum of the percentages is less than 100% + │ │ └── it should return false + │ └── when the sum of the percentages is greater than 100% + │ └── it should return false + └── when the sum of the percentages equals 100% + └── it should return true diff --git a/test/integration/merkle-streamer/ll/claim/claim.t.sol b/test/integration/merkle-lockup/ll/claim/claim.t.sol similarity index 56% rename from test/integration/merkle-streamer/ll/claim/claim.t.sol rename to test/integration/merkle-lockup/ll/claim/claim.t.sol index a4742934..30d9441d 100644 --- a/test/integration/merkle-streamer/ll/claim/claim.t.sol +++ b/test/integration/merkle-lockup/ll/claim/claim.t.sol @@ -1,27 +1,26 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Lockup, LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; -import { ud } from "@prb/math/src/UD60x18.sol"; import { Errors } from "src/libraries/Errors.sol"; -import { MerkleStreamer_Integration_Test } from "../../MerkleStreamer.t.sol"; +import { MerkleLockup_Integration_Test } from "../../MerkleLockup.t.sol"; -contract Claim_Integration_Test is MerkleStreamer_Integration_Test { +contract Claim_Integration_Test is MerkleLockup_Integration_Test { function setUp() public virtual override { - MerkleStreamer_Integration_Test.setUp(); + MerkleLockup_Integration_Test.setUp(); } function test_RevertGiven_CampaignExpired() external { uint40 expiration = defaults.EXPIRATION(); uint256 warpTime = expiration + 1 seconds; bytes32[] memory merkleProof; - vm.warp({ timestamp: warpTime }); + vm.warp({ newTimestamp: warpTime }); vm.expectRevert( - abi.encodeWithSelector(Errors.SablierV2MerkleStreamer_CampaignExpired.selector, warpTime, expiration) + abi.encodeWithSelector(Errors.SablierV2MerkleLockup_CampaignExpired.selector, warpTime, expiration) ); - merkleStreamerLL.claim({ index: 1, recipient: users.recipient1, amount: 1, merkleProof: merkleProof }); + merkleLL.claim({ index: 1, recipient: users.recipient1, amount: 1, merkleProof: merkleProof }); } modifier givenCampaignNotExpired() { @@ -33,8 +32,8 @@ contract Claim_Integration_Test is MerkleStreamer_Integration_Test { uint256 index1 = defaults.INDEX1(); uint128 amount = defaults.CLAIM_AMOUNT(); bytes32[] memory merkleProof = defaults.index1Proof(); - vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2MerkleStreamer_StreamClaimed.selector, index1)); - merkleStreamerLL.claim(index1, users.recipient1, amount, merkleProof); + vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2MerkleLockup_StreamClaimed.selector, index1)); + merkleLL.claim(index1, users.recipient1, amount, merkleProof); } modifier givenNotClaimed() { @@ -54,8 +53,8 @@ contract Claim_Integration_Test is MerkleStreamer_Integration_Test { uint256 invalidIndex = 1337; uint128 amount = defaults.CLAIM_AMOUNT(); bytes32[] memory merkleProof = defaults.index1Proof(); - vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2MerkleStreamer_InvalidProof.selector)); - merkleStreamerLL.claim(invalidIndex, users.recipient1, amount, merkleProof); + vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2MerkleLockup_InvalidProof.selector)); + merkleLL.claim(invalidIndex, users.recipient1, amount, merkleProof); } function test_RevertWhen_InvalidRecipient() @@ -68,8 +67,8 @@ contract Claim_Integration_Test is MerkleStreamer_Integration_Test { address invalidRecipient = address(1337); uint128 amount = defaults.CLAIM_AMOUNT(); bytes32[] memory merkleProof = defaults.index1Proof(); - vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2MerkleStreamer_InvalidProof.selector)); - merkleStreamerLL.claim(index1, invalidRecipient, amount, merkleProof); + vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2MerkleLockup_InvalidProof.selector)); + merkleLL.claim(index1, invalidRecipient, amount, merkleProof); } function test_RevertWhen_InvalidAmount() @@ -81,8 +80,8 @@ contract Claim_Integration_Test is MerkleStreamer_Integration_Test { uint256 index1 = defaults.INDEX1(); uint128 invalidAmount = 1337; bytes32[] memory merkleProof = defaults.index1Proof(); - vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2MerkleStreamer_InvalidProof.selector)); - merkleStreamerLL.claim(index1, users.recipient1, invalidAmount, merkleProof); + vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2MerkleLockup_InvalidProof.selector)); + merkleLL.claim(index1, users.recipient1, invalidAmount, merkleProof); } function test_RevertWhen_InvalidMerkleProof() @@ -94,61 +93,38 @@ contract Claim_Integration_Test is MerkleStreamer_Integration_Test { uint256 index1 = defaults.INDEX1(); uint128 amount = defaults.CLAIM_AMOUNT(); bytes32[] memory invalidMerkleProof = defaults.index2Proof(); - vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2MerkleStreamer_InvalidProof.selector)); - merkleStreamerLL.claim(index1, users.recipient1, amount, invalidMerkleProof); + vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2MerkleLockup_InvalidProof.selector)); + merkleLL.claim(index1, users.recipient1, amount, invalidMerkleProof); } modifier givenIncludedInMerkleTree() { _; } - function test_RevertGiven_ProtocolFeeNotZero() - external - givenCampaignNotExpired - givenNotClaimed - givenIncludedInMerkleTree - { - uint128 claimAmount = defaults.CLAIM_AMOUNT(); - bytes32[] memory merkleProof = defaults.index1Proof(); - changePrank({ msgSender: users.admin }); - comptroller.setProtocolFee({ asset: asset, newProtocolFee: ud(0.1e18) }); - vm.expectRevert(Errors.SablierV2MerkleStreamer_ProtocolFeeNotZero.selector); - merkleStreamerLL.claim({ index: 1, recipient: users.recipient1, amount: claimAmount, merkleProof: merkleProof }); - } - - modifier givenProtocolFeeZero() { - _; - } - - function test_Claim() - external - givenCampaignNotExpired - givenNotClaimed - givenIncludedInMerkleTree - givenProtocolFeeZero - { + function test_Claim() external givenCampaignNotExpired givenNotClaimed givenIncludedInMerkleTree { uint256 expectedStreamId = lockupLinear.nextStreamId(); - vm.expectEmit({ emitter: address(merkleStreamerLL) }); + vm.expectEmit({ emitter: address(merkleLL) }); emit Claim(defaults.INDEX1(), users.recipient1, defaults.CLAIM_AMOUNT(), expectedStreamId); uint256 actualStreamId = claimLL(); - LockupLinear.Stream memory actualStream = lockupLinear.getStream(actualStreamId); - LockupLinear.Stream memory expectedStream = LockupLinear.Stream({ + LockupLinear.StreamLL memory actualStream = lockupLinear.getStream(actualStreamId); + LockupLinear.StreamLL memory expectedStream = LockupLinear.StreamLL({ amounts: Lockup.Amounts({ deposited: defaults.CLAIM_AMOUNT(), refunded: 0, withdrawn: 0 }), - asset: asset, - cliffTime: uint40(block.timestamp) + defaults.CLIFF_DURATION(), - endTime: uint40(block.timestamp) + defaults.TOTAL_DURATION(), + asset: dai, + cliffTime: getBlockTimestamp() + defaults.CLIFF_DURATION(), + endTime: getBlockTimestamp() + defaults.TOTAL_DURATION(), isCancelable: defaults.CANCELABLE(), isDepleted: false, isStream: true, isTransferable: defaults.TRANSFERABLE(), + recipient: users.recipient1, sender: users.admin, - startTime: uint40(block.timestamp), + startTime: getBlockTimestamp(), wasCanceled: false }); - assertTrue(merkleStreamerLL.hasClaimed(defaults.INDEX1()), "not claimed"); + assertTrue(merkleLL.hasClaimed(defaults.INDEX1()), "not claimed"); assertEq(actualStreamId, expectedStreamId, "invalid stream id"); assertEq(actualStream, expectedStream); } diff --git a/test/integration/merkle-streamer/ll/claim/claim.tree b/test/integration/merkle-lockup/ll/claim/claim.tree similarity index 69% rename from test/integration/merkle-streamer/ll/claim/claim.tree rename to test/integration/merkle-lockup/ll/claim/claim.tree index d59287b0..b6122178 100644 --- a/test/integration/merkle-streamer/ll/claim/claim.tree +++ b/test/integration/merkle-lockup/ll/claim/claim.tree @@ -15,9 +15,6 @@ claim.t.sol │ └── when the Merkle proof is not valid │ └── it should revert └── given the claim is included in the Merkle tree - ├── given the protocol fee is greater than zero - │ └── it should revert - └── 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 + ├── it should mark the index as claimed + ├── it should create a stream + └── it should emit a {Claim} event diff --git a/test/integration/merkle-lockup/ll/clawback/clawback.t.sol b/test/integration/merkle-lockup/ll/clawback/clawback.t.sol new file mode 100644 index 00000000..226b62a6 --- /dev/null +++ b/test/integration/merkle-lockup/ll/clawback/clawback.t.sol @@ -0,0 +1,85 @@ +// 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 Clawback_Integration_Test is MerkleLockup_Integration_Test { + function setUp() public virtual override { + MerkleLockup_Integration_Test.setUp(); + } + + function test_RevertWhen_CallerNotAdmin() external { + resetPrank({ msgSender: users.eve }); + vm.expectRevert(abi.encodeWithSelector(V2CoreErrors.CallerNotAdmin.selector, users.admin, users.eve)); + merkleLL.clawback({ to: users.eve, amount: 1 }); + } + + modifier whenCallerAdmin() { + resetPrank({ msgSender: users.admin }); + _; + } + + function test_Clawback_BeforeFirstClaim() external whenCallerAdmin { + test_Clawback(users.admin); + } + + modifier afterFirstClaim() { + // Make the first claim to set `_firstClaimTime`. + claimLL(); + _; + } + + function test_Clawback_GracePeriod() external whenCallerAdmin afterFirstClaim { + vm.warp({ newTimestamp: block.timestamp + 6 days }); + test_Clawback(users.admin); + } + + modifier postGracePeriod() { + vm.warp({ newTimestamp: block.timestamp + 8 days }); + _; + } + + function test_RevertGiven_CampaignNotExpired() external whenCallerAdmin afterFirstClaim postGracePeriod { + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierV2MerkleLockup_ClawbackNotAllowed.selector, + block.timestamp, + defaults.EXPIRATION(), + defaults.FIRST_CLAIM_TIME() + ) + ); + merkleLL.clawback({ to: users.admin, amount: 1 }); + } + + modifier givenCampaignExpired() { + vm.warp({ newTimestamp: defaults.EXPIRATION() + 1 seconds }); + _; + } + + function test_Clawback() external whenCallerAdmin afterFirstClaim postGracePeriod givenCampaignExpired { + test_Clawback(users.admin); + } + + function testFuzz_Clawback(address to) + external + whenCallerAdmin + afterFirstClaim + postGracePeriod + givenCampaignExpired + { + vm.assume(to != address(0)); + test_Clawback(to); + } + + function test_Clawback(address to) internal { + uint128 clawbackAmount = uint128(dai.balanceOf(address(merkleLL))); + expectCallToTransfer({ to: to, amount: clawbackAmount }); + vm.expectEmit({ emitter: address(merkleLL) }); + emit Clawback({ admin: users.admin, to: to, amount: clawbackAmount }); + merkleLL.clawback({ to: to, amount: clawbackAmount }); + } +} diff --git a/test/integration/merkle-lockup/ll/clawback/clawback.tree b/test/integration/merkle-lockup/ll/clawback/clawback.tree new file mode 100644 index 00000000..b961e888 --- /dev/null +++ b/test/integration/merkle-lockup/ll/clawback/clawback.tree @@ -0,0 +1,17 @@ +clawback.t.sol +├── when the caller is not the admin +│ └── it should revert +└── when the caller is the admin + ├── when the first claim has not been made + │ ├── it should perform the ERC-20 transfer + │ └── it should emit a {Clawback} event + └── when the first claim has been made + ├── given the current time is not more than 7 days after the first claim + │ ├── it should perform the ERC-20 transfer + │ └── it should emit a {Clawback} event + └── given the current time is more than 7 days after the first claim + ├── 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-streamer/ll/constructor/constructor.t.sol b/test/integration/merkle-lockup/ll/constructor/constructor.t.sol similarity index 60% rename from test/integration/merkle-streamer/ll/constructor/constructor.t.sol rename to test/integration/merkle-lockup/ll/constructor/constructor.t.sol index 676b9d18..4ae23a8e 100644 --- a/test/integration/merkle-streamer/ll/constructor/constructor.t.sol +++ b/test/integration/merkle-lockup/ll/constructor/constructor.t.sol @@ -1,84 +1,88 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; -import { SablierV2MerkleStreamerLL } from "src/SablierV2MerkleStreamerLL.sol"; +import { SablierV2MerkleLL } from "src/SablierV2MerkleLL.sol"; -import { MerkleStreamer_Integration_Test } from "../../MerkleStreamer.t.sol"; +import { MerkleLockup_Integration_Test } from "../../MerkleLockup.t.sol"; -contract Constructor_MerkleStreamerLL_Integration_Test is MerkleStreamer_Integration_Test { +contract Constructor_MerkleLL_Integration_Test is MerkleLockup_Integration_Test { /// @dev Needed to prevent "Stack too deep" error struct Vars { address actualAdmin; uint256 actualAllowance; address actualAsset; + string actualIpfsCID; + string actualName; bool actualCancelable; - bool actualTransferable; LockupLinear.Durations actualDurations; uint40 actualExpiration; address actualLockupLinear; bytes32 actualMerkleRoot; + bool actualTransferable; address expectedAdmin; uint256 expectedAllowance; address expectedAsset; bool expectedCancelable; - bool expectedTransferable; LockupLinear.Durations expectedDurations; uint40 expectedExpiration; + string expectedIpfsCID; address expectedLockupLinear; bytes32 expectedMerkleRoot; + bytes32 expectedName; + bool expectedTransferable; } function test_Constructor() external { - SablierV2MerkleStreamerLL constructedStreamerLL = new SablierV2MerkleStreamerLL( - users.admin, - lockupLinear, - asset, - defaults.MERKLE_ROOT(), - defaults.EXPIRATION(), - defaults.durations(), - defaults.CANCELABLE(), - defaults.TRANSFERABLE() - ); + SablierV2MerkleLL constructedLL = + new SablierV2MerkleLL(defaults.baseParams(), lockupLinear, defaults.durations()); Vars memory vars; - vars.actualAdmin = constructedStreamerLL.admin(); + vars.actualAdmin = constructedLL.admin(); vars.expectedAdmin = users.admin; assertEq(vars.actualAdmin, vars.expectedAdmin, "admin"); - vars.actualAsset = address(constructedStreamerLL.ASSET()); - vars.expectedAsset = address(asset); - assertEq(vars.actualAsset, vars.expectedAsset, "asset"); + vars.actualAllowance = dai.allowance(address(constructedLL), address(lockupLinear)); + vars.expectedAllowance = MAX_UINT256; + assertEq(vars.actualAllowance, vars.expectedAllowance, "allowance"); - vars.actualMerkleRoot = constructedStreamerLL.MERKLE_ROOT(); - vars.expectedMerkleRoot = defaults.MERKLE_ROOT(); - assertEq(vars.actualMerkleRoot, vars.expectedMerkleRoot, "merkleRoot"); + vars.actualAsset = address(constructedLL.ASSET()); + vars.expectedAsset = address(dai); + assertEq(vars.actualAsset, vars.expectedAsset, "asset"); - vars.actualCancelable = constructedStreamerLL.CANCELABLE(); + vars.actualCancelable = constructedLL.CANCELABLE(); vars.expectedCancelable = defaults.CANCELABLE(); assertEq(vars.actualCancelable, vars.expectedCancelable, "cancelable"); - vars.actualTransferable = constructedStreamerLL.TRANSFERABLE(); - vars.expectedTransferable = defaults.TRANSFERABLE(); - assertEq(vars.actualTransferable, vars.expectedTransferable, "transferable"); + (vars.actualDurations.cliff, vars.actualDurations.total) = constructedLL.streamDurations(); + vars.expectedDurations = defaults.durations(); + assertEq(vars.actualDurations.cliff, vars.expectedDurations.cliff, "durations.cliff"); + assertEq(vars.actualDurations.total, vars.expectedDurations.total, "durations.total"); - vars.actualExpiration = constructedStreamerLL.EXPIRATION(); + vars.actualExpiration = constructedLL.EXPIRATION(); vars.expectedExpiration = defaults.EXPIRATION(); assertEq(vars.actualExpiration, vars.expectedExpiration, "expiration"); - vars.actualLockupLinear = address(constructedStreamerLL.LOCKUP_LINEAR()); + vars.actualIpfsCID = constructedLL.ipfsCID(); + vars.expectedIpfsCID = defaults.IPFS_CID(); + assertEq(vars.actualIpfsCID, vars.expectedIpfsCID, "ipfsCID"); + + vars.actualLockupLinear = address(constructedLL.LOCKUP_LINEAR()); vars.expectedLockupLinear = address(lockupLinear); assertEq(vars.actualLockupLinear, vars.expectedLockupLinear, "lockupLinear"); - (vars.actualDurations.cliff, vars.actualDurations.total) = constructedStreamerLL.streamDurations(); - vars.expectedDurations = defaults.durations(); - assertEq(vars.actualDurations.cliff, vars.expectedDurations.cliff, "durations.cliff"); - assertEq(vars.actualDurations.total, vars.expectedDurations.total, "durations.total"); + vars.actualMerkleRoot = constructedLL.MERKLE_ROOT(); + vars.expectedMerkleRoot = defaults.MERKLE_ROOT(); + assertEq(vars.actualMerkleRoot, vars.expectedMerkleRoot, "merkleRoot"); - vars.actualAllowance = asset.allowance(address(constructedStreamerLL), address(lockupLinear)); - vars.expectedAllowance = MAX_UINT256; - assertEq(vars.actualAllowance, vars.expectedAllowance, "allowance"); + vars.actualName = constructedLL.name(); + vars.expectedName = defaults.NAME_BYTES32(); + assertEq(bytes32(abi.encodePacked(vars.actualName)), vars.expectedName, "name"); + + vars.actualTransferable = constructedLL.TRANSFERABLE(); + vars.expectedTransferable = defaults.TRANSFERABLE(); + assertEq(vars.actualTransferable, vars.expectedTransferable, "transferable"); } } diff --git a/test/integration/merkle-lockup/ll/get-first-claim-time/getFirstClaimTime.t.sol b/test/integration/merkle-lockup/ll/get-first-claim-time/getFirstClaimTime.t.sol new file mode 100644 index 00000000..bd61c2e1 --- /dev/null +++ b/test/integration/merkle-lockup/ll/get-first-claim-time/getFirstClaimTime.t.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { MerkleLockup_Integration_Test } from "../../MerkleLockup.t.sol"; + +contract GetFirstClaimTime_Integration_Test is MerkleLockup_Integration_Test { + function setUp() public virtual override { + MerkleLockup_Integration_Test.setUp(); + } + + function test_GetFirstClaimTime_BeforeFirstClaim() external view { + uint256 firstClaimTime = merkleLL.getFirstClaimTime(); + assertEq(firstClaimTime, 0); + } + + modifier afterFirstClaim() { + // Make the first claim to set `_firstClaimTime`. + claimLL(); + _; + } + + function test_GetFirstClaimTime() external afterFirstClaim { + uint256 firstClaimTime = merkleLL.getFirstClaimTime(); + assertEq(firstClaimTime, block.timestamp); + } +} diff --git a/test/integration/merkle-lockup/ll/get-first-claim-time/getFirstClaimTime.tree b/test/integration/merkle-lockup/ll/get-first-claim-time/getFirstClaimTime.tree new file mode 100644 index 00000000..cab77842 --- /dev/null +++ b/test/integration/merkle-lockup/ll/get-first-claim-time/getFirstClaimTime.tree @@ -0,0 +1,5 @@ +getFirstClaimTime.t.sol +├── when the first claim has not been made +│ └── it should return 0 +└── when the first claim has been made + └── it should return the time of the first claim diff --git a/test/integration/merkle-streamer/ll/has-claimed/hasClaimed.t.sol b/test/integration/merkle-lockup/ll/has-claimed/hasClaimed.t.sol similarity index 51% rename from test/integration/merkle-streamer/ll/has-claimed/hasClaimed.t.sol rename to test/integration/merkle-lockup/ll/has-claimed/hasClaimed.t.sol index a2b12ee8..0b07e916 100644 --- a/test/integration/merkle-streamer/ll/has-claimed/hasClaimed.t.sol +++ b/test/integration/merkle-lockup/ll/has-claimed/hasClaimed.t.sol @@ -1,16 +1,16 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; -import { MerkleStreamer_Integration_Test } from "../../MerkleStreamer.t.sol"; +import { MerkleLockup_Integration_Test } from "../../MerkleLockup.t.sol"; -contract HasClaimed_Integration_Test is MerkleStreamer_Integration_Test { +contract HasClaimed_Integration_Test is MerkleLockup_Integration_Test { function setUp() public virtual override { - MerkleStreamer_Integration_Test.setUp(); + MerkleLockup_Integration_Test.setUp(); } function test_HasClaimed_IndexNotInTree() external { uint256 indexNotInTree = 1337e18; - assertFalse(merkleStreamerLL.hasClaimed(indexNotInTree), "claimed"); + assertFalse(merkleLL.hasClaimed(indexNotInTree), "claimed"); } modifier whenIndexInTree() { @@ -18,7 +18,7 @@ contract HasClaimed_Integration_Test is MerkleStreamer_Integration_Test { } function test_HasClaimed_NotClaimed() external whenIndexInTree { - assertFalse(merkleStreamerLL.hasClaimed(defaults.INDEX1()), "claimed"); + assertFalse(merkleLL.hasClaimed(defaults.INDEX1()), "claimed"); } modifier givenRecipientHasClaimed() { @@ -27,6 +27,6 @@ contract HasClaimed_Integration_Test is MerkleStreamer_Integration_Test { } function test_HasClaimed() external whenIndexInTree givenRecipientHasClaimed { - assertTrue(merkleStreamerLL.hasClaimed(defaults.INDEX1()), "not claimed"); + assertTrue(merkleLL.hasClaimed(defaults.INDEX1()), "not claimed"); } } diff --git a/test/integration/merkle-streamer/ll/has-claimed/hasClaimed.tree b/test/integration/merkle-lockup/ll/has-claimed/hasClaimed.tree similarity index 100% rename from test/integration/merkle-streamer/ll/has-claimed/hasClaimed.tree rename to test/integration/merkle-lockup/ll/has-claimed/hasClaimed.tree diff --git a/test/integration/merkle-lockup/ll/has-expired/hasExpired.t.sol b/test/integration/merkle-lockup/ll/has-expired/hasExpired.t.sol new file mode 100644 index 00000000..85de0fa2 --- /dev/null +++ b/test/integration/merkle-lockup/ll/has-expired/hasExpired.t.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { ISablierV2MerkleLL } from "src/interfaces/ISablierV2MerkleLL.sol"; + +import { MerkleLockup_Integration_Test } from "../../MerkleLockup.t.sol"; + +contract HasExpired_Integration_Test is MerkleLockup_Integration_Test { + function setUp() public virtual override { + MerkleLockup_Integration_Test.setUp(); + } + + function test_HasExpired_ExpirationZero() external { + ISablierV2MerkleLL testLockup = createMerkleLL({ expiration: 0 }); + assertFalse(testLockup.hasExpired(), "campaign expired"); + } + + modifier givenExpirationNotZero() { + _; + } + + function test_HasExpired_ExpirationLessThanBlockTimestamp() external view givenExpirationNotZero { + assertFalse(merkleLL.hasExpired(), "campaign expired"); + } + + function test_HasExpired_ExpirationEqualToBlockTimestamp() external givenExpirationNotZero { + vm.warp({ newTimestamp: defaults.EXPIRATION() }); + assertTrue(merkleLL.hasExpired(), "campaign not expired"); + } + + function test_HasExpired_ExpirationGreaterThanBlockTimestamp() external givenExpirationNotZero { + vm.warp({ newTimestamp: defaults.EXPIRATION() + 1 seconds }); + assertTrue(merkleLL.hasExpired(), "campaign not expired"); + } +} diff --git a/test/integration/merkle-streamer/ll/has-expired/hasExpired.tree b/test/integration/merkle-lockup/ll/has-expired/hasExpired.tree similarity index 55% rename from test/integration/merkle-streamer/ll/has-expired/hasExpired.tree rename to test/integration/merkle-lockup/ll/has-expired/hasExpired.tree index 18fd5766..5fd22925 100644 --- a/test/integration/merkle-streamer/ll/has-expired/hasExpired.tree +++ b/test/integration/merkle-lockup/ll/has-expired/hasExpired.tree @@ -2,9 +2,9 @@ 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 + ├── when the expiration is less than the block timestamp │ └── it should return false - ├── when the expiration is equal to the current time + ├── when the expiration is equal to the block timestamp │ └── it should return true - └── when the expiration is greater than current time + └── when the expiration is greater than the block timestamp └── it should return true diff --git a/test/integration/merkle-lockup/lt/claim/claim.t.sol b/test/integration/merkle-lockup/lt/claim/claim.t.sol new file mode 100644 index 00000000..2052bcea --- /dev/null +++ b/test/integration/merkle-lockup/lt/claim/claim.t.sol @@ -0,0 +1,281 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { Arrays } from "@openzeppelin/contracts/utils/Arrays.sol"; +import { ud2x18 } from "@prb/math/src/UD2x18.sol"; +import { Lockup, LockupTranched } from "@sablier/v2-core/src/types/DataTypes.sol"; + +import { ISablierV2MerkleLT } from "src/interfaces/ISablierV2MerkleLT.sol"; +import { Errors } from "src/libraries/Errors.sol"; +import { MerkleLockup, MerkleLT } from "src/types/DataTypes.sol"; + +import { MerkleBuilder } from "../../../../utils/MerkleBuilder.sol"; +import { Merkle } from "../../../../utils/Murky.sol"; + +import { MerkleLockup_Integration_Test } from "../../MerkleLockup.t.sol"; + +contract Claim_Integration_Test is Merkle, MerkleLockup_Integration_Test { + using MerkleBuilder for uint256[]; + + function setUp() public virtual override { + MerkleLockup_Integration_Test.setUp(); + } + + modifier whenTotalPercentageNotOneHundred() { + _; + } + + function test_RevertWhen_TotalPercentageLessThanOneHundred() external whenTotalPercentageNotOneHundred { + // Create a MerkleLT campaign with a total percentage less than 100. + MerkleLockup.ConstructorParams memory baseParams = defaults.baseParams(); + uint256 aggregateAmount = defaults.AGGREGATE_AMOUNT(); + uint256 recipientCount = defaults.RECIPIENT_COUNT(); + + MerkleLT.TrancheWithPercentage[] memory tranchesWithPercentages = defaults.tranchesWithPercentages(); + tranchesWithPercentages[0].unlockPercentage = ud2x18(0.05e18); + tranchesWithPercentages[1].unlockPercentage = ud2x18(0.2e18); + + uint64 totalPercentage = + tranchesWithPercentages[0].unlockPercentage.unwrap() + tranchesWithPercentages[1].unlockPercentage.unwrap(); + + merkleLT = merkleLockupFactory.createMerkleLT( + baseParams, lockupTranched, tranchesWithPercentages, aggregateAmount, recipientCount + ); + + // Claim an airstream. + bytes32[] memory merkleProof = defaults.index1Proof(); + + vm.expectRevert( + abi.encodeWithSelector(Errors.SablierV2MerkleLT_TotalPercentageNotOneHundred.selector, totalPercentage) + ); + + merkleLT.claim({ index: 1, recipient: users.recipient1, amount: 1, merkleProof: merkleProof }); + } + + function test_RevertWhen_TotalPercentageGreaterThanOneHundred() external whenTotalPercentageNotOneHundred { + // Create a MerkleLT campaign with a total percentage less than 100. + MerkleLockup.ConstructorParams memory baseParams = defaults.baseParams(); + uint256 aggregateAmount = defaults.AGGREGATE_AMOUNT(); + uint256 recipientCount = defaults.RECIPIENT_COUNT(); + + MerkleLT.TrancheWithPercentage[] memory tranchesWithPercentages = defaults.tranchesWithPercentages(); + tranchesWithPercentages[0].unlockPercentage = ud2x18(0.75e18); + tranchesWithPercentages[1].unlockPercentage = ud2x18(0.8e18); + + uint64 totalPercentage = + tranchesWithPercentages[0].unlockPercentage.unwrap() + tranchesWithPercentages[1].unlockPercentage.unwrap(); + + merkleLT = merkleLockupFactory.createMerkleLT( + baseParams, lockupTranched, tranchesWithPercentages, aggregateAmount, recipientCount + ); + + // Claim an airstream. + bytes32[] memory merkleProof = defaults.index1Proof(); + + vm.expectRevert( + abi.encodeWithSelector(Errors.SablierV2MerkleLT_TotalPercentageNotOneHundred.selector, totalPercentage) + ); + + merkleLT.claim({ index: 1, recipient: users.recipient1, amount: 1, merkleProof: merkleProof }); + } + + modifier whenTotalPercentageOneHundred() { + _; + } + + function test_RevertGiven_CampaignExpired() external whenTotalPercentageOneHundred { + uint40 expiration = defaults.EXPIRATION(); + uint256 warpTime = expiration + 1 seconds; + bytes32[] memory merkleProof; + vm.warp({ newTimestamp: warpTime }); + vm.expectRevert( + abi.encodeWithSelector(Errors.SablierV2MerkleLockup_CampaignExpired.selector, warpTime, expiration) + ); + merkleLT.claim({ index: 1, recipient: users.recipient1, amount: 1, merkleProof: merkleProof }); + } + + modifier givenCampaignNotExpired() { + _; + } + + function test_RevertGiven_AlreadyClaimed() external whenTotalPercentageOneHundred givenCampaignNotExpired { + claimLT(); + uint256 index1 = defaults.INDEX1(); + uint128 amount = defaults.CLAIM_AMOUNT(); + bytes32[] memory merkleProof = defaults.index1Proof(); + vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2MerkleLockup_StreamClaimed.selector, index1)); + merkleLT.claim(index1, users.recipient1, amount, merkleProof); + } + + modifier givenNotClaimed() { + _; + } + + modifier givenNotIncludedInMerkleTree() { + _; + } + + function test_RevertWhen_InvalidIndex() + external + whenTotalPercentageOneHundred + givenCampaignNotExpired + givenNotClaimed + givenNotIncludedInMerkleTree + { + uint256 invalidIndex = 1337; + uint128 amount = defaults.CLAIM_AMOUNT(); + bytes32[] memory merkleProof = defaults.index1Proof(); + vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2MerkleLockup_InvalidProof.selector)); + merkleLT.claim(invalidIndex, users.recipient1, amount, merkleProof); + } + + function test_RevertWhen_InvalidRecipient() + external + whenTotalPercentageOneHundred + givenCampaignNotExpired + givenNotClaimed + givenNotIncludedInMerkleTree + { + uint256 index1 = defaults.INDEX1(); + address invalidRecipient = address(1337); + uint128 amount = defaults.CLAIM_AMOUNT(); + bytes32[] memory merkleProof = defaults.index1Proof(); + vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2MerkleLockup_InvalidProof.selector)); + merkleLT.claim(index1, invalidRecipient, amount, merkleProof); + } + + function test_RevertWhen_InvalidAmount() + external + whenTotalPercentageOneHundred + givenCampaignNotExpired + givenNotClaimed + givenNotIncludedInMerkleTree + { + uint256 index1 = defaults.INDEX1(); + uint128 invalidAmount = 1337; + bytes32[] memory merkleProof = defaults.index1Proof(); + vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2MerkleLockup_InvalidProof.selector)); + merkleLT.claim(index1, users.recipient1, invalidAmount, merkleProof); + } + + function test_RevertWhen_InvalidMerkleProof() + external + whenTotalPercentageOneHundred + givenCampaignNotExpired + givenNotClaimed + givenNotIncludedInMerkleTree + { + uint256 index1 = defaults.INDEX1(); + uint128 amount = defaults.CLAIM_AMOUNT(); + bytes32[] memory invalidMerkleProof = defaults.index2Proof(); + vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2MerkleLockup_InvalidProof.selector)); + merkleLT.claim(index1, users.recipient1, amount, invalidMerkleProof); + } + + modifier givenIncludedInMerkleTree() { + _; + } + + /// @dev Needed this variable in storage due to how the imported libraries work. + uint256[] public leaves = new uint256[](4); // same number of recipients as in Defaults + + function test_Claim_CalculatedAmountsSumNotEqualClaimAmount() + external + whenTotalPercentageOneHundred + givenCampaignNotExpired + givenNotClaimed + givenIncludedInMerkleTree + { + // Declare a claim amount that will cause a rounding error. + uint128 claimAmount = defaults.CLAIM_AMOUNT() + 1; + + // Compute the test Merkle tree. + leaves = defaults.getLeaves(); + uint256 leaf = MerkleBuilder.computeLeaf(defaults.INDEX1(), users.recipient1, claimAmount); + leaves[0] = leaf; + MerkleBuilder.sortLeaves(leaves); + + // Compute the test Merkle proof. + uint256 pos = Arrays.findUpperBound(leaves, leaf); + bytes32[] memory proof = getProof(leaves.toBytes32(), pos); + + /// Declare the constructor params. + MerkleLockup.ConstructorParams memory baseParams = defaults.baseParams(); + baseParams.merkleRoot = getRoot(leaves.toBytes32()); + + // Deploy a test MerkleLT contract. + ISablierV2MerkleLT testMerkleLT = merkleLockupFactory.createMerkleLT( + baseParams, + lockupTranched, + defaults.tranchesWithPercentages(), + defaults.AGGREGATE_AMOUNT(), + defaults.RECIPIENT_COUNT() + ); + + // Fund the MerkleLT contract. + deal({ token: address(dai), to: address(testMerkleLT), give: defaults.AGGREGATE_AMOUNT() }); + + uint256 expectedStreamId = lockupTranched.nextStreamId(); + vm.expectEmit({ emitter: address(testMerkleLT) }); + emit Claim(defaults.INDEX1(), users.recipient1, claimAmount, expectedStreamId); + + uint256 actualStreamId = testMerkleLT.claim(defaults.INDEX1(), users.recipient1, claimAmount, proof); + LockupTranched.StreamLT memory actualStream = lockupTranched.getStream(actualStreamId); + LockupTranched.StreamLT memory expectedStream = LockupTranched.StreamLT({ + amounts: Lockup.Amounts({ deposited: claimAmount, refunded: 0, withdrawn: 0 }), + asset: dai, + endTime: getBlockTimestamp() + defaults.TOTAL_DURATION(), + isCancelable: defaults.CANCELABLE(), + isDepleted: false, + isStream: true, + isTransferable: defaults.TRANSFERABLE(), + recipient: users.recipient1, + sender: users.admin, + startTime: getBlockTimestamp(), + tranches: defaults.tranches(claimAmount), + wasCanceled: false + }); + + assertTrue(testMerkleLT.hasClaimed(defaults.INDEX1()), "not claimed"); + assertEq(actualStreamId, expectedStreamId, "invalid stream id"); + assertEq(actualStream, expectedStream); + } + + modifier whenCalculatedAmountsSumEqualsClaimAmount() { + _; + } + + function test_Claim() + external + whenTotalPercentageOneHundred + givenCampaignNotExpired + givenNotClaimed + givenIncludedInMerkleTree + whenCalculatedAmountsSumEqualsClaimAmount + { + uint256 expectedStreamId = lockupTranched.nextStreamId(); + vm.expectEmit({ emitter: address(merkleLT) }); + emit Claim(defaults.INDEX1(), users.recipient1, defaults.CLAIM_AMOUNT(), expectedStreamId); + + uint256 actualStreamId = claimLT(); + LockupTranched.StreamLT memory actualStream = lockupTranched.getStream(actualStreamId); + LockupTranched.StreamLT memory expectedStream = LockupTranched.StreamLT({ + amounts: Lockup.Amounts({ deposited: defaults.CLAIM_AMOUNT(), refunded: 0, withdrawn: 0 }), + asset: dai, + endTime: getBlockTimestamp() + defaults.TOTAL_DURATION(), + isCancelable: defaults.CANCELABLE(), + isDepleted: false, + isStream: true, + isTransferable: defaults.TRANSFERABLE(), + recipient: users.recipient1, + sender: users.admin, + startTime: getBlockTimestamp(), + tranches: defaults.tranches(), + wasCanceled: false + }); + + assertTrue(merkleLT.hasClaimed(defaults.INDEX1()), "not claimed"); + assertEq(actualStreamId, expectedStreamId, "invalid stream id"); + assertEq(actualStream, expectedStream); + } +} diff --git a/test/integration/merkle-lockup/lt/claim/claim.tree b/test/integration/merkle-lockup/lt/claim/claim.tree new file mode 100644 index 00000000..ab986c29 --- /dev/null +++ b/test/integration/merkle-lockup/lt/claim/claim.tree @@ -0,0 +1,33 @@ +claim.t.sol +. +├── when the total percentage does not equal 100% +│ ├── when the total percentage is less than 100% +│ │ └── it should revert +│ └── when the total percentage is greater than 100% +│ └── it should revert +└── when the total percentage equals 100% + ├── 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 amount is not valid + │ │ └── it should revert + │ └── when the Merkle proof is not valid + │ └── it should revert + └── given the claim is included in the Merkle tree + ├── when the sum of the calculated amounts does not equal the claim amount + │ ├── it should adjust the last tranche amount + │ ├── it should mark the index as claimed + │ ├── it should create a stream + │ └── it should emit a {Claim} event + └── when the sum of the calculated amounts equals the claim amount + ├── it should mark the index as claimed + ├── it should create a stream + └── it should emit a {Claim} event diff --git a/test/integration/merkle-lockup/lt/clawback/clawback.t.sol b/test/integration/merkle-lockup/lt/clawback/clawback.t.sol new file mode 100644 index 00000000..41c8f0a5 --- /dev/null +++ b/test/integration/merkle-lockup/lt/clawback/clawback.t.sol @@ -0,0 +1,84 @@ +// 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 Clawback_Integration_Test is MerkleLockup_Integration_Test { + function setUp() public virtual override { + MerkleLockup_Integration_Test.setUp(); + } + + function test_RevertWhen_CallerNotAdmin() external { + resetPrank({ msgSender: users.eve }); + vm.expectRevert(abi.encodeWithSelector(V2CoreErrors.CallerNotAdmin.selector, users.admin, users.eve)); + merkleLT.clawback({ to: users.eve, amount: 1 }); + } + + modifier whenCallerAdmin() { + resetPrank({ msgSender: users.admin }); + _; + } + + function test_Clawback_BeforeFirstClaim() external whenCallerAdmin { + test_Clawback(users.admin); + } + + modifier afterFirstClaim() { + claimLT(); + _; + } + + function test_Clawback_GracePeriod() external whenCallerAdmin afterFirstClaim { + vm.warp({ newTimestamp: block.timestamp + 6 days }); + test_Clawback(users.admin); + } + + modifier postGracePeriod() { + vm.warp({ newTimestamp: block.timestamp + 8 days }); + _; + } + + function test_RevertGiven_CampaignNotExpired() external whenCallerAdmin afterFirstClaim postGracePeriod { + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierV2MerkleLockup_ClawbackNotAllowed.selector, + block.timestamp, + defaults.EXPIRATION(), + defaults.FIRST_CLAIM_TIME() + ) + ); + merkleLT.clawback({ to: users.admin, amount: 1 }); + } + + modifier givenCampaignExpired() { + vm.warp({ newTimestamp: defaults.EXPIRATION() + 1 seconds }); + _; + } + + function test_Clawback() external whenCallerAdmin afterFirstClaim postGracePeriod givenCampaignExpired { + test_Clawback(users.admin); + } + + function testFuzz_Clawback(address to) + external + whenCallerAdmin + afterFirstClaim + postGracePeriod + givenCampaignExpired + { + vm.assume(to != address(0)); + test_Clawback(to); + } + + function test_Clawback(address to) internal { + uint128 clawbackAmount = uint128(dai.balanceOf(address(merkleLT))); + expectCallToTransfer({ to: to, amount: clawbackAmount }); + vm.expectEmit({ emitter: address(merkleLT) }); + emit Clawback({ admin: users.admin, to: to, amount: clawbackAmount }); + merkleLT.clawback({ to: to, amount: clawbackAmount }); + } +} diff --git a/test/integration/merkle-lockup/lt/clawback/clawback.tree b/test/integration/merkle-lockup/lt/clawback/clawback.tree new file mode 100644 index 00000000..b961e888 --- /dev/null +++ b/test/integration/merkle-lockup/lt/clawback/clawback.tree @@ -0,0 +1,17 @@ +clawback.t.sol +├── when the caller is not the admin +│ └── it should revert +└── when the caller is the admin + ├── when the first claim has not been made + │ ├── it should perform the ERC-20 transfer + │ └── it should emit a {Clawback} event + └── when the first claim has been made + ├── given the current time is not more than 7 days after the first claim + │ ├── it should perform the ERC-20 transfer + │ └── it should emit a {Clawback} event + └── given the current time is more than 7 days after the first claim + ├── 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/lt/constructor/constructor.t.sol b/test/integration/merkle-lockup/lt/constructor/constructor.t.sol new file mode 100644 index 00000000..2b98685f --- /dev/null +++ b/test/integration/merkle-lockup/lt/constructor/constructor.t.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { SablierV2MerkleLT } from "src/SablierV2MerkleLT.sol"; +import { MerkleLT } from "src/types/DataTypes.sol"; + +import { MerkleLockup_Integration_Test } from "../../MerkleLockup.t.sol"; + +contract Constructor_MerkleLT_Integration_Test is MerkleLockup_Integration_Test { + /// @dev Needed to prevent "Stack too deep" error + struct Vars { + address actualAdmin; + uint256 actualAllowance; + address actualAsset; + string actualIpfsCID; + string actualName; + bool actualCancelable; + uint40 actualExpiration; + address actualLockupTranched; + bytes32 actualMerkleRoot; + uint64 actualTotalPercentage; + MerkleLT.TrancheWithPercentage[] actualTranchesWithPercentages; + bool actualTransferable; + address expectedAdmin; + uint256 expectedAllowance; + address expectedAsset; + bool expectedCancelable; + string expectedIpfsCID; + uint40 expectedExpiration; + address expectedLockupTranched; + bytes32 expectedMerkleRoot; + bytes32 expectedName; + uint64 expectedTotalPercentage; + MerkleLT.TrancheWithPercentage[] expectedTranchesWithPercentages; + bool expectedTransferable; + } + + function test_Constructor() external { + SablierV2MerkleLT constructedLT = + new SablierV2MerkleLT(defaults.baseParams(), lockupTranched, defaults.tranchesWithPercentages()); + + Vars memory vars; + + vars.actualAdmin = constructedLT.admin(); + vars.expectedAdmin = users.admin; + assertEq(vars.actualAdmin, vars.expectedAdmin, "admin"); + + vars.actualAllowance = dai.allowance(address(constructedLT), address(lockupTranched)); + vars.expectedAllowance = MAX_UINT256; + assertEq(vars.actualAllowance, vars.expectedAllowance, "allowance"); + + vars.actualAsset = address(constructedLT.ASSET()); + vars.expectedAsset = address(dai); + assertEq(vars.actualAsset, vars.expectedAsset, "asset"); + + vars.actualCancelable = constructedLT.CANCELABLE(); + vars.expectedCancelable = defaults.CANCELABLE(); + assertEq(vars.actualCancelable, vars.expectedCancelable, "cancelable"); + + vars.actualExpiration = constructedLT.EXPIRATION(); + vars.expectedExpiration = defaults.EXPIRATION(); + assertEq(vars.actualExpiration, vars.expectedExpiration, "expiration"); + + vars.actualIpfsCID = constructedLT.ipfsCID(); + vars.expectedIpfsCID = defaults.IPFS_CID(); + assertEq(vars.actualIpfsCID, vars.expectedIpfsCID, "ipfsCID"); + + vars.actualLockupTranched = address(constructedLT.LOCKUP_TRANCHED()); + vars.expectedLockupTranched = address(lockupTranched); + assertEq(vars.actualLockupTranched, vars.expectedLockupTranched, "lockupTranched"); + + vars.actualName = constructedLT.name(); + vars.expectedName = defaults.NAME_BYTES32(); + assertEq(bytes32(abi.encodePacked(vars.actualName)), vars.expectedName, "name"); + + vars.actualMerkleRoot = constructedLT.MERKLE_ROOT(); + vars.expectedMerkleRoot = defaults.MERKLE_ROOT(); + assertEq(vars.actualMerkleRoot, vars.expectedMerkleRoot, "merkleRoot"); + + vars.actualTotalPercentage = constructedLT.TOTAL_PERCENTAGE(); + vars.expectedTotalPercentage = defaults.TOTAL_PERCENTAGE(); + assertEq(vars.actualTotalPercentage, vars.expectedTotalPercentage, "totalPercentage"); + + vars.actualTranchesWithPercentages = constructedLT.getTranchesWithPercentages(); + vars.expectedTranchesWithPercentages = defaults.tranchesWithPercentages(); + assertEq(vars.actualTranchesWithPercentages, vars.expectedTranchesWithPercentages, "tranchesWithPercentages"); + + vars.actualTransferable = constructedLT.TRANSFERABLE(); + vars.expectedTransferable = defaults.TRANSFERABLE(); + assertEq(vars.actualTransferable, vars.expectedTransferable, "transferable"); + } +} diff --git a/test/integration/merkle-lockup/lt/get-first-claim-time/getFirstClaimTime.t.sol b/test/integration/merkle-lockup/lt/get-first-claim-time/getFirstClaimTime.t.sol new file mode 100644 index 00000000..3e286f81 --- /dev/null +++ b/test/integration/merkle-lockup/lt/get-first-claim-time/getFirstClaimTime.t.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { MerkleLockup_Integration_Test } from "../../MerkleLockup.t.sol"; + +contract GetFirstClaimTime_Integration_Test is MerkleLockup_Integration_Test { + function setUp() public virtual override { + MerkleLockup_Integration_Test.setUp(); + } + + function test_GetFirstClaimTime_BeforeFirstClaim() external view { + uint256 firstClaimTime = merkleLT.getFirstClaimTime(); + assertEq(firstClaimTime, 0); + } + + modifier afterFirstClaim() { + // Make the first claim to set `_firstClaimTime`. + claimLT(); + _; + } + + function test_GetFirstClaimTime() external afterFirstClaim { + uint256 firstClaimTime = merkleLT.getFirstClaimTime(); + assertEq(firstClaimTime, block.timestamp); + } +} diff --git a/test/integration/merkle-lockup/lt/get-first-claim-time/getFirstClaimTime.tree b/test/integration/merkle-lockup/lt/get-first-claim-time/getFirstClaimTime.tree new file mode 100644 index 00000000..cab77842 --- /dev/null +++ b/test/integration/merkle-lockup/lt/get-first-claim-time/getFirstClaimTime.tree @@ -0,0 +1,5 @@ +getFirstClaimTime.t.sol +├── when the first claim has not been made +│ └── it should return 0 +└── when the first claim has been made + └── it should return the time of the first claim diff --git a/test/integration/merkle-lockup/lt/has-claimed/hasClaimed.t.sol b/test/integration/merkle-lockup/lt/has-claimed/hasClaimed.t.sol new file mode 100644 index 00000000..54b13ac6 --- /dev/null +++ b/test/integration/merkle-lockup/lt/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 HasClaimed_Integration_Test is MerkleLockup_Integration_Test { + function setUp() public virtual override { + MerkleLockup_Integration_Test.setUp(); + } + + function test_HasClaimed_IndexNotInTree() external { + uint256 indexNotInTree = 1337e18; + assertFalse(merkleLT.hasClaimed(indexNotInTree), "claimed"); + } + + modifier whenIndexInTree() { + _; + } + + function test_HasClaimed_NotClaimed() external whenIndexInTree { + assertFalse(merkleLT.hasClaimed(defaults.INDEX1()), "claimed"); + } + + modifier givenRecipientHasClaimed() { + claimLT(); + _; + } + + function test_HasClaimed() external whenIndexInTree givenRecipientHasClaimed { + assertTrue(merkleLT.hasClaimed(defaults.INDEX1()), "not claimed"); + } +} diff --git a/test/integration/merkle-lockup/lt/has-claimed/hasClaimed.tree b/test/integration/merkle-lockup/lt/has-claimed/hasClaimed.tree new file mode 100644 index 00000000..dcbcafe7 --- /dev/null +++ b/test/integration/merkle-lockup/lt/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/lt/has-expired/hasExpired.t.sol b/test/integration/merkle-lockup/lt/has-expired/hasExpired.t.sol new file mode 100644 index 00000000..93a47eaa --- /dev/null +++ b/test/integration/merkle-lockup/lt/has-expired/hasExpired.t.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { ISablierV2MerkleLT } from "src/interfaces/ISablierV2MerkleLT.sol"; + +import { MerkleLockup_Integration_Test } from "../../MerkleLockup.t.sol"; + +contract HasExpired_Integration_Test is MerkleLockup_Integration_Test { + function setUp() public virtual override { + MerkleLockup_Integration_Test.setUp(); + } + + function test_HasExpired_ExpirationZero() external { + ISablierV2MerkleLT testLockup = createMerkleLT({ expiration: 0 }); + assertFalse(testLockup.hasExpired(), "campaign expired"); + } + + modifier givenExpirationNotZero() { + _; + } + + function test_HasExpired_ExpirationLessThanBlockTimestamp() external view givenExpirationNotZero { + assertFalse(merkleLT.hasExpired(), "campaign expired"); + } + + function test_HasExpired_ExpirationEqualToBlockTimestamp() external givenExpirationNotZero { + vm.warp({ newTimestamp: defaults.EXPIRATION() }); + assertTrue(merkleLT.hasExpired(), "campaign not expired"); + } + + function test_HasExpired_ExpirationGreaterThanBlockTimestamp() external givenExpirationNotZero { + vm.warp({ newTimestamp: defaults.EXPIRATION() + 1 seconds }); + assertTrue(merkleLT.hasExpired(), "campaign not expired"); + } +} diff --git a/test/integration/merkle-lockup/lt/has-expired/hasExpired.tree b/test/integration/merkle-lockup/lt/has-expired/hasExpired.tree new file mode 100644 index 00000000..2a359a81 --- /dev/null +++ b/test/integration/merkle-lockup/lt/has-expired/hasExpired.tree @@ -0,0 +1,10 @@ +hasExpired.t.sol +├── given the expiration is zero +│ └── it should return false +└── given the expiration is not zero + ├── given the expiration is less than the block timestamp + │ └── it should return false + ├── given the expiration is equal to the block timestamp + │ └── it should return true + └── given the expiration is greater than the block timestamp + └── it should return true diff --git a/test/integration/merkle-streamer/MerkleStreamer.t.sol b/test/integration/merkle-streamer/MerkleStreamer.t.sol deleted file mode 100644 index fd6eeaaa..00000000 --- a/test/integration/merkle-streamer/MerkleStreamer.t.sol +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19; - -import { ISablierV2MerkleStreamerLL } from "src/interfaces/ISablierV2MerkleStreamerLL.sol"; - -import { Integration_Test } from "../Integration.t.sol"; - -abstract contract MerkleStreamer_Integration_Test is Integration_Test { - function setUp() public virtual override { - Integration_Test.setUp(); - - // Create the default Merkle streamer. - merkleStreamerLL = createMerkleStreamerLL(); - - // Fund the Merkle streamer. - deal({ token: address(asset), to: address(merkleStreamerLL), give: defaults.AGGREGATE_AMOUNT() }); - } - - function claimLL() internal returns (uint256) { - return merkleStreamerLL.claim({ - index: defaults.INDEX1(), - recipient: users.recipient1, - amount: defaults.CLAIM_AMOUNT(), - merkleProof: defaults.index1Proof() - }); - } - - function computeMerkleStreamerLLAddress() internal returns (address) { - return computeMerkleStreamerLLAddress(users.admin, defaults.MERKLE_ROOT(), defaults.EXPIRATION()); - } - - function computeMerkleStreamerLLAddress(address admin) internal returns (address) { - return computeMerkleStreamerLLAddress(admin, defaults.MERKLE_ROOT(), defaults.EXPIRATION()); - } - - function computeMerkleStreamerLLAddress(address admin, uint40 expiration) internal returns (address) { - return computeMerkleStreamerLLAddress(admin, defaults.MERKLE_ROOT(), expiration); - } - - function computeMerkleStreamerLLAddress(address admin, bytes32 merkleRoot) internal returns (address) { - return computeMerkleStreamerLLAddress(admin, merkleRoot, defaults.EXPIRATION()); - } - - function createMerkleStreamerLL() internal returns (ISablierV2MerkleStreamerLL) { - return createMerkleStreamerLL(users.admin, defaults.EXPIRATION()); - } - - function createMerkleStreamerLL(address admin) internal returns (ISablierV2MerkleStreamerLL) { - return createMerkleStreamerLL(admin, defaults.EXPIRATION()); - } - - function createMerkleStreamerLL(uint40 expiration) internal returns (ISablierV2MerkleStreamerLL) { - return createMerkleStreamerLL(users.admin, expiration); - } - - function createMerkleStreamerLL(address admin, uint40 expiration) internal returns (ISablierV2MerkleStreamerLL) { - return merkleStreamerFactory.createMerkleStreamerLL({ - initialAdmin: admin, - lockupLinear: lockupLinear, - asset: asset, - merkleRoot: defaults.MERKLE_ROOT(), - expiration: expiration, - cancelable: defaults.CANCELABLE(), - transferable: defaults.TRANSFERABLE(), - streamDurations: defaults.durations(), - ipfsCID: defaults.IPFS_CID(), - aggregateAmount: defaults.AGGREGATE_AMOUNT(), - recipientsCount: defaults.RECIPIENTS_COUNT() - }); - } -} diff --git a/test/integration/merkle-streamer/factory/create-merkle-streamer-ll/createMerkleStreamerLL.t.sol b/test/integration/merkle-streamer/factory/create-merkle-streamer-ll/createMerkleStreamerLL.t.sol deleted file mode 100644 index 73d04060..00000000 --- a/test/integration/merkle-streamer/factory/create-merkle-streamer-ll/createMerkleStreamerLL.t.sol +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; - -import { ISablierV2MerkleStreamerLL } from "src/interfaces/ISablierV2MerkleStreamerLL.sol"; - -import { MerkleStreamer_Integration_Test } from "../../MerkleStreamer.t.sol"; - -contract CreateMerkleStreamerLL_Integration_Test is MerkleStreamer_Integration_Test { - function setUp() public override { - MerkleStreamer_Integration_Test.setUp(); - } - - /// @dev This test works because a default Merkle streamer is deployed in {Integration_Test.setUp} - function test_RevertGiven_AlreadyDeployed() external { - bytes32 merkleRoot = defaults.MERKLE_ROOT(); - uint40 expiration = defaults.EXPIRATION(); - bool cancelable = defaults.CANCELABLE(); - bool transferable = defaults.TRANSFERABLE(); - LockupLinear.Durations memory streamDurations = defaults.durations(); - string memory ipfsCID = defaults.IPFS_CID(); - uint256 aggregateAmount = defaults.AGGREGATE_AMOUNT(); - uint256 recipientsCount = defaults.RECIPIENTS_COUNT(); - - vm.expectRevert(); - merkleStreamerFactory.createMerkleStreamerLL({ - initialAdmin: users.admin, - lockupLinear: lockupLinear, - asset: asset, - merkleRoot: merkleRoot, - expiration: expiration, - cancelable: cancelable, - transferable: transferable, - streamDurations: streamDurations, - ipfsCID: ipfsCID, - aggregateAmount: aggregateAmount, - recipientsCount: recipientsCount - }); - } - - modifier givenNotAlreadyDeployed() { - _; - } - - function testFuzz_CreateMerkleStreamerLL(address admin, uint40 expiration) external givenNotAlreadyDeployed { - vm.assume(admin != users.admin); - address expectedStreamerLL = computeMerkleStreamerLLAddress(admin, expiration); - - vm.expectEmit({ emitter: address(merkleStreamerFactory) }); - emit CreateMerkleStreamerLL({ - merkleStreamer: ISablierV2MerkleStreamerLL(expectedStreamerLL), - admin: admin, - lockupLinear: lockupLinear, - asset: asset, - merkleRoot: defaults.MERKLE_ROOT(), - expiration: expiration, - streamDurations: defaults.durations(), - cancelable: defaults.CANCELABLE(), - transferable: defaults.TRANSFERABLE(), - ipfsCID: defaults.IPFS_CID(), - aggregateAmount: defaults.AGGREGATE_AMOUNT(), - recipientsCount: defaults.RECIPIENTS_COUNT() - }); - - address actualStreamerLL = address(createMerkleStreamerLL(admin, expiration)); - - assertGt(actualStreamerLL.code.length, 0, "MerkleStreamerLL contract not created"); - assertEq(actualStreamerLL, expectedStreamerLL, "MerkleStreamerLL contract does not match computed address"); - } -} diff --git a/test/integration/merkle-streamer/factory/create-merkle-streamer-ll/createMerkleStreamerLL.tree b/test/integration/merkle-streamer/factory/create-merkle-streamer-ll/createMerkleStreamerLL.tree deleted file mode 100644 index 48eeaea2..00000000 --- a/test/integration/merkle-streamer/factory/create-merkle-streamer-ll/createMerkleStreamerLL.tree +++ /dev/null @@ -1,6 +0,0 @@ -createMerkleStreamerLL.t.sol -├── given the Merkle streamer has been deployed with CREATE2 -│ └── it should revert -└── given the Merkle streamer has not been deployed - ├── it should deploy the Merkle streamer - └── it should emit an {CreateMerkleStreamerLL} event diff --git a/test/integration/merkle-streamer/ll/clawback/clawback.t.sol b/test/integration/merkle-streamer/ll/clawback/clawback.t.sol deleted file mode 100644 index 69b28afb..00000000 --- a/test/integration/merkle-streamer/ll/clawback/clawback.t.sol +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { Errors as V2CoreErrors } from "@sablier/v2-core/src/libraries/Errors.sol"; -import { ud } from "@prb/math/src/UD60x18.sol"; - -import { Errors } from "src/libraries/Errors.sol"; - -import { MerkleStreamer_Integration_Test } from "../../MerkleStreamer.t.sol"; - -contract Clawback_Integration_Test is MerkleStreamer_Integration_Test { - function setUp() public virtual override { - MerkleStreamer_Integration_Test.setUp(); - } - - function test_RevertWhen_CallerNotAdmin() external { - changePrank({ msgSender: users.eve }); - vm.expectRevert(abi.encodeWithSelector(V2CoreErrors.CallerNotAdmin.selector, users.admin, users.eve)); - merkleStreamerLL.clawback({ to: users.eve, amount: 1 }); - } - - modifier whenCallerAdmin() { - changePrank({ msgSender: users.admin }); - _; - } - - modifier givenProtocolFeeZero() { - _; - } - - function test_RevertGiven_CampaignNotExpired() external whenCallerAdmin givenProtocolFeeZero { - vm.expectRevert( - abi.encodeWithSelector( - Errors.SablierV2MerkleStreamer_CampaignNotExpired.selector, block.timestamp, defaults.EXPIRATION() - ) - ); - merkleStreamerLL.clawback({ to: users.admin, amount: 1 }); - } - - modifier givenCampaignExpired() { - // Make a claim to have a different contract balance. - claimLL(); - vm.warp({ timestamp: defaults.EXPIRATION() + 1 seconds }); - _; - } - - function test_Clawback() external whenCallerAdmin givenProtocolFeeZero givenCampaignExpired { - test_Clawback(users.admin); - } - - modifier givenProtocolFeeNotZero() { - comptroller.setProtocolFee({ asset: asset, newProtocolFee: ud(0.03e18) }); - _; - } - - function testFuzz_Clawback_CampaignNotExpired(address to) external whenCallerAdmin givenProtocolFeeNotZero { - vm.assume(to != address(0)); - test_Clawback(to); - } - - function testFuzz_Clawback(address to) external whenCallerAdmin givenCampaignExpired givenProtocolFeeNotZero { - vm.assume(to != address(0)); - test_Clawback(to); - } - - function test_Clawback(address to) internal { - uint128 clawbackAmount = uint128(asset.balanceOf(address(merkleStreamerLL))); - expectCallToTransfer({ to: to, amount: clawbackAmount }); - vm.expectEmit({ emitter: address(merkleStreamerLL) }); - emit Clawback({ admin: users.admin, to: to, amount: clawbackAmount }); - merkleStreamerLL.clawback({ to: to, amount: clawbackAmount }); - } -} diff --git a/test/integration/merkle-streamer/ll/clawback/clawback.tree b/test/integration/merkle-streamer/ll/clawback/clawback.tree deleted file mode 100644 index 55f5c04e..00000000 --- a/test/integration/merkle-streamer/ll/clawback/clawback.tree +++ /dev/null @@ -1,17 +0,0 @@ -clawback.t.sol -├── when the caller is not the admin -│ └── it should revert -└── when the caller is the admin - ├── given the protocol fee is not greater than zero - │ ├── 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 - └── given the protocol fee is greater than zero - ├── given the campaign has not expired - │ ├── it should perform the ERC-20 transfer - │ └── it should emit a {Clawback} event - └── given the campaign has expired - ├── it should perform the ERC-20 transfer - └── it should emit a {Clawback} event diff --git a/test/integration/merkle-streamer/ll/has-expired/hasExpired.t.sol b/test/integration/merkle-streamer/ll/has-expired/hasExpired.t.sol deleted file mode 100644 index eca58123..00000000 --- a/test/integration/merkle-streamer/ll/has-expired/hasExpired.t.sol +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { ISablierV2MerkleStreamerLL } from "src/interfaces/ISablierV2MerkleStreamerLL.sol"; - -import { MerkleStreamer_Integration_Test } from "../../MerkleStreamer.t.sol"; - -contract HasExpired_Integration_Test is MerkleStreamer_Integration_Test { - function setUp() public virtual override { - MerkleStreamer_Integration_Test.setUp(); - } - - function test_HasExpired_ExpirationZero() external { - ISablierV2MerkleStreamerLL testStreamer = createMerkleStreamerLL({ expiration: 0 }); - assertFalse(testStreamer.hasExpired(), "campaign expired"); - } - - modifier whenExpirationNotZero() { - _; - } - - function test_HasExpired_ExpirationLessThanCurrentTime() external whenExpirationNotZero { - assertFalse(merkleStreamerLL.hasExpired(), "campaign expired"); - } - - function test_HasExpired_ExpirationEqualToCurrentTime() external whenExpirationNotZero { - vm.warp({ timestamp: defaults.EXPIRATION() }); - assertTrue(merkleStreamerLL.hasExpired(), "campaign not expired"); - } - - function test_HasExpired_ExpirationGreaterThanCurrentTime() external whenExpirationNotZero { - vm.warp({ timestamp: defaults.EXPIRATION() + 1 seconds }); - assertTrue(merkleStreamerLL.hasExpired(), "campaign not expired"); - } -} diff --git a/test/mocks/erc20/ERC20Mock.sol b/test/mocks/erc20/ERC20Mock.sol new file mode 100644 index 00000000..8cf2389d --- /dev/null +++ b/test/mocks/erc20/ERC20Mock.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract ERC20Mock is ERC20 { + constructor(string memory name, string memory symbol) ERC20(name, symbol) { } +} diff --git a/test/utils/.npmignore b/test/utils/.npmignore new file mode 100644 index 00000000..c607d373 --- /dev/null +++ b/test/utils/.npmignore @@ -0,0 +1 @@ +*.t.sol diff --git a/test/utils/ArrayBuilder.sol b/test/utils/ArrayBuilder.sol index 4ca431a7..c53ec64e 100644 --- a/test/utils/ArrayBuilder.sol +++ b/test/utils/ArrayBuilder.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; library ArrayBuilder { /// @notice Generates an ordered array of integers which starts at `firstStreamId` and ends at `firstStreamId + @@ -13,10 +13,8 @@ library ArrayBuilder { returns (uint256[] memory streamIds) { streamIds = new uint256[](batchSize); - unchecked { - for (uint256 i = 0; i < batchSize; ++i) { - streamIds[i] = firstStreamId + i; - } + for (uint256 i = 0; i < batchSize; ++i) { + streamIds[i] = firstStreamId + i; } } } diff --git a/test/utils/Assertions.sol b/test/utils/Assertions.sol new file mode 100644 index 00000000..d4fa320d --- /dev/null +++ b/test/utils/Assertions.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable event-name-camelcase +pragma solidity >=0.8.22; + +import { PRBMathAssertions } from "@prb/math/test/utils/Assertions.sol"; + +import { MerkleLT } from "src/types/DataTypes.sol"; + +abstract contract Assertions is PRBMathAssertions { + event log_named_array(string key, MerkleLT.TrancheWithPercentage[] tranchesWithPercentages); + + /// @dev Compares two {MerkleLT.TrancheWithPercentage} arrays. + function assertEq(MerkleLT.TrancheWithPercentage[] memory a, MerkleLT.TrancheWithPercentage[] memory b) internal { + if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) { + emit log("Error: a == b not satisfied [MerkleLT.TrancheWithPercentage[]]"); + emit log_named_array(" Left", a); + emit log_named_array(" Right", b); + fail(); + } + } + + /// @dev Compares two {MerkleLT.TrancheWithPercentage} arrays. + function assertEq( + MerkleLT.TrancheWithPercentage[] memory a, + MerkleLT.TrancheWithPercentage[] memory b, + string memory err + ) + internal + { + if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) { + emit log_named_string("Error", err); + assertEq(a, b); + } + } +} diff --git a/test/utils/BaseScript.t.sol b/test/utils/BaseScript.t.sol index 28c15073..6fa78951 100644 --- a/test/utils/BaseScript.t.sol +++ b/test/utils/BaseScript.t.sol @@ -1,19 +1,19 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; -import { PRBTest } from "@prb/test/src/PRBTest.sol"; +import { StdAssertions } from "forge-std/src/StdAssertions.sol"; import { BaseScript } from "script/Base.s.sol"; -contract BaseScript_Test is PRBTest { +contract BaseScript_Test is StdAssertions { using Strings for uint256; BaseScript internal baseScript = new BaseScript(); - function test_ConstructCreate2Salt() public { + function test_ConstructCreate2Salt() public view { string memory chainId = block.chainid.toString(); - string memory version = "1.1.1"; + string memory version = "1.2.0"; string memory salt = string.concat("ChainID ", chainId, ", Version ", version); bytes32 actualSalt = baseScript.constructCreate2Salt(); diff --git a/test/utils/BatchBuilder.sol b/test/utils/BatchBuilder.sol deleted file mode 100644 index 361a5f0a..00000000 --- a/test/utils/BatchBuilder.sol +++ /dev/null @@ -1,165 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; - -import { LockupDynamic, LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; - -import { Batch } from "../../src/types/DataTypes.sol"; - -library BatchBuilder { - /// @notice Generates an array containing `batchSize` copies of `batchSingle`. - function fillBatch( - Batch.CreateWithDeltas memory batchSingle, - uint256 batchSize - ) - internal - pure - returns (Batch.CreateWithDeltas[] memory batch) - { - batch = new Batch.CreateWithDeltas[](batchSize); - unchecked { - for (uint256 i = 0; i < batchSize; ++i) { - batch[i] = batchSingle; - } - } - } - - /// @notice Turns the `params` into an array of `Batch.CreateWithDeltas` structs. - function fillBatch( - LockupDynamic.CreateWithDeltas memory params, - uint256 batchSize - ) - internal - pure - returns (Batch.CreateWithDeltas[] memory batch) - { - batch = new Batch.CreateWithDeltas[](batchSize); - Batch.CreateWithDeltas memory batchSingle = Batch.CreateWithDeltas({ - broker: params.broker, - cancelable: params.cancelable, - recipient: params.recipient, - segments: params.segments, - sender: params.sender, - totalAmount: params.totalAmount, - transferable: params.transferable - }); - batch = fillBatch(batchSingle, batchSize); - } - - /// @notice Generates an array containing `batchSize` copies of `batchSingle`. - function fillBatch( - Batch.CreateWithDurations memory batchSingle, - uint256 batchSize - ) - internal - pure - returns (Batch.CreateWithDurations[] memory batch) - { - batch = new Batch.CreateWithDurations[](batchSize); - unchecked { - for (uint256 i = 0; i < batchSize; ++i) { - batch[i] = batchSingle; - } - } - } - - /// @notice Turns the `params` into an array of `Batch.CreateWithDurations` structs. - function fillBatch( - LockupLinear.CreateWithDurations memory params, - uint256 batchSize - ) - internal - pure - returns (Batch.CreateWithDurations[] memory batch) - { - batch = new Batch.CreateWithDurations[](batchSize); - Batch.CreateWithDurations memory batchSingle = Batch.CreateWithDurations({ - broker: params.broker, - cancelable: params.cancelable, - durations: params.durations, - recipient: params.recipient, - sender: params.sender, - totalAmount: params.totalAmount, - transferable: params.transferable - }); - batch = fillBatch(batchSingle, batchSize); - } - - /// @notice Generates an array containing `batchSize` copies of `batchSingle`. - function fillBatch( - Batch.CreateWithMilestones memory batchSingle, - uint256 batchSize - ) - internal - pure - returns (Batch.CreateWithMilestones[] memory batch) - { - batch = new Batch.CreateWithMilestones[](batchSize); - unchecked { - for (uint256 i = 0; i < batchSize; ++i) { - batch[i] = batchSingle; - } - } - } - - /// @notice Turns the `params` into an array of `Batch.CreateWithMilestones` structs. - function fillBatch( - LockupDynamic.CreateWithMilestones memory params, - uint256 batchSize - ) - internal - pure - returns (Batch.CreateWithMilestones[] memory batch) - { - batch = new Batch.CreateWithMilestones[](batchSize); - Batch.CreateWithMilestones memory batchSingle = Batch.CreateWithMilestones({ - broker: params.broker, - cancelable: params.cancelable, - recipient: params.recipient, - segments: params.segments, - sender: params.sender, - startTime: params.startTime, - totalAmount: params.totalAmount, - transferable: params.transferable - }); - batch = fillBatch(batchSingle, batchSize); - } - - /// @notice Generates an array containing `batchSize` copies of `batchSingle`. - function fillBatch( - Batch.CreateWithRange memory batchSingle, - uint256 batchSize - ) - internal - pure - returns (Batch.CreateWithRange[] memory batch) - { - batch = new Batch.CreateWithRange[](batchSize); - unchecked { - for (uint256 i = 0; i < batchSize; ++i) { - batch[i] = batchSingle; - } - } - } - - /// @notice Turns the `params` into an array of `Batch.CreateWithRange` structs. - function fillBatch( - LockupLinear.CreateWithRange memory params, - uint256 batchSize - ) - internal - pure - returns (Batch.CreateWithRange[] memory batch) - { - batch = new Batch.CreateWithRange[](batchSize); - Batch.CreateWithRange memory batchSingle = Batch.CreateWithRange({ - broker: params.broker, - cancelable: params.cancelable, - range: params.range, - recipient: params.recipient, - sender: params.sender, - totalAmount: params.totalAmount, - transferable: params.transferable - }); - batch = fillBatch(batchSingle, batchSize); - } -} diff --git a/test/utils/BatchLockupBuilder.sol b/test/utils/BatchLockupBuilder.sol new file mode 100644 index 00000000..3cf567fa --- /dev/null +++ b/test/utils/BatchLockupBuilder.sol @@ -0,0 +1,232 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { LockupDynamic, LockupLinear, LockupTranched } from "@sablier/v2-core/src/types/DataTypes.sol"; + +import { BatchLockup } from "../../src/types/DataTypes.sol"; + +library BatchLockupBuilder { + /// @notice Generates an array containing `batchSize` copies of `batchSingle`. + function fillBatch( + BatchLockup.CreateWithDurationsLD memory batchSingle, + uint256 batchSize + ) + internal + pure + returns (BatchLockup.CreateWithDurationsLD[] memory batch) + { + batch = new BatchLockup.CreateWithDurationsLD[](batchSize); + for (uint256 i = 0; i < batchSize; ++i) { + batch[i] = batchSingle; + } + } + + /// @notice Turns the `params` into an array of {BatchLockup.CreateWithDurationsLD} structs. + function fillBatch( + LockupDynamic.CreateWithDurations memory params, + uint256 batchSize + ) + internal + pure + returns (BatchLockup.CreateWithDurationsLD[] memory batch) + { + batch = new BatchLockup.CreateWithDurationsLD[](batchSize); + BatchLockup.CreateWithDurationsLD memory batchSingle = BatchLockup.CreateWithDurationsLD({ + sender: params.sender, + recipient: params.recipient, + totalAmount: params.totalAmount, + cancelable: params.cancelable, + transferable: params.transferable, + segments: params.segments, + broker: params.broker + }); + batch = fillBatch(batchSingle, batchSize); + } + + /// @notice Generates an array containing `batchSize` copies of `batchSingle`. + function fillBatch( + BatchLockup.CreateWithDurationsLL memory batchSingle, + uint256 batchSize + ) + internal + pure + returns (BatchLockup.CreateWithDurationsLL[] memory batch) + { + batch = new BatchLockup.CreateWithDurationsLL[](batchSize); + for (uint256 i = 0; i < batchSize; ++i) { + batch[i] = batchSingle; + } + } + + /// @notice Turns the `params` into an array of {BatchLockup.CreateWithDurationsLL} structs. + function fillBatch( + LockupLinear.CreateWithDurations memory params, + uint256 batchSize + ) + internal + pure + returns (BatchLockup.CreateWithDurationsLL[] memory batch) + { + batch = new BatchLockup.CreateWithDurationsLL[](batchSize); + BatchLockup.CreateWithDurationsLL memory batchSingle = BatchLockup.CreateWithDurationsLL({ + sender: params.sender, + recipient: params.recipient, + totalAmount: params.totalAmount, + cancelable: params.cancelable, + transferable: params.transferable, + durations: params.durations, + broker: params.broker + }); + batch = fillBatch(batchSingle, batchSize); + } + + /// @notice Generates an array containing `batchSize` copies of `batchSingle`. + function fillBatch( + BatchLockup.CreateWithDurationsLT memory batchSingle, + uint256 batchSize + ) + internal + pure + returns (BatchLockup.CreateWithDurationsLT[] memory batch) + { + batch = new BatchLockup.CreateWithDurationsLT[](batchSize); + for (uint256 i = 0; i < batchSize; ++i) { + batch[i] = batchSingle; + } + } + + /// @notice Turns the `params` into an array of {BatchLockup.CreateWithDurationsLT} structs. + function fillBatch( + LockupTranched.CreateWithDurations memory params, + uint256 batchSize + ) + internal + pure + returns (BatchLockup.CreateWithDurationsLT[] memory batch) + { + batch = new BatchLockup.CreateWithDurationsLT[](batchSize); + BatchLockup.CreateWithDurationsLT memory batchSingle = BatchLockup.CreateWithDurationsLT({ + sender: params.sender, + recipient: params.recipient, + totalAmount: params.totalAmount, + cancelable: params.cancelable, + transferable: params.transferable, + tranches: params.tranches, + broker: params.broker + }); + batch = fillBatch(batchSingle, batchSize); + } + + /// @notice Generates an array containing `batchSize` copies of `batchSingle`. + function fillBatch( + BatchLockup.CreateWithTimestampsLD memory batchSingle, + uint256 batchSize + ) + internal + pure + returns (BatchLockup.CreateWithTimestampsLD[] memory batch) + { + batch = new BatchLockup.CreateWithTimestampsLD[](batchSize); + for (uint256 i = 0; i < batchSize; ++i) { + batch[i] = batchSingle; + } + } + + /// @notice Turns the `params` into an array of {BatchLockup.CreateWithTimestampsLDs} structs. + function fillBatch( + LockupDynamic.CreateWithTimestamps memory params, + uint256 batchSize + ) + internal + pure + returns (BatchLockup.CreateWithTimestampsLD[] memory batch) + { + batch = new BatchLockup.CreateWithTimestampsLD[](batchSize); + BatchLockup.CreateWithTimestampsLD memory batchSingle = BatchLockup.CreateWithTimestampsLD({ + sender: params.sender, + recipient: params.recipient, + totalAmount: params.totalAmount, + cancelable: params.cancelable, + transferable: params.transferable, + startTime: params.startTime, + segments: params.segments, + broker: params.broker + }); + batch = fillBatch(batchSingle, batchSize); + } + + /// @notice Generates an array containing `batchSize` copies of `batchSingle`. + function fillBatch( + BatchLockup.CreateWithTimestampsLL memory batchSingle, + uint256 batchSize + ) + internal + pure + returns (BatchLockup.CreateWithTimestampsLL[] memory batch) + { + batch = new BatchLockup.CreateWithTimestampsLL[](batchSize); + for (uint256 i = 0; i < batchSize; ++i) { + batch[i] = batchSingle; + } + } + + /// @notice Turns the `params` into an array of {BatchLockup.CreateWithTimestampsLL} structs. + function fillBatch( + LockupLinear.CreateWithTimestamps memory params, + uint256 batchSize + ) + internal + pure + returns (BatchLockup.CreateWithTimestampsLL[] memory batch) + { + batch = new BatchLockup.CreateWithTimestampsLL[](batchSize); + BatchLockup.CreateWithTimestampsLL memory batchSingle = BatchLockup.CreateWithTimestampsLL({ + sender: params.sender, + recipient: params.recipient, + totalAmount: params.totalAmount, + cancelable: params.cancelable, + transferable: params.transferable, + timestamps: params.timestamps, + broker: params.broker + }); + batch = fillBatch(batchSingle, batchSize); + } + + /// @notice Generates an array containing `batchSize` copies of `batchSingle`. + function fillBatch( + BatchLockup.CreateWithTimestampsLT memory batchSingle, + uint256 batchSize + ) + internal + pure + returns (BatchLockup.CreateWithTimestampsLT[] memory batch) + { + batch = new BatchLockup.CreateWithTimestampsLT[](batchSize); + for (uint256 i = 0; i < batchSize; ++i) { + batch[i] = batchSingle; + } + } + + /// @notice Turns the `params` into an array of {BatchLockup.CreateWithTimestampsLT} structs. + function fillBatch( + LockupTranched.CreateWithTimestamps memory params, + uint256 batchSize + ) + internal + pure + returns (BatchLockup.CreateWithTimestampsLT[] memory batch) + { + batch = new BatchLockup.CreateWithTimestampsLT[](batchSize); + BatchLockup.CreateWithTimestampsLT memory batchSingle = BatchLockup.CreateWithTimestampsLT({ + sender: params.sender, + recipient: params.recipient, + totalAmount: params.totalAmount, + cancelable: params.cancelable, + transferable: params.transferable, + startTime: params.startTime, + tranches: params.tranches, + broker: params.broker + }); + batch = fillBatch(batchSingle, batchSize); + } +} diff --git a/test/utils/Defaults.sol b/test/utils/Defaults.sol index b468c84e..93c664de 100644 --- a/test/utils/Defaults.sol +++ b/test/utils/Defaults.sol @@ -1,16 +1,16 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19 <0.9.0; +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 { ud2x18 } from "@prb/math/src/UD2x18.sol"; -import { UD60x18 } from "@prb/math/src/UD60x18.sol"; -import { Broker, LockupDynamic, LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; +import { ud2x18, uUNIT } from "@prb/math/src/UD2x18.sol"; +import { UD60x18, ud } from "@prb/math/src/UD60x18.sol"; +import { Broker, LockupDynamic, LockupLinear, LockupTranched } from "@sablier/v2-core/src/types/DataTypes.sol"; -import { Batch } from "src/types/DataTypes.sol"; +import { BatchLockup, MerkleLockup, MerkleLT } from "src/types/DataTypes.sol"; import { ArrayBuilder } from "./ArrayBuilder.sol"; -import { BatchBuilder } from "./BatchBuilder.sol"; +import { BatchLockupBuilder } from "./BatchLockupBuilder.sol"; import { Merkle } from "./Murky.sol"; import { MerkleBuilder } from "./MerkleBuilder.sol"; import { Users } from "./Types.sol"; @@ -29,9 +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; 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 uint40 public immutable START_TIME; uint40 public constant TOTAL_DURATION = 10_000 seconds; @@ -39,22 +37,26 @@ contract Defaults is Merkle { uint128 public constant WITHDRAW_AMOUNT = 2500e18; /*////////////////////////////////////////////////////////////////////////// - MERKLE-STREAMER + MERKLE-LOCKUP //////////////////////////////////////////////////////////////////////////*/ - uint256 public constant AGGREGATE_AMOUNT = CLAIM_AMOUNT * RECIPIENTS_COUNT; + uint256 public constant AGGREGATE_AMOUNT = CLAIM_AMOUNT * RECIPIENT_COUNT; bool public constant CANCELABLE = false; uint128 public constant CLAIM_AMOUNT = 10_000e18; uint40 public immutable EXPIRATION; + uint40 public immutable FIRST_CLAIM_TIME; uint256 public constant INDEX1 = 1; uint256 public constant INDEX2 = 2; uint256 public constant INDEX3 = 3; uint256 public constant INDEX4 = 4; string public constant IPFS_CID = "QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR"; - uint256 public constant RECIPIENTS_COUNT = 4; - bool public constant TRANSFERABLE = false; - uint256[] public LEAVES = new uint256[](RECIPIENTS_COUNT); + uint256[] public LEAVES = new uint256[](RECIPIENT_COUNT); + uint256 public constant RECIPIENT_COUNT = 4; bytes32 public immutable MERKLE_ROOT; + string public constant NAME = "Airdrop Campaign"; + bytes32 public constant NAME_BYTES32 = bytes32(abi.encodePacked("Airdrop Campaign")); + uint64 public constant TOTAL_PERCENTAGE = uUNIT; + bool public constant TRANSFERABLE = false; /*////////////////////////////////////////////////////////////////////////// VARIABLES @@ -76,6 +78,7 @@ contract Defaults is Merkle { CLIFF_TIME = START_TIME + CLIFF_DURATION; END_TIME = START_TIME + TOTAL_DURATION; EXPIRATION = uint40(block.timestamp) + 12 weeks; + FIRST_CLAIM_TIME = uint40(block.timestamp); // Initialize the Merkle tree. LEAVES[0] = MerkleBuilder.computeLeaf(INDEX1, users.recipient1, CLAIM_AMOUNT); @@ -86,8 +89,12 @@ contract Defaults is Merkle { MERKLE_ROOT = getRoot(LEAVES.toBytes32()); } + function getLeaves() public view returns (uint256[] memory) { + return LEAVES; + } + /*////////////////////////////////////////////////////////////////////////// - MERKLE-STREAMER + MERKLE-LOCKUP //////////////////////////////////////////////////////////////////////////*/ function index1Proof() public view returns (bytes32[] memory) { @@ -114,6 +121,44 @@ contract Defaults is Merkle { return getProof(LEAVES.toBytes32(), pos); } + function baseParams() public view returns (MerkleLockup.ConstructorParams memory) { + return baseParams(users.admin, asset, EXPIRATION, MERKLE_ROOT); + } + + function baseParams( + address admin, + IERC20 asset_, + uint40 expiration, + bytes32 merkleRoot + ) + public + pure + returns (MerkleLockup.ConstructorParams memory) + { + return MerkleLockup.ConstructorParams({ + asset: asset_, + cancelable: CANCELABLE, + expiration: expiration, + initialAdmin: admin, + ipfsCID: IPFS_CID, + merkleRoot: merkleRoot, + name: NAME, + transferable: TRANSFERABLE + }); + } + + function tranchesWithPercentages() + public + pure + returns (MerkleLT.TrancheWithPercentage[] memory tranchesWithPercentages_) + { + tranchesWithPercentages_ = new MerkleLT.TrancheWithPercentage[](2); + tranchesWithPercentages_[0] = + MerkleLT.TrancheWithPercentage({ unlockPercentage: ud2x18(0.25e18), duration: 2500 seconds }); + tranchesWithPercentages_[1] = + MerkleLT.TrancheWithPercentage({ unlockPercentage: ud2x18(0.75e18), duration: 7500 seconds }); + } + /*////////////////////////////////////////////////////////////////////////// SABLIER-V2-LOCKUP //////////////////////////////////////////////////////////////////////////*/ @@ -135,162 +180,294 @@ contract Defaults is Merkle { SABLIER-V2-LOCKUP-DYNAMIC //////////////////////////////////////////////////////////////////////////*/ - function createWithDeltas() public view returns (LockupDynamic.CreateWithDeltas memory) { - return createWithDeltas(asset); + function createWithDurationsLD() public view returns (LockupDynamic.CreateWithDurations memory) { + return createWithDurationsLD(asset, PER_STREAM_AMOUNT, segmentsWithDurations()); } - function createWithDeltas(IERC20 asset_) public view returns (LockupDynamic.CreateWithDeltas memory) { - return LockupDynamic.CreateWithDeltas({ + function createWithDurationsLD( + IERC20 asset_, + uint128 totalAmount_, + LockupDynamic.SegmentWithDuration[] memory segments_ + ) + public + view + returns (LockupDynamic.CreateWithDurations memory) + { + return LockupDynamic.CreateWithDurations({ + sender: users.alice, + recipient: users.recipient0, + totalAmount: totalAmount_, asset: asset_, - broker: broker(), cancelable: true, - recipient: users.recipient0, - segments: segmentsWithDeltas(), - sender: users.alice, - totalAmount: PER_STREAM_AMOUNT, - transferable: true + transferable: true, + segments: segments_, + broker: broker() }); } - function createWithMilestones() public view returns (LockupDynamic.CreateWithMilestones memory) { - return createWithMilestones(asset); + function createWithTimestampsLD() public view returns (LockupDynamic.CreateWithTimestamps memory) { + return createWithTimestampsLD(asset, PER_STREAM_AMOUNT, segments()); } - function createWithMilestones(IERC20 asset_) public view returns (LockupDynamic.CreateWithMilestones memory) { - return LockupDynamic.CreateWithMilestones({ + function createWithTimestampsLD( + IERC20 asset_, + uint128 totalAmount_, + LockupDynamic.Segment[] memory segments_ + ) + public + view + returns (LockupDynamic.CreateWithTimestamps memory) + { + return LockupDynamic.CreateWithTimestamps({ + sender: users.alice, + recipient: users.recipient0, + totalAmount: totalAmount_, asset: asset_, - broker: broker(), cancelable: true, - recipient: users.recipient0, - segments: segments(), - sender: users.alice, + transferable: true, startTime: START_TIME, - totalAmount: PER_STREAM_AMOUNT, - transferable: true + segments: segments_, + broker: broker() }); } - 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_) { + /// @dev Returns a batch of {LockupDynamic.Segment} parameters. + function segments() public view returns (LockupDynamic.Segment[] memory segments_) { segments_ = new LockupDynamic.Segment[](2); segments_[0] = LockupDynamic.Segment({ amount: 2500e18, exponent: ud2x18(3.14e18), - milestone: START_TIME + CLIFF_DURATION + timestamp: START_TIME + CLIFF_DURATION }); segments_[1] = LockupDynamic.Segment({ amount: 7500e18, exponent: ud2x18(3.14e18), - milestone: START_TIME + TOTAL_DURATION + timestamp: START_TIME + TOTAL_DURATION }); } - /// @dev Returns a batch of `LockupDynamic.SegmentWithDelta` parameters. - function segmentsWithDeltas() public pure returns (LockupDynamic.SegmentWithDelta[] memory) { - return segmentsWithDeltas({ amount0: 2500e18, amount1: 7500e18 }); + /// @dev Returns a batch of {LockupDynamic.SegmentWithDuration} parameters. + function segmentsWithDurations() public pure returns (LockupDynamic.SegmentWithDuration[] memory) { + return segmentsWithDurations({ amount0: 2500e18, amount1: 7500e18 }); } - /// @dev Returns a batch of `LockupDynamic.SegmentWithDelta` parameters. - function segmentsWithDeltas( + /// @dev Returns a batch of {LockupDynamic.SegmentWithDuration} parameters. + function segmentsWithDurations( uint128 amount0, uint128 amount1 ) public pure - returns (LockupDynamic.SegmentWithDelta[] memory segments_) + returns (LockupDynamic.SegmentWithDuration[] memory segments_) { - segments_ = new LockupDynamic.SegmentWithDelta[](2); + segments_ = new LockupDynamic.SegmentWithDuration[](2); segments_[0] = - LockupDynamic.SegmentWithDelta({ amount: amount0, exponent: ud2x18(3.14e18), delta: 2500 seconds }); + LockupDynamic.SegmentWithDuration({ amount: amount0, exponent: ud2x18(3.14e18), duration: 2500 seconds }); segments_[1] = - LockupDynamic.SegmentWithDelta({ amount: amount1, exponent: ud2x18(3.14e18), delta: 7500 seconds }); + LockupDynamic.SegmentWithDuration({ amount: amount1, exponent: ud2x18(3.14e18), duration: 7500 seconds }); } /*////////////////////////////////////////////////////////////////////////// SABLIER-V2-LOCKUP-LINEAR //////////////////////////////////////////////////////////////////////////*/ - function createWithDurations() public view returns (LockupLinear.CreateWithDurations memory) { - return createWithDurations(asset); + function createWithDurationsLL() public view returns (LockupLinear.CreateWithDurations memory) { + return createWithDurationsLL(asset); } - function createWithDurations(IERC20 asset_) public view returns (LockupLinear.CreateWithDurations memory) { + function createWithDurationsLL(IERC20 asset_) public view returns (LockupLinear.CreateWithDurations memory) { return LockupLinear.CreateWithDurations({ + sender: users.alice, + recipient: users.recipient0, + totalAmount: PER_STREAM_AMOUNT, asset: asset_, - broker: broker(), cancelable: true, + transferable: true, durations: durations(), - recipient: users.recipient0, + broker: broker() + }); + } + + function createWithTimestampsLL() public view returns (LockupLinear.CreateWithTimestamps memory) { + return createWithTimestampsLL(asset); + } + + function createWithTimestampsLL(IERC20 asset_) public view returns (LockupLinear.CreateWithTimestamps memory) { + return LockupLinear.CreateWithTimestamps({ sender: users.alice, + recipient: users.recipient0, totalAmount: PER_STREAM_AMOUNT, - transferable: true + asset: asset_, + cancelable: true, + transferable: true, + timestamps: linearTimestamps(), + broker: broker() }); } - function createWithRange() public view returns (LockupLinear.CreateWithRange memory) { - return createWithRange(asset); + function durations() public pure returns (LockupLinear.Durations memory) { + return LockupLinear.Durations({ cliff: CLIFF_DURATION, total: TOTAL_DURATION }); + } + + function linearTimestamps() private view returns (LockupLinear.Timestamps memory) { + return LockupLinear.Timestamps({ start: START_TIME, cliff: CLIFF_TIME, end: END_TIME }); } - function createWithRange(IERC20 asset_) public view returns (LockupLinear.CreateWithRange memory) { - return LockupLinear.CreateWithRange({ + /*////////////////////////////////////////////////////////////////////////// + SABLIER-V2-LOCKUP-TRANCHED + //////////////////////////////////////////////////////////////////////////*/ + + function createWithDurationsLT() public view returns (LockupTranched.CreateWithDurations memory) { + return createWithDurationsLT(asset, PER_STREAM_AMOUNT, tranchesWithDurations()); + } + + function createWithDurationsLT( + IERC20 asset_, + uint128 totalAmount_, + LockupTranched.TrancheWithDuration[] memory tranches_ + ) + public + view + returns (LockupTranched.CreateWithDurations memory) + { + return LockupTranched.CreateWithDurations({ + sender: users.alice, + recipient: users.recipient0, + totalAmount: totalAmount_, asset: asset_, - broker: broker(), cancelable: true, - range: linearRange(), - recipient: users.recipient0, + transferable: true, + tranches: tranches_, + broker: broker() + }); + } + + function createWithTimestampsLT() public view returns (LockupTranched.CreateWithTimestamps memory) { + return createWithTimestampsLT(asset, PER_STREAM_AMOUNT, tranches()); + } + + function createWithTimestampsLT( + IERC20 asset_, + uint128 totalAmount_, + LockupTranched.Tranche[] memory tranches_ + ) + public + view + returns (LockupTranched.CreateWithTimestamps memory) + { + return LockupTranched.CreateWithTimestamps({ sender: users.alice, - totalAmount: PER_STREAM_AMOUNT, - transferable: true + recipient: users.recipient0, + totalAmount: totalAmount_, + asset: asset_, + cancelable: true, + transferable: true, + startTime: START_TIME, + tranches: tranches_, + broker: broker() }); } - function durations() public pure returns (LockupLinear.Durations memory) { - return LockupLinear.Durations({ cliff: CLIFF_DURATION, total: TOTAL_DURATION }); + function tranches() public view returns (LockupTranched.Tranche[] memory tranches_) { + tranches_ = new LockupTranched.Tranche[](2); + tranches_[0] = LockupTranched.Tranche({ amount: 2500e18, timestamp: uint40(block.timestamp) + CLIFF_DURATION }); + tranches_[1] = LockupTranched.Tranche({ amount: 7500e18, timestamp: uint40(block.timestamp) + TOTAL_DURATION }); + } + + /// @dev Mirros the logic from {SablierV2MerkleLT._calculateTranches}. + function tranches(uint128 totalAmount) public view returns (LockupTranched.Tranche[] memory tranches_) { + tranches_ = tranches(); + + uint128 amount0 = ud(totalAmount).mul(tranchesWithPercentages()[0].unlockPercentage.intoUD60x18()).intoUint128(); + uint128 amount1 = ud(totalAmount).mul(tranchesWithPercentages()[1].unlockPercentage.intoUD60x18()).intoUint128(); + + tranches_[0].amount = amount0; + tranches_[1].amount = amount1; + + uint128 amountsSum = amount0 + amount1; + + if (amountsSum != totalAmount) { + tranches_[1].amount += totalAmount - amountsSum; + } } - function linearRange() private view returns (LockupLinear.Range memory) { - return LockupLinear.Range({ start: START_TIME, cliff: CLIFF_TIME, end: END_TIME }); + /// @dev Returns a batch of {LockupTranched.TrancheWithDuration} parameters. + function tranchesWithDurations() public pure returns (LockupTranched.TrancheWithDuration[] memory) { + return tranchesWithDurations({ amount0: 2500e18, amount1: 7500e18 }); + } + + /// @dev Returns a batch of {LockupTranched.TrancheWithDuration} parameters. + function tranchesWithDurations( + uint128 amount0, + uint128 amount1 + ) + public + pure + returns (LockupTranched.TrancheWithDuration[] memory segments_) + { + segments_ = new LockupTranched.TrancheWithDuration[](2); + segments_[0] = LockupTranched.TrancheWithDuration({ amount: amount0, duration: 2500 seconds }); + segments_[1] = LockupTranched.TrancheWithDuration({ amount: amount1, duration: 7500 seconds }); } /*////////////////////////////////////////////////////////////////////////// - BATCH + BATCH-LOCKUP //////////////////////////////////////////////////////////////////////////*/ - /// @dev Returns a default-size batch of `Batch.CreateWithDeltas` parameters. - function batchCreateWithDeltas() public view returns (Batch.CreateWithDeltas[] memory batch) { - batch = BatchBuilder.fillBatch(createWithDeltas(), BATCH_SIZE); + /// @dev Returns a default-size batch of {BatchLockup.CreateWithDurationsLD} parameters. + function batchCreateWithDurationsLD() public view returns (BatchLockup.CreateWithDurationsLD[] memory batch) { + batch = BatchLockupBuilder.fillBatch(createWithDurationsLD(), BATCH_SIZE); + } + + /// @dev Returns a default-size batch of {BatchLockup.CreateWithDurationsLL} parameters. + function batchCreateWithDurationsLL() public view returns (BatchLockup.CreateWithDurationsLL[] memory batch) { + batch = BatchLockupBuilder.fillBatch(createWithDurationsLL(), BATCH_SIZE); } - /// @dev Returns a default-size batch of `Batch.CreateWithDurations` parameters. - function batchCreateWithDurations() public view returns (Batch.CreateWithDurations[] memory batch) { - batch = BatchBuilder.fillBatch(createWithDurations(), BATCH_SIZE); + /// @dev Returns a default-size batch of {BatchLockup.CreateWithDurationsLT} parameters. + function batchCreateWithDurationsLT() public view returns (BatchLockup.CreateWithDurationsLT[] memory batch) { + batch = BatchLockupBuilder.fillBatch(createWithDurationsLT(), BATCH_SIZE); } - /// @dev Returns a default-size batch of `Batch.CreateWithMilestones` parameters. - function batchCreateWithMilestones() public view returns (Batch.CreateWithMilestones[] memory batch) { - batch = batchCreateWithMilestones(BATCH_SIZE); + /// @dev Returns a default-size batch of {BatchLockup.CreateWithTimestampsLD} parameters. + function batchCreateWithTimestampsLD() public view returns (BatchLockup.CreateWithTimestampsLD[] memory batch) { + batch = batchCreateWithTimestampsLD(BATCH_SIZE); } - /// @dev Returns a batch of `Batch.CreateWithMilestones` parameters. - function batchCreateWithMilestones(uint256 batchSize) + /// @dev Returns a batch of {BatchLockup.CreateWithTimestampsLD} parameters. + function batchCreateWithTimestampsLD(uint256 batchSize) public view - returns (Batch.CreateWithMilestones[] memory batch) + returns (BatchLockup.CreateWithTimestampsLD[] memory batch) { - batch = BatchBuilder.fillBatch(createWithMilestones(), batchSize); + batch = BatchLockupBuilder.fillBatch(createWithTimestampsLD(), batchSize); } - /// @dev Returns a default-size batch of `Batch.CreateWithRange` parameters. - function batchCreateWithRange() public view returns (Batch.CreateWithRange[] memory batch) { - batch = batchCreateWithRange(BATCH_SIZE); + /// @dev Returns a default-size batch of {BatchLockup.CreateWithTimestampsLL} parameters. + function batchCreateWithTimestampsLL() public view returns (BatchLockup.CreateWithTimestampsLL[] memory batch) { + batch = batchCreateWithTimestampsLL(BATCH_SIZE); } - /// @dev Returns a batch of `Batch.CreateWithRange` parameters. - function batchCreateWithRange(uint256 batchSize) public view returns (Batch.CreateWithRange[] memory batch) { - batch = BatchBuilder.fillBatch(createWithRange(), batchSize); + /// @dev Returns a batch of {BatchLockup.CreateWithTimestampsLL} parameters. + function batchCreateWithTimestampsLL(uint256 batchSize) + public + view + returns (BatchLockup.CreateWithTimestampsLL[] memory batch) + { + batch = BatchLockupBuilder.fillBatch(createWithTimestampsLL(), batchSize); + } + + /// @dev Returns a default-size batch of {BatchLockup.CreateWithTimestampsLT} parameters. + function batchCreateWithTimestampsLT() public view returns (BatchLockup.CreateWithTimestampsLT[] memory batch) { + batch = batchCreateWithTimestampsLT(BATCH_SIZE); + } + + /// @dev Returns a batch of {BatchLockup.CreateWithTimestampsLL} parameters. + function batchCreateWithTimestampsLT(uint256 batchSize) + public + view + returns (BatchLockup.CreateWithTimestampsLT[] memory batch) + { + batch = BatchLockupBuilder.fillBatch(createWithTimestampsLT(), batchSize); } } diff --git a/test/utils/DeployOptimized.sol b/test/utils/DeployOptimized.sol index f49840f8..0b6c63e4 100644 --- a/test/utils/DeployOptimized.sol +++ b/test/utils/DeployOptimized.sol @@ -1,29 +1,29 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { StdCheats } from "forge-std/src/StdCheats.sol"; -import { ISablierV2Batch } from "../../src/interfaces/ISablierV2Batch.sol"; -import { ISablierV2MerkleStreamerFactory } from "../../src/interfaces/ISablierV2MerkleStreamerFactory.sol"; +import { ISablierV2BatchLockup } from "../../src/interfaces/ISablierV2BatchLockup.sol"; +import { ISablierV2MerkleLockupFactory } from "../../src/interfaces/ISablierV2MerkleLockupFactory.sol"; abstract contract DeployOptimized is StdCheats { - /// @dev Deploys {SablierV2Batch} from an optimized source compiled with `--via-ir`. - function deployOptimizedBatch() internal returns (ISablierV2Batch) { - return ISablierV2Batch(deployCode("out-optimized/SablierV2Batch.sol/SablierV2Batch.json")); + /// @dev Deploys {SablierV2BatchLockup} from an optimized source compiled with `--via-ir`. + function deployOptimizedBatchLockup() internal returns (ISablierV2BatchLockup) { + return ISablierV2BatchLockup(deployCode("out-optimized/SablierV2BatchLockup.sol/SablierV2BatchLockup.json")); } - /// @dev Deploys {SablierV2MerkleStreamerFactory} from an optimized source compiled with `--via-ir`. - function deployOptimizedMerkleStreamerFactory() internal returns (ISablierV2MerkleStreamerFactory) { - return ISablierV2MerkleStreamerFactory( - deployCode("out-optimized/SablierV2MerkleStreamerFactory.sol/SablierV2MerkleStreamerFactory.json") + /// @dev Deploys {SablierV2MerkleLockupFactory} from an optimized source compiled with `--via-ir`. + function deployOptimizedMerkleLockupFactory() internal returns (ISablierV2MerkleLockupFactory) { + return ISablierV2MerkleLockupFactory( + deployCode("out-optimized/SablierV2MerkleLockupFactory.sol/SablierV2MerkleLockupFactory.json") ); } - /// @notice Deploys all V2 Periphery contracts from a optimized source in the following order: + /// @notice Deploys all V2 Periphery contracts from an optimized source in the following order: /// - /// 1. {SablierV2Batch} - /// 2. {SablierV2MerkleStreamerFactory} - function deployOptimizedPeriphery() internal returns (ISablierV2Batch, ISablierV2MerkleStreamerFactory) { - return (deployOptimizedBatch(), deployOptimizedMerkleStreamerFactory()); + /// 1. {SablierV2BatchLockup} + /// 2. {SablierV2MerkleLockupFactory} + function deployOptimizedPeriphery() internal returns (ISablierV2BatchLockup, ISablierV2MerkleLockupFactory) { + return (deployOptimizedBatchLockup(), deployOptimizedMerkleLockupFactory()); } } diff --git a/test/utils/Events.sol b/test/utils/Events.sol index 1c6aed6a..9910c0a4 100644 --- a/test/utils/Events.sol +++ b/test/utils/Events.sol @@ -1,28 +1,33 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; +import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; -import { ISablierV2MerkleStreamerLL } from "src/interfaces/ISablierV2MerkleStreamerLL.sol"; +import { ISablierV2MerkleLL } from "src/interfaces/ISablierV2MerkleLL.sol"; +import { ISablierV2MerkleLT } from "src/interfaces/ISablierV2MerkleLT.sol"; +import { MerkleLockup, MerkleLT } from "src/types/DataTypes.sol"; /// @notice Abstract contract containing all the events emitted by the protocol. abstract contract Events { event Claim(uint256 index, address indexed recipient, uint128 amount, uint256 indexed streamId); event Clawback(address indexed admin, address indexed to, uint128 amount); - event CreateMerkleStreamerLL( - ISablierV2MerkleStreamerLL merkleStreamer, - address indexed admin, - ISablierV2LockupLinear indexed lockupLinear, - IERC20 indexed asset, - bytes32 merkleRoot, - uint40 expiration, + event CreateMerkleLL( + ISablierV2MerkleLL indexed merkleLL, + MerkleLockup.ConstructorParams baseParams, + ISablierV2LockupLinear lockupLinear, LockupLinear.Durations streamDurations, - bool cancelable, - bool transferable, - string ipfsCID, uint256 aggregateAmount, - uint256 recipientsCount + uint256 recipientCount + ); + event CreateMerkleLT( + ISablierV2MerkleLT indexed merkleLT, + MerkleLockup.ConstructorParams baseParams, + ISablierV2LockupTranched lockupTranched, + MerkleLT.TrancheWithPercentage[] tranchesWithPercentages, + uint256 totalDuration, + uint256 aggregateAmount, + uint256 recipientCount ); } diff --git a/test/utils/MerkleBuilder.sol b/test/utils/MerkleBuilder.sol index e0a454bd..ad6bb0c2 100644 --- a/test/utils/MerkleBuilder.sol +++ b/test/utils/MerkleBuilder.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later // solhint-disable reason-string -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; import { LibSort } from "solady/src/utils/LibSort.sol"; diff --git a/test/utils/MerkleBuilder.t.sol b/test/utils/MerkleBuilder.t.sol index 38963573..ff670085 100644 --- a/test/utils/MerkleBuilder.t.sol +++ b/test/utils/MerkleBuilder.t.sol @@ -1,13 +1,13 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; -import { PRBTest } from "@prb/test/src/PRBTest.sol"; +import { StdAssertions } from "forge-std/src/StdAssertions.sol"; import { StdUtils } from "forge-std/src/StdUtils.sol"; import { MerkleBuilder } from "./MerkleBuilder.sol"; -contract MerkleBuilder_Test is PRBTest, StdUtils { - function testFuzz_ComputeLeaf(uint256 index, address recipient, uint128 amount) external { +contract MerkleBuilder_Test is StdAssertions, StdUtils { + function testFuzz_ComputeLeaf(uint256 index, address recipient, uint128 amount) external pure { uint256 actualLeaf = MerkleBuilder.computeLeaf(index, recipient, amount); uint256 expectedLeaf = uint256(keccak256(bytes.concat(keccak256(abi.encode(index, recipient, amount))))); assertEq(actualLeaf, expectedLeaf, "computeLeaf"); @@ -20,7 +20,7 @@ contract MerkleBuilder_Test is PRBTest, StdUtils { uint128 amounts; } - function testFuzz_ComputeLeaves(LeavesParams[] memory params) external { + function testFuzz_ComputeLeaves(LeavesParams[] memory params) external pure { uint256 count = params.length; uint256[] memory indexes = new uint256[](count); diff --git a/test/utils/Murky.sol b/test/utils/Murky.sol index 85324189..df29f9a4 100644 --- a/test/utils/Murky.sol +++ b/test/utils/Murky.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // solhint-disable code-complexity,no-inline-assembly,reason-string -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; /// @dev Credits to https://github.com/dmfxyz/murky abstract contract MurkyBase { diff --git a/test/utils/Precompiles.sol b/test/utils/Precompiles.sol deleted file mode 100644 index f52c768e..00000000 --- a/test/utils/Precompiles.sol +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// solhint-disable max-line-length,no-inline-assembly,reason-string -pragma solidity >=0.8.19; - -import { ISablierV2Batch } from "../../src/interfaces/ISablierV2Batch.sol"; -import { ISablierV2MerkleStreamerFactory } from "../../src/interfaces/ISablierV2MerkleStreamerFactory.sol"; - -contract Precompiles { - /*////////////////////////////////////////////////////////////////////////// - BYTECODES - //////////////////////////////////////////////////////////////////////////*/ - - bytes public constant BYTECODE_BATCH = - hex"6080806040523461001657611a49908161001c8239f35b600080fdfe608080604052600436101561001357600080fd5b60003560e01c9081632b754bb014610c7d575080639b38b39a146108645780639b675ad6146104ac5763e8d349611461004b57600080fd5b346104345760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610434576100826111c0565b61008a6110c6565b9060443567ffffffffffffffff808211610434573660238301121561043457816004013511610434573660246101208360040135028301011161043457806004013515610482576000805b8260040135821061044d5761010291508473ffffffffffffffffffffffffffffffffffffffff851661158a565b61010f8160040135611339565b9160005b82600401358110610130576040518061012c8682611184565b0390f35b8060e061014582866004013560248801611579565b01610163606061015d84886004013560248a01611579565b01611388565b9061017683876004013560248901611579565b91610194602061018e868a6004013560248c01611579565b01611395565b6101ae6101a9868a6004013560248c01611579565b611395565b916fffffffffffffffffffffffffffffffff6101dd60406101d78960048e013560248f01611579565b01611252565b73ffffffffffffffffffffffffffffffffffffffff61020c8c61015d60809c8260248f94600401359101611579565b94816040519761021b8961126f565b16875216602086015216604084015273ffffffffffffffffffffffffffffffffffffffff8b166060840152151586830152151560a082015260407fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff608436030112610434576103c4926102c060e093604051610295816112a8565b6102a160a08501611450565b81526102b060c0809501611450565b602082015283850152369061140a565b83830152604051957fab167ccc00000000000000000000000000000000000000000000000000000000875273ffffffffffffffffffffffffffffffffffffffff835116600488015273ffffffffffffffffffffffffffffffffffffffff60208401511660248801526fffffffffffffffffffffffffffffffff604084015116604488015273ffffffffffffffffffffffffffffffffffffffff60608401511660648801528201511515608487015260a0820151151560a4870152810151602064ffffffffff918281511660c489015201511660e486015201516101048401906020809173ffffffffffffffffffffffffffffffffffffffff81511684520151910152565b60208261014481600073ffffffffffffffffffffffffffffffffffffffff88165af1801561044157600090610409575b600192506104028287611514565b5201610113565b506020823d602011610439575b81610423602093836112e0565b8101031261043457600191516103f4565b600080fd5b3d9150610416565b6040513d6000823e3d90fd5b6001906fffffffffffffffffffffffffffffffff61047860406101d786886004013560248a01611579565b16019101906100d5565b60046040517f763e559d000000000000000000000000000000000000000000000000000000008152fd5b34610434576104ba366110e9565b909281156104825760009060005b838110610836575073ffffffffffffffffffffffffffffffffffffffff6104f2911691848361158a565b6104fb82611339565b9260005b838110610514576040518061012c8782611184565b61051f818588611539565b60c001908685610530838284611539565b60600161053c90611388565b9381610549858286611539565b60200161055590611395565b85610561818488611539565b60a0810161056e916113b6565b9561057a929197611539565b61058390611395565b968c87610591818684611539565b60400161059d90611252565b946105a792611539565b6080016105b390611388565b90604051986105c18a61126f565b73ffffffffffffffffffffffffffffffffffffffff168952151560208901521515604088015273ffffffffffffffffffffffffffffffffffffffff1660608701526fffffffffffffffffffffffffffffffff16608086015273ffffffffffffffffffffffffffffffffffffffff861660a08601523661063f9161140a565b60c0850152369061064f92611462565b60e083015260405180927f168444560000000000000000000000000000000000000000000000000000000082526004820160209052610144820190805173ffffffffffffffffffffffffffffffffffffffff166024840152602081015115156044840152604081015115156064840152606081015173ffffffffffffffffffffffffffffffffffffffff16608484015260808101516fffffffffffffffffffffffffffffffff1660a484015260a081015173ffffffffffffffffffffffffffffffffffffffff1660c484015260c081015160e4840161074d916020809173ffffffffffffffffffffffffffffffffffffffff81511684520151910152565b60e0015190610124830161012090528151809152610164830191602001906000905b8082106107db57505050908060209203816000885af18015610441576000906107a8575b600192506107a18288611514565b52016104ff565b506020823d6020116107d3575b816107c2602093836112e0565b810103126104345760019151610793565b3d91506107b5565b919350916020606082610828600194885164ffffffffff604080926fffffffffffffffffffffffffffffffff815116855267ffffffffffffffff6020820151166020860152015116910152565b01940192018593929161076f565b916001906fffffffffffffffffffffffffffffffff61085b60406101d787898c611539565b160192016104c8565b346104345760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126104345761089b6111c0565b6108a36110c6565b6044359167ffffffffffffffff8084116104345736602385011215610434578360040135116104345760248301903660246101408660040135028601011161043457836004013515610482576000805b85600401358210610c4b5761092091508473ffffffffffffffffffffffffffffffffffffffff841661158a565b61092d8460040135611339565b9260005b8560040135811061094a576040518061012c8782611184565b808661010061095f8794836004013586611528565b0183610975606061015d86866004013585611528565b610998602061018e8761098d81896004013588611528565b976004013586611528565b906fffffffffffffffffffffffffffffffff8c73ffffffffffffffffffffffffffffffffffffffff6109fe6109ea60406101d78c6109de6101a98260048a01358e611528565b9a876004013590611528565b9261015d8b60809d8e936004013590611528565b948160405197610a0d8961126f565b16875216602086015216604084015273ffffffffffffffffffffffffffffffffffffffff88166060840152151586830152151560a082015260607fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60843603011261043457610bd392610ac260e093604051610a878161128c565b610a9360a08501611450565b8152610ab28660c095610aa7878201611450565b602085015201611450565b604082015283850152369061140a565b83830152604051957f96ce143100000000000000000000000000000000000000000000000000000000875273ffffffffffffffffffffffffffffffffffffffff835116600488015273ffffffffffffffffffffffffffffffffffffffff60208401511660248801526fffffffffffffffffffffffffffffffff604084015116604488015273ffffffffffffffffffffffffffffffffffffffff60608401511660648801528201511515608487015260a0820151151560a4870152810151604064ffffffffff918281511660c48901528260208201511660e489015201511661010486015201516101248401906020809173ffffffffffffffffffffffffffffffffffffffff81511684520151910152565b60208261016481600073ffffffffffffffffffffffffffffffffffffffff89165af1801561044157600090610c18575b60019250610c118288611514565b5201610931565b506020823d602011610c43575b81610c32602093836112e0565b810103126104345760019151610c03565b3d9150610c25565b6001906fffffffffffffffffffffffffffffffff610c7360406101d7868b600401358a611528565b16019101906108f3565b3461043457610c8b366110e9565b929093831561109e57506000805b84821061107057610cc291508373ffffffffffffffffffffffffffffffffffffffff841661158a565b610ccb83611339565b9360005b848110610ce4576040518061012c8882611184565b60e0610cf18287856111e3565b0190610d03608061015d8389876111e3565b91610d14602061018e848a886111e3565b92610d2d610d23848a886111e3565b60c08101906113b6565b9091610d3d6101a9868c8a6111e3565b936060610d4b878d8b6111e3565b01359464ffffffffff861686036104345788610d7e60a061015d8f80610d7860406101d78f80958a6111e3565b956111e3565b96604051998a61012081011067ffffffffffffffff6101208d0111176110415773ffffffffffffffffffffffffffffffffffffffff908b99610e3999989764ffffffffff6fffffffffffffffffffffffffffffffff96956101009f86610e2d9b9a61012083016040521690521660208d0152151560408c0152151560608b01521660808901521660a087015273ffffffffffffffffffffffffffffffffffffffff8b1660c0870152369061140a565b60e08501523691611462565b838201526040519283917fc33cd35e0000000000000000000000000000000000000000000000000000000083526020600484015273ffffffffffffffffffffffffffffffffffffffff815116602484015264ffffffffff602082015116604484015260408101511515606484015260608101511515608484015273ffffffffffffffffffffffffffffffffffffffff60808201511660a48401526fffffffffffffffffffffffffffffffff60a08201511660c484015273ffffffffffffffffffffffffffffffffffffffff60c08201511660e4840152610f4260e08201516101048501906020809173ffffffffffffffffffffffffffffffffffffffff81511684520151910152565b0151610140610144830152805180610164840152602061018484019201906000905b808210610fe65750505090806020920381600073ffffffffffffffffffffffffffffffffffffffff89165af1801561044157600090610fb3575b60019250610fac8289611514565b5201610ccf565b506020823d602011610fde575b81610fcd602093836112e0565b810103126104345760019151610f9e565b3d9150610fc0565b919350916020606082611033600194885164ffffffffff604080926fffffffffffffffffffffffffffffffff815116855267ffffffffffffffff6020820151166020860152015116910152565b019401920185939291610f64565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6001906fffffffffffffffffffffffffffffffff61109460406101d7868a8c6111e3565b1601910190610c99565b807f763e559d0000000000000000000000000000000000000000000000000000000060049252fd5b6024359073ffffffffffffffffffffffffffffffffffffffff8216820361043457565b9060607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc8301126104345773ffffffffffffffffffffffffffffffffffffffff91600435838116810361043457926024359081168103610434579160443567ffffffffffffffff9283821161043457806023830112156104345781600401359384116104345760248460051b83010111610434576024019190565b602090602060408183019282815285518094520193019160005b8281106111ac575050505090565b83518552938101939281019260010161119e565b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361043457565b91908110156112235760051b810135907ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffee181360301821215610434570190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b356fffffffffffffffffffffffffffffffff811681036104345790565b610100810190811067ffffffffffffffff82111761104157604052565b6060810190811067ffffffffffffffff82111761104157604052565b6040810190811067ffffffffffffffff82111761104157604052565b6080810190811067ffffffffffffffff82111761104157604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff82111761104157604052565b67ffffffffffffffff81116110415760051b60200190565b9061134382611321565b61135060405191826112e0565b8281527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe061137e8294611321565b0190602036910137565b3580151581036104345790565b3573ffffffffffffffffffffffffffffffffffffffff811681036104345790565b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe181360301821215610434570180359067ffffffffffffffff82116104345760200191606082023603831361043457565b919082604091031261043457604051611422816112a8565b8092803573ffffffffffffffffffffffffffffffffffffffff81168103610434578252602090810135910152565b359064ffffffffff8216820361043457565b92919261146e82611321565b60409461147e60405192836112e0565b8195848352602080930191606080960285019481861161043457925b8584106114aa5750505050505050565b8684830312610434578251906114bf8261128c565b84356fffffffffffffffffffffffffffffffff81168103610434578252858501359067ffffffffffffffff8216820361043457828792838b950152611505868801611450565b8682015281520193019261149a565b80518210156112235760209160051b010190565b919081101561122357610140020190565b91908110156112235760051b810135907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0181360301821215610434570190565b919081101561122357610120020190565b90604080516020907f23b872dd000000000000000000000000000000000000000000000000000000008282015233602482015260449030828201528660648201526064815260a081019080821067ffffffffffffffff831117611041576115f39185528561179c565b73ffffffffffffffffffffffffffffffffffffffff94858516958451917fdd62ed3e0000000000000000000000000000000000000000000000000000000083523060048401521690816024820152838184818a5afa90811561177957908891600091611748575b501061166a575b50505050505050565b8351956000808589017f095ea7b3000000000000000000000000000000000000000000000000000000009a8b82528560248c0152868b0152858a526116ae8a6112c4565b89519082855af1906116be6118bc565b82611715575b508161170a575b50611661576116fe966116f9945193840152602483015260008183015281526116f3816112c4565b8261179c565b61179c565b38808080808080611661565b90503b1515386116cb565b809192505190858215928315611730575b50505090386116c4565b6117409350820181019101611784565b388581611726565b809250858092503d8311611772575b61176181836112e0565b81010312610434578790513861165a565b503d611757565b85513d6000823e3d90fd5b90816020910312610434575180151581036104345790565b6040516118079173ffffffffffffffffffffffffffffffffffffffff166117c2826112a8565b6000806020958685527f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c656487860152868151910182855af16118016118bc565b9161191a565b8051908282159283156118a4575b505050156118205750565b608490604051907f08c379a00000000000000000000000000000000000000000000000000000000082526004820152602a60248201527f5361666545524332303a204552433230206f7065726174696f6e20646964206e60448201527f6f742073756363656564000000000000000000000000000000000000000000006064820152fd5b6118b49350820181019101611784565b388281611815565b3d15611915573d9067ffffffffffffffff8211611041576040519161190960207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f84011601846112e0565b82523d6000602084013e565b606090565b91929015611995575081511561192e575090565b3b156119375790565b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152fd5b8251909150156119a85750805190602001fd5b604051907f08c379a000000000000000000000000000000000000000000000000000000000825281602080600483015282519283602484015260005b848110611a25575050507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f836000604480968601015201168101030190fd5b8181018301518682016044015285935082016119e456fea164736f6c6343000817000a"; - bytes public constant BYTECODE_MERKLE_STREAMER_FACTORY = - hex"6080806040523461001657611d39908161001c8239f35b600080fdfe600436101561000d57600080fd5b60003560e01c6373b01dbb1461002257600080fd5b3461053b576101807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261053b5773ffffffffffffffffffffffffffffffffffffffff600435166004350361053b5773ffffffffffffffffffffffffffffffffffffffff602435166024350361053b5773ffffffffffffffffffffffffffffffffffffffff604435166044350361053b5764ffffffffff608435166084350361053b5760407fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5c36011261053b5760c060405260a43564ffffffffff8116810361053b5760805264ffffffffff60c4351660c4350361053b5760c43560a05260e435801515810361053b5761010435801515810361053b5767ffffffffffffffff610124351161053b573660236101243501121561053b5767ffffffffffffffff61012435600401351161050c57604051906101ae60207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f610124356004013501160183610540565b600461012435908101358084523691016024011161053b5761012435600401356024610124350160208401376000602061012435600401358401015260405161020f6020820160806020908164ffffffffff91828151168552015116910152565b6040815280606081011067ffffffffffffffff60608301111761050c5760608101604052606060808201917fffffffffffffffffffffffffffffffffffffffff00000000000000000000000080600435841b16845280602435841b166094830152604435831b1660a882015260643560bc8201527fffffffffff00000000000000000000000000000000000000000000000000000060843560d81b1660dc8201526102f56023838380516102ca8160e1840160208501610581565b81018a151560f81b60e182015288151560f81b60e28201520301600381018585015201838301610540565b015190206040518061178881011067ffffffffffffffff6117888301111761050c576117886105a5823973ffffffffffffffffffffffffffffffffffffffff60043581166117888301908152602435821660208201526044359091166040820152606435606082015264ffffffffff608435811660808084019190915251811660a080840191909152511660c082015285151560e08201528315156101008201526101209082900301906000f591821561050057602093604051926101409173ffffffffffffffffffffffffffffffffffffffff861685526064358786015264ffffffffff6084351660408601526104056060860160806020908164ffffffffff91828151168552015116910152565b151560a0850152151560c08401528060e0840152815180918401526104338161016093878587019101610581565b61014435610100840152610164356101208401527fb2f6bc588a802f2ce7f8dec57a5096c107d2fc000d5b4cc65745a2c0e232349c73ffffffffffffffffffffffffffffffffffffffff604435169373ffffffffffffffffffffffffffffffffffffffff6024351693817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f73ffffffffffffffffffffffffffffffffffffffff600435169601168101030190a473ffffffffffffffffffffffffffffffffffffffff60405191168152f35b6040513d6000823e3d90fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b600080fd5b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff82111761050c57604052565b60005b8381106105945750506000910152565b818101518382015260200161058456fe610160604081815234620002e0578162001788803803809162000023828562000301565b833981010361012091828212620002e05783516001600160a01b038082169391849003620002e057602090818701519481861693848703620002e0578589015192831692838103620002e05760608a015190876200008460808d0162000325565b93609f190112620002e0578751918289016001600160401b03811184821017620002ca578952620000b860a08d0162000325565b8352620000c860c08d0162000325565b83880190815293620000dd60e08e0162000338565b91610100809e01620000ef9062000338565b93600097600160a01b60019003198954161788556080528960e0528d5260c05260a05289526101409788525164ffffffffff16600254915160281b69ffffffffff00000000001691600160501b600190031916171760025584519280840191808063095ea7b360e01b948581528860248901526000196044890152604488526200017988620002e5565b87519082885af16200018a62000346565b8162000288575b50806200027d575b156200023b575b505050505050519161121b93846200056d8539608051848181610377015281816106460152610cc4015260a0518481816107320152610bb0015260c05184818161015101528181610afc01528181610f1b01526110b7015260e05184818161020e015281816105d00152610c63015251838181610327015261056801525182818161075a0152610b730152518181816101a201526108cd0152f35b62000271956200026b9388519384015260248301526044820152604481526200026481620002e5565b8262000389565b62000389565b388080808080620001a0565b50833b151562000199565b80915051838115918215620002a3575b505090503862000191565b8380929350010312620002c65782620002bd910162000338565b80833862000298565b5080fd5b634e487b7160e01b600052604160045260246000fd5b600080fd5b608081019081106001600160401b03821117620002ca57604052565b601f909101601f19168101906001600160401b03821190821017620002ca57604052565b519064ffffffffff82168203620002e057565b51908115158203620002e057565b3d1562000384573d906001600160401b038211620002ca576040519162000378601f8201601f19166020018462000301565b82523d6000602084013e565b606090565b60408051908101916001600160a01b03166001600160401b03831182841017620002ca57620003fd926040526000806020958685527f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c656487860152868151910182855af1620003f662000346565b9162000492565b80518281159182156200046f575b5050905015620004185750565b6084906040519062461bcd60e51b82526004820152602a60248201527f5361666545524332303a204552433230206f7065726174696f6e20646964206e6044820152691bdd081cdd58d8d9595960b21b6064820152fd5b8380929350010312620002e0578162000489910162000338565b8082386200040b565b91929015620004f75750815115620004a8575090565b3b15620004b25790565b60405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152606490fd5b8251909150156200050b5750805190602001fd5b6040519062461bcd60e51b82528160208060048301528251908160248401526000935b82851062000552575050604492506000838284010152601f80199101168101030190fd5b84810182015186860160440152938101938593506200052e56fe608080604052600436101561001357600080fd5b60003560e01c9081631686c90914610bd55750806316c3549d14610b985780631bfd681414610b5b5780633bfe03a814610b2c5780633f31ae3f1461039b5780634800d97f1461034a57806351e75e8b1461030f57806375829def14610232578063845aef4b146101e157806390e64d13146101c65780639e93e57714610175578063bb4b573414610133578063ce516507146100f25763f851a440146100b957600080fd5b346100ed5760006003193601126100ed57602073ffffffffffffffffffffffffffffffffffffffff60005416604051908152f35b600080fd5b346100ed5760206003193601126100ed57602061012960043560ff6001918060081c60005282602052161b60406000205416151590565b6040519015158152f35b346100ed5760006003193601126100ed57602060405164ffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b346100ed5760006003193601126100ed57602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b346100ed5760006003193601126100ed5760206101296110af565b346100ed5760006003193601126100ed57602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b346100ed5760206003193601126100ed5761024b610fe7565b60005473ffffffffffffffffffffffffffffffffffffffff808216923384036102c2577fffffffffffffffffffffffff00000000000000000000000000000000000000009350169182911617600055337fbdd36143ee09de60bdefca70680e0f71189b2ed7acee364b53917ad433fdaf80600080a3005b6040517fc6cce6a400000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff85166004820152336024820152604490fd5b346100ed5760006003193601126100ed5760206040517f00000000000000000000000000000000000000000000000000000000000000008152f35b346100ed5760006003193601126100ed57602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b346100ed5760806003193601126100ed5760243573ffffffffffffffffffffffffffffffffffffffff811681036100ed57604435906fffffffffffffffffffffffffffffffff821682036100ed5767ffffffffffffffff606435116100ed573660236064350112156100ed5767ffffffffffffffff60643560040135116100ed573660246064356004013560051b6064350101116100ed576040516020810190600435825273ffffffffffffffffffffffffffffffffffffffff831660408201526fffffffffffffffffffffffffffffffff841660608201526060815261048181611026565b519020604051602081019182526020815261049b8161100a565b519020916104a76110af565b610ace576104cf60043560ff6001918060081c60005282602052161b60406000205416151590565b610a9c57604051926104ed60206064356004013560051b0185611042565b60643560048101358552602401602085015b60246064356004013560051b60643501018210610a8c575050906000915b84518310156105645760208360051b860101519081811060001461055157600052602052600160406000205b92019161051d565b9060005260205260016040600020610549565b83907f000000000000000000000000000000000000000000000000000000000000000003610a6257604051907f5fe3b56700000000000000000000000000000000000000000000000000000000825260208260048173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa91821561099a57600092610a31575b506040517fdcf844a700000000000000000000000000000000000000000000000000000000815260208160248173ffffffffffffffffffffffffffffffffffffffff807f00000000000000000000000000000000000000000000000000000000000000001697886004840152165afa90811561099a576000916109ff575b506109d55760043560081c60005260016020526040600020600160ff600435161b81541790556040516106b18161100a565b600081526000602082015273ffffffffffffffffffffffffffffffffffffffff600054169260405193610100850185811067ffffffffffffffff8211176109a657604052845273ffffffffffffffffffffffffffffffffffffffff831660208501526fffffffffffffffffffffffffffffffff8516604085015260608401527f0000000000000000000000000000000000000000000000000000000000000000151560808401527f0000000000000000000000000000000000000000000000000000000000000000151560a084015260405161078c8161100a565b64ffffffffff600254818116835260281c16602082015260c084015260e0830152602060e0604051937fab167ccc00000000000000000000000000000000000000000000000000000000855273ffffffffffffffffffffffffffffffffffffffff815116600486015273ffffffffffffffffffffffffffffffffffffffff838201511660248601526fffffffffffffffffffffffffffffffff604082015116604486015273ffffffffffffffffffffffffffffffffffffffff606082015116606486015260808101511515608486015260a0810151151560a486015264ffffffffff8360c08301518281511660c489015201511660e4860152015173ffffffffffffffffffffffffffffffffffffffff815116610104850152015161012483015260208261014481600073ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165af191821561099a57600092610965575b506020927f28b58397e03322f670d6b223cc863f8c148e368b8b615412e6798a641a22842d604073ffffffffffffffffffffffffffffffffffffffff85946fffffffffffffffffffffffffffffffff835195600435875216888601521692a3604051908152f35b91506020823d602011610992575b8161098060209383611042565b810103126100ed5790519060206108fe565b3d9150610973565b6040513d6000823e3d90fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b60046040517fa4432b51000000000000000000000000000000000000000000000000000000008152fd5b90506020813d602011610a29575b81610a1a60209383611042565b810103126100ed57518461067f565b3d9150610a0d565b610a5491925060203d602011610a5b575b610a4c8183611042565b810190611083565b9083610601565b503d610a42565b60046040517fb3f3b2a6000000000000000000000000000000000000000000000000000000008152fd5b81358152602091820191016104ff565b60246040517f3548783b0000000000000000000000000000000000000000000000000000000081526004356004820152fd5b6040517f74b43bd00000000000000000000000000000000000000000000000000000000081524260048201527f000000000000000000000000000000000000000000000000000000000000000064ffffffffff166024820152604490fd5b346100ed5760006003193601126100ed57604060025464ffffffffff825191818116835260281c166020820152f35b346100ed5760006003193601126100ed5760206040517f000000000000000000000000000000000000000000000000000000000000000015158152f35b346100ed5760006003193601126100ed5760206040517f000000000000000000000000000000000000000000000000000000000000000015158152f35b346100ed5760406003193601126100ed57610bee610fe7565b90602435906fffffffffffffffffffffffffffffffff82168092036100ed5773ffffffffffffffffffffffffffffffffffffffff908160005416338103610f9e575050604051907f5fe3b5670000000000000000000000000000000000000000000000000000000082526020918281600481857f0000000000000000000000000000000000000000000000000000000000000000165afa90811561099a57600091610f81575b506040517fdcf844a7000000000000000000000000000000000000000000000000000000008152838160248186807f00000000000000000000000000000000000000000000000000000000000000001696876004840152165afa90811561099a57600091610f54575b50610d066110af565b159081610f4b575b50610eed5760405160008084868401987fa9059cbb000000000000000000000000000000000000000000000000000000008a52169788602485015287604485015260448452610d5c84611026565b60405193610d698561100a565b8785527f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c656488860152519082865af13d15610ee0573d9067ffffffffffffffff82116109a657610df59360405192610de7887fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8401160185611042565b83523d60008885013e6110ec565b8051838115918215610ec0575b5050905015610e3c57907f2e9d425ba8b27655048400b366d7b6a1f7180ebdb088e06bb7389704860ffe1f916000541692604051908152a3005b608482604051907f08c379a00000000000000000000000000000000000000000000000000000000082526004820152602a60248201527f5361666545524332303a204552433230206f7065726174696f6e20646964206e60448201527f6f742073756363656564000000000000000000000000000000000000000000006064820152fd5b83809293500103126100ed5782015180151581036100ed57808387610e02565b91610df5926060916110ec565b6040517f1351f21d0000000000000000000000000000000000000000000000000000000081524260048201527f000000000000000000000000000000000000000000000000000000000000000064ffffffffff166024820152604490fd5b90501586610d0e565b90508381813d8311610f7a575b610f6b8183611042565b810103126100ed575186610cfd565b503d610f61565b610f989150833d8511610a5b57610a4c8183611042565b85610c94565b7fc6cce6a400000000000000000000000000000000000000000000000000000000825273ffffffffffffffffffffffffffffffffffffffff166004820152336024820152604490fd5b6004359073ffffffffffffffffffffffffffffffffffffffff821682036100ed57565b6040810190811067ffffffffffffffff8211176109a657604052565b6080810190811067ffffffffffffffff8211176109a657604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff8211176109a657604052565b908160209103126100ed575173ffffffffffffffffffffffffffffffffffffffff811681036100ed5790565b64ffffffffff7f00000000000000000000000000000000000000000000000000000000000000001680151590816110e4575090565b905042101590565b919290156111675750815115611100575090565b3b156111095790565b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152fd5b82519091501561117a5750805190602001fd5b604051907f08c379a000000000000000000000000000000000000000000000000000000000825281602080600483015282519283602484015260005b8481106111f7575050507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f836000604480968601015201168101030190fd5b8181018301518682016044015285935082016111b656fea164736f6c6343000817000aa164736f6c6343000817000a"; - - /*////////////////////////////////////////////////////////////////////////// - DEPLOYERS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Deploys {SablierV2Batch} from precompiled bytecode. - function deployBatch() public returns (ISablierV2Batch batch) { - bytes memory creationBytecode = BYTECODE_BATCH; - assembly { - batch := create(0, add(creationBytecode, 0x20), mload(creationBytecode)) - } - require(address(batch) != address(0), "Sablier V2 Precompiles: deployment failed for Batch contract"); - } - - /// @notice Deploys {SablierV2MerkleStreamerFactory} from precompiled bytecode. - function deployMerkleStreamerFactory() public returns (ISablierV2MerkleStreamerFactory factory) { - bytes memory creationBytecode = BYTECODE_MERKLE_STREAMER_FACTORY; - assembly { - factory := create(0, add(creationBytecode, 0x20), mload(creationBytecode)) - } - require( - address(factory) != address(0), - "Sablier V2 Precompiles: deployment failed for MerkleStreamerFactory contract" - ); - } - - /// @notice Deploys all V2 Periphery contracts in the following order: - /// - /// 1. {SablierV2Batch} - /// 2. {SablierV2MerkleStreamerFactory} - function deployPeriphery() - public - returns (ISablierV2Batch batch, ISablierV2MerkleStreamerFactory merkleStreamerFactory) - { - batch = deployBatch(); - merkleStreamerFactory = deployMerkleStreamerFactory(); - } -} diff --git a/test/utils/Precompiles.t.sol b/test/utils/Precompiles.t.sol index ed23578e..1fec3c09 100644 --- a/test/utils/Precompiles.t.sol +++ b/test/utils/Precompiles.t.sol @@ -1,18 +1,13 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; -import { LibString } from "solady/src/utils/LibString.sol"; - -import { ISablierV2Batch } from "../../src/interfaces/ISablierV2Batch.sol"; -import { ISablierV2MerkleStreamerFactory } from "../../src/interfaces/ISablierV2MerkleStreamerFactory.sol"; +import { Precompiles } from "precompiles/Precompiles.sol"; +import { ISablierV2BatchLockup } from "src/interfaces/ISablierV2BatchLockup.sol"; +import { ISablierV2MerkleLockupFactory } from "src/interfaces/ISablierV2MerkleLockupFactory.sol"; import { Base_Test } from "../Base.t.sol"; -import { Precompiles } from "./Precompiles.sol"; contract Precompiles_Test is Base_Test { - using LibString for address; - using LibString for string; - Precompiles internal precompiles = new Precompiles(); modifier onlyTestOptimizedProfile() { @@ -21,28 +16,28 @@ contract Precompiles_Test is Base_Test { } } - function test_DeployBatch() external onlyTestOptimizedProfile { - address actualBatch = address(precompiles.deployBatch()); - address expectedBatch = address(deployOptimizedBatch()); - assertEq(actualBatch.code, expectedBatch.code, "bytecodes mismatch"); + function test_DeployBatchLockup() external onlyTestOptimizedProfile { + address actualBatchLockup = address(precompiles.deployBatchLockup()); + address expectedBatchLockup = address(deployOptimizedBatchLockup()); + assertEq(actualBatchLockup.code, expectedBatchLockup.code, "bytecodes mismatch"); } - function test_DeployMerkleStreamerFactory() external onlyTestOptimizedProfile { - address actualFactory = address(precompiles.deployMerkleStreamerFactory()); - address expectedFactory = address(deployOptimizedMerkleStreamerFactory()); + function test_DeployMerkleLockupFactory() external onlyTestOptimizedProfile { + address actualFactory = address(precompiles.deployMerkleLockupFactory()); + address expectedFactory = address(deployOptimizedMerkleLockupFactory()); assertEq(actualFactory.code, expectedFactory.code, "bytecodes mismatch"); } function test_DeployPeriphery() external onlyTestOptimizedProfile { - (ISablierV2Batch actualBatch, ISablierV2MerkleStreamerFactory actualMerkleStreamerFactory) = + (ISablierV2BatchLockup actualBatchLockup, ISablierV2MerkleLockupFactory actualMerkleLockupFactory) = precompiles.deployPeriphery(); - (ISablierV2Batch expectedBatch, ISablierV2MerkleStreamerFactory expectedMerkleStreamerFactory) = + (ISablierV2BatchLockup expectedBatchLockup, ISablierV2MerkleLockupFactory expectedMerkleLockupFactory) = deployOptimizedPeriphery(); - assertEq(address(actualBatch).code, address(expectedBatch).code, "bytecodes mismatch"); + assertEq(address(actualBatchLockup).code, address(expectedBatchLockup).code, "bytecodes mismatch"); assertEq( - address(actualMerkleStreamerFactory).code, address(expectedMerkleStreamerFactory).code, "bytecodes mismatch" + address(actualMerkleLockupFactory).code, address(expectedMerkleLockupFactory).code, "bytecodes mismatch" ); } } diff --git a/test/utils/Types.sol b/test/utils/Types.sol index d722cd61..39fdda5a 100644 --- a/test/utils/Types.sol +++ b/test/utils/Types.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; struct Users { address alice;