├── .editorconfig ├── .env.example ├── .github └── workflows │ ├── ci-deep.yml │ ├── ci-fork.yml │ ├── ci-multibuild.yml │ ├── ci-slither.yml │ ├── ci.yml │ ├── cron-stale.yml │ └── generate-svg.yml ├── .gitignore ├── .husky └── pre-commit ├── .lintstagedrc.yml ├── .prettierignore ├── .prettierrc.yml ├── .solhint.json ├── .vscode └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE-GPL.md ├── LICENSE.md ├── README.md ├── SECURITY.md ├── benchmark ├── BatchLockup.Gas.t.sol ├── Benchmark.t.sol ├── EstimateMaxCount.t.sol ├── LockupDynamic.Gas.t.sol ├── LockupLinear.Gas.t.sol ├── LockupTranched.Gas.t.sol └── results │ ├── SablierBatchLockup.md │ ├── SablierLockup_Dynamic.md │ ├── SablierLockup_Linear.md │ └── SablierLockup_Tranched.md ├── bun.lockb ├── codecov.yml ├── foundry.toml ├── funding.json ├── package.json ├── remappings.txt ├── script ├── Base.s.sol ├── DeployBatchLockup.s.sol ├── DeployDeterministicBatchLockup.s.sol ├── DeployDeterministicLockup.s.sol ├── DeployDeterministicNFTDescriptor.s.sol ├── DeployDeterministicProtocol.s.sol ├── DeployLockup.s.sol ├── DeployNFTDescriptor.s.sol ├── DeployProtocol.s.sol ├── GenerateSVG.s.sol └── Init.s.sol ├── shell ├── generate-svg-panoply.sh ├── generate-svg.sh ├── prepare-artifacts.sh └── update-counts.sh ├── slither.config.json ├── src ├── LockupNFTDescriptor.sol ├── SablierBatchLockup.sol ├── SablierLockup.sol ├── abstracts │ ├── Adminable.sol │ ├── Batch.sol │ ├── NoDelegateCall.sol │ └── SablierLockupBase.sol ├── interfaces │ ├── IAdminable.sol │ ├── IBatch.sol │ ├── ILockupNFTDescriptor.sol │ ├── ISablierBatchLockup.sol │ ├── ISablierLockup.sol │ ├── ISablierLockupBase.sol │ └── ISablierLockupRecipient.sol ├── libraries │ ├── Errors.sol │ ├── Helpers.sol │ ├── NFTSVG.sol │ ├── SVGElements.sol │ └── VestingMath.sol └── types │ └── DataTypes.sol └── tests ├── Base.t.sol ├── fork ├── Fork.t.sol ├── LockupDynamic.t.sol ├── LockupLinear.t.sol ├── LockupTranched.t.sol └── tokens │ ├── DAI.t.sol │ ├── EURS.t.sol │ ├── SHIB.t.sol │ ├── USDC.t.sol │ └── USDT.t.sol ├── integration ├── Integration.t.sol ├── concrete │ ├── batch-lockup │ │ ├── create-with-durations-ld │ │ │ ├── createWithDurationsLD.t.sol │ │ │ └── createWithDurationsLD.tree │ │ ├── create-with-durations-ll │ │ │ ├── createWithDurationsLL.t.sol │ │ │ └── createWithDurationsLL.tree │ │ ├── create-with-durations-lt │ │ │ ├── createWithDurationsLT.t.sol │ │ │ └── createWithDurationsLT.tree │ │ ├── create-with-timestamps-ld │ │ │ ├── createWithTimestampsLD.t.sol │ │ │ └── createWithTimestampsLD.tree │ │ ├── create-with-timestamps-ll │ │ │ ├── createWithTimestamps.t.sol │ │ │ └── createWithTimestamps.tree │ │ └── create-with-timestamps-lt │ │ │ ├── createWithTimestampsLT.t.sol │ │ │ └── createWithTimestampsLT.tree │ ├── batch │ │ └── batch.t.sol │ ├── constructor.t.sol │ ├── lockup-base │ │ ├── allow-to-hook │ │ │ ├── allowToHook.t.sol │ │ │ └── allowToHook.tree │ │ ├── burn │ │ │ ├── burn.t.sol │ │ │ └── burn.tree │ │ ├── cancel-multiple │ │ │ ├── cancelMultiple.t.sol │ │ │ └── cancelMultiple.tree │ │ ├── cancel │ │ │ ├── cancel.t.sol │ │ │ └── cancel.tree │ │ ├── collect-fees │ │ │ ├── collectFees.t.sol │ │ │ └── collectFees.tree │ │ ├── create-with-timestamps │ │ │ ├── createWithTimestamps.t.sol │ │ │ └── createWithTimestamps.tree │ │ ├── getters │ │ │ ├── getters.t.sol │ │ │ └── getters.tree │ │ ├── refundable-amount-of │ │ │ ├── refundableAmountOf.t.sol │ │ │ └── refundableAmountOf.tree │ │ ├── renounce-multiple │ │ │ ├── renounceMultiple.t.sol │ │ │ └── renounceMultiple.tree │ │ ├── renounce │ │ │ ├── renounce.t.sol │ │ │ └── renounce.tree │ │ ├── set-nft-descriptor │ │ │ ├── setNFTDescriptor.t.sol │ │ │ └── setNFTDescriptor.tree │ │ ├── status-of │ │ │ ├── statusOf.t.sol │ │ │ └── statusOf.tree │ │ ├── streamed-amount-of │ │ │ ├── streamedAmountOf.t.sol │ │ │ └── streamedAmountOf.tree │ │ ├── token-uri │ │ │ ├── tokenURI.t.sol │ │ │ └── tokenURI.tree │ │ ├── transfer-from │ │ │ ├── transferFrom.t.sol │ │ │ └── transferFrom.tree │ │ ├── withdraw-hooks │ │ │ ├── withdrawHooks.t.sol │ │ │ └── withdrawHooks.tree │ │ ├── withdraw-max-and-transfer │ │ │ ├── withdrawMaxAndTransfer.t.sol │ │ │ └── withdrawMaxAndTransfer.tree │ │ ├── withdraw-max │ │ │ ├── withdrawMax.t.sol │ │ │ └── withdrawMax.tree │ │ ├── withdraw-multiple │ │ │ ├── withdrawMultiple.t.sol │ │ │ └── withdrawMultiple.tree │ │ ├── withdraw │ │ │ ├── withdraw.t.sol │ │ │ └── withdraw.tree │ │ └── withdrawable-amount-of │ │ │ ├── withdrawableAmountOf.t.sol │ │ │ └── withdrawableAmountOf.tree │ ├── lockup-dynamic │ │ ├── LockupDynamic.t.sol │ │ ├── create-with-durations-ld │ │ │ ├── createWithDurationsLD.t.sol │ │ │ └── createWithDurationsLD.tree │ │ ├── create-with-timestamps-ld │ │ │ ├── createWithTimestampsLD.t.sol │ │ │ └── createWithTimestampsLD.tree │ │ ├── get-segments │ │ │ ├── getSegments.t.sol │ │ │ └── getSegments.tree │ │ ├── streamed-amount-of │ │ │ ├── streamedAmountOf.t.sol │ │ │ └── streamedAmountOf.tree │ │ └── withdrawable-amount-of │ │ │ ├── withdrawableAmountOf.t.sol │ │ │ └── withdrawableAmountOf.tree │ ├── lockup-linear │ │ ├── LockupLinear.t.sol │ │ ├── create-with-durations-ll │ │ │ ├── createWithDurationsLL.t.sol │ │ │ └── createWithDurationsLL.tree │ │ ├── create-with-timestamps-ll │ │ │ ├── createWithTimestampsLL.t.sol │ │ │ └── createWithTimestampsLL.tree │ │ ├── get-cliff-time │ │ │ ├── getCliffTime.t.sol │ │ │ └── getCliffTime.tree │ │ ├── get-unlock-amounts │ │ │ ├── getUnlockAmounts.t.sol │ │ │ └── getUnlockAmounts.tree │ │ ├── streamed-amount-of │ │ │ ├── streamedAmountOf.t.sol │ │ │ └── streamedAmountOf.tree │ │ └── withdrawable-amount-of │ │ │ ├── withdrawableAmountOf.t.sol │ │ │ └── withdrawableAmountOf.tree │ ├── lockup-tranched │ │ ├── LockupTranched.t.sol │ │ ├── create-with-durations-lt │ │ │ ├── createWithDurationsLT.t.sol │ │ │ └── createWithDurationsLT.tree │ │ ├── create-with-timestamps-lt │ │ │ ├── createWithTimestampsLT.t.sol │ │ │ └── createWithTimestampsLT.tree │ │ ├── get-tranches │ │ │ ├── getTranches.t.sol │ │ │ └── getTranches.tree │ │ ├── streamed-amount-of │ │ │ ├── streamedAmountOf.t.sol │ │ │ └── streamedAmountOf.tree │ │ └── withdrawable-amount-of │ │ │ ├── withdrawableAmountOf.t.sol │ │ │ └── withdrawableAmountOf.tree │ └── nft-descriptor │ │ ├── generateAccentColor.t.sol │ │ ├── is-allowed-character │ │ ├── IsAllowedCharacter.t.sol │ │ └── IsAllowedCharacter.tree │ │ ├── safe-token-decimals │ │ ├── safeTokenDecimals.t.sol │ │ └── safeTokenDecimals.tree │ │ └── safe-token-symbol │ │ ├── safeTokenSymbol.t.sol │ │ └── safeTokenSymbol.tree └── fuzz │ ├── lockup-base │ ├── cancel.t.sol │ ├── refundableAmountOf.t.sol │ ├── withdraw.t.sol │ ├── withdrawMax.t.sol │ └── withdrawMaxAndTransfer.t.sol │ ├── lockup-dynamic │ ├── LockupDynamic.t.sol │ ├── createWithDurationsLD.t.sol │ ├── createWithTimestampsLD.t.sol │ ├── streamedAmountOf.t.sol │ ├── withdraw.t.sol │ └── withdrawableAmountOf.t.sol │ ├── lockup-linear │ ├── LockupLinear.t.sol │ ├── createWithDurationsLL.t.sol │ ├── createWithTimestampsLL.t.sol │ ├── streamedAmountOf.t.sol │ └── withdrawableAmountOf.t.sol │ ├── lockup-tranched │ ├── LockupTranched.t.sol │ ├── createWithDurationsLT.t.sol │ ├── createWithTimestampsLT.t.sol │ ├── streamedAmountOf.t.sol │ ├── withdraw.t.sol │ └── withdrawableAmountOf.t.sol │ └── nft-descriptor │ └── isAllowedCharacter.t.sol ├── invariant ├── Invariant.t.sol ├── handlers │ ├── BaseHandler.sol │ ├── LockupCreateHandler.sol │ └── LockupHandler.sol └── stores │ └── LockupStore.sol ├── mocks ├── AdminableMock.sol ├── BatchMock.sol ├── Hooks.sol ├── NFTDescriptorMock.sol ├── Noop.sol ├── Receive.sol └── erc20 │ ├── ERC20Bytes32.sol │ ├── ERC20MissingReturn.sol │ └── ERC20Mock.sol ├── unit ├── concrete │ ├── adminable │ │ └── transfer-admin │ │ │ ├── transferAdmin.t.sol │ │ │ └── transferAdmin.tree │ ├── batch │ │ ├── batch.t.sol │ │ └── batch.tree │ └── nft-descriptor │ │ ├── abbreviateAmount.t.sol │ │ ├── calculateDurationInDays.t.sol │ │ ├── calculatePixelWidth.t.sol │ │ ├── calculateStreamedPercentage.t.sol │ │ ├── generateAttributes.t.sol │ │ ├── generateDescription.t.sol │ │ ├── generateSVG.t.sol │ │ ├── hourglass.t.sol │ │ ├── stringifyCardType.t.sol │ │ ├── stringifyFractionalAmount.t.sol │ │ ├── stringifyPercentage.t.sol │ │ └── stringifyStatus.t.sol ├── fuzz │ └── transferAdmin.t.sol └── shared │ └── Adminable.t.sol └── utils ├── .npmignore ├── ArrayBuilder.sol ├── Assertions.sol ├── BaseScript.t.sol ├── BatchLockupBuilder.sol ├── Calculations.sol ├── Constants.sol ├── Defaults.sol ├── DeployOptimized.t.sol ├── Fuzzers.sol ├── Modifiers.sol ├── Types.sol └── Utils.sol /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # All files 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | indent_size = 2 11 | indent_style = space 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | 15 | [*.sol] 16 | indent_size = 4 17 | 18 | [*.tree] 19 | indent_size = 1 20 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | export EOA="YOUR_EOA_ADDRESS" 2 | export FOUNDRY_PROFILE="lite" 3 | export MNEMONIC="YOUR_MNEMONIC" 4 | export MAINNET_RPC_URL="YOUR_MAINNET_RPC_URL" 5 | 6 | -------------------------------------------------------------------------------- /.github/workflows/ci-deep.yml: -------------------------------------------------------------------------------- 1 | name: "CI Deep" 2 | 3 | env: 4 | MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} 5 | 6 | on: 7 | schedule: 8 | - cron: "0 3 * * 0" # at 3:00am UTC every Sunday 9 | workflow_dispatch: 10 | inputs: 11 | unitFuzzRuns: 12 | default: "50000" 13 | description: "Unit: number of fuzz runs." 14 | required: false 15 | integrationFuzzRuns: 16 | default: "50000" 17 | description: "Integration: number of fuzz runs." 18 | required: false 19 | invariantRuns: 20 | default: "100" 21 | description: "Invariant runs: number of sequences of function calls generated and run." 22 | required: false 23 | invariantDepth: 24 | default: "100" 25 | description: "Invariant depth: number of function calls made in a given run." 26 | required: false 27 | forkFuzzRuns: 28 | default: "1000" 29 | description: "Fork: number of fuzz runs." 30 | required: false 31 | 32 | jobs: 33 | lint: 34 | uses: "sablier-labs/gha-utils/.github/workflows/evm-lint.yml@main" 35 | 36 | build: 37 | uses: "sablier-labs/gha-utils/.github/workflows/forge-build.yml@main" 38 | 39 | test-unit: 40 | needs: ["lint", "build"] 41 | uses: "sablier-labs/gha-utils/.github/workflows/forge-test.yml@main" 42 | with: 43 | foundry-fuzz-runs: ${{ fromJSON(inputs.unitFuzzRuns || '50000') }} 44 | foundry-profile: "test-optimized" 45 | match-path: "tests/unit/**/*.sol" 46 | name: "Unit tests" 47 | 48 | test-integration: 49 | needs: ["lint", "build"] 50 | uses: "sablier-labs/gha-utils/.github/workflows/forge-test.yml@main" 51 | with: 52 | foundry-fuzz-runs: ${{ fromJSON(inputs.integrationFuzzRuns || '50000') }} 53 | foundry-profile: "test-optimized" 54 | match-path: "tests/integration/**/*.sol" 55 | name: "Integration tests" 56 | 57 | test-invariant: 58 | needs: ["lint", "build"] 59 | uses: "sablier-labs/gha-utils/.github/workflows/forge-test.yml@main" 60 | with: 61 | foundry-invariant-depth: ${{ fromJSON(inputs.invariantDepth || '100') }} 62 | foundry-invariant-runs: ${{ fromJSON(inputs.invariantRuns || '100') }} 63 | foundry-profile: "test-optimized" 64 | match-path: "tests/invariant/**/*.sol" 65 | name: "Invariant tests" 66 | 67 | test-fork: 68 | needs: ["lint", "build"] 69 | secrets: 70 | MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} 71 | uses: "sablier-labs/gha-utils/.github/workflows/forge-test.yml@main" 72 | with: 73 | foundry-fuzz-runs: ${{ fromJSON(inputs.forkFuzzRuns || '1000') }} 74 | foundry-profile: "test-optimized" 75 | match-path: "tests/fork/**/*.sol" 76 | name: "Fork tests" 77 | 78 | notify-on-failure: 79 | if: failure() 80 | needs: ["lint", "build", "test-unit", "test-integration", "test-invariant", "test-fork"] 81 | runs-on: "ubuntu-latest" 82 | steps: 83 | - name: "Send Slack notification" 84 | uses: "rtCamp/action-slack-notify@v2" 85 | env: 86 | SLACK_CHANNEL: "#ci-notifications" 87 | SLACK_MESSAGE: "CI Workflow failed for ${{ github.repository }} on branch ${{ github.ref }} at job ${{ github.job }}." 88 | SLACK_USERNAME: "GitHub CI" 89 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }} 90 | -------------------------------------------------------------------------------- /.github/workflows/ci-fork.yml: -------------------------------------------------------------------------------- 1 | name: "CI Fork and Util tests" 2 | 3 | on: 4 | schedule: 5 | - cron: "0 3 * * 1,3,5" # at 3:00 AM UTC on Monday, Wednesday and Friday 6 | 7 | jobs: 8 | lint: 9 | uses: "sablier-labs/gha-utils/.github/workflows/evm-lint.yml@main" 10 | 11 | build: 12 | uses: "sablier-labs/gha-utils/.github/workflows/forge-build.yml@main" 13 | 14 | test-fork: 15 | needs: ["lint", "build"] 16 | secrets: 17 | MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} 18 | uses: "sablier-labs/gha-utils/.github/workflows/forge-test.yml@main" 19 | with: 20 | foundry-fuzz-runs: 100 21 | foundry-profile: "test-optimized" 22 | fuzz-seed: true 23 | match-path: "tests/fork/**/*.sol" 24 | name: "Fork tests" 25 | 26 | test-utils: 27 | needs: ["lint", "build"] 28 | uses: "sablier-labs/gha-utils/.github/workflows/forge-test.yml@main" 29 | with: 30 | foundry-profile: "test-optimized" 31 | match-path: "tests/utils/**/*.sol" 32 | name: "Utils tests" 33 | 34 | notify-on-failure: 35 | if: failure() 36 | needs: ["lint", "build", "test-fork", "test-utils"] 37 | runs-on: "ubuntu-latest" 38 | steps: 39 | - name: "Send Slack notification" 40 | uses: "rtCamp/action-slack-notify@v2" 41 | env: 42 | SLACK_CHANNEL: "#ci-notifications" 43 | SLACK_MESSAGE: "CI Workflow failed for ${{ github.repository }} on branch ${{ github.ref }} at job ${{ github.job }}." 44 | SLACK_USERNAME: "GitHub CI" 45 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }} 46 | -------------------------------------------------------------------------------- /.github/workflows/ci-multibuild.yml: -------------------------------------------------------------------------------- 1 | name: "Multibuild" 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 3 * * 0" # at 3:00am UTC every Sunday 7 | 8 | jobs: 9 | multibuild: 10 | runs-on: "ubuntu-latest" 11 | steps: 12 | - name: "Check out the repo" 13 | uses: "actions/checkout@v4" 14 | 15 | - name: "Install Bun" 16 | uses: "oven-sh/setup-bun@v2" 17 | with: 18 | bun-version: "latest" 19 | - name: "Install the Node.js dependencies" 20 | run: "bun install --frozen-lockfile" 21 | 22 | - name: "Check that the project can be built with multiple Solidity versions" 23 | uses: "PaulRBerg/foundry-multibuild@v1" 24 | with: 25 | min: "0.8.22" 26 | max: "0.8.29" 27 | skip-test: "true" 28 | -------------------------------------------------------------------------------- /.github/workflows/ci-slither.yml: -------------------------------------------------------------------------------- 1 | name: "CI Slither" 2 | 3 | on: 4 | schedule: 5 | - cron: "0 3 * * 0" # at 3:00am UTC every Sunday 6 | 7 | jobs: 8 | lint: 9 | uses: "sablier-labs/gha-utils/.github/workflows/evm-lint.yml@main" 10 | 11 | slither-analyze: 12 | needs: "lint" 13 | uses: "sablier-labs/gha-utils/.github/workflows/slither-analyze.yml@main" 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | 3 | concurrency: 4 | cancel-in-progress: true 5 | group: ${{github.workflow}}-${{github.ref}} 6 | 7 | on: 8 | workflow_dispatch: 9 | pull_request: 10 | push: 11 | branches: 12 | - "main" 13 | - "staging" 14 | - "staging-blast" 15 | 16 | jobs: 17 | lint: 18 | uses: "sablier-labs/gha-utils/.github/workflows/evm-lint.yml@main" 19 | 20 | build: 21 | uses: "sablier-labs/gha-utils/.github/workflows/forge-build.yml@main" 22 | 23 | test-bulloak: 24 | needs: ["lint", "build"] 25 | if: needs.build.outputs.cache-status != 'primary' 26 | uses: "sablier-labs/gha-utils/.github/workflows/bulloak-check.yml@main" 27 | with: 28 | skip-modifiers: true 29 | tree-path: "tests" 30 | 31 | test-unit: 32 | needs: ["lint", "build"] 33 | if: needs.build.outputs.cache-status != 'primary' 34 | uses: "sablier-labs/gha-utils/.github/workflows/forge-test.yml@main" 35 | with: 36 | foundry-fuzz-runs: 2000 37 | foundry-profile: "test-optimized" 38 | match-path: "tests/unit/**/*.sol" 39 | name: "Unit tests" 40 | 41 | test-integration: 42 | needs: ["lint", "build"] 43 | if: needs.build.outputs.cache-status != 'primary' 44 | uses: "sablier-labs/gha-utils/.github/workflows/forge-test.yml@main" 45 | with: 46 | foundry-fuzz-runs: 2000 47 | foundry-profile: "test-optimized" 48 | match-path: "tests/integration/**/*.sol" 49 | name: "Integration tests" 50 | 51 | test-invariant: 52 | needs: ["lint", "build"] 53 | if: needs.build.outputs.cache-status != 'primary' 54 | uses: "sablier-labs/gha-utils/.github/workflows/forge-test.yml@main" 55 | with: 56 | foundry-profile: "test-optimized" 57 | match-path: "tests/invariant/**/*.sol" 58 | name: "Invariant tests" 59 | 60 | test-fork: 61 | needs: ["lint", "build"] 62 | if: needs.build.outputs.cache-status != 'primary' 63 | secrets: 64 | MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} 65 | uses: "sablier-labs/gha-utils/.github/workflows/forge-test.yml@main" 66 | with: 67 | foundry-fuzz-runs: 20 68 | foundry-profile: "test-optimized" 69 | match-path: "tests/fork/**/*.sol" 70 | name: "Fork tests" 71 | 72 | coverage: 73 | needs: ["lint", "build"] 74 | if: needs.build.outputs.cache-status != 'primary' 75 | secrets: 76 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 77 | MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} 78 | uses: "sablier-labs/gha-utils/.github/workflows/forge-coverage.yml@main" 79 | with: 80 | match-path: "tests/{fork,integration,unit}/**/*.sol" 81 | -------------------------------------------------------------------------------- /.github/workflows/cron-stale.yml: -------------------------------------------------------------------------------- 1 | name: "Cron: Close Stale Issues and PRs" 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 3 * * 0" # at 3:00am UTC every Sunday 7 | 8 | jobs: 9 | cron-stale: 10 | uses: "sablier-labs/gha-utils/.github/workflows/cron-stale.yml@main" 11 | -------------------------------------------------------------------------------- /.github/workflows/generate-svg.yml: -------------------------------------------------------------------------------- 1 | name: "Generate SVG" 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | progress: 7 | description: "The streamed amount as a numerical percentage with 4 implied decimals." 8 | required: true 9 | status: 10 | description: "The status of the stream, as a string." 11 | required: true 12 | streamed: 13 | description: "The abbreviated streamed amount, as a string." 14 | required: true 15 | duration: 16 | description: "The total duration of the stream in days, as a number." 17 | required: true 18 | 19 | jobs: 20 | generate-svg: 21 | runs-on: "ubuntu-latest" 22 | steps: 23 | - name: "Check out the repo" 24 | uses: "actions/checkout@v4" 25 | 26 | - name: "Install Foundry" 27 | uses: "foundry-rs/foundry-toolchain@v1" 28 | 29 | - name: "Generate an NFT SVG using the user-provided parameters" 30 | run: >- 31 | forge script scripts/solidity/GenerateSVG.s.sol 32 | --sig "run(uint256,string,string,uint256)" 33 | "${{ fromJSON(inputs.progress || true) }}", 34 | "${{ fromJSON(inputs.status || true) }}" 35 | "${{ fromJSON(inputs.streamed || true) }}" 36 | "${{ fromJSON(inputs.duration || true) }}" 37 | 38 | - name: "Add workflow summary" 39 | run: | 40 | echo "## Result" >> $GITHUB_STEP_SUMMARY 41 | echo "✅ Done" >> $GITHUB_STEP_SUMMARY 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # directories 2 | artifacts 3 | artifacts-zk 4 | broadcast 5 | cache 6 | cache_hardhat-zk 7 | coverage 8 | deployments 9 | deployments-zk 10 | docs 11 | node_modules 12 | out 13 | out-optimized 14 | out-svg 15 | typechain-types 16 | 17 | # files 18 | *.env 19 | *.env.deployment 20 | *.log 21 | .DS_Store 22 | .pnp.* 23 | deployments.md 24 | lcov.info 25 | package-lock.json 26 | pnpm-lock.yaml 27 | yarn.lock 28 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | bun lint-staged 3 | -------------------------------------------------------------------------------- /.lintstagedrc.yml: -------------------------------------------------------------------------------- 1 | "*.{json,md,svg,yml}": "prettier --write" 2 | "*.sol": 3 | - "bun solhint {benchmark,script,src,tests}/**/*.sol --fix --noPrompt" 4 | - "forge fmt" 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # directories 2 | .husky 3 | broadcast 4 | cache 5 | coverage 6 | docs 7 | node_modules 8 | out 9 | out-optimized 10 | out-svg 11 | 12 | # files 13 | *.env 14 | *.log 15 | *.sol 16 | .DS_Store 17 | .pnp.* 18 | bun.lockb 19 | lcov.info 20 | package-lock.json 21 | pnpm-lock.yaml 22 | yarn.lock 23 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | printWidth: 120 2 | trailingComma: "all" 3 | overrides: 4 | - files: "*.svg" 5 | options: 6 | parser: "html" 7 | - files: "*.md" 8 | options: 9 | proseWrap: "always" 10 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "avoid-low-level-calls": "off", 5 | "code-complexity": ["error", 10], 6 | "compiler-version": ["error", ">=0.8.22"], 7 | "contract-name-camelcase": "off", 8 | "const-name-snakecase": "off", 9 | "func-name-mixedcase": "off", 10 | "func-visibility": ["error", { "ignoreConstructors": true }], 11 | "gas-custom-errors": "off", 12 | "max-states-count": ["warn", 20], 13 | "max-line-length": ["error", 124], 14 | "named-parameters-mapping": "warn", 15 | "no-empty-blocks": "off", 16 | "not-rely-on-time": "off", 17 | "one-contract-per-file": "off", 18 | "var-name-mixedcase": "off" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[solidity]": { 3 | "editor.defaultFormatter": "NomicFoundation.hardhat-solidity" 4 | }, 5 | "[toml]": { 6 | "editor.defaultFormatter": "tamasfe.even-better-toml" 7 | }, 8 | "files.associations": { 9 | ".gas-snapshot": "julia" 10 | }, 11 | "editor.formatOnSave": true, 12 | "prettier.documentSelectors": ["**/*.svg"], 13 | "search.exclude": { 14 | "**/node_modules": true 15 | }, 16 | "solidity.formatter": "forge" 17 | } 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Feel free to dive in! [Open](https://github.com/sablier-labs/lockup/issues/new) an issue, 4 | [start](https://github.com/sablier-labs/lockup/discussions/new) a discussion or submit a PR. For any informal concerns 5 | or feedback, please join our [Discord server](https://discord.gg/bSwRCwWRsT). 6 | 7 | Contributions to Sablier Lockup are welcome by anyone interested in writing more tests, improving readability, 8 | optimizing for gas efficiency, or extending the protocol via new features. 9 | 10 | ## Pre Requisites 11 | 12 | You will need the following software on your machine: 13 | 14 | - [Git](https://git-scm.com/downloads) 15 | - [Foundry](https://github.com/foundry-rs/foundry) 16 | - [Node.Js](https://nodejs.org/en/download/) 17 | - [Bun](https://bun.sh/) 18 | 19 | In addition, familiarity with [Solidity](https://soliditylang.org/) is requisite. 20 | 21 | ## Set Up 22 | 23 | Clone this repository including submodules: 24 | 25 | ```shell 26 | $ git clone --recurse-submodules -j8 git@github.com:sablier-labs/lockup.git 27 | ``` 28 | 29 | Then, inside the project's directory, run this to install the Node.js dependencies and build the contracts: 30 | 31 | ```shell 32 | $ bun install 33 | $ bun run build 34 | ``` 35 | 36 | Switch to the `staging` branch, where all development work should be done: 37 | 38 | ```shell 39 | $ git switch staging 40 | ``` 41 | 42 | Now you can start making changes. 43 | 44 | To see a list of all available scripts: 45 | 46 | ```shell 47 | $ bun run 48 | ``` 49 | 50 | ## Pull Requests 51 | 52 | When making a pull request, ensure that: 53 | 54 | - The base branch is `staging`. 55 | - All tests pass. 56 | - Concrete tests are generated using Bulloak and the Branching Tree Technique (BTT). 57 | - You can learn more about this on the [Bulloak website](https://bulloak.dev). 58 | - If you modify a test tree, use this command to generate the corresponding test contract that complies with BTT: 59 | `bulloak scaffold -wf /path/to/file.tree` 60 | - Code coverage remains the same or greater. 61 | - All new code adheres to the style guide: 62 | - All lint checks pass. 63 | - Code is thoroughly commented with NatSpec where relevant. 64 | - If making a change to the contracts: 65 | - Gas snapshots are provided and demonstrate an improvement (or an acceptable deficit given other improvements). 66 | - Reference contracts are modified correspondingly if relevant. 67 | - New tests are included for all new features or code paths. 68 | - A descriptive summary of the PR has been provided. 69 | 70 | ## Environment Variables 71 | 72 | ### Local setup 73 | 74 | To build locally, follow the [`.env.example`](./.env.example) file to create a `.env` file at the root of the repo and 75 | populate it with the appropriate environment values. You need to provide your mnemonic phrase and a few API keys. 76 | 77 | ### Deployment 78 | 79 | To make CI work in your pull request, ensure that the necessary environment variables are configured in your forked 80 | repository's secrets. Please add the following variable in your GitHub Secrets: 81 | 82 | - MAINNET_RPC_URL 83 | 84 | ## Integration with VSCode: 85 | 86 | Install the following VSCode extensions: 87 | 88 | - [esbenp.prettier-vscode](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) 89 | - [hardhat-solidity](https://marketplace.visualstudio.com/items?itemName=NomicFoundation.hardhat-solidity) 90 | - [vscode-tree-language](https://marketplace.visualstudio.com/items?itemName=CTC.vscode-tree-extension) 91 | -------------------------------------------------------------------------------- /benchmark/EstimateMaxCount.t.sol: -------------------------------------------------------------------------------- 1 | // solhint-disable no-console 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { console2 } from "forge-std/src/console2.sol"; 5 | import { Test } from "forge-std/src/Test.sol"; 6 | 7 | import { Lockup_Dynamic_Gas_Test } from "./LockupDynamic.Gas.t.sol"; 8 | 9 | /// @notice Structure to group the block gas limit and chain id. 10 | struct ChainInfo { 11 | uint256 blockGasLimit; 12 | uint256 chainId; 13 | } 14 | 15 | contract EstimateMaxCount is Test { 16 | // Buffer gas units to be deducted from the block gas limit so that the max count never exceeds the block limit. 17 | uint256 public constant BUFFER_GAS = 1_000_000; 18 | 19 | // Initial guess for the maximum number of segments/tranches. 20 | uint128 public constant INITIAL_GUESS = 240; 21 | 22 | /// @dev List of chains with their block gas limit. 23 | ChainInfo[] public chains; 24 | 25 | constructor() { 26 | chains.push(ChainInfo({ blockGasLimit: 32_000_000, chainId: 42_161 })); // Arbitrum 27 | chains.push(ChainInfo({ blockGasLimit: 15_000_000, chainId: 43_114 })); // Avalanche 28 | chains.push(ChainInfo({ blockGasLimit: 60_000_000, chainId: 8453 })); // Base 29 | chains.push(ChainInfo({ blockGasLimit: 30_000_000, chainId: 81_457 })); // Blast 30 | chains.push(ChainInfo({ blockGasLimit: 138_000_000, chainId: 56 })); // BNB 31 | chains.push(ChainInfo({ blockGasLimit: 30_000_000, chainId: 1 })); // Ethereum 32 | chains.push(ChainInfo({ blockGasLimit: 17_000_000, chainId: 100 })); // Gnosis 33 | chains.push(ChainInfo({ blockGasLimit: 30_000_000, chainId: 10 })); // Optimism 34 | chains.push(ChainInfo({ blockGasLimit: 30_000_000, chainId: 137 })); // Polygon 35 | chains.push(ChainInfo({ blockGasLimit: 10_000_000, chainId: 534_352 })); // Scroll 36 | chains.push(ChainInfo({ blockGasLimit: 30_000_000, chainId: 11_155_111 })); // Sepolia 37 | } 38 | 39 | /// @notice Estimate the maximum number of segments allowed in LockupDynamic. 40 | function test_EstimateSegments() public { 41 | Lockup_Dynamic_Gas_Test lockupDynamicGasTest = new Lockup_Dynamic_Gas_Test(); 42 | lockupDynamicGasTest.setUp(); 43 | 44 | for (uint256 i = 0; i < chains.length; ++i) { 45 | uint128 count = INITIAL_GUESS; 46 | 47 | // Subtract `BUFFER_GAS` from `blockGasLimit` as an additional precaution to account for the dynamic gas for 48 | // ether transfer on different chains. 49 | uint256 blockGasLimit = chains[i].blockGasLimit - BUFFER_GAS; 50 | 51 | uint256 gasConsumed = 0; 52 | uint256 lastGasConsumed = 0; 53 | while (blockGasLimit > gasConsumed) { 54 | count += 10; 55 | lastGasConsumed = gasConsumed; 56 | 57 | // Estimate the gas consumed by adding 10 segments. 58 | gasConsumed = lockupDynamicGasTest.computeGas_CreateWithDurationsLD(count + 10); 59 | } 60 | 61 | console2.log("count: %d and gasUsed: %d and chainId: %d", count, lastGasConsumed, chains[i].chainId); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /benchmark/results/SablierBatchLockup.md: -------------------------------------------------------------------------------- 1 | # Benchmarks for BatchLockup 2 | 3 | | Function | Lockup Type | Segments/Tranches | Batch Size | Gas Usage | 4 | | ------------------------ | --------------- | ----------------- | ---------- | --------- | 5 | | `createWithDurationsLL` | Lockup Linear | N/A | 5 | 937003 | 6 | | `createWithTimestampsLL` | Lockup Linear | N/A | 5 | 898916 | 7 | | `createWithDurationsLD` | Lockup Dynamic | 24 | 5 | 4123217 | 8 | | `createWithTimestampsLD` | Lockup Dynamic | 24 | 5 | 3895052 | 9 | | `createWithDurationsLT` | Lockup Tranched | 24 | 5 | 4013105 | 10 | | `createWithTimestampsLT` | Lockup Tranched | 24 | 5 | 3822707 | 11 | | `createWithDurationsLL` | Lockup Linear | N/A | 10 | 1740955 | 12 | | `createWithTimestampsLL` | Lockup Linear | N/A | 10 | 1747416 | 13 | | `createWithDurationsLD` | Lockup Dynamic | 24 | 10 | 8202890 | 14 | | `createWithTimestampsLD` | Lockup Dynamic | 24 | 10 | 7741699 | 15 | | `createWithDurationsLT` | Lockup Tranched | 24 | 10 | 7974447 | 16 | | `createWithTimestampsLT` | Lockup Tranched | 24 | 10 | 7597402 | 17 | | `createWithDurationsLL` | Lockup Linear | N/A | 20 | 3433786 | 18 | | `createWithTimestampsLL` | Lockup Linear | N/A | 20 | 3447467 | 19 | | `createWithDurationsLD` | Lockup Dynamic | 24 | 20 | 16380960 | 20 | | `createWithTimestampsLD` | Lockup Dynamic | 24 | 20 | 15440827 | 21 | | `createWithDurationsLT` | Lockup Tranched | 24 | 20 | 15896070 | 22 | | `createWithTimestampsLT` | Lockup Tranched | 24 | 20 | 15152551 | 23 | | `createWithDurationsLL` | Lockup Linear | N/A | 30 | 5125959 | 24 | | `createWithTimestampsLL` | Lockup Linear | N/A | 30 | 5155292 | 25 | | `createWithDurationsLD` | Lockup Dynamic | 24 | 30 | 24603376 | 26 | | `createWithTimestampsLD` | Lockup Dynamic | 24 | 30 | 23157026 | 27 | | `createWithDurationsLT` | Lockup Tranched | 24 | 30 | 23818565 | 28 | | `createWithTimestampsLT` | Lockup Tranched | 24 | 30 | 22725003 | 29 | | `createWithDurationsLL` | Lockup Linear | N/A | 50 | 8532644 | 30 | | `createWithTimestampsLL` | Lockup Linear | N/A | 50 | 8582221 | 31 | | `createWithDurationsLD` | Lockup Dynamic | 12 | 50 | 24275049 | 32 | | `createWithTimestampsLD` | Lockup Dynamic | 12 | 50 | 23058857 | 33 | | `createWithDurationsLT` | Lockup Tranched | 12 | 50 | 23611123 | 34 | | `createWithTimestampsLT` | Lockup Tranched | 12 | 50 | 22718936 | 35 | -------------------------------------------------------------------------------- /benchmark/results/SablierLockup_Dynamic.md: -------------------------------------------------------------------------------- 1 | # Benchmarks for the Lockup Dynamic model 2 | 3 | | Implementation | Gas Usage | 4 | | ------------------------------------------------------------ | --------- | 5 | | `burn` | 16141 | 6 | | `cancel` | 65381 | 7 | | `renounce` | 27721 | 8 | | `createWithDurationsLD` (2 segments) (Broker fee set) | 216788 | 9 | | `createWithDurationsLD` (2 segments) (Broker fee not set) | 200461 | 10 | | `createWithTimestampsLD` (2 segments) (Broker fee set) | 197652 | 11 | | `createWithTimestampsLD` (2 segments) (Broker fee not set) | 192627 | 12 | | `withdraw` (2 segments) (After End Time) (by Recipient) | 23885 | 13 | | `withdraw` (2 segments) (Before End Time) (by Recipient) | 29903 | 14 | | `withdraw` (2 segments) (After End Time) (by Anyone) | 19175 | 15 | | `withdraw` (2 segments) (Before End Time) (by Anyone) | 29992 | 16 | | `createWithDurationsLD` (10 segments) (Broker fee set) | 422199 | 17 | | `createWithDurationsLD` (10 segments) (Broker fee not set) | 417189 | 18 | | `createWithTimestampsLD` (10 segments) (Broker fee set) | 402125 | 19 | | `createWithTimestampsLD` (10 segments) (Broker fee not set) | 397126 | 20 | | `withdraw` (10 segments) (After End Time) (by Recipient) | 24167 | 21 | | `withdraw` (10 segments) (Before End Time) (by Recipient) | 37190 | 22 | | `withdraw` (10 segments) (After End Time) (by Anyone) | 24278 | 23 | | `withdraw` (10 segments) (Before End Time) (by Anyone) | 37279 | 24 | | `createWithDurationsLD` (100 segments) (Broker fee set) | 2898563 | 25 | | `createWithDurationsLD` (100 segments) (Broker fee not set) | 2894573 | 26 | | `createWithTimestampsLD` (100 segments) (Broker fee set) | 2706641 | 27 | | `createWithTimestampsLD` (100 segments) (Broker fee not set) | 2702660 | 28 | | `withdraw` (100 segments) (After End Time) (by Recipient) | 81920 | 29 | | `withdraw` (100 segments) (Before End Time) (by Recipient) | 119603 | 30 | | `withdraw` (100 segments) (After End Time) (by Anyone) | 82009 | 31 | | `withdraw` (100 segments) (Before End Time) (by Anyone) | 119692 | 32 | -------------------------------------------------------------------------------- /benchmark/results/SablierLockup_Linear.md: -------------------------------------------------------------------------------- 1 | # Benchmarks for the Lockup Linear model 2 | 3 | | Implementation | Gas Usage | 4 | | ------------------------------------------------------------- | --------- | 5 | | `burn` | 16141 | 6 | | `cancel` | 65381 | 7 | | `renounce` | 27721 | 8 | | `createWithDurationsLL` (Broker fee set) (cliff not set) | 138649 | 9 | | `createWithDurationsLL` (Broker fee not set) (cliff not set) | 122287 | 10 | | `createWithDurationsLL` (Broker fee set) (cliff set) | 169335 | 11 | | `createWithDurationsLL` (Broker fee not set) (cliff set) | 164278 | 12 | | `createWithTimestampsLL` (Broker fee set) (cliff not set) | 125100 | 13 | | `createWithTimestampsLL` (Broker fee not set) (cliff not set) | 120038 | 14 | | `createWithTimestampsLL` (Broker fee set) (cliff set) | 169682 | 15 | | `createWithTimestampsLL` (Broker fee not set) (cliff set) | 164614 | 16 | | `withdraw` (After End Time) (by Recipient) | 33179 | 17 | | `withdraw` (Before End Time) (by Recipient) | 23303 | 18 | | `withdraw` (After End Time) (by Anyone) | 29561 | 19 | | `withdraw` (Before End Time) (by Anyone) | 22815 | 20 | -------------------------------------------------------------------------------- /benchmark/results/SablierLockup_Tranched.md: -------------------------------------------------------------------------------- 1 | # Benchmarks for the Lockup Tranched model 2 | 3 | | Implementation | Gas Usage | 4 | | ------------------------------------------------------------ | --------- | 5 | | `burn` | 16141 | 6 | | `cancel` | 65381 | 7 | | `renounce` | 27721 | 8 | | `createWithDurationsLT` (2 tranches) (Broker fee set) | 215994 | 9 | | `createWithDurationsLT` (2 tranches) (Broker fee not set) | 199665 | 10 | | `createWithTimestampsLT` (2 tranches) (Broker fee set) | 196988 | 11 | | `createWithTimestampsLT` (2 tranches) (Broker fee not set) | 191964 | 12 | | `withdraw` (2 tranches) (After End Time) (by Recipient) | 23599 | 13 | | `withdraw` (2 tranches) (Before End Time) (by Recipient) | 18503 | 14 | | `withdraw` (2 tranches) (After End Time) (by Anyone) | 18889 | 15 | | `withdraw` (2 tranches) (Before End Time) (by Anyone) | 18592 | 16 | | `createWithDurationsLT` (10 tranches) (Broker fee set) | 414411 | 17 | | `createWithDurationsLT` (10 tranches) (Broker fee not set) | 409394 | 18 | | `createWithTimestampsLT` (10 tranches) (Broker fee set) | 397045 | 19 | | `createWithTimestampsLT` (10 tranches) (Broker fee not set) | 392026 | 20 | | `withdraw` (10 tranches) (After End Time) (by Recipient) | 23318 | 21 | | `withdraw` (10 tranches) (Before End Time) (by Recipient) | 25403 | 22 | | `withdraw` (10 tranches) (After End Time) (by Anyone) | 23427 | 23 | | `withdraw` (10 tranches) (Before End Time) (by Anyone) | 25492 | 24 | | `createWithDurationsLT` (100 tranches) (Broker fee set) | 2808652 | 25 | | `createWithDurationsLT` (100 tranches) (Broker fee not set) | 2804166 | 26 | | `createWithTimestampsLT` (100 tranches) (Broker fee set) | 2649659 | 27 | | `createWithTimestampsLT` (100 tranches) (Broker fee not set) | 2645177 | 28 | | `withdraw` (100 tranches) (After End Time) (by Recipient) | 74530 | 29 | | `withdraw` (100 tranches) (Before End Time) (by Recipient) | 103255 | 30 | | `withdraw` (100 tranches) (After End Time) (by Anyone) | 74619 | 31 | | `withdraw` (100 tranches) (Before End Time) (by Anyone) | 103344 | 32 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sablier-labs/lockup/d85521f5615f6c19612ff250ee89c57b9afa6aa2/bun.lockb -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: false 3 | comment: false 4 | coverage: 5 | status: 6 | patch: off 7 | ignore: 8 | - "script" 9 | - "src/libraries/NFTSVG.sol" 10 | - "src/libraries/SVGElements.sol" 11 | - "tests" 12 | -------------------------------------------------------------------------------- /funding.json: -------------------------------------------------------------------------------- 1 | { 2 | "opRetro": { 3 | "projectId": "0x7262ed9c020b3b41ac7ba405aab4ff37575f8b6f975ebed2e65554a08419f8f4" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sablier/lockup", 3 | "description": "Core smart contracts of the Lockup token distribution protocol", 4 | "license": "BUSL-1.1", 5 | "version": "2.0.1", 6 | "author": { 7 | "name": "Sablier Labs Ltd", 8 | "url": "https://sablier.com" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/sablier-labs/lockup/issues" 12 | }, 13 | "dependencies": { 14 | "@openzeppelin/contracts": "5.0.2", 15 | "@prb/math": "4.1.0" 16 | }, 17 | "devDependencies": { 18 | "forge-std": "github:foundry-rs/forge-std#v1.8.2", 19 | "husky": "^9.1.4", 20 | "lint-staged": "^15.2.8", 21 | "prettier": "^3.3.2", 22 | "solady": "0.0.208", 23 | "solarray": "github:evmcheb/solarray#a547630", 24 | "solhint": "^5.0.3" 25 | }, 26 | "files": [ 27 | "artifacts", 28 | "src", 29 | "tests/utils", 30 | "CHANGELOG.md", 31 | "LICENSE-GPL.md" 32 | ], 33 | "homepage": "https://github.com/sablier-labs/lockup#readme", 34 | "keywords": [ 35 | "asset-distribution", 36 | "asset-streaming", 37 | "blockchain", 38 | "cryptoasset-streaming", 39 | "cryptoassets", 40 | "ethereum", 41 | "foundry", 42 | "lockup", 43 | "money-streaming", 44 | "real-time-finance", 45 | "sablier", 46 | "sablier-v2", 47 | "sablier-lockup", 48 | "smart-contracts", 49 | "solidity", 50 | "token-distribution", 51 | "token-streaming", 52 | "token-vesting", 53 | "vesting", 54 | "web3" 55 | ], 56 | "peerDependencies": { 57 | "@prb/math": "4.x.x" 58 | }, 59 | "publishConfig": { 60 | "access": "public" 61 | }, 62 | "repository": "github.com/sablier-labs/lockup", 63 | "scripts": { 64 | "benchmark": "bun run build:optimized && FOUNDRY_PROFILE=benchmark forge test --mt testGas && bun run prettier:write", 65 | "build": "forge build", 66 | "build:optimized": "FOUNDRY_PROFILE=optimized forge build", 67 | "build:smt": "FOUNDRY_PROFILE=smt forge build", 68 | "clean": "rm -rf artifacts broadcast cache docs out out-optimized out-svg", 69 | "lint": "bun run lint:sol && bun run prettier:check", 70 | "lint:fix": "bun run lint:sol:fix && forge fmt", 71 | "lint:sol": "forge fmt --check && bun solhint \"{benchmark,script,src,tests}/**/*.sol\"", 72 | "lint:sol:fix": "bun solhint \"{benchmark,script,src,tests}/**/*.sol\" --fix --noPrompt", 73 | "prepack": "bun install && bash ./shell/prepare-artifacts.sh", 74 | "prepare": "husky", 75 | "prettier:check": "prettier --check \"**/*.{json,md,svg,yml}\"", 76 | "prettier:write": "prettier --write \"**/*.{json,md,svg,yml}\"", 77 | "test": "forge test", 78 | "test:lite": "FOUNDRY_PROFILE=lite forge test", 79 | "test:optimized": "bun run build:optimized && FOUNDRY_PROFILE=test-optimized forge test" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | @openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ 2 | @prb/math/=node_modules/@prb/math/ 3 | forge-std/=node_modules/forge-std/ 4 | solady/=node_modules/solady/ 5 | solarray/=node_modules/solarray/ 6 | -------------------------------------------------------------------------------- /script/DeployBatchLockup.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { SablierBatchLockup } from "../src/SablierBatchLockup.sol"; 5 | 6 | import { BaseScript } from "./Base.s.sol"; 7 | 8 | contract DeployBatchLockup is BaseScript { 9 | /// @dev Deploy via Forge. 10 | function run() public broadcast returns (SablierBatchLockup batchLockup) { 11 | batchLockup = new SablierBatchLockup(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /script/DeployDeterministicBatchLockup.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { SablierBatchLockup } from "../src/SablierBatchLockup.sol"; 5 | 6 | import { BaseScript } from "./Base.s.sol"; 7 | 8 | contract DeployDeterministicBatchLockup is BaseScript { 9 | /// @dev Deploy via Forge. 10 | function run() public broadcast returns (SablierBatchLockup batchLockup) { 11 | batchLockup = new SablierBatchLockup{ salt: SALT }(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /script/DeployDeterministicLockup.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { ILockupNFTDescriptor } from "../src/interfaces/ILockupNFTDescriptor.sol"; 5 | import { SablierLockup } from "../src/SablierLockup.sol"; 6 | import { BaseScript } from "./Base.s.sol"; 7 | 8 | /// @notice Deploys {SablierLockup} at a deterministic address across chains. 9 | /// @dev Reverts if the contract has already been deployed. 10 | contract DeployDeterministicLockup is BaseScript { 11 | function run( 12 | address initialAdmin, 13 | ILockupNFTDescriptor nftDescriptor 14 | ) 15 | public 16 | broadcast 17 | returns (SablierLockup lockup) 18 | { 19 | lockup = new SablierLockup{ salt: SALT }(initialAdmin, nftDescriptor, maxCountMap[block.chainid]); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /script/DeployDeterministicNFTDescriptor.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { LockupNFTDescriptor } from "../src/LockupNFTDescriptor.sol"; 5 | 6 | import { BaseScript } from "./Base.s.sol"; 7 | 8 | /// @dev Deploys {LockupNFTDescriptor} at a deterministic address across chains. 9 | /// @dev Reverts if the contract has already been deployed. 10 | contract DeployDeterministicNFTDescriptor is BaseScript { 11 | function run() public broadcast returns (LockupNFTDescriptor nftDescriptor) { 12 | nftDescriptor = new LockupNFTDescriptor{ salt: SALT }(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /script/DeployDeterministicProtocol.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { LockupNFTDescriptor } from "../src/LockupNFTDescriptor.sol"; 5 | import { SablierBatchLockup } from "../src/SablierBatchLockup.sol"; 6 | import { SablierLockup } from "../src/SablierLockup.sol"; 7 | 8 | import { BaseScript } from "./Base.s.sol"; 9 | 10 | /// @notice Deploys the Lockup Protocol at deterministic addresses across chains. 11 | contract DeployDeterministicProtocol is BaseScript { 12 | /// @dev Deploys the protocol with the admin set in `adminMap`. 13 | function run() 14 | public 15 | returns (LockupNFTDescriptor nftDescriptor, SablierLockup lockup, SablierBatchLockup batchLockup) 16 | { 17 | address initialAdmin = adminMap[block.chainid]; 18 | (nftDescriptor, lockup, batchLockup) = _run(initialAdmin); 19 | } 20 | 21 | /// @dev Deploys the protocol with the given `initialAdmin`. 22 | function run(address initialAdmin) 23 | public 24 | returns (LockupNFTDescriptor nftDescriptor, SablierLockup lockup, SablierBatchLockup batchLockup) 25 | { 26 | (nftDescriptor, lockup, batchLockup) = _run(initialAdmin); 27 | } 28 | 29 | /// @dev Common logic for the run functions. 30 | function _run(address initialAdmin) 31 | internal 32 | broadcast 33 | returns (LockupNFTDescriptor nftDescriptor, SablierLockup lockup, SablierBatchLockup batchLockup) 34 | { 35 | batchLockup = new SablierBatchLockup{ salt: SALT }(); 36 | nftDescriptor = new LockupNFTDescriptor{ salt: SALT }(); 37 | lockup = new SablierLockup{ salt: SALT }(initialAdmin, nftDescriptor, maxCountMap[block.chainid]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /script/DeployLockup.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { ILockupNFTDescriptor } from "../src/interfaces/ILockupNFTDescriptor.sol"; 5 | import { SablierLockup } from "../src/SablierLockup.sol"; 6 | 7 | import { BaseScript } from "./Base.s.sol"; 8 | 9 | /// @notice Deploys {SablierLockup} contract. 10 | contract DeployLockup is BaseScript { 11 | function run( 12 | address initialAdmin, 13 | ILockupNFTDescriptor nftDescriptor 14 | ) 15 | public 16 | broadcast 17 | returns (SablierLockup lockup) 18 | { 19 | lockup = new SablierLockup(initialAdmin, nftDescriptor, maxCountMap[block.chainid]); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /script/DeployNFTDescriptor.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { LockupNFTDescriptor } from "../src/LockupNFTDescriptor.sol"; 5 | 6 | import { BaseScript } from "./Base.s.sol"; 7 | 8 | /// @notice Deploys {LockupNFTDescriptor} contract. 9 | contract DeployNFTDescriptor is BaseScript { 10 | function run() public broadcast returns (LockupNFTDescriptor nftDescriptor) { 11 | nftDescriptor = new LockupNFTDescriptor(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /script/DeployProtocol.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { LockupNFTDescriptor } from "../src/LockupNFTDescriptor.sol"; 5 | import { SablierBatchLockup } from "../src/SablierBatchLockup.sol"; 6 | import { SablierLockup } from "../src/SablierLockup.sol"; 7 | 8 | import { BaseScript } from "./Base.s.sol"; 9 | 10 | /// @notice Deploys the Lockup Protocol. 11 | contract DeployProtocol is BaseScript { 12 | /// @dev Deploys the protocol with the admin set in `adminMap`. 13 | function run() 14 | public 15 | returns (LockupNFTDescriptor nftDescriptor, SablierLockup lockup, SablierBatchLockup batchLockup) 16 | { 17 | address initialAdmin = adminMap[block.chainid]; 18 | (nftDescriptor, lockup, batchLockup) = _run(initialAdmin); 19 | } 20 | 21 | /// @dev Deploys the protocol with the given `initialAdmin`. 22 | function run(address initialAdmin) 23 | public 24 | returns (LockupNFTDescriptor nftDescriptor, SablierLockup lockup, SablierBatchLockup batchLockup) 25 | { 26 | (nftDescriptor, lockup, batchLockup) = _run(initialAdmin); 27 | } 28 | 29 | /// @dev Common logic for the run functions. 30 | function _run(address initialAdmin) 31 | internal 32 | broadcast 33 | returns (LockupNFTDescriptor nftDescriptor, SablierLockup lockup, SablierBatchLockup batchLockup) 34 | { 35 | batchLockup = new SablierBatchLockup(); 36 | nftDescriptor = new LockupNFTDescriptor(); 37 | lockup = new SablierLockup(initialAdmin, nftDescriptor, maxCountMap[block.chainid]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /script/GenerateSVG.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; 5 | import { NFTSVG } from "./../src/libraries/NFTSVG.sol"; 6 | import { SVGElements } from "./../src/libraries/SVGElements.sol"; 7 | import { LockupNFTDescriptor } from "./../src/LockupNFTDescriptor.sol"; 8 | import { BaseScript } from "././Base.s.sol"; 9 | 10 | /// @notice Generates an NFT SVG using the user-provided parameters. 11 | contract GenerateSVG is BaseScript, LockupNFTDescriptor { 12 | using Strings for address; 13 | using Strings for string; 14 | 15 | address internal constant DAI = address(uint160(uint256(keccak256("DAI")))); 16 | address internal constant LOCKUP = address(uint160(uint256(keccak256("SablierLockup")))); 17 | 18 | /// @param progress The streamed amount as a numerical percentage with 4 implied decimals. 19 | /// @param status The status of the stream, as a string. 20 | /// @param amount The abbreviated deposited amount, as a string. 21 | /// @param duration The total duration of the stream in days, as a number. 22 | function run( 23 | uint256 progress, 24 | string memory status, 25 | string memory amount, 26 | uint256 duration 27 | ) 28 | public 29 | view 30 | returns (string memory svg) 31 | { 32 | svg = NFTSVG.generateSVG( 33 | NFTSVG.SVGParams({ 34 | accentColor: generateAccentColor({ sablier: LOCKUP, streamId: uint256(keccak256(msg.data)) }), 35 | amount: string.concat(SVGElements.SIGN_GE, " ", amount), 36 | tokenAddress: DAI.toHexString(), 37 | tokenSymbol: "DAI", 38 | duration: calculateDurationInDays({ startTime: 0, endTime: duration * 1 days }), 39 | progress: stringifyPercentage(progress), 40 | progressNumerical: progress, 41 | lockupAddress: LOCKUP.toHexString(), 42 | status: status 43 | }) 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /shell/generate-svg-panoply.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Notes: 4 | # - Generates a panoply of SVGs with different accent colors and card contents. 5 | 6 | # Pre-requisites: 7 | # - foundry (https://getfoundry.sh) 8 | 9 | # Strict mode: https://gist.github.com/vncsna/64825d5609c146e80de8b1fd623011ca 10 | set -euo pipefail 11 | 12 | ./shell/generate-svg.sh 0 "Pending" "100" 5 13 | ./shell/generate-svg.sh 0 "Pending" "100" 21 14 | ./shell/generate-svg.sh 0 "Pending" "100" 565 15 | 16 | ./shell/generate-svg.sh 0 "Canceled" "100" 3 17 | ./shell/generate-svg.sh 0 "Canceled" "100" 3 18 | ./shell/generate-svg.sh 144 "Canceled" "29.81K" 24 19 | ./shell/generate-svg.sh 7231 "Canceled" "421.11K" 24 20 | 21 | ./shell/generate-svg.sh 15 "Streaming" "86.1K" 0 22 | ./shell/generate-svg.sh 42 "Streaming" "581" 0 23 | ./shell/generate-svg.sh 79 "Streaming" "66.01K" 0 24 | ./shell/generate-svg.sh 399 "Streaming" "314K" 0 25 | ./shell/generate-svg.sh 800 "Streaming" "50.04K" 0 26 | ./shell/generate-svg.sh 1030 "Streaming" "48.93M" 1021 27 | ./shell/generate-svg.sh 4235 "Streaming" "8.91M" 1 28 | ./shell/generate-svg.sh 5000 "Streaming" "1.5K" 1 29 | ./shell/generate-svg.sh 7291 "Streaming" "756.12T" 7211 30 | ./shell/generate-svg.sh 9999 "Streaming" "3.32K" 88 31 | ./shell/generate-svg.sh 4999 "Streaming" "999.45K" 10000 32 | 33 | ./shell/generate-svg.sh 10000 "Settled" "1" 892 34 | ./shell/generate-svg.sh 10000 "Settled" "14.94K" 11 35 | ./shell/generate-svg.sh 10000 "Settled" "733" 3402 36 | ./shell/generate-svg.sh 10000 "Settled" "645.01M" 3402 37 | ./shell/generate-svg.sh 10000 "Settled" "990.12B" 6503 38 | 39 | ./shell/generate-svg.sh 10000 "Depleted" "1" 892 40 | ./shell/generate-svg.sh 10000 "Depleted" "79.1B" 892 41 | ./shell/generate-svg.sh 4972 "Depleted" "29" 3402 42 | ./shell/generate-svg.sh 744 "Depleted" "343.01K" 3402 43 | ./shell/generate-svg.sh 10000 "Depleted" "84.1M" 6503 44 | -------------------------------------------------------------------------------- /shell/generate-svg.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Notes: 4 | # - There are four input arguments: progress, status, deposit amount, and duration 5 | 6 | # Pre-requisites: 7 | # - foundry (https://getfoundry.sh) 8 | # - sd (https://github.com/chmln/sd) 9 | 10 | # Strict mode: https://gist.github.com/vncsna/64825d5609c146e80de8b1fd623011ca 11 | set -euo pipefail 12 | 13 | # Load the arguments while using default values 14 | arg_progress=${1:-4235} 15 | arg_status=${2:-"Streaming"} 16 | arg_amount=${3:-"1.23M"} 17 | arg_duration=${4:-"91"} 18 | 19 | # Run the Forge script and extract the SVG from stdout 20 | output=$( 21 | forge script script/GenerateSVG.s.sol \ 22 | --sig "run(uint256,string,string,uint256)" \ 23 | "$arg_progress" \ 24 | "$arg_status" \ 25 | "$arg_amount" \ 26 | "$arg_duration" 27 | ) 28 | 29 | # Forge adds 'svg: string ' as a prefix before the SVG 30 | # - The awk command records everything after the prefix, while filtering out empty lines 31 | # - `sd \\"` '"'` removes the escape backslashes 32 | # - `sd ^\"|\"$' ''` removes the starting and the ending double quotes 33 | svg=$(echo "$output" | awk -F "svg: string " '/svg: string /{print $2; exit}' | sd '\\"' '"' | sd '^"|"$' '') 34 | 35 | # Generate the file name 36 | name="nft-${arg_progress}-${arg_status}-${arg_amount}-${arg_duration}.svg" 37 | sanitized="$(echo "$name" | sd ' ' '' )" # remove whitespaces 38 | 39 | # Put the SVG in a file 40 | mkdir -p "out-svg" 41 | echo $svg > "out-svg/$sanitized" 42 | -------------------------------------------------------------------------------- /shell/prepare-artifacts.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Pre-requisites: 4 | # - foundry (https://getfoundry.sh) 5 | # - bun (https://bun.sh) 6 | 7 | # Strict mode: https://gist.github.com/vncsna/64825d5609c146e80de8b1fd623011ca 8 | set -euo pipefail 9 | 10 | # Generate the artifacts with Forge 11 | FOUNDRY_PROFILE=optimized forge build 12 | 13 | # Delete the current artifacts 14 | artifacts=./artifacts 15 | rm -rf $artifacts 16 | 17 | # Create the new artifacts directories 18 | mkdir $artifacts \ 19 | "$artifacts/interfaces" \ 20 | "$artifacts/libraries" \ 21 | "$artifacts/erc20" \ 22 | "$artifacts/erc721" 23 | 24 | ################################################ 25 | #### LOCKUP #### 26 | ################################################ 27 | 28 | lockup=./artifacts/ 29 | cp out-optimized/LockupNFTDescriptor.sol/LockupNFTDescriptor.json $lockup 30 | cp out-optimized/SablierLockup.sol/SablierLockup.json $lockup 31 | cp out-optimized/SablierBatchLockup.sol/SablierBatchLockup.json $lockup 32 | 33 | lockup_interfaces=./artifacts/interfaces 34 | cp out-optimized/ISablierBatchLockup.sol/ISablierBatchLockup.json $lockup_interfaces 35 | cp out-optimized/ILockupNFTDescriptor.sol/ILockupNFTDescriptor.json $lockup_interfaces 36 | cp out-optimized/ISablierLockupRecipient.sol/ISablierLockupRecipient.json $lockup_interfaces 37 | cp out-optimized/ISablierLockupBase.sol/ISablierLockupBase.json $lockup_interfaces 38 | cp out-optimized/ISablierLockup.sol/ISablierLockup.json $lockup_interfaces 39 | 40 | lockup_libraries=./artifacts/libraries 41 | cp out-optimized/Errors.sol/Errors.json $lockup_libraries 42 | cp out-optimized/Helpers.sol/Helpers.json $lockup_libraries 43 | cp out-optimized/VestingMath.sol/VestingMath.json $lockup_libraries 44 | 45 | 46 | ################################################ 47 | #### OTHERS #### 48 | ################################################ 49 | 50 | erc20=./artifacts/erc20 51 | cp out-optimized/IERC20.sol/IERC20.json $erc20 52 | 53 | erc721=./artifacts/erc721 54 | cp out-optimized/IERC721.sol/IERC721.json $erc721 55 | cp out-optimized/IERC721Metadata.sol/IERC721Metadata.json $erc721 56 | 57 | # Format the artifacts with Prettier 58 | bun prettier --write ./artifacts 59 | -------------------------------------------------------------------------------- /shell/update-counts.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Pre-requisites for running this script: 4 | # 5 | # - bun (https://bun.sh) 6 | # - foundry (https://getfoundry.sh) 7 | 8 | # Strict mode 9 | set -euo pipefail 10 | 11 | # Path to the Base Script 12 | BASE_SCRIPT="script/Base.s.sol" 13 | 14 | # Compile the contracts with the optimized profile 15 | bun run build:optimized 16 | 17 | # Generalized function to update counts 18 | update_counts() { 19 | local test_name="Segments" 20 | local map_name="maxCountMap" 21 | echo -e "\nRunning forge test for estimating $test_name..." 22 | local output=$(FOUNDRY_PROFILE=benchmark forge t --mt "test_Estimate${test_name}" -vv) 23 | echo -e "\nParsing output for $test_name..." 24 | 25 | # Define a table with headers. This table is not put in the Solidity script file, 26 | # but is used to be displayed in the terminal. 27 | local table="Category,Chain ID,New Max Count" 28 | 29 | # Parse the output to extract counts and chain IDs 30 | while IFS= read -r line; do 31 | local count=$(echo $line | awk '{print $2}') 32 | local chain_id=$(echo $line | awk '{print $8}') 33 | 34 | # Add the data to the table 35 | table+="\n$map_name,$chain_id,$count" 36 | 37 | # Update the map for each chain ID using sd 38 | sd "$map_name\[$chain_id\] = [0-9]+;" "$map_name[$chain_id] = $count;" $BASE_SCRIPT 39 | done < <(echo "$output" | grep 'count:') 40 | 41 | # Print the table using the column command 42 | echo -e $table | column -t -s ',' 43 | } 44 | 45 | # Call the function with specific parameters for segments and tranches 46 | update_counts 47 | 48 | # Reformat the code with Forge 49 | forge fmt $BASE_SCRIPT 50 | 51 | printf "\n\nAll mappings updated." 52 | -------------------------------------------------------------------------------- /slither.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "detectors_to_exclude": "naming-convention,reentrancy-events,solc-version,timestamp", 3 | "filter_paths": "(node_modules/,tests/)", 4 | "solc_remaps": [ 5 | "@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/", 6 | "@prb/math/=node_modules/@prb-math/", 7 | "forge-std/=node_modules/forge-std/", 8 | "solady/=node_modules/solady/", 9 | "solarray/=node_modules/solarray/" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/abstracts/Adminable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { IAdminable } from "../interfaces/IAdminable.sol"; 5 | import { Errors } from "../libraries/Errors.sol"; 6 | 7 | /// @title Adminable 8 | /// @notice See the documentation in {IAdminable}. 9 | abstract contract Adminable is IAdminable { 10 | /*////////////////////////////////////////////////////////////////////////// 11 | STATE VARIABLES 12 | //////////////////////////////////////////////////////////////////////////*/ 13 | 14 | /// @inheritdoc IAdminable 15 | address public override admin; 16 | 17 | /*////////////////////////////////////////////////////////////////////////// 18 | MODIFIERS 19 | //////////////////////////////////////////////////////////////////////////*/ 20 | 21 | /// @notice Reverts if called by any account other than the admin. 22 | modifier onlyAdmin() { 23 | if (admin != msg.sender) { 24 | revert Errors.CallerNotAdmin({ admin: admin, caller: msg.sender }); 25 | } 26 | _; 27 | } 28 | 29 | /*////////////////////////////////////////////////////////////////////////// 30 | CONSTRUCTOR 31 | //////////////////////////////////////////////////////////////////////////*/ 32 | 33 | /// @dev Emits a {TransferAdmin} event. 34 | /// @param initialAdmin The address of the initial admin. 35 | constructor(address initialAdmin) { 36 | admin = initialAdmin; 37 | emit TransferAdmin({ oldAdmin: address(0), newAdmin: initialAdmin }); 38 | } 39 | 40 | /*////////////////////////////////////////////////////////////////////////// 41 | USER-FACING NON-CONSTANT FUNCTIONS 42 | //////////////////////////////////////////////////////////////////////////*/ 43 | 44 | /// @inheritdoc IAdminable 45 | function transferAdmin(address newAdmin) public virtual override onlyAdmin { 46 | // Effect: update the admin. 47 | admin = newAdmin; 48 | 49 | // Log the transfer of the admin. 50 | emit IAdminable.TransferAdmin({ oldAdmin: msg.sender, newAdmin: newAdmin }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/abstracts/Batch.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | // solhint-disable no-inline-assembly 3 | pragma solidity >=0.8.22; 4 | 5 | import { IBatch } from "../interfaces/IBatch.sol"; 6 | 7 | /// @title Batch 8 | /// @notice See the documentation in {IBatch}. 9 | abstract contract Batch is IBatch { 10 | /*////////////////////////////////////////////////////////////////////////// 11 | USER-FACING NON-CONSTANT FUNCTIONS 12 | //////////////////////////////////////////////////////////////////////////*/ 13 | 14 | /// @inheritdoc IBatch 15 | /// @dev Since `msg.value` can be reused across calls, be VERY CAREFUL when using it. Refer to 16 | /// https://paradigm.xyz/2021/08/two-rights-might-make-a-wrong for more information. 17 | function batch(bytes[] calldata calls) external payable override returns (bytes[] memory results) { 18 | uint256 count = calls.length; 19 | results = new bytes[](count); 20 | 21 | for (uint256 i = 0; i < count; ++i) { 22 | (bool success, bytes memory result) = address(this).delegatecall(calls[i]); 23 | 24 | // Check: If the delegatecall failed, load and bubble up the revert data. 25 | if (!success) { 26 | assembly { 27 | // Get the length of the result stored in the first 32 bytes. 28 | let resultSize := mload(result) 29 | 30 | // Forward the pointer by 32 bytes to skip the length argument, and revert with the result. 31 | revert(add(32, result), resultSize) 32 | } 33 | } 34 | 35 | // Push the result into the results array. 36 | results[i] = result; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/abstracts/NoDelegateCall.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { Errors } from "../libraries/Errors.sol"; 5 | 6 | /// @title NoDelegateCall 7 | /// @notice This contract implements logic to prevent delegate calls. 8 | abstract contract NoDelegateCall { 9 | /// @dev The address of the original contract that was deployed. 10 | address private immutable ORIGINAL; 11 | 12 | /// @dev Sets the original contract address. 13 | constructor() { 14 | ORIGINAL = address(this); 15 | } 16 | 17 | /// @notice Prevents delegate calls. 18 | modifier noDelegateCall() { 19 | _preventDelegateCall(); 20 | _; 21 | } 22 | 23 | /// @dev This function checks whether the current call is a delegate call, and reverts if it is. 24 | /// 25 | /// - A private function is used instead of inlining this logic in a modifier because Solidity copies modifiers into 26 | /// every function that uses them. The `ORIGINAL` address would get copied in every place the modifier is used, 27 | /// which would increase the contract size. By using a function instead, we can avoid this duplication of code 28 | /// and reduce the overall size of the contract. 29 | function _preventDelegateCall() private view { 30 | if (address(this) != ORIGINAL) { 31 | revert Errors.DelegateCall(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/interfaces/IAdminable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | /// @title IAdminable 5 | /// @notice Contract module that provides a basic access control mechanism, with an admin that can be 6 | /// granted exclusive access to specific functions. The inheriting contract must set the initial admin 7 | /// in the constructor. 8 | interface IAdminable { 9 | /*////////////////////////////////////////////////////////////////////////// 10 | EVENTS 11 | //////////////////////////////////////////////////////////////////////////*/ 12 | 13 | /// @notice Emitted when the admin is transferred. 14 | /// @param oldAdmin The address of the old admin. 15 | /// @param newAdmin The address of the new admin. 16 | event TransferAdmin(address indexed oldAdmin, address indexed newAdmin); 17 | 18 | /*////////////////////////////////////////////////////////////////////////// 19 | CONSTANT FUNCTIONS 20 | //////////////////////////////////////////////////////////////////////////*/ 21 | 22 | /// @notice The address of the admin account or contract. 23 | function admin() external view returns (address); 24 | 25 | /*////////////////////////////////////////////////////////////////////////// 26 | NON-CONSTANT FUNCTIONS 27 | //////////////////////////////////////////////////////////////////////////*/ 28 | 29 | /// @notice Transfers the contract admin to a new address. 30 | /// 31 | /// @dev Notes: 32 | /// - Does not revert if the admin is the same. 33 | /// - This function can potentially leave the contract without an admin, thereby removing any 34 | /// functionality that is only available to the admin. 35 | /// 36 | /// Requirements: 37 | /// - `msg.sender` must be the contract admin. 38 | /// 39 | /// @param newAdmin The address of the new admin. 40 | function transferAdmin(address newAdmin) external; 41 | } 42 | -------------------------------------------------------------------------------- /src/interfaces/IBatch.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | /// @notice This contract implements logic to batch call any function. 5 | interface IBatch { 6 | /// @notice Allows batched calls to self, i.e., `this` contract. 7 | /// @dev Since `msg.value` can be reused across calls, be VERY CAREFUL when using it. Refer to 8 | /// https://paradigm.xyz/2021/08/two-rights-might-make-a-wrong for more information. 9 | /// @param calls An array of inputs for each call. 10 | /// @return results An array of results from each call. Empty when the calls do not return anything. 11 | function batch(bytes[] calldata calls) external payable returns (bytes[] memory results); 12 | } 13 | -------------------------------------------------------------------------------- /src/interfaces/ILockupNFTDescriptor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; 5 | 6 | /// @title ILockupNFTDescriptor 7 | /// @notice This contract generates the URI describing the Sablier stream NFTs. 8 | /// @dev Inspired by Uniswap V3 Positions NFTs. 9 | interface ILockupNFTDescriptor { 10 | /// @notice Produces the URI describing a particular stream NFT. 11 | /// @dev This is a data URI with the JSON contents directly inlined. 12 | /// @param sablier The address of the Sablier contract the stream was created in. 13 | /// @param streamId The ID of the stream for which to produce a description. 14 | /// @return uri The URI of the ERC721-compliant metadata. 15 | function tokenURI(IERC721Metadata sablier, uint256 streamId) external view returns (string memory uri); 16 | } 17 | -------------------------------------------------------------------------------- /src/interfaces/ISablierLockupRecipient.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; 5 | 6 | /// @title ISablierLockupRecipient 7 | /// @notice Interface for recipient contracts capable of reacting to cancellations and withdrawals. For this to be able 8 | /// to hook into Sablier, it must fully implement this interface and it must have been allowlisted by the Lockup 9 | /// contract's admin. 10 | /// @dev See {IERC165-supportsInterface}. 11 | /// The implementation MUST implement the {IERC165-supportsInterface} method, which MUST return `true` when called with 12 | /// `0xf8ee98d3`, i.e. `type(ISablierLockupRecipient).interfaceId`. 13 | interface ISablierLockupRecipient is IERC165 { 14 | /// @notice Responds to cancellations. 15 | /// 16 | /// @dev Notes: 17 | /// - The function MUST return the selector `ISablierLockupRecipient.onSablierLockupCancel.selector`. 18 | /// - If this function reverts, the execution in the Lockup contract will revert as well. 19 | /// 20 | /// @param streamId The ID of the canceled stream. 21 | /// @param sender The stream's sender, who canceled the stream. 22 | /// @param senderAmount The amount of tokens refunded to the stream's sender, denoted in units of the token's 23 | /// decimals. 24 | /// @param recipientAmount The amount of tokens left for the stream's recipient to withdraw, denoted in units of 25 | /// the token's decimals. 26 | /// 27 | /// @return selector The selector of this function needed to validate the hook. 28 | function onSablierLockupCancel( 29 | uint256 streamId, 30 | address sender, 31 | uint128 senderAmount, 32 | uint128 recipientAmount 33 | ) 34 | external 35 | returns (bytes4 selector); 36 | 37 | /// @notice Responds to withdrawals triggered by any address except the contract implementing this interface. 38 | /// 39 | /// @dev Notes: 40 | /// - The function MUST return the selector `ISablierLockupRecipient.onSablierLockupWithdraw.selector`. 41 | /// - If this function reverts, the execution in the Lockup contract will revert as well. 42 | /// 43 | /// @param streamId The ID of the stream being withdrawn from. 44 | /// @param caller The original `msg.sender` address that triggered the withdrawal. 45 | /// @param to The address receiving the withdrawn tokens. 46 | /// @param amount The amount of tokens withdrawn, denoted in units of the token's decimals. 47 | /// 48 | /// @return selector The selector of this function needed to validate the hook. 49 | function onSablierLockupWithdraw( 50 | uint256 streamId, 51 | address caller, 52 | address to, 53 | uint128 amount 54 | ) 55 | external 56 | returns (bytes4 selector); 57 | } 58 | -------------------------------------------------------------------------------- /tests/fork/tokens/DAI.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | import { Lockup_Dynamic_Fork_Test } from "../LockupDynamic.t.sol"; 7 | import { Lockup_Linear_Fork_Test } from "../LockupLinear.t.sol"; 8 | import { Lockup_Tranched_Fork_Test } from "../LockupTranched.t.sol"; 9 | 10 | /// @dev A typical 18-decimal ERC-20 token with a normal total supply. 11 | IERC20 constant FORK_TOKEN = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); 12 | 13 | contract DAI_Lockup_Dynamic_Fork_Test is Lockup_Dynamic_Fork_Test(FORK_TOKEN) { } 14 | 15 | contract DAI_Lockup_Linear_Fork_Test is Lockup_Linear_Fork_Test(FORK_TOKEN) { } 16 | 17 | contract DAI_Lockup_Tranched_Fork_Test is Lockup_Tranched_Fork_Test(FORK_TOKEN) { } 18 | -------------------------------------------------------------------------------- /tests/fork/tokens/EURS.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | import { Lockup_Dynamic_Fork_Test } from "../LockupDynamic.t.sol"; 7 | import { Lockup_Linear_Fork_Test } from "../LockupLinear.t.sol"; 8 | import { Lockup_Tranched_Fork_Test } from "../LockupTranched.t.sol"; 9 | 10 | /// @dev An ERC-20 token with 2 decimals. 11 | IERC20 constant FORK_TOKEN = IERC20(0xdB25f211AB05b1c97D595516F45794528a807ad8); 12 | 13 | contract EURS_Lockup_Dynamic_Fork_Test is Lockup_Dynamic_Fork_Test(FORK_TOKEN) { } 14 | 15 | contract EURS_Lockup_Linear_Fork_Test is Lockup_Linear_Fork_Test(FORK_TOKEN) { } 16 | 17 | contract EURS_Lockup_Tranched_Fork_Test is Lockup_Tranched_Fork_Test(FORK_TOKEN) { } 18 | -------------------------------------------------------------------------------- /tests/fork/tokens/SHIB.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | import { Lockup_Dynamic_Fork_Test } from "../LockupDynamic.t.sol"; 7 | import { Lockup_Linear_Fork_Test } from "../LockupLinear.t.sol"; 8 | import { Lockup_Tranched_Fork_Test } from "../LockupTranched.t.sol"; 9 | 10 | /// @dev An ERC-20 token with a large total supply. 11 | IERC20 constant FORK_TOKEN = IERC20(0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE); 12 | 13 | contract SHIB_Lockup_Dynamic_Fork_Test is Lockup_Dynamic_Fork_Test(FORK_TOKEN) { } 14 | 15 | contract SHIB_Lockup_Linear_Fork_Test is Lockup_Linear_Fork_Test(FORK_TOKEN) { } 16 | 17 | contract SHIB_Lockup_Tranched_Fork_Test is Lockup_Tranched_Fork_Test(FORK_TOKEN) { } 18 | -------------------------------------------------------------------------------- /tests/fork/tokens/USDC.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | import { Lockup_Dynamic_Fork_Test } from "../LockupDynamic.t.sol"; 7 | import { Lockup_Linear_Fork_Test } from "../LockupLinear.t.sol"; 8 | import { Lockup_Tranched_Fork_Test } from "../LockupTranched.t.sol"; 9 | 10 | /// @dev An ERC-20 token with 6 decimals. 11 | IERC20 constant FORK_TOKEN = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); 12 | 13 | contract USDC_Lockup_Dynamic_Fork_Test is Lockup_Dynamic_Fork_Test(FORK_TOKEN) { } 14 | 15 | contract USDC_Lockup_Linear_Fork_Test is Lockup_Linear_Fork_Test(FORK_TOKEN) { } 16 | 17 | contract USDC_Lockup_Tranched_Fork_Test is Lockup_Tranched_Fork_Test(FORK_TOKEN) { } 18 | -------------------------------------------------------------------------------- /tests/fork/tokens/USDT.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | import { Lockup_Dynamic_Fork_Test } from "../LockupDynamic.t.sol"; 7 | import { Lockup_Linear_Fork_Test } from "../LockupLinear.t.sol"; 8 | import { Lockup_Tranched_Fork_Test } from "../LockupTranched.t.sol"; 9 | 10 | /// @dev An ERC-20 token that suffers from the missing return value bug. 11 | IERC20 constant FORK_TOKEN = IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7); 12 | 13 | contract USDT_Lockup_Dynamic_Fork_Test is Lockup_Dynamic_Fork_Test(FORK_TOKEN) { } 14 | 15 | contract USDT_Lockup_Linear_Fork_Test is Lockup_Linear_Fork_Test(FORK_TOKEN) { } 16 | 17 | contract USDT_Lockup_Tranched_Fork_Test is Lockup_Tranched_Fork_Test(FORK_TOKEN) { } 18 | -------------------------------------------------------------------------------- /tests/integration/concrete/batch-lockup/create-with-durations-ld/createWithDurationsLD.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Errors } from "src/libraries/Errors.sol"; 5 | import { BatchLockup } from "src/types/DataTypes.sol"; 6 | 7 | import { Integration_Test } from "../../../Integration.t.sol"; 8 | 9 | contract CreateWithDurationsLD_Integration_Test is Integration_Test { 10 | function test_RevertWhen_BatchSizeZero() external { 11 | BatchLockup.CreateWithDurationsLD[] memory batchParams = new BatchLockup.CreateWithDurationsLD[](0); 12 | vm.expectRevert(Errors.SablierBatchLockup_BatchSizeZero.selector); 13 | batchLockup.createWithDurationsLD(lockup, dai, batchParams); 14 | } 15 | 16 | function test_WhenBatchSizeNotZero() external { 17 | // Token flow: Sender → batchLockup → SablierLockup 18 | // Expect transfers from Alice to the batchLockup, and then from the batchLockup to the Lockup contract. 19 | expectCallToTransferFrom({ 20 | from: users.sender, 21 | to: address(batchLockup), 22 | value: defaults.TOTAL_TRANSFER_AMOUNT() 23 | }); 24 | 25 | expectMultipleCallsToCreateWithDurationsLD({ 26 | count: defaults.BATCH_SIZE(), 27 | params: defaults.createWithDurationsBrokerNull(), 28 | segmentsWithDuration: defaults.segmentsWithDurations() 29 | }); 30 | expectMultipleCallsToTransferFrom({ 31 | count: defaults.BATCH_SIZE(), 32 | from: address(batchLockup), 33 | to: address(lockup), 34 | value: defaults.DEPOSIT_AMOUNT() 35 | }); 36 | 37 | uint256 firstStreamId = lockup.nextStreamId(); 38 | 39 | // Assert that the batch of streams has been created successfully. 40 | uint256[] memory actualStreamIds = 41 | batchLockup.createWithDurationsLD(lockup, dai, defaults.batchCreateWithDurationsLD()); 42 | uint256[] memory expectedStreamIds = defaults.incrementalStreamIds({ firstStreamId: firstStreamId }); 43 | assertEq(actualStreamIds, expectedStreamIds, "stream ids mismatch"); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/integration/concrete/batch-lockup/create-with-durations-ld/createWithDurationsLD.tree: -------------------------------------------------------------------------------- 1 | CreateWithDurationsLD_Integration_Test 2 | ├── when batch size zero 3 | │ └── it should revert 4 | └── when batch size not zero 5 | ├── it should create a batch of streams with durations 6 | └── it should perform the ERC-20 transfers 7 | -------------------------------------------------------------------------------- /tests/integration/concrete/batch-lockup/create-with-durations-ll/createWithDurationsLL.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Errors } from "src/libraries/Errors.sol"; 5 | import { BatchLockup } from "src/types/DataTypes.sol"; 6 | 7 | import { Integration_Test } from "../../../Integration.t.sol"; 8 | 9 | contract CreateWithDurationsLL_Integration_Test is Integration_Test { 10 | function test_RevertWhen_BatchSizeZero() external { 11 | BatchLockup.CreateWithDurationsLL[] memory batchParams = new BatchLockup.CreateWithDurationsLL[](0); 12 | vm.expectRevert(Errors.SablierBatchLockup_BatchSizeZero.selector); 13 | batchLockup.createWithDurationsLL(lockup, dai, batchParams); 14 | } 15 | 16 | function test_WhenBatchSizeNotZero() external { 17 | // Token flow: Sender → batchLockup → SablierLockup 18 | // Expect transfers from Alice to the batchLockup, and then from the batchLockup to the Lockup contract. 19 | expectCallToTransferFrom({ 20 | from: users.sender, 21 | to: address(batchLockup), 22 | value: defaults.TOTAL_TRANSFER_AMOUNT() 23 | }); 24 | 25 | expectMultipleCallsToCreateWithDurationsLL({ 26 | count: defaults.BATCH_SIZE(), 27 | params: defaults.createWithDurationsBrokerNull(), 28 | unlockAmounts: defaults.unlockAmounts(), 29 | durations: defaults.durations() 30 | }); 31 | expectMultipleCallsToTransferFrom({ 32 | count: defaults.BATCH_SIZE(), 33 | from: address(batchLockup), 34 | to: address(lockup), 35 | value: defaults.DEPOSIT_AMOUNT() 36 | }); 37 | 38 | uint256 firstStreamId = lockup.nextStreamId(); 39 | 40 | // Assert that the batch of streams has been created successfully. 41 | uint256[] memory actualStreamIds = 42 | batchLockup.createWithDurationsLL(lockup, dai, defaults.batchCreateWithDurationsLL()); 43 | uint256[] memory expectedStreamIds = defaults.incrementalStreamIds({ firstStreamId: firstStreamId }); 44 | assertEq(actualStreamIds, expectedStreamIds, "stream ids mismatch"); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/integration/concrete/batch-lockup/create-with-durations-ll/createWithDurationsLL.tree: -------------------------------------------------------------------------------- 1 | CreateWithDurationsLL_Integration_Test 2 | ├── when batch size zero 3 | │ └── it should revert 4 | └── when batch size not zero 5 | ├── it should create a batch of streams with durations 6 | └── it should perform the ERC-20 transfers 7 | -------------------------------------------------------------------------------- /tests/integration/concrete/batch-lockup/create-with-durations-lt/createWithDurationsLT.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Errors } from "src/libraries/Errors.sol"; 5 | import { BatchLockup } from "src/types/DataTypes.sol"; 6 | 7 | import { Integration_Test } from "../../../Integration.t.sol"; 8 | 9 | contract CreateWithDurationsLT_Integration_Test is Integration_Test { 10 | function test_RevertWhen_BatchSizeZero() external { 11 | BatchLockup.CreateWithDurationsLT[] memory batchParams = new BatchLockup.CreateWithDurationsLT[](0); 12 | vm.expectRevert(Errors.SablierBatchLockup_BatchSizeZero.selector); 13 | batchLockup.createWithDurationsLT(lockup, dai, batchParams); 14 | } 15 | 16 | function test_WhenBatchSizeNotZero() external { 17 | // Token flow: Sender → batchLockup → SablierLockup 18 | // Expect transfers from Alice to the batchLockup, and then from the batchLockup to the Lockup contract. 19 | expectCallToTransferFrom({ 20 | from: users.sender, 21 | to: address(batchLockup), 22 | value: defaults.TOTAL_TRANSFER_AMOUNT() 23 | }); 24 | 25 | expectMultipleCallsToCreateWithDurationsLT({ 26 | count: defaults.BATCH_SIZE(), 27 | params: defaults.createWithDurationsBrokerNull(), 28 | tranches: defaults.tranchesWithDurations() 29 | }); 30 | expectMultipleCallsToTransferFrom({ 31 | count: defaults.BATCH_SIZE(), 32 | from: address(batchLockup), 33 | to: address(lockup), 34 | value: defaults.DEPOSIT_AMOUNT() 35 | }); 36 | 37 | uint256 firstStreamId = lockup.nextStreamId(); 38 | 39 | // Assert that the batch of streams has been created successfully. 40 | uint256[] memory actualStreamIds = 41 | batchLockup.createWithDurationsLT(lockup, dai, defaults.batchCreateWithDurationsLT()); 42 | uint256[] memory expectedStreamIds = defaults.incrementalStreamIds({ firstStreamId: firstStreamId }); 43 | assertEq(actualStreamIds, expectedStreamIds, "stream ids mismatch"); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/integration/concrete/batch-lockup/create-with-durations-lt/createWithDurationsLT.tree: -------------------------------------------------------------------------------- 1 | CreateWithDurationsLT_Integration_Test 2 | ├── when batch size zero 3 | │ └── it should revert 4 | └── when batch size not zero 5 | ├── it should create a batch of streams with durations 6 | └── it should perform the ERC-20 transfers 7 | -------------------------------------------------------------------------------- /tests/integration/concrete/batch-lockup/create-with-timestamps-ld/createWithTimestampsLD.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Errors } from "src/libraries/Errors.sol"; 5 | import { BatchLockup } from "src/types/DataTypes.sol"; 6 | 7 | import { Integration_Test } from "../../../Integration.t.sol"; 8 | 9 | contract CreateWithTimestampsLD_Integration_Test is Integration_Test { 10 | function test_RevertWhen_BatchSizeZero() external { 11 | BatchLockup.CreateWithTimestampsLD[] memory batchParams = new BatchLockup.CreateWithTimestampsLD[](0); 12 | vm.expectRevert(Errors.SablierBatchLockup_BatchSizeZero.selector); 13 | batchLockup.createWithTimestampsLD(lockup, dai, batchParams); 14 | } 15 | 16 | function test_WhenBatchSizeNotZero() external { 17 | // Token flow: Sender → batchLockup → SablierLockup 18 | // Expect transfers from Alice to the batchLockup, and then from the batchLockup to the Lockup contract. 19 | expectCallToTransferFrom({ 20 | from: users.sender, 21 | to: address(batchLockup), 22 | value: defaults.TOTAL_TRANSFER_AMOUNT() 23 | }); 24 | 25 | expectMultipleCallsToCreateWithTimestampsLD({ 26 | count: defaults.BATCH_SIZE(), 27 | params: defaults.createWithTimestampsBrokerNull(), 28 | segments: defaults.segments() 29 | }); 30 | expectMultipleCallsToTransferFrom({ 31 | count: defaults.BATCH_SIZE(), 32 | from: address(batchLockup), 33 | to: address(lockup), 34 | value: defaults.DEPOSIT_AMOUNT() 35 | }); 36 | 37 | uint256 firstStreamId = lockup.nextStreamId(); 38 | 39 | // Assert that the batch of streams has been created successfully. 40 | uint256[] memory actualStreamIds = 41 | batchLockup.createWithTimestampsLD(lockup, dai, defaults.batchCreateWithTimestampsLD()); 42 | uint256[] memory expectedStreamIds = defaults.incrementalStreamIds({ firstStreamId: firstStreamId }); 43 | assertEq(actualStreamIds, expectedStreamIds, "stream ids mismatch"); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/integration/concrete/batch-lockup/create-with-timestamps-ld/createWithTimestampsLD.tree: -------------------------------------------------------------------------------- 1 | CreateWithTimestampsLD_Integration_Test 2 | ├── when batch size zero 3 | │ └── it should revert 4 | └── when batch size not zero 5 | ├── it should create a batch of streams with timestamps 6 | └── it should perform the ERC-20 transfers 7 | -------------------------------------------------------------------------------- /tests/integration/concrete/batch-lockup/create-with-timestamps-ll/createWithTimestamps.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Errors } from "src/libraries/Errors.sol"; 5 | import { BatchLockup } from "src/types/DataTypes.sol"; 6 | 7 | import { Integration_Test } from "../../../Integration.t.sol"; 8 | 9 | contract CreateWithTimestampsLL_Integration_Test is Integration_Test { 10 | function test_RevertWhen_BatchSizeZero() external { 11 | BatchLockup.CreateWithTimestampsLL[] memory batchParams = new BatchLockup.CreateWithTimestampsLL[](0); 12 | vm.expectRevert(Errors.SablierBatchLockup_BatchSizeZero.selector); 13 | batchLockup.createWithTimestampsLL(lockup, dai, batchParams); 14 | } 15 | 16 | function test_WhenBatchSizeNotZero() external { 17 | // Token flow: Sender → batchLockup → SablierLockup 18 | // Expect transfers from Alice to the batchLockup, and then from the batchLockup to the Lockup contract. 19 | expectCallToTransferFrom({ 20 | from: users.sender, 21 | to: address(batchLockup), 22 | value: defaults.TOTAL_TRANSFER_AMOUNT() 23 | }); 24 | 25 | expectMultipleCallsToCreateWithTimestampsLL({ 26 | count: defaults.BATCH_SIZE(), 27 | params: defaults.createWithTimestampsBrokerNull(), 28 | unlockAmounts: defaults.unlockAmounts(), 29 | cliffTime: defaults.CLIFF_TIME() 30 | }); 31 | expectMultipleCallsToTransferFrom({ 32 | count: defaults.BATCH_SIZE(), 33 | from: address(batchLockup), 34 | to: address(lockup), 35 | value: defaults.DEPOSIT_AMOUNT() 36 | }); 37 | 38 | uint256 firstStreamId = lockup.nextStreamId(); 39 | 40 | // Assert that the batch of streams has been created successfully. 41 | uint256[] memory actualStreamIds = 42 | batchLockup.createWithTimestampsLL(lockup, dai, defaults.batchCreateWithTimestampsLL()); 43 | uint256[] memory expectedStreamIds = defaults.incrementalStreamIds({ firstStreamId: firstStreamId }); 44 | assertEq(actualStreamIds, expectedStreamIds, "stream ids mismatch"); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/integration/concrete/batch-lockup/create-with-timestamps-ll/createWithTimestamps.tree: -------------------------------------------------------------------------------- 1 | CreateWithTimestampsLL_Integration_Test 2 | ├── when batch size zero 3 | │ └── it should revert 4 | └── when batch size not zero 5 | ├── it should create a batch of streams with timestamps 6 | └── it should perform the ERC-20 transfers 7 | -------------------------------------------------------------------------------- /tests/integration/concrete/batch-lockup/create-with-timestamps-lt/createWithTimestampsLT.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Errors } from "src/libraries/Errors.sol"; 5 | import { BatchLockup } from "src/types/DataTypes.sol"; 6 | 7 | import { Integration_Test } from "../../../Integration.t.sol"; 8 | 9 | contract CreateWithTimestampsLT_Integration_Test is Integration_Test { 10 | function test_RevertWhen_BatchSizeZero() external { 11 | BatchLockup.CreateWithTimestampsLT[] memory batchParams = new BatchLockup.CreateWithTimestampsLT[](0); 12 | vm.expectRevert(Errors.SablierBatchLockup_BatchSizeZero.selector); 13 | batchLockup.createWithTimestampsLT(lockup, dai, batchParams); 14 | } 15 | 16 | function test_WhenBatchSizeNotZero() external { 17 | // Token flow: Sender → batchLockup → SablierLockup 18 | // Expect transfers from Alice to the batchLockup, and then from the batchLockup to the Lockup contract. 19 | expectCallToTransferFrom({ 20 | from: users.sender, 21 | to: address(batchLockup), 22 | value: defaults.TOTAL_TRANSFER_AMOUNT() 23 | }); 24 | 25 | expectMultipleCallsToCreateWithTimestampsLT({ 26 | count: defaults.BATCH_SIZE(), 27 | params: defaults.createWithTimestampsBrokerNull(), 28 | tranches: defaults.tranches() 29 | }); 30 | expectMultipleCallsToTransferFrom({ 31 | count: defaults.BATCH_SIZE(), 32 | from: address(batchLockup), 33 | to: address(lockup), 34 | value: defaults.DEPOSIT_AMOUNT() 35 | }); 36 | 37 | uint256 firstStreamId = lockup.nextStreamId(); 38 | 39 | // Assert that the batch of streams has been created successfully. 40 | uint256[] memory actualStreamIds = 41 | batchLockup.createWithTimestampsLT(lockup, dai, defaults.batchCreateWithTimestampsLT()); 42 | uint256[] memory expectedStreamIds = defaults.incrementalStreamIds({ firstStreamId: firstStreamId }); 43 | assertEq(actualStreamIds, expectedStreamIds, "stream ids mismatch"); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/integration/concrete/batch-lockup/create-with-timestamps-lt/createWithTimestampsLT.tree: -------------------------------------------------------------------------------- 1 | CreateWithTimestampsLT_Integration_Test 2 | ├── when batch size zero 3 | │ └── it should revert 4 | └── when batch size not zero 5 | ├── it should create a batch of streams with timestamps 6 | └── it should perform the ERC-20 transfers 7 | -------------------------------------------------------------------------------- /tests/integration/concrete/constructor.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { UD60x18 } from "@prb/math/src/UD60x18.sol"; 5 | 6 | import { IAdminable } from "src/interfaces/IAdminable.sol"; 7 | import { SablierLockup } from "src/SablierLockup.sol"; 8 | 9 | import { Integration_Test } from "../Integration.t.sol"; 10 | 11 | contract Constructor_Integration_Concrete_Test is Integration_Test { 12 | function test_Constructor() external { 13 | // Expect the relevant event to be emitted. 14 | vm.expectEmit(); 15 | emit IAdminable.TransferAdmin({ oldAdmin: address(0), newAdmin: users.admin }); 16 | 17 | // Construct the contract. 18 | SablierLockup constructedLockup = new SablierLockup({ 19 | initialAdmin: users.admin, 20 | initialNFTDescriptor: nftDescriptor, 21 | maxCount: defaults.MAX_COUNT() 22 | }); 23 | 24 | // {SablierLockupBase.constant} 25 | UD60x18 actualMaxBrokerFee = constructedLockup.MAX_BROKER_FEE(); 26 | UD60x18 expectedMaxBrokerFee = UD60x18.wrap(0.1e18); 27 | assertEq(actualMaxBrokerFee, expectedMaxBrokerFee, "MAX_BROKER_FEE"); 28 | 29 | // {Adminable.constructor} 30 | address actualAdmin = constructedLockup.admin(); 31 | address expectedAdmin = users.admin; 32 | assertEq(actualAdmin, expectedAdmin, "admin"); 33 | 34 | // {SablierLockupBase.constructor} 35 | uint256 actualStreamId = constructedLockup.nextStreamId(); 36 | uint256 expectedStreamId = 1; 37 | assertEq(actualStreamId, expectedStreamId, "nextStreamId"); 38 | 39 | // {SablierLockupBase.constructor} 40 | address actualNFTDescriptor = address(constructedLockup.nftDescriptor()); 41 | address expectedNFTDescriptor = address(nftDescriptor); 42 | assertEq(actualNFTDescriptor, expectedNFTDescriptor, "nftDescriptor"); 43 | 44 | // {SablierLockupBase.supportsInterface} 45 | assertTrue(constructedLockup.supportsInterface(0x49064906), "ERC-4906 interface ID"); 46 | 47 | // {SablierLockup.constructor} 48 | uint256 actualMaxCount = constructedLockup.MAX_COUNT(); 49 | uint256 expectedMaxCount = defaults.MAX_COUNT(); 50 | assertEq(actualMaxCount, expectedMaxCount, "MAX_COUNT"); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-base/allow-to-hook/allowToHook.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; 5 | import { Errors } from "src/libraries/Errors.sol"; 6 | 7 | import { RecipientGood } from "../../../../mocks/Hooks.sol"; 8 | import { Integration_Test } from "../../../Integration.t.sol"; 9 | 10 | contract AllowToHook_Integration_Concrete_Test is Integration_Test { 11 | function test_RevertWhen_CallerNotAdmin() external { 12 | // Make Eve the caller in this test. 13 | resetPrank({ msgSender: users.eve }); 14 | 15 | // Run the test. 16 | vm.expectRevert(abi.encodeWithSelector(Errors.CallerNotAdmin.selector, users.admin, users.eve)); 17 | lockup.allowToHook(users.eve); 18 | } 19 | 20 | function test_RevertWhen_ProvidedAddressNotContract() external whenCallerAdmin { 21 | address eoa = vm.addr({ privateKey: 1 }); 22 | vm.expectRevert(abi.encodeWithSelector(Errors.SablierLockupBase_AllowToHookZeroCodeSize.selector, eoa)); 23 | lockup.allowToHook(eoa); 24 | } 25 | 26 | function test_RevertWhen_ProvidedAddressNotReturnInterfaceId() 27 | external 28 | whenCallerAdmin 29 | whenProvidedAddressContract 30 | { 31 | // Incorrect interface ID. 32 | address recipient = address(recipientInterfaceIDIncorrect); 33 | vm.expectRevert( 34 | abi.encodeWithSelector(Errors.SablierLockupBase_AllowToHookUnsupportedInterface.selector, recipient) 35 | ); 36 | lockup.allowToHook(recipient); 37 | 38 | // Missing interface ID. 39 | recipient = address(recipientInterfaceIDMissing); 40 | vm.expectRevert(bytes("")); 41 | lockup.allowToHook(recipient); 42 | } 43 | 44 | function test_WhenProvidedAddressReturnsInterfaceId() external whenCallerAdmin whenProvidedAddressContract { 45 | // Define a recipient that implementes the interface correctly. 46 | RecipientGood recipientWithInterfaceId = new RecipientGood(); 47 | 48 | // It should emit a {AllowToHook} event. 49 | vm.expectEmit({ emitter: address(lockup) }); 50 | emit ISablierLockupBase.AllowToHook(users.admin, address(recipientWithInterfaceId)); 51 | 52 | // Allow the provided address to hook. 53 | lockup.allowToHook(address(recipientWithInterfaceId)); 54 | 55 | // It should put the address on the allowlist. 56 | bool isAllowedToHook = lockup.isAllowedToHook(address(recipientWithInterfaceId)); 57 | assertTrue(isAllowedToHook, "address not put on the allowlist"); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-base/allow-to-hook/allowToHook.tree: -------------------------------------------------------------------------------- 1 | AllowToHook_Integration_Concrete_Test 2 | ├── when caller not admin 3 | │ └── it should revert 4 | └── when caller admin 5 | ├── when provided address not contract 6 | │ └── it should revert 7 | └── when provided address contract 8 | ├── when provided address not return interface id 9 | │ └── it should revert 10 | └── when provided address returns interface id 11 | ├── it should put the address on the allowlist 12 | └── it should emit a {AllowToHook} event 13 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-base/burn/burn.tree: -------------------------------------------------------------------------------- 1 | Burn_Integration_Concrete_Test 2 | ├── when delegate call 3 | │ └── it should revert 4 | └── when no delegate call 5 | ├── given null 6 | │ └── it should revert 7 | └── given not null 8 | ├── given not depleted stream 9 | │ ├── given PENDING status 10 | │ │ └── it should revert 11 | │ ├── given STREAMING status 12 | │ │ └── it should revert 13 | │ ├── given SETTLED status 14 | │ │ └── it should revert 15 | │ └── given CANCELED status 16 | │ └── it should revert 17 | └── given depleted stream 18 | ├── when caller not recipient 19 | │ ├── when caller malicious third party 20 | │ │ └── it should revert 21 | │ ├── when caller sender 22 | │ │ └── it should revert 23 | │ └── when caller approved third party 24 | │ └── it should burn the NFT 25 | └── when caller recipient 26 | ├── given NFT not exist 27 | │ └── it should revert 28 | └── given NFT exists 29 | ├── given non transferable NFT 30 | │ ├── it should burn the NFT 31 | │ └── it should emit a {MetadataUpdate} event 32 | └── given transferable NFT 33 | ├── it should burn the NFT 34 | └── it should emit a {MetadataUpdate} event -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-base/cancel-multiple/cancelMultiple.tree: -------------------------------------------------------------------------------- 1 | CancelMultiple_Integration_Concrete_Test 2 | ├── when delegate call 3 | │ └── it should revert 4 | └── when no delegate call 5 | ├── when zero array length 6 | │ └── it should do nothing 7 | └── when non zero array length 8 | ├── given atleast one null stream 9 | │ └── it should revert 10 | └── given no null streams 11 | ├── given atleast one cold stream 12 | │ └── it should revert 13 | └── given no cold streams 14 | ├── when caller unauthorized for any 15 | │ └── it should revert 16 | └── when caller authorized for all streams 17 | ├── given atleast one non cancelable stream 18 | │ └── it should revert 19 | └── given all streams cancelable 20 | ├── it should mark the streams as canceled 21 | ├── it should make the streams as non cancelable 22 | ├── it should refund the sender 23 | ├── it should update the refunded amounts 24 | ├── it should not burn the NFT for all streams 25 | └── it should emit {CancelLockupStream} events for all streams 26 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-base/cancel/cancel.tree: -------------------------------------------------------------------------------- 1 | Cancel_Integration_Concrete_Test 2 | ├── when delegate call 3 | │ └── it should revert 4 | └── when no delegate call 5 | ├── given null 6 | │ └── it should revert 7 | └── given not null 8 | ├── given cold stream 9 | │ ├── given DEPLETED status 10 | │ │ └── it should revert 11 | │ ├── given CANCELED status 12 | │ │ └── it should revert 13 | │ └── given SETTLED status 14 | │ └── it should revert 15 | └── given warm stream 16 | ├── when caller not sender 17 | │ ├── when caller malicious third party 18 | │ │ └── it should revert 19 | │ └── when caller recipient 20 | │ └── it should revert 21 | └── when caller sender 22 | ├── given non cancelable stream 23 | │ └── it should revert 24 | └── given cancelable stream 25 | ├── given PENDING status 26 | │ ├── it should mark the stream as depleted 27 | │ └── it should make the stream not cancelable 28 | └── given STREAMING status 29 | ├── given recipient not allowed to hook 30 | │ ├── it should mark the stream as canceled 31 | │ └── it should not make Sablier run the recipient hook 32 | └── given recipient allowed to hook 33 | ├── when reverting recipient 34 | │ └── it should revert the entire transaction 35 | └── when non reverting recipient 36 | ├── when recipient returns invalid selector 37 | │ └── it should revert 38 | └── when recipient returns valid selector 39 | ├── when reentrancy 40 | │ ├── it should mark the stream as depleted 41 | │ ├── it should make Sablier run the recipient hook 42 | │ ├── it should perform a reentrancy call to the Lockup contract 43 | │ └── it should make the withdrawal via the reentrancy 44 | └── when no reentrancy 45 | ├── it should mark the stream as canceled 46 | ├── it should make the stream as non cancelable 47 | ├── it should update the refunded amount 48 | ├── it should refund the sender 49 | ├── it should make Sablier run the recipient hook 50 | ├── it should not burn the NFT 51 | └── it should emit {MetadataUpdate} and {CancelLockupStream} events 52 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-base/collect-fees/collectFees.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; 5 | import { Errors } from "src/libraries/Errors.sol"; 6 | 7 | import { Integration_Test } from "../../../Integration.t.sol"; 8 | 9 | contract CollectFees_Integration_Concrete_Test is Integration_Test { 10 | function test_GivenAdminIsNotContract() external { 11 | _test_CollectFees(users.admin); 12 | } 13 | 14 | function test_RevertGiven_AdminDoesNotImplementReceiveFunction() external givenAdminIsContract { 15 | // Transfer the admin to a contract that does not implement the receive function. 16 | resetPrank({ msgSender: users.admin }); 17 | lockup.transferAdmin(address(contractWithoutReceive)); 18 | 19 | // Make the contract the caller. 20 | resetPrank({ msgSender: address(contractWithoutReceive) }); 21 | 22 | // Expect a revert. 23 | vm.expectRevert( 24 | abi.encodeWithSelector( 25 | Errors.SablierLockupBase_FeeTransferFail.selector, 26 | address(contractWithoutReceive), 27 | address(lockup).balance 28 | ) 29 | ); 30 | 31 | // Collect the fees. 32 | lockup.collectFees(); 33 | } 34 | 35 | function test_GivenAdminImplementsReceiveFunction() external givenAdminIsContract { 36 | // Transfer the admin to a contract that implements the receive function. 37 | resetPrank({ msgSender: users.admin }); 38 | lockup.transferAdmin(address(contractWithReceive)); 39 | 40 | // Make the contract the caller. 41 | resetPrank({ msgSender: address(contractWithReceive) }); 42 | 43 | // Run the tests. 44 | _test_CollectFees(address(contractWithReceive)); 45 | } 46 | 47 | function _test_CollectFees(address admin) private { 48 | vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); 49 | 50 | // Load the initial ETH balance of the admin. 51 | uint256 initialAdminBalance = admin.balance; 52 | 53 | // Make Alice the caller. 54 | resetPrank({ msgSender: users.alice }); 55 | 56 | // Make a withdrawal and pay the fee. 57 | lockup.withdrawMax{ value: FEE }({ streamId: defaultStreamId, to: users.recipient }); 58 | 59 | // It should emit a {CollectFees} event. 60 | vm.expectEmit({ emitter: address(lockup) }); 61 | emit ISablierLockupBase.CollectFees({ admin: admin, feeAmount: FEE }); 62 | 63 | lockup.collectFees(); 64 | 65 | // It should transfer the fee. 66 | assertEq(admin.balance, initialAdminBalance + FEE, "admin ETH balance"); 67 | 68 | // It should decrease contract balance to zero. 69 | assertEq(address(lockup).balance, 0, "lockup ETH balance"); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-base/collect-fees/collectFees.tree: -------------------------------------------------------------------------------- 1 | CollectFees_Integration_Concrete_Test 2 | ├── given admin is not contract 3 | │ ├── it should transfer fee 4 | │ ├── it should decrease contract balance to zero 5 | │ └── it should emit a {CollectFees} event 6 | └── given admin is contract 7 | ├── given admin does not implement receive function 8 | │ └── it should revert 9 | └── given admin implements receive function 10 | ├── it should transfer fee 11 | ├── it should decrease contract balance to zero 12 | └── it should emit a {CollectFees} event 13 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-base/create-with-timestamps/createWithTimestamps.tree: -------------------------------------------------------------------------------- 1 | CreateWithTimestamps_Integration_Concrete_Test 2 | ├── when delegate call 3 | │ └── it should revert 4 | └── when no delegate call 5 | ├── when shape exceeds 32 bytes 6 | │ └── it should revert 7 | └── when shape not exceed 32 bytes 8 | ├── when broker fee exceeds max value 9 | │ └── it should revert 10 | └── when broker fee not exceed max value 11 | ├── when sender zero address 12 | │ └── it should revert 13 | └── when sender not zero address 14 | ├── when recipient zero address 15 | │ └── it should revert 16 | └── when recipient not zero address 17 | ├── when deposit amount zero 18 | │ └── it should revert 19 | └── when deposit amount not zero 20 | ├── when start time zero 21 | │ └── it should revert 22 | └── when start time not zero 23 | └── when token not contract 24 | └── it should revert 25 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-base/refundable-amount-of/refundableAmountOf.tree: -------------------------------------------------------------------------------- 1 | RefundableAmountOf_Integration_Concrete_Test 2 | ├── given null 3 | │ └── it should revert 4 | └── given not null 5 | ├── given non cancelable stream 6 | │ └── it should return zero 7 | └── given cancelable stream 8 | ├── given canceled stream and CANCELED status 9 | │ └── it should return zero 10 | ├── given canceled stream and DEPLETED status 11 | │ └── it should return zero 12 | └── given not canceled stream 13 | ├── given PENDING status 14 | │ └── it should return the deposited amount 15 | ├── given STREAMING status 16 | │ └── it should return the correct refundable amount 17 | ├── given SETTLED status 18 | │ └── it should return zero 19 | └── given DEPLETED status 20 | └── it should return zero 21 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-base/renounce-multiple/renounceMultiple.tree: -------------------------------------------------------------------------------- 1 | RenounceMultiple_Integration_Concrete_Test 2 | ├── when delegate call 3 | │ └── it should revert 4 | └── when no delegate call 5 | ├── when zero array length 6 | │ └── it should do nothing 7 | └── when non zero array length 8 | ├── given at least one null stream 9 | │ └── it should revert 10 | └── given no null streams 11 | ├── given at least one cold stream 12 | │ └── it should revert 13 | └── given no cold streams 14 | ├── when caller unauthorized for any 15 | │ └── it should revert 16 | └── when caller authorized for all streams 17 | ├── given at least one non cancelable stream 18 | │ └── it should revert 19 | └── given all streams cancelable 20 | ├── it should emit {RenounceLockupStream} events 21 | └── it should make streams non cancelable 22 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-base/renounce/renounce.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; 5 | import { Errors } from "src/libraries/Errors.sol"; 6 | 7 | import { Integration_Test } from "../../../Integration.t.sol"; 8 | 9 | abstract contract Renounce_Integration_Concrete_Test is Integration_Test { 10 | uint256 internal streamId; 11 | 12 | function test_RevertWhen_DelegateCall() external { 13 | expectRevert_DelegateCall({ callData: abi.encodeCall(lockup.renounce, defaultStreamId) }); 14 | } 15 | 16 | function test_RevertGiven_Null() external whenNoDelegateCall { 17 | expectRevert_Null({ callData: abi.encodeCall(lockup.renounce, nullStreamId) }); 18 | } 19 | 20 | function test_RevertGiven_DEPLETEDStatus() external whenNoDelegateCall givenNotNull givenColdStream { 21 | expectRevert_DEPLETEDStatus({ callData: abi.encodeCall(lockup.renounce, defaultStreamId) }); 22 | } 23 | 24 | function test_RevertGiven_CANCELEDStatus() external whenNoDelegateCall givenNotNull givenColdStream { 25 | expectRevert_CANCELEDStatus({ callData: abi.encodeCall(lockup.renounce, defaultStreamId) }); 26 | } 27 | 28 | function test_RevertGiven_SETTLEDStatus() external whenNoDelegateCall givenNotNull givenColdStream { 29 | expectRevert_SETTLEDStatus({ callData: abi.encodeCall(lockup.renounce, defaultStreamId) }); 30 | } 31 | 32 | modifier givenWarmStreamRenounce() { 33 | vm.warp({ newTimestamp: defaults.START_TIME() - 1 seconds }); 34 | streamId = defaultStreamId; 35 | _; 36 | 37 | vm.warp({ newTimestamp: defaults.START_TIME() }); 38 | streamId = recipientGoodStreamId; 39 | _; 40 | } 41 | 42 | function test_RevertWhen_CallerNotSender() external whenNoDelegateCall givenNotNull givenWarmStreamRenounce { 43 | expectRevert_CallerMaliciousThirdParty({ callData: abi.encodeCall(lockup.renounce, defaultStreamId) }); 44 | } 45 | 46 | function test_RevertGiven_NonCancelableStream() 47 | external 48 | whenNoDelegateCall 49 | givenNotNull 50 | givenWarmStreamRenounce 51 | whenCallerSender 52 | { 53 | // Run the test. 54 | vm.expectRevert( 55 | abi.encodeWithSelector(Errors.SablierLockupBase_StreamNotCancelable.selector, notCancelableStreamId) 56 | ); 57 | lockup.renounce(notCancelableStreamId); 58 | } 59 | 60 | function test_GivenCancelableStream() 61 | external 62 | whenNoDelegateCall 63 | givenNotNull 64 | givenWarmStreamRenounce 65 | whenCallerSender 66 | { 67 | // It should emit {RenounceLockupStream} event. 68 | vm.expectEmit({ emitter: address(lockup) }); 69 | emit ISablierLockupBase.RenounceLockupStream(streamId); 70 | 71 | // Renounce the stream. 72 | lockup.renounce(streamId); 73 | 74 | // It should make stream non cancelable. 75 | assertFalse(lockup.isCancelable(streamId), "isCancelable"); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-base/renounce/renounce.tree: -------------------------------------------------------------------------------- 1 | Renounce_Integration_Concrete_Test 2 | ├── when delegate call 3 | │ └── it should revert 4 | └── when no delegate call 5 | ├── given null 6 | │ └── it should revert 7 | └── given not null 8 | ├── given cold stream 9 | │ ├── given DEPLETED status 10 | │ │ └── it should revert 11 | │ ├── given CANCELED status 12 | │ │ └── it should revert 13 | │ └── given SETTLED status 14 | │ └── it should revert 15 | └── given warm stream 16 | ├── when caller not sender 17 | │ └── it should revert 18 | └── when caller sender 19 | ├── given non cancelable stream 20 | │ └── it should revert 21 | └── given cancelable stream 22 | ├── it should emit {RenounceLockupStream} event 23 | └── it should make stream non cancelable 24 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-base/set-nft-descriptor/setNFTDescriptor.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; 5 | import { ILockupNFTDescriptor } from "src/interfaces/ILockupNFTDescriptor.sol"; 6 | import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; 7 | import { Errors } from "src/libraries/Errors.sol"; 8 | import { LockupNFTDescriptor } from "src/LockupNFTDescriptor.sol"; 9 | import { Integration_Test } from "../../../Integration.t.sol"; 10 | 11 | contract SetNFTDescriptor_Integration_Concrete_Test is Integration_Test { 12 | function test_RevertWhen_CallerNotAdmin() external { 13 | // Make Eve the caller in this test. 14 | resetPrank({ msgSender: users.eve }); 15 | 16 | // Run the test. 17 | vm.expectRevert(abi.encodeWithSelector(Errors.CallerNotAdmin.selector, users.admin, users.eve)); 18 | lockup.setNFTDescriptor(ILockupNFTDescriptor(users.eve)); 19 | } 20 | 21 | function test_WhenProvidedAddressMatchesCurrentNFTDescriptor() external whenCallerAdmin { 22 | // It should emit {SetNFTDescriptor} and {BatchMetadataUpdate} events. 23 | vm.expectEmit({ emitter: address(lockup) }); 24 | emit ISablierLockupBase.SetNFTDescriptor(users.admin, nftDescriptor, nftDescriptor); 25 | vm.expectEmit({ emitter: address(lockup) }); 26 | emit IERC4906.BatchMetadataUpdate({ _fromTokenId: 1, _toTokenId: lockup.nextStreamId() - 1 }); 27 | 28 | // Re-set the NFT descriptor. 29 | lockup.setNFTDescriptor(nftDescriptor); 30 | 31 | // It should re-set the NFT descriptor. 32 | vm.expectCall(address(nftDescriptor), abi.encodeCall(ILockupNFTDescriptor.tokenURI, (lockup, defaultStreamId))); 33 | lockup.tokenURI({ tokenId: defaultStreamId }); 34 | } 35 | 36 | function test_WhenProvidedAddressNotMatchCurrentNFTDescriptor() external whenCallerAdmin { 37 | // Deploy another NFT descriptor. 38 | ILockupNFTDescriptor newNFTDescriptor = new LockupNFTDescriptor(); 39 | 40 | // It should emit {SetNFTDescriptor} and {BatchMetadataUpdate} events. 41 | vm.expectEmit({ emitter: address(lockup) }); 42 | emit ISablierLockupBase.SetNFTDescriptor(users.admin, nftDescriptor, newNFTDescriptor); 43 | vm.expectEmit({ emitter: address(lockup) }); 44 | emit IERC4906.BatchMetadataUpdate({ _fromTokenId: 1, _toTokenId: lockup.nextStreamId() - 1 }); 45 | 46 | // Set the new NFT descriptor. 47 | lockup.setNFTDescriptor(newNFTDescriptor); 48 | 49 | // It should set the new NFT descriptor. 50 | vm.expectCall(address(newNFTDescriptor), abi.encodeCall(ILockupNFTDescriptor.tokenURI, (lockup, 1))); 51 | lockup.tokenURI({ tokenId: defaultStreamId }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-base/set-nft-descriptor/setNFTDescriptor.tree: -------------------------------------------------------------------------------- 1 | SetNFTDescriptor_Integration_Concrete_Test 2 | ├── when caller not admin 3 | │ └── it should revert 4 | └── when caller admin 5 | ├── when provided address matches current NFT descriptor 6 | │ ├── it should re-set the NFT descriptor 7 | │ └── it should emit {SetNFTDescriptor} and {BatchMetadataUpdate} events 8 | └── when provided address not match current NFT descriptor 9 | ├── it should set the new NFT descriptor 10 | └── it should emit {SetNFTDescriptor} and {BatchMetadataUpdate} events 11 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-base/status-of/statusOf.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Lockup } from "src/types/DataTypes.sol"; 5 | 6 | import { Integration_Test } from "../../../Integration.t.sol"; 7 | 8 | contract StatusOf_Integration_Concrete_Test is Integration_Test { 9 | function test_RevertGiven_Null() external { 10 | expectRevert_Null({ callData: abi.encodeCall(lockup.statusOf, nullStreamId) }); 11 | } 12 | 13 | function test_GivenTokensFullyWithdrawn() external givenNotNull { 14 | vm.warp({ newTimestamp: defaults.END_TIME() }); 15 | lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); 16 | 17 | // It should return DEPLETED. 18 | Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); 19 | Lockup.Status expectedStatus = Lockup.Status.DEPLETED; 20 | assertEq(actualStatus, expectedStatus); 21 | } 22 | 23 | function test_GivenCanceledStream() external givenNotNull givenTokensNotFullyWithdrawn { 24 | vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); 25 | lockup.cancel(defaultStreamId); 26 | 27 | // It should return CANCELED. 28 | Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); 29 | Lockup.Status expectedStatus = Lockup.Status.CANCELED; 30 | assertEq(actualStatus, expectedStatus); 31 | } 32 | 33 | function test_GivenStartTimeInFuture() external givenNotNull givenTokensNotFullyWithdrawn givenNotCanceledStream { 34 | vm.warp({ newTimestamp: getBlockTimestamp() - 1 seconds }); 35 | 36 | // It should return PENDING. 37 | Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); 38 | Lockup.Status expectedStatus = Lockup.Status.PENDING; 39 | assertEq(actualStatus, expectedStatus); 40 | } 41 | 42 | function test_GivenZeroRefundableAmount() 43 | external 44 | givenNotNull 45 | givenTokensNotFullyWithdrawn 46 | givenNotCanceledStream 47 | givenStartTimeNotInFuture 48 | { 49 | vm.warp({ newTimestamp: defaults.END_TIME() }); 50 | 51 | // It should return SETTLED. 52 | Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); 53 | Lockup.Status expectedStatus = Lockup.Status.SETTLED; 54 | assertEq(actualStatus, expectedStatus); 55 | } 56 | 57 | function test_GivenNonZeroRefundableAmount() 58 | external 59 | givenNotNull 60 | givenTokensNotFullyWithdrawn 61 | givenNotCanceledStream 62 | givenStartTimeNotInFuture 63 | { 64 | vm.warp({ newTimestamp: defaults.START_TIME() + 1 seconds }); 65 | 66 | // It should return STREAMING. 67 | Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); 68 | Lockup.Status expectedStatus = Lockup.Status.STREAMING; 69 | assertEq(actualStatus, expectedStatus); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-base/status-of/statusOf.tree: -------------------------------------------------------------------------------- 1 | StatusOf_Integration_Concrete_Test 2 | ├── given null 3 | │ └── it should revert 4 | └── given not null 5 | ├── given tokens fully withdrawn 6 | │ └── it should return DEPLETED 7 | └── given tokens not fully withdrawn 8 | ├── given canceled stream 9 | │ └── it should return CANCELED 10 | └── given not canceled stream 11 | ├── given start time in future 12 | │ └── it should return PENDING 13 | └── given start time not in future 14 | ├── given zero refundable amount 15 | │ └── it should return SETTLED 16 | └── given non zero refundable amount 17 | └── it should return STREAMING 18 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-base/streamed-amount-of/streamedAmountOf.tree: -------------------------------------------------------------------------------- 1 | StreamedAmountOf_Integration_Concrete_Test 2 | ├── given null 3 | │ └── it should revert 4 | └── given not null 5 | ├── given canceled stream and CANCELED status 6 | │ └── it should return the correct streamed amount 7 | ├── given canceled stream and DEPLETED status 8 | │ └── it should return the correct streamed amount 9 | └── given not canceled stream 10 | ├── given PENDING status 11 | │ └── it should return zero 12 | ├── given STREAMING status 13 | │ └── it should the correct streamed amount 14 | ├── given SETTLED status 15 | │ └── it should return the deposited amount 16 | └── given DEPLETED status 17 | └── it should return the deposited amount 18 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-base/token-uri/tokenURI.tree: -------------------------------------------------------------------------------- 1 | TokenURI_Lockup_Integration_Concrete_Test 2 | ├── given NFT not exist 3 | │ └── it should revert 4 | └── given NFT exists 5 | ├── when token URI decoded 6 | │ └── it should return the correct token URI 7 | └── when token URI not decoded 8 | └── it should return the correct token URI 9 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-base/transfer-from/transferFrom.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; 5 | import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 6 | 7 | import { Errors } from "src/libraries/Errors.sol"; 8 | 9 | import { Integration_Test } from "../../../Integration.t.sol"; 10 | 11 | contract TransferFrom_Integration_Concrete_Test is Integration_Test { 12 | function setUp() public virtual override { 13 | Integration_Test.setUp(); 14 | 15 | // Set recipient as caller for this test. 16 | resetPrank({ msgSender: users.recipient }); 17 | } 18 | 19 | function test_RevertGiven_NonTransferableStream() external { 20 | vm.expectRevert( 21 | abi.encodeWithSelector(Errors.SablierLockupBase_NotTransferable.selector, notTransferableStreamId) 22 | ); 23 | lockup.transferFrom({ from: users.recipient, to: users.alice, tokenId: notTransferableStreamId }); 24 | } 25 | 26 | function test_GivenTransferableStream() external { 27 | // It should emit {MetadataUpdate} and {Transfer} events. 28 | vm.expectEmit({ emitter: address(lockup) }); 29 | emit IERC4906.MetadataUpdate({ _tokenId: defaultStreamId }); 30 | vm.expectEmit({ emitter: address(lockup) }); 31 | emit IERC721.Transfer({ from: users.recipient, to: users.alice, tokenId: defaultStreamId }); 32 | 33 | // Transfer the NFT. 34 | lockup.transferFrom({ from: users.recipient, to: users.alice, tokenId: defaultStreamId }); 35 | 36 | // It should change the stream recipient (and NFT owner). 37 | address actualRecipient = lockup.getRecipient(defaultStreamId); 38 | address expectedRecipient = users.alice; 39 | assertEq(actualRecipient, expectedRecipient, "recipient"); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-base/transfer-from/transferFrom.tree: -------------------------------------------------------------------------------- 1 | TransferFrom_Integration_Concrete_Test 2 | ├── given non transferable stream 3 | │ └── it should revert 4 | └── given transferable stream 5 | ├── it should change the stream recipient (and NFT owner) 6 | └── it should emit {MetadataUpdate} and {Transfer} events 7 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-base/withdraw-hooks/withdrawHooks.tree: -------------------------------------------------------------------------------- 1 | WithdrawHooks_Integration_Concrete_Test 2 | ├── given recipient same as sender 3 | │ └── it should not make Sablier run the user hook 4 | └── given recipient not same as sender 5 | ├── when caller unknown 6 | │ └── it should make Sablier run the recipient hook 7 | ├── when caller approved third party 8 | │ └── it should make Sablier run the recipient hook 9 | ├── when caller sender 10 | │ └── it should make Sablier run the recipient hook 11 | └── when caller recipient 12 | └── it should not make Sablier run the recipient hook 13 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-base/withdraw-max-and-transfer/withdrawMaxAndTransfer.tree: -------------------------------------------------------------------------------- 1 | WithdrawMaxAndTransfer_Integration_Concrete_Test 2 | ├── when delegate call 3 | │ └── it should revert 4 | └── when no delegate call 5 | ├── given null 6 | │ └── it should revert 7 | └── given not null 8 | ├── given non transferable stream 9 | │ └── it should revert 10 | └── given transferable stream 11 | ├── given burned NFT 12 | │ └── it should revert 13 | └── given not burned NFT 14 | ├── given zero withdrawable amount 15 | │ ├── it should not expect a transfer call on token 16 | │ └── it should emit {Transfer} event on NFT 17 | └── given non zero withdrawable amount 18 | ├── when caller not current recipient 19 | │ └── it should revert 20 | ├── when caller approved third party 21 | │ ├── it should update the withdrawn amount 22 | │ └── it should transfer the NFT 23 | └── when caller current recipient 24 | ├── it should update the withdrawn amount 25 | ├── it should transfer the NFT 26 | └── it should emit {Transfer}, {WithdrawFromLockupStream} and {MetadataUpdate} events 27 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-base/withdraw-max/withdrawMax.tree: -------------------------------------------------------------------------------- 1 | WithdrawMax_Integration_Concrete_Test 2 | ├── given end time not in future 3 | │ ├── it should update the withdrawn amount 4 | │ ├── it should mark the stream as depleted 5 | │ ├── it should make the stream not cancelable 6 | │ ├── it should emit a {WithdrawFromLockupStream} event 7 | │ └── it should return the withdrawn amount 8 | └── given end time in future 9 | ├── it should emit a {WithdrawFromLockupStream} event 10 | └── it should return the withdrawable amount 11 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-base/withdraw-multiple/withdrawMultiple.tree: -------------------------------------------------------------------------------- 1 | WithdrawMultiple_Integration_Concrete_Test 2 | ├── when delegate call 3 | │ └── it should revert 4 | └── when no delegate call 5 | ├── when unequal arrays length 6 | │ └── it should revert 7 | └── when equal arrays length 8 | ├── when zero array length 9 | │ └── it should do nothing 10 | └── when non zero array length 11 | ├── when one stream reverts 12 | │ ├── it should emit {WithdrawFromLockupStream} events for non-reverting streams 13 | │ ├── it should emit {InvalidWithdrawalInWithdrawMultiple} event for reverting stream 14 | │ └── it should update the withdrawn amounts only for non-reverting streams 15 | └── when no streams revert 16 | ├── it should make the withdrawals on all streams 17 | ├── it should update the statuses 18 | ├── it should update the withdrawn amounts 19 | └── it should emit {WithdrawFromLockupStream} events for all streams 20 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-base/withdrawable-amount-of/withdrawableAmountOf.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Integration_Test } from "../../../Integration.t.sol"; 5 | 6 | abstract contract WithdrawableAmountOf_Integration_Concrete_Test is Integration_Test { 7 | function test_RevertGiven_Null() external { 8 | expectRevert_Null({ callData: abi.encodeCall(lockup.withdrawableAmountOf, nullStreamId) }); 9 | } 10 | 11 | function test_GivenCanceledStreamAndCANCELEDStatus() external givenNotNull { 12 | vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); 13 | lockup.cancel(defaultStreamId); 14 | 15 | // It should return the correct withdrawable amount. 16 | uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); 17 | uint256 expectedWithdrawableAmount = defaults.STREAMED_AMOUNT_26_PERCENT(); 18 | assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); 19 | } 20 | 21 | function test_GivenCanceledStreamAndDEPLETEDStatus() external givenNotNull { 22 | vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); 23 | lockup.cancel(defaultStreamId); 24 | lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); 25 | vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() + 10 seconds }); 26 | 27 | // It should return zero. 28 | uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); 29 | uint128 expectedWithdrawableAmount = 0; 30 | assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); 31 | } 32 | 33 | function test_GivenPENDINGStatus() external givenNotNull givenNotCanceledStream { 34 | vm.warp({ newTimestamp: getBlockTimestamp() - 1 seconds }); 35 | 36 | // It should return zero. 37 | uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); 38 | uint128 expectedWithdrawableAmount = 0; 39 | assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); 40 | } 41 | 42 | function test_GivenSETTLEDStatus() external givenNotNull givenNotCanceledStream { 43 | vm.warp({ newTimestamp: defaults.END_TIME() }); 44 | 45 | // It should return the correct withdrawable amount. 46 | uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); 47 | uint128 expectedWithdrawableAmount = defaults.DEPOSIT_AMOUNT(); 48 | assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); 49 | } 50 | 51 | function test_GivenDEPLETEDStatus() external givenNotNull givenNotCanceledStream { 52 | vm.warp({ newTimestamp: defaults.END_TIME() }); 53 | lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); 54 | 55 | // It should return zero. 56 | uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); 57 | uint128 expectedWithdrawableAmount = 0; 58 | assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-base/withdrawable-amount-of/withdrawableAmountOf.tree: -------------------------------------------------------------------------------- 1 | WithdrawableAmountOf_Integration_Concrete_Test 2 | ├── given null 3 | │ └── it should revert 4 | └── given not null 5 | ├── given canceled stream and CANCELED status 6 | │ └── it should return the correct withdrawable amount 7 | ├── given canceled stream and DEPLETED status 8 | │ └── it should return zero 9 | └── given not canceled stream 10 | ├── given PENDING status 11 | │ └── it should return zero 12 | ├── given SETTLED status 13 | │ └── it should return the correct withdrawable amount 14 | └── given DEPLETED status 15 | └── it should return zero 16 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-dynamic/LockupDynamic.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Lockup } from "src/types/DataTypes.sol"; 5 | 6 | import { Integration_Test } from "../../Integration.t.sol"; 7 | import { Cancel_Integration_Concrete_Test } from "../lockup-base/cancel/cancel.t.sol"; 8 | import { RefundableAmountOf_Integration_Concrete_Test } from 9 | "../lockup-base/refundable-amount-of/refundableAmountOf.t.sol"; 10 | import { Renounce_Integration_Concrete_Test } from "../lockup-base/renounce/renounce.t.sol"; 11 | import { Withdraw_Integration_Concrete_Test } from "../lockup-base/withdraw/withdraw.t.sol"; 12 | 13 | abstract contract Lockup_Dynamic_Integration_Concrete_Test is Integration_Test { 14 | function setUp() public virtual override { 15 | Integration_Test.setUp(); 16 | 17 | lockupModel = Lockup.Model.LOCKUP_DYNAMIC; 18 | initializeDefaultStreams(); 19 | } 20 | } 21 | 22 | /*////////////////////////////////////////////////////////////////////////// 23 | SHARED TESTS 24 | //////////////////////////////////////////////////////////////////////////*/ 25 | 26 | contract Cancel_Lockup_Dynamic_Integration_Concrete_Test is 27 | Lockup_Dynamic_Integration_Concrete_Test, 28 | Cancel_Integration_Concrete_Test 29 | { 30 | function setUp() public virtual override(Lockup_Dynamic_Integration_Concrete_Test, Integration_Test) { 31 | Lockup_Dynamic_Integration_Concrete_Test.setUp(); 32 | } 33 | } 34 | 35 | contract RefundableAmountOf_Lockup_Dynamic_Integration_Concrete_Test is 36 | Lockup_Dynamic_Integration_Concrete_Test, 37 | RefundableAmountOf_Integration_Concrete_Test 38 | { 39 | function setUp() public virtual override(Lockup_Dynamic_Integration_Concrete_Test, Integration_Test) { 40 | Lockup_Dynamic_Integration_Concrete_Test.setUp(); 41 | } 42 | } 43 | 44 | contract Renounce_Lockup_Dynamic_Integration_Concrete_Test is 45 | Lockup_Dynamic_Integration_Concrete_Test, 46 | Renounce_Integration_Concrete_Test 47 | { 48 | function setUp() public virtual override(Lockup_Dynamic_Integration_Concrete_Test, Integration_Test) { 49 | Lockup_Dynamic_Integration_Concrete_Test.setUp(); 50 | } 51 | } 52 | 53 | contract Withdraw_Lockup_Dynamic_Integration_Concrete_Test is 54 | Lockup_Dynamic_Integration_Concrete_Test, 55 | Withdraw_Integration_Concrete_Test 56 | { 57 | function setUp() public virtual override(Lockup_Dynamic_Integration_Concrete_Test, Integration_Test) { 58 | Lockup_Dynamic_Integration_Concrete_Test.setUp(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-dynamic/create-with-durations-ld/createWithDurationsLD.tree: -------------------------------------------------------------------------------- 1 | CreateWithDurationsLD_Integration_Concrete_Test 2 | ├── when delegate call 3 | │ └── it should revert 4 | └── when no delegate call 5 | ├── when segment count exceeds max value 6 | │ └── it should revert 7 | └── when segment count not exceed max value 8 | ├── when first index has zero duration 9 | │ └── it should revert 10 | └── when first index has non zero duration 11 | ├── when timestamps calculation overflows 12 | │ ├── when start time exceeds first timestamp 13 | │ │ └── it should revert 14 | │ └── when start time not exceeds first timestamp 15 | │ └── when timestamps not strictly increasing 16 | │ └── it should revert 17 | └── when timestamps calculation not overflow 18 | ├── it should create the stream 19 | ├── it should bump the next stream ID 20 | ├── it should mint the NFT 21 | ├── it should emit {CreateLockupDynamicStream} and {MetadataUpdate} events 22 | └── it should perform the ERC-20 transfers 23 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-dynamic/create-with-timestamps-ld/createWithTimestampsLD.tree: -------------------------------------------------------------------------------- 1 | CreateWithTimestampsLD_Integration_Concrete_Test 2 | └── when token contract 3 | ├── when segment count zero 4 | │ └── it should revert 5 | └── when segment count not zero 6 | ├── when segment count exceeds max value 7 | │ └── it should revert 8 | └── when segment count not exceed max value 9 | ├── when segment amounts sum overflows 10 | │ └── it should revert 11 | └── when segment amounts sum not overflow 12 | ├── when start time greater than first timestamp 13 | │ └── it should revert 14 | ├── when start time equals first timestamp 15 | │ └── it should revert 16 | └── when start time less than first timestamp 17 | ├── when timestamps not strictly increasing 18 | │ └── it should revert 19 | └── when timestamps strictly increasing 20 | ├── when deposit amount not equal segment amounts sum 21 | │ └── it should revert 22 | └── when deposit amount equals segment amounts sum 23 | ├── when token misses ERC20 return value 24 | │ ├── it should create the stream 25 | │ ├── it should bump the next stream ID 26 | │ ├── it should mint the NFT 27 | │ ├── it should emit {CreateLockupDynamicStream} and {MetadataUpdate} events 28 | │ └── it should perform the ERC-20 transfers 29 | └── when token not miss ERC20 return value 30 | ├── it should create the stream 31 | ├── it should bump the next stream ID 32 | ├── it should mint the NFT 33 | ├── it should emit {CreateLockupDynamicStream} and {MetadataUpdate} events 34 | └── it should perform the ERC-20 transfers -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-dynamic/get-segments/getSegments.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Errors } from "src/libraries/Errors.sol"; 5 | import { Lockup, LockupDynamic } from "src/types/DataTypes.sol"; 6 | 7 | import { Lockup_Dynamic_Integration_Concrete_Test } from "../LockupDynamic.t.sol"; 8 | 9 | contract GetSegments_Integration_Concrete_Test is Lockup_Dynamic_Integration_Concrete_Test { 10 | function test_RevertGiven_Null() external { 11 | expectRevert_Null({ callData: abi.encodeCall(lockup.getSegments, nullStreamId) }); 12 | } 13 | 14 | function test_RevertGiven_NotDynamicModel() external givenNotNull { 15 | lockupModel = Lockup.Model.LOCKUP_LINEAR; 16 | uint256 streamId = createDefaultStream(); 17 | vm.expectRevert( 18 | abi.encodeWithSelector( 19 | Errors.SablierLockup_NotExpectedModel.selector, Lockup.Model.LOCKUP_LINEAR, Lockup.Model.LOCKUP_DYNAMIC 20 | ) 21 | ); 22 | lockup.getSegments(streamId); 23 | } 24 | 25 | function test_GivenDynamicModel() external givenNotNull { 26 | LockupDynamic.Segment[] memory actualSegments = lockup.getSegments(defaultStreamId); 27 | LockupDynamic.Segment[] memory expectedSegments = defaults.segments(); 28 | assertEq(actualSegments, expectedSegments); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-dynamic/get-segments/getSegments.tree: -------------------------------------------------------------------------------- 1 | GetSegments_Integration_Concrete_Test 2 | ├── given null 3 | │ └── it should revert 4 | └── given not null 5 | ├── given not dynamic model 6 | │ └── it should revert 7 | └── given dynamic model 8 | └── it should return the correct segments 9 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-dynamic/streamed-amount-of/streamedAmountOf.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { LockupDynamic } from "src/types/DataTypes.sol"; 5 | import { StreamedAmountOf_Integration_Concrete_Test } from "../../lockup-base/streamed-amount-of/streamedAmountOf.t.sol"; 6 | import { Lockup_Dynamic_Integration_Concrete_Test, Integration_Test } from "../LockupDynamic.t.sol"; 7 | 8 | contract StreamedAmountOf_Lockup_Dynamic_Integration_Concrete_Test is 9 | Lockup_Dynamic_Integration_Concrete_Test, 10 | StreamedAmountOf_Integration_Concrete_Test 11 | { 12 | function setUp() public virtual override(Lockup_Dynamic_Integration_Concrete_Test, Integration_Test) { 13 | Lockup_Dynamic_Integration_Concrete_Test.setUp(); 14 | } 15 | 16 | function test_GivenSingleSegment() external givenSTREAMINGStatus givenStartTimeInPast givenEndTimeInFuture { 17 | // Simulate the passage of time. 18 | vm.warp({ newTimestamp: defaults.START_TIME() + 2000 seconds }); 19 | 20 | // Create an array with one segment. 21 | LockupDynamic.Segment[] memory segments = new LockupDynamic.Segment[](1); 22 | segments[0] = LockupDynamic.Segment({ 23 | amount: defaults.DEPOSIT_AMOUNT(), 24 | exponent: defaults.segments()[1].exponent, 25 | timestamp: defaults.END_TIME() 26 | }); 27 | 28 | // Create the stream. 29 | uint256 streamId = lockup.createWithTimestampsLD(_defaultParams.createWithTimestamps, segments); 30 | 31 | // It should return the correct streamed amount. 32 | uint128 actualStreamedAmount = lockup.streamedAmountOf(streamId); 33 | uint128 expectedStreamedAmount = 4472.13595499957941e18; // (0.2^0.5)*10,000 34 | assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); 35 | } 36 | 37 | function test_GivenMultipleSegments() external givenSTREAMINGStatus givenStartTimeInPast givenEndTimeInFuture { 38 | // Simulate the passage of time. 740 seconds is ~10% of the way in the second segment. 39 | vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() + 740 seconds }); 40 | 41 | // It should return the correct streamed amount. 42 | uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); 43 | uint128 expectedStreamedAmount = defaults.segments()[0].amount + 2340.0854685246007116e18; // ~7,400*0.1^{0.5} 44 | assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-dynamic/streamed-amount-of/streamedAmountOf.tree: -------------------------------------------------------------------------------- 1 | StreamedAmountOf_Lockup_Dynamic_Integration_Concrete_Test 2 | └── given STREAMING status 3 | ├── given single segment 4 | │ └── it should return the correct streamed amount 5 | └── given multiple segments 6 | └── it should return the correct streamed amount 7 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-dynamic/withdrawable-amount-of/withdrawableAmountOf.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { WithdrawableAmountOf_Integration_Concrete_Test } from 5 | "../../lockup-base/withdrawable-amount-of/withdrawableAmountOf.t.sol"; 6 | import { Lockup_Dynamic_Integration_Concrete_Test, Integration_Test } from "../LockupDynamic.t.sol"; 7 | 8 | contract WithdrawableAmountOf_Lockup_Dynamic_Integration_Concrete_Test is 9 | Lockup_Dynamic_Integration_Concrete_Test, 10 | WithdrawableAmountOf_Integration_Concrete_Test 11 | { 12 | function setUp() public virtual override(Lockup_Dynamic_Integration_Concrete_Test, Integration_Test) { 13 | Lockup_Dynamic_Integration_Concrete_Test.setUp(); 14 | } 15 | 16 | function test_GivenStartTimeInPresent() external givenSTREAMINGStatus { 17 | vm.warp({ newTimestamp: defaults.START_TIME() }); 18 | uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); 19 | uint128 expectedWithdrawableAmount = 0; 20 | assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); 21 | } 22 | 23 | function test_GivenNoPreviousWithdrawals() external givenSTREAMINGStatus givenStartTimeInPast { 24 | // Simulate the passage of time. 25 | vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() + 3750 seconds }); 26 | 27 | // Run the test. 28 | uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); 29 | // The second term is 7,400*0.5^{0.5} 30 | uint128 expectedWithdrawableAmount = defaults.segments()[0].amount + 5267.8268764263694426e18; 31 | assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); 32 | } 33 | 34 | function test_GivenPreviousWithdrawal() external givenSTREAMINGStatus givenStartTimeInPast { 35 | // Simulate the passage of time. 36 | vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() + 3750 seconds }); 37 | 38 | // Make the withdrawal. 39 | lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: defaults.STREAMED_AMOUNT_26_PERCENT() }); 40 | 41 | // Run the test. 42 | uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); 43 | 44 | // The second term is 7,500*0.5^{0.5} 45 | uint128 expectedWithdrawableAmount = 46 | defaults.segments()[0].amount + 5267.8268764263694426e18 - defaults.STREAMED_AMOUNT_26_PERCENT(); 47 | assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-dynamic/withdrawable-amount-of/withdrawableAmountOf.tree: -------------------------------------------------------------------------------- 1 | WithdrawableAmountOf_Lockup_Dynamic_Integration_Concrete_Test 2 | └── given STREAMING status 3 | ├── given start time in present 4 | │ └── it should return zero 5 | └── given start time in past 6 | ├── given no previous withdrawals 7 | │ └── it should return the correct withdrawable amount 8 | └── given previous withdrawal 9 | └── it should return the correct withdrawable amount 10 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-linear/LockupLinear.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Lockup } from "src/types/DataTypes.sol"; 5 | 6 | import { Integration_Test } from "../../Integration.t.sol"; 7 | import { Cancel_Integration_Concrete_Test } from "../lockup-base/cancel/cancel.t.sol"; 8 | import { RefundableAmountOf_Integration_Concrete_Test } from 9 | "../lockup-base/refundable-amount-of/refundableAmountOf.t.sol"; 10 | import { Renounce_Integration_Concrete_Test } from "../lockup-base/renounce/renounce.t.sol"; 11 | import { Withdraw_Integration_Concrete_Test } from "../lockup-base/withdraw/withdraw.t.sol"; 12 | 13 | abstract contract Lockup_Linear_Integration_Concrete_Test is Integration_Test { 14 | function setUp() public virtual override { 15 | Integration_Test.setUp(); 16 | 17 | lockupModel = Lockup.Model.LOCKUP_LINEAR; 18 | initializeDefaultStreams(); 19 | } 20 | } 21 | 22 | /*////////////////////////////////////////////////////////////////////////// 23 | SHARED TESTS 24 | //////////////////////////////////////////////////////////////////////////*/ 25 | 26 | contract Cancel_Lockup_Linear_Integration_Concrete_Test is 27 | Lockup_Linear_Integration_Concrete_Test, 28 | Cancel_Integration_Concrete_Test 29 | { 30 | function setUp() public virtual override(Lockup_Linear_Integration_Concrete_Test, Integration_Test) { 31 | Lockup_Linear_Integration_Concrete_Test.setUp(); 32 | } 33 | } 34 | 35 | contract RefundableAmountOf_Lockup_Linear_Integration_Concrete_Test is 36 | Lockup_Linear_Integration_Concrete_Test, 37 | RefundableAmountOf_Integration_Concrete_Test 38 | { 39 | function setUp() public virtual override(Lockup_Linear_Integration_Concrete_Test, Integration_Test) { 40 | Lockup_Linear_Integration_Concrete_Test.setUp(); 41 | } 42 | } 43 | 44 | contract Renounce_Lockup_Linear_Integration_Concrete_Test is 45 | Lockup_Linear_Integration_Concrete_Test, 46 | Renounce_Integration_Concrete_Test 47 | { 48 | function setUp() public virtual override(Lockup_Linear_Integration_Concrete_Test, Integration_Test) { 49 | Lockup_Linear_Integration_Concrete_Test.setUp(); 50 | } 51 | } 52 | 53 | contract Withdraw_Lockup_Linear_Integration_Concrete_Test is 54 | Lockup_Linear_Integration_Concrete_Test, 55 | Withdraw_Integration_Concrete_Test 56 | { 57 | function setUp() public virtual override(Lockup_Linear_Integration_Concrete_Test, Integration_Test) { 58 | Lockup_Linear_Integration_Concrete_Test.setUp(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-linear/create-with-durations-ll/createWithDurationsLL.tree: -------------------------------------------------------------------------------- 1 | CreateWithDurationsLL_Integration_Concrete_Test 2 | ├── when delegate call 3 | │ └── it should revert 4 | └── when no delegate call 5 | ├── when cliff duration not zero 6 | │ ├── it should create the stream 7 | │ ├── it should bump the next stream ID 8 | │ ├── it should mint the NFT 9 | │ ├── it should perform the ERC-20 transfers 10 | │ └── it should emit {CreateLockupLinearStream} and {MetadataUpdate} events 11 | └── when cliff duration zero 12 | ├── it should create the stream 13 | ├── it should bump the next stream ID 14 | ├── it should mint the NFT 15 | ├── it should perform the ERC-20 transfers 16 | └── it should emit {CreateLockupLinearStream} and {MetadataUpdate} events 17 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-linear/create-with-timestamps-ll/createWithTimestampsLL.tree: -------------------------------------------------------------------------------- 1 | CreateWithTimestampsLL_Integration_Concrete_Test 2 | └── when token contract 3 | ├── when cliff time zero 4 | │ ├── when cliff unlock amount not zero 5 | │ │ └── it should revert 6 | │ ├── when start time not less than end time 7 | │ │ └── it should revert 8 | │ └── when start time less than end time 9 | │ └── it should create the stream 10 | └── when cliff time not zero 11 | ├── when start time not less than cliff time 12 | │ └── it should revert 13 | └── when start time less than cliff time 14 | ├── when cliff time not less than end time 15 | │ └── it should revert 16 | └── when cliff time less than end time 17 | ├── when unlock amounts sum exceeds deposit amount 18 | │ └── it should revert 19 | └── when unlock amounts sum not exceed deposit amount 20 | ├── when token misses ERC20 return value 21 | │ ├── it should create the stream 22 | │ ├── it should bump the next stream ID 23 | │ ├── it should mint the NFT 24 | │ ├── it should emit {MetadataUpdate} and {CreateLockupLinearStream} events 25 | │ └── it should perform the ERC-20 transfers 26 | └── when token not miss ERC20 return value 27 | ├── it should create the stream 28 | ├── it should bump the next stream ID 29 | ├── it should mint the NFT 30 | ├── it should emit {MetadataUpdate} and {CreateLockupLinearStream} events 31 | └── it should perform the ERC-20 transfers -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-linear/get-cliff-time/getCliffTime.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Errors } from "src/libraries/Errors.sol"; 5 | import { Lockup } from "src/types/DataTypes.sol"; 6 | 7 | import { Lockup_Linear_Integration_Concrete_Test } from "../LockupLinear.t.sol"; 8 | 9 | contract GetCliffTime_Integration_Concrete_Test is Lockup_Linear_Integration_Concrete_Test { 10 | function test_RevertGiven_Null() external { 11 | expectRevert_Null({ callData: abi.encodeCall(lockup.getCliffTime, nullStreamId) }); 12 | } 13 | 14 | function test_RevertGiven_NotLinearModel() external givenNotNull { 15 | lockupModel = Lockup.Model.LOCKUP_TRANCHED; 16 | uint256 streamId = createDefaultStream(); 17 | vm.expectRevert( 18 | abi.encodeWithSelector( 19 | Errors.SablierLockup_NotExpectedModel.selector, Lockup.Model.LOCKUP_TRANCHED, Lockup.Model.LOCKUP_LINEAR 20 | ) 21 | ); 22 | lockup.getCliffTime(streamId); 23 | } 24 | 25 | function test_GivenLinearModel() external view givenNotNull { 26 | uint40 actualCliffTime = lockup.getCliffTime(defaultStreamId); 27 | uint40 expectedCliffTime = defaults.CLIFF_TIME(); 28 | assertEq(actualCliffTime, expectedCliffTime, "cliffTime"); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-linear/get-cliff-time/getCliffTime.tree: -------------------------------------------------------------------------------- 1 | GetCliffTime_Integration_Concrete_Test 2 | ├── given null 3 | │ └── it should revert 4 | └── given not null 5 | ├── given not linear model 6 | │ └── it should revert 7 | └── given linear model 8 | └── it should return the correct cliff time 9 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-linear/get-unlock-amounts/getUnlockAmounts.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Errors } from "src/libraries/Errors.sol"; 5 | import { Lockup, LockupLinear } from "src/types/DataTypes.sol"; 6 | 7 | import { Lockup_Linear_Integration_Concrete_Test } from "../LockupLinear.t.sol"; 8 | 9 | contract GetUnlockAmounts_Integration_Concrete_Test is Lockup_Linear_Integration_Concrete_Test { 10 | function test_RevertGiven_Null() external { 11 | expectRevert_Null({ callData: abi.encodeCall(lockup.getUnlockAmounts, nullStreamId) }); 12 | } 13 | 14 | function test_RevertGiven_NotLinearModel() external givenNotNull { 15 | lockupModel = Lockup.Model.LOCKUP_TRANCHED; 16 | uint256 streamId = createDefaultStream(); 17 | vm.expectRevert( 18 | abi.encodeWithSelector( 19 | Errors.SablierLockup_NotExpectedModel.selector, Lockup.Model.LOCKUP_TRANCHED, Lockup.Model.LOCKUP_LINEAR 20 | ) 21 | ); 22 | lockup.getUnlockAmounts(streamId); 23 | } 24 | 25 | function test_GivenBothAmountsZero() external givenNotNull givenLinearModel { 26 | _defaultParams.unlockAmounts = defaults.unlockAmountsZero(); 27 | uint256 streamId = createDefaultStream(); 28 | LockupLinear.UnlockAmounts memory unlockAmounts = lockup.getUnlockAmounts(streamId); 29 | assertEq(unlockAmounts.start, 0, "unlockAmounts.start"); 30 | assertEq(unlockAmounts.cliff, 0, "unlockAmounts.cliff"); 31 | } 32 | 33 | function test_GivenStartUnlockAmountZero() external view givenNotNull givenLinearModel givenOnlyOneAmountZero { 34 | LockupLinear.UnlockAmounts memory unlockAmounts = lockup.getUnlockAmounts(defaultStreamId); 35 | assertEq(unlockAmounts.start, 0, "unlockAmounts.start"); 36 | assertEq(unlockAmounts.cliff, defaults.CLIFF_AMOUNT(), "unlockAmounts.cliff"); 37 | } 38 | 39 | function test_GivenStartUnlockAmountNotZero() external givenNotNull givenLinearModel givenOnlyOneAmountZero { 40 | _defaultParams.unlockAmounts.start = 1; 41 | _defaultParams.unlockAmounts.cliff = 0; 42 | uint256 streamId = createDefaultStream(); 43 | LockupLinear.UnlockAmounts memory unlockAmounts = lockup.getUnlockAmounts(streamId); 44 | assertEq(unlockAmounts.start, 1, "unlockAmounts.start"); 45 | assertEq(unlockAmounts.cliff, 0, "unlockAmounts.cliff"); 46 | } 47 | 48 | function test_GivenBothAmountsNotZero() external givenNotNull givenLinearModel { 49 | _defaultParams.unlockAmounts.start = 1; 50 | uint256 streamId = createDefaultStream(); 51 | LockupLinear.UnlockAmounts memory unlockAmounts = lockup.getUnlockAmounts(streamId); 52 | assertEq(unlockAmounts.start, 1, "unlockAmounts.start"); 53 | assertEq(unlockAmounts.cliff, defaults.CLIFF_AMOUNT(), "unlockAmounts.cliff"); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-linear/get-unlock-amounts/getUnlockAmounts.tree: -------------------------------------------------------------------------------- 1 | GetUnlockAmounts_Integration_Concrete_Test 2 | ├── given null 3 | │ └── it should revert 4 | └── given not null 5 | ├── given not linear model 6 | │ └── it should revert 7 | └── given linear model 8 | ├── given both amounts zero 9 | │ └── it should return the correct unlock amount 10 | ├── given only one amount zero 11 | │ ├── given start unlock amount zero 12 | │ │ └── it should return the correct unlock amount 13 | │ └── given start unlock amount not zero 14 | │ └── it should return the correct unlock amount 15 | └── given both amounts not zero 16 | └── it should return the correct unlock amount 17 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-linear/streamed-amount-of/streamedAmountOf.tree: -------------------------------------------------------------------------------- 1 | StreamedAmountOf_Lockup_Linear_Integration_Concrete_Test 2 | └── given STREAMING status 3 | ├── given cliff time zero 4 | │ └── it should return correct streamed amount 5 | └── given cliff time not zero 6 | ├── given cliff time in future 7 | │ └── it should return start amount 8 | ├── given cliff time in present 9 | │ └── it should return the correct streamed amount 10 | └── given cliff time in past 11 | ├── given start amount 12 | │ └── it should return correct streamed amount 13 | └── given no start amount 14 | ├── given no cliff amount 15 | │ └── it should return correct streamed amount 16 | └── given cliff amount 17 | └── it should return correct streamed amount -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-linear/withdrawable-amount-of/withdrawableAmountOf.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { WithdrawableAmountOf_Integration_Concrete_Test } from 5 | "../../lockup-base/withdrawable-amount-of/withdrawableAmountOf.t.sol"; 6 | import { Lockup_Linear_Integration_Concrete_Test, Integration_Test } from "./../LockupLinear.t.sol"; 7 | 8 | contract WithdrawableAmountOf_Lockup_Linear_Integration_Concrete_Test is 9 | Lockup_Linear_Integration_Concrete_Test, 10 | WithdrawableAmountOf_Integration_Concrete_Test 11 | { 12 | function setUp() public virtual override(Lockup_Linear_Integration_Concrete_Test, Integration_Test) { 13 | Lockup_Linear_Integration_Concrete_Test.setUp(); 14 | } 15 | 16 | function test_GivenCliffTimeInFuture() external givenSTREAMINGStatus { 17 | vm.warp({ newTimestamp: defaults.CLIFF_TIME() - 1 }); 18 | uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); 19 | uint128 expectedWithdrawableAmount = 0; 20 | assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); 21 | } 22 | 23 | function test_GivenNoPreviousWithdrawals() external givenSTREAMINGStatus givenCliffTimeNotInFuture { 24 | uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); 25 | uint128 expectedWithdrawableAmount = defaults.STREAMED_AMOUNT_26_PERCENT(); 26 | assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); 27 | } 28 | 29 | function test_GivenPreviousWithdrawal() external givenSTREAMINGStatus givenCliffTimeNotInFuture { 30 | lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: defaults.STREAMED_AMOUNT_26_PERCENT() }); 31 | 32 | uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); 33 | uint128 expectedWithdrawableAmount = 0; 34 | assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-linear/withdrawable-amount-of/withdrawableAmountOf.tree: -------------------------------------------------------------------------------- 1 | WithdrawableAmountOf_Lockup_Linear_Integration_Concrete_Test 2 | └── given STREAMING status 3 | ├── given cliff time in future 4 | │ └── it should return zero 5 | └── given cliff time not in future 6 | ├── given no previous withdrawals 7 | │ └── it should return the correct withdrawable amount 8 | └── given previous withdrawal 9 | └── it should return the correct withdrawable amount 10 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-tranched/LockupTranched.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Lockup } from "src/types/DataTypes.sol"; 5 | 6 | import { Integration_Test } from "../../Integration.t.sol"; 7 | import { Cancel_Integration_Concrete_Test } from "../lockup-base/cancel/cancel.t.sol"; 8 | import { RefundableAmountOf_Integration_Concrete_Test } from 9 | "../lockup-base/refundable-amount-of/refundableAmountOf.t.sol"; 10 | import { Renounce_Integration_Concrete_Test } from "../lockup-base/renounce/renounce.t.sol"; 11 | import { Withdraw_Integration_Concrete_Test } from "../lockup-base/withdraw/withdraw.t.sol"; 12 | 13 | abstract contract Lockup_Tranched_Integration_Concrete_Test is Integration_Test { 14 | function setUp() public virtual override { 15 | Integration_Test.setUp(); 16 | 17 | lockupModel = Lockup.Model.LOCKUP_TRANCHED; 18 | initializeDefaultStreams(); 19 | } 20 | } 21 | 22 | /*////////////////////////////////////////////////////////////////////////// 23 | SHARED TESTS 24 | //////////////////////////////////////////////////////////////////////////*/ 25 | 26 | contract Cancel_Lockup_Tranched_Integration_Concrete_Test is 27 | Lockup_Tranched_Integration_Concrete_Test, 28 | Cancel_Integration_Concrete_Test 29 | { 30 | function setUp() public virtual override(Lockup_Tranched_Integration_Concrete_Test, Integration_Test) { 31 | Lockup_Tranched_Integration_Concrete_Test.setUp(); 32 | } 33 | } 34 | 35 | contract RefundableAmountOf_Lockup_Tranched_Integration_Concrete_Test is 36 | Lockup_Tranched_Integration_Concrete_Test, 37 | RefundableAmountOf_Integration_Concrete_Test 38 | { 39 | function setUp() public virtual override(Lockup_Tranched_Integration_Concrete_Test, Integration_Test) { 40 | Lockup_Tranched_Integration_Concrete_Test.setUp(); 41 | } 42 | } 43 | 44 | contract Renounce_Lockup_Tranched_Integration_Concrete_Test is 45 | Lockup_Tranched_Integration_Concrete_Test, 46 | Renounce_Integration_Concrete_Test 47 | { 48 | function setUp() public virtual override(Lockup_Tranched_Integration_Concrete_Test, Integration_Test) { 49 | Lockup_Tranched_Integration_Concrete_Test.setUp(); 50 | } 51 | } 52 | 53 | contract Withdraw_Lockup_Tranched_Integration_Concrete_Test is 54 | Lockup_Tranched_Integration_Concrete_Test, 55 | Withdraw_Integration_Concrete_Test 56 | { 57 | function setUp() public virtual override(Lockup_Tranched_Integration_Concrete_Test, Integration_Test) { 58 | Lockup_Tranched_Integration_Concrete_Test.setUp(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-tranched/create-with-durations-lt/createWithDurationsLT.tree: -------------------------------------------------------------------------------- 1 | CreateWithDurationsLT_Integration_Concrete_Test 2 | ├── when delegate call 3 | │ └── it should revert 4 | └── when no delegate call 5 | ├── when tranche count exceeds max value 6 | │ └── it should revert 7 | └── when tranche count not exceed max value 8 | ├── when first index has zero duration 9 | │ └── it should revert 10 | └── when first index has non zero duration 11 | ├── when timestamps calculation overflows 12 | │ ├── when start time exceeds first timestamp 13 | │ │ └── it should revert 14 | │ └── when start time not exceeds first timestamp 15 | │ └── when timestamps not strictly increasing 16 | │ └── it should revert 17 | └── when timestamps calculation not overflow 18 | ├── it should create the stream 19 | ├── it should bump the next stream ID 20 | ├── it should mint the NFT 21 | ├── it should emit {CreateLockupDynamicStream} and {MetadataUpdate} events 22 | └── it should perform the ERC-20 transfers 23 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-tranched/create-with-timestamps-lt/createWithTimestampsLT.tree: -------------------------------------------------------------------------------- 1 | CreateWithTimestampsLT_Integration_Concrete_Test 2 | └── when token contract 3 | ├── when tranche count zero 4 | │ └── it should revert 5 | └── when tranche count not zero 6 | ├── when tranche count exceeds max value 7 | │ └── it should revert 8 | └── when tranche count not exceed max value 9 | ├── when tranche amounts sum overflows 10 | │ └── it should revert 11 | └── when tranche amounts sum not overflow 12 | ├── when start time greater than first timestamp 13 | │ └── it should revert 14 | ├── when start time equals first timestamp 15 | │ └── it should revert 16 | └── when start time less than first timestamp 17 | ├── when timestamps not strictly increasing 18 | │ └── it should revert 19 | └── when timestamps strictly increasing 20 | ├── when deposit amount not equal tranche amounts sum 21 | │ └── it should revert 22 | └── when deposit amount equals tranche amounts sum 23 | ├── when token misses ERC20 return value 24 | │ ├── it should create the stream 25 | │ ├── it should bump the next stream ID 26 | │ ├── it should mint the NFT 27 | │ ├── it should emit {CreateLockupTranchedStream} and {MetadataUpdate} events 28 | │ └── it should perform the ERC-20 transfers 29 | └── when token not miss ERC20 return value 30 | ├── it should create the stream 31 | ├── it should bump the next stream ID 32 | ├── it should mint the NFT 33 | ├── it should emit {CreateLockupTranchedStream} and {MetadataUpdate} events 34 | └── it should perform the ERC-20 transfers -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-tranched/get-tranches/getTranches.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Errors } from "src/libraries/Errors.sol"; 5 | import { Lockup, LockupTranched } from "src/types/DataTypes.sol"; 6 | 7 | import { Lockup_Tranched_Integration_Concrete_Test } from "../LockupTranched.t.sol"; 8 | 9 | contract GetTranches_Integration_Concrete_Test is Lockup_Tranched_Integration_Concrete_Test { 10 | function test_RevertGiven_Null() external { 11 | expectRevert_Null({ callData: abi.encodeCall(lockup.getTranches, nullStreamId) }); 12 | } 13 | 14 | function test_RevertGiven_NotTranchedModel() external givenNotNull { 15 | lockupModel = Lockup.Model.LOCKUP_LINEAR; 16 | uint256 streamId = createDefaultStream(); 17 | vm.expectRevert( 18 | abi.encodeWithSelector( 19 | Errors.SablierLockup_NotExpectedModel.selector, Lockup.Model.LOCKUP_LINEAR, Lockup.Model.LOCKUP_TRANCHED 20 | ) 21 | ); 22 | lockup.getTranches(streamId); 23 | } 24 | 25 | function test_GivenTranchedModel() external givenNotNull { 26 | LockupTranched.Tranche[] memory actualTranches = lockup.getTranches(defaultStreamId); 27 | LockupTranched.Tranche[] memory expectedTranches = defaults.tranches(); 28 | assertEq(actualTranches, expectedTranches); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-tranched/get-tranches/getTranches.tree: -------------------------------------------------------------------------------- 1 | GetTranches_Integration_Concrete_Test 2 | ├── given null 3 | │ └── it should revert 4 | └── given not null 5 | ├── given not tranched model 6 | │ └── it should revert 7 | └── given tranched model 8 | └── it should return the correct tranches 9 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-tranched/streamed-amount-of/streamedAmountOf.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { StreamedAmountOf_Integration_Concrete_Test } from "../../lockup-base/streamed-amount-of/streamedAmountOf.t.sol"; 5 | import { Lockup_Tranched_Integration_Concrete_Test, Integration_Test } from "./../LockupTranched.t.sol"; 6 | 7 | contract StreamedAmountOf_Lockup_Tranched_Integration_Concrete_Test is 8 | Lockup_Tranched_Integration_Concrete_Test, 9 | StreamedAmountOf_Integration_Concrete_Test 10 | { 11 | function setUp() public virtual override(Lockup_Tranched_Integration_Concrete_Test, Integration_Test) { 12 | Lockup_Tranched_Integration_Concrete_Test.setUp(); 13 | } 14 | 15 | function test_GivenStartTimeInPresent() external givenSTREAMINGStatus { 16 | vm.warp({ newTimestamp: defaults.START_TIME() }); 17 | uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); 18 | uint128 expectedStreamedAmount = 0; 19 | assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); 20 | } 21 | 22 | function test_GivenEndTimeNotInFuture() external givenSTREAMINGStatus givenStartTimeInPast { 23 | vm.warp({ newTimestamp: defaults.END_TIME() + 1 seconds }); 24 | 25 | // It should return the deposited amount. 26 | uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); 27 | uint128 expectedStreamedAmount = defaults.DEPOSIT_AMOUNT(); 28 | assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); 29 | } 30 | 31 | function test_GivenFirstTrancheTimestampInFuture() 32 | external 33 | givenSTREAMINGStatus 34 | givenStartTimeInPast 35 | givenEndTimeInFuture 36 | { 37 | vm.warp({ newTimestamp: defaults.START_TIME() + 1 seconds }); 38 | 39 | // It should return 0. 40 | uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); 41 | uint128 expectedStreamedAmount = 0; 42 | assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); 43 | } 44 | 45 | function test_GivenFirstTrancheTimestampNotInFuture() 46 | external 47 | givenSTREAMINGStatus 48 | givenStartTimeInPast 49 | givenEndTimeInFuture 50 | { 51 | vm.warp({ newTimestamp: defaults.END_TIME() - 1 seconds }); 52 | 53 | // It should return the correct streamed amount. 54 | uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); 55 | uint128 expectedStreamedAmount = defaults.tranches()[0].amount; 56 | assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-tranched/streamed-amount-of/streamedAmountOf.tree: -------------------------------------------------------------------------------- 1 | StreamedAmountOf_Lockup_Tranched_Integration_Concrete_Test 2 | └── given STREAMING status 3 | ├── given start time in present 4 | │ └── it should return zero 5 | └── given start time in past 6 | ├── given first tranche timestamp in future 7 | │ └── it should return 0 8 | └── given first tranche timestamp not in future 9 | └── it should return the correct streamed amount 10 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-tranched/withdrawable-amount-of/withdrawableAmountOf.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { WithdrawableAmountOf_Integration_Concrete_Test } from 5 | "../../lockup-base/withdrawable-amount-of/withdrawableAmountOf.t.sol"; 6 | import { Lockup_Tranched_Integration_Concrete_Test, Integration_Test } from "./../LockupTranched.t.sol"; 7 | 8 | contract WithdrawableAmountOf_Lockup_Tranched_Integration_Concrete_Test is 9 | Lockup_Tranched_Integration_Concrete_Test, 10 | WithdrawableAmountOf_Integration_Concrete_Test 11 | { 12 | function setUp() public virtual override(Lockup_Tranched_Integration_Concrete_Test, Integration_Test) { 13 | Lockup_Tranched_Integration_Concrete_Test.setUp(); 14 | } 15 | 16 | function test_GivenStartTimeInPresent() external givenSTREAMINGStatus { 17 | vm.warp({ newTimestamp: defaults.START_TIME() }); 18 | uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); 19 | uint128 expectedWithdrawableAmount = 0; 20 | assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); 21 | } 22 | 23 | function test_GivenNoPreviousWithdrawals() external givenSTREAMINGStatus givenStartTimeInPast { 24 | // Simulate the passage of time. 25 | vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); 26 | 27 | // Run the test. 28 | uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); 29 | uint128 expectedWithdrawableAmount = defaults.tranches()[0].amount; 30 | assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); 31 | } 32 | 33 | function test_GivenPreviousWithdrawal() external givenSTREAMINGStatus givenStartTimeInPast { 34 | // Simulate the passage of time. 35 | vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); 36 | 37 | // Make the withdrawal. 38 | lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: defaults.STREAMED_AMOUNT_26_PERCENT() }); 39 | 40 | // Run the test. 41 | uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); 42 | 43 | uint128 expectedWithdrawableAmount = defaults.tranches()[0].amount - defaults.STREAMED_AMOUNT_26_PERCENT(); 44 | assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/integration/concrete/lockup-tranched/withdrawable-amount-of/withdrawableAmountOf.tree: -------------------------------------------------------------------------------- 1 | WithdrawableAmountOf_Lockup_Tranched_Integration_Concrete_Test 2 | └── given STREAMING status 3 | ├── given start time in present 4 | │ └── it should return zero 5 | └── given start time in past 6 | ├── given no previous withdrawals 7 | │ └── it should return the correct withdrawable amount 8 | └── given previous withdrawal 9 | └── it should return the correct withdrawable amount 10 | -------------------------------------------------------------------------------- /tests/integration/concrete/nft-descriptor/generateAccentColor.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Base_Test } from "tests/Base.t.sol"; 5 | 6 | contract GenerateAccentColor_Integration_Concrete_Test is Base_Test { 7 | function test_GenerateAccentColor() external view { 8 | // Passing a dummy contract instead of a real Lockup contract to make this test easy to maintain. 9 | // Note: the address of `noop` depends on the order of the state variables in {Base_Test}. 10 | string memory actualColor = nftDescriptorMock.generateAccentColor_({ sablier: address(noop), streamId: 1337 }); 11 | string memory expectedColor = "hsl(115,39%,48%)"; 12 | assertEq(actualColor, expectedColor, "accentColor"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/integration/concrete/nft-descriptor/is-allowed-character/IsAllowedCharacter.tree: -------------------------------------------------------------------------------- 1 | IsAllowedCharacter_Integration_Concrete_Test 2 | ├── when empty string 3 | │ └── it should return true 4 | └── when not empty string 5 | ├── given unsupported characters 6 | │ └── it should return false 7 | └── given supported characters 8 | └── it should return true 9 | -------------------------------------------------------------------------------- /tests/integration/concrete/nft-descriptor/safe-token-decimals/safeTokenDecimals.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Base_Test } from "tests/Base.t.sol"; 5 | 6 | contract SafeTokenDecimals_Integration_Concrete_Test is Base_Test { 7 | function test_WhenTokenNotContract() external view { 8 | address eoa = vm.addr({ privateKey: 1 }); 9 | uint8 actualDecimals = nftDescriptorMock.safeTokenDecimals_(address(eoa)); 10 | uint8 expectedDecimals = 0; 11 | assertEq(actualDecimals, expectedDecimals, "decimals"); 12 | } 13 | 14 | function test_WhenDecimalsNotImplemented() external view whenTokenContract { 15 | uint8 actualDecimals = nftDescriptorMock.safeTokenDecimals_(address(noop)); 16 | uint8 expectedDecimals = 0; 17 | assertEq(actualDecimals, expectedDecimals, "decimals"); 18 | } 19 | 20 | function test_WhenDecimalsImplemented() external view whenTokenContract { 21 | uint8 actualDecimals = nftDescriptorMock.safeTokenDecimals_(address(dai)); 22 | uint8 expectedDecimals = dai.decimals(); 23 | assertEq(actualDecimals, expectedDecimals, "decimals"); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/integration/concrete/nft-descriptor/safe-token-decimals/safeTokenDecimals.tree: -------------------------------------------------------------------------------- 1 | SafeTokenDecimals_Integration_Concrete_Test 2 | ├── when token not contract 3 | │ └── it should return 0 4 | └── when token contract 5 | ├── when decimals not implemented 6 | │ └── it should return 0 7 | └── when decimals implemented 8 | └── it should return the correct decimal value 9 | -------------------------------------------------------------------------------- /tests/integration/concrete/nft-descriptor/safe-token-symbol/safeTokenSymbol.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Base_Test } from "tests/Base.t.sol"; 5 | import { ERC20Bytes32 } from "tests/mocks/erc20/ERC20Bytes32.sol"; 6 | import { ERC20Mock } from "tests/mocks/erc20/ERC20Mock.sol"; 7 | 8 | contract SafeTokenSymbol_Integration_Concrete_Test is Base_Test { 9 | function test_WhenTokenNotContract() external view { 10 | address eoa = vm.addr({ privateKey: 1 }); 11 | string memory actualSymbol = nftDescriptorMock.safeTokenSymbol_(address(eoa)); 12 | string memory expectedSymbol = "ERC20"; 13 | assertEq(actualSymbol, expectedSymbol, "symbol"); 14 | } 15 | 16 | function test_GivenSymbolNotImplemented() external view whenTokenContract { 17 | string memory actualSymbol = nftDescriptorMock.safeTokenSymbol_(address(noop)); 18 | string memory expectedSymbol = "ERC20"; 19 | assertEq(actualSymbol, expectedSymbol, "symbol"); 20 | } 21 | 22 | function test_GivenSymbolAsBytes32() external whenTokenContract givenSymbolImplemented { 23 | ERC20Bytes32 token = new ERC20Bytes32(); 24 | string memory actualSymbol = nftDescriptorMock.safeTokenSymbol_(address(token)); 25 | string memory expectedSymbol = "ERC20"; 26 | assertEq(actualSymbol, expectedSymbol, "symbol"); 27 | } 28 | 29 | function test_GivenSymbolLongerThan30Chars() 30 | external 31 | whenTokenContract 32 | givenSymbolImplemented 33 | givenSymbolAsString 34 | { 35 | ERC20Mock token = new ERC20Mock({ 36 | name: "Token", 37 | symbol: "This symbol is has more than 30 characters and it should be ignored" 38 | }); 39 | string memory actualSymbol = nftDescriptorMock.safeTokenSymbol_(address(token)); 40 | string memory expectedSymbol = "Long Symbol"; 41 | assertEq(actualSymbol, expectedSymbol, "symbol"); 42 | } 43 | 44 | function test_GivenSymbolContainsNon_alphanumericChars() 45 | external 46 | whenTokenContract 47 | givenSymbolImplemented 48 | givenSymbolAsString 49 | givenSymbolNotLongerThan30Chars 50 | { 51 | ERC20Mock token = new ERC20Mock({ name: "Token", symbol: "" }); 52 | string memory actualSymbol = nftDescriptorMock.safeTokenSymbol_(address(token)); 53 | string memory expectedSymbol = "Unsupported Symbol"; 54 | assertEq(actualSymbol, expectedSymbol, "symbol"); 55 | } 56 | 57 | function test_GivenSymbolContainsAlphanumericChars() 58 | external 59 | view 60 | whenTokenContract 61 | givenSymbolImplemented 62 | givenSymbolAsString 63 | givenSymbolNotLongerThan30Chars 64 | { 65 | string memory actualSymbol = nftDescriptorMock.safeTokenSymbol_(address(dai)); 66 | string memory expectedSymbol = dai.symbol(); 67 | assertEq(actualSymbol, expectedSymbol, "symbol"); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/integration/concrete/nft-descriptor/safe-token-symbol/safeTokenSymbol.tree: -------------------------------------------------------------------------------- 1 | SafeTokenSymbol_Integration_Concrete_Test 2 | ├── when token not contract 3 | │ └── it should return a hard-coded value 4 | └── when token contract 5 | ├── given symbol not implemented 6 | │ └── it should return a hard-coded value 7 | └── given symbol implemented 8 | ├── given symbol as bytes32 9 | │ └── it should return a hard-coded value 10 | └── given symbol as string 11 | ├── given symbol longer than 30 chars 12 | │ └── it should return a hard-coded values 13 | └── given symbol not longer than 30 chars 14 | ├── given symbol contains non-alphanumeric chars 15 | │ └── it should return a hard-coded value 16 | └── given symbol contains alphanumeric chars 17 | └── it should return the correct symbol value 18 | -------------------------------------------------------------------------------- /tests/integration/fuzz/lockup-base/refundableAmountOf.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Integration_Test } from "../../Integration.t.sol"; 5 | 6 | abstract contract RefundableAmountOf_Integration_Fuzz_Test is Integration_Test { 7 | /// @dev Given enough fuzz runs, all of the following scenarios will be fuzzed: 8 | /// 9 | /// - Status streaming 10 | /// - Status settled 11 | function testFuzz_RefundableAmountOf(uint256 timeJump) external { 12 | timeJump = _bound(timeJump, 0 seconds, defaults.TOTAL_DURATION() * 2); 13 | 14 | // Simulate the passage of time. 15 | vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); 16 | 17 | // Get the streamed amount. 18 | uint128 streamedAmount = lockup.streamedAmountOf(defaultStreamId); 19 | 20 | // Run the test. 21 | uint256 actualRefundableAmount = lockup.refundableAmountOf(defaultStreamId); 22 | uint256 expectedRefundableAmount = defaults.DEPOSIT_AMOUNT() - streamedAmount; 23 | assertEq(actualRefundableAmount, expectedRefundableAmount, "refundableAmount"); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/integration/fuzz/lockup-base/withdrawMaxAndTransfer.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 5 | 6 | import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; 7 | 8 | import { Integration_Test } from "../../Integration.t.sol"; 9 | 10 | contract WithdrawMaxAndTransfer_Integration_Fuzz_Test is Integration_Test { 11 | /// @dev Given enough fuzz runs, all of the following scenarios will be fuzzed: 12 | /// 13 | /// - New recipient same and different from the current one 14 | /// - Withdrawable amount zero and not zero 15 | function testFuzz_WithdrawMaxAndTransfer( 16 | uint256 timeJump, 17 | address newRecipient 18 | ) 19 | external 20 | whenNoDelegateCall 21 | givenNotNull 22 | whenCallerRecipient 23 | givenNotBurnedNFT 24 | givenTransferableStream 25 | { 26 | vm.assume(newRecipient != address(0)); 27 | timeJump = _bound(timeJump, 0, defaults.TOTAL_DURATION() * 2); 28 | 29 | // Simulate the passage of time. 30 | vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); 31 | 32 | // Get the withdraw amount. 33 | uint128 withdrawAmount = lockup.withdrawableAmountOf(defaultStreamId); 34 | 35 | if (withdrawAmount > 0) { 36 | // Expect the tokens to be transferred to the fuzzed recipient. 37 | expectCallToTransfer({ to: users.recipient, value: withdrawAmount }); 38 | 39 | // Expect the relevant event to be emitted. 40 | vm.expectEmit({ emitter: address(lockup) }); 41 | emit ISablierLockupBase.WithdrawFromLockupStream({ 42 | streamId: defaultStreamId, 43 | to: users.recipient, 44 | token: dai, 45 | amount: withdrawAmount 46 | }); 47 | } 48 | 49 | // Expect the relevant event to be emitted. 50 | vm.expectEmit({ emitter: address(lockup) }); 51 | emit IERC721.Transfer({ from: users.recipient, to: newRecipient, tokenId: defaultStreamId }); 52 | 53 | // Make the max withdrawal and transfer the NFT. 54 | lockup.withdrawMaxAndTransfer({ streamId: defaultStreamId, newRecipient: newRecipient }); 55 | 56 | // Assert that the withdrawn amount has been updated. 57 | uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(defaultStreamId); 58 | uint128 expectedWithdrawnAmount = withdrawAmount; 59 | assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); 60 | 61 | // Assert that the fuzzed recipient is the new stream recipient (and NFT owner). 62 | address actualRecipient = lockup.getRecipient(defaultStreamId); 63 | address expectedRecipient = newRecipient; 64 | assertEq(actualRecipient, expectedRecipient, "recipient"); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/integration/fuzz/lockup-dynamic/LockupDynamic.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Lockup } from "src/types/DataTypes.sol"; 5 | 6 | import { Integration_Test } from "../../Integration.t.sol"; 7 | import { Cancel_Integration_Fuzz_Test } from "./../lockup-base/cancel.t.sol"; 8 | import { RefundableAmountOf_Integration_Fuzz_Test } from "./../lockup-base/refundableAmountOf.t.sol"; 9 | 10 | abstract contract Lockup_Dynamic_Integration_Fuzz_Test is Integration_Test { 11 | function setUp() public virtual override { 12 | Integration_Test.setUp(); 13 | 14 | lockupModel = Lockup.Model.LOCKUP_DYNAMIC; 15 | initializeDefaultStreams(); 16 | } 17 | } 18 | 19 | /*////////////////////////////////////////////////////////////////////////// 20 | SHARED TESTS 21 | //////////////////////////////////////////////////////////////////////////*/ 22 | 23 | contract Cancel_Lockup_Dynamic_Integration_Fuzz_Test is 24 | Lockup_Dynamic_Integration_Fuzz_Test, 25 | Cancel_Integration_Fuzz_Test 26 | { 27 | function setUp() public virtual override(Lockup_Dynamic_Integration_Fuzz_Test, Integration_Test) { 28 | Lockup_Dynamic_Integration_Fuzz_Test.setUp(); 29 | } 30 | } 31 | 32 | contract RefundableAmountOf_Lockup_Dynamic_Integration_Fuzz_Test is 33 | Lockup_Dynamic_Integration_Fuzz_Test, 34 | RefundableAmountOf_Integration_Fuzz_Test 35 | { 36 | function setUp() public virtual override(Lockup_Dynamic_Integration_Fuzz_Test, Integration_Test) { 37 | Lockup_Dynamic_Integration_Fuzz_Test.setUp(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/integration/fuzz/lockup-linear/LockupLinear.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Lockup } from "src/types/DataTypes.sol"; 5 | 6 | import { Integration_Test } from "../../Integration.t.sol"; 7 | import { Cancel_Integration_Fuzz_Test } from "./../lockup-base/cancel.t.sol"; 8 | import { RefundableAmountOf_Integration_Fuzz_Test } from "./../lockup-base/refundableAmountOf.t.sol"; 9 | import { Withdraw_Integration_Fuzz_Test } from "./../lockup-base/withdraw.t.sol"; 10 | 11 | abstract contract Lockup_Linear_Integration_Fuzz_Test is Integration_Test { 12 | function setUp() public virtual override { 13 | Integration_Test.setUp(); 14 | 15 | lockupModel = Lockup.Model.LOCKUP_LINEAR; 16 | initializeDefaultStreams(); 17 | } 18 | } 19 | 20 | /*////////////////////////////////////////////////////////////////////////// 21 | SHARED TESTS 22 | //////////////////////////////////////////////////////////////////////////*/ 23 | 24 | contract Cancel_Lockup_Linear_Integration_Fuzz_Test is 25 | Lockup_Linear_Integration_Fuzz_Test, 26 | Cancel_Integration_Fuzz_Test 27 | { 28 | function setUp() public virtual override(Lockup_Linear_Integration_Fuzz_Test, Integration_Test) { 29 | Lockup_Linear_Integration_Fuzz_Test.setUp(); 30 | } 31 | } 32 | 33 | contract RefundableAmountOf_Lockup_Linear_Integration_Fuzz_Test is 34 | Lockup_Linear_Integration_Fuzz_Test, 35 | RefundableAmountOf_Integration_Fuzz_Test 36 | { 37 | function setUp() public virtual override(Lockup_Linear_Integration_Fuzz_Test, Integration_Test) { 38 | Lockup_Linear_Integration_Fuzz_Test.setUp(); 39 | } 40 | } 41 | 42 | contract Withdraw_Lockup_Linear_Integration_Fuzz_Test is 43 | Lockup_Linear_Integration_Fuzz_Test, 44 | Withdraw_Integration_Fuzz_Test 45 | { 46 | function setUp() public virtual override(Lockup_Linear_Integration_Fuzz_Test, Integration_Test) { 47 | Lockup_Linear_Integration_Fuzz_Test.setUp(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/integration/fuzz/lockup-tranched/LockupTranched.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Lockup } from "src/types/DataTypes.sol"; 5 | 6 | import { Integration_Test } from "./../../Integration.t.sol"; 7 | import { Cancel_Integration_Fuzz_Test } from "./../lockup-base/cancel.t.sol"; 8 | import { RefundableAmountOf_Integration_Fuzz_Test } from "./../lockup-base/refundableAmountOf.t.sol"; 9 | 10 | abstract contract Lockup_Tranched_Integration_Fuzz_Test is Integration_Test { 11 | function setUp() public virtual override { 12 | Integration_Test.setUp(); 13 | 14 | lockupModel = Lockup.Model.LOCKUP_TRANCHED; 15 | initializeDefaultStreams(); 16 | } 17 | } 18 | 19 | /*////////////////////////////////////////////////////////////////////////// 20 | SHARED TESTS 21 | //////////////////////////////////////////////////////////////////////////*/ 22 | 23 | contract Cancel_Lockup_Tranched_Integration_Fuzz_Test is 24 | Lockup_Tranched_Integration_Fuzz_Test, 25 | Cancel_Integration_Fuzz_Test 26 | { 27 | function setUp() public virtual override(Lockup_Tranched_Integration_Fuzz_Test, Integration_Test) { 28 | Lockup_Tranched_Integration_Fuzz_Test.setUp(); 29 | } 30 | } 31 | 32 | contract RefundableAmountOf_Lockup_Tranched_Integration_Fuzz_Test is 33 | Lockup_Tranched_Integration_Fuzz_Test, 34 | RefundableAmountOf_Integration_Fuzz_Test 35 | { 36 | function setUp() public virtual override(Lockup_Tranched_Integration_Fuzz_Test, Integration_Test) { 37 | Lockup_Tranched_Integration_Fuzz_Test.setUp(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/integration/fuzz/nft-descriptor/isAllowedCharacter.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Base_Test } from "tests/Base.t.sol"; 5 | 6 | contract IsAllowedCharacter_Integration_Fuzz_Test is Base_Test { 7 | bytes1 internal constant SPACE = 0x20; // ASCII 32 8 | bytes1 internal constant DASH = 0x2D; // ASCII 45 9 | bytes1 internal constant ZERO = 0x30; // ASCII 48 10 | bytes1 internal constant NINE = 0x39; // ASCII 57 11 | bytes1 internal constant A = 0x41; // ASCII 65 12 | bytes1 internal constant Z = 0x5A; // ASCII 90 13 | bytes1 internal constant a = 0x61; // ASCII 97 14 | bytes1 internal constant z = 0x7A; // ASCII 122 15 | 16 | /// @dev Given enough fuzz runs, all the following scenarios will be fuzzed: 17 | /// 18 | /// - String with only alphanumerical characters 19 | /// - String with only non-alphanumerical characters 20 | /// - String with both alphanumerical and non-alphanumerical characters 21 | function testFuzz_IsAllowedCharacter(string memory symbol) external view whenNotEmptyString { 22 | bytes memory b = bytes(symbol); 23 | uint256 length = b.length; 24 | bool expectedResult = true; 25 | for (uint256 i = 0; i < length; ++i) { 26 | bytes1 char = b[i]; 27 | if (!isAlphanumericOrSpaceChar(char)) { 28 | expectedResult = false; 29 | break; 30 | } 31 | } 32 | bool actualResult = nftDescriptorMock.isAllowedCharacter_(symbol); 33 | assertEq(actualResult, expectedResult, "isAllowedCharacter"); 34 | } 35 | 36 | function isAlphanumericOrSpaceChar(bytes1 char) internal pure returns (bool) { 37 | bool isSpace = char == SPACE; 38 | bool isDash = char == DASH; 39 | bool isDigit = char >= ZERO && char <= NINE; 40 | bool isUppercaseLetter = char >= A && char <= Z; 41 | bool isLowercaseLetter = char >= a && char <= z; 42 | return isSpace || isDash || isDigit || isUppercaseLetter || isLowercaseLetter; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/invariant/handlers/BaseHandler.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import { StdCheats } from "forge-std/src/StdCheats.sol"; 6 | import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; 7 | 8 | import { Constants } from "../../utils/Constants.sol"; 9 | import { Fuzzers } from "../../utils/Fuzzers.sol"; 10 | 11 | /// @notice Base contract with common logic needed by {LockupHandler} and {LockupCreateHandler} contracts. 12 | abstract contract BaseHandler is Constants, Fuzzers, StdCheats { 13 | /*////////////////////////////////////////////////////////////////////////// 14 | STATE-VARIABLES 15 | //////////////////////////////////////////////////////////////////////////*/ 16 | 17 | /// @dev Maximum number of streams that can be created during an invariant campaign. 18 | uint256 internal constant MAX_STREAM_COUNT = 300; 19 | 20 | /// @dev Maps function names to the number of times they have been called. 21 | mapping(string func => uint256 calls) public calls; 22 | 23 | /// @dev The total number of calls made to this contract. 24 | uint256 public totalCalls; 25 | 26 | ISablierLockup public lockup; 27 | 28 | /*////////////////////////////////////////////////////////////////////////// 29 | TEST CONTRACTS 30 | //////////////////////////////////////////////////////////////////////////*/ 31 | 32 | /// @dev Default ERC-20 token used for testing. 33 | IERC20 public token; 34 | 35 | /*////////////////////////////////////////////////////////////////////////// 36 | CONSTRUCTOR 37 | //////////////////////////////////////////////////////////////////////////*/ 38 | 39 | constructor(IERC20 token_, ISablierLockup lockup_) { 40 | token = token_; 41 | lockup = lockup_; 42 | } 43 | 44 | /*////////////////////////////////////////////////////////////////////////// 45 | MODIFIERS 46 | //////////////////////////////////////////////////////////////////////////*/ 47 | 48 | /// @dev Simulates the passage of time. The time jump is upper bounded so that streams don't settle too quickly. 49 | /// @param timeJumpSeed A fuzzed value needed for generating random time warps. 50 | modifier adjustTimestamp(uint256 timeJumpSeed) { 51 | uint256 timeJump = _bound(timeJumpSeed, 2 minutes, 40 days); 52 | vm.warp(getBlockTimestamp() + timeJump); 53 | _; 54 | } 55 | 56 | /// @dev Checks user assumptions. 57 | modifier checkUsers(address sender, address recipient, address broker) { 58 | // Prevent the sender, recipient and broker to be the zero address. 59 | vm.assume(sender != address(0) && recipient != address(0) && broker != address(0)); 60 | 61 | // Prevent the contract itself from playing the role of any user. 62 | vm.assume(sender != address(this) && recipient != address(this) && broker != address(this)); 63 | _; 64 | } 65 | 66 | /// @dev Records a function call for instrumentation purposes. 67 | modifier instrument(string memory functionName) { 68 | calls[functionName]++; 69 | totalCalls++; 70 | _; 71 | } 72 | 73 | /// @dev Makes the provided sender the caller. 74 | modifier useNewSender(address sender) { 75 | resetPrank(sender); 76 | _; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/invariant/stores/LockupStore.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Lockup } from "src/types/DataTypes.sol"; 5 | 6 | /// @dev Storage variables needed by all lockup handlers. 7 | contract LockupStore { 8 | /*////////////////////////////////////////////////////////////////////////// 9 | VARIABLES 10 | //////////////////////////////////////////////////////////////////////////*/ 11 | 12 | mapping(uint256 streamId => bool recorded) public isPreviousStatusRecorded; 13 | uint256 public lastStreamId; 14 | mapping(uint256 streamId => Lockup.Status status) public previousStatusOf; 15 | mapping(uint256 streamId => address recipient) public recipients; 16 | mapping(uint256 streamId => address sender) public senders; 17 | uint256[] public streamIds; 18 | 19 | /*////////////////////////////////////////////////////////////////////////// 20 | HELPERS 21 | //////////////////////////////////////////////////////////////////////////*/ 22 | 23 | function pushStreamId(uint256 streamId, address sender, address recipient) external { 24 | // Store the stream IDs, the senders, and the recipients. 25 | streamIds.push(streamId); 26 | senders[streamId] = sender; 27 | recipients[streamId] = recipient; 28 | 29 | // Update the last stream ID. 30 | lastStreamId = streamId; 31 | } 32 | 33 | function updateIsPreviousStatusRecorded(uint256 streamId) external { 34 | isPreviousStatusRecorded[streamId] = true; 35 | } 36 | 37 | function updatePreviousStatusOf(uint256 streamId, Lockup.Status status) external { 38 | previousStatusOf[streamId] = status; 39 | } 40 | 41 | function updateRecipient(uint256 streamId, address newRecipient) external { 42 | recipients[streamId] = newRecipient; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/mocks/AdminableMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22; 3 | 4 | import { Adminable } from "src/abstracts/Adminable.sol"; 5 | 6 | contract AdminableMock is Adminable { 7 | constructor(address initialAdmin) Adminable(initialAdmin) { } 8 | } 9 | -------------------------------------------------------------------------------- /tests/mocks/BatchMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22; 3 | 4 | import { Batch } from "src/abstracts/Batch.sol"; 5 | 6 | contract BatchMock is Batch { 7 | error InvalidNumber(uint256); 8 | 9 | uint256 internal _number = 42; 10 | 11 | // A view only function. 12 | function getNumber() public view returns (uint256) { 13 | return _number; 14 | } 15 | 16 | // A view only function that reverts. 17 | function getNumberAndRevert() public pure returns (uint256) { 18 | revert InvalidNumber(1); 19 | } 20 | 21 | // A state changing function with no payable modifier and no return value. 22 | function setNumber(uint256 number) public { 23 | _number = number; 24 | } 25 | 26 | // A state changing function with a payable modifier and no return value. 27 | function setNumberWithPayable(uint256 number) public payable { 28 | _number = number; 29 | } 30 | 31 | // A state changing function with a payable modifier and a return value. 32 | function setNumberWithPayableAndReturn(uint256 number) public payable returns (uint256) { 33 | _number = number; 34 | return _number; 35 | } 36 | 37 | // A state changing function with a payable modifier, which reverts with a custom error. 38 | function setNumberWithPayableAndRevertError(uint256 number) public payable { 39 | _number = number; 40 | revert InvalidNumber(number); 41 | } 42 | 43 | // A state changing function with a payable modifier, which reverts with a reason string. 44 | function setNumberWithPayableAndRevertString(uint256 number) public payable { 45 | _number = number; 46 | revert("You cannot pass"); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/mocks/Noop.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22; 3 | 4 | /// @dev This contract does nothing (no-op = no operation). 5 | contract Noop { } 6 | -------------------------------------------------------------------------------- /tests/mocks/Receive.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | contract ContractWithoutReceive { } 5 | 6 | contract ContractWithReceive { 7 | receive() external payable { } 8 | } 9 | -------------------------------------------------------------------------------- /tests/mocks/erc20/ERC20Bytes32.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | contract ERC20Bytes32 { 5 | function symbol() external pure returns (bytes32) { 6 | return bytes32("ERC20"); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/mocks/erc20/ERC20MissingReturn.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | /// @notice An implementation of ERC-20 that does not return a boolean in {transfer} and {transferFrom}. 5 | /// @dev See https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca/. 6 | contract ERC20MissingReturn { 7 | uint8 public decimals; 8 | string public name; 9 | string public symbol; 10 | uint256 public totalSupply; 11 | 12 | mapping(address owner => mapping(address spender => uint256 allowance)) internal _allowances; 13 | mapping(address account => uint256 balance) internal _balances; 14 | 15 | event Transfer(address indexed from, address indexed to, uint256 amount); 16 | 17 | event Approval(address indexed owner, address indexed spender, uint256 amount); 18 | 19 | constructor(string memory name_, string memory symbol_, uint8 decimals_) { 20 | name = name_; 21 | symbol = symbol_; 22 | decimals = decimals_; 23 | } 24 | 25 | function allowance(address owner, address spender) public view returns (uint256) { 26 | return _allowances[owner][spender]; 27 | } 28 | 29 | function balanceOf(address account) public view returns (uint256) { 30 | return _balances[account]; 31 | } 32 | 33 | function approve(address spender, uint256 value) public returns (bool) { 34 | _approve(msg.sender, spender, value); 35 | return true; 36 | } 37 | 38 | function burn(address holder, uint256 amount) public { 39 | _balances[holder] -= amount; 40 | totalSupply -= amount; 41 | emit Transfer(holder, address(0), amount); 42 | } 43 | 44 | function mint(address beneficiary, uint256 amount) public { 45 | _balances[beneficiary] += amount; 46 | totalSupply += amount; 47 | emit Transfer(address(0), beneficiary, amount); 48 | } 49 | 50 | function _approve(address owner, address spender, uint256 value) internal virtual { 51 | _allowances[owner][spender] = value; 52 | emit Approval(owner, spender, value); 53 | } 54 | 55 | /// @dev This function does not return a value, although the ERC-20 standard mandates that it should. 56 | function transfer(address to, uint256 amount) public { 57 | _transfer(msg.sender, to, amount); 58 | } 59 | 60 | /// @dev This function does not return a value, although the ERC-20 standard mandates that it should. 61 | function transferFrom(address from, address to, uint256 amount) public { 62 | _transfer(from, to, amount); 63 | _approve(from, msg.sender, _allowances[from][msg.sender] - amount); 64 | } 65 | 66 | function _transfer(address from, address to, uint256 amount) internal virtual { 67 | _balances[from] = _balances[from] - amount; 68 | _balances[to] = _balances[to] + amount; 69 | emit Transfer(from, to, amount); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/mocks/erc20/ERC20Mock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract ERC20Mock is ERC20 { 7 | constructor(string memory name, string memory symbol) ERC20(name, symbol) { } 8 | } 9 | -------------------------------------------------------------------------------- /tests/unit/concrete/adminable/transfer-admin/transferAdmin.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { IAdminable } from "src/interfaces/IAdminable.sol"; 5 | import { Errors } from "src/libraries/Errors.sol"; 6 | 7 | import { Adminable_Unit_Shared_Test } from "../../../shared/Adminable.t.sol"; 8 | 9 | contract TransferAdmin_Unit_Concrete_Test is Adminable_Unit_Shared_Test { 10 | function test_RevertWhen_CallerNotAdmin() external { 11 | // Make Eve the caller in this test. 12 | resetPrank(users.eve); 13 | 14 | // Run the test. 15 | vm.expectRevert(abi.encodeWithSelector(Errors.CallerNotAdmin.selector, users.admin, users.eve)); 16 | adminableMock.transferAdmin(users.eve); 17 | } 18 | 19 | function test_WhenNewAdminSameAsCurrentAdmin() external whenCallerAdmin { 20 | // It should emit a {TransferAdmin} event. 21 | vm.expectEmit({ emitter: address(adminableMock) }); 22 | emit IAdminable.TransferAdmin({ oldAdmin: users.admin, newAdmin: users.admin }); 23 | 24 | // Transfer the admin. 25 | adminableMock.transferAdmin(users.admin); 26 | 27 | // It should keep the same admin. 28 | address actualAdmin = adminableMock.admin(); 29 | address expectedAdmin = users.admin; 30 | assertEq(actualAdmin, expectedAdmin, "admin"); 31 | } 32 | 33 | function test_WhenNewAdminZeroAddress() external whenCallerAdmin whenNewAdminNotSameAsCurrentAdmin { 34 | // It should emit a {TransferAdmin}. 35 | vm.expectEmit({ emitter: address(adminableMock) }); 36 | emit IAdminable.TransferAdmin({ oldAdmin: users.admin, newAdmin: address(0) }); 37 | 38 | // Transfer the admin. 39 | adminableMock.transferAdmin(address(0)); 40 | 41 | // It should set the admin to the zero address. 42 | address actualAdmin = adminableMock.admin(); 43 | address expectedAdmin = address(0); 44 | assertEq(actualAdmin, expectedAdmin, "admin"); 45 | } 46 | 47 | function test_WhenNewAdminNotZeroAddress() external whenCallerAdmin whenNewAdminNotSameAsCurrentAdmin { 48 | // It should emit a {TransferAdmin} event. 49 | vm.expectEmit({ emitter: address(adminableMock) }); 50 | emit IAdminable.TransferAdmin({ oldAdmin: users.admin, newAdmin: users.alice }); 51 | 52 | // Transfer the admin. 53 | adminableMock.transferAdmin(users.alice); 54 | 55 | // It should set the new admin. 56 | address actualAdmin = adminableMock.admin(); 57 | address expectedAdmin = users.alice; 58 | assertEq(actualAdmin, expectedAdmin, "admin"); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/unit/concrete/adminable/transfer-admin/transferAdmin.tree: -------------------------------------------------------------------------------- 1 | TransferAdmin_Unit_Concrete_Test 2 | ├── when caller not admin 3 | │ └── it should revert 4 | └── when caller admin 5 | ├── when new admin same as current admin 6 | │ ├── it should keep the same admin 7 | │ └── it should emit a {TransferAdmin} event 8 | └── when new admin not same as current admin 9 | ├── when new admin zero address 10 | │ ├── it should set the admin to the zero address 11 | │ └── it should emit a {TransferAdmin} 12 | └── when new admin not zero address 13 | ├── it should set the new admin 14 | └── it should emit a {TransferAdmin} event 15 | -------------------------------------------------------------------------------- /tests/unit/concrete/batch/batch.tree: -------------------------------------------------------------------------------- 1 | Batch_Unit_Concrete_Test 2 | ├── when function does not exist 3 | │ └── it should revert 4 | └── when function exists 5 | ├── when non state changing function 6 | │ ├── when function reverts 7 | │ │ └── it should revert 8 | │ └── when function not revert 9 | │ └── it should return expected value 10 | └── when state changing function 11 | ├── when not payable 12 | │ ├── when batch includes ETH value 13 | │ │ └── it should revert 14 | │ └── when batch not include ETH value 15 | │ └── it should return empty value 16 | └── when payable 17 | ├── when function reverts with custom error 18 | │ └── it should revert 19 | ├── when function reverts with string error 20 | │ └── it should revert 21 | ├── when function returns a value 22 | │ └── it should return expected value 23 | └── when function does not return a value 24 | └── it should return empty value 25 | -------------------------------------------------------------------------------- /tests/unit/concrete/nft-descriptor/calculateDurationInDays.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { SVGElements } from "src/libraries/SVGElements.sol"; 5 | 6 | import { Base_Test } from "tests/Base.t.sol"; 7 | 8 | contract CalculateDurationInDays_Unit_Concrete_Test is Base_Test { 9 | function test_CalculateDurationInDays_Zero() external view { 10 | uint256 startTime = getBlockTimestamp(); 11 | uint256 endTime = startTime + 1 days - 1 seconds; 12 | string memory actualDurationInDays = nftDescriptorMock.calculateDurationInDays_(startTime, endTime); 13 | string memory expectedDurationInDays = string.concat(SVGElements.SIGN_LT, " 1 Day"); 14 | assertEq(actualDurationInDays, expectedDurationInDays, "durationInDays"); 15 | } 16 | 17 | function test_CalculateDurationInDays_One() external view { 18 | uint256 startTime = getBlockTimestamp(); 19 | uint256 endTime = startTime + 1 days; 20 | string memory actualDurationInDays = nftDescriptorMock.calculateDurationInDays_(startTime, endTime); 21 | string memory expectedDurationInDays = "1 Day"; 22 | assertEq(actualDurationInDays, expectedDurationInDays, "durationInDays"); 23 | } 24 | 25 | function test_CalculateDurationInDays_FortyTwo() external view { 26 | uint256 startTime = getBlockTimestamp(); 27 | uint256 endTime = startTime + 42 days; 28 | string memory actualDurationInDays = nftDescriptorMock.calculateDurationInDays_(startTime, endTime); 29 | string memory expectedDurationInDays = "42 Days"; 30 | assertEq(actualDurationInDays, expectedDurationInDays, "durationInDays"); 31 | } 32 | 33 | function test_CalculateDurationInDays_Leet() external view { 34 | uint256 startTime = getBlockTimestamp(); 35 | uint256 endTime = startTime + 1337 days; 36 | string memory actualDurationInDays = nftDescriptorMock.calculateDurationInDays_(startTime, endTime); 37 | string memory expectedDurationInDays = "1337 Days"; 38 | assertEq(actualDurationInDays, expectedDurationInDays, "durationInDays"); 39 | } 40 | 41 | function test_CalculateDurationInDays_TenThousand() external view { 42 | uint256 startTime = getBlockTimestamp(); 43 | uint256 endTime = startTime + 10_000 days; 44 | string memory actualDurationInDays = nftDescriptorMock.calculateDurationInDays_(startTime, endTime); 45 | string memory expectedDurationInDays = string.concat(SVGElements.SIGN_GT, " 9999 Days"); 46 | assertEq(actualDurationInDays, expectedDurationInDays, "durationInDays"); 47 | } 48 | 49 | function test_CalculateDurationInDays_Overflow() external view { 50 | uint256 startTime = getBlockTimestamp(); 51 | uint256 endTime = startTime - 1 seconds; 52 | string memory actualDurationInDays = nftDescriptorMock.calculateDurationInDays_(startTime, endTime); 53 | string memory expectedDurationInDays = string.concat(SVGElements.SIGN_GT, " 9999 Days"); 54 | assertEq(actualDurationInDays, expectedDurationInDays, "durationInDays"); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/unit/concrete/nft-descriptor/calculateStreamedPercentage.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Base_Test } from "tests/Base.t.sol"; 5 | 6 | contract CalculateStreamedPercentage_Unit_Concrete_Test is Base_Test { 7 | function test_CalculateStreamedPercentage_Zero() external view { 8 | uint256 actualStreamedPercentage = 9 | nftDescriptorMock.calculateStreamedPercentage_({ streamedAmount: 0, depositedAmount: 1337e18 }); 10 | uint256 expectedStreamedPercentage = 0; 11 | assertEq(actualStreamedPercentage, expectedStreamedPercentage, "streamedPercentage"); 12 | } 13 | 14 | function test_CalculateStreamedPercentage_Streaming() external view { 15 | uint256 actualStreamedPercentage = 16 | nftDescriptorMock.calculateStreamedPercentage_({ streamedAmount: 100e18, depositedAmount: 400e18 }); 17 | uint256 expectedStreamedPercentage = 2500; 18 | assertEq(actualStreamedPercentage, expectedStreamedPercentage, "streamedPercentage"); 19 | } 20 | 21 | function test_CalculateStreamedPercentage_Settled() external view { 22 | uint256 actualStreamedPercentage = 23 | nftDescriptorMock.calculateStreamedPercentage_({ streamedAmount: 1337e18, depositedAmount: 1337e18 }); 24 | uint256 expectedStreamedPercentage = 10_000; 25 | assertEq(actualStreamedPercentage, expectedStreamedPercentage, "streamedPercentage"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/unit/concrete/nft-descriptor/generateAttributes.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | // solhint-disable max-line-length,quotes 3 | pragma solidity >=0.8.22 <0.9.0; 4 | 5 | import { Base_Test } from "tests/Base.t.sol"; 6 | 7 | contract GenerateAttributes_Unit_Concrete_Test is Base_Test { 8 | function test_GenerateAttributes_Empty() external view { 9 | string memory actualAttributes = nftDescriptorMock.generateAttributes_("", "", ""); 10 | string memory expectedAttributes = 11 | '[{"trait_type":"Token","value":""},{"trait_type":"Sender","value":""},{"trait_type":"Status","value":""}]'; 12 | assertEq(actualAttributes, expectedAttributes, "metadata attributes"); 13 | } 14 | 15 | function test_GenerateAttributes() external view { 16 | string memory actualAttributes = 17 | nftDescriptorMock.generateAttributes_("DAI", "0x50725493D337CdC4e381f658e10d29d128BD6927", "Streaming"); 18 | string memory expectedAttributes = 19 | '[{"trait_type":"Token","value":"DAI"},{"trait_type":"Sender","value":"0x50725493D337CdC4e381f658e10d29d128BD6927"},{"trait_type":"Status","value":"Streaming"}]'; 20 | assertEq(actualAttributes, expectedAttributes, "metadata attributes"); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/unit/concrete/nft-descriptor/hourglass.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { LibString } from "solady/src/utils/LibString.sol"; 5 | 6 | import { SVGElements } from "src/libraries/SVGElements.sol"; 7 | 8 | import { Base_Test } from "tests/Base.t.sol"; 9 | 10 | contract Hourglass_Unit_Concrete_Test is Base_Test { 11 | using LibString for string; 12 | 13 | function test_Hourglass_Pending() external view { 14 | string memory hourglass = nftDescriptorMock.hourglass_("pending"); 15 | uint256 index = hourglass.indexOf(SVGElements.HOURGLASS_UPPER_BULB); 16 | assertNotEq(index, LibString.NOT_FOUND, "hourglass upper bulb should be present"); 17 | } 18 | 19 | function test_Hourglass_Streaming() external view { 20 | string memory hourglass = nftDescriptorMock.hourglass_("Streaming"); 21 | uint256 index = hourglass.indexOf(SVGElements.HOURGLASS_UPPER_BULB); 22 | assertNotEq(index, LibString.NOT_FOUND, "hourglass upper bulb should be present"); 23 | } 24 | 25 | function test_Hourglass_Settled() external view { 26 | string memory hourglass = nftDescriptorMock.hourglass_("Settled"); 27 | uint256 index = hourglass.indexOf(SVGElements.HOURGLASS_UPPER_BULB); 28 | assertEq(index, LibString.NOT_FOUND, "hourglass upper bulb should NOT be present"); 29 | } 30 | 31 | function test_Hourglass_Canceled() external view { 32 | string memory hourglass = nftDescriptorMock.hourglass_("Canceled"); 33 | uint256 index = hourglass.indexOf(SVGElements.HOURGLASS_UPPER_BULB); 34 | assertNotEq(index, LibString.NOT_FOUND, "hourglass upper bulb should be present"); 35 | } 36 | 37 | function test_Hourglass_Depleted() external view { 38 | string memory hourglass = nftDescriptorMock.hourglass_("Depleted"); 39 | uint256 index = hourglass.indexOf(SVGElements.HOURGLASS_UPPER_BULB); 40 | assertEq(index, LibString.NOT_FOUND, "hourglass upper bulb should NOT be present"); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/unit/concrete/nft-descriptor/stringifyCardType.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { SVGElements } from "src/libraries/SVGElements.sol"; 5 | 6 | import { Base_Test } from "tests/Base.t.sol"; 7 | 8 | contract StringifyCardType_Unit_Concrete_Test is Base_Test { 9 | function test_StringifyCardType() external view { 10 | assertEq(nftDescriptorMock.stringifyCardType_(SVGElements.CardType.PROGRESS), "Progress"); 11 | assertEq(nftDescriptorMock.stringifyCardType_(SVGElements.CardType.STATUS), "Status"); 12 | assertEq(nftDescriptorMock.stringifyCardType_(SVGElements.CardType.AMOUNT), "Amount"); 13 | assertEq(nftDescriptorMock.stringifyCardType_(SVGElements.CardType.DURATION), "Duration"); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/unit/concrete/nft-descriptor/stringifyFractionalAmount.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Base_Test } from "tests/Base.t.sol"; 5 | 6 | contract StringifyFractionalAmount_Unit_Concrete_Test is Base_Test { 7 | function sfa(uint256 amount) internal view returns (string memory) { 8 | return nftDescriptorMock.stringifyFractionalAmount_(amount); 9 | } 10 | 11 | function test_FractionalAmount_Zero() external view { 12 | assertEq(sfa(0), "", "fractional part mismatch"); 13 | } 14 | 15 | function test_FractionalAmount_LeadingZero() external view { 16 | assertEq(sfa(1), ".01", "fractional part mismatch"); 17 | assertEq(sfa(5), ".05", "fractional part mismatch"); 18 | assertEq(sfa(9), ".09", "fractional part mismatch"); 19 | } 20 | 21 | function test_FractionalAmount_NoLeadingZero() external view { 22 | assertEq(sfa(10), ".10", "fractional part mismatch"); 23 | assertEq(sfa(12), ".12", "fractional part mismatch"); 24 | assertEq(sfa(33), ".33", "fractional part mismatch"); 25 | assertEq(sfa(42), ".42", "fractional part mismatch"); 26 | assertEq(sfa(70), ".70", "fractional part mismatch"); 27 | assertEq(sfa(99), ".99", "fractional part mismatch"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/unit/concrete/nft-descriptor/stringifyPercentage.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Base_Test } from "tests/Base.t.sol"; 5 | 6 | contract StringifyPercentage_Unit_Concrete_Test is Base_Test { 7 | function sp(uint256 percentage) internal view returns (string memory) { 8 | return nftDescriptorMock.stringifyPercentage_(percentage); 9 | } 10 | 11 | function test_StringifyPercentage_NoFractionalPart() external view { 12 | assertEq(sp(0), "0%", "percentage mismatch"); 13 | assertEq(sp(100), "1%", "percentage mismatch"); 14 | assertEq(sp(300), "3%", "percentage mismatch"); 15 | assertEq(sp(1000), "10%", "percentage mismatch"); 16 | assertEq(sp(4200), "42%", "percentage mismatch"); 17 | assertEq(sp(10_000), "100%", "percentage mismatch"); 18 | } 19 | 20 | function test_StringifyPercentage_FractionalPart() external view { 21 | assertEq(sp(1), "0.01%", "percentage mismatch"); 22 | assertEq(sp(42), "0.42%", "percentage mismatch"); 23 | assertEq(sp(314), "3.14%", "percentage mismatch"); 24 | assertEq(sp(2064), "20.64%", "percentage mismatch"); 25 | assertEq(sp(6588), "65.88%", "percentage mismatch"); 26 | assertEq(sp(9999), "99.99%", "percentage mismatch"); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/unit/concrete/nft-descriptor/stringifyStatus.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Lockup } from "src/types/DataTypes.sol"; 5 | 6 | import { Base_Test } from "tests/Base.t.sol"; 7 | 8 | contract StringifyStatus_Unit_Concrete_Test is Base_Test { 9 | function test_StringifyStatus() external view { 10 | assertEq(nftDescriptorMock.stringifyStatus_(Lockup.Status.DEPLETED), "Depleted", "depleted status mismatch"); 11 | assertEq(nftDescriptorMock.stringifyStatus_(Lockup.Status.CANCELED), "Canceled", "canceled status mismatch"); 12 | assertEq(nftDescriptorMock.stringifyStatus_(Lockup.Status.STREAMING), "Streaming", "streaming status mismatch"); 13 | assertEq(nftDescriptorMock.stringifyStatus_(Lockup.Status.SETTLED), "Settled", "settled status mismatch"); 14 | assertEq(nftDescriptorMock.stringifyStatus_(Lockup.Status.PENDING), "Pending", "pending status mismatch"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/unit/fuzz/transferAdmin.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { IAdminable } from "src/interfaces/IAdminable.sol"; 5 | import { Errors } from "src/libraries/Errors.sol"; 6 | 7 | import { Adminable_Unit_Shared_Test } from "../shared/Adminable.t.sol"; 8 | 9 | contract TransferAdmin_Unit_Fuzz_Test is Adminable_Unit_Shared_Test { 10 | function testFuzz_RevertWhen_CallerNotAdmin(address eve) external { 11 | vm.assume(eve != address(0) && eve != users.admin); 12 | assumeNotPrecompile(eve); 13 | 14 | // Make Eve the caller in this test. 15 | resetPrank(eve); 16 | 17 | // Run the test. 18 | vm.expectRevert(abi.encodeWithSelector(Errors.CallerNotAdmin.selector, users.admin, eve)); 19 | adminableMock.transferAdmin(eve); 20 | } 21 | 22 | function testFuzz_TransferAdmin(address newAdmin) external whenCallerAdmin { 23 | vm.assume(newAdmin != address(0)); 24 | 25 | // Expect the relevant event to be emitted. 26 | vm.expectEmit({ emitter: address(adminableMock) }); 27 | emit IAdminable.TransferAdmin({ oldAdmin: users.admin, newAdmin: newAdmin }); 28 | 29 | // Transfer the admin. 30 | adminableMock.transferAdmin(newAdmin); 31 | 32 | // Assert that the admin has been transferred. 33 | address actualAdmin = adminableMock.admin(); 34 | address expectedAdmin = newAdmin; 35 | assertEq(actualAdmin, expectedAdmin, "admin"); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/unit/shared/Adminable.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Base_Test } from "../../Base.t.sol"; 5 | import { AdminableMock } from "../../mocks/AdminableMock.sol"; 6 | 7 | abstract contract Adminable_Unit_Shared_Test is Base_Test { 8 | AdminableMock internal adminableMock; 9 | 10 | function setUp() public virtual override { 11 | Base_Test.setUp(); 12 | deployConditionally(); 13 | resetPrank({ msgSender: users.admin }); 14 | } 15 | 16 | /// @dev Conditionally deploys {AdminableMock} normally or from a source precompiled with `--via-ir`. 17 | function deployConditionally() internal { 18 | if (!isTestOptimizedProfile()) { 19 | adminableMock = new AdminableMock(users.admin); 20 | } else { 21 | adminableMock = 22 | AdminableMock(deployCode("out-optimized/AdminableMock.sol/AdminableMock.json", abi.encode(users.admin))); 23 | } 24 | vm.label({ account: address(adminableMock), newLabel: "AdminableMock" }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/utils/.npmignore: -------------------------------------------------------------------------------- 1 | *.t.sol 2 | -------------------------------------------------------------------------------- /tests/utils/ArrayBuilder.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | library ArrayBuilder { 5 | /// @notice Generates an ordered array of integers which starts at `firstStreamId` and ends at `firstStreamId + 6 | /// batchSize - 1`. 7 | function fillStreamIds( 8 | uint256 firstStreamId, 9 | uint256 batchSize 10 | ) 11 | internal 12 | pure 13 | returns (uint256[] memory streamIds) 14 | { 15 | streamIds = new uint256[](batchSize); 16 | for (uint256 i = 0; i < batchSize; ++i) { 17 | streamIds[i] = firstStreamId + i; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/utils/BaseScript.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22 <0.9.0; 3 | 4 | import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; 5 | import { StdAssertions } from "forge-std/src/StdAssertions.sol"; 6 | 7 | import { BaseScript } from "script/Base.s.sol"; 8 | 9 | contract BaseScript_Test is StdAssertions { 10 | using Strings for uint256; 11 | 12 | BaseScript internal baseScript; 13 | 14 | function setUp() public { 15 | baseScript = new BaseScript(); 16 | } 17 | 18 | function test_ConstructCreate2Salt() public view { 19 | string memory chainId = block.chainid.toString(); 20 | string memory version = "2.0.1"; 21 | string memory salt = string.concat("ChainID ", chainId, ", Version ", version); 22 | 23 | bytes32 actualSalt = baseScript.constructCreate2Salt(); 24 | bytes32 expectedSalt = bytes32(abi.encodePacked(salt)); 25 | assertEq(actualSalt, expectedSalt, "CREATE2 salt mismatch"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/utils/Constants.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import { UD60x18 } from "@prb/math/src/UD60x18.sol"; 5 | 6 | abstract contract Constants { 7 | uint256 internal constant FEE = 0.001e18; 8 | uint40 internal constant JULY_1_2024 = 1_719_792_000; 9 | UD60x18 internal constant MAX_BROKER_FEE = UD60x18.wrap(0.1e18); // 10% 10 | uint128 internal constant MAX_UINT128 = type(uint128).max; 11 | uint256 internal constant MAX_UINT256 = type(uint256).max; 12 | uint40 internal constant MAX_UINT40 = type(uint40).max; 13 | uint40 internal constant MAX_UNIX_TIMESTAMP = 2_147_483_647; // 2^31 - 1 14 | } 15 | -------------------------------------------------------------------------------- /tests/utils/Types.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | struct Users { 5 | // Default admin. 6 | address payable admin; 7 | // Impartial user. 8 | address payable alice; 9 | // Default stream broker. 10 | address payable broker; 11 | // Malicious user. 12 | address payable eve; 13 | // Default NFT operator. 14 | address payable operator; 15 | // Default stream recipient. 16 | address payable recipient; 17 | // Default stream sender. 18 | address payable sender; 19 | } 20 | --------------------------------------------------------------------------------