diff --git a/.env.example b/.env.example index 15da80920..ccc756547 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ -export EOA="YOUR_EOA_ADDRESS" -export FOUNDRY_PROFILE="lite" -export MNEMONIC="YOUR_MNEMONIC" -export MAINNET_RPC_URL="YOUR_MAINNET_RPC_URL" - +ETH_FROM="YOUR_DEPLOYER_ADDRESS" +FOUNDRY_PROFILE="lite" +MNEMONIC="YOUR_MNEMONIC" +MAINNET_RPC_URL="YOUR_MAINNET_RPC_URL" +ROUTEMESH_API_KEY="YOUR_API_KEY" # Get yours from https://routeme.sh/ diff --git a/.github/workflows/ci-deep.yml b/.github/workflows/ci-deep.yml index d63c52392..465c2a069 100644 --- a/.github/workflows/ci-deep.yml +++ b/.github/workflows/ci-deep.yml @@ -1,44 +1,41 @@ name: "CI Deep" -env: - MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} - on: schedule: - cron: "0 3 * * 0" # at 3:00am UTC every Sunday workflow_dispatch: inputs: unitFuzzRuns: - default: '50000' + default: "50000" description: "Unit: number of fuzz runs." required: false integrationFuzzRuns: - default: '50000' + default: "50000" description: "Integration: number of fuzz runs." required: false invariantRuns: - default: '100' + default: "100" description: "Invariant runs: number of sequences of function calls generated and run." required: false invariantDepth: - default: '100' + default: "100" description: "Invariant depth: number of function calls made in a given run." required: false forkFuzzRuns: - default: '1000' + default: "1000" description: "Fork: number of fuzz runs." required: false jobs: - lint: - uses: "sablier-labs/reusable-workflows/.github/workflows/forge-lint.yml@main" + check: + uses: "sablier-labs/gha-utils/.github/workflows/full-check.yml@main" build: - uses: "sablier-labs/reusable-workflows/.github/workflows/forge-build.yml@main" + uses: "sablier-labs/gha-utils/.github/workflows/forge-build.yml@main" test-unit: - needs: ["lint", "build"] - uses: "sablier-labs/reusable-workflows/.github/workflows/forge-test.yml@main" + needs: ["check", "build"] + uses: "sablier-labs/gha-utils/.github/workflows/forge-test.yml@main" with: foundry-fuzz-runs: ${{ fromJSON(inputs.unitFuzzRuns || '50000') }} foundry-profile: "test-optimized" @@ -46,8 +43,8 @@ jobs: name: "Unit tests" test-integration: - needs: ["lint", "build"] - uses: "sablier-labs/reusable-workflows/.github/workflows/forge-test.yml@main" + needs: ["check", "build"] + uses: "sablier-labs/gha-utils/.github/workflows/forge-test.yml@main" with: foundry-fuzz-runs: ${{ fromJSON(inputs.integrationFuzzRuns || '50000') }} foundry-profile: "test-optimized" @@ -55,8 +52,8 @@ jobs: name: "Integration tests" test-invariant: - needs: ["lint", "build"] - uses: "sablier-labs/reusable-workflows/.github/workflows/forge-test.yml@main" + needs: ["check", "build"] + uses: "sablier-labs/gha-utils/.github/workflows/forge-test.yml@main" with: foundry-invariant-depth: ${{ fromJSON(inputs.invariantDepth || '100') }} foundry-invariant-runs: ${{ fromJSON(inputs.invariantRuns || '100') }} @@ -65,19 +62,21 @@ jobs: name: "Invariant tests" test-fork: - needs: ["lint", "build"] + needs: ["check", "build"] secrets: MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} - uses: "sablier-labs/reusable-workflows/.github/workflows/forge-test.yml@main" + ROUTEMESH_API_KEY: ${{ secrets.ROUTEMESH_API_KEY }} + uses: "sablier-labs/gha-utils/.github/workflows/forge-test.yml@main" with: foundry-fuzz-runs: ${{ fromJSON(inputs.forkFuzzRuns || '1000') }} foundry-profile: "test-optimized" match-path: "tests/fork/**/*.sol" name: "Fork tests" + retry-attempts: 2 notify-on-failure: if: failure() - needs: ["lint", "build", "test-unit", "test-integration", "test-invariant", "test-fork"] + needs: ["check", "build", "test-unit", "test-integration", "test-invariant", "test-fork"] runs-on: "ubuntu-latest" steps: - name: "Send Slack notification" diff --git a/.github/workflows/ci-fork.yml b/.github/workflows/ci-fork.yml index 02945652f..b41563d73 100644 --- a/.github/workflows/ci-fork.yml +++ b/.github/workflows/ci-fork.yml @@ -1,31 +1,34 @@ name: "CI Fork and Util tests" on: + workflow_dispatch: schedule: - cron: "0 3 * * 1,3,5" # at 3:00 AM UTC on Monday, Wednesday and Friday jobs: - lint: - uses: "sablier-labs/reusable-workflows/.github/workflows/forge-lint.yml@main" + check: + uses: "sablier-labs/gha-utils/.github/workflows/full-check.yml@main" build: - uses: "sablier-labs/reusable-workflows/.github/workflows/forge-build.yml@main" + uses: "sablier-labs/gha-utils/.github/workflows/forge-build.yml@main" test-fork: - needs: ["lint", "build"] - secrets: - MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} - uses: "sablier-labs/reusable-workflows/.github/workflows/forge-test.yml@main" - with: - foundry-fuzz-runs: 100 - foundry-profile: "test-optimized" - fuzz-seed: true - match-path: "tests/fork/**/*.sol" - name: "Fork tests" + needs: ["check", "build"] + secrets: + MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} + ROUTEMESH_API_KEY: ${{ secrets.ROUTEMESH_API_KEY }} + uses: "sablier-labs/gha-utils/.github/workflows/forge-test.yml@main" + with: + foundry-fuzz-runs: 100 + foundry-profile: "test-optimized" + fuzz-seed: true + match-path: "tests/fork/**/*.sol" + name: "Fork tests" + retry-attempts: 2 test-utils: - needs: ["lint", "build"] - uses: "sablier-labs/reusable-workflows/.github/workflows/forge-test.yml@main" + needs: ["check", "build"] + uses: "sablier-labs/gha-utils/.github/workflows/forge-test.yml@main" with: foundry-profile: "test-optimized" match-path: "tests/utils/**/*.sol" @@ -33,7 +36,7 @@ jobs: notify-on-failure: if: failure() - needs: ["lint", "build", "test-fork", "test-utils"] + needs: ["check", "build", "test-fork", "test-utils"] runs-on: "ubuntu-latest" steps: - name: "Send Slack notification" diff --git a/.github/workflows/multibuild.yml b/.github/workflows/ci-multibuild.yml similarity index 78% rename from .github/workflows/multibuild.yml rename to .github/workflows/ci-multibuild.yml index bd42ec6f0..a4d90470e 100644 --- a/.github/workflows/multibuild.yml +++ b/.github/workflows/ci-multibuild.yml @@ -10,10 +10,12 @@ jobs: runs-on: "ubuntu-latest" steps: - name: "Check out the repo" - uses: "actions/checkout@v4" + uses: "actions/checkout@v5" - name: "Install Bun" - uses: "oven-sh/setup-bun@v1" + uses: "oven-sh/setup-bun@v2" + with: + bun-version: "latest" - name: "Install the Node.js dependencies" run: "bun install --frozen-lockfile" @@ -22,5 +24,5 @@ jobs: uses: "PaulRBerg/foundry-multibuild@v1" with: min: "0.8.22" - max: "0.8.26" + max: "0.8.29" skip-test: "true" diff --git a/.github/workflows/ci-slither.yml b/.github/workflows/ci-slither.yml index aef1c9066..e4cb33ea5 100644 --- a/.github/workflows/ci-slither.yml +++ b/.github/workflows/ci-slither.yml @@ -5,8 +5,9 @@ on: - cron: "0 3 * * 0" # at 3:00am UTC every Sunday jobs: - lint: - uses: "sablier-labs/reusable-workflows/.github/workflows/forge-lint.yml@main" + check: + uses: "sablier-labs/gha-utils/.github/workflows/full-check.yml@main" slither-analyze: - uses: "sablier-labs/reusable-workflows/.github/workflows/slither-analyze.yml@main" + needs: "check" + uses: "sablier-labs/gha-utils/.github/workflows/slither-analyze.yml@main" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index edb08be70..feda2f01f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,22 +14,24 @@ on: - "staging-blast" jobs: - lint: - uses: "sablier-labs/reusable-workflows/.github/workflows/forge-lint.yml@main" + check: + uses: "sablier-labs/gha-utils/.github/workflows/full-check.yml@main" - bulloak: - needs: ["lint"] - uses: "sablier-labs/reusable-workflows/.github/workflows/bulloak-check.yml@main" + build: + uses: "sablier-labs/gha-utils/.github/workflows/forge-build.yml@main" + + test-bulloak: + needs: ["check", "build"] + if: needs.build.outputs.cache-status != 'primary' + uses: "sablier-labs/gha-utils/.github/workflows/bulloak-check.yml@main" with: skip-modifiers: true - tree-path: "tests" - - build: - uses: "sablier-labs/reusable-workflows/.github/workflows/forge-build.yml@main" + test-dir: "tests" test-unit: - needs: ["lint", "build"] - uses: "sablier-labs/reusable-workflows/.github/workflows/forge-test.yml@main" + needs: ["check", "build"] + if: needs.build.outputs.cache-status != 'primary' + uses: "sablier-labs/gha-utils/.github/workflows/forge-test.yml@main" with: foundry-fuzz-runs: 2000 foundry-profile: "test-optimized" @@ -37,8 +39,9 @@ jobs: name: "Unit tests" test-integration: - needs: ["lint", "build"] - uses: "sablier-labs/reusable-workflows/.github/workflows/forge-test.yml@main" + needs: ["check", "build"] + if: needs.build.outputs.cache-status != 'primary' + uses: "sablier-labs/gha-utils/.github/workflows/forge-test.yml@main" with: foundry-fuzz-runs: 2000 foundry-profile: "test-optimized" @@ -46,18 +49,21 @@ jobs: name: "Integration tests" test-invariant: - needs: ["lint", "build"] - uses: "sablier-labs/reusable-workflows/.github/workflows/forge-test.yml@main" + needs: ["check", "build"] + if: needs.build.outputs.cache-status != 'primary' + uses: "sablier-labs/gha-utils/.github/workflows/forge-test.yml@main" with: foundry-profile: "test-optimized" match-path: "tests/invariant/**/*.sol" name: "Invariant tests" test-fork: - needs: ["lint", "build"] + needs: ["check", "build"] + if: needs.build.outputs.cache-status != 'primary' secrets: MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} - uses: "sablier-labs/reusable-workflows/.github/workflows/forge-test.yml@main" + ROUTEMESH_API_KEY: ${{ secrets.ROUTEMESH_API_KEY }} + uses: "sablier-labs/gha-utils/.github/workflows/forge-test.yml@main" with: foundry-fuzz-runs: 20 foundry-profile: "test-optimized" @@ -65,10 +71,11 @@ jobs: name: "Fork tests" coverage: - needs: ["lint", "build"] + needs: ["check", "build"] + if: needs.build.outputs.cache-status != 'primary' secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} - uses: "sablier-labs/reusable-workflows/.github/workflows/forge-coverage.yml@main" + uses: "sablier-labs/gha-utils/.github/workflows/forge-coverage.yml@main" with: - match-path: "tests/{fork,integration,unit}/**/*.sol" + match-path: "tests/{integration,unit}/**/*.sol" diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 000000000..7eba8c07f --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,59 @@ +name: Claude Code + +on: + issues: + types: [opened, assigned] + issue_comment: + types: [created] + pull_request_review: + types: [submitted] + pull_request_review_comment: + types: [created] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + actions: read # Required for Claude to read CI results on PRs + contents: read + id-token: write + issues: read + pull-requests: read + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@beta + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + + # This is an optional setting that allows Claude to read CI results on PRs + additional_permissions: | + actions: read + + # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4.1) + model: "claude-opus-4-1-20250805" + + # Optional: Trigger when specific user is assigned to an issue + # assignee_trigger: "claude-bot" + + # Optional: Allow Claude to run specific commands + # allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)" + # Optional: Add custom instructions for Claude to customize its behavior for your project + # custom_instructions: | + # Follow our coding standards + # Ensure all new code has tests + # Use TypeScript for new files + + # Optional: Custom environment variables for Claude + # claude_env: | + # NODE_ENV: test diff --git a/.github/workflows/cron-stale.yml b/.github/workflows/cron-stale.yml new file mode 100644 index 000000000..3cb6118eb --- /dev/null +++ b/.github/workflows/cron-stale.yml @@ -0,0 +1,10 @@ +name: "Cron: Close Stale Issues and PRs" + +on: + workflow_dispatch: + schedule: + - cron: "0 3 * * 0" # at 3:00am UTC every Sunday + +jobs: + cron-stale: + uses: "sablier-labs/gha-utils/.github/workflows/cron-stale.yml@main" diff --git a/.github/workflows/generate-svg.yml b/.github/workflows/generate-svg.yml deleted file mode 100644 index 3e774d567..000000000 --- a/.github/workflows/generate-svg.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: "Generate SVG" - -on: - workflow_dispatch: - inputs: - progress: - description: "The streamed amount as a numerical percentage with 4 implied decimals." - required: true - status: - description: "The status of the stream, as a string." - required: true - streamed: - description: "The abbreviated streamed amount, as a string." - required: true - duration: - description: "The total duration of the stream in days, as a number." - required: true - -jobs: - generate-svg: - runs-on: "ubuntu-latest" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v4" - - - name: "Install Foundry" - uses: "foundry-rs/foundry-toolchain@v1" - - - name: "Generate an NFT SVG using the user-provided parameters" - run: >- - forge script script/GenerateSVG.s.sol - --sig "run(uint256,string,string,uint256)" - "${{ fromJSON(inputs.progress || true) }}", - "${{ fromJSON(inputs.status || true) }}" - "${{ fromJSON(inputs.streamed || true) }}" - "${{ fromJSON(inputs.duration || true) }}" - - - name: "Add workflow summary" - run: | - echo "## Result" >> $GITHUB_STEP_SUMMARY - echo "✅ Done" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/run-smtchecker.yml b/.github/workflows/run-smtchecker.yml deleted file mode 100644 index 9df7b5718..000000000 --- a/.github/workflows/run-smtchecker.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: "Run SMTChecker" - -on: "workflow_dispatch" - -jobs: - run-smtchecker: - runs-on: "macos-latest" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v4" - - - name: "Install Foundry" - uses: "foundry-rs/foundry-toolchain@v1" - - - name: "Run SMTChecker and pipe the output to a file" - run: "FOUNDRY_PROFILE=smt forge build > smtchecker-report.txt" - - - name: "Store the report as an artifact" - uses: "actions/upload-artifact@v4" - with: - name: smtchecker-report - path: "smtchecker-report.txt" diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml deleted file mode 100644 index a717e9753..000000000 --- a/.github/workflows/stale.yml +++ /dev/null @@ -1,10 +0,0 @@ -name: "Close stale issues and PRs" - -on: - workflow_dispatch: - schedule: - - cron: "0 3 * * 0" # at 3:00am UTC every Sunday - -jobs: - stale: - uses: "sablier-labs/reusable-workflows/.github/workflows/stale.yml@main" diff --git a/.gitignore b/.gitignore index 902d153f9..1a14c3de3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,19 @@ # directories artifacts -artifacts-zk +artifacts-* broadcast cache cache_hardhat-zk coverage deployments -deployments-zk +deployments-* docs node_modules out -out-optimized -out-svg +out-* +repomix typechain-types +zkout # files *.env @@ -20,6 +21,7 @@ typechain-types *.log .DS_Store .pnp.* +bun.lockb deployments.md lcov.info package-lock.json diff --git a/.lintstagedrc.js b/.lintstagedrc.js new file mode 100644 index 000000000..d535c02ce --- /dev/null +++ b/.lintstagedrc.js @@ -0,0 +1,7 @@ +/** + * @type {import("lint-staged").Configuration} + */ +module.exports = { + "*.{json,md,svg,yml}": "bun prettier --cache --write", + "*.sol": ["bun solhint --cache --fix --noPrompt", "forge fmt"], +}; diff --git a/.lintstagedrc.yml b/.lintstagedrc.yml deleted file mode 100644 index e056edefb..000000000 --- a/.lintstagedrc.yml +++ /dev/null @@ -1,4 +0,0 @@ -"*.{json,md,svg,yml}": "prettier --write" -"*.sol": - - "bun solhint {benchmark,script,src,tests}/**/*.sol --fix --noPrompt" - - "forge fmt" diff --git a/.prettierignore b/.prettierignore index 0fba7f6e2..4f47f8901 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,22 +1,16 @@ # directories -.github/workflows broadcast cache +cache_hardhat-zk coverage docs node_modules out -out-optimized -out-svg +out-* +repomix # files -*.env -*.log *.sol -.DS_Store .pnp.* -bun.lockb -lcov.info package-lock.json pnpm-lock.yaml -yarn.lock diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 000000000..17bd3d56d --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,20 @@ +const baseConfig = require("@sablier/devkit/prettier"); + +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ +const config = { + ...baseConfig, + overrides: [ + ...(baseConfig.overrides || []), + { + files: "*.svg", + options: { + parser: "html", + }, + }, + ], +}; + +module.exports = config; diff --git a/.prettierrc.yml b/.prettierrc.yml deleted file mode 100644 index d2c1f6cab..000000000 --- a/.prettierrc.yml +++ /dev/null @@ -1,11 +0,0 @@ -bracketSpacing: true -printWidth: 120 -proseWrap: "always" -singleQuote: false -tabWidth: 2 -trailingComma: "all" -useTabs: false -overrides: - - files: "*.svg" - options: - parser: "html" diff --git a/.solhint.json b/.solhint.json index b7274a4e4..99fe31e29 100644 --- a/.solhint.json +++ b/.solhint.json @@ -2,19 +2,20 @@ "extends": "solhint:recommended", "rules": { "avoid-low-level-calls": "off", - "code-complexity": ["error", 10], + "code-complexity": ["error", 9], "compiler-version": ["error", ">=0.8.22"], - "contract-name-camelcase": "off", - "const-name-snakecase": "off", - "func-name-mixedcase": "off", "func-visibility": ["error", { "ignoreConstructors": true }], - "gas-custom-errors": "off", + "function-max-lines": "off", + "gas-increment-by-one": "off", + "gas-indexed-events": "off", + "gas-small-strings": "off", + "gas-strict-inequalities": "off", + "gas-struct-packing": "off", + "max-line-length": ["error", 128], "max-states-count": ["warn", 20], - "max-line-length": ["error", 124], - "named-parameters-mapping": "warn", "no-empty-blocks": "off", "not-rely-on-time": "off", - "one-contract-per-file": "off", - "var-name-mixedcase": "off" + "use-natspec": "off", + "var-name-mixedcase": "warn" } } diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..ae14729a8 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "esbenp.prettier-vscode", + "NomicFoundation.hardhat-solidity", + "PraneshASP.vscode-solidity-inspector", + "tamasfe.even-better-toml" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 436b490ae..462335f8e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,17 +1,24 @@ { + "editor.formatOnSave": true, + "evenBetterToml.formatter.alignComments": true, + "evenBetterToml.formatter.arrayAutoCollapse": false, + "evenBetterToml.formatter.columnWidth": 120, + "evenBetterToml.formatter.reorderArrays": true, + "evenBetterToml.formatter.reorderKeys": true, + "evenBetterToml.formatter.reorderInlineTables": true, + "prettier.documentSelectors": ["**/*.svg"], + "solidity.formatter": "forge", + "search.exclude": { + "**/node_modules": true, + "**/repomix": true + }, + "[javascript][json][jsonc][markdown][svg][yaml]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, "[solidity]": { "editor.defaultFormatter": "NomicFoundation.hardhat-solidity" }, "[toml]": { "editor.defaultFormatter": "tamasfe.even-better-toml" - }, - "files.associations": { - ".gas-snapshot": "julia" - }, - "editor.formatOnSave": true, - "prettier.documentSelectors": ["**/*.svg"], - "search.exclude": { - "**/node_modules": true - }, - "solidity.formatter": "forge" + } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 45e92d1e7..5e56914c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. The format is based on [Common Changelog](https://common-changelog.org/). +[3.0.1]: https://github.com/sablier-labs/lockup/compare/v3.0.0...v3.0.1 +[3.0.0]: https://github.com/sablier-labs/lockup/compare/v2.0.1...v3.0.0 [2.0.1]: https://github.com/sablier-labs/lockup/compare/v2.0.0...v2.0.1 [2.0.0]: https://github.com/sablier-labs/lockup/compare/v1.2.0...v2.0.0 [1.2.0]: https://github.com/sablier-labs/lockup/compare/v1.1.2...v1.2.0 @@ -14,6 +16,43 @@ The format is based on [Common Changelog](https://common-changelog.org/). [1.0.1]: https://github.com/sablier-labs/lockup/compare/v1.0.0...v1.0.1 [1.0.0]: https://github.com/sablier-labs/lockup/releases/tag/v1.0.0 +## [3.0.1] - 2025-10-14 + +### Changed + +- Bump package version for NPM release ([#1297](https://github.com/sablier-labs/lockup/pull/1297)) + +## [3.0.0] - 2025-10-07 + +### Changed + +- **Breaking:** Refactor `SablierLockup` contract into model-specific abstract contracts + ([#1261](https://github.com/sablier-labs/lockup/pull/1261)) +- **Breaking:** Refactor `DataTypes` into separate type files + ([#1261](https://github.com/sablier-labs/lockup/pull/1261)) +- Replace admin with comptroller ([#1260](https://github.com/sablier-labs/lockup/pull/1260), + [#1268](https://github.com/sablier-labs/lockup/pull/1268)) +- Rename `VestingMath` library to `LockupMath` +- Rename `SablierLockupBase` to `SablierLockupState` ([#1247](https://github.com/sablier-labs/lockup/pull/1247)) +- Make `cancelMultiple` non-reverting ([#1173](https://github.com/sablier-labs/lockup/pull/1173)) +- Rename `aggregateBalance` to `aggregateAmount` ([#1228](https://github.com/sablier-labs/lockup/pull/1228)) +- Bump Solidity compiler to 0.8.29 ([#1207](https://github.com/sablier-labs/lockup/pull/1207)) +- Bump `@openzeppelin/contracts` from 5.0.2 to 5.3.0 + +### Added + +- ERC20 recovery functionality ([#1182](https://github.com/sablier-labs/lockup/pull/1182)) +- Function to calculate minimum fee in wei ([#1270](https://github.com/sablier-labs/lockup/pull/1270)) +- `CreateBatchLockup` event in `BatchLockup` ([#1274](https://github.com/sablier-labs/lockup/pull/1274)) +- Return refunded amount in `cancel` ([#1173](https://github.com/sablier-labs/lockup/pull/1173)) +- Add `@sablier/evm-utils` dependency + +### Removed + +- **Breaking:** Remove `MAX_COUNT` constant ([#1243](https://github.com/sablier-labs/lockup/pull/1243)) +- **Breaking**: Remove broker functionality ([#1166](https://github.com/sablier-labs/lockup/pull/1166)) +- Remove `Adminable` and `Batch` contracts (moved to `@sablier/evm-utils`) + ## [2.0.1] - 2025-02-05 ### Changed @@ -86,6 +125,7 @@ The format is based on [Common Changelog](https://common-changelog.org/). [#852](https://github.com/sablier-labs/lockup/pull/852)) - Rename create functions `createWithTimestamps` and `createWithDurations` across all lockup contracts ([#798](https://github.com/sablier-labs/lockup/pull/798)) +- Rename `milestone` to `timestamp` in the `LockupDynamic.Segment` data type - Switch to Bun ([#775](https://github.com/sablier-labs/lockup/pull/775)) - Use Solidity v0.8.26 ([#944](https://github.com/sablier-labs/lockup/pull/944)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 273e8ab32..afe929acc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,57 +1,70 @@ -# Contributing + -Feel free to dive in! [Open](https://github.com/sablier-labs/lockup/issues/new) an issue, -[start](https://github.com/sablier-labs/lockup/discussions/new) a discussion or submit a PR. For any informal concerns -or feedback, please join our [Discord server](https://discord.gg/bSwRCwWRsT). +# Contributing -Contributions to Sablier Lockup are welcome by anyone interested in writing more tests, improving readability, -optimizing for gas efficiency, or extending the protocol via new features. +Feel free to dive in! [Open](../../issues/new) an issue, [start](../../discussions/new) a discussion or submit a PR. For +any informal concerns or feedback, please join our [Discord server](https://discord.gg/bSwRCwWRsT). -## Pre Requisites +Contributions are welcome by anyone interested in writing more tests, improving readability, optimizing for gas +efficiency, or extending the protocol via new features. -You will need the following software on your machine: +## Prerequisites -- [Git](https://git-scm.com/downloads) -- [Foundry](https://github.com/foundry-rs/foundry) -- [Node.Js](https://nodejs.org/en/download/) -- [Bun](https://bun.sh/) +- [Node.js](https://nodejs.org) (v20+) +- [Just](https://github.com/casey/just) (command runner) +- [Bun](https://bun.sh) (package manager) +- [Ni](https://github.com/antfu-collective/ni) (package manager resolver) +- [Foundry](https://github.com/foundry-rs/foundry) (EVM development framework) +- [Rust](https://rust-lang.org/tools/install) (Rust compiler) +- [Bulloak](https://bulloak.dev) (CLI for checking tests) -In addition, familiarity with [Solidity](https://soliditylang.org/) is requisite. +In addition, familiarity with [Solidity](https://soliditylang.org) is requisite. ## Set Up -Clone this repository including submodules: +Clone this repository: + +```shell +git clone git@github.com:sablier-labs/lockup.git sablier-lockup && cd sablier-lockup +``` + +To install Node.js dependencies: + +```shell +bun install +``` + +Then, execute the one-time setup script: ```shell -$ git clone --recurse-submodules -j8 git@github.com:sablier-labs/lockup.git +just setup ``` -Then, inside the project's directory, run this to install the Node.js dependencies and build the contracts: +To build the contracts: ```shell -$ bun install -$ bun run build +just build ``` Switch to the `staging` branch, where all development work should be done: ```shell -$ git switch staging +git switch staging ``` Now you can start making changes. -To see a list of all available scripts: +To see a list of all available scripts, run this command: ```shell -$ bun run +just --list ``` ## Pull Requests When making a pull request, ensure that: -- The base branch is `staging`. +- The base development branch is `staging`. - All tests pass. - Concrete tests are generated using Bulloak and the Branching Tree Technique (BTT). - You can learn more about this on the [Bulloak website](https://bulloak.dev). @@ -80,11 +93,13 @@ To make CI work in your pull request, ensure that the necessary environment vari repository's secrets. Please add the following variable in your GitHub Secrets: - MAINNET_RPC_URL +- ROUTEMESH_API_KEY -## Integration with VSCode: +## Integration with VSCode -Install the following VSCode extensions: +The following VSCode extensions are not required but are recommended for a better development experience: -- [esbenp.prettier-vscode](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) +- [even-better-toml](https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml) - [hardhat-solidity](https://marketplace.visualstudio.com/items?itemName=NomicFoundation.hardhat-solidity) -- [vscode-tree-language](https://marketplace.visualstudio.com/items?itemName=CTC.vscode-tree-extension) +- [prettier-vscode](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) +- [vscode-solidity-inspector](https://marketplace.visualstudio.com/items?itemName=PraneshASP.vscode-solidity-inspector) diff --git a/LICENSE-BUSL.md b/LICENSE-BUSL.md new file mode 100644 index 000000000..49697289a --- /dev/null +++ b/LICENSE-BUSL.md @@ -0,0 +1,82 @@ +Business Source License 1.1 + +License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved. "Business Source License" is a trademark of +MariaDB Corporation Ab. + +--- + +Parameters + +Licensor: Sablier Labs Ltd + +Licensed Work: Sablier Lockup The Licensed Work is (C) 2025 Sablier Labs Ltd + +Additional Use Grant: Any uses listed and defined at +[`license-grants.sablier.eth`](https://app.ens.domains/license-grants.sablier.eth) + +Change Date: The earlier of 2029-07-01 or a date specified at +[`license-dates.sablier.eth`](https://app.ens.domains/license-dates.sablier.eth) + +Change License: GNU General Public License v3.0 or later + +--- + +Terms + +The Licensor hereby grants you the right to copy, modify, create derivative works, redistribute, and make non-production +use of the Licensed Work. The Licensor may make an Additional Use Grant, above, permitting limited production use. + +Effective on the Change Date, or the fourth anniversary of the first publicly available distribution of a specific +version of the Licensed Work under this License, whichever comes first, the Licensor hereby grants you rights under the +terms of the Change License, and the rights granted in the paragraph above terminate. + +If your use of the Licensed Work does not comply with the requirements currently in effect as described in this License, +you must purchase a commercial license from the Licensor, its affiliated entities, or authorized resellers, or you must +refrain from using the Licensed Work. + +All copies of the original and modified Licensed Work, and derivative works of the Licensed Work, are subject to this +License. This License applies separately for each version of the Licensed Work and the Change Date may vary for each +version of the Licensed Work released by Licensor. + +You must conspicuously display this License on each original or modified copy of the Licensed Work. If you receive the +Licensed Work in original or modified form from a third party, the terms and conditions set forth in this License apply +to your use of that work. + +Any use of the Licensed Work in violation of this License will automatically terminate your rights under this License +for the current and all other versions of the Licensed Work. + +This License does not grant you any right in any trademark or logo of Licensor or its affiliates (provided that you may +use a trademark or logo of Licensor as expressly required by this License). + +TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS +ALL WARRANTIES AND CONDITIONS, EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND TITLE. + +MariaDB hereby grants you permission to use this License’s text to license your works, and to refer to it using the +trademark "Business Source License", as long as you comply with the Covenants of Licensor below. + +--- + +Covenants of Licensor + +In consideration of the right to use this License’s text and the "Business Source License" name and trademark, Licensor +covenants to MariaDB, and to all other recipients of the licensed work to be provided by Licensor: + +1. To specify as the Change License the GPL Version 3.0 or any later version, or a license that is compatible with GPL + Version 3.0 or a later version, where "compatible" means that software provided under the Change License can be + included in a program with software provided under GPL Version 3.0 or a later version. Licensor may specify + additional Change Licenses without limitation. + +2. To either: (a) specify an additional grant of rights to use that does not impose any additional restriction on the + right granted in this License, as the Additional Use Grant; or (b) insert the text "None". + +3. To specify a Change Date. + +4. Not to modify this License in any other way. + +--- + +Notice + +The Business Source License (this document, or the "License") is not an Open Source license. However, the Licensed Work +will eventually be made available under an Open Source License, as stated in this License. diff --git a/LICENSE-GPL.md b/LICENSE-GPL.md index c83358d45..f79abad7d 100644 --- a/LICENSE-GPL.md +++ b/LICENSE-GPL.md @@ -438,11 +438,11 @@ to where the full notice is found. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License - along with this program. If not, see . + along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. diff --git a/LICENSE.md b/LICENSE.md index 59aedb41a..e5b12c9a3 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,80 +1,9 @@ -Business Source License 1.1 +This package is licensed under multiple licenses: -License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved. "Business Source License" is a trademark of -MariaDB Corporation Ab. +- The primary contracts are licensed under the Business Source License 1.1 (BUSL-1.1). See + [LICENSE-BUSL.md](./LICENSE-BUSL.md). +- Some files are licensed under the GNU General Public License v3.0 or later (GPL-3.0-or-later). See + [LICENSE-GPL.md](./LICENSE-GPL.md). +- Test files in `tests/` are unlicensed. ---- - -Parameters - -Licensor: Sablier Labs Ltd - -Licensed Work: Sablier Lockup The Licensed Work is (C) 2024 Sablier Labs Ltd - -Additional Use Grant: Any uses listed and defined at license-grants.sablier.eth - -Change Date: The earlier of 2028-07-03 or a date specified at license-date.sablier.eth - -Change License: GNU General Public License v3.0 or later - ---- - -Terms - -The Licensor hereby grants you the right to copy, modify, create derivative works, redistribute, and make non-production -use of the Licensed Work. The Licensor may make an Additional Use Grant, above, permitting limited production use. - -Effective on the Change Date, or the fourth anniversary of the first publicly available distribution of a specific -version of the Licensed Work under this License, whichever comes first, the Licensor hereby grants you rights under the -terms of the Change License, and the rights granted in the paragraph above terminate. - -If your use of the Licensed Work does not comply with the requirements currently in effect as described in this License, -you must purchase a commercial license from the Licensor, its affiliated entities, or authorized resellers, or you must -refrain from using the Licensed Work. - -All copies of the original and modified Licensed Work, and derivative works of the Licensed Work, are subject to this -License. This License applies separately for each version of the Licensed Work and the Change Date may vary for each -version of the Licensed Work released by Licensor. - -You must conspicuously display this License on each original or modified copy of the Licensed Work. If you receive the -Licensed Work in original or modified form from a third party, the terms and conditions set forth in this License apply -to your use of that work. - -Any use of the Licensed Work in violation of this License will automatically terminate your rights under this License -for the current and all other versions of the Licensed Work. - -This License does not grant you any right in any trademark or logo of Licensor or its affiliates (provided that you may -use a trademark or logo of Licensor as expressly required by this License). - -TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS -ALL WARRANTIES AND CONDITIONS, EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND TITLE. - -MariaDB hereby grants you permission to use this License’s text to license your works, and to refer to it using the -trademark "Business Source License", as long as you comply with the Covenants of Licensor below. - ---- - -Covenants of Licensor - -In consideration of the right to use this License’s text and the "Business Source License" name and trademark, Licensor -covenants to MariaDB, and to all other recipients of the licensed work to be provided by Licensor: - -1. To specify as the Change License the GPL Version 3.0 or any later version, or a license that is compatible with GPL - Version 3.0 or a later version, where "compatible" means that software provided under the Change License can be - included in a program with software provided under GPL Version 3.0 or a later version. Licensor may specify - additional Change Licenses without limitation. - -2. To either: (a) specify an additional grant of rights to use that does not impose any additional restriction on the - right granted in this License, as the Additional Use Grant; or (b) insert the text "None". - -3. To specify a Change Date. - -4. Not to modify this License in any other way. - ---- - -Notice - -The Business Source License (this document, or the "License") is not an Open Source license. However, the Licensed Work -will eventually be made available under an Open Source License, as stated in this License. +See the top of each source file for its specific license. diff --git a/README.md b/README.md index ea3b875a9..bffb501a5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Sablier Lockup [![Github Actions][gha-badge]][gha] [![Coverage][codecov-badge]][codecov] [![Foundry][foundry-badge]][foundry] [![Discord][discord-badge]][discord] +# Sablier Lockup [![Github Actions][gha-badge]][gha] [![Coverage][codecov-badge]][codecov] [![Foundry][foundry-badge]][foundry] [![Discord][discord-badge]][discord] [![Twitter][twitter-badge]][twitter] [gha]: https://github.com/sablier-labs/lockup/actions [gha-badge]: https://github.com/sablier-labs/lockup/actions/workflows/ci.yml/badge.svg @@ -8,6 +8,8 @@ [discord-badge]: https://img.shields.io/discord/659709894315868191 [foundry]: https://getfoundry.sh [foundry-badge]: https://img.shields.io/badge/Built%20with-Foundry-FFDB1C.svg +[twitter-badge]: https://img.shields.io/twitter/follow/Sablier +[twitter]: https://x.com/Sablier In-depth documentation is available at [docs.sablier.com](https://docs.sablier.com). @@ -33,37 +35,26 @@ Install Lockup using your favorite package manager, e.g., with Bun: bun add @sablier/lockup ``` -Then, if you are using Foundry, you need to add these to your `remappings.txt` file: - -```text -@sablier/lockup/=node_modules/@sablier/lockup/ -@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ -@prb/math/=node_modules/@prb/math/ -``` - ### Git Submodules This installation method is not recommended, but it is available for those who prefer it. -First, install the submodule using Forge: +Install the submodule using Forge: ```shell -forge install --no-commit sablier-labs/lockup +forge install sablier-labs/lockup ``` -Second, install the project's dependencies: +Then, install the project's dependencies: ```shell -forge install --no-commit OpenZeppelin/openzeppelin-contracts@v5.0.2 PaulRBerg/prb-math@v4.1.0 +forge install sablier-labs/evm-utils@v1.0.0 OpenZeppelin/openzeppelin-contracts@v5.3.0 PaulRBerg/prb-math@v4.1.0 ``` -Finally, add these to your `remappings.txt` file: +### Branching Tree Technique -```text -@sablier/lockup/=lib/lockup/ -@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ -@prb/math/=lib/prb-math/ -``` +You may notice that some test files are accompanied by `.tree` files. This is because we are using Branching Tree +Technique and [Bulloak](https://bulloak.dev/). ## Usage @@ -91,15 +82,11 @@ contract, which is more gas-efficient and easier to maintain. For more information, see the [Technical Overview](https://docs.sablier.com/reference/overview) in our docs, as well as these [diagrams](https://docs.sablier.com/reference/lockup/diagrams). -### Branching Tree Technique - -You may notice that some test files are accompanied by `.tree` files. This is called the Branching Tree Technique, and -it is explained in depth [here](https://www.bulloak.dev/). - ## Deployments -The list of all deployment addresses can be found [here](https://docs.sablier.com). For guidance on the deploy scripts, -see the [Deployments wiki](https://github.com/sablier-labs/lockup/wiki/Deployments). +The list of all deployment addresses can be found [here](https://docs.sablier.com/guides/lockup/deployments). For +guidance on the deployment scripts, see the [Deployments Guide](https://docs.sablier.com/guides/custom-deployments) in +our docs. ## Security @@ -119,11 +106,4 @@ For guidance on how to create PRs, see the [CONTRIBUTING](./CONTRIBUTING.md) gui ## License -The primary license for Sablier Lockup is the Business Source License 1.1 (`BUSL-1.1`), see -[`LICENSE.md`](./LICENSE.md). However, there are exceptions: - -- All files in `src/interfaces/` and `src/types` are licensed under `GPL-3.0-or-later`, see - [`LICENSE-GPL.md`](./LICENSE-GPL.md). -- Several files in `src`, `script`, and `tests` are licensed under `GPL-3.0-or-later`, see - [`LICENSE-GPL.md`](./LICENSE-GPL.md). -- Many files in `tests/` remain unlicensed (as indicated in their SPDX headers). +See [LICENSE.md](./LICENSE.md). diff --git a/SECURITY.md b/SECURITY.md index acf76f0d8..8ed267058 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,50 +2,16 @@ Ensuring the security of the Sablier Protocol is our utmost priority. We have dedicated significant efforts towards the design and testing of the protocol to guarantee its safety and reliability. However, we are aware that security is a -continuous process. If you believe you have found a security vulnerability, please read the following guidelines and -report it to us. +continuous process. If you believe you have found a security vulnerability, please read the +[Bug Bounty Program](https://sablier.notion.site/bug-bounty), and share a report privately with us. -## Bug Bounty - -### Overview - -Starting on July 1, 2023, the [sablier-labs/lockup](https://github.com/sablier-labs/lockup) repository is subject to the -Sablier Bug Bounty (the "Program") to incentivize responsible bug disclosure. - -We are limiting the scope of the Program to critical and high severity bugs, and are offering a reward of up to -$100,000. Happy hunting! - -### Scope - -The scope of the Program is limited to bugs that result in the draining of funds locked up in contracts. - -The Program does NOT cover the following: - -- Code located in the [tests](./tests) or [script](./script) directories. -- External code in `node_modules`, except for code that is explicitly used by a deployed contract located in the - [src](./src) directory. -- Contract deployments on test networks, such as Sepolia. -- Bugs in third-party contracts or platforms interacting with Sablier Lockup. -- Previously reported or discovered vulnerabilities in contracts built by third parties on Sablier Lockup. -- Bugs that have already been reported. - -Vulnerabilities contingent upon the occurrence of any of the following also are outside the scope of this Program: - -- Front-end bugs (clickjacking etc.) -- DDoS attacks -- Spamming -- Phishing -- Social engineering attacks -- Private key leaks -- Automated tools (Github Actions, etc.) -- Compromise or misuse of third party systems or services - -### Assumptions +## Protocol Assumptions Sablier Lockup has been developed with a number of technical assumptions in mind. For a disclosure to qualify as a vulnerability, it must adhere to these assumptions as well: -- The immutable `MAX_COUNT` has value that cannot lead to an overflow of the block gas limit. +- The number of segments/tranches should be such that creating a stream should not lead to an overflow of the block gas + limit. - The total supply of any ERC-20 token remains below 2128 - 1, i.e., `type(uint128).max`. - The `transfer` and `transferFrom` methods of any ERC-20 token strictly reduce the sender's balance by the transfer amount and increase the recipient's balance by the same amount. In other words, tokens that charge fees on transfers @@ -53,68 +19,10 @@ vulnerability, it must adhere to these assumptions as well: - An address' ERC-20 balance can only change as a result of a `transfer` call by the sender or a `transferFrom` call by an approved address. This excludes rebase tokens, interest-bearing tokens, and permissioned tokens where the admin can arbitrarily change balances. +- The token contract is not an ERC-20 representation of the native token of the chain. For example, the + [$POL token](https://polygonscan.com/address/0x0000000000000000000000000000000000001010) on Polygon is not supported. +- The token contract has only one entry point. - The token contract does not allow callbacks (e.g. ERC-777 is not supported). - There is no need for exponents greater than ~18.44 in `LockupDynamic` segments. - Recipient contracts on the hook allowlist have gone through due diligence and are assumed to expose no risk to the Sablier protocol. - -### Rewards - -Rewards will be allocated based on the severity of the bug disclosed and will be evaluated and rewarded at the -discretion of the Sablier Labs team. For critical bugs that lead to any loss of user funds, rewards of up to $100,000 -will be granted. Lower severity bugs will be rewarded at the discretion of the team. - -### Disclosure - -Any vulnerability or bug discovered must be reported only to the following email: -[security@sablier.com](mailto:security@sablier.com). - -The vulnerability must not be disclosed publicly or to any other person, entity or email address before Sablier Labs has -been notified, has fixed the issue, and has granted permission for public disclosure. In addition, disclosure must be -made within 24 hours following discovery of the vulnerability. - -A detailed report of a vulnerability increases the likelihood of a reward and may increase the reward amount. Please -provide as much information about the vulnerability as possible, including: - -- The conditions on which reproducing the bug is contingent. -- The steps needed to reproduce the bug or, preferably, a proof of concept. -- The potential implications of the vulnerability being abused. - -Anyone who reports a unique, previously-unreported vulnerability that results in a change to the code or a configuration -change and who keeps such vulnerability confidential until it has been resolved by our engineers will be recognized -publicly for their contribution if they so choose. - -### Eligibility - -To qualify for a reward under this Program, you must adhere to the following criteria: - -- Identify a previously unreported, non-public vulnerability that could result in the loss or freeze of any ERC-20 token - in Sablier Lockup (but not on any third-party platform interacting with Sablier Lockup) and that is within the scope - of this Program. -- The vulnerability must be distinct from the issues covered in the [Audits](https://github.com/sablier-labs/audits). -- Be the first to report the unique vulnerability to [security@sablier.com](mailto:security@sablier.com) in accordance - with the disclosure requirements specified above. If multiple similar vulnerabilities are reported within a 24-hour - timeframe, rewards will be split at the discretion of Sablier Labs. -- Provide sufficient information to enable our engineers to reproduce and fix the vulnerability. -- Not engage in any unlawful conduct when disclosing the bug, including through threats, demands, or any other coercive - tactics. -- Avoid exploiting the vulnerability in any manner, such as making it public or profiting from it (aside from the reward - offered under this Program). -- Make a genuine effort to prevent privacy violations, data destruction, and any interruption or degradation of Sablier - Lockup. -- Submit only one vulnerability per submission, unless chaining vulnerabilities is necessary to demonstrate the impact - of any of them. -- Do not submit a vulnerability that stems from an underlying issue for which a reward has already been paid under this - Program. -- You must not be a current or former employee, vendor, or contractor of Sablier Labs, or an employee of any of its - vendors or contractors. -- You must not be subject to UK sanctions or reside in a UK-embargoed country. -- Be at least 18 years old, or if younger, submit the vulnerability with the consent of a parent or guardian. - -### Other Terms - -By submitting your report, you grant Sablier Labs any and all rights, including intellectual property rights, needed to -validate, mitigate, and disclose the vulnerability. All reward decisions, including eligibility for and amounts of the -rewards and the manner in which such rewards will be paid, are made at our sole discretion. - -The terms and conditions of this Program may be altered at any time. diff --git a/benchmark/BatchLockup.Gas.t.sol b/benchmark/BatchLockup.Gas.t.sol deleted file mode 100644 index df8ca44b9..000000000 --- a/benchmark/BatchLockup.Gas.t.sol +++ /dev/null @@ -1,273 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.22; - -import { ud2x18 } from "@prb/math/src/UD2x18.sol"; - -import { Lockup, LockupDynamic, LockupTranched } from "../src/types/DataTypes.sol"; -import { BatchLockup } from "../src/types/DataTypes.sol"; -import { BatchLockupBuilder } from "../tests/utils/BatchLockupBuilder.sol"; - -import { Benchmark_Test } from "./Benchmark.t.sol"; - -/// @notice Tests used to benchmark {BatchLockup}. -/// @dev This contract creates a Markdown file with the gas usage of each function. -contract BatchLockup_Gas_Test is Benchmark_Test { - /*////////////////////////////////////////////////////////////////////////// - STATE VARIABLES - //////////////////////////////////////////////////////////////////////////*/ - - uint128 internal constant AMOUNT_PER_ITEM = 10e18; - uint8[5] internal batches = [5, 10, 20, 30, 50]; - uint8[5] internal counts = [24, 24, 24, 24, 12]; - - /*////////////////////////////////////////////////////////////////////////// - TEST FUNCTION - //////////////////////////////////////////////////////////////////////////*/ - - function testGas_Implementations() external { - // Set the file path. - benchmarkResultsFile = string.concat(benchmarkResults, "SablierBatchLockup.md"); - - // Create the file if it doesn't exist, otherwise overwrite it. - vm.writeFile({ - path: benchmarkResultsFile, - data: string.concat( - "# Benchmarks for BatchLockup\n\n", - "| Function | Lockup Type | Segments/Tranches | Batch Size | Gas Usage |\n", - "| --- | --- | --- | --- | --- |\n" - ) - }); - - for (uint256 i; i < batches.length; ++i) { - // Benchmark the batch create functions for Lockup Linear. - gasCreateWithDurationsLL(batches[i]); - gasCreateWithTimestampsLL(batches[i]); - - // Benchmark the batch create functions for Lockup Dynamic. - gasCreateWithDurationsLD({ batchSize: batches[i], segmentsCount: counts[i] }); - gasCreateWithTimestampsLD({ batchSize: batches[i], segmentsCount: counts[i] }); - - // Benchmark the batch create functions for Lockup Tranched. - gasCreateWithDurationsLT({ batchSize: batches[i], tranchesCount: counts[i] }); - gasCreateWithTimestampsLT({ batchSize: batches[i], tranchesCount: counts[i] }); - } - } - - /*////////////////////////////////////////////////////////////////////////// - GAS BENCHMARKS FOR BATCH FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - function gasCreateWithDurationsLD(uint256 batchSize, uint256 segmentsCount) internal { - Lockup.CreateWithDurations memory createParams = defaults.createWithDurationsBrokerNull(); - createParams.totalAmount = uint128(AMOUNT_PER_ITEM * segmentsCount); - LockupDynamic.SegmentWithDuration[] memory segments = _generateSegmentsWithDuration(segmentsCount); - BatchLockup.CreateWithDurationsLD[] memory params = - BatchLockupBuilder.fillBatch(createParams, segments, batchSize); - - uint256 initialGas = gasleft(); - batchLockup.createWithDurationsLD(lockup, dai, params); - string memory gasUsed = vm.toString(initialGas - gasleft()); - - contentToAppend = string.concat( - "| `createWithDurationsLD` | Lockup Dynamic |", - vm.toString(segmentsCount), - " |", - vm.toString(batchSize), - " | ", - gasUsed, - " |" - ); - - // Append the content to the file. - _appendToFile(benchmarkResultsFile, contentToAppend); - } - - function gasCreateWithTimestampsLD(uint256 batchSize, uint256 segmentsCount) internal { - Lockup.CreateWithTimestamps memory createParams = defaults.createWithTimestampsBrokerNull(); - LockupDynamic.Segment[] memory segments = _generateSegments(segmentsCount); - createParams.timestamps.start = getBlockTimestamp(); - createParams.timestamps.end = segments[segments.length - 1].timestamp; - createParams.totalAmount = uint128(AMOUNT_PER_ITEM * segmentsCount); - BatchLockup.CreateWithTimestampsLD[] memory params = - BatchLockupBuilder.fillBatch(createParams, segments, batchSize); - - uint256 initialGas = gasleft(); - batchLockup.createWithTimestampsLD(lockup, dai, params); - string memory gasUsed = vm.toString(initialGas - gasleft()); - - contentToAppend = string.concat( - "| `createWithTimestampsLD` | Lockup Dynamic |", - vm.toString(segmentsCount), - " |", - vm.toString(batchSize), - " | ", - gasUsed, - " |" - ); - - // Append the data to the file - _appendToFile(benchmarkResultsFile, contentToAppend); - } - - function gasCreateWithDurationsLL(uint256 batchSize) internal { - BatchLockup.CreateWithDurationsLL[] memory params = BatchLockupBuilder.fillBatch({ - params: defaults.createWithDurationsBrokerNull(), - unlockAmounts: defaults.unlockAmounts(), - durations: defaults.durations(), - batchSize: batchSize - }); - - uint256 initialGas = gasleft(); - batchLockup.createWithDurationsLL(lockup, dai, params); - string memory gasUsed = vm.toString(initialGas - gasleft()); - - contentToAppend = string.concat( - "| `createWithDurationsLL` | Lockup Linear | N/A |", vm.toString(batchSize), " | ", gasUsed, " |" - ); - - // Append the content to the file. - _appendToFile(benchmarkResultsFile, contentToAppend); - } - - function gasCreateWithTimestampsLL(uint256 batchSize) internal { - BatchLockup.CreateWithTimestampsLL[] memory params = BatchLockupBuilder.fillBatch({ - params: defaults.createWithTimestampsBrokerNull(), - unlockAmounts: defaults.unlockAmounts(), - cliffTime: defaults.CLIFF_TIME(), - batchSize: batchSize - }); - - uint256 initialGas = gasleft(); - batchLockup.createWithTimestampsLL(lockup, dai, params); - string memory gasUsed = vm.toString(initialGas - gasleft()); - - contentToAppend = string.concat( - "| `createWithTimestampsLL` | Lockup Linear | N/A |", vm.toString(batchSize), " | ", gasUsed, " |" - ); - - // Append the data to the file - _appendToFile(benchmarkResultsFile, contentToAppend); - } - - function gasCreateWithDurationsLT(uint256 batchSize, uint256 tranchesCount) internal { - Lockup.CreateWithDurations memory createParams = defaults.createWithDurationsBrokerNull(); - LockupTranched.TrancheWithDuration[] memory tranches = _generateTranchesWithDuration(tranchesCount); - createParams.totalAmount = uint128(AMOUNT_PER_ITEM * tranchesCount); - BatchLockup.CreateWithDurationsLT[] memory params = - BatchLockupBuilder.fillBatch(createParams, tranches, batchSize); - - uint256 initialGas = gasleft(); - batchLockup.createWithDurationsLT(lockup, dai, params); - string memory gasUsed = vm.toString(initialGas - gasleft()); - - contentToAppend = string.concat( - "| `createWithDurationsLT` | Lockup Tranched |", - vm.toString(tranchesCount), - " |", - vm.toString(batchSize), - " | ", - gasUsed, - " |" - ); - - // Append the content to the file. - _appendToFile(benchmarkResultsFile, contentToAppend); - } - - function gasCreateWithTimestampsLT(uint256 batchSize, uint256 tranchesCount) internal { - Lockup.CreateWithTimestamps memory createParams = defaults.createWithTimestampsBrokerNull(); - LockupTranched.Tranche[] memory tranches = _generateTranches(tranchesCount); - createParams.timestamps.start = getBlockTimestamp(); - createParams.timestamps.end = tranches[tranches.length - 1].timestamp; - createParams.totalAmount = uint128(AMOUNT_PER_ITEM * tranchesCount); - BatchLockup.CreateWithTimestampsLT[] memory params = - BatchLockupBuilder.fillBatch(createParams, tranches, batchSize); - - uint256 initialGas = gasleft(); - batchLockup.createWithTimestampsLT(lockup, dai, params); - string memory gasUsed = vm.toString(initialGas - gasleft()); - - contentToAppend = string.concat( - "| `createWithTimestampsLT` | Lockup Tranched |", - vm.toString(tranchesCount), - " |", - vm.toString(batchSize), - " | ", - gasUsed, - " |" - ); - - // Append the data to the file - _appendToFile(benchmarkResultsFile, contentToAppend); - } - - /*////////////////////////////////////////////////////////////////////////// - HELPERS - //////////////////////////////////////////////////////////////////////////*/ - - function _generateSegments(uint256 segmentsCount) private view returns (LockupDynamic.Segment[] memory) { - LockupDynamic.Segment[] memory segments = new LockupDynamic.Segment[](segmentsCount); - - // Populate segments. - for (uint256 i = 0; i < segmentsCount; ++i) { - segments[i] = LockupDynamic.Segment({ - amount: AMOUNT_PER_ITEM, - exponent: ud2x18(0.5e18), - timestamp: getBlockTimestamp() + uint40(defaults.CLIFF_DURATION() * (1 + i)) - }); - } - - return segments; - } - - function _generateSegmentsWithDuration(uint256 segmentsCount) - private - view - returns (LockupDynamic.SegmentWithDuration[] memory) - { - LockupDynamic.SegmentWithDuration[] memory segments = new LockupDynamic.SegmentWithDuration[](segmentsCount); - - // Populate segments. - for (uint256 i; i < segmentsCount; ++i) { - segments[i] = LockupDynamic.SegmentWithDuration({ - amount: AMOUNT_PER_ITEM, - exponent: ud2x18(0.5e18), - duration: defaults.CLIFF_DURATION() - }); - } - - return segments; - } - - function _generateTranches(uint256 tranchesCount) private view returns (LockupTranched.Tranche[] memory) { - LockupTranched.Tranche[] memory tranches = new LockupTranched.Tranche[](tranchesCount); - - // Populate tranches. - for (uint256 i = 0; i < tranchesCount; ++i) { - tranches[i] = ( - LockupTranched.Tranche({ - amount: AMOUNT_PER_ITEM, - timestamp: getBlockTimestamp() + uint40(defaults.CLIFF_DURATION() * (1 + i)) - }) - ); - } - - return tranches; - } - - function _generateTranchesWithDuration(uint256 tranchesCount) - private - view - returns (LockupTranched.TrancheWithDuration[] memory) - { - LockupTranched.TrancheWithDuration[] memory tranches = new LockupTranched.TrancheWithDuration[](tranchesCount); - - // Populate tranches. - for (uint256 i; i < tranchesCount; ++i) { - tranches[i] = - LockupTranched.TrancheWithDuration({ amount: AMOUNT_PER_ITEM, duration: defaults.CLIFF_DURATION() }); - } - - return tranches; - } -} diff --git a/benchmark/Benchmark.t.sol b/benchmark/Benchmark.t.sol deleted file mode 100644 index c908852ed..000000000 --- a/benchmark/Benchmark.t.sol +++ /dev/null @@ -1,174 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.22; - -import { UD60x18, ud } from "@prb/math/src/UD60x18.sol"; - -import { Base_Test } from "../tests/Base.t.sol"; - -/// @notice Benchmark contract with common logic needed by all tests. -abstract contract Benchmark_Test is Base_Test { - /*////////////////////////////////////////////////////////////////////////// - STATE VARIABLES - //////////////////////////////////////////////////////////////////////////*/ - - uint128 internal immutable AMOUNT_PER_SEGMENT = 100e18; - uint128 internal immutable AMOUNT_PER_TRANCHE = 100e18; - - /// @dev The directory where the benchmark files are stored. - string internal benchmarkResults = "benchmark/results/"; - - /// @dev The path to the file where the benchmark results are stored. - string internal benchmarkResultsFile; - - /// @dev A variable used to store the content to append to the results file. - string internal contentToAppend; - - uint256[7] internal streamIds = [50, 51, 52, 53, 54, 55, 56]; - - /*////////////////////////////////////////////////////////////////////////// - SET-UP FUNCTION - //////////////////////////////////////////////////////////////////////////*/ - - function setUp() public virtual override { - Base_Test.setUp(); - - deal({ token: address(dai), to: users.sender, give: type(uint256).max }); - resetPrank({ msgSender: users.sender }); - - // Create the first streams in each Lockup contract to initialize all the variables. - _createFewStreams(); - } - - /*////////////////////////////////////////////////////////////////////////// - GAS FUNCTIONS FOR SHARED IMPLEMENTATIONS - //////////////////////////////////////////////////////////////////////////*/ - - function gasBurn() internal { - // Set the caller to the Recipient for `burn` and change timestamp to the end time. - resetPrank({ msgSender: users.recipient }); - - vm.warp({ newTimestamp: defaults.END_TIME() }); - - lockup.withdrawMax(streamIds[0], users.recipient); - - uint256 initialGas = gasleft(); - lockup.burn(streamIds[0]); - string memory gasUsed = vm.toString(initialGas - gasleft()); - - contentToAppend = string.concat("| `burn` | ", gasUsed, " |"); - - // Append the content to the file. - _appendToFile(benchmarkResultsFile, contentToAppend); - } - - function gasCancel() internal { - // Set the caller to the Sender for the next calls and change timestamp to before end time - resetPrank({ msgSender: users.sender }); - - uint256 initialGas = gasleft(); - lockup.cancel(streamIds[1]); - string memory gasUsed = vm.toString(initialGas - gasleft()); - - contentToAppend = string.concat("| `cancel` | ", gasUsed, " |"); - - // Append the content to the file. - _appendToFile(benchmarkResultsFile, contentToAppend); - } - - function gasRenounce() internal { - // Set the caller to the Sender for the next calls and change timestamp to before end time. - resetPrank({ msgSender: users.sender }); - - uint256 initialGas = gasleft(); - lockup.renounce(streamIds[2]); - string memory gasUsed = vm.toString(initialGas - gasleft()); - - contentToAppend = string.concat("| `renounce` | ", gasUsed, " |"); - - // Append the content to the file. - _appendToFile(benchmarkResultsFile, contentToAppend); - } - - function gasWithdraw(uint256 streamId, address caller, address to, string memory extraInfo) internal { - resetPrank({ msgSender: caller }); - - uint128 withdrawAmount = lockup.withdrawableAmountOf(streamId); - - uint256 initialGas = gasleft(); - lockup.withdraw(streamId, to, withdrawAmount); - string memory gasUsed = vm.toString(initialGas - gasleft()); - - // Check if caller is recipient or not. - bool isCallerRecipient = caller == users.recipient; - - string memory s = isCallerRecipient - ? string.concat("| `withdraw` ", extraInfo, " (by Recipient) | ") - : string.concat("| `withdraw` ", extraInfo, " (by Anyone) | "); - contentToAppend = string.concat(s, gasUsed, " |"); - - // Append the data to the file. - _appendToFile(benchmarkResultsFile, contentToAppend); - } - - function gasWithdraw_AfterEndTime(uint256 streamId, address caller, address to, string memory extraInfo) internal { - extraInfo = string.concat(extraInfo, " (After End Time)"); - - uint256 warpTime = lockup.getEndTime(streamId) + 1; - vm.warp({ newTimestamp: warpTime }); - - gasWithdraw(streamId, caller, to, extraInfo); - } - - function gasWithdraw_BeforeEndTime( - uint256 streamId, - address caller, - address to, - string memory extraInfo - ) - internal - { - extraInfo = string.concat(extraInfo, " (Before End Time)"); - - uint256 warpTime = lockup.getEndTime(streamId) - 1; - vm.warp({ newTimestamp: warpTime }); - - gasWithdraw(streamId, caller, to, extraInfo); - } - - function gasWithdraw_ByAnyone(uint256 streamId1, uint256 streamId2, string memory extraInfo) internal { - gasWithdraw_AfterEndTime(streamId1, users.sender, users.recipient, extraInfo); - gasWithdraw_BeforeEndTime(streamId2, users.sender, users.recipient, extraInfo); - } - - function gasWithdraw_ByRecipient(uint256 streamId1, uint256 streamId2, string memory extraInfo) internal { - gasWithdraw_AfterEndTime(streamId1, users.recipient, users.alice, extraInfo); - gasWithdraw_BeforeEndTime(streamId2, users.recipient, users.alice, extraInfo); - } - - /*////////////////////////////////////////////////////////////////////////// - HELPERS - //////////////////////////////////////////////////////////////////////////*/ - - /// @dev Append a line to the file at given path. - function _appendToFile(string memory path, string memory line) internal { - vm.writeLine({ path: path, data: line }); - } - - /// @dev Calculates the total amount to be deposited in the stream, by accounting for the broker fee. - function _calculateTotalAmount(uint128 depositAmount, UD60x18 brokerFee) internal pure returns (uint128) { - UD60x18 factor = ud(1e18); - UD60x18 totalAmount = ud(depositAmount).mul(factor).div(factor.sub(brokerFee)); - return totalAmount.intoUint128(); - } - - /// @dev Internal function to creates a few streams in each Lockup contract. - function _createFewStreams() internal { - for (uint128 i = 0; i < 100; ++i) { - lockup.createWithTimestampsLD(defaults.createWithTimestamps(), defaults.segments()); - lockup.createWithTimestampsLL( - defaults.createWithTimestamps(), defaults.unlockAmounts(), defaults.CLIFF_TIME() - ); - lockup.createWithTimestampsLT(defaults.createWithTimestamps(), defaults.tranches()); - } - } -} diff --git a/benchmark/EstimateMaxCount.t.sol b/benchmark/EstimateMaxCount.t.sol deleted file mode 100644 index 854d966a8..000000000 --- a/benchmark/EstimateMaxCount.t.sol +++ /dev/null @@ -1,64 +0,0 @@ -// solhint-disable no-console -pragma solidity >=0.8.22 <0.9.0; - -import { console2 } from "forge-std/src/console2.sol"; -import { Test } from "forge-std/src/Test.sol"; - -import { Lockup_Dynamic_Gas_Test } from "./LockupDynamic.Gas.t.sol"; - -/// @notice Structure to group the block gas limit and chain id. -struct ChainInfo { - uint256 blockGasLimit; - uint256 chainId; -} - -contract EstimateMaxCount is Test { - // Buffer gas units to be deducted from the block gas limit so that the max count never exceeds the block limit. - uint256 public constant BUFFER_GAS = 1_000_000; - - // Initial guess for the maximum number of segments/tranches. - uint128 public constant INITIAL_GUESS = 240; - - /// @dev List of chains with their block gas limit. - ChainInfo[] public chains; - - constructor() { - chains.push(ChainInfo({ blockGasLimit: 32_000_000, chainId: 42_161 })); // Arbitrum - chains.push(ChainInfo({ blockGasLimit: 15_000_000, chainId: 43_114 })); // Avalanche - chains.push(ChainInfo({ blockGasLimit: 60_000_000, chainId: 8453 })); // Base - chains.push(ChainInfo({ blockGasLimit: 30_000_000, chainId: 81_457 })); // Blast - chains.push(ChainInfo({ blockGasLimit: 138_000_000, chainId: 56 })); // BNB - chains.push(ChainInfo({ blockGasLimit: 30_000_000, chainId: 1 })); // Ethereum - chains.push(ChainInfo({ blockGasLimit: 17_000_000, chainId: 100 })); // Gnosis - chains.push(ChainInfo({ blockGasLimit: 30_000_000, chainId: 10 })); // Optimism - chains.push(ChainInfo({ blockGasLimit: 30_000_000, chainId: 137 })); // Polygon - chains.push(ChainInfo({ blockGasLimit: 10_000_000, chainId: 534_352 })); // Scroll - chains.push(ChainInfo({ blockGasLimit: 30_000_000, chainId: 11_155_111 })); // Sepolia - } - - /// @notice Estimate the maximum number of segments allowed in LockupDynamic. - function test_EstimateSegments() public { - Lockup_Dynamic_Gas_Test lockupDynamicGasTest = new Lockup_Dynamic_Gas_Test(); - lockupDynamicGasTest.setUp(); - - for (uint256 i = 0; i < chains.length; ++i) { - uint128 count = INITIAL_GUESS; - - // Subtract `BUFFER_GAS` from `blockGasLimit` as an additional precaution to account for the dynamic gas for - // ether transfer on different chains. - uint256 blockGasLimit = chains[i].blockGasLimit - BUFFER_GAS; - - uint256 gasConsumed = 0; - uint256 lastGasConsumed = 0; - while (blockGasLimit > gasConsumed) { - count += 10; - lastGasConsumed = gasConsumed; - - // Estimate the gas consumed by adding 10 segments. - gasConsumed = lockupDynamicGasTest.computeGas_CreateWithDurationsLD(count + 10); - } - - console2.log("count: %d and gasUsed: %d and chainId: %d", count, lastGasConsumed, chains[i].chainId); - } - } -} diff --git a/benchmark/LockupDynamic.Gas.t.sol b/benchmark/LockupDynamic.Gas.t.sol deleted file mode 100644 index fc604b8aa..000000000 --- a/benchmark/LockupDynamic.Gas.t.sol +++ /dev/null @@ -1,232 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.22; - -import { ud2x18 } from "@prb/math/src/UD2x18.sol"; -import { UD60x18, ud } from "@prb/math/src/UD60x18.sol"; - -import { Lockup, LockupDynamic } from "../src/types/DataTypes.sol"; - -import { Benchmark_Test } from "./Benchmark.t.sol"; - -/// @notice Tests used to benchmark Lockup streams created using Dynamic model. -/// @dev This contract creates a Markdown file with the gas usage of each function. -contract Lockup_Dynamic_Gas_Test is Benchmark_Test { - /*////////////////////////////////////////////////////////////////////////// - STATE VARIABLES - //////////////////////////////////////////////////////////////////////////*/ - - uint128[] internal _segments = [2, 10, 100]; - uint256[] internal _streamIdsForWithdraw = new uint256[](4); - - /*////////////////////////////////////////////////////////////////////////// - SETUP - //////////////////////////////////////////////////////////////////////////*/ - - function setUp() public override { - Benchmark_Test.setUp(); - } - - /*////////////////////////////////////////////////////////////////////////// - TEST FUNCTION - //////////////////////////////////////////////////////////////////////////*/ - - function testGas_Implementations() external { - // Set the file path. - benchmarkResultsFile = string.concat(benchmarkResults, "SablierLockup_Dynamic.md"); - - // Create the file if it doesn't exist, otherwise overwrite it. - vm.writeFile({ - path: benchmarkResultsFile, - data: string.concat( - "# Benchmarks for the Lockup Dynamic model\n\n", "| Implementation | Gas Usage |\n", "| --- | --- |\n" - ) - }); - - vm.warp({ newTimestamp: defaults.END_TIME() }); - gasBurn(); - - vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); - - gasCancel(); - - gasRenounce(); - - // Create streams with different number of segments. - for (uint256 i; i < _segments.length; ++i) { - gasCreateWithDurationsLD({ totalSegments: _segments[i] }); - gasCreateWithTimestampsLD({ totalSegments: _segments[i] }); - - gasWithdraw_ByRecipient( - _streamIdsForWithdraw[0], - _streamIdsForWithdraw[1], - string.concat("(", vm.toString(_segments[i]), " segments)") - ); - gasWithdraw_ByAnyone( - _streamIdsForWithdraw[2], - _streamIdsForWithdraw[3], - string.concat("(", vm.toString(_segments[i]), " segments)") - ); - } - } - - /*////////////////////////////////////////////////////////////////////////// - GAS BENCHMARKS FOR CREATE FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - // The following function is used in the estimations of `MAX_COUNT`. - function computeGas_CreateWithDurationsLD(uint128 totalSegments) public returns (uint256 gasUsed) { - (Lockup.CreateWithDurations memory params, LockupDynamic.SegmentWithDuration[] memory segments) = - _createWithDurationParamsLD(totalSegments, defaults.BROKER_FEE()); - - uint256 beforeGas = gasleft(); - lockup.createWithDurationsLD(params, segments); - - gasUsed = beforeGas - gasleft(); - } - - function gasCreateWithDurationsLD(uint128 totalSegments) internal { - // Set the caller to the Sender for the next calls and change timestamp to before end time. - resetPrank({ msgSender: users.sender }); - - (Lockup.CreateWithDurations memory params, LockupDynamic.SegmentWithDuration[] memory segments) = - _createWithDurationParamsLD(totalSegments, defaults.BROKER_FEE()); - - uint256 beforeGas = gasleft(); - lockup.createWithDurationsLD(params, segments); - string memory gasUsed = vm.toString(beforeGas - gasleft()); - - contentToAppend = string.concat( - "| `createWithDurationsLD` (", vm.toString(totalSegments), " segments) (Broker fee set) | ", gasUsed, " |" - ); - - // Append the content to the file. - _appendToFile(benchmarkResultsFile, contentToAppend); - - // Calculate gas usage without broker fee. - (params, segments) = _createWithDurationParamsLD(totalSegments, ud(0)); - - beforeGas = gasleft(); - lockup.createWithDurationsLD(params, segments); - gasUsed = vm.toString(beforeGas - gasleft()); - - contentToAppend = string.concat( - "| `createWithDurationsLD` (", - vm.toString(totalSegments), - " segments) (Broker fee not set) | ", - gasUsed, - " |" - ); - - _appendToFile(benchmarkResultsFile, contentToAppend); - - // Store the last 2 streams IDs for withdraw gas benchmark. - _streamIdsForWithdraw[0] = lockup.nextStreamId() - 2; - _streamIdsForWithdraw[1] = lockup.nextStreamId() - 1; - - // Create 2 more streams for withdraw gas benchmark. - _streamIdsForWithdraw[2] = lockup.createWithDurationsLD(params, segments); - _streamIdsForWithdraw[3] = lockup.createWithDurationsLD(params, segments); - } - - function gasCreateWithTimestampsLD(uint128 totalSegments) internal { - // Set the caller to the Sender for the next calls and change timestamp to before end time - resetPrank({ msgSender: users.sender }); - - (Lockup.CreateWithTimestamps memory params, LockupDynamic.Segment[] memory segments) = - _createWithTimestampParamsLD(totalSegments, defaults.BROKER_FEE()); - - uint256 beforeGas = gasleft(); - lockup.createWithTimestampsLD(params, segments); - - string memory gasUsed = vm.toString(beforeGas - gasleft()); - - contentToAppend = string.concat( - "| `createWithTimestampsLD` (", vm.toString(totalSegments), " segments) (Broker fee set) | ", gasUsed, " |" - ); - - // Append the data to the file - _appendToFile(benchmarkResultsFile, contentToAppend); - - // Calculate gas usage without broker fee. - (params, segments) = _createWithTimestampParamsLD(totalSegments, ud(0)); - - beforeGas = gasleft(); - lockup.createWithTimestampsLD(params, segments); - gasUsed = vm.toString(beforeGas - gasleft()); - - contentToAppend = string.concat( - "| `createWithTimestampsLD` (", - vm.toString(totalSegments), - " segments) (Broker fee not set) | ", - gasUsed, - " |" - ); - - // Append the data to the file - _appendToFile(benchmarkResultsFile, contentToAppend); - } - - /*////////////////////////////////////////////////////////////////////////// - HELPERS - //////////////////////////////////////////////////////////////////////////*/ - - function _createWithDurationParamsLD( - uint128 totalSegments, - UD60x18 brokerFee - ) - private - view - returns (Lockup.CreateWithDurations memory params, LockupDynamic.SegmentWithDuration[] memory segments_) - { - segments_ = new LockupDynamic.SegmentWithDuration[](totalSegments); - - // Populate segments. - for (uint256 i = 0; i < totalSegments; ++i) { - segments_[i] = ( - LockupDynamic.SegmentWithDuration({ - amount: AMOUNT_PER_SEGMENT, - exponent: ud2x18(0.5e18), - duration: defaults.CLIFF_DURATION() - }) - ); - } - - uint128 depositAmount = AMOUNT_PER_SEGMENT * totalSegments; - - params = defaults.createWithDurations(); - params.totalAmount = _calculateTotalAmount(depositAmount, brokerFee); - params.broker.fee = brokerFee; - return (params, segments_); - } - - function _createWithTimestampParamsLD( - uint128 totalSegments, - UD60x18 brokerFee - ) - private - view - returns (Lockup.CreateWithTimestamps memory params, LockupDynamic.Segment[] memory segments_) - { - segments_ = new LockupDynamic.Segment[](totalSegments); - - // Populate segments. - for (uint256 i = 0; i < totalSegments; ++i) { - segments_[i] = ( - LockupDynamic.Segment({ - amount: AMOUNT_PER_SEGMENT, - exponent: ud2x18(0.5e18), - timestamp: getBlockTimestamp() + uint40(defaults.CLIFF_DURATION() * (1 + i)) - }) - ); - } - - uint128 depositAmount = AMOUNT_PER_SEGMENT * totalSegments; - - params = defaults.createWithTimestamps(); - params.totalAmount = _calculateTotalAmount(depositAmount, brokerFee); - params.timestamps.start = getBlockTimestamp(); - params.timestamps.end = segments_[totalSegments - 1].timestamp; - params.broker.fee = brokerFee; - return (params, segments_); - } -} diff --git a/benchmark/LockupLinear.Gas.t.sol b/benchmark/LockupLinear.Gas.t.sol deleted file mode 100644 index 7b9f7c8f2..000000000 --- a/benchmark/LockupLinear.Gas.t.sol +++ /dev/null @@ -1,123 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.22; - -import { ud } from "@prb/math/src/UD60x18.sol"; - -import { Lockup, LockupLinear } from "../src/types/DataTypes.sol"; - -import { Benchmark_Test } from "./Benchmark.t.sol"; - -/// @notice Tests used to benchmark Lockup streams created using Linear model. -/// @dev This contract creates a Markdown file with the gas usage of each function. -contract Lockup_Linear_Gas_Test is Benchmark_Test { - /*////////////////////////////////////////////////////////////////////////// - TEST FUNCTION - //////////////////////////////////////////////////////////////////////////*/ - - function testGas_Implementations() external { - // Set the file path. - benchmarkResultsFile = string.concat(benchmarkResults, "SablierLockup_Linear.md"); - - // Create the file if it doesn't exist, otherwise overwrite it. - vm.writeFile({ - path: benchmarkResultsFile, - data: string.concat( - "# Benchmarks for the Lockup Linear model\n\n", "| Implementation | Gas Usage |\n", "| --- | --- |\n" - ) - }); - - vm.warp({ newTimestamp: defaults.END_TIME() }); - gasBurn(); - - vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); - gasCancel(); - - gasRenounce(); - - gasCreateWithDurationsLL({ cliffDuration: 0 }); - gasCreateWithDurationsLL({ cliffDuration: defaults.CLIFF_DURATION() }); - - gasCreateWithTimestampsLL({ cliffTime: 0 }); - gasCreateWithTimestampsLL({ cliffTime: defaults.CLIFF_TIME() }); - - gasWithdraw_ByRecipient(streamIds[3], streamIds[4], ""); - gasWithdraw_ByAnyone(streamIds[5], streamIds[6], ""); - } - - /*////////////////////////////////////////////////////////////////////////// - GAS BENCHMARKS FOR CREATE FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - function gasCreateWithDurationsLL(uint40 cliffDuration) internal { - // Set the caller to the Sender for the next calls and change timestamp to before end time. - resetPrank({ msgSender: users.sender }); - - Lockup.CreateWithDurations memory params = defaults.createWithDurations(); - LockupLinear.Durations memory durations = defaults.durations(); - durations.cliff = cliffDuration; - - LockupLinear.UnlockAmounts memory unlockAmounts = defaults.unlockAmounts(); - if (cliffDuration == 0) unlockAmounts.cliff = 0; - - uint256 beforeGas = gasleft(); - lockup.createWithDurationsLL(params, unlockAmounts, durations); - string memory gasUsed = vm.toString(beforeGas - gasleft()); - - string memory cliffSetOrNot = cliffDuration == 0 ? " (cliff not set)" : " (cliff set)"; - - contentToAppend = - string.concat("| `createWithDurationsLL` (Broker fee set)", cliffSetOrNot, " | ", gasUsed, " |"); - - // Append the content to the file. - _appendToFile(benchmarkResultsFile, contentToAppend); - - // Calculate gas usage without broker fee. - params.broker.fee = ud(0); - params.totalAmount = _calculateTotalAmount(defaults.DEPOSIT_AMOUNT(), ud(0)); - - beforeGas = gasleft(); - lockup.createWithDurationsLL(params, unlockAmounts, durations); - gasUsed = vm.toString(beforeGas - gasleft()); - - contentToAppend = - string.concat("| `createWithDurationsLL` (Broker fee not set)", cliffSetOrNot, " | ", gasUsed, " |"); - - // Append the content to the file. - _appendToFile(benchmarkResultsFile, contentToAppend); - } - - function gasCreateWithTimestampsLL(uint40 cliffTime) internal { - // Set the caller to the Sender for the next calls and change timestamp to before end time. - resetPrank({ msgSender: users.sender }); - - Lockup.CreateWithTimestamps memory params = defaults.createWithTimestamps(); - LockupLinear.UnlockAmounts memory unlockAmounts = defaults.unlockAmounts(); - if (cliffTime == 0) unlockAmounts.cliff = 0; - - uint256 beforeGas = gasleft(); - lockup.createWithTimestampsLL(params, unlockAmounts, cliffTime); - string memory gasUsed = vm.toString(beforeGas - gasleft()); - - string memory cliffSetOrNot = cliffTime == 0 ? " (cliff not set)" : " (cliff set)"; - - contentToAppend = - string.concat("| `createWithTimestampsLL` (Broker fee set)", cliffSetOrNot, " | ", gasUsed, " |"); - - // Append the content to the file. - _appendToFile(benchmarkResultsFile, contentToAppend); - - // Calculate gas usage without broker fee. - params.broker.fee = ud(0); - params.totalAmount = _calculateTotalAmount(defaults.DEPOSIT_AMOUNT(), ud(0)); - - beforeGas = gasleft(); - lockup.createWithTimestampsLL(params, unlockAmounts, cliffTime); - gasUsed = vm.toString(beforeGas - gasleft()); - - contentToAppend = - string.concat("| `createWithTimestampsLL` (Broker fee not set)", cliffSetOrNot, " | ", gasUsed, " |"); - - // Append the content to the file. - _appendToFile(benchmarkResultsFile, contentToAppend); - } -} diff --git a/benchmark/LockupTranched.Gas.t.sol b/benchmark/LockupTranched.Gas.t.sol deleted file mode 100644 index 0ef67fe0c..000000000 --- a/benchmark/LockupTranched.Gas.t.sol +++ /dev/null @@ -1,215 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.22; - -import { UD60x18, ud } from "@prb/math/src/UD60x18.sol"; - -import { Lockup, LockupTranched } from "../src/types/DataTypes.sol"; - -import { Benchmark_Test } from "./Benchmark.t.sol"; - -/// @notice Tests used to benchmark Lockup streams created using Tranched model. -/// @dev This contract creates a Markdown file with the gas usage of each function. -contract Lockup_Tranched_Gas_Test is Benchmark_Test { - /*////////////////////////////////////////////////////////////////////////// - STATE VARIABLES - //////////////////////////////////////////////////////////////////////////*/ - - uint128[] internal _tranches = [2, 10, 100]; - uint256[] internal _streamIdsForWithdraw = new uint256[](4); - - /*////////////////////////////////////////////////////////////////////////// - TEST FUNCTION - //////////////////////////////////////////////////////////////////////////*/ - - function testGas_Implementations() external { - // Set the file path. - benchmarkResultsFile = string.concat(benchmarkResults, "SablierLockup_Tranched.md"); - - // Create the file if it doesn't exist, otherwise overwrite it. - vm.writeFile({ - path: benchmarkResultsFile, - data: string.concat( - "# Benchmarks for the Lockup Tranched model\n\n", "| Implementation | Gas Usage |\n", "| --- | --- |\n" - ) - }); - - vm.warp({ newTimestamp: defaults.END_TIME() }); - gasBurn(); - - vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); - - gasCancel(); - - gasRenounce(); - - // Create streams with different number of tranches. - for (uint256 i; i < _tranches.length; ++i) { - gasCreateWithDurationsLT({ totalTranches: _tranches[i] }); - gasCreateWithTimestampsLT({ totalTranches: _tranches[i] }); - - gasWithdraw_ByRecipient( - _streamIdsForWithdraw[0], - _streamIdsForWithdraw[1], - string.concat("(", vm.toString(_tranches[i]), " tranches)") - ); - gasWithdraw_ByAnyone( - _streamIdsForWithdraw[2], - _streamIdsForWithdraw[3], - string.concat("(", vm.toString(_tranches[i]), " tranches)") - ); - } - } - - /*////////////////////////////////////////////////////////////////////////// - GAS BENCHMARKS FOR CREATE FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - // The following function is used in the estimation of `MAX_COUNT` - function computeGas_CreateWithDurationsLT(uint128 totalTranches) public returns (uint256 gasUsed) { - (Lockup.CreateWithDurations memory params, LockupTranched.TrancheWithDuration[] memory tranches) = - _createWithDurationParamsLT(totalTranches, defaults.BROKER_FEE()); - - uint256 beforeGas = gasleft(); - lockup.createWithDurationsLT(params, tranches); - - gasUsed = beforeGas - gasleft(); - } - - function gasCreateWithDurationsLT(uint128 totalTranches) internal { - // Set the caller to the Sender for the next calls and change timestamp to before end time. - resetPrank({ msgSender: users.sender }); - - (Lockup.CreateWithDurations memory params, LockupTranched.TrancheWithDuration[] memory tranches) = - _createWithDurationParamsLT(totalTranches, defaults.BROKER_FEE()); - - uint256 beforeGas = gasleft(); - lockup.createWithDurationsLT(params, tranches); - string memory gasUsed = vm.toString(beforeGas - gasleft()); - - contentToAppend = string.concat( - "| `createWithDurationsLT` (", vm.toString(totalTranches), " tranches) (Broker fee set) | ", gasUsed, " |" - ); - - // Append the content to the file. - _appendToFile(benchmarkResultsFile, contentToAppend); - - // Calculate gas usage without broker fee. - (params, tranches) = _createWithDurationParamsLT(totalTranches, ud(0)); - - beforeGas = gasleft(); - lockup.createWithDurationsLT(params, tranches); - gasUsed = vm.toString(beforeGas - gasleft()); - - contentToAppend = string.concat( - "| `createWithDurationsLT` (", - vm.toString(totalTranches), - " tranches) (Broker fee not set) | ", - gasUsed, - " |" - ); - - _appendToFile(benchmarkResultsFile, contentToAppend); - - // Store the last 2 streams IDs for withdraw gas benchmark. - _streamIdsForWithdraw[0] = lockup.nextStreamId() - 2; - _streamIdsForWithdraw[1] = lockup.nextStreamId() - 1; - - // Create 2 more streams for withdraw gas benchmark. - _streamIdsForWithdraw[2] = lockup.createWithDurationsLT(params, tranches); - _streamIdsForWithdraw[3] = lockup.createWithDurationsLT(params, tranches); - } - - function gasCreateWithTimestampsLT(uint128 totalTranches) internal { - // Set the caller to the Sender for the next calls and change timestamp to before end time. - resetPrank({ msgSender: users.sender }); - - (Lockup.CreateWithTimestamps memory params, LockupTranched.Tranche[] memory tranches) = - _createWithTimestampParamsLT(totalTranches, defaults.BROKER_FEE()); - uint256 beforeGas = gasleft(); - lockup.createWithTimestampsLT(params, tranches); - - string memory gasUsed = vm.toString(beforeGas - gasleft()); - - contentToAppend = string.concat( - "| `createWithTimestampsLT` (", vm.toString(totalTranches), " tranches) (Broker fee set) | ", gasUsed, " |" - ); - - // Append the content to the file. - _appendToFile(benchmarkResultsFile, contentToAppend); - - (params, tranches) = _createWithTimestampParamsLT(totalTranches, ud(0)); - beforeGas = gasleft(); - lockup.createWithTimestampsLT(params, tranches); - gasUsed = vm.toString(beforeGas - gasleft()); - - contentToAppend = string.concat( - "| `createWithTimestampsLT` (", - vm.toString(totalTranches), - " tranches) (Broker fee not set) | ", - gasUsed, - " |" - ); - - // Append the content to the file. - _appendToFile(benchmarkResultsFile, contentToAppend); - } - - /*////////////////////////////////////////////////////////////////////////// - HELPERS - //////////////////////////////////////////////////////////////////////////*/ - - function _createWithDurationParamsLT( - uint128 totalTranches, - UD60x18 brokerFee - ) - private - view - returns (Lockup.CreateWithDurations memory params, LockupTranched.TrancheWithDuration[] memory tranches_) - { - tranches_ = new LockupTranched.TrancheWithDuration[](totalTranches); - - // Populate tranches - for (uint256 i = 0; i < totalTranches; ++i) { - tranches_[i] = ( - LockupTranched.TrancheWithDuration({ amount: AMOUNT_PER_TRANCHE, duration: defaults.CLIFF_DURATION() }) - ); - } - - uint128 depositAmount = AMOUNT_PER_SEGMENT * totalTranches; - - params = defaults.createWithDurations(); - params.broker.fee = brokerFee; - params.totalAmount = _calculateTotalAmount(depositAmount, brokerFee); - return (params, tranches_); - } - - function _createWithTimestampParamsLT( - uint128 totalTranches, - UD60x18 brokerFee - ) - private - view - returns (Lockup.CreateWithTimestamps memory params, LockupTranched.Tranche[] memory tranches_) - { - tranches_ = new LockupTranched.Tranche[](totalTranches); - - // Populate tranches. - for (uint256 i = 0; i < totalTranches; ++i) { - tranches_[i] = ( - LockupTranched.Tranche({ - amount: AMOUNT_PER_TRANCHE, - timestamp: getBlockTimestamp() + uint40(defaults.CLIFF_DURATION() * (1 + i)) - }) - ); - } - - uint128 depositAmount = AMOUNT_PER_SEGMENT * totalTranches; - - params = defaults.createWithTimestamps(); - params.broker.fee = brokerFee; - params.timestamps.start = getBlockTimestamp(); - params.timestamps.end = tranches_[totalTranches - 1].timestamp; - params.totalAmount = _calculateTotalAmount(depositAmount, brokerFee); - return (params, tranches_); - } -} diff --git a/benchmark/results/SablierBatchLockup.md b/benchmark/results/SablierBatchLockup.md deleted file mode 100644 index 2ad68aded..000000000 --- a/benchmark/results/SablierBatchLockup.md +++ /dev/null @@ -1,34 +0,0 @@ -# Benchmarks for BatchLockup - -| Function | Lockup Type | Segments/Tranches | Batch Size | Gas Usage | -| ------------------------ | --------------- | ----------------- | ---------- | --------- | -| `createWithDurationsLL` | Lockup Linear | N/A | 5 | 937003 | -| `createWithTimestampsLL` | Lockup Linear | N/A | 5 | 898916 | -| `createWithDurationsLD` | Lockup Dynamic | 24 | 5 | 4123217 | -| `createWithTimestampsLD` | Lockup Dynamic | 24 | 5 | 3895052 | -| `createWithDurationsLT` | Lockup Tranched | 24 | 5 | 4013105 | -| `createWithTimestampsLT` | Lockup Tranched | 24 | 5 | 3822707 | -| `createWithDurationsLL` | Lockup Linear | N/A | 10 | 1740955 | -| `createWithTimestampsLL` | Lockup Linear | N/A | 10 | 1747416 | -| `createWithDurationsLD` | Lockup Dynamic | 24 | 10 | 8202890 | -| `createWithTimestampsLD` | Lockup Dynamic | 24 | 10 | 7741699 | -| `createWithDurationsLT` | Lockup Tranched | 24 | 10 | 7974447 | -| `createWithTimestampsLT` | Lockup Tranched | 24 | 10 | 7597402 | -| `createWithDurationsLL` | Lockup Linear | N/A | 20 | 3433786 | -| `createWithTimestampsLL` | Lockup Linear | N/A | 20 | 3447467 | -| `createWithDurationsLD` | Lockup Dynamic | 24 | 20 | 16380960 | -| `createWithTimestampsLD` | Lockup Dynamic | 24 | 20 | 15440827 | -| `createWithDurationsLT` | Lockup Tranched | 24 | 20 | 15896070 | -| `createWithTimestampsLT` | Lockup Tranched | 24 | 20 | 15152551 | -| `createWithDurationsLL` | Lockup Linear | N/A | 30 | 5125959 | -| `createWithTimestampsLL` | Lockup Linear | N/A | 30 | 5155292 | -| `createWithDurationsLD` | Lockup Dynamic | 24 | 30 | 24603376 | -| `createWithTimestampsLD` | Lockup Dynamic | 24 | 30 | 23157026 | -| `createWithDurationsLT` | Lockup Tranched | 24 | 30 | 23818565 | -| `createWithTimestampsLT` | Lockup Tranched | 24 | 30 | 22725003 | -| `createWithDurationsLL` | Lockup Linear | N/A | 50 | 8532644 | -| `createWithTimestampsLL` | Lockup Linear | N/A | 50 | 8582221 | -| `createWithDurationsLD` | Lockup Dynamic | 12 | 50 | 24275049 | -| `createWithTimestampsLD` | Lockup Dynamic | 12 | 50 | 23058857 | -| `createWithDurationsLT` | Lockup Tranched | 12 | 50 | 23611123 | -| `createWithTimestampsLT` | Lockup Tranched | 12 | 50 | 22718936 | diff --git a/benchmark/results/SablierLockup_Dynamic.md b/benchmark/results/SablierLockup_Dynamic.md deleted file mode 100644 index 9ec898bc0..000000000 --- a/benchmark/results/SablierLockup_Dynamic.md +++ /dev/null @@ -1,31 +0,0 @@ -# Benchmarks for the Lockup Dynamic model - -| Implementation | Gas Usage | -| ------------------------------------------------------------ | --------- | -| `burn` | 16141 | -| `cancel` | 65381 | -| `renounce` | 27721 | -| `createWithDurationsLD` (2 segments) (Broker fee set) | 216788 | -| `createWithDurationsLD` (2 segments) (Broker fee not set) | 200461 | -| `createWithTimestampsLD` (2 segments) (Broker fee set) | 197652 | -| `createWithTimestampsLD` (2 segments) (Broker fee not set) | 192627 | -| `withdraw` (2 segments) (After End Time) (by Recipient) | 23885 | -| `withdraw` (2 segments) (Before End Time) (by Recipient) | 29903 | -| `withdraw` (2 segments) (After End Time) (by Anyone) | 19175 | -| `withdraw` (2 segments) (Before End Time) (by Anyone) | 29992 | -| `createWithDurationsLD` (10 segments) (Broker fee set) | 422199 | -| `createWithDurationsLD` (10 segments) (Broker fee not set) | 417189 | -| `createWithTimestampsLD` (10 segments) (Broker fee set) | 402125 | -| `createWithTimestampsLD` (10 segments) (Broker fee not set) | 397126 | -| `withdraw` (10 segments) (After End Time) (by Recipient) | 24167 | -| `withdraw` (10 segments) (Before End Time) (by Recipient) | 37190 | -| `withdraw` (10 segments) (After End Time) (by Anyone) | 24278 | -| `withdraw` (10 segments) (Before End Time) (by Anyone) | 37279 | -| `createWithDurationsLD` (100 segments) (Broker fee set) | 2898563 | -| `createWithDurationsLD` (100 segments) (Broker fee not set) | 2894573 | -| `createWithTimestampsLD` (100 segments) (Broker fee set) | 2706641 | -| `createWithTimestampsLD` (100 segments) (Broker fee not set) | 2702660 | -| `withdraw` (100 segments) (After End Time) (by Recipient) | 81920 | -| `withdraw` (100 segments) (Before End Time) (by Recipient) | 119603 | -| `withdraw` (100 segments) (After End Time) (by Anyone) | 82009 | -| `withdraw` (100 segments) (Before End Time) (by Anyone) | 119692 | diff --git a/benchmark/results/SablierLockup_Linear.md b/benchmark/results/SablierLockup_Linear.md deleted file mode 100644 index fc9378295..000000000 --- a/benchmark/results/SablierLockup_Linear.md +++ /dev/null @@ -1,19 +0,0 @@ -# Benchmarks for the Lockup Linear model - -| Implementation | Gas Usage | -| ------------------------------------------------------------- | --------- | -| `burn` | 16141 | -| `cancel` | 65381 | -| `renounce` | 27721 | -| `createWithDurationsLL` (Broker fee set) (cliff not set) | 138649 | -| `createWithDurationsLL` (Broker fee not set) (cliff not set) | 122287 | -| `createWithDurationsLL` (Broker fee set) (cliff set) | 169335 | -| `createWithDurationsLL` (Broker fee not set) (cliff set) | 164278 | -| `createWithTimestampsLL` (Broker fee set) (cliff not set) | 125100 | -| `createWithTimestampsLL` (Broker fee not set) (cliff not set) | 120038 | -| `createWithTimestampsLL` (Broker fee set) (cliff set) | 169682 | -| `createWithTimestampsLL` (Broker fee not set) (cliff set) | 164614 | -| `withdraw` (After End Time) (by Recipient) | 33179 | -| `withdraw` (Before End Time) (by Recipient) | 23303 | -| `withdraw` (After End Time) (by Anyone) | 29561 | -| `withdraw` (Before End Time) (by Anyone) | 22815 | diff --git a/benchmark/results/SablierLockup_Tranched.md b/benchmark/results/SablierLockup_Tranched.md deleted file mode 100644 index 01bac3ff2..000000000 --- a/benchmark/results/SablierLockup_Tranched.md +++ /dev/null @@ -1,31 +0,0 @@ -# Benchmarks for the Lockup Tranched model - -| Implementation | Gas Usage | -| ------------------------------------------------------------ | --------- | -| `burn` | 16141 | -| `cancel` | 65381 | -| `renounce` | 27721 | -| `createWithDurationsLT` (2 tranches) (Broker fee set) | 215994 | -| `createWithDurationsLT` (2 tranches) (Broker fee not set) | 199665 | -| `createWithTimestampsLT` (2 tranches) (Broker fee set) | 196988 | -| `createWithTimestampsLT` (2 tranches) (Broker fee not set) | 191964 | -| `withdraw` (2 tranches) (After End Time) (by Recipient) | 23599 | -| `withdraw` (2 tranches) (Before End Time) (by Recipient) | 18503 | -| `withdraw` (2 tranches) (After End Time) (by Anyone) | 18889 | -| `withdraw` (2 tranches) (Before End Time) (by Anyone) | 18592 | -| `createWithDurationsLT` (10 tranches) (Broker fee set) | 414411 | -| `createWithDurationsLT` (10 tranches) (Broker fee not set) | 409394 | -| `createWithTimestampsLT` (10 tranches) (Broker fee set) | 397045 | -| `createWithTimestampsLT` (10 tranches) (Broker fee not set) | 392026 | -| `withdraw` (10 tranches) (After End Time) (by Recipient) | 23318 | -| `withdraw` (10 tranches) (Before End Time) (by Recipient) | 25403 | -| `withdraw` (10 tranches) (After End Time) (by Anyone) | 23427 | -| `withdraw` (10 tranches) (Before End Time) (by Anyone) | 25492 | -| `createWithDurationsLT` (100 tranches) (Broker fee set) | 2808652 | -| `createWithDurationsLT` (100 tranches) (Broker fee not set) | 2804166 | -| `createWithTimestampsLT` (100 tranches) (Broker fee set) | 2649659 | -| `createWithTimestampsLT` (100 tranches) (Broker fee not set) | 2645177 | -| `withdraw` (100 tranches) (After End Time) (by Recipient) | 74530 | -| `withdraw` (100 tranches) (Before End Time) (by Recipient) | 103255 | -| `withdraw` (100 tranches) (After End Time) (by Anyone) | 74619 | -| `withdraw` (100 tranches) (Before End Time) (by Anyone) | 103344 | diff --git a/bun.lock b/bun.lock new file mode 100644 index 000000000..67c3764dc --- /dev/null +++ b/bun.lock @@ -0,0 +1,899 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "@sablier/lockup", + "dependencies": { + "@openzeppelin/contracts": "5.3.0", + "@prb/math": "4.1.0", + "@sablier/evm-utils": "1.0.0", + }, + "devDependencies": { + "@sablier/devkit": "github:sablier-labs/devkit", + "forge-std": "github:foundry-rs/forge-std#v1.10.0", + "husky": "^9.1", + "lint-staged": "^16.1", + "prettier": "^3.5", + "solady": "0.0.208", + "solarray": "github:evmcheb/solarray#a547630", + "solhint": "^6.0", + }, + "peerDependencies": { + "@prb/math": "4.x.x", + }, + }, + }, + "packages": { + "@arbitrum/nitro-contracts": ["@arbitrum/nitro-contracts@1.1.1", "", { "dependencies": { "@offchainlabs/upgrade-executor": "1.1.0-beta.0", "@openzeppelin/contracts": "4.5.0", "@openzeppelin/contracts-upgradeable": "4.5.2", "patch-package": "^6.4.7" } }, "sha512-4Tyk3XVHz+bm8UujUC78LYSw3xAxyYvBCxfEX4z3qE4/ww7Qck/rmce5gbHMzQjArEAzAP2YSfYIFuIFuRXtfg=="], + + "@arbitrum/token-bridge-contracts": ["@arbitrum/token-bridge-contracts@1.1.2", "", { "dependencies": { "@arbitrum/nitro-contracts": "^1.0.0-beta.8", "@offchainlabs/upgrade-executor": "1.1.0-beta.0", "@openzeppelin/contracts": "4.8.3", "@openzeppelin/contracts-upgradeable": "4.8.3" }, "optionalDependencies": { "@openzeppelin/upgrades-core": "^1.24.1" } }, "sha512-k7AZXiB2HFecJ1KfaDBqgOKe3Loo1ttGLC7hUOVB+0YrihIR6cYpJRuqKSKK4YCy+FF21AUDtaG3x57OFM667Q=="], + + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], + + "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], + + "@bytecodealliance/preview2-shim": ["@bytecodealliance/preview2-shim@0.17.0", "", {}, "sha512-JorcEwe4ud0x5BS/Ar2aQWOQoFzjq/7jcnxYXCvSMh0oRm0dQXzOA+hqLDBnOMks1LLBA7dmiLLsEBl09Yd6iQ=="], + + "@chainlink/contracts": ["@chainlink/contracts@1.3.0", "", { "dependencies": { "@arbitrum/nitro-contracts": "1.1.1", "@arbitrum/token-bridge-contracts": "1.1.2", "@changesets/changelog-github": "^0.5.0", "@changesets/cli": "~2.27.8", "@eth-optimism/contracts": "0.6.0", "@openzeppelin/contracts": "4.9.3", "@openzeppelin/contracts-upgradeable": "4.9.3", "@scroll-tech/contracts": "0.1.0", "@zksync/contracts": "git+https://github.com/matter-labs/era-contracts.git#446d391d34bdb48255d5f8fef8a8248925fc98b9", "semver": "^7.6.3" } }, "sha512-Vk93nijTC5iRFW/L6FKUzeMuJy7k5dNzAtqlHpdreqtzL7efO/qXbYCkqjJFNXGurfOXVehHlehFoH4tWvSbfw=="], + + "@changesets/apply-release-plan": ["@changesets/apply-release-plan@7.0.13", "", { "dependencies": { "@changesets/config": "^3.1.1", "@changesets/get-version-range-type": "^0.4.0", "@changesets/git": "^3.0.4", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "detect-indent": "^6.0.0", "fs-extra": "^7.0.1", "lodash.startcase": "^4.4.0", "outdent": "^0.5.0", "prettier": "^2.7.1", "resolve-from": "^5.0.0", "semver": "^7.5.3" } }, "sha512-BIW7bofD2yAWoE8H4V40FikC+1nNFEKBisMECccS16W1rt6qqhNTBDmIw5HaqmMgtLNz9e7oiALiEUuKrQ4oHg=="], + + "@changesets/assemble-release-plan": ["@changesets/assemble-release-plan@6.0.9", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.3", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "semver": "^7.5.3" } }, "sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ=="], + + "@changesets/changelog-git": ["@changesets/changelog-git@0.2.1", "", { "dependencies": { "@changesets/types": "^6.1.0" } }, "sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q=="], + + "@changesets/changelog-github": ["@changesets/changelog-github@0.5.1", "", { "dependencies": { "@changesets/get-github-info": "^0.6.0", "@changesets/types": "^6.1.0", "dotenv": "^8.1.0" } }, "sha512-BVuHtF+hrhUScSoHnJwTELB4/INQxVFc+P/Qdt20BLiBFIHFJDDUaGsZw+8fQeJTRP5hJZrzpt3oZWh0G19rAQ=="], + + "@changesets/cli": ["@changesets/cli@2.27.12", "", { "dependencies": { "@changesets/apply-release-plan": "^7.0.8", "@changesets/assemble-release-plan": "^6.0.5", "@changesets/changelog-git": "^0.2.0", "@changesets/config": "^3.0.5", "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.2", "@changesets/get-release-plan": "^4.0.6", "@changesets/git": "^3.0.2", "@changesets/logger": "^0.1.1", "@changesets/pre": "^2.0.1", "@changesets/read": "^0.6.2", "@changesets/should-skip-package": "^0.1.1", "@changesets/types": "^6.0.0", "@changesets/write": "^0.3.2", "@manypkg/get-packages": "^1.1.3", "ansi-colors": "^4.1.3", "ci-info": "^3.7.0", "enquirer": "^2.4.1", "external-editor": "^3.1.0", "fs-extra": "^7.0.1", "mri": "^1.2.0", "p-limit": "^2.2.0", "package-manager-detector": "^0.2.0", "picocolors": "^1.1.0", "resolve-from": "^5.0.0", "semver": "^7.5.3", "spawndamnit": "^3.0.1", "term-size": "^2.1.0" }, "bin": { "changeset": "bin.js" } }, "sha512-9o3fOfHYOvBnyEn0mcahB7wzaA3P4bGJf8PNqGit5PKaMEFdsRixik+txkrJWd2VX+O6wRFXpxQL8j/1ANKE9g=="], + + "@changesets/config": ["@changesets/config@3.1.1", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.3", "@changesets/logger": "^0.1.1", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1", "micromatch": "^4.0.8" } }, "sha512-bd+3Ap2TKXxljCggI0mKPfzCQKeV/TU4yO2h2C6vAihIo8tzseAn2e7klSuiyYYXvgu53zMN1OeYMIQkaQoWnA=="], + + "@changesets/errors": ["@changesets/errors@0.2.0", "", { "dependencies": { "extendable-error": "^0.1.5" } }, "sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow=="], + + "@changesets/get-dependents-graph": ["@changesets/get-dependents-graph@2.1.3", "", { "dependencies": { "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "picocolors": "^1.1.0", "semver": "^7.5.3" } }, "sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ=="], + + "@changesets/get-github-info": ["@changesets/get-github-info@0.6.0", "", { "dependencies": { "dataloader": "^1.4.0", "node-fetch": "^2.5.0" } }, "sha512-v/TSnFVXI8vzX9/w3DU2Ol+UlTZcu3m0kXTjTT4KlAdwSvwutcByYwyYn9hwerPWfPkT2JfpoX0KgvCEi8Q/SA=="], + + "@changesets/get-release-plan": ["@changesets/get-release-plan@4.0.13", "", { "dependencies": { "@changesets/assemble-release-plan": "^6.0.9", "@changesets/config": "^3.1.1", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.5", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3" } }, "sha512-DWG1pus72FcNeXkM12tx+xtExyH/c9I1z+2aXlObH3i9YA7+WZEVaiHzHl03thpvAgWTRaH64MpfHxozfF7Dvg=="], + + "@changesets/get-version-range-type": ["@changesets/get-version-range-type@0.4.0", "", {}, "sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ=="], + + "@changesets/git": ["@changesets/git@3.0.4", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@manypkg/get-packages": "^1.1.3", "is-subdir": "^1.1.1", "micromatch": "^4.0.8", "spawndamnit": "^3.0.1" } }, "sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw=="], + + "@changesets/logger": ["@changesets/logger@0.1.1", "", { "dependencies": { "picocolors": "^1.1.0" } }, "sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg=="], + + "@changesets/parse": ["@changesets/parse@0.4.1", "", { "dependencies": { "@changesets/types": "^6.1.0", "js-yaml": "^3.13.1" } }, "sha512-iwksMs5Bf/wUItfcg+OXrEpravm5rEd9Bf4oyIPL4kVTmJQ7PNDSd6MDYkpSJR1pn7tz/k8Zf2DhTCqX08Ou+Q=="], + + "@changesets/pre": ["@changesets/pre@2.0.2", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1" } }, "sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug=="], + + "@changesets/read": ["@changesets/read@0.6.5", "", { "dependencies": { "@changesets/git": "^3.0.4", "@changesets/logger": "^0.1.1", "@changesets/parse": "^0.4.1", "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "p-filter": "^2.1.0", "picocolors": "^1.1.0" } }, "sha512-UPzNGhsSjHD3Veb0xO/MwvasGe8eMyNrR/sT9gR8Q3DhOQZirgKhhXv/8hVsI0QpPjR004Z9iFxoJU6in3uGMg=="], + + "@changesets/should-skip-package": ["@changesets/should-skip-package@0.1.2", "", { "dependencies": { "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3" } }, "sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw=="], + + "@changesets/types": ["@changesets/types@6.1.0", "", {}, "sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA=="], + + "@changesets/write": ["@changesets/write@0.3.2", "", { "dependencies": { "@changesets/types": "^6.0.0", "fs-extra": "^7.0.1", "human-id": "^1.0.2", "prettier": "^2.7.1" } }, "sha512-kDxDrPNpUgsjDbWBvUo27PzKX4gqeKOlhibaOXDJA6kuBisGqNHv/HwGJrAu8U/dSf8ZEFIeHIPtvSlZI1kULw=="], + + "@eth-optimism/contracts": ["@eth-optimism/contracts@0.6.0", "", { "dependencies": { "@eth-optimism/core-utils": "0.12.0", "@ethersproject/abstract-provider": "^5.7.0", "@ethersproject/abstract-signer": "^5.7.0" }, "peerDependencies": { "ethers": "^5" } }, "sha512-vQ04wfG9kMf1Fwy3FEMqH2QZbgS0gldKhcBeBUPfO8zu68L61VI97UDXmsMQXzTsEAxK8HnokW3/gosl4/NW3w=="], + + "@eth-optimism/core-utils": ["@eth-optimism/core-utils@0.12.0", "", { "dependencies": { "@ethersproject/abi": "^5.7.0", "@ethersproject/abstract-provider": "^5.7.0", "@ethersproject/address": "^5.7.0", "@ethersproject/bignumber": "^5.7.0", "@ethersproject/bytes": "^5.7.0", "@ethersproject/constants": "^5.7.0", "@ethersproject/contracts": "^5.7.0", "@ethersproject/hash": "^5.7.0", "@ethersproject/keccak256": "^5.7.0", "@ethersproject/properties": "^5.7.0", "@ethersproject/providers": "^5.7.0", "@ethersproject/rlp": "^5.7.0", "@ethersproject/transactions": "^5.7.0", "@ethersproject/web": "^5.7.0", "bufio": "^1.0.7", "chai": "^4.3.4" } }, "sha512-qW+7LZYCz7i8dRa7SRlUKIo1VBU8lvN0HeXCxJR+z+xtMzMQpPds20XJNCMclszxYQHkXY00fOT6GvFw9ZL6nw=="], + + "@ethersproject/abi": ["@ethersproject/abi@5.8.0", "", { "dependencies": { "@ethersproject/address": "^5.8.0", "@ethersproject/bignumber": "^5.8.0", "@ethersproject/bytes": "^5.8.0", "@ethersproject/constants": "^5.8.0", "@ethersproject/hash": "^5.8.0", "@ethersproject/keccak256": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/properties": "^5.8.0", "@ethersproject/strings": "^5.8.0" } }, "sha512-b9YS/43ObplgyV6SlyQsG53/vkSal0MNA1fskSC4mbnCMi8R+NkcH8K9FPYNESf6jUefBUniE4SOKms0E/KK1Q=="], + + "@ethersproject/abstract-provider": ["@ethersproject/abstract-provider@5.8.0", "", { "dependencies": { "@ethersproject/bignumber": "^5.8.0", "@ethersproject/bytes": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/networks": "^5.8.0", "@ethersproject/properties": "^5.8.0", "@ethersproject/transactions": "^5.8.0", "@ethersproject/web": "^5.8.0" } }, "sha512-wC9SFcmh4UK0oKuLJQItoQdzS/qZ51EJegK6EmAWlh+OptpQ/npECOR3QqECd8iGHC0RJb4WKbVdSfif4ammrg=="], + + "@ethersproject/abstract-signer": ["@ethersproject/abstract-signer@5.8.0", "", { "dependencies": { "@ethersproject/abstract-provider": "^5.8.0", "@ethersproject/bignumber": "^5.8.0", "@ethersproject/bytes": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/properties": "^5.8.0" } }, "sha512-N0XhZTswXcmIZQdYtUnd79VJzvEwXQw6PK0dTl9VoYrEBxxCPXqS0Eod7q5TNKRxe1/5WUMuR0u0nqTF/avdCA=="], + + "@ethersproject/address": ["@ethersproject/address@5.8.0", "", { "dependencies": { "@ethersproject/bignumber": "^5.8.0", "@ethersproject/bytes": "^5.8.0", "@ethersproject/keccak256": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/rlp": "^5.8.0" } }, "sha512-GhH/abcC46LJwshoN+uBNoKVFPxUuZm6dA257z0vZkKmU1+t8xTn8oK7B9qrj8W2rFRMch4gbJl6PmVxjxBEBA=="], + + "@ethersproject/base64": ["@ethersproject/base64@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0" } }, "sha512-lN0oIwfkYj9LbPx4xEkie6rAMJtySbpOAFXSDVQaBnAzYfB4X2Qr+FXJGxMoc3Bxp2Sm8OwvzMrywxyw0gLjIQ=="], + + "@ethersproject/basex": ["@ethersproject/basex@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0", "@ethersproject/properties": "^5.8.0" } }, "sha512-PIgTszMlDRmNwW9nhS6iqtVfdTAKosA7llYXNmGPw4YAI1PUyMv28988wAb41/gHF/WqGdoLv0erHaRcHRKW2Q=="], + + "@ethersproject/bignumber": ["@ethersproject/bignumber@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0", "@ethersproject/logger": "^5.8.0", "bn.js": "^5.2.1" } }, "sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA=="], + + "@ethersproject/bytes": ["@ethersproject/bytes@5.8.0", "", { "dependencies": { "@ethersproject/logger": "^5.8.0" } }, "sha512-vTkeohgJVCPVHu5c25XWaWQOZ4v+DkGoC42/TS2ond+PARCxTJvgTFUNDZovyQ/uAQ4EcpqqowKydcdmRKjg7A=="], + + "@ethersproject/constants": ["@ethersproject/constants@5.8.0", "", { "dependencies": { "@ethersproject/bignumber": "^5.8.0" } }, "sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg=="], + + "@ethersproject/contracts": ["@ethersproject/contracts@5.8.0", "", { "dependencies": { "@ethersproject/abi": "^5.8.0", "@ethersproject/abstract-provider": "^5.8.0", "@ethersproject/abstract-signer": "^5.8.0", "@ethersproject/address": "^5.8.0", "@ethersproject/bignumber": "^5.8.0", "@ethersproject/bytes": "^5.8.0", "@ethersproject/constants": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/properties": "^5.8.0", "@ethersproject/transactions": "^5.8.0" } }, "sha512-0eFjGz9GtuAi6MZwhb4uvUM216F38xiuR0yYCjKJpNfSEy4HUM8hvqqBj9Jmm0IUz8l0xKEhWwLIhPgxNY0yvQ=="], + + "@ethersproject/hash": ["@ethersproject/hash@5.8.0", "", { "dependencies": { "@ethersproject/abstract-signer": "^5.8.0", "@ethersproject/address": "^5.8.0", "@ethersproject/base64": "^5.8.0", "@ethersproject/bignumber": "^5.8.0", "@ethersproject/bytes": "^5.8.0", "@ethersproject/keccak256": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/properties": "^5.8.0", "@ethersproject/strings": "^5.8.0" } }, "sha512-ac/lBcTbEWW/VGJij0CNSw/wPcw9bSRgCB0AIBz8CvED/jfvDoV9hsIIiWfvWmFEi8RcXtlNwp2jv6ozWOsooA=="], + + "@ethersproject/hdnode": ["@ethersproject/hdnode@5.8.0", "", { "dependencies": { "@ethersproject/abstract-signer": "^5.8.0", "@ethersproject/basex": "^5.8.0", "@ethersproject/bignumber": "^5.8.0", "@ethersproject/bytes": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/pbkdf2": "^5.8.0", "@ethersproject/properties": "^5.8.0", "@ethersproject/sha2": "^5.8.0", "@ethersproject/signing-key": "^5.8.0", "@ethersproject/strings": "^5.8.0", "@ethersproject/transactions": "^5.8.0", "@ethersproject/wordlists": "^5.8.0" } }, "sha512-4bK1VF6E83/3/Im0ERnnUeWOY3P1BZml4ZD3wcH8Ys0/d1h1xaFt6Zc+Dh9zXf9TapGro0T4wvO71UTCp3/uoA=="], + + "@ethersproject/json-wallets": ["@ethersproject/json-wallets@5.8.0", "", { "dependencies": { "@ethersproject/abstract-signer": "^5.8.0", "@ethersproject/address": "^5.8.0", "@ethersproject/bytes": "^5.8.0", "@ethersproject/hdnode": "^5.8.0", "@ethersproject/keccak256": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/pbkdf2": "^5.8.0", "@ethersproject/properties": "^5.8.0", "@ethersproject/random": "^5.8.0", "@ethersproject/strings": "^5.8.0", "@ethersproject/transactions": "^5.8.0", "aes-js": "3.0.0", "scrypt-js": "3.0.1" } }, "sha512-HxblNck8FVUtNxS3VTEYJAcwiKYsBIF77W15HufqlBF9gGfhmYOJtYZp8fSDZtn9y5EaXTE87zDwzxRoTFk11w=="], + + "@ethersproject/keccak256": ["@ethersproject/keccak256@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0", "js-sha3": "0.8.0" } }, "sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng=="], + + "@ethersproject/logger": ["@ethersproject/logger@5.8.0", "", {}, "sha512-Qe6knGmY+zPPWTC+wQrpitodgBfH7XoceCGL5bJVejmH+yCS3R8jJm8iiWuvWbG76RUmyEG53oqv6GMVWqunjA=="], + + "@ethersproject/networks": ["@ethersproject/networks@5.8.0", "", { "dependencies": { "@ethersproject/logger": "^5.8.0" } }, "sha512-egPJh3aPVAzbHwq8DD7Po53J4OUSsA1MjQp8Vf/OZPav5rlmWUaFLiq8cvQiGK0Z5K6LYzm29+VA/p4RL1FzNg=="], + + "@ethersproject/pbkdf2": ["@ethersproject/pbkdf2@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0", "@ethersproject/sha2": "^5.8.0" } }, "sha512-wuHiv97BrzCmfEaPbUFpMjlVg/IDkZThp9Ri88BpjRleg4iePJaj2SW8AIyE8cXn5V1tuAaMj6lzvsGJkGWskg=="], + + "@ethersproject/properties": ["@ethersproject/properties@5.8.0", "", { "dependencies": { "@ethersproject/logger": "^5.8.0" } }, "sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw=="], + + "@ethersproject/providers": ["@ethersproject/providers@5.8.0", "", { "dependencies": { "@ethersproject/abstract-provider": "^5.8.0", "@ethersproject/abstract-signer": "^5.8.0", "@ethersproject/address": "^5.8.0", "@ethersproject/base64": "^5.8.0", "@ethersproject/basex": "^5.8.0", "@ethersproject/bignumber": "^5.8.0", "@ethersproject/bytes": "^5.8.0", "@ethersproject/constants": "^5.8.0", "@ethersproject/hash": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/networks": "^5.8.0", "@ethersproject/properties": "^5.8.0", "@ethersproject/random": "^5.8.0", "@ethersproject/rlp": "^5.8.0", "@ethersproject/sha2": "^5.8.0", "@ethersproject/strings": "^5.8.0", "@ethersproject/transactions": "^5.8.0", "@ethersproject/web": "^5.8.0", "bech32": "1.1.4", "ws": "8.18.0" } }, "sha512-3Il3oTzEx3o6kzcg9ZzbE+oCZYyY+3Zh83sKkn4s1DZfTUjIegHnN2Cm0kbn9YFy45FDVcuCLLONhU7ny0SsCw=="], + + "@ethersproject/random": ["@ethersproject/random@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0", "@ethersproject/logger": "^5.8.0" } }, "sha512-E4I5TDl7SVqyg4/kkA/qTfuLWAQGXmSOgYyO01So8hLfwgKvYK5snIlzxJMk72IFdG/7oh8yuSqY2KX7MMwg+A=="], + + "@ethersproject/rlp": ["@ethersproject/rlp@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0", "@ethersproject/logger": "^5.8.0" } }, "sha512-LqZgAznqDbiEunaUvykH2JAoXTT9NV0Atqk8rQN9nx9SEgThA/WMx5DnW8a9FOufo//6FZOCHZ+XiClzgbqV9Q=="], + + "@ethersproject/sha2": ["@ethersproject/sha2@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0", "@ethersproject/logger": "^5.8.0", "hash.js": "1.1.7" } }, "sha512-dDOUrXr9wF/YFltgTBYS0tKslPEKr6AekjqDW2dbn1L1xmjGR+9GiKu4ajxovnrDbwxAKdHjW8jNcwfz8PAz4A=="], + + "@ethersproject/signing-key": ["@ethersproject/signing-key@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/properties": "^5.8.0", "bn.js": "^5.2.1", "elliptic": "6.6.1", "hash.js": "1.1.7" } }, "sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w=="], + + "@ethersproject/solidity": ["@ethersproject/solidity@5.8.0", "", { "dependencies": { "@ethersproject/bignumber": "^5.8.0", "@ethersproject/bytes": "^5.8.0", "@ethersproject/keccak256": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/sha2": "^5.8.0", "@ethersproject/strings": "^5.8.0" } }, "sha512-4CxFeCgmIWamOHwYN9d+QWGxye9qQLilpgTU0XhYs1OahkclF+ewO+3V1U0mvpiuQxm5EHHmv8f7ClVII8EHsA=="], + + "@ethersproject/strings": ["@ethersproject/strings@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0", "@ethersproject/constants": "^5.8.0", "@ethersproject/logger": "^5.8.0" } }, "sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg=="], + + "@ethersproject/transactions": ["@ethersproject/transactions@5.8.0", "", { "dependencies": { "@ethersproject/address": "^5.8.0", "@ethersproject/bignumber": "^5.8.0", "@ethersproject/bytes": "^5.8.0", "@ethersproject/constants": "^5.8.0", "@ethersproject/keccak256": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/properties": "^5.8.0", "@ethersproject/rlp": "^5.8.0", "@ethersproject/signing-key": "^5.8.0" } }, "sha512-UglxSDjByHG0TuU17bDfCemZ3AnKO2vYrL5/2n2oXvKzvb7Cz+W9gOWXKARjp2URVwcWlQlPOEQyAviKwT4AHg=="], + + "@ethersproject/units": ["@ethersproject/units@5.8.0", "", { "dependencies": { "@ethersproject/bignumber": "^5.8.0", "@ethersproject/constants": "^5.8.0", "@ethersproject/logger": "^5.8.0" } }, "sha512-lxq0CAnc5kMGIiWW4Mr041VT8IhNM+Pn5T3haO74XZWFulk7wH1Gv64HqE96hT4a7iiNMdOCFEBgaxWuk8ETKQ=="], + + "@ethersproject/wallet": ["@ethersproject/wallet@5.8.0", "", { "dependencies": { "@ethersproject/abstract-provider": "^5.8.0", "@ethersproject/abstract-signer": "^5.8.0", "@ethersproject/address": "^5.8.0", "@ethersproject/bignumber": "^5.8.0", "@ethersproject/bytes": "^5.8.0", "@ethersproject/hash": "^5.8.0", "@ethersproject/hdnode": "^5.8.0", "@ethersproject/json-wallets": "^5.8.0", "@ethersproject/keccak256": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/properties": "^5.8.0", "@ethersproject/random": "^5.8.0", "@ethersproject/signing-key": "^5.8.0", "@ethersproject/transactions": "^5.8.0", "@ethersproject/wordlists": "^5.8.0" } }, "sha512-G+jnzmgg6UxurVKRKvw27h0kvG75YKXZKdlLYmAHeF32TGUzHkOFd7Zn6QHOTYRFWnfjtSSFjBowKo7vfrXzPA=="], + + "@ethersproject/web": ["@ethersproject/web@5.8.0", "", { "dependencies": { "@ethersproject/base64": "^5.8.0", "@ethersproject/bytes": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/properties": "^5.8.0", "@ethersproject/strings": "^5.8.0" } }, "sha512-j7+Ksi/9KfGviws6Qtf9Q7KCqRhpwrYKQPs+JBA/rKVFF/yaWLHJEH3zfVP2plVu+eys0d2DlFmhoQJayFewcw=="], + + "@ethersproject/wordlists": ["@ethersproject/wordlists@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0", "@ethersproject/hash": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/properties": "^5.8.0", "@ethersproject/strings": "^5.8.0" } }, "sha512-2df9bbXicZws2Sb5S6ET493uJ0Z84Fjr3pC4tu/qlnZERibZCeUVuqdtt+7Tv9xxhUxHoIekIA7avrKUWHrezg=="], + + "@humanwhocodes/momoa": ["@humanwhocodes/momoa@2.0.4", "", {}, "sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA=="], + + "@manypkg/find-root": ["@manypkg/find-root@1.1.0", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@types/node": "^12.7.1", "find-up": "^4.1.0", "fs-extra": "^8.1.0" } }, "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA=="], + + "@manypkg/get-packages": ["@manypkg/get-packages@1.1.3", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@changesets/types": "^4.0.1", "@manypkg/find-root": "^1.1.0", "fs-extra": "^8.1.0", "globby": "^11.0.0", "read-yaml-file": "^1.1.0" } }, "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@nomicfoundation/slang": ["@nomicfoundation/slang@0.18.3", "", { "dependencies": { "@bytecodealliance/preview2-shim": "0.17.0" } }, "sha512-YqAWgckqbHM0/CZxi9Nlf4hjk9wUNLC9ngWCWBiqMxPIZmzsVKYuChdlrfeBPQyvQQBoOhbx+7C1005kLVQDZQ=="], + + "@offchainlabs/upgrade-executor": ["@offchainlabs/upgrade-executor@1.1.0-beta.0", "", { "dependencies": { "@openzeppelin/contracts": "4.7.3", "@openzeppelin/contracts-upgradeable": "4.7.3" } }, "sha512-mpn6PHjH/KDDjNX0pXHEKdyv8m6DVGQiI2nGzQn0JbM1nOSHJpWx6fvfjtH7YxHJ6zBZTcsKkqGkFKDtCfoSLw=="], + + "@openzeppelin/contracts": ["@openzeppelin/contracts@5.3.0", "", {}, "sha512-zj/KGoW7zxWUE8qOI++rUM18v+VeLTTzKs/DJFkSzHpQFPD/jKKF0TrMxBfGLl3kpdELCNccvB3zmofSzm4nlA=="], + + "@openzeppelin/contracts-upgradeable": ["@openzeppelin/contracts-upgradeable@5.3.0", "", { "peerDependencies": { "@openzeppelin/contracts": "5.3.0" } }, "sha512-yVzSSyTMWO6rapGI5tuqkcLpcGGXA0UA1vScyV5EhE5yw8By3Ewex9rDUw8lfVw0iTkvR/egjfcW5vpk03lqZg=="], + + "@openzeppelin/upgrades-core": ["@openzeppelin/upgrades-core@1.44.1", "", { "dependencies": { "@nomicfoundation/slang": "^0.18.3", "bignumber.js": "^9.1.2", "cbor": "^10.0.0", "chalk": "^4.1.0", "compare-versions": "^6.0.0", "debug": "^4.1.1", "ethereumjs-util": "^7.0.3", "minimatch": "^9.0.5", "minimist": "^1.2.7", "proper-lockfile": "^4.1.1", "solidity-ast": "^0.4.60" }, "bin": { "openzeppelin-upgrades-core": "dist/cli/cli.js" } }, "sha512-yqvDj7eC7m5kCDgqCxVFgk9sVo9SXP/fQFaExPousNfAJJbX+20l4fKZp17aXbNTpo1g+2205s6cR9VhFFOCaQ=="], + + "@pnpm/config.env-replace": ["@pnpm/config.env-replace@1.1.0", "", {}, "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w=="], + + "@pnpm/network.ca-file": ["@pnpm/network.ca-file@1.0.2", "", { "dependencies": { "graceful-fs": "4.2.10" } }, "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA=="], + + "@pnpm/npm-conf": ["@pnpm/npm-conf@2.3.1", "", { "dependencies": { "@pnpm/config.env-replace": "^1.1.0", "@pnpm/network.ca-file": "^1.0.1", "config-chain": "^1.1.11" } }, "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw=="], + + "@prb/math": ["@prb/math@4.1.0", "", {}, "sha512-ef5Xrlh3BeX4xT5/Wi810dpEPq2bYPndRxgFIaKSU1F/Op/s8af03kyom+mfU7gEpvfIZ46xu8W0duiHplbBMg=="], + + "@sablier/devkit": ["@sablier/devkit@github:sablier-labs/devkit#da0e832", {}, "sablier-labs-devkit-da0e832"], + + "@sablier/evm-utils": ["@sablier/evm-utils@1.0.0", "", { "dependencies": { "@chainlink/contracts": "1.3.0", "@openzeppelin/contracts": "5.3.0", "@openzeppelin/contracts-upgradeable": "5.3.0", "forge-std": "github:foundry-rs/forge-std#v1.10.0" } }, "sha512-GMkIjV0Io0HdIa5o/XaGAcXYqsCpZQZbyXqit9AwMvWKEy4okA0x1mW8k6Gw8UyC2nye9kjKkH9jAstxfRqNeA=="], + + "@scroll-tech/contracts": ["@scroll-tech/contracts@0.1.0", "", {}, "sha512-aBbDOc3WB/WveZdpJYcrfvMYMz7ZTEiW8M9XMJLba8p9FAR5KGYB/cV+8+EUsq3MKt7C1BfR+WnXoTVdvwIY6w=="], + + "@sindresorhus/is": ["@sindresorhus/is@5.6.0", "", {}, "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g=="], + + "@solidity-parser/parser": ["@solidity-parser/parser@0.20.2", "", {}, "sha512-rbu0bzwNvMcwAjH86hiEAcOeRI2EeK8zCkHDrFykh/Al8mvJeFmjy3UrE7GYQjNwOgbGUUtCn5/k8CB8zIu7QA=="], + + "@szmarczak/http-timer": ["@szmarczak/http-timer@5.0.1", "", { "dependencies": { "defer-to-connect": "^2.0.1" } }, "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw=="], + + "@types/bn.js": ["@types/bn.js@5.2.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q=="], + + "@types/http-cache-semantics": ["@types/http-cache-semantics@4.0.4", "", {}, "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA=="], + + "@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], + + "@types/pbkdf2": ["@types/pbkdf2@3.1.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-uRwJqmiXmh9++aSu1VNEn3iIxWOhd8AHXNSdlaLfdAAdSTY9jYVeGWnzejM3dvrkbqE3/hyQkQQ29IFATEGlew=="], + + "@types/secp256k1": ["@types/secp256k1@4.0.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-Rcvjl6vARGAKRO6jHeKMatGrvOMGrR/AR11N1x2LqintPCyDZ7NBhrh238Z2VZc7aM7KIwnFpFQ7fnfK4H/9Qw=="], + + "@yarnpkg/lockfile": ["@yarnpkg/lockfile@1.1.0", "", {}, "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ=="], + + "@zksync/contracts": ["era-contracts@github:matter-labs/era-contracts#446d391", {}, "matter-labs-era-contracts-446d391"], + + "aes-js": ["aes-js@3.0.0", "", {}, "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw=="], + + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "ajv-errors": ["ajv-errors@1.0.1", "", { "peerDependencies": { "ajv": ">=5.0.0" } }, "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ=="], + + "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], + + "ansi-escapes": ["ansi-escapes@7.1.1", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "antlr4": ["antlr4@4.13.2", "", {}, "sha512-QiVbZhyy4xAZ17UPEuG3YTOt8ZaoeOR1CvEAqrEsDBsOqINslaB147i9xqljZqoyf5S+EUlGStaj+t22LT9MOg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], + + "assertion-error": ["assertion-error@1.1.0", "", {}, "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw=="], + + "ast-parents": ["ast-parents@0.0.1", "", {}, "sha512-XHusKxKz3zoYk1ic8Un640joHbFMhbqneyoZfoKnEGtf2ey9Uh/IdpcQplODdO/kENaMIWsD0nJm4+wX3UNLHA=="], + + "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], + + "at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="], + + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "base-x": ["base-x@3.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA=="], + + "bech32": ["bech32@1.1.4", "", {}, "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ=="], + + "better-ajv-errors": ["better-ajv-errors@2.0.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@humanwhocodes/momoa": "^2.0.4", "chalk": "^4.1.2", "jsonpointer": "^5.0.1", "leven": "^3.1.0 < 4" }, "peerDependencies": { "ajv": "4.11.8 - 8" } }, "sha512-1cLrJXEq46n0hjV8dDYwg9LKYjDb3KbeW7nZTv4kvfoDD9c2DXHIE31nxM+Y/cIfXMggLUfmxbm6h/JoM/yotA=="], + + "better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="], + + "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], + + "blakejs": ["blakejs@1.2.1", "", {}, "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ=="], + + "bn.js": ["bn.js@5.2.2", "", {}, "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw=="], + + "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "brorand": ["brorand@1.1.0", "", {}, "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w=="], + + "browserify-aes": ["browserify-aes@1.2.0", "", { "dependencies": { "buffer-xor": "^1.0.3", "cipher-base": "^1.0.0", "create-hash": "^1.1.0", "evp_bytestokey": "^1.0.3", "inherits": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA=="], + + "bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="], + + "bs58check": ["bs58check@2.1.2", "", { "dependencies": { "bs58": "^4.0.0", "create-hash": "^1.1.0", "safe-buffer": "^5.1.2" } }, "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA=="], + + "buffer-xor": ["buffer-xor@1.0.3", "", {}, "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ=="], + + "bufio": ["bufio@1.2.3", "", {}, "sha512-5Tt66bRzYUSlVZatc0E92uDenreJ+DpTBmSAUwL4VSxJn3e6cUyYwx+PoqML0GRZatgA/VX8ybhxItF8InZgqA=="], + + "cacheable-lookup": ["cacheable-lookup@7.0.0", "", {}, "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w=="], + + "cacheable-request": ["cacheable-request@10.2.14", "", { "dependencies": { "@types/http-cache-semantics": "^4.0.2", "get-stream": "^6.0.1", "http-cache-semantics": "^4.1.1", "keyv": "^4.5.3", "mimic-response": "^4.0.0", "normalize-url": "^8.0.0", "responselike": "^3.0.0" } }, "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ=="], + + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "cbor": ["cbor@10.0.11", "", { "dependencies": { "nofilter": "^3.0.2" } }, "sha512-vIwORDd/WyB8Nc23o2zNN5RrtFGlR6Fca61TtjkUXueI3Jf2DOZDl1zsshvBntZ3wZHBM9ztjnkXSmzQDaq3WA=="], + + "chai": ["chai@4.5.0", "", { "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", "deep-eql": "^4.1.3", "get-func-name": "^2.0.2", "loupe": "^2.3.6", "pathval": "^1.1.1", "type-detect": "^4.1.0" } }, "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "chardet": ["chardet@0.7.0", "", {}, "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="], + + "check-error": ["check-error@1.0.3", "", { "dependencies": { "get-func-name": "^2.0.2" } }, "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg=="], + + "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], + + "cipher-base": ["cipher-base@1.0.7", "", { "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1", "to-buffer": "^1.2.2" } }, "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA=="], + + "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + + "cli-truncate": ["cli-truncate@5.1.0", "", { "dependencies": { "slice-ansi": "^7.1.0", "string-width": "^8.0.0" } }, "sha512-7JDGG+4Zp0CsknDCedl0DYdaeOhc46QNpXi3NLQblkZpXXgA6LncLDUUyvrjSvZeF3VRQa+KiMGomazQrC1V8g=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + + "commander": ["commander@14.0.1", "", {}, "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A=="], + + "compare-versions": ["compare-versions@6.1.1", "", {}, "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "config-chain": ["config-chain@1.1.13", "", { "dependencies": { "ini": "^1.3.4", "proto-list": "~1.2.1" } }, "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ=="], + + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + + "cosmiconfig": ["cosmiconfig@8.3.6", "", { "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA=="], + + "create-hash": ["create-hash@1.2.0", "", { "dependencies": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", "md5.js": "^1.3.4", "ripemd160": "^2.0.1", "sha.js": "^2.4.0" } }, "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg=="], + + "create-hmac": ["create-hmac@1.1.7", "", { "dependencies": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", "inherits": "^2.0.1", "ripemd160": "^2.0.0", "safe-buffer": "^5.0.1", "sha.js": "^2.4.8" } }, "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg=="], + + "cross-spawn": ["cross-spawn@6.0.6", "", { "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" } }, "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw=="], + + "dataloader": ["dataloader@1.4.0", "", {}, "sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], + + "deep-eql": ["deep-eql@4.1.4", "", { "dependencies": { "type-detect": "^4.0.0" } }, "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg=="], + + "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + + "defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="], + + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="], + + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], + + "dotenv": ["dotenv@8.6.0", "", {}, "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "elliptic": ["elliptic@6.6.1", "", { "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", "hash.js": "^1.0.0", "hmac-drbg": "^1.0.1", "inherits": "^2.0.4", "minimalistic-assert": "^1.0.1", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], + + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + + "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "ethereum-cryptography": ["ethereum-cryptography@0.1.3", "", { "dependencies": { "@types/pbkdf2": "^3.0.0", "@types/secp256k1": "^4.0.1", "blakejs": "^1.1.0", "browserify-aes": "^1.2.0", "bs58check": "^2.1.2", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", "hash.js": "^1.1.7", "keccak": "^3.0.0", "pbkdf2": "^3.0.17", "randombytes": "^2.1.0", "safe-buffer": "^5.1.2", "scrypt-js": "^3.0.0", "secp256k1": "^4.0.1", "setimmediate": "^1.0.5" } }, "sha512-w8/4x1SGGzc+tO97TASLja6SLd3fRIK2tLVcV2Gx4IB21hE19atll5Cq9o3d0ZmAYC/8aw0ipieTSiekAea4SQ=="], + + "ethereumjs-util": ["ethereumjs-util@7.1.5", "", { "dependencies": { "@types/bn.js": "^5.1.0", "bn.js": "^5.1.2", "create-hash": "^1.1.2", "ethereum-cryptography": "^0.1.3", "rlp": "^2.2.4" } }, "sha512-SDl5kKrQAudFBUe5OJM9Ac6WmMyYmXX/6sTmLZ3ffG2eY6ZIGBes3pEDxNN6V72WyOw4CPD5RomKdsa8DAAwLg=="], + + "ethers": ["ethers@5.8.0", "", { "dependencies": { "@ethersproject/abi": "5.8.0", "@ethersproject/abstract-provider": "5.8.0", "@ethersproject/abstract-signer": "5.8.0", "@ethersproject/address": "5.8.0", "@ethersproject/base64": "5.8.0", "@ethersproject/basex": "5.8.0", "@ethersproject/bignumber": "5.8.0", "@ethersproject/bytes": "5.8.0", "@ethersproject/constants": "5.8.0", "@ethersproject/contracts": "5.8.0", "@ethersproject/hash": "5.8.0", "@ethersproject/hdnode": "5.8.0", "@ethersproject/json-wallets": "5.8.0", "@ethersproject/keccak256": "5.8.0", "@ethersproject/logger": "5.8.0", "@ethersproject/networks": "5.8.0", "@ethersproject/pbkdf2": "5.8.0", "@ethersproject/properties": "5.8.0", "@ethersproject/providers": "5.8.0", "@ethersproject/random": "5.8.0", "@ethersproject/rlp": "5.8.0", "@ethersproject/sha2": "5.8.0", "@ethersproject/signing-key": "5.8.0", "@ethersproject/solidity": "5.8.0", "@ethersproject/strings": "5.8.0", "@ethersproject/transactions": "5.8.0", "@ethersproject/units": "5.8.0", "@ethersproject/wallet": "5.8.0", "@ethersproject/web": "5.8.0", "@ethersproject/wordlists": "5.8.0" } }, "sha512-DUq+7fHrCg1aPDFCHx6UIPb3nmt2XMpM7Y/g2gLhsl3lIBqeAfOJIl1qEvRf2uq3BiKxmh6Fh5pfp2ieyek7Kg=="], + + "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + + "evp_bytestokey": ["evp_bytestokey@1.0.3", "", { "dependencies": { "md5.js": "^1.3.4", "safe-buffer": "^5.1.1" } }, "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA=="], + + "extendable-error": ["extendable-error@0.1.7", "", {}, "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg=="], + + "external-editor": ["external-editor@3.1.0", "", { "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "find-yarn-workspace-root": ["find-yarn-workspace-root@2.0.0", "", { "dependencies": { "micromatch": "^4.0.2" } }, "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ=="], + + "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + + "forge-std": ["forge-std@github:foundry-rs/forge-std#8bbcf6e", {}, "foundry-rs-forge-std-8bbcf6e"], + + "form-data-encoder": ["form-data-encoder@2.1.4", "", {}, "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw=="], + + "fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], + + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], + + "get-func-name": ["get-func-name@2.0.2", "", {}, "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + + "glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "got": ["got@12.6.1", "", { "dependencies": { "@sindresorhus/is": "^5.2.0", "@szmarczak/http-timer": "^5.0.1", "cacheable-lookup": "^7.0.0", "cacheable-request": "^10.2.8", "decompress-response": "^6.0.0", "form-data-encoder": "^2.1.2", "get-stream": "^6.0.1", "http2-wrapper": "^2.1.10", "lowercase-keys": "^3.0.0", "p-cancelable": "^3.0.0", "responselike": "^3.0.0" } }, "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hash-base": ["hash-base@3.1.2", "", { "dependencies": { "inherits": "^2.0.4", "readable-stream": "^2.3.8", "safe-buffer": "^5.2.1", "to-buffer": "^1.2.1" } }, "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg=="], + + "hash.js": ["hash.js@1.1.7", "", { "dependencies": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" } }, "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "hmac-drbg": ["hmac-drbg@1.0.1", "", { "dependencies": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg=="], + + "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], + + "http2-wrapper": ["http2-wrapper@2.2.1", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" } }, "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ=="], + + "human-id": ["human-id@1.0.2", "", {}, "sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw=="], + + "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], + + "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + + "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + + "is-ci": ["is-ci@2.0.0", "", { "dependencies": { "ci-info": "^2.0.0" }, "bin": { "is-ci": "bin.js" } }, "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w=="], + + "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-subdir": ["is-subdir@1.2.0", "", { "dependencies": { "better-path-resolve": "1.0.0" } }, "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw=="], + + "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + + "is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="], + + "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + + "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "js-sha3": ["js-sha3@0.8.0", "", {}, "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="], + + "keccak": ["keccak@3.0.4", "", { "dependencies": { "node-addon-api": "^2.0.0", "node-gyp-build": "^4.2.0", "readable-stream": "^3.6.0" } }, "sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "klaw-sync": ["klaw-sync@6.0.0", "", { "dependencies": { "graceful-fs": "^4.1.11" } }, "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ=="], + + "latest-version": ["latest-version@7.0.0", "", { "dependencies": { "package-json": "^8.1.0" } }, "sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg=="], + + "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "lint-staged": ["lint-staged@16.2.3", "", { "dependencies": { "commander": "^14.0.1", "listr2": "^9.0.4", "micromatch": "^4.0.8", "nano-spawn": "^1.0.3", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.8.1" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-1OnJEESB9zZqsp61XHH2fvpS1es3hRCxMplF/AJUDa8Ho8VrscYDIuxGrj3m8KPXbcWZ8fT9XTMUhEQmOVKpKw=="], + + "listr2": ["listr2@9.0.4", "", { "dependencies": { "cli-truncate": "^5.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-1wd/kpAdKRLwv7/3OKC8zZ5U8e/fajCfWMxacUvB79S5nLrYGPtUI/8chMQhn3LQjsRVErTb9i1ECAwW0ZIHnQ=="], + + "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + + "lodash.startcase": ["lodash.startcase@4.4.0", "", {}, "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg=="], + + "lodash.truncate": ["lodash.truncate@4.4.2", "", {}, "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw=="], + + "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], + + "loupe": ["loupe@2.3.7", "", { "dependencies": { "get-func-name": "^2.0.1" } }, "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA=="], + + "lowercase-keys": ["lowercase-keys@3.0.0", "", {}, "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "md5.js": ["md5.js@1.3.5", "", { "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1", "safe-buffer": "^5.1.2" } }, "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + + "mimic-response": ["mimic-response@4.0.0", "", {}, "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg=="], + + "minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="], + + "minimalistic-crypto-utils": ["minimalistic-crypto-utils@1.0.1", "", {}, "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg=="], + + "minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nano-spawn": ["nano-spawn@1.0.3", "", {}, "sha512-jtpsQDetTnvS2Ts1fiRdci5rx0VYws5jGyC+4IYOTnIQ/wwdf6JdomlHBwqC3bJYOvaKu0C2GSZ1A60anrYpaA=="], + + "nice-try": ["nice-try@1.0.5", "", {}, "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="], + + "node-addon-api": ["node-addon-api@2.0.2", "", {}, "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA=="], + + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + + "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], + + "nofilter": ["nofilter@3.1.0", "", {}, "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g=="], + + "normalize-url": ["normalize-url@8.1.0", "", {}, "sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + + "open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="], + + "os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="], + + "outdent": ["outdent@0.5.0", "", {}, "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q=="], + + "p-cancelable": ["p-cancelable@3.0.0", "", {}, "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="], + + "p-filter": ["p-filter@2.1.0", "", { "dependencies": { "p-map": "^2.0.0" } }, "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw=="], + + "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "p-map": ["p-map@2.1.0", "", {}, "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="], + + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + + "package-json": ["package-json@8.1.1", "", { "dependencies": { "got": "^12.1.0", "registry-auth-token": "^5.0.1", "registry-url": "^6.0.0", "semver": "^7.3.7" } }, "sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA=="], + + "package-manager-detector": ["package-manager-detector@0.2.11", "", { "dependencies": { "quansync": "^0.2.7" } }, "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "patch-package": ["patch-package@6.5.1", "", { "dependencies": { "@yarnpkg/lockfile": "^1.1.0", "chalk": "^4.1.2", "cross-spawn": "^6.0.5", "find-yarn-workspace-root": "^2.0.0", "fs-extra": "^9.0.0", "is-ci": "^2.0.0", "klaw-sync": "^6.0.0", "minimist": "^1.2.6", "open": "^7.4.2", "rimraf": "^2.6.3", "semver": "^5.6.0", "slash": "^2.0.0", "tmp": "^0.0.33", "yaml": "^1.10.2" }, "bin": { "patch-package": "index.js" } }, "sha512-I/4Zsalfhc6bphmJTlrLoOcAF87jcxko4q0qsv4bGcurbr8IskEOtdnt9iCmsQVGL1B+iUhSQqweyTLJfCF9rA=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + + "path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="], + + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + + "pathval": ["pathval@1.1.1", "", {}, "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ=="], + + "pbkdf2": ["pbkdf2@3.1.5", "", { "dependencies": { "create-hash": "^1.2.0", "create-hmac": "^1.1.7", "ripemd160": "^2.0.3", "safe-buffer": "^5.2.1", "sha.js": "^2.4.12", "to-buffer": "^1.2.1" } }, "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], + + "pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], + + "pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="], + + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], + + "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], + + "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], + + "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], + + "proto-list": ["proto-list@1.2.4", "", {}, "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], + + "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="], + + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], + + "read-yaml-file": ["read-yaml-file@1.1.0", "", { "dependencies": { "graceful-fs": "^4.1.5", "js-yaml": "^3.6.1", "pify": "^4.0.1", "strip-bom": "^3.0.0" } }, "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "registry-auth-token": ["registry-auth-token@5.1.0", "", { "dependencies": { "@pnpm/npm-conf": "^2.1.0" } }, "sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw=="], + + "registry-url": ["registry-url@6.0.1", "", { "dependencies": { "rc": "1.2.8" } }, "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "responselike": ["responselike@3.0.0", "", { "dependencies": { "lowercase-keys": "^3.0.0" } }, "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg=="], + + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + + "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + + "rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="], + + "ripemd160": ["ripemd160@2.0.3", "", { "dependencies": { "hash-base": "^3.1.2", "inherits": "^2.0.4" } }, "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA=="], + + "rlp": ["rlp@2.2.7", "", { "dependencies": { "bn.js": "^5.2.0" }, "bin": { "rlp": "bin/rlp" } }, "sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "scrypt-js": ["scrypt-js@3.0.1", "", {}, "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA=="], + + "secp256k1": ["secp256k1@4.0.4", "", { "dependencies": { "elliptic": "^6.5.7", "node-addon-api": "^5.0.0", "node-gyp-build": "^4.2.0" } }, "sha512-6JfvwvjUOn8F/jUoBY2Q1v5WY5XS+rj8qSe0v8Y4ezH4InLgTEeOOPQsRll9OV429Pvo6BCHGavIyJfr3TAhsw=="], + + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], + + "sha.js": ["sha.js@2.4.12", "", { "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1", "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" } }, "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w=="], + + "shebang-command": ["shebang-command@1.2.0", "", { "dependencies": { "shebang-regex": "^1.0.0" } }, "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg=="], + + "shebang-regex": ["shebang-regex@1.0.0", "", {}, "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "slash": ["slash@2.0.0", "", {}, "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A=="], + + "slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="], + + "solady": ["solady@0.0.208", "", {}, "sha512-JuSgLFa7yR1l6pneJjO6h37ITPYQjN/ExATLgJemWQVwd+th4fBUXFLFUX2KIAdAw/UPrhob7ksVhDETkw7MhA=="], + + "solarray": ["solarray@github:evmcheb/solarray#a547630", {}, "evmcheb-solarray-a547630"], + + "solhint": ["solhint@6.0.1", "", { "dependencies": { "@solidity-parser/parser": "^0.20.2", "ajv": "^6.12.6", "ajv-errors": "^1.0.1", "antlr4": "^4.13.1-patch-1", "ast-parents": "^0.0.1", "better-ajv-errors": "^2.0.2", "chalk": "^4.1.2", "commander": "^10.0.0", "cosmiconfig": "^8.0.0", "fast-diff": "^1.2.0", "glob": "^8.0.3", "ignore": "^5.2.4", "js-yaml": "^4.1.0", "latest-version": "^7.0.0", "lodash": "^4.17.21", "pluralize": "^8.0.0", "semver": "^7.5.2", "table": "^6.8.1", "text-table": "^0.2.0" }, "optionalDependencies": { "prettier": "^2.8.3" }, "bin": { "solhint": "solhint.js" } }, "sha512-Lew5nhmkXqHPybzBzkMzvvWkpOJSSLTkfTZwRriWvfR2naS4YW2PsjVGaoX9tZFmHh7SuS+e2GEGo5FPYYmJ8g=="], + + "solidity-ast": ["solidity-ast@0.4.61", "", {}, "sha512-OYBJYcYyG7gLV0VuXl9CUrvgJXjV/v0XnR4+1YomVe3q+QyENQXJJxAEASUz4vN6lMAl+C8RSRSr5MBAz09f6w=="], + + "spawndamnit": ["spawndamnit@3.0.1", "", { "dependencies": { "cross-spawn": "^7.0.5", "signal-exit": "^4.0.1" } }, "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg=="], + + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + + "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "table": ["table@6.9.0", "", { "dependencies": { "ajv": "^8.0.1", "lodash.truncate": "^4.4.2", "slice-ansi": "^4.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1" } }, "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A=="], + + "term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="], + + "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], + + "tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="], + + "to-buffer": ["to-buffer@1.2.2", "", { "dependencies": { "isarray": "^2.0.5", "safe-buffer": "^5.2.1", "typed-array-buffer": "^1.0.3" } }, "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "type-detect": ["type-detect@4.1.0", "", {}, "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw=="], + + "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], + + "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], + + "which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="], + + "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], + + "yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="], + + "@arbitrum/nitro-contracts/@openzeppelin/contracts": ["@openzeppelin/contracts@4.5.0", "", {}, "sha512-fdkzKPYMjrRiPK6K4y64e6GzULR7R7RwxSigHS8DDp7aWDeoReqsQI+cxHV1UuhAqX69L1lAaWDxenfP+xiqzA=="], + + "@arbitrum/nitro-contracts/@openzeppelin/contracts-upgradeable": ["@openzeppelin/contracts-upgradeable@4.5.2", "", {}, "sha512-xgWZYaPlrEOQo3cBj97Ufiuv79SPd8Brh4GcFYhPgb6WvAq4ppz8dWKL6h+jLAK01rUqMRp/TS9AdXgAeNvCLA=="], + + "@arbitrum/token-bridge-contracts/@openzeppelin/contracts": ["@openzeppelin/contracts@4.8.3", "", {}, "sha512-bQHV8R9Me8IaJoJ2vPG4rXcL7seB7YVuskr4f+f5RyOStSZetwzkWtoqDMl5erkBJy0lDRUnIR2WIkPiC0GJlg=="], + + "@arbitrum/token-bridge-contracts/@openzeppelin/contracts-upgradeable": ["@openzeppelin/contracts-upgradeable@4.8.3", "", {}, "sha512-SXDRl7HKpl2WDoJpn7CK/M9U4Z8gNXDHHChAKh0Iz+Wew3wu6CmFYBeie3je8V0GSXZAIYYwUktSrnW/kwVPtg=="], + + "@chainlink/contracts/@openzeppelin/contracts": ["@openzeppelin/contracts@4.9.3", "", {}, "sha512-He3LieZ1pP2TNt5JbkPA4PNT9WC3gOTOlDcFGJW4Le4QKqwmiNJCRt44APfxMxvq7OugU/cqYuPcSBzOw38DAg=="], + + "@chainlink/contracts/@openzeppelin/contracts-upgradeable": ["@openzeppelin/contracts-upgradeable@4.9.3", "", {}, "sha512-jjaHAVRMrE4UuZNfDwjlLGDxTHWIOwTJS2ldnc278a0gevfXfPr8hxKEVBGFBE96kl2G3VHDZhUimw/+G3TG2A=="], + + "@changesets/apply-release-plan/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], + + "@changesets/apply-release-plan/resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + + "@changesets/cli/resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + + "@changesets/parse/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], + + "@changesets/write/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], + + "@manypkg/find-root/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + + "@manypkg/get-packages/@changesets/types": ["@changesets/types@4.1.0", "", {}, "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw=="], + + "@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + + "@offchainlabs/upgrade-executor/@openzeppelin/contracts": ["@openzeppelin/contracts@4.7.3", "", {}, "sha512-dGRS0agJzu8ybo44pCIf3xBaPQN/65AIXNgK8+4gzKd5kbvlqyxryUYVLJv7fK98Seyd2hDZzVEHSWAh0Bt1Yw=="], + + "@offchainlabs/upgrade-executor/@openzeppelin/contracts-upgradeable": ["@openzeppelin/contracts-upgradeable@4.7.3", "", {}, "sha512-+wuegAMaLcZnLCJIvrVUDzA9z/Wp93f0Dla/4jJvIhijRrPabjQbZe6fWiECLaJyfn5ci9fqf9vTw3xpQOad2A=="], + + "@openzeppelin/upgrades-core/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@pnpm/network.ca-file/graceful-fs": ["graceful-fs@4.2.10", "", {}, "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="], + + "cli-truncate/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], + + "cli-truncate/string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="], + + "cross-spawn/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + + "decompress-response/mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + + "elliptic/bn.js": ["bn.js@4.12.2", "", {}, "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="], + + "globby/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + + "hash-base/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + + "is-ci/ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="], + + "log-update/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], + + "log-update/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + + "patch-package/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + + "patch-package/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + + "patch-package/yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], + + "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "read-yaml-file/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], + + "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "secp256k1/node-addon-api": ["node-addon-api@5.1.0", "", {}, "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="], + + "solhint/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="], + + "solhint/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], + + "spawndamnit/cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "table/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "wrap-ansi/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + + "@changesets/parse/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "cli-truncate/slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "cli-truncate/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + + "cli-truncate/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + + "hash-base/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "hash-base/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "hash-base/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + + "log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "log-update/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + + "log-update/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "patch-package/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + + "patch-package/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "spawndamnit/cross-spawn/path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "spawndamnit/cross-spawn/shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "spawndamnit/cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "table/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.5.0", "", {}, "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg=="], + + "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "spawndamnit/cross-spawn/shebang-command/shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + } +} diff --git a/bun.lockb b/bun.lockb deleted file mode 100755 index af27a299b..000000000 Binary files a/bun.lockb and /dev/null differ diff --git a/codecov.yml b/codecov.yml index 6489219a6..d1701cc1c 100644 --- a/codecov.yml +++ b/codecov.yml @@ -5,7 +5,7 @@ coverage: status: patch: off ignore: - - "script" + - "scripts/solidity" - "src/libraries/NFTSVG.sol" - "src/libraries/SVGElements.sol" - "tests" diff --git a/foundry.toml b/foundry.toml index 34fcee976..d2fee2277 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,20 +1,18 @@ [profile.default] auto_detect_solc = false - bytecode_hash = "none" - evm_version = "shanghai" + bytecode_hash = "ipfs" + evm_version = "shanghai" # needed for greater coverage of EVM chains fs_permissions = [ { access = "read", path = "./out-optimized" }, { access = "read", path = "package.json" }, - { access = "read-write", path = "./benchmark/results" }, - { access = "read-write", path = "./script/"} ] gas_limit = 9223372036854775807 optimizer = true - optimizer_runs = 570 + optimizer_runs = 500 out = "out" - script = "script" + script = "scripts/solidity" sender = "0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38" - solc = "0.8.26" + solc = "0.8.29" src = "src" test = "tests" @@ -27,52 +25,18 @@ depth = 20 # Number of calls executed in one run fail_on_revert = true runs = 20 - -# Run only the code inside benchmark directory -[profile.benchmark] - test = "benchmark" + show_metrics = false # Speed up compilation and tests during development [profile.lite] optimizer = false -# Compile only the production code and the test mocks with via IR +# Compile only the production code [profile.optimized] out = "out-optimized" test = "tests/mocks" via_ir = true - -# See "SMTChecker and Formal Verification" in the Solidity docs -[profile.smt] - ignored_error_codes = [ - 7737, # Disable inline assembly warnings - ] - out = "out-optimized" - script = "src" - test = "src" - via_ir = true - -[profile.smt.model_checker] - engine = "chc" # constrained Horn clauses - invariants = ["contract", "reentrancy"] - show_proved_safe = true - show_unproved = true - show_unsupported = true - timeout = 100_000 # in milliseconds, per solving query - targets = [ - "assert", - "constantCondition", - "divByZero", - "outOfBounds", - "overflow", - "underflow", - ] - -[profile.smt.model_checker.contracts] - "src/LockupNFTDescriptor.sol" = ["LockupNFTDescriptor"] - "src/SablierLockup.sol" = ["SablierLockup"] - # Test the optimized contracts without re-compiling them [profile.test-optimized] src = "tests" @@ -95,34 +59,41 @@ tab_width = 4 wrap_comments = true +[lint] + lint_on_build = false + [rpc_endpoints] - arbitrum = "${ARBITRUM_RPC_URL}" - arbitrum_sepolia = "https://arbitrum-sepolia-rpc.publicnode.com" - avalanche = "${AVALANCHE_RPC_URL}" - base = "https://mainnet.base.org" - base_sepolia = "https://sepolia.base.org" - berachain_artio = "https://bartio.rpc.berachain.com/" - blast = "https://rpc.blast.io" - blast_sepolia = "https://sepolia.blast.io" - bnb = "https://bsc-dataseed.binance.org" - core_dao = "https://rpc.coredao.org" - gnosis = "https://rpc.gnosischain.com" + # mainnets + abstract = "https://direct.routeme.sh/rpc/2741/${ROUTEMESH_API_KEY}" + arbitrum = "https://direct.routeme.sh/rpc/42161/${ROUTEMESH_API_KEY}" + avalanche = "https://direct.routeme.sh/rpc/43114/${ROUTEMESH_API_KEY}" + base = "https://direct.routeme.sh/rpc/8453/${ROUTEMESH_API_KEY}" + berachain = "https://direct.routeme.sh/rpc/80094/${ROUTEMESH_API_KEY}" + blast = "https://direct.routeme.sh/rpc/81457/${ROUTEMESH_API_KEY}" + bsc = "https://direct.routeme.sh/rpc/56/${ROUTEMESH_API_KEY}" + chiliz = "https://direct.routeme.sh/rpc/88888/${ROUTEMESH_API_KEY}" + core_dao = "https://direct.routeme.sh/rpc/1116/${ROUTEMESH_API_KEY}" + ethereum = "https://direct.routeme.sh/rpc/1/${ROUTEMESH_API_KEY}" + gnosis = "https://direct.routeme.sh/rpc/100/${ROUTEMESH_API_KEY}" + hyperevm = "https://direct.routeme.sh/rpc/999/${ROUTEMESH_API_KEY}" lightlink = "https://replicator.phoenix.lightlink.io/rpc/v1" - linea = "https://rpc.linea.build" - linea_sepolia = "https://rpc.sepolia.linea.build" - localhost = "http://localhost:8545" - mainnet = "${MAINNET_RPC_URL}" - mode = "https://mainnet.mode.network/" - mode_sepolia = "https://sepolia.mode.network/" + linea = "https://direct.routeme.sh/rpc/59144/${ROUTEMESH_API_KEY}" + mode = "https://direct.routeme.sh/rpc/34443/${ROUTEMESH_API_KEY}" morph = "https://rpc.morphl2.io" - optimism = "${OPTIMISM_RPC_URL}" + optimism = "https://direct.routeme.sh/rpc/10/${ROUTEMESH_API_KEY}" + polygon = "https://direct.routeme.sh/rpc/137/${ROUTEMESH_API_KEY}" + scroll = "https://direct.routeme.sh/rpc/534352/${ROUTEMESH_API_KEY}" + sei = "https://direct.routeme.sh/rpc/1329/${ROUTEMESH_API_KEY}" + sonic = "https://direct.routeme.sh/rpc/146/${ROUTEMESH_API_KEY}" + sophon = "https://direct.routeme.sh/rpc/50104/${ROUTEMESH_API_KEY}" + superseed = "https://direct.routeme.sh/rpc/5330/${ROUTEMESH_API_KEY}" + tangle = "https://direct.routeme.sh/rpc/5845/${ROUTEMESH_API_KEY}" + unichain = "https://direct.routeme.sh/rpc/130/${ROUTEMESH_API_KEY}" + xdc = "https://direct.routeme.sh/rpc/50/${ROUTEMESH_API_KEY}" + zksync = "https://direct.routeme.sh/rpc/324/${ROUTEMESH_API_KEY}" + # testnets + arbitrum_sepolia = "https://sepolia-rollup.arbitrum.io/rpc" + base_sepolia = "https://sepolia.base.org" + mode_sepolia = "https://sepolia.mode.network/" optimism_sepolia = "https://sepolia.optimism.io" - polygon = "${POLYGON_RPC_URL}" - scroll = "https://rpc.scroll.io/" - sei = "https://evm-rpc.sei-apis.com" - sei_testnet = "https://evm-rpc.arctic-1.seinetwork.io" - sepolia = "${SEPOLIA_RPC_URL}" - superseed = "https://mainnet.superseed.xyz" - superseed_sepolia = "https://sepolia.superseed.xyz" - taiko_hekla = "https://rpc.hekla.taiko.xyz" - taiko_mainnet = "https://rpc.mainnet.taiko.xyz" + sepolia = "https://direct.routeme.sh/rpc/11155111/${ROUTEMESH_API_KEY}" diff --git a/justfile b/justfile new file mode 100644 index 000000000..e692ebe02 --- /dev/null +++ b/justfile @@ -0,0 +1,6 @@ +# See https://github.com/sablier-labs/devkit/blob/main/just/evm.just +# Run just --list to see all available commands +import "./node_modules/@sablier/devkit/just/evm.just" + +default: + @just --list \ No newline at end of file diff --git a/package.json b/package.json index 162be4774..b9cebed5b 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "@sablier/lockup", - "description": "Core smart contracts of the Lockup token distribution protocol", - "license": "BUSL-1.1", - "version": "2.0.1", + "description": "EVM smart contracts of the Sablier Lockup token distribution protocol", + "license": "SEE LICENSE IN LICENSE", + "version": "3.0.1", "author": { "name": "Sablier Labs Ltd", "url": "https://sablier.com" @@ -11,39 +11,41 @@ "url": "https://github.com/sablier-labs/lockup/issues" }, "dependencies": { - "@openzeppelin/contracts": "5.0.2", - "@prb/math": "4.1.0" + "@openzeppelin/contracts": "5.3.0", + "@prb/math": "4.1.0", + "@sablier/evm-utils": "1.0.0" }, "devDependencies": { - "forge-std": "github:foundry-rs/forge-std#v1.8.2", - "husky": "^9.1.4", - "lint-staged": "^15.2.8", - "prettier": "^3.3.2", + "@sablier/devkit": "github:sablier-labs/devkit", + "forge-std": "github:foundry-rs/forge-std#v1.10.0", + "husky": "^9.1", + "lint-staged": "^16.1", + "prettier": "^3.5", "solady": "0.0.208", "solarray": "github:evmcheb/solarray#a547630", - "solhint": "^5.0.3" + "solhint": "^6.0" }, "files": [ "artifacts", "src", "tests/utils", "CHANGELOG.md", + "LICENSE.md", + "LICENSE-BUSL.md", "LICENSE-GPL.md" ], "homepage": "https://github.com/sablier-labs/lockup#readme", "keywords": [ - "asset-distribution", - "asset-streaming", "blockchain", "cryptoasset-streaming", "cryptoassets", "ethereum", + "evm", "foundry", "lockup", "money-streaming", "real-time-finance", "sablier", - "sablier-v2", "sablier-lockup", "smart-contracts", "solidity", @@ -59,23 +61,12 @@ "publishConfig": { "access": "public" }, - "repository": "github.com/sablier-labs/lockup", + "repository": { + "type": "git", + "url": "git+https://github.com/sablier-labs/lockup.git" + }, "scripts": { - "benchmark": "bun run build:optimized && FOUNDRY_PROFILE=benchmark forge test --mt testGas && bun run prettier:write", - "build": "forge build", - "build:optimized": "FOUNDRY_PROFILE=optimized forge build", - "build:smt": "FOUNDRY_PROFILE=smt forge build", - "clean": "rm -rf artifacts broadcast cache docs out out-optimized out-svg", - "lint": "bun run lint:sol && bun run prettier:check", - "lint:fix": "bun run lint:sol:fix && forge fmt", - "lint:sol": "forge fmt --check && bun solhint \"{benchmark,script,src,tests}/**/*.sol\"", - "lint:sol:fix": "bun solhint \"{benchmark,script,src,tests}/**/*.sol\" --fix --noPrompt", - "prepack": "bun install && bash ./shell/prepare-artifacts.sh", - "prepare": "husky", - "prettier:check": "prettier --check \"**/*.{json,md,svg,yml}\"", - "prettier:write": "prettier --write \"**/*.{json,md,svg,yml}\"", - "test": "forge test", - "test:lite": "FOUNDRY_PROFILE=lite forge test", - "test:optimized": "bun run build:optimized && FOUNDRY_PROFILE=test-optimized forge test" + "prepack": "bun install --frozen-lockfile && bash ./scripts/bash/prepare-artifacts.sh", + "setup": "husky" } } diff --git a/remappings.txt b/remappings.txt deleted file mode 100644 index 8f1a7a740..000000000 --- a/remappings.txt +++ /dev/null @@ -1,5 +0,0 @@ -@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ -@prb/math/=node_modules/@prb/math/ -forge-std/=node_modules/forge-std/ -solady/=node_modules/solady/ -solarray/=node_modules/solarray/ diff --git a/repomix.config.jsonc b/repomix.config.jsonc new file mode 100644 index 000000000..45016b64b --- /dev/null +++ b/repomix.config.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "https://repomix.com/schemas/latest/schema.json", + "ignore": { + "customPatterns": ["LICENSE-GPL.md", "**/generateSVG.t.sol", "**/tokenURI.t.sol"], + "useDefaultPatterns": true, + "useGitignore": true, + }, + "include": ["."], + "output": { + "filePath": "repomix/output.txt", + "style": "plain", + }, +} diff --git a/script/Base.s.sol b/script/Base.s.sol deleted file mode 100644 index 0283a2042..000000000 --- a/script/Base.s.sol +++ /dev/null @@ -1,154 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// solhint-disable no-console -pragma solidity >=0.8.22 <0.9.0; - -import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; - -import { console2 } from "forge-std/src/console2.sol"; -import { Script } from "forge-std/src/Script.sol"; -import { stdJson } from "forge-std/src/StdJson.sol"; - -contract BaseScript is Script { - using Strings for uint256; - using stdJson for string; - - /// @dev The default value for `maxCountMap`. - uint256 internal constant DEFAULT_MAX_COUNT = 500; - - /// @dev The address of the default Sablier admin. - address internal constant DEFAULT_SABLIER_ADMIN = 0xb1bEF51ebCA01EB12001a639bDBbFF6eEcA12B9F; - - /// @dev The salt used for deterministic deployments. - bytes32 internal immutable SALT; - - /// @dev Included to enable compilation of the script without a $MNEMONIC environment variable. - string internal constant TEST_MNEMONIC = "test test test test test test test test test test test junk"; - - /// @dev Admin address mapped by the chain Id. - mapping(uint256 chainId => address admin) internal adminMap; - - /// @dev The address of the transaction broadcaster. - address internal broadcaster; - - /// @dev Used to derive the broadcaster's address if $EOA is not defined. - string internal mnemonic; - - /// @dev Maximum count for segments and tranches mapped by the chain Id. - mapping(uint256 chainId => uint256 count) internal maxCountMap; - - /// @dev Initializes the transaction broadcaster like this: - /// - /// - If $EOA is defined, use it. - /// - Otherwise, derive the broadcaster address from $MNEMONIC. - /// - If $MNEMONIC is not defined, default to a test mnemonic. - /// - /// The use case for $EOA is to specify the broadcaster key and its address via the command line. - constructor() { - address from = vm.envOr({ name: "EOA", defaultValue: address(0) }); - if (from != address(0)) { - broadcaster = from; - } else { - mnemonic = vm.envOr({ name: "MNEMONIC", defaultValue: TEST_MNEMONIC }); - (broadcaster,) = deriveRememberKey({ mnemonic: mnemonic, index: 0 }); - } - - // Construct the salt for deterministic deployments. - SALT = constructCreate2Salt(); - - // Populate the admin map. - populateAdminMap(); - - // Populate the max count map for segments and tranches. - populateMaxCountMap(); - - // If there is no admin set for a specific chain, use the default Sablier admin. - if (adminMap[block.chainid] == address(0)) { - adminMap[block.chainid] = DEFAULT_SABLIER_ADMIN; - } - - // If there is no maximum value set for a specific chain, use the default value. - if (maxCountMap[block.chainid] == 0) { - maxCountMap[block.chainid] = DEFAULT_MAX_COUNT; - } - } - - modifier broadcast() { - vm.startBroadcast(broadcaster); - _; - vm.stopBroadcast(); - } - - /// @dev The presence of the salt instructs Forge to deploy contracts via this deterministic CREATE2 factory: - /// https://github.com/Arachnid/deterministic-deployment-proxy - /// - /// Notes: - /// - The salt format is "ChainID , Version ". - function constructCreate2Salt() public view returns (bytes32) { - string memory chainId = block.chainid.toString(); - string memory version = getVersion(); - string memory create2Salt = string.concat("ChainID ", chainId, ", Version ", version); - console2.log("The CREATE2 salt is \"%s\"", create2Salt); - return bytes32(abi.encodePacked(create2Salt)); - } - - /// @dev The version is obtained from `package.json`. - function getVersion() internal view returns (string memory) { - string memory json = vm.readFile("package.json"); - return json.readString(".version"); - } - - /// @dev Populates the admin map. The reason the chain IDs configured for the admin map do not match the other - /// maps is that we only have multisigs for the chains listed below, otherwise, the default admin is used.​ - function populateAdminMap() internal { - adminMap[42_161] = 0xF34E41a6f6Ce5A45559B1D3Ee92E141a3De96376; // Arbitrum - adminMap[43_114] = 0x4735517616373c5137dE8bcCDc887637B8ac85Ce; // Avalanche - adminMap[8453] = 0x83A6fA8c04420B3F9C7A4CF1c040b63Fbbc89B66; // Base - adminMap[56] = 0x6666cA940D2f4B65883b454b7Bc7EEB039f64fa3; // BNB - adminMap[100] = 0x72ACB57fa6a8fa768bE44Db453B1CDBa8B12A399; // Gnosis - adminMap[1] = 0x79Fb3e81aAc012c08501f41296CCC145a1E15844; // Mainnet - adminMap[59_144] = 0x72dCfa0483d5Ef91562817C6f20E8Ce07A81319D; // Linea - adminMap[10] = 0x43c76FE8Aec91F63EbEfb4f5d2a4ba88ef880350; // Optimism - adminMap[137] = 0x40A518C5B9c1d3D6d62Ba789501CE4D526C9d9C6; // Polygon - adminMap[534_352] = 0x0F7Ad835235Ede685180A5c611111610813457a9; // Scroll - } - - /// @dev Updates max values for segments and tranches. Values can be updated using the `update-counts.sh` script. - function populateMaxCountMap() internal { - // forgefmt: disable-start - - // Arbitrum chain ID - maxCountMap[42161] = 1090; - - // Avalanche chain ID. - maxCountMap[43114] = 490; - - // Base chain ID. - maxCountMap[8453] = 2030; - - // Blast chain ID. - maxCountMap[81457] = 1020; - - // BNB chain ID. - maxCountMap[56] = 4460; - - // Ethereum chain ID. - maxCountMap[1] = 1020; - - // Gnosis chain ID. - maxCountMap[100] = 560; - - // Optimism chain ID. - maxCountMap[10] = 1020; - - // Polygon chain ID. - maxCountMap[137] = 1020; - - // Scroll chain ID. - maxCountMap[534352] = 320; - - // Sepolia chain ID. - maxCountMap[11155111] = 1020; - - // forgefmt: disable-end - } -} diff --git a/script/DeployDeterministicLockup.s.sol b/script/DeployDeterministicLockup.s.sol deleted file mode 100644 index eec8560cd..000000000 --- a/script/DeployDeterministicLockup.s.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.22 <0.9.0; - -import { ILockupNFTDescriptor } from "../src/interfaces/ILockupNFTDescriptor.sol"; -import { SablierLockup } from "../src/SablierLockup.sol"; -import { BaseScript } from "./Base.s.sol"; - -/// @notice Deploys {SablierLockup} at a deterministic address across chains. -/// @dev Reverts if the contract has already been deployed. -contract DeployDeterministicLockup is BaseScript { - function run( - address initialAdmin, - ILockupNFTDescriptor nftDescriptor - ) - public - broadcast - returns (SablierLockup lockup) - { - lockup = new SablierLockup{ salt: SALT }(initialAdmin, nftDescriptor, maxCountMap[block.chainid]); - } -} diff --git a/script/DeployDeterministicNFTDescriptor.s.sol b/script/DeployDeterministicNFTDescriptor.s.sol deleted file mode 100644 index 892aa49a8..000000000 --- a/script/DeployDeterministicNFTDescriptor.s.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.22 <0.9.0; - -import { LockupNFTDescriptor } from "../src/LockupNFTDescriptor.sol"; - -import { BaseScript } from "./Base.s.sol"; - -/// @dev Deploys {LockupNFTDescriptor} at a deterministic address across chains. -/// @dev Reverts if the contract has already been deployed. -contract DeployDeterministicNFTDescriptor is BaseScript { - function run() public broadcast returns (LockupNFTDescriptor nftDescriptor) { - nftDescriptor = new LockupNFTDescriptor{ salt: SALT }(); - } -} diff --git a/script/DeployDeterministicProtocol.s.sol b/script/DeployDeterministicProtocol.s.sol deleted file mode 100644 index b377f1f0a..000000000 --- a/script/DeployDeterministicProtocol.s.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.22 <0.9.0; - -import { LockupNFTDescriptor } from "../src/LockupNFTDescriptor.sol"; -import { SablierBatchLockup } from "../src/SablierBatchLockup.sol"; -import { SablierLockup } from "../src/SablierLockup.sol"; - -import { BaseScript } from "./Base.s.sol"; - -/// @notice Deploys the Lockup Protocol at deterministic addresses across chains. -contract DeployDeterministicProtocol is BaseScript { - /// @dev Deploys the protocol with the admin set in `adminMap`. - function run() - public - returns (LockupNFTDescriptor nftDescriptor, SablierLockup lockup, SablierBatchLockup batchLockup) - { - address initialAdmin = adminMap[block.chainid]; - (nftDescriptor, lockup, batchLockup) = _run(initialAdmin); - } - - /// @dev Deploys the protocol with the given `initialAdmin`. - function run(address initialAdmin) - public - returns (LockupNFTDescriptor nftDescriptor, SablierLockup lockup, SablierBatchLockup batchLockup) - { - (nftDescriptor, lockup, batchLockup) = _run(initialAdmin); - } - - /// @dev Common logic for the run functions. - function _run(address initialAdmin) - internal - broadcast - returns (LockupNFTDescriptor nftDescriptor, SablierLockup lockup, SablierBatchLockup batchLockup) - { - batchLockup = new SablierBatchLockup{ salt: SALT }(); - nftDescriptor = new LockupNFTDescriptor{ salt: SALT }(); - lockup = new SablierLockup{ salt: SALT }(initialAdmin, nftDescriptor, maxCountMap[block.chainid]); - } -} diff --git a/script/DeployLockup.s.sol b/script/DeployLockup.s.sol deleted file mode 100644 index 25bfe6f68..000000000 --- a/script/DeployLockup.s.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.22 <0.9.0; - -import { ILockupNFTDescriptor } from "../src/interfaces/ILockupNFTDescriptor.sol"; -import { SablierLockup } from "../src/SablierLockup.sol"; - -import { BaseScript } from "./Base.s.sol"; - -/// @notice Deploys {SablierLockup} contract. -contract DeployLockup is BaseScript { - function run( - address initialAdmin, - ILockupNFTDescriptor nftDescriptor - ) - public - broadcast - returns (SablierLockup lockup) - { - lockup = new SablierLockup(initialAdmin, nftDescriptor, maxCountMap[block.chainid]); - } -} diff --git a/script/DeployProtocol.s.sol b/script/DeployProtocol.s.sol deleted file mode 100644 index 017419e79..000000000 --- a/script/DeployProtocol.s.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.22 <0.9.0; - -import { LockupNFTDescriptor } from "../src/LockupNFTDescriptor.sol"; -import { SablierBatchLockup } from "../src/SablierBatchLockup.sol"; -import { SablierLockup } from "../src/SablierLockup.sol"; - -import { BaseScript } from "./Base.s.sol"; - -/// @notice Deploys the Lockup Protocol. -contract DeployProtocol is BaseScript { - /// @dev Deploys the protocol with the admin set in `adminMap`. - function run() - public - returns (LockupNFTDescriptor nftDescriptor, SablierLockup lockup, SablierBatchLockup batchLockup) - { - address initialAdmin = adminMap[block.chainid]; - (nftDescriptor, lockup, batchLockup) = _run(initialAdmin); - } - - /// @dev Deploys the protocol with the given `initialAdmin`. - function run(address initialAdmin) - public - returns (LockupNFTDescriptor nftDescriptor, SablierLockup lockup, SablierBatchLockup batchLockup) - { - (nftDescriptor, lockup, batchLockup) = _run(initialAdmin); - } - - /// @dev Common logic for the run functions. - function _run(address initialAdmin) - internal - broadcast - returns (LockupNFTDescriptor nftDescriptor, SablierLockup lockup, SablierBatchLockup batchLockup) - { - batchLockup = new SablierBatchLockup(); - nftDescriptor = new LockupNFTDescriptor(); - lockup = new SablierLockup(initialAdmin, nftDescriptor, maxCountMap[block.chainid]); - } -} diff --git a/scripts/bash/generate-svg-panoply.sh b/scripts/bash/generate-svg-panoply.sh new file mode 100755 index 000000000..b8b94a798 --- /dev/null +++ b/scripts/bash/generate-svg-panoply.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +# Notes: +# - Generates a panoply of SVGs with different accent colors and card contents. + +# Pre-requisites: +# - foundry (https://getfoundry.sh) + +# Strict mode: https://gist.github.com/vncsna/64825d5609c146e80de8b1fd623011ca +set -euo pipefail + +./scripts/bash/generate-svg.sh 0 "Pending" "100" 5 +./scripts/bash/generate-svg.sh 0 "Pending" "100" 21 +./scripts/bash/generate-svg.sh 0 "Pending" "100" 565 + +./scripts/bash/generate-svg.sh 0 "Canceled" "100" 3 +./scripts/bash/generate-svg.sh 0 "Canceled" "100" 3 +./scripts/bash/generate-svg.sh 144 "Canceled" "29.81K" 24 +./scripts/bash/generate-svg.sh 7231 "Canceled" "421.11K" 24 + +./scripts/bash/generate-svg.sh 15 "Streaming" "86.1K" 0 +./scripts/bash/generate-svg.sh 42 "Streaming" "581" 0 +./scripts/bash/generate-svg.sh 79 "Streaming" "66.01K" 0 +./scripts/bash/generate-svg.sh 399 "Streaming" "314K" 0 +./scripts/bash/generate-svg.sh 800 "Streaming" "50.04K" 0 +./scripts/bash/generate-svg.sh 1030 "Streaming" "48.93M" 1021 +./scripts/bash/generate-svg.sh 4235 "Streaming" "8.91M" 1 +./scripts/bash/generate-svg.sh 5000 "Streaming" "1.5K" 1 +./scripts/bash/generate-svg.sh 7291 "Streaming" "756.12T" 7211 +./scripts/bash/generate-svg.sh 9999 "Streaming" "3.32K" 88 +./scripts/bash/generate-svg.sh 4999 "Streaming" "999.45K" 10000 + +./scripts/bash/generate-svg.sh 10000 "Settled" "1" 892 +./scripts/bash/generate-svg.sh 10000 "Settled" "14.94K" 11 +./scripts/bash/generate-svg.sh 10000 "Settled" "733" 3402 +./scripts/bash/generate-svg.sh 10000 "Settled" "645.01M" 3402 +./scripts/bash/generate-svg.sh 10000 "Settled" "990.12B" 6503 + +./scripts/bash/generate-svg.sh 10000 "Depleted" "1" 892 +./scripts/bash/generate-svg.sh 10000 "Depleted" "79.1B" 892 +./scripts/bash/generate-svg.sh 4972 "Depleted" "29" 3402 +./scripts/bash/generate-svg.sh 744 "Depleted" "343.01K" 3402 +./scripts/bash/generate-svg.sh 10000 "Depleted" "84.1M" 6503 diff --git a/shell/generate-svg.sh b/scripts/bash/generate-svg.sh similarity index 96% rename from shell/generate-svg.sh rename to scripts/bash/generate-svg.sh index 7748644d6..238e43852 100755 --- a/shell/generate-svg.sh +++ b/scripts/bash/generate-svg.sh @@ -18,7 +18,7 @@ arg_duration=${4:-"91"} # Run the Forge script and extract the SVG from stdout output=$( - forge script script/GenerateSVG.s.sol \ + forge script scripts/solidity/GenerateSVG.s.sol \ --sig "run(uint256,string,string,uint256)" \ "$arg_progress" \ "$arg_status" \ diff --git a/shell/prepare-artifacts.sh b/scripts/bash/prepare-artifacts.sh similarity index 92% rename from shell/prepare-artifacts.sh rename to scripts/bash/prepare-artifacts.sh index 9349325ec..904c2d61d 100755 --- a/shell/prepare-artifacts.sh +++ b/scripts/bash/prepare-artifacts.sh @@ -34,13 +34,13 @@ lockup_interfaces=./artifacts/interfaces cp out-optimized/ISablierBatchLockup.sol/ISablierBatchLockup.json $lockup_interfaces cp out-optimized/ILockupNFTDescriptor.sol/ILockupNFTDescriptor.json $lockup_interfaces cp out-optimized/ISablierLockupRecipient.sol/ISablierLockupRecipient.json $lockup_interfaces -cp out-optimized/ISablierLockupBase.sol/ISablierLockupBase.json $lockup_interfaces +cp out-optimized/ISablierLockupState.sol/ISablierLockupState.json $lockup_interfaces cp out-optimized/ISablierLockup.sol/ISablierLockup.json $lockup_interfaces lockup_libraries=./artifacts/libraries cp out-optimized/Errors.sol/Errors.json $lockup_libraries cp out-optimized/Helpers.sol/Helpers.json $lockup_libraries -cp out-optimized/VestingMath.sol/VestingMath.json $lockup_libraries +cp out-optimized/LockupMath.sol/LockupMath.json $lockup_libraries ################################################ diff --git a/script/DeployBatchLockup.s.sol b/scripts/solidity/DeployBatchLockup.s.sol similarity index 66% rename from script/DeployBatchLockup.s.sol rename to scripts/solidity/DeployBatchLockup.s.sol index e81cf09fb..0aee8698c 100644 --- a/script/DeployBatchLockup.s.sol +++ b/scripts/solidity/DeployBatchLockup.s.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity >=0.8.22 <0.9.0; -import { SablierBatchLockup } from "../src/SablierBatchLockup.sol"; +import { BaseScript } from "@sablier/evm-utils/src/tests/BaseScript.sol"; -import { BaseScript } from "./Base.s.sol"; +import { SablierBatchLockup } from "../../src/SablierBatchLockup.sol"; contract DeployBatchLockup is BaseScript { /// @dev Deploy via Forge. diff --git a/script/DeployDeterministicBatchLockup.s.sol b/scripts/solidity/DeployDeterministicBatchLockup.s.sol similarity index 68% rename from script/DeployDeterministicBatchLockup.s.sol rename to scripts/solidity/DeployDeterministicBatchLockup.s.sol index b5adac045..564c38906 100644 --- a/script/DeployDeterministicBatchLockup.s.sol +++ b/scripts/solidity/DeployDeterministicBatchLockup.s.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity >=0.8.22 <0.9.0; -import { SablierBatchLockup } from "../src/SablierBatchLockup.sol"; +import { BaseScript } from "@sablier/evm-utils/src/tests/BaseScript.sol"; -import { BaseScript } from "./Base.s.sol"; +import { SablierBatchLockup } from "../../src/SablierBatchLockup.sol"; contract DeployDeterministicBatchLockup is BaseScript { /// @dev Deploy via Forge. diff --git a/scripts/solidity/DeployDeterministicLockup.s.sol b/scripts/solidity/DeployDeterministicLockup.s.sol new file mode 100644 index 000000000..f8b1bc849 --- /dev/null +++ b/scripts/solidity/DeployDeterministicLockup.s.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22 <0.9.0; + +import { BaseScript } from "@sablier/evm-utils/src/tests/BaseScript.sol"; + +import { LockupNFTDescriptor } from "../../src/LockupNFTDescriptor.sol"; +import { SablierLockup } from "../../src/SablierLockup.sol"; + +import { LockupNFTDescriptorAddresses } from "./LockupNFTDescriptorAddresses.sol"; + +/// @notice Deploys {SablierLockup} at a deterministic address across chains. +/// @dev Reverts if the contract has already been deployed. +contract DeployDeterministicLockup is BaseScript, LockupNFTDescriptorAddresses { + function run() public broadcast returns (SablierLockup lockup, LockupNFTDescriptor nftDescriptor) { + // If the contract is not already deployed, deploy it. + if (nftDescriptorAddress() == address(0)) { + // Use just the version as salt as we want to deploy at the same address across all chains. + bytes32 nftDescriptorSalt = bytes32(abi.encodePacked(getVersion())); + + nftDescriptor = new LockupNFTDescriptor{ salt: nftDescriptorSalt }(); + } + // Otherwise, use the address of the existing contract. + else { + nftDescriptor = LockupNFTDescriptor(nftDescriptorAddress()); + } + + lockup = new SablierLockup{ salt: SALT }(getComptroller(), address(nftDescriptor)); + } +} diff --git a/scripts/solidity/DeployDeterministicLockupNFTDescriptor.s.sol b/scripts/solidity/DeployDeterministicLockupNFTDescriptor.s.sol new file mode 100644 index 000000000..ba068bfc9 --- /dev/null +++ b/scripts/solidity/DeployDeterministicLockupNFTDescriptor.s.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22 <0.9.0; + +import { BaseScript } from "@sablier/evm-utils/src/tests/BaseScript.sol"; + +import { LockupNFTDescriptor } from "../../src/LockupNFTDescriptor.sol"; + +/// @dev Deploys {LockupNFTDescriptor} at a deterministic address across chains. +/// @dev Reverts if the contract has already been deployed. +contract DeployDeterministicLockupNFTDescriptor is BaseScript { + function run() public broadcast returns (LockupNFTDescriptor nftDescriptor) { + // Use just the version as salt as we want to deploy at the same address across all chains. + bytes32 nftDescriptorSalt = bytes32(abi.encodePacked(getVersion())); + nftDescriptor = new LockupNFTDescriptor{ salt: nftDescriptorSalt }(); + } +} diff --git a/scripts/solidity/DeployDeterministicProtocol.s.sol b/scripts/solidity/DeployDeterministicProtocol.s.sol new file mode 100644 index 000000000..f6ea2eb07 --- /dev/null +++ b/scripts/solidity/DeployDeterministicProtocol.s.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22 <0.9.0; + +import { BaseScript } from "@sablier/evm-utils/src/tests/BaseScript.sol"; + +import { LockupNFTDescriptor } from "../../src/LockupNFTDescriptor.sol"; +import { SablierBatchLockup } from "../../src/SablierBatchLockup.sol"; +import { SablierLockup } from "../../src/SablierLockup.sol"; + +import { LockupNFTDescriptorAddresses } from "./LockupNFTDescriptorAddresses.sol"; + +/// @notice Deploys the Lockup Protocol at deterministic addresses across chains. +contract DeployDeterministicProtocol is BaseScript, LockupNFTDescriptorAddresses { + /// @dev Deploys the protocol. + function run() + public + broadcast + returns (SablierLockup lockup, SablierBatchLockup batchLockup, LockupNFTDescriptor nftDescriptor) + { + // If the contract is not already deployed, deploy it. + if (nftDescriptorAddress() == address(0)) { + // Use just the version as salt as we want to deploy at the same address across all chains. + bytes32 nftDescriptorSalt = bytes32(abi.encodePacked(getVersion())); + + nftDescriptor = new LockupNFTDescriptor{ salt: nftDescriptorSalt }(); + } + // Otherwise, use the address of the existing contract. + else { + nftDescriptor = LockupNFTDescriptor(nftDescriptorAddress()); + } + + batchLockup = new SablierBatchLockup{ salt: SALT }(); + lockup = new SablierLockup{ salt: SALT }(getComptroller(), address(nftDescriptor)); + } +} diff --git a/scripts/solidity/DeployLockup.s.sol b/scripts/solidity/DeployLockup.s.sol new file mode 100644 index 000000000..6b6512830 --- /dev/null +++ b/scripts/solidity/DeployLockup.s.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22 <0.9.0; + +import { BaseScript } from "@sablier/evm-utils/src/tests/BaseScript.sol"; + +import { ILockupNFTDescriptor } from "../../src/interfaces/ILockupNFTDescriptor.sol"; +import { SablierLockup } from "../../src/SablierLockup.sol"; + +/// @notice Deploys {SablierLockup} contract. +contract DeployLockup is BaseScript { + function run(ILockupNFTDescriptor nftDescriptor) public broadcast returns (SablierLockup lockup) { + lockup = new SablierLockup(getComptroller(), address(nftDescriptor)); + } +} diff --git a/script/DeployNFTDescriptor.s.sol b/scripts/solidity/DeployLockupNFTDescriptor.s.sol similarity index 58% rename from script/DeployNFTDescriptor.s.sol rename to scripts/solidity/DeployLockupNFTDescriptor.s.sol index 0e9891f8b..6f3fec6fc 100644 --- a/script/DeployNFTDescriptor.s.sol +++ b/scripts/solidity/DeployLockupNFTDescriptor.s.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity >=0.8.22 <0.9.0; -import { LockupNFTDescriptor } from "../src/LockupNFTDescriptor.sol"; +import { BaseScript } from "@sablier/evm-utils/src/tests/BaseScript.sol"; -import { BaseScript } from "./Base.s.sol"; +import { LockupNFTDescriptor } from "../../src/LockupNFTDescriptor.sol"; /// @notice Deploys {LockupNFTDescriptor} contract. -contract DeployNFTDescriptor is BaseScript { +contract DeployLockupNFTDescriptor is BaseScript { function run() public broadcast returns (LockupNFTDescriptor nftDescriptor) { nftDescriptor = new LockupNFTDescriptor(); } diff --git a/scripts/solidity/DeployProtocol.s.sol b/scripts/solidity/DeployProtocol.s.sol new file mode 100644 index 000000000..870320fee --- /dev/null +++ b/scripts/solidity/DeployProtocol.s.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22 <0.9.0; + +import { BaseScript } from "@sablier/evm-utils/src/tests/BaseScript.sol"; + +import { LockupNFTDescriptor } from "../../src/LockupNFTDescriptor.sol"; +import { SablierBatchLockup } from "../../src/SablierBatchLockup.sol"; +import { SablierLockup } from "../../src/SablierLockup.sol"; + +import { LockupNFTDescriptorAddresses } from "./LockupNFTDescriptorAddresses.sol"; + +/// @notice Deploys the Lockup Protocol. +contract DeployProtocol is BaseScript, LockupNFTDescriptorAddresses { + /// @dev Deploys the protocol. + function run() + public + broadcast + returns (SablierLockup lockup, SablierBatchLockup batchLockup, LockupNFTDescriptor nftDescriptor) + { + // If the contract is not already deployed, deploy it. + if (nftDescriptorAddress() == address(0)) { + // Use just the version as salt as we want to deploy at the same address across all chains. + bytes32 nftDescriptorSalt = bytes32(abi.encodePacked(getVersion())); + + nftDescriptor = new LockupNFTDescriptor{ salt: nftDescriptorSalt }(); + } + // Otherwise, use the address of the existing contract. + else { + nftDescriptor = LockupNFTDescriptor(nftDescriptorAddress()); + } + + batchLockup = new SablierBatchLockup(); + lockup = new SablierLockup(getComptroller(), address(nftDescriptor)); + } +} diff --git a/script/GenerateSVG.s.sol b/scripts/solidity/GenerateSVG.s.sol similarity index 85% rename from script/GenerateSVG.s.sol rename to scripts/solidity/GenerateSVG.s.sol index a69881fcd..0b7471dc7 100644 --- a/script/GenerateSVG.s.sol +++ b/scripts/solidity/GenerateSVG.s.sol @@ -2,10 +2,11 @@ pragma solidity >=0.8.22 <0.9.0; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; -import { NFTSVG } from "./../src/libraries/NFTSVG.sol"; -import { SVGElements } from "./../src/libraries/SVGElements.sol"; -import { LockupNFTDescriptor } from "./../src/LockupNFTDescriptor.sol"; -import { BaseScript } from "././Base.s.sol"; +import { BaseScript } from "@sablier/evm-utils/src/tests/BaseScript.sol"; + +import { NFTSVG } from "./../../src/libraries/NFTSVG.sol"; +import { SVGElements } from "./../../src/libraries/SVGElements.sol"; +import { LockupNFTDescriptor } from "./../../src/LockupNFTDescriptor.sol"; /// @notice Generates an NFT SVG using the user-provided parameters. contract GenerateSVG is BaseScript, LockupNFTDescriptor { diff --git a/script/Init.s.sol b/scripts/solidity/Init.s.sol similarity index 64% rename from script/Init.s.sol rename to scripts/solidity/Init.s.sol index 31aaf0af2..1c72cb521 100644 --- a/script/Init.s.sol +++ b/scripts/solidity/Init.s.sol @@ -3,13 +3,14 @@ pragma solidity >=0.8.22 <0.9.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ud2x18 } from "@prb/math/src/UD2x18.sol"; -import { ud60x18 } from "@prb/math/src/UD60x18.sol"; +import { BaseScript } from "@sablier/evm-utils/src/tests/BaseScript.sol"; import { Solarray } from "solarray/src/Solarray.sol"; -import { ISablierLockup } from "../src/interfaces/ISablierLockup.sol"; -import { Broker, Lockup, LockupDynamic, LockupLinear } from "../src/types/DataTypes.sol"; - -import { BaseScript } from "./Base.s.sol"; +import { ISablierLockup } from "../../src/interfaces/ISablierLockup.sol"; +import { Lockup } from "../../src/types/Lockup.sol"; +import { LockupDynamic } from "../../src/types/LockupDynamic.sol"; +import { LockupLinear } from "../../src/types/LockupLinear.sol"; +import { LockupTranched } from "../../src/types/LockupTranched.sol"; interface IERC20Mint { function mint(address beneficiary, uint256 value) external; @@ -31,14 +32,15 @@ contract Init is BaseScript { // Approve the Lockup contracts to transfer the ERC-20 tokens from the sender. token.approve({ spender: address(lockup), value: type(uint256).max }); - // Create 7 Lockup Linear streams with various amounts and durations. + // Create 7 LL streams with various amounts and durations. // // - 1st stream: meant to be depleted. // - 2th to 4th streams: pending or streaming. // - 5th stream: meant to be renounced. // - 6th stream: meant to canceled. // - 7th stream: meant to be transferred to a third party. - uint128[] memory totalAmounts = Solarray.uint128s(0.1e18, 1e18, 100e18, 1000e18, 5000e18, 25_000e18, 100_000e18); + uint128[] memory depositAmounts = + Solarray.uint128s(0.1e18, 1e18, 100e18, 1000e18, 5000e18, 25_000e18, 100_000e18); uint40[] memory cliffDurations = Solarray.uint40s(0, 0, 0, 0, 24 hours, 1 weeks, 12 weeks); uint40[] memory totalDurations = Solarray.uint40s(1 seconds, 1 hours, 24 hours, 1 weeks, 4 weeks, 12 weeks, 48 weeks); @@ -47,12 +49,11 @@ contract Init is BaseScript { Lockup.CreateWithDurations({ sender: sender, recipient: recipient, - totalAmount: totalAmounts[i], + depositAmount: depositAmounts[i], token: token, cancelable: true, transferable: true, - shape: "Cliff Linear", - broker: Broker(address(0), ud60x18(0)) + shape: "Cliff Linear" }), LockupLinear.UnlockAmounts({ start: 0, cliff: 0 }), LockupLinear.Durations({ cliff: cliffDurations[i], total: totalDurations[i] }) @@ -69,7 +70,7 @@ contract Init is BaseScript { LOCKUP-DYNAMIC //////////////////////////////////////////////////////////////////////////*/ - // Create the default lockupDynamic stream. + // Create the default Lockup Dynamic stream. LockupDynamic.SegmentWithDuration[] memory segments = new LockupDynamic.SegmentWithDuration[](2); segments[0] = LockupDynamic.SegmentWithDuration({ amount: 2500e18, exponent: ud2x18(3.14e18), duration: 1 hours }); @@ -79,14 +80,34 @@ contract Init is BaseScript { Lockup.CreateWithDurations({ sender: sender, recipient: recipient, - totalAmount: 10_000e18, + depositAmount: 10_000e18, token: token, cancelable: true, transferable: true, - shape: "Exponential Dynamic", - broker: Broker(address(0), ud60x18(0)) + shape: "Exponential Dynamic" }), segments ); + + /*////////////////////////////////////////////////////////////////////////// + LOCKUP-TRANCHED + //////////////////////////////////////////////////////////////////////////*/ + + // Create the default Lockup Tranched stream. + LockupTranched.TrancheWithDuration[] memory tranches = new LockupTranched.TrancheWithDuration[](2); + tranches[0] = LockupTranched.TrancheWithDuration({ amount: 1e18, duration: 1 hours }); + tranches[1] = LockupTranched.TrancheWithDuration({ amount: 1.5e18, duration: 1 weeks }); + lockup.createWithDurationsLT( + Lockup.CreateWithDurations({ + sender: sender, + recipient: recipient, + depositAmount: 2.5e18, + token: token, + cancelable: true, + transferable: true, + shape: "Two Tranches" + }), + tranches + ); } } diff --git a/scripts/solidity/LockupNFTDescriptorAddresses.sol b/scripts/solidity/LockupNFTDescriptorAddresses.sol new file mode 100644 index 000000000..df8d6b4ff --- /dev/null +++ b/scripts/solidity/LockupNFTDescriptorAddresses.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable code-complexity +pragma solidity >=0.8.22; + +import { ChainId } from "@sablier/evm-utils/src/tests/ChainId.sol"; + +abstract contract LockupNFTDescriptorAddresses { + /// @notice Returns the LockupNFTDescriptor for the supported chains. + /// @dev If the chain does not have a LockupNFTDescriptor contract, return 0. + function nftDescriptorAddress() public view returns (address addr) { + uint256 chainId = block.chainid; + + // Mainnets. + if (chainId == ChainId.ABSTRACT) return 0x63Ff2E370788C163D5a1909B5FCb299DB327AEF9; + if (chainId == ChainId.ARBITRUM) return 0xd5c6a0Dd2E1822865c308850b8b3E2CcE762D061; + if (chainId == ChainId.AVALANCHE) return 0x906A4BD5dD0EF13654eA29bFD6185d0d64A4b674; + if (chainId == ChainId.BASE) return 0x87e437030b7439150605a641483de98672E26317; + if (chainId == ChainId.BERACHAIN) return 0x3bbE0a21792564604B0fDc00019532Adeffa70eb; + if (chainId == ChainId.BLAST) return 0x959c412d5919b1Ec5D07bee3443ea68c91d57dd7; + if (chainId == ChainId.BSC) return 0x56831a5a932793E02251126831174Ab8Bf2f7695; + if (chainId == ChainId.CHILIZ) return 0x8A96f827082FB349B6e268baa0a7A5584c4Ccda6; + if (chainId == ChainId.COREDAO) return 0xac0cF0F2A96Ed7ec3cfA4D0Be621C67ADC9Dd903; + if (chainId == ChainId.ETHEREUM) return 0xA9dC6878C979B5cc1d98a1803F0664ad725A1f56; + if (chainId == ChainId.GNOSIS) return 0x3140a6900AA2FF3186730741ad8255ee4e6d8Ff1; + if (chainId == ChainId.HYPEREVM) return 0x7263d77e9e872f82A15e5E1a9816440D23758708; + if (chainId == ChainId.LIGHTLINK) return 0xCFB5F90370A7884DEc59C55533782B45FA24f4d1; + if (chainId == ChainId.LINEA) return 0x1514a869D29a8B22961e8F9eBa3DC64000b96BCe; + if (chainId == ChainId.MODE) return 0x64e7879558b6dfE2f510bd4b9Ad196ef0371EAA8; + if (chainId == ChainId.MORPH) return 0x660314f09ac3B65E216B6De288aAdc2599AF14e2; + if (chainId == ChainId.OPTIMISM) return 0x41dBa1AfBB6DF91b3330dc009842327A9858Cbae; + if (chainId == ChainId.POLYGON) return 0xf5e12d0bA25FCa0D738Ec57f149736B2e4C46980; + if (chainId == ChainId.SCROLL) return 0x00Ff6443E902874924dd217c1435e3be04f57431; + if (chainId == ChainId.SEI) return 0xeaFB40669fe3523b073904De76410b46e79a56D7; + if (chainId == ChainId.SONIC) return 0x955dC7A2170782344FA9Ac11De0C0C42C05De2Fc; + if (chainId == ChainId.SOPHON) return 0xAc2E42b520364940c90Ce164412Ca9BA212d014B; + if (chainId == ChainId.SUPERSEED) return 0xa4576b58Ec760A8282D081dc94F3dc716DFc61e9; + if (chainId == ChainId.UNICHAIN) return 0xa5F12D63E18a28C9BE27B6f3d91ce693320067ba; + if (chainId == ChainId.XDC) return 0x4c1311a9d88BFb7023148aB04F7321C2E91c29bf; + if (chainId == ChainId.ZKSYNC) return 0x955dC7A2170782344FA9Ac11De0C0C42C05De2Fc; + + // Testnets. + if (chainId == ChainId.ARBITRUM_SEPOLIA) return 0x8224eb5D7d76B2D7Df43b868D875E79B11500eA8; + if (chainId == ChainId.BASE_SEPOLIA) return 0xCA2593027BA24856c292Fdcb5F987E0c25e755a4; + if (chainId == ChainId.MODE_SEPOLIA) return 0xDd695E927b97460C8d454D8f6d8Cd797Dcf1FCfD; + if (chainId == ChainId.OPTIMISM_SEPOLIA) return 0xDf6163ddD3Ebcb552Cc1379a9c65AFe68683534e; + if (chainId == ChainId.SEPOLIA) return 0x955dC7A2170782344FA9Ac11De0C0C42C05De2Fc; + + // Return address zero for unsupported chain. + return address(0); + } +} diff --git a/shell/generate-svg-panoply.sh b/shell/generate-svg-panoply.sh deleted file mode 100755 index 4f9973ba5..000000000 --- a/shell/generate-svg-panoply.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env bash - -# Notes: -# - Generates a panoply of SVGs with different accent colors and card contents. - -# Pre-requisites: -# - foundry (https://getfoundry.sh) - -# Strict mode: https://gist.github.com/vncsna/64825d5609c146e80de8b1fd623011ca -set -euo pipefail - -./shell/generate-svg.sh 0 "Pending" "100" 5 -./shell/generate-svg.sh 0 "Pending" "100" 21 -./shell/generate-svg.sh 0 "Pending" "100" 565 - -./shell/generate-svg.sh 0 "Canceled" "100" 3 -./shell/generate-svg.sh 0 "Canceled" "100" 3 -./shell/generate-svg.sh 144 "Canceled" "29.81K" 24 -./shell/generate-svg.sh 7231 "Canceled" "421.11K" 24 - -./shell/generate-svg.sh 15 "Streaming" "86.1K" 0 -./shell/generate-svg.sh 42 "Streaming" "581" 0 -./shell/generate-svg.sh 79 "Streaming" "66.01K" 0 -./shell/generate-svg.sh 399 "Streaming" "314K" 0 -./shell/generate-svg.sh 800 "Streaming" "50.04K" 0 -./shell/generate-svg.sh 1030 "Streaming" "48.93M" 1021 -./shell/generate-svg.sh 4235 "Streaming" "8.91M" 1 -./shell/generate-svg.sh 5000 "Streaming" "1.5K" 1 -./shell/generate-svg.sh 7291 "Streaming" "756.12T" 7211 -./shell/generate-svg.sh 9999 "Streaming" "3.32K" 88 -./shell/generate-svg.sh 4999 "Streaming" "999.45K" 10000 - -./shell/generate-svg.sh 10000 "Settled" "1" 892 -./shell/generate-svg.sh 10000 "Settled" "14.94K" 11 -./shell/generate-svg.sh 10000 "Settled" "733" 3402 -./shell/generate-svg.sh 10000 "Settled" "645.01M" 3402 -./shell/generate-svg.sh 10000 "Settled" "990.12B" 6503 - -./shell/generate-svg.sh 10000 "Depleted" "1" 892 -./shell/generate-svg.sh 10000 "Depleted" "79.1B" 892 -./shell/generate-svg.sh 4972 "Depleted" "29" 3402 -./shell/generate-svg.sh 744 "Depleted" "343.01K" 3402 -./shell/generate-svg.sh 10000 "Depleted" "84.1M" 6503 diff --git a/shell/update-counts.sh b/shell/update-counts.sh deleted file mode 100755 index f9c0cd97f..000000000 --- a/shell/update-counts.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env bash - -# Pre-requisites for running this script: -# -# - bun (https://bun.sh) -# - foundry (https://getfoundry.sh) - -# Strict mode -set -euo pipefail - -# Path to the Base Script -BASE_SCRIPT="script/Base.s.sol" - -# Compile the contracts with the optimized profile -bun run build:optimized - -# Generalized function to update counts -update_counts() { - local test_name="Segments" - local map_name="maxCountMap" - echo -e "\nRunning forge test for estimating $test_name..." - local output=$(FOUNDRY_PROFILE=benchmark forge t --mt "test_Estimate${test_name}" -vv) - echo -e "\nParsing output for $test_name..." - - # Define a table with headers. This table is not put in the Solidity script file, - # but is used to be displayed in the terminal. - local table="Category,Chain ID,New Max Count" - - # Parse the output to extract counts and chain IDs - while IFS= read -r line; do - local count=$(echo $line | awk '{print $2}') - local chain_id=$(echo $line | awk '{print $8}') - - # Add the data to the table - table+="\n$map_name,$chain_id,$count" - - # Update the map for each chain ID using sd - sd "$map_name\[$chain_id\] = [0-9]+;" "$map_name[$chain_id] = $count;" $BASE_SCRIPT - done < <(echo "$output" | grep 'count:') - - # Print the table using the column command - echo -e $table | column -t -s ',' -} - -# Call the function with specific parameters for segments and tranches -update_counts - -# Reformat the code with Forge -forge fmt $BASE_SCRIPT - -printf "\n\nAll mappings updated." diff --git a/src/LockupNFTDescriptor.sol b/src/LockupNFTDescriptor.sol index d0885901e..ae33fc35b 100644 --- a/src/LockupNFTDescriptor.sol +++ b/src/LockupNFTDescriptor.sol @@ -10,7 +10,7 @@ import { ILockupNFTDescriptor } from "./interfaces/ILockupNFTDescriptor.sol"; import { ISablierLockup } from "./interfaces/ISablierLockup.sol"; import { NFTSVG } from "./libraries/NFTSVG.sol"; import { SVGElements } from "./libraries/SVGElements.sol"; -import { Lockup } from "./types/DataTypes.sol"; +import { Lockup } from "./types/Lockup.sol"; /* @@ -38,7 +38,7 @@ contract LockupNFTDescriptor is ILockupNFTDescriptor { using Strings for uint256; /*////////////////////////////////////////////////////////////////////////// - USER-FACING CONSTANT FUNCTIONS + USER-FACING READ-ONLY FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ /// @dev Needed to avoid Stack Too Deep. @@ -120,7 +120,7 @@ contract LockupNFTDescriptor is ILockupNFTDescriptor { } /*////////////////////////////////////////////////////////////////////////// - INTERNAL CONSTANT FUNCTIONS + INTERNAL READ-ONLY FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ /// @notice Creates an abbreviated representation of the provided amount, rounded down and prefixed with ">= ". diff --git a/src/SablierBatchLockup.sol b/src/SablierBatchLockup.sol index 699d30143..7df579402 100644 --- a/src/SablierBatchLockup.sol +++ b/src/SablierBatchLockup.sol @@ -7,7 +7,8 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s import { ISablierBatchLockup } from "./interfaces/ISablierBatchLockup.sol"; import { ISablierLockup } from "./interfaces/ISablierLockup.sol"; import { Errors } from "./libraries/Errors.sol"; -import { BatchLockup, Lockup } from "./types/DataTypes.sol"; +import { BatchLockup } from "./types/BatchLockup.sol"; +import { Lockup } from "./types/Lockup.sol"; /* @@ -33,7 +34,7 @@ contract SablierBatchLockup is ISablierBatchLockup { using SafeERC20 for IERC20; /*////////////////////////////////////////////////////////////////////////// - SABLIER-LOCKUP-DYNAMIC + USER-FACING STATE-CHANGING FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ /// @inheritdoc ISablierBatchLockup @@ -58,7 +59,7 @@ contract SablierBatchLockup is ISablierBatchLockup { uint256 transferAmount; for (i = 0; i < batchSize; ++i) { unchecked { - transferAmount += batch[i].totalAmount; + transferAmount += batch[i].depositAmount; } } @@ -73,16 +74,18 @@ contract SablierBatchLockup is ISablierBatchLockup { Lockup.CreateWithDurations({ sender: batch[i].sender, recipient: batch[i].recipient, - totalAmount: batch[i].totalAmount, + depositAmount: batch[i].depositAmount, token: token, cancelable: batch[i].cancelable, transferable: batch[i].transferable, - shape: batch[i].shape, - broker: batch[i].broker + shape: batch[i].shape }), batch[i].segmentsWithDuration ); } + + // Log the creation of the batch of streams. + emit ISablierBatchLockup.CreateLockupBatch({ funder: msg.sender, lockup: lockup, streamIds: streamIds }); } /// @inheritdoc ISablierBatchLockup @@ -107,7 +110,7 @@ contract SablierBatchLockup is ISablierBatchLockup { uint256 transferAmount; for (i = 0; i < batchSize; ++i) { unchecked { - transferAmount += batch[i].totalAmount; + transferAmount += batch[i].depositAmount; } } @@ -129,22 +132,20 @@ contract SablierBatchLockup is ISablierBatchLockup { Lockup.CreateWithTimestamps({ sender: batch[i].sender, recipient: batch[i].recipient, - totalAmount: batch[i].totalAmount, + depositAmount: batch[i].depositAmount, token: token, cancelable: batch[i].cancelable, transferable: batch[i].transferable, timestamps: Lockup.Timestamps({ start: batch[i].startTime, end: endTime }), - shape: batch[i].shape, - broker: batch[i].broker + shape: batch[i].shape }), batch[i].segments ); } - } - /*////////////////////////////////////////////////////////////////////////// - SABLIER-LOCKUP-LINEAR - //////////////////////////////////////////////////////////////////////////*/ + // Log the creation of the batch of streams. + emit ISablierBatchLockup.CreateLockupBatch({ funder: msg.sender, lockup: lockup, streamIds: streamIds }); + } /// @inheritdoc ISablierBatchLockup function createWithDurationsLL( @@ -168,7 +169,7 @@ contract SablierBatchLockup is ISablierBatchLockup { uint256 transferAmount; for (i = 0; i < batchSize; ++i) { unchecked { - transferAmount += batch[i].totalAmount; + transferAmount += batch[i].depositAmount; } } @@ -183,17 +184,19 @@ contract SablierBatchLockup is ISablierBatchLockup { Lockup.CreateWithDurations({ sender: batch[i].sender, recipient: batch[i].recipient, - totalAmount: batch[i].totalAmount, + depositAmount: batch[i].depositAmount, token: token, cancelable: batch[i].cancelable, transferable: batch[i].transferable, - broker: batch[i].broker, shape: batch[i].shape }), batch[i].unlockAmounts, batch[i].durations ); } + + // Log the creation of the batch of streams. + emit ISablierBatchLockup.CreateLockupBatch({ funder: msg.sender, lockup: lockup, streamIds: streamIds }); } /// @inheritdoc ISablierBatchLockup @@ -218,7 +221,7 @@ contract SablierBatchLockup is ISablierBatchLockup { uint256 transferAmount; for (i = 0; i < batchSize; ++i) { unchecked { - transferAmount += batch[i].totalAmount; + transferAmount += batch[i].depositAmount; } } @@ -233,23 +236,21 @@ contract SablierBatchLockup is ISablierBatchLockup { Lockup.CreateWithTimestamps({ sender: batch[i].sender, recipient: batch[i].recipient, - totalAmount: batch[i].totalAmount, + depositAmount: batch[i].depositAmount, token: token, cancelable: batch[i].cancelable, transferable: batch[i].transferable, timestamps: batch[i].timestamps, - shape: batch[i].shape, - broker: batch[i].broker + shape: batch[i].shape }), batch[i].unlockAmounts, batch[i].cliffTime ); } - } - /*////////////////////////////////////////////////////////////////////////// - SABLIER-LOCKUP-TRANCHED - //////////////////////////////////////////////////////////////////////////*/ + // Log the creation of the batch of streams. + emit ISablierBatchLockup.CreateLockupBatch({ funder: msg.sender, lockup: lockup, streamIds: streamIds }); + } /// @inheritdoc ISablierBatchLockup function createWithDurationsLT( @@ -273,7 +274,7 @@ contract SablierBatchLockup is ISablierBatchLockup { uint256 transferAmount; for (i = 0; i < batchSize; ++i) { unchecked { - transferAmount += batch[i].totalAmount; + transferAmount += batch[i].depositAmount; } } @@ -288,16 +289,18 @@ contract SablierBatchLockup is ISablierBatchLockup { Lockup.CreateWithDurations({ sender: batch[i].sender, recipient: batch[i].recipient, - totalAmount: batch[i].totalAmount, + depositAmount: batch[i].depositAmount, token: token, cancelable: batch[i].cancelable, transferable: batch[i].transferable, - shape: batch[i].shape, - broker: batch[i].broker + shape: batch[i].shape }), batch[i].tranchesWithDuration ); } + + // Log the creation of the batch of streams. + emit ISablierBatchLockup.CreateLockupBatch({ funder: msg.sender, lockup: lockup, streamIds: streamIds }); } /// @inheritdoc ISablierBatchLockup @@ -322,7 +325,7 @@ contract SablierBatchLockup is ISablierBatchLockup { uint256 transferAmount; for (i = 0; i < batchSize; ++i) { unchecked { - transferAmount += batch[i].totalAmount; + transferAmount += batch[i].depositAmount; } } @@ -344,21 +347,23 @@ contract SablierBatchLockup is ISablierBatchLockup { Lockup.CreateWithTimestamps({ sender: batch[i].sender, recipient: batch[i].recipient, - totalAmount: batch[i].totalAmount, + depositAmount: batch[i].depositAmount, token: token, cancelable: batch[i].cancelable, transferable: batch[i].transferable, timestamps: Lockup.Timestamps({ start: batch[i].startTime, end: endTime }), - shape: batch[i].shape, - broker: batch[i].broker + shape: batch[i].shape }), batch[i].tranches ); } + + // Log the creation of the batch of streams. + emit ISablierBatchLockup.CreateLockupBatch({ funder: msg.sender, lockup: lockup, streamIds: streamIds }); } /*////////////////////////////////////////////////////////////////////////// - HELPER FUNCTIONS + INTERNAL STATE-CHANGING FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ /// @dev Helper function to approve a Lockup contract to spend funds from the batchLockup. If the current allowance diff --git a/src/SablierLockup.sol b/src/SablierLockup.sol index f0743c421..b6e4d7315 100644 --- a/src/SablierLockup.sol +++ b/src/SablierLockup.sol @@ -1,17 +1,26 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity >=0.8.22; +import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; - -import { SablierLockupBase } from "./abstracts/SablierLockupBase.sol"; +import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; +import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import { Batch } from "@sablier/evm-utils/src/Batch.sol"; +import { Comptrollerable } from "@sablier/evm-utils/src/Comptrollerable.sol"; +import { ISablierComptroller } from "@sablier/evm-utils/src/interfaces/ISablierComptroller.sol"; + +import { SablierLockupDynamic } from "./abstracts/SablierLockupDynamic.sol"; +import { SablierLockupLinear } from "./abstracts/SablierLockupLinear.sol"; +import { SablierLockupState } from "./abstracts/SablierLockupState.sol"; +import { SablierLockupTranched } from "./abstracts/SablierLockupTranched.sol"; import { ILockupNFTDescriptor } from "./interfaces/ILockupNFTDescriptor.sol"; import { ISablierLockup } from "./interfaces/ISablierLockup.sol"; +import { ISablierLockupRecipient } from "./interfaces/ISablierLockupRecipient.sol"; import { Errors } from "./libraries/Errors.sol"; -import { Helpers } from "./libraries/Helpers.sol"; -import { VestingMath } from "./libraries/VestingMath.sol"; -import { Lockup, LockupDynamic, LockupLinear, LockupTranched } from "./types/DataTypes.sol"; +import { LockupMath } from "./libraries/LockupMath.sol"; +import { Lockup } from "./types/Lockup.sol"; /* @@ -26,526 +35,673 @@ import { Lockup, LockupDynamic, LockupLinear, LockupTranched } from "./types/Dat /// @title SablierLockup /// @notice See the documentation in {ISablierLockup}. -contract SablierLockup is ISablierLockup, SablierLockupBase { +contract SablierLockup is + Batch, // 1 inherited component + Comptrollerable, // 1 inherited component + ERC721, // 6 inherited components + ISablierLockup, // 10 inherited components + SablierLockupDynamic, // 4 inherited components + SablierLockupLinear, // 4 inherited components + SablierLockupTranched // 4 inherited components +{ using SafeERC20 for IERC20; - /*////////////////////////////////////////////////////////////////////////// - STATE VARIABLES - //////////////////////////////////////////////////////////////////////////*/ - - /// @inheritdoc ISablierLockup - uint256 public immutable override MAX_COUNT; - - /// @dev Cliff timestamp mapped by stream IDs. This is used in Lockup Linear models. - mapping(uint256 streamId => uint40 cliffTime) internal _cliffs; - - /// @dev Stream segments mapped by stream IDs. This is used in Lockup Dynamic models. - mapping(uint256 streamId => LockupDynamic.Segment[] segments) internal _segments; - - /// @dev Stream tranches mapped by stream IDs. This is used in Lockup Tranched models. - mapping(uint256 streamId => LockupTranched.Tranche[] tranches) internal _tranches; - - /// @dev Unlock amounts mapped by stream IDs. This is used in Lockup Linear models. - mapping(uint256 streamId => LockupLinear.UnlockAmounts unlockAmounts) internal _unlockAmounts; - /*////////////////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////////////////*/ - /// @param initialAdmin The address of the initial contract admin. + /// @param initialComptroller The address of the initial comptroller contract. /// @param initialNFTDescriptor The address of the NFT descriptor contract. - /// @param maxCount The maximum number of segments and tranched allowed in Lockup Dynamic and Lockup Tranched - /// models, respectively. constructor( - address initialAdmin, - ILockupNFTDescriptor initialNFTDescriptor, - uint256 maxCount + address initialComptroller, + address initialNFTDescriptor ) + Comptrollerable(initialComptroller) ERC721("Sablier Lockup NFT", "SAB-LOCKUP") - SablierLockupBase(initialAdmin, initialNFTDescriptor) - { - MAX_COUNT = maxCount; - nextStreamId = 1; - } + SablierLockupState(initialNFTDescriptor) + { } /*////////////////////////////////////////////////////////////////////////// - USER-FACING CONSTANT FUNCTIONS + USER-FACING READ-ONLY FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ /// @inheritdoc ISablierLockup - function getCliffTime(uint256 streamId) external view override notNull(streamId) returns (uint40 cliffTime) { - if (_streams[streamId].lockupModel != Lockup.Model.LOCKUP_LINEAR) { - revert Errors.SablierLockup_NotExpectedModel(_streams[streamId].lockupModel, Lockup.Model.LOCKUP_LINEAR); - } + function calculateMinFeeWei(uint256 streamId) + external + view + override + notNull(streamId) + returns (uint256 minFeeWei) + { + // Calculate the minimum fee in wei for the stream sender. + minFeeWei = comptroller.calculateMinFeeWeiFor({ + protocol: ISablierComptroller.Protocol.Lockup, + user: _streams[streamId].sender + }); + } + + /// @inheritdoc ISablierLockup + function getRecipient(uint256 streamId) external view override returns (address recipient) { + // Check the stream NFT exists and return the owner, which is the stream's recipient. + recipient = _requireOwned({ tokenId: streamId }); + } - cliffTime = _cliffs[streamId]; + /// @inheritdoc ISablierLockup + function isCold(uint256 streamId) external view override notNull(streamId) returns (bool result) { + Lockup.Status status = _statusOf(streamId); + result = status == Lockup.Status.SETTLED || status == Lockup.Status.CANCELED || status == Lockup.Status.DEPLETED; } /// @inheritdoc ISablierLockup - function getSegments(uint256 streamId) + function isWarm(uint256 streamId) external view override notNull(streamId) returns (bool result) { + Lockup.Status status = _statusOf(streamId); + result = status == Lockup.Status.PENDING || status == Lockup.Status.STREAMING; + } + + /// @inheritdoc ISablierLockup + function refundableAmountOf(uint256 streamId) external view override notNull(streamId) - returns (LockupDynamic.Segment[] memory segments) + returns (uint128 refundableAmount) { - if (_streams[streamId].lockupModel != Lockup.Model.LOCKUP_DYNAMIC) { - revert Errors.SablierLockup_NotExpectedModel(_streams[streamId].lockupModel, Lockup.Model.LOCKUP_DYNAMIC); + // Note that checking for `isCancelable` also checks if the stream `wasCanceled` thanks to the protocol + // invariant that canceled streams are not cancelable anymore. + if (_streams[streamId].isCancelable && !_streams[streamId].isDepleted) { + refundableAmount = _streams[streamId].amounts.deposited - _streamedAmountOf(streamId); } + // Otherwise, the result is implicitly zero. + } - segments = _segments[streamId]; + /// @inheritdoc ISablierLockup + function statusOf(uint256 streamId) external view override notNull(streamId) returns (Lockup.Status status) { + status = _statusOf(streamId); } /// @inheritdoc ISablierLockup - function getTranches(uint256 streamId) + function streamedAmountOf(uint256 streamId) external view override notNull(streamId) - returns (LockupTranched.Tranche[] memory tranches) + returns (uint128 streamedAmount) { - if (_streams[streamId].lockupModel != Lockup.Model.LOCKUP_TRANCHED) { - revert Errors.SablierLockup_NotExpectedModel(_streams[streamId].lockupModel, Lockup.Model.LOCKUP_TRANCHED); - } + streamedAmount = _streamedAmountOf(streamId); + } + + /// @inheritdoc ERC721 + function supportsInterface(bytes4 interfaceId) public view override(IERC165, ERC721) returns (bool) { + // 0x49064906 is the ERC-165 interface ID required by ERC-4906 + return interfaceId == 0x49064906 || super.supportsInterface(interfaceId); + } - tranches = _tranches[streamId]; + /// @inheritdoc ERC721 + function tokenURI(uint256 streamId) public view override(IERC721Metadata, ERC721) returns (string memory uri) { + // Check: the stream NFT exists. + _requireOwned({ tokenId: streamId }); + + // Generate the URI describing the stream NFT. + uri = nftDescriptor.tokenURI({ sablier: this, streamId: streamId }); } /// @inheritdoc ISablierLockup - function getUnlockAmounts(uint256 streamId) + function withdrawableAmountOf(uint256 streamId) external view override notNull(streamId) - returns (LockupLinear.UnlockAmounts memory unlockAmounts) + returns (uint128 withdrawableAmount) { - if (_streams[streamId].lockupModel != Lockup.Model.LOCKUP_LINEAR) { - revert Errors.SablierLockup_NotExpectedModel(_streams[streamId].lockupModel, Lockup.Model.LOCKUP_LINEAR); - } - - unlockAmounts = _unlockAmounts[streamId]; + withdrawableAmount = _withdrawableAmountOf(streamId); } /*////////////////////////////////////////////////////////////////////////// - USER-FACING NON-CONSTANT FUNCTIONS + USER-FACING STATE-CHANGING FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ /// @inheritdoc ISablierLockup - function createWithDurationsLD( - Lockup.CreateWithDurations calldata params, - LockupDynamic.SegmentWithDuration[] calldata segmentsWithDuration - ) - external - payable - override - noDelegateCall - returns (uint256 streamId) - { - // Use the block timestamp as the start time. - uint40 startTime = uint40(block.timestamp); - - // Generate the canonical segments. - LockupDynamic.Segment[] memory segments = Helpers.calculateSegmentTimestamps(segmentsWithDuration, startTime); - - // Declare the timestamps for the stream. - Lockup.Timestamps memory timestamps = - Lockup.Timestamps({ start: startTime, end: segments[segments.length - 1].timestamp }); - - // Checks, Effects and Interactions: create the stream. - streamId = _createLD( - Lockup.CreateWithTimestamps({ - sender: params.sender, - recipient: params.recipient, - totalAmount: params.totalAmount, - token: params.token, - cancelable: params.cancelable, - transferable: params.transferable, - timestamps: timestamps, - shape: params.shape, - broker: params.broker - }), - segments - ); + function allowToHook(address recipient) external override onlyComptroller { + // Check: recipients implements the ERC-165 interface ID required by {ISablierLockupRecipient}. + bytes4 interfaceId = type(ISablierLockupRecipient).interfaceId; + if (!ISablierLockupRecipient(recipient).supportsInterface(interfaceId)) { + revert Errors.SablierLockup_AllowToHookUnsupportedInterface(recipient); + } + + // Effect: put the recipient on the allowlist. + _allowedToHook[recipient] = true; + + // Log the allowlist addition. + emit ISablierLockup.AllowToHook(comptroller, recipient); } /// @inheritdoc ISablierLockup - function createWithDurationsLL( - Lockup.CreateWithDurations calldata params, - LockupLinear.UnlockAmounts calldata unlockAmounts, - LockupLinear.Durations calldata durations - ) - external + function burn(uint256 streamId) external payable override noDelegateCall notNull(streamId) { + // Check: only depleted streams can be burned. + if (!_streams[streamId].isDepleted) { + revert Errors.SablierLockup_StreamNotDepleted(streamId); + } + + // Retrieve the current owner. + address currentRecipient = _ownerOf(streamId); + + // Check: `msg.sender` is either the owner of the NFT or an approved third party. + if (!_isCallerStreamRecipientOrApproved(streamId, currentRecipient)) { + revert Errors.SablierLockup_Unauthorized(streamId, msg.sender); + } + + // Effect: burn the NFT. + _burn({ tokenId: streamId }); + } + + /// @inheritdoc ISablierLockup + function cancel(uint256 streamId) + public payable override noDelegateCall - returns (uint256 streamId) + notNull(streamId) + returns (uint128 refundedAmount) { - // Set the current block timestamp as the stream's start time. - Lockup.Timestamps memory timestamps = Lockup.Timestamps({ start: uint40(block.timestamp), end: 0 }); - - uint40 cliffTime; - - // Calculate the cliff time and the end time. - if (durations.cliff > 0) { - cliffTime = timestamps.start + durations.cliff; - } - timestamps.end = timestamps.start + durations.total; - - // Checks, Effects and Interactions: create the stream. - streamId = _createLL( - Lockup.CreateWithTimestamps({ - sender: params.sender, - recipient: params.recipient, - totalAmount: params.totalAmount, - token: params.token, - cancelable: params.cancelable, - transferable: params.transferable, - timestamps: timestamps, - shape: params.shape, - broker: params.broker - }), - unlockAmounts, - cliffTime - ); + // Check: the stream is neither depleted nor canceled. + if (_streams[streamId].isDepleted) { + revert Errors.SablierLockup_StreamDepleted(streamId); + } else if (_streams[streamId].wasCanceled) { + revert Errors.SablierLockup_StreamCanceled(streamId); + } + + // Check: `msg.sender` is the stream's sender. + if (msg.sender != _streams[streamId].sender) { + revert Errors.SablierLockup_Unauthorized(streamId, msg.sender); + } + + // Checks, Effects and Interactions: cancel the stream. + refundedAmount = _cancel(streamId); } /// @inheritdoc ISablierLockup - function createWithDurationsLT( - Lockup.CreateWithDurations calldata params, - LockupTranched.TrancheWithDuration[] calldata tranchesWithDuration - ) + function cancelMultiple(uint256[] calldata streamIds) external payable override noDelegateCall - returns (uint256 streamId) + returns (uint128[] memory refundedAmounts) { - // Use the block timestamp as the start time. - uint40 startTime = uint40(block.timestamp); - - // Generate the canonical tranches. - LockupTranched.Tranche[] memory tranches = Helpers.calculateTrancheTimestamps(tranchesWithDuration, startTime); - - // Declare the timestamps for the stream. - Lockup.Timestamps memory timestamps = - Lockup.Timestamps({ start: startTime, end: tranches[tranches.length - 1].timestamp }); - - // Checks, Effects and Interactions: create the stream. - streamId = _createLT( - Lockup.CreateWithTimestamps({ - sender: params.sender, - recipient: params.recipient, - totalAmount: params.totalAmount, - token: params.token, - cancelable: params.cancelable, - transferable: params.transferable, - timestamps: timestamps, - shape: params.shape, - broker: params.broker - }), - tranches - ); + uint256 count = streamIds.length; + + // Initialize the refunded amounts array. + refundedAmounts = new uint128[](count); + + // Iterate over the provided array of stream IDs and cancel each stream. + for (uint256 i = 0; i < count; ++i) { + // Checks, Effects and Interactions: cancel the stream using a delegate call to self. + (bool success, bytes memory result) = + address(this).delegatecall(abi.encodeCall(ISablierLockup.cancel, (streamIds[i]))); + + // If there is a revert, log it using an event, and continue with the next stream. + if (!success) { + emit ISablierLockup.InvalidStreamInCancelMultiple(streamIds[i], result); + } + // Otherwise, the call is successful, so insert the refunded amount into the array. + else { + // Update the amounts array. + refundedAmounts[i] = abi.decode(result, (uint128)); + } + } + } + + /// @inheritdoc ISablierLockup + function recover(IERC20 token, address to) external override onlyComptroller { + // If tokens are directly transferred to the contract without using the stream creation functions, the + // ERC-20 balance may be greater than the aggregate amount. + uint256 surplus = token.balanceOf(address(this)) - aggregateAmount[token]; + + // Interaction: transfer the surplus to the provided address. + token.safeTransfer({ to: to, value: surplus }); } /// @inheritdoc ISablierLockup - function createWithTimestampsLD( - Lockup.CreateWithTimestamps calldata params, - LockupDynamic.Segment[] calldata segments + function renounce(uint256 streamId) public payable override noDelegateCall notNull(streamId) { + // Check: the stream is not cold. + Lockup.Status status = _statusOf(streamId); + if (status == Lockup.Status.DEPLETED) { + revert Errors.SablierLockup_StreamDepleted(streamId); + } else if (status == Lockup.Status.CANCELED) { + revert Errors.SablierLockup_StreamCanceled(streamId); + } else if (status == Lockup.Status.SETTLED) { + revert Errors.SablierLockup_StreamSettled(streamId); + } + + // Check: `msg.sender` is the stream's sender. + if (msg.sender != _streams[streamId].sender) { + revert Errors.SablierLockup_Unauthorized(streamId, msg.sender); + } + + // Check: the stream is cancelable. + if (!_streams[streamId].isCancelable) { + revert Errors.SablierLockup_StreamNotCancelable(streamId); + } + + // Effect: renounce the stream by making it not cancelable. + _streams[streamId].isCancelable = false; + + // Log the renouncement. + emit ISablierLockup.RenounceLockupStream(streamId); + } + + /// @inheritdoc ISablierLockup + function setNativeToken(address newNativeToken) external override onlyComptroller { + // Check: native token is not set. + if (nativeToken != address(0)) { + revert Errors.SablierLockup_NativeTokenAlreadySet(nativeToken); + } + + // Effect: set the native token. + nativeToken = newNativeToken; + } + + /// @inheritdoc ISablierLockup + function setNFTDescriptor(ILockupNFTDescriptor newNFTDescriptor) external override onlyComptroller { + // Effect: set the NFT descriptor. + ILockupNFTDescriptor oldNftDescriptor = nftDescriptor; + nftDescriptor = newNFTDescriptor; + + // Log the change of the NFT descriptor. + emit ISablierLockup.SetNFTDescriptor(comptroller, oldNftDescriptor, newNFTDescriptor); + + // Refresh the NFT metadata for all streams. + emit IERC4906.BatchMetadataUpdate({ _fromTokenId: 1, _toTokenId: nextStreamId - 1 }); + } + + /// @inheritdoc ISablierLockup + function withdraw( + uint256 streamId, + address to, + uint128 amount ) - external + public payable override noDelegateCall - returns (uint256 streamId) + notNull(streamId) { - // Checks, Effects and Interactions: create the stream. - streamId = _createLD(params, segments); + // Check: the stream is not depleted. + if (_streams[streamId].isDepleted) { + revert Errors.SablierLockup_StreamDepleted(streamId); + } + + // Check: the withdrawal address is not zero. + if (to == address(0)) { + revert Errors.SablierLockup_WithdrawToZeroAddress(streamId); + } + + // Retrieve the recipient from storage. + address recipient = _ownerOf(streamId); + + // Check: if `msg.sender` is neither the stream's recipient nor an approved third party, the withdrawal address + // must be the recipient. + if (to != recipient && !_isCallerStreamRecipientOrApproved(streamId, recipient)) { + revert Errors.SablierLockup_WithdrawalAddressNotRecipient(streamId, msg.sender, to); + } + + // Check: the withdraw amount is not zero. + if (amount == 0) { + revert Errors.SablierLockup_WithdrawAmountZero(streamId); + } + + // Check: the withdraw amount is not greater than the withdrawable amount. + uint128 withdrawableAmount = _withdrawableAmountOf(streamId); + if (amount > withdrawableAmount) { + revert Errors.SablierLockup_Overdraw(streamId, amount, withdrawableAmount); + } + + // Effects and Interactions: make the withdrawal. + _withdraw(streamId, to, amount); + + // Emit an ERC-4906 event to trigger an update of the NFT metadata. + emit IERC4906.MetadataUpdate({ _tokenId: streamId }); + + // Interaction: if `msg.sender` is not the recipient and the recipient is on the allowlist, run the hook. + if (msg.sender != recipient && _allowedToHook[recipient]) { + bytes4 selector = ISablierLockupRecipient(recipient).onSablierLockupWithdraw({ + streamId: streamId, + caller: msg.sender, + to: to, + amount: amount + }); + + // Check: the recipient's hook returned the correct selector. + if (selector != ISablierLockupRecipient.onSablierLockupWithdraw.selector) { + revert Errors.SablierLockup_InvalidHookSelector(recipient); + } + } + } + + /// @inheritdoc ISablierLockup + function withdrawMax(uint256 streamId, address to) external payable override returns (uint128 withdrawnAmount) { + withdrawnAmount = _withdrawableAmountOf(streamId); + withdraw({ streamId: streamId, to: to, amount: withdrawnAmount }); } /// @inheritdoc ISablierLockup - function createWithTimestampsLL( - Lockup.CreateWithTimestamps calldata params, - LockupLinear.UnlockAmounts calldata unlockAmounts, - uint40 cliffTime + function withdrawMaxAndTransfer( + uint256 streamId, + address newRecipient ) external payable override noDelegateCall - returns (uint256 streamId) + notNull(streamId) + returns (uint128 withdrawnAmount) { - // Checks, Effects and Interactions: create the stream. - streamId = _createLL(params, unlockAmounts, cliffTime); + // Retrieve the current owner. This also checks that the NFT was not burned. + address currentRecipient = _ownerOf(streamId); + + // Check: `msg.sender` is either the stream's recipient or an approved third party. + if (!_isCallerStreamRecipientOrApproved(streamId, currentRecipient)) { + revert Errors.SablierLockup_Unauthorized(streamId, msg.sender); + } + + // Skip the withdrawal if the withdrawable amount is zero. + withdrawnAmount = _withdrawableAmountOf(streamId); + if (withdrawnAmount > 0) { + withdraw({ streamId: streamId, to: currentRecipient, amount: withdrawnAmount }); + } + + // Checks and Effects: transfer the NFT. + _transfer({ from: currentRecipient, to: newRecipient, tokenId: streamId }); } /// @inheritdoc ISablierLockup - function createWithTimestampsLT( - Lockup.CreateWithTimestamps calldata params, - LockupTranched.Tranche[] calldata tranches + function withdrawMultiple( + uint256[] calldata streamIds, + uint128[] calldata amounts ) external payable override noDelegateCall - returns (uint256 streamId) { - // Checks, Effects and Interactions: create the stream. - streamId = _createLT(params, tranches); + // Check: there is an equal number of `streamIds` and `amounts`. + uint256 streamIdsCount = streamIds.length; + uint256 amountsCount = amounts.length; + if (streamIdsCount != amountsCount) { + revert Errors.SablierLockup_WithdrawArrayCountsNotEqual(streamIdsCount, amountsCount); + } + + // Iterate over the provided array of stream IDs and withdraw from each stream to the recipient. + for (uint256 i = 0; i < streamIdsCount; ++i) { + // Checks, Effects and Interactions: withdraw using a delegate call to self. + (bool success, bytes memory result) = address(this).delegatecall( + abi.encodeCall(ISablierLockup.withdraw, (streamIds[i], _ownerOf(streamIds[i]), amounts[i])) + ); + // If there is a revert, log it using an event, and continue with the next stream. + if (!success) { + emit ISablierLockup.InvalidWithdrawalInWithdrawMultiple(streamIds[i], result); + } + } } /*////////////////////////////////////////////////////////////////////////// - INTERNAL CONSTANT FUNCTIONS + INTERNAL READ-ONLY FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @inheritdoc SablierLockupBase - function _calculateStreamedAmount(uint256 streamId) internal view override returns (uint128) { - // Load in memory the parameters used in {VestingMath}. - uint40 blockTimestamp = uint40(block.timestamp); - uint128 depositedAmount = _streams[streamId].amounts.deposited; - Lockup.Model lockupModel = _streams[streamId].lockupModel; - uint128 streamedAmount; - Lockup.Timestamps memory timestamps = - Lockup.Timestamps({ start: _streams[streamId].startTime, end: _streams[streamId].endTime }); - - // Calculate the streamed amount for the Lockup Dynamic model. - if (lockupModel == Lockup.Model.LOCKUP_DYNAMIC) { - streamedAmount = VestingMath.calculateLockupDynamicStreamedAmount({ - depositedAmount: depositedAmount, + /// @inheritdoc SablierLockupState + function _streamedAmountOf(uint256 streamId) internal view override returns (uint128 streamedAmount) { + // Load the stream from storage. + Lockup.Stream memory stream = _streams[streamId]; + + if (stream.isDepleted) { + return stream.amounts.withdrawn; + } else if (stream.wasCanceled) { + return stream.amounts.deposited - stream.amounts.refunded; + } + + // Calculate the streamed amount for the LD model. + if (stream.lockupModel == Lockup.Model.LOCKUP_DYNAMIC) { + streamedAmount = LockupMath.calculateStreamedAmountLD({ + depositedAmount: stream.amounts.deposited, + endTime: stream.endTime, segments: _segments[streamId], - blockTimestamp: blockTimestamp, - timestamps: timestamps, - withdrawnAmount: _streams[streamId].amounts.withdrawn + startTime: stream.startTime, + withdrawnAmount: stream.amounts.withdrawn }); } - // Calculate the streamed amount for the Lockup Linear model. - else if (lockupModel == Lockup.Model.LOCKUP_LINEAR) { - streamedAmount = VestingMath.calculateLockupLinearStreamedAmount({ - depositedAmount: depositedAmount, - blockTimestamp: blockTimestamp, - timestamps: timestamps, + // Calculate the streamed amount for the LL model. + else if (stream.lockupModel == Lockup.Model.LOCKUP_LINEAR) { + streamedAmount = LockupMath.calculateStreamedAmountLL({ cliffTime: _cliffs[streamId], + depositedAmount: stream.amounts.deposited, + endTime: stream.endTime, + startTime: stream.startTime, unlockAmounts: _unlockAmounts[streamId], - withdrawnAmount: _streams[streamId].amounts.withdrawn + withdrawnAmount: stream.amounts.withdrawn }); } - // Calculate the streamed amount for the Lockup Tranched model. - else if (lockupModel == Lockup.Model.LOCKUP_TRANCHED) { - streamedAmount = VestingMath.calculateLockupTranchedStreamedAmount({ - depositedAmount: depositedAmount, - blockTimestamp: blockTimestamp, - timestamps: timestamps, + // Calculate the streamed amount for the LT model. + else if (stream.lockupModel == Lockup.Model.LOCKUP_TRANCHED) { + streamedAmount = LockupMath.calculateStreamedAmountLT({ + depositedAmount: stream.amounts.deposited, + endTime: stream.endTime, + startTime: stream.startTime, tranches: _tranches[streamId] }); } - - return streamedAmount; } /*////////////////////////////////////////////////////////////////////////// - INTERNAL NON-CONSTANT FUNCTIONS + INTERNAL STATE-CHANGING FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @dev Common logic for creating a stream. - /// @return The common parameters emitted in the create event between all Lockup models. + /// @inheritdoc SablierLockupState function _create( + bool cancelable, + uint128 depositAmount, + Lockup.Model lockupModel, + address recipient, + address sender, uint256 streamId, - Lockup.CreateWithTimestamps memory params, - Lockup.CreateAmounts memory createAmounts, - Lockup.Model lockupModel + Lockup.Timestamps memory timestamps, + IERC20 token, + bool transferable ) internal - returns (Lockup.CreateEventCommon memory) + override { // Effect: create the stream. _streams[streamId] = Lockup.Stream({ - sender: params.sender, - startTime: params.timestamps.start, - endTime: params.timestamps.end, - isCancelable: params.cancelable, + sender: sender, + startTime: timestamps.start, + endTime: timestamps.end, + isCancelable: cancelable, wasCanceled: false, - token: params.token, + token: token, isDepleted: false, - isStream: true, - isTransferable: params.transferable, + isTransferable: transferable, lockupModel: lockupModel, - amounts: Lockup.Amounts({ deposited: createAmounts.deposit, withdrawn: 0, refunded: 0 }) + amounts: Lockup.Amounts({ deposited: depositAmount, withdrawn: 0, refunded: 0 }) }); // Effect: mint the NFT to the recipient. - _mint({ to: params.recipient, tokenId: streamId }); + _mint({ to: recipient, tokenId: streamId }); unchecked { // Effect: bump the next stream ID. nextStreamId = streamId + 1; + + // Effect: increase the aggregate amount. + aggregateAmount[token] += depositAmount; } // Interaction: transfer the deposit amount. - params.token.safeTransferFrom({ from: msg.sender, to: address(this), value: createAmounts.deposit }); - - // Interaction: pay the broker fee, if not zero. - if (createAmounts.brokerFee > 0) { - params.token.safeTransferFrom({ from: msg.sender, to: params.broker.account, value: createAmounts.brokerFee }); - } - - return Lockup.CreateEventCommon({ - funder: msg.sender, - sender: params.sender, - recipient: params.recipient, - amounts: createAmounts, - token: params.token, - cancelable: params.cancelable, - transferable: params.transferable, - timestamps: params.timestamps, - shape: params.shape, - broker: params.broker.account - }); + token.safeTransferFrom({ from: msg.sender, to: address(this), value: depositAmount }); } - /// @dev See the documentation for the user-facing functions that call this internal function. - function _createLD( - Lockup.CreateWithTimestamps memory params, - LockupDynamic.Segment[] memory segments - ) - internal - returns (uint256 streamId) - { - // Check: validate the user-provided parameters and segments. - Lockup.CreateAmounts memory createAmounts = Helpers.checkCreateLockupDynamic({ - sender: params.sender, - timestamps: params.timestamps, - totalAmount: params.totalAmount, - segments: segments, - maxCount: MAX_COUNT, - brokerFee: params.broker.fee, - shape: params.shape, - maxBrokerFee: MAX_BROKER_FEE - }); + /// @notice Overrides the {ERC-721._update} function to check that the stream is transferable, and emits an + /// ERC-4906 event. + /// @dev There are two cases when the transferable flag is ignored: + /// - If the current owner is 0, then the update is a mint and is allowed. + /// - If `to` is 0, then the update is a burn and is also allowed. + /// @param to The address of the new recipient of the stream. + /// @param streamId ID of the stream to update. + /// @param auth Optional parameter. If the value is not zero, the overridden implementation will check that + /// `auth` is either the recipient of the stream, or an approved third party. + /// @return The original recipient of the `streamId` before the update. + function _update(address to, uint256 streamId, address auth) internal override returns (address) { + address from = _ownerOf(streamId); + + if (from != address(0) && to != address(0) && !_streams[streamId].isTransferable) { + revert Errors.SablierLockup_NotTransferable(streamId); + } - // Load the stream ID in a variable. - streamId = nextStreamId; + // Emit an ERC-4906 event to trigger an update of the NFT metadata. + emit IERC4906.MetadataUpdate({ _tokenId: streamId }); - // Effect: store the segments. Since Solidity lacks a syntax for copying arrays of structs directly from - // memory to storage, a manual approach is necessary. See https://github.com/ethereum/solidity/issues/12783. - uint256 segmentCount = segments.length; - for (uint256 i = 0; i < segmentCount; ++i) { - _segments[streamId].push(segments[i]); - } + return super._update(to, streamId, auth); + } - // Effect: create the stream, mint the NFT and transfer the deposit amount. - Lockup.CreateEventCommon memory commonParams = _create({ - streamId: streamId, - params: params, - createAmounts: createAmounts, - lockupModel: Lockup.Model.LOCKUP_DYNAMIC - }); + /*////////////////////////////////////////////////////////////////////////// + PRIVATE READ-ONLY FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ - // Log the newly created stream. - emit ISablierLockup.CreateLockupDynamicStream({ - streamId: streamId, - commonParams: commonParams, - segments: segments - }); + /// @notice Checks whether `msg.sender` is the stream's recipient or an approved third party, when the `recipient` + /// is known in advance. + /// @param streamId The stream ID for the query. + /// @param recipient The address of the stream's recipient. + function _isCallerStreamRecipientOrApproved(uint256 streamId, address recipient) private view returns (bool) { + return _isAuthorized({ owner: recipient, spender: msg.sender, tokenId: streamId }); } - /// @dev See the documentation for the user-facing functions that call this internal function. - function _createLL( - Lockup.CreateWithTimestamps memory params, - LockupLinear.UnlockAmounts memory unlockAmounts, - uint40 cliffTime - ) - internal - returns (uint256 streamId) - { - // Check: validate the user-provided parameters and cliff time. - Lockup.CreateAmounts memory createAmounts = Helpers.checkCreateLockupLinear({ - sender: params.sender, - timestamps: params.timestamps, - cliffTime: cliffTime, - totalAmount: params.totalAmount, - unlockAmounts: unlockAmounts, - brokerFee: params.broker.fee, - shape: params.shape, - maxBrokerFee: MAX_BROKER_FEE - }); + /// @dev See the documentation for the user-facing functions that call this private function. + function _withdrawableAmountOf(uint256 streamId) private view returns (uint128) { + return _streamedAmountOf(streamId) - _streams[streamId].amounts.withdrawn; + } + + /*////////////////////////////////////////////////////////////////////////// + PRIVATE STATE-CHANGING FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev See the documentation for the user-facing functions that call this private function. + function _cancel(uint256 streamId) private returns (uint128 senderAmount) { + // Calculate the streamed amount. + uint128 streamedAmount = _streamedAmountOf(streamId); + + // Retrieve the amounts from storage. + Lockup.Amounts memory amounts = _streams[streamId].amounts; - // Load the stream ID in a variable. - streamId = nextStreamId; + // Check: the stream is not settled. + if (streamedAmount >= amounts.deposited) { + revert Errors.SablierLockup_StreamSettled(streamId); + } + + // Check: the stream is cancelable. + if (!_streams[streamId].isCancelable) { + revert Errors.SablierLockup_StreamNotCancelable(streamId); + } - // Effect: set the start unlock amount if it is non-zero. - if (unlockAmounts.start > 0) { - _unlockAmounts[streamId].start = unlockAmounts.start; + // Calculate the sender's amount. + unchecked { + senderAmount = amounts.deposited - streamedAmount; } - // Effect: update cliff time if it is non-zero. - if (cliffTime > 0) { - _cliffs[streamId] = cliffTime; + // Calculate the recipient's amount. + uint128 recipientAmount = streamedAmount - amounts.withdrawn; - // Effect: set the cliff unlock amount if it is non-zero. - if (unlockAmounts.cliff > 0) { - _unlockAmounts[streamId].cliff = unlockAmounts.cliff; - } + // Effect: mark the stream as canceled. + _streams[streamId].wasCanceled = true; + + // Effect: make the stream not cancelable anymore, because a stream can only be canceled once. + _streams[streamId].isCancelable = false; + + // Effect: if there are no tokens left for the recipient to withdraw, mark the stream as depleted. + if (recipientAmount == 0) { + _streams[streamId].isDepleted = true; } - // Effect: create the stream, mint the NFT and transfer the deposit amount. - Lockup.CreateEventCommon memory commonParams = _create({ - streamId: streamId, - params: params, - createAmounts: createAmounts, - lockupModel: Lockup.Model.LOCKUP_LINEAR - }); + // Effect: set the refunded amount. + _streams[streamId].amounts.refunded = senderAmount; - // Log the newly created stream. - emit ISablierLockup.CreateLockupLinearStream({ - streamId: streamId, - commonParams: commonParams, - cliffTime: cliffTime, - unlockAmounts: unlockAmounts - }); + // Retrieve the sender and the recipient from storage. + address sender = _streams[streamId].sender; + address recipient = _ownerOf(streamId); + + // Retrieve the ERC-20 token from storage. + IERC20 token = _streams[streamId].token; + + unchecked { + // Effect: decrease the aggregate amount. + aggregateAmount[token] -= senderAmount; + } + + // Interaction: refund the sender. + token.safeTransfer({ to: sender, value: senderAmount }); + + // Log the cancellation. + emit ISablierLockup.CancelLockupStream(streamId, sender, recipient, token, senderAmount, recipientAmount); + + // Emit an ERC-4906 event to trigger an update of the NFT metadata. + emit IERC4906.MetadataUpdate({ _tokenId: streamId }); + + // Interaction: if the recipient is on the allowlist, run the hook. + if (_allowedToHook[recipient]) { + bytes4 selector = ISablierLockupRecipient(recipient).onSablierLockupCancel({ + streamId: streamId, + sender: sender, + senderAmount: senderAmount, + recipientAmount: recipientAmount + }); + + // Check: the recipient's hook returned the correct selector. + if (selector != ISablierLockupRecipient.onSablierLockupCancel.selector) { + revert Errors.SablierLockup_InvalidHookSelector(recipient); + } + } } - /// @dev See the documentation for the user-facing functions that call this internal function. - function _createLT( - Lockup.CreateWithTimestamps memory params, - LockupTranched.Tranche[] memory tranches - ) - internal - returns (uint256 streamId) - { - // Check: validate the user-provided parameters and tranches. - Lockup.CreateAmounts memory createAmounts = Helpers.checkCreateLockupTranched({ - sender: params.sender, - timestamps: params.timestamps, - totalAmount: params.totalAmount, - tranches: tranches, - maxCount: MAX_COUNT, - brokerFee: params.broker.fee, - shape: params.shape, - maxBrokerFee: MAX_BROKER_FEE + /// @dev See the documentation for the user-facing functions that call this private function. + function _withdraw(uint256 streamId, address to, uint128 amount) private { + // Calculate the minimum fee in wei for the stream sender. + uint256 minFeeWei = comptroller.calculateMinFeeWeiFor({ + protocol: ISablierComptroller.Protocol.Lockup, + user: _streams[streamId].sender }); - // Load the stream ID in a variable. - streamId = nextStreamId; + uint256 feePaid = msg.value; - // Effect: store the tranches. Since Solidity lacks a syntax for copying arrays of structs directly from - // memory to storage, a manual approach is necessary. See https://github.com/ethereum/solidity/issues/12783. - uint256 trancheCount = tranches.length; - for (uint256 i = 0; i < trancheCount; ++i) { - _tranches[streamId].push(tranches[i]); + // Check: fee paid is at least the minimum fee. + if (feePaid < minFeeWei) { + revert Errors.SablierLockup_InsufficientFeePayment(feePaid, minFeeWei); } - // Effect: create the stream, mint the NFT and transfer the deposit amount. - Lockup.CreateEventCommon memory commonParams = _create({ - streamId: streamId, - params: params, - createAmounts: createAmounts, - lockupModel: Lockup.Model.LOCKUP_TRANCHED - }); + // Effect: update the withdrawn amount. + _streams[streamId].amounts.withdrawn = _streams[streamId].amounts.withdrawn + amount; - // Log the newly created stream. - emit ISablierLockup.CreateLockupTranchedStream({ - streamId: streamId, - commonParams: commonParams, - tranches: tranches - }); + // Retrieve the amounts from storage. + Lockup.Amounts memory amounts = _streams[streamId].amounts; + + // Using ">=" instead of "==" for additional safety reasons. In the event of an unforeseen increase in the + // withdrawn amount, the stream will still be marked as depleted. + if (amounts.withdrawn >= amounts.deposited - amounts.refunded) { + // Effect: mark the stream as depleted. + _streams[streamId].isDepleted = true; + + // Effect: make the stream not cancelable anymore, because a depleted stream cannot be canceled. + _streams[streamId].isCancelable = false; + } + + // Retrieve the ERC-20 token from storage. + IERC20 token = _streams[streamId].token; + + unchecked { + // Effect: decrease the aggregate amount. + aggregateAmount[token] -= amount; + } + + // Interaction: perform the ERC-20 transfer. + token.safeTransfer({ to: to, value: amount }); + + // Log the withdrawal. + emit ISablierLockup.WithdrawFromLockupStream(streamId, to, token, amount); } } diff --git a/src/abstracts/Adminable.sol b/src/abstracts/Adminable.sol deleted file mode 100644 index 30108fb5e..000000000 --- a/src/abstracts/Adminable.sol +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.22; - -import { IAdminable } from "../interfaces/IAdminable.sol"; -import { Errors } from "../libraries/Errors.sol"; - -/// @title Adminable -/// @notice See the documentation in {IAdminable}. -abstract contract Adminable is IAdminable { - /*////////////////////////////////////////////////////////////////////////// - STATE VARIABLES - //////////////////////////////////////////////////////////////////////////*/ - - /// @inheritdoc IAdminable - address public override admin; - - /*////////////////////////////////////////////////////////////////////////// - MODIFIERS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Reverts if called by any account other than the admin. - modifier onlyAdmin() { - if (admin != msg.sender) { - revert Errors.CallerNotAdmin({ admin: admin, caller: msg.sender }); - } - _; - } - - /*////////////////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////////////////*/ - - /// @dev Emits a {TransferAdmin} event. - /// @param initialAdmin The address of the initial admin. - constructor(address initialAdmin) { - admin = initialAdmin; - emit TransferAdmin({ oldAdmin: address(0), newAdmin: initialAdmin }); - } - - /*////////////////////////////////////////////////////////////////////////// - USER-FACING NON-CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @inheritdoc IAdminable - function transferAdmin(address newAdmin) public virtual override onlyAdmin { - // Effect: update the admin. - admin = newAdmin; - - // Log the transfer of the admin. - emit IAdminable.TransferAdmin({ oldAdmin: msg.sender, newAdmin: newAdmin }); - } -} diff --git a/src/abstracts/Batch.sol b/src/abstracts/Batch.sol deleted file mode 100644 index 129d108ad..000000000 --- a/src/abstracts/Batch.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// solhint-disable no-inline-assembly -pragma solidity >=0.8.22; - -import { IBatch } from "../interfaces/IBatch.sol"; - -/// @title Batch -/// @notice See the documentation in {IBatch}. -abstract contract Batch is IBatch { - /*////////////////////////////////////////////////////////////////////////// - USER-FACING NON-CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @inheritdoc IBatch - /// @dev Since `msg.value` can be reused across calls, be VERY CAREFUL when using it. Refer to - /// https://paradigm.xyz/2021/08/two-rights-might-make-a-wrong for more information. - function batch(bytes[] calldata calls) external payable override returns (bytes[] memory results) { - uint256 count = calls.length; - results = new bytes[](count); - - for (uint256 i = 0; i < count; ++i) { - (bool success, bytes memory result) = address(this).delegatecall(calls[i]); - - // Check: If the delegatecall failed, load and bubble up the revert data. - if (!success) { - assembly { - // Get the length of the result stored in the first 32 bytes. - let resultSize := mload(result) - - // Forward the pointer by 32 bytes to skip the length argument, and revert with the result. - revert(add(32, result), resultSize) - } - } - - // Push the result into the results array. - results[i] = result; - } - } -} diff --git a/src/abstracts/NoDelegateCall.sol b/src/abstracts/NoDelegateCall.sol deleted file mode 100644 index 615297067..000000000 --- a/src/abstracts/NoDelegateCall.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.22; - -import { Errors } from "../libraries/Errors.sol"; - -/// @title NoDelegateCall -/// @notice This contract implements logic to prevent delegate calls. -abstract contract NoDelegateCall { - /// @dev The address of the original contract that was deployed. - address private immutable ORIGINAL; - - /// @dev Sets the original contract address. - constructor() { - ORIGINAL = address(this); - } - - /// @notice Prevents delegate calls. - modifier noDelegateCall() { - _preventDelegateCall(); - _; - } - - /// @dev This function checks whether the current call is a delegate call, and reverts if it is. - /// - /// - A private function is used instead of inlining this logic in a modifier because Solidity copies modifiers into - /// every function that uses them. The `ORIGINAL` address would get copied in every place the modifier is used, - /// which would increase the contract size. By using a function instead, we can avoid this duplication of code - /// and reduce the overall size of the contract. - function _preventDelegateCall() private view { - if (address(this) != ORIGINAL) { - revert Errors.DelegateCall(); - } - } -} diff --git a/src/abstracts/SablierLockupBase.sol b/src/abstracts/SablierLockupBase.sol deleted file mode 100644 index 484942fc8..000000000 --- a/src/abstracts/SablierLockupBase.sol +++ /dev/null @@ -1,717 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity >=0.8.22; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; -import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; -import { UD60x18 } from "@prb/math/src/UD60x18.sol"; - -import { ILockupNFTDescriptor } from "./../interfaces/ILockupNFTDescriptor.sol"; -import { ISablierLockupBase } from "./../interfaces/ISablierLockupBase.sol"; -import { ISablierLockupRecipient } from "./../interfaces/ISablierLockupRecipient.sol"; -import { Errors } from "./../libraries/Errors.sol"; -import { Lockup } from "./../types/DataTypes.sol"; -import { Adminable } from "./Adminable.sol"; -import { Batch } from "./Batch.sol"; -import { NoDelegateCall } from "./NoDelegateCall.sol"; - -/// @title SablierLockupBase -/// @notice See the documentation in {ISablierLockupBase}. -abstract contract SablierLockupBase is - Batch, // 1 inherited components - NoDelegateCall, // 0 inherited components - Adminable, // 1 inherited components - ISablierLockupBase, // 6 inherited components - ERC721 // 6 inherited components -{ - using SafeERC20 for IERC20; - - /*////////////////////////////////////////////////////////////////////////// - STATE VARIABLES - //////////////////////////////////////////////////////////////////////////*/ - - /// @inheritdoc ISablierLockupBase - UD60x18 public constant override MAX_BROKER_FEE = UD60x18.wrap(0.1e18); - - /// @inheritdoc ISablierLockupBase - uint256 public override nextStreamId; - - /// @inheritdoc ISablierLockupBase - ILockupNFTDescriptor public override nftDescriptor; - - /// @dev Mapping of contracts allowed to hook to Sablier when a stream is canceled or when tokens are withdrawn. - mapping(address recipient => bool allowed) internal _allowedToHook; - - /// @dev Lockup streams mapped by unsigned integers. - mapping(uint256 id => Lockup.Stream stream) internal _streams; - - /*////////////////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////////////////*/ - - /// @param initialAdmin The address of the initial contract admin. - /// @param initialNFTDescriptor The address of the initial NFT descriptor. - constructor(address initialAdmin, ILockupNFTDescriptor initialNFTDescriptor) Adminable(initialAdmin) { - nftDescriptor = initialNFTDescriptor; - } - - /*////////////////////////////////////////////////////////////////////////// - MODIFIERS - //////////////////////////////////////////////////////////////////////////*/ - - /// @dev Checks that `streamId` does not reference a null stream. - modifier notNull(uint256 streamId) { - if (!_streams[streamId].isStream) { - revert Errors.SablierLockupBase_Null(streamId); - } - _; - } - - /*////////////////////////////////////////////////////////////////////////// - USER-FACING CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @inheritdoc ISablierLockupBase - function getDepositedAmount(uint256 streamId) - external - view - override - notNull(streamId) - returns (uint128 depositedAmount) - { - depositedAmount = _streams[streamId].amounts.deposited; - } - - /// @inheritdoc ISablierLockupBase - function getEndTime(uint256 streamId) external view override notNull(streamId) returns (uint40 endTime) { - endTime = _streams[streamId].endTime; - } - - /// @inheritdoc ISablierLockupBase - function getLockupModel(uint256 streamId) - external - view - override - notNull(streamId) - returns (Lockup.Model lockupModel) - { - lockupModel = _streams[streamId].lockupModel; - } - - /// @inheritdoc ISablierLockupBase - function getRecipient(uint256 streamId) external view override returns (address recipient) { - // Check the stream NFT exists and return the owner, which is the stream's recipient. - recipient = _requireOwned({ tokenId: streamId }); - } - - /// @inheritdoc ISablierLockupBase - function getRefundedAmount(uint256 streamId) - external - view - override - notNull(streamId) - returns (uint128 refundedAmount) - { - refundedAmount = _streams[streamId].amounts.refunded; - } - - /// @inheritdoc ISablierLockupBase - function getSender(uint256 streamId) external view override notNull(streamId) returns (address sender) { - sender = _streams[streamId].sender; - } - - /// @inheritdoc ISablierLockupBase - function getStartTime(uint256 streamId) external view override notNull(streamId) returns (uint40 startTime) { - startTime = _streams[streamId].startTime; - } - - /// @inheritdoc ISablierLockupBase - function getUnderlyingToken(uint256 streamId) external view override notNull(streamId) returns (IERC20 token) { - token = _streams[streamId].token; - } - - /// @inheritdoc ISablierLockupBase - function getWithdrawnAmount(uint256 streamId) - external - view - override - notNull(streamId) - returns (uint128 withdrawnAmount) - { - withdrawnAmount = _streams[streamId].amounts.withdrawn; - } - - /// @inheritdoc ISablierLockupBase - function isAllowedToHook(address recipient) external view returns (bool result) { - result = _allowedToHook[recipient]; - } - - /// @inheritdoc ISablierLockupBase - function isCancelable(uint256 streamId) external view override notNull(streamId) returns (bool result) { - if (_statusOf(streamId) != Lockup.Status.SETTLED) { - result = _streams[streamId].isCancelable; - } - } - - /// @inheritdoc ISablierLockupBase - function isCold(uint256 streamId) external view override notNull(streamId) returns (bool result) { - Lockup.Status status = _statusOf(streamId); - result = status == Lockup.Status.SETTLED || status == Lockup.Status.CANCELED || status == Lockup.Status.DEPLETED; - } - - /// @inheritdoc ISablierLockupBase - function isDepleted(uint256 streamId) external view override notNull(streamId) returns (bool result) { - result = _streams[streamId].isDepleted; - } - - /// @inheritdoc ISablierLockupBase - function isStream(uint256 streamId) external view override returns (bool result) { - result = _streams[streamId].isStream; - } - - /// @inheritdoc ISablierLockupBase - function isTransferable(uint256 streamId) external view override notNull(streamId) returns (bool result) { - result = _streams[streamId].isTransferable; - } - - /// @inheritdoc ISablierLockupBase - function isWarm(uint256 streamId) external view override notNull(streamId) returns (bool result) { - Lockup.Status status = _statusOf(streamId); - result = status == Lockup.Status.PENDING || status == Lockup.Status.STREAMING; - } - - /// @inheritdoc ISablierLockupBase - function refundableAmountOf(uint256 streamId) - external - view - override - notNull(streamId) - returns (uint128 refundableAmount) - { - // These checks are needed because {_calculateStreamedAmount} does not look up the stream's status. Note that - // checking for `isCancelable` also checks if the stream `wasCanceled` thanks to the protocol invariant that - // canceled streams are not cancelable anymore. - if (_streams[streamId].isCancelable && !_streams[streamId].isDepleted) { - refundableAmount = _streams[streamId].amounts.deposited - _calculateStreamedAmount(streamId); - } - // Otherwise, the result is implicitly zero. - } - - /// @inheritdoc ISablierLockupBase - function statusOf(uint256 streamId) external view override notNull(streamId) returns (Lockup.Status status) { - status = _statusOf(streamId); - } - - /// @inheritdoc ISablierLockupBase - function streamedAmountOf(uint256 streamId) - external - view - override - notNull(streamId) - returns (uint128 streamedAmount) - { - streamedAmount = _streamedAmountOf(streamId); - } - - /// @inheritdoc ERC721 - function supportsInterface(bytes4 interfaceId) public view override(IERC165, ERC721) returns (bool) { - // 0x49064906 is the ERC-165 interface ID required by ERC-4906 - return interfaceId == 0x49064906 || super.supportsInterface(interfaceId); - } - - /// @inheritdoc ERC721 - function tokenURI(uint256 streamId) public view override(IERC721Metadata, ERC721) returns (string memory uri) { - // Check: the stream NFT exists. - _requireOwned({ tokenId: streamId }); - - // Generate the URI describing the stream NFT. - uri = nftDescriptor.tokenURI({ sablier: this, streamId: streamId }); - } - - /// @inheritdoc ISablierLockupBase - function wasCanceled(uint256 streamId) external view override notNull(streamId) returns (bool result) { - result = _streams[streamId].wasCanceled; - } - - /// @inheritdoc ISablierLockupBase - function withdrawableAmountOf(uint256 streamId) - external - view - override - notNull(streamId) - returns (uint128 withdrawableAmount) - { - withdrawableAmount = _withdrawableAmountOf(streamId); - } - - /*////////////////////////////////////////////////////////////////////////// - USER-FACING NON-CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @inheritdoc ISablierLockupBase - function allowToHook(address recipient) external override onlyAdmin { - // Check: non-zero code size. - if (recipient.code.length == 0) { - revert Errors.SablierLockupBase_AllowToHookZeroCodeSize(recipient); - } - - // Check: recipients implements the ERC-165 interface ID required by {ISablierLockupRecipient}. - bytes4 interfaceId = type(ISablierLockupRecipient).interfaceId; - if (!ISablierLockupRecipient(recipient).supportsInterface(interfaceId)) { - revert Errors.SablierLockupBase_AllowToHookUnsupportedInterface(recipient); - } - - // Effect: put the recipient on the allowlist. - _allowedToHook[recipient] = true; - - // Log the allowlist addition. - emit ISablierLockupBase.AllowToHook({ admin: msg.sender, recipient: recipient }); - } - - /// @inheritdoc ISablierLockupBase - function burn(uint256 streamId) external payable override noDelegateCall notNull(streamId) { - // Check: only depleted streams can be burned. - if (!_streams[streamId].isDepleted) { - revert Errors.SablierLockupBase_StreamNotDepleted(streamId); - } - - // Retrieve the current owner. - address currentRecipient = _ownerOf(streamId); - - // Check: - // 1. NFT exists (see {IERC721.getApproved}). - // 2. `msg.sender` is either the owner of the NFT or an approved third party. - if (!_isCallerStreamRecipientOrApproved(streamId, currentRecipient)) { - revert Errors.SablierLockupBase_Unauthorized(streamId, msg.sender); - } - - // Effect: burn the NFT. - _burn({ tokenId: streamId }); - } - - /// @inheritdoc ISablierLockupBase - function cancel(uint256 streamId) public payable override noDelegateCall notNull(streamId) { - // Check: the stream is neither depleted nor canceled. - if (_streams[streamId].isDepleted) { - revert Errors.SablierLockupBase_StreamDepleted(streamId); - } else if (_streams[streamId].wasCanceled) { - revert Errors.SablierLockupBase_StreamCanceled(streamId); - } - - // Check: `msg.sender` is the stream's sender. - if (!_isCallerStreamSender(streamId)) { - revert Errors.SablierLockupBase_Unauthorized(streamId, msg.sender); - } - - // Checks, Effects and Interactions: cancel the stream. - _cancel(streamId); - } - - /// @inheritdoc ISablierLockupBase - function cancelMultiple(uint256[] calldata streamIds) external payable override noDelegateCall { - // Iterate over the provided array of stream IDs and cancel each stream. - uint256 count = streamIds.length; - for (uint256 i = 0; i < count; ++i) { - // Effects and Interactions: cancel the stream. - cancel(streamIds[i]); - } - } - - /// @inheritdoc ISablierLockupBase - function collectFees() external override { - uint256 feeAmount = address(this).balance; - - // Effect: transfer the fees to the admin. - (bool success,) = admin.call{ value: feeAmount }(""); - - // Revert if the call failed. - if (!success) { - revert Errors.SablierLockupBase_FeeTransferFail(admin, feeAmount); - } - - // Log the fee withdrawal. - emit ISablierLockupBase.CollectFees({ admin: admin, feeAmount: feeAmount }); - } - - /// @inheritdoc ISablierLockupBase - function renounce(uint256 streamId) public payable override noDelegateCall notNull(streamId) { - // Check: the stream is not cold. - Lockup.Status status = _statusOf(streamId); - if (status == Lockup.Status.DEPLETED) { - revert Errors.SablierLockupBase_StreamDepleted(streamId); - } else if (status == Lockup.Status.CANCELED) { - revert Errors.SablierLockupBase_StreamCanceled(streamId); - } else if (status == Lockup.Status.SETTLED) { - revert Errors.SablierLockupBase_StreamSettled(streamId); - } - - // Check: `msg.sender` is the stream's sender. - if (!_isCallerStreamSender(streamId)) { - revert Errors.SablierLockupBase_Unauthorized(streamId, msg.sender); - } - - // Checks and Effects: renounce the stream. - _renounce(streamId); - - // Log the renouncement. - emit ISablierLockupBase.RenounceLockupStream(streamId); - } - - /// @inheritdoc ISablierLockupBase - function renounceMultiple(uint256[] calldata streamIds) external payable override noDelegateCall { - // Iterate over the provided array of stream IDs and renounce each stream. - uint256 count = streamIds.length; - for (uint256 i = 0; i < count; ++i) { - // Call the existing renounce function for each stream ID. - renounce(streamIds[i]); - } - } - - /// @inheritdoc ISablierLockupBase - function setNFTDescriptor(ILockupNFTDescriptor newNFTDescriptor) external override onlyAdmin { - // Effect: set the NFT descriptor. - ILockupNFTDescriptor oldNftDescriptor = nftDescriptor; - nftDescriptor = newNFTDescriptor; - - // Log the change of the NFT descriptor. - emit ISablierLockupBase.SetNFTDescriptor({ - admin: msg.sender, - oldNFTDescriptor: oldNftDescriptor, - newNFTDescriptor: newNFTDescriptor - }); - - // Refresh the NFT metadata for all streams. - emit BatchMetadataUpdate({ _fromTokenId: 1, _toTokenId: nextStreamId - 1 }); - } - - /// @inheritdoc ISablierLockupBase - function withdraw( - uint256 streamId, - address to, - uint128 amount - ) - public - payable - override - noDelegateCall - notNull(streamId) - { - // Check: the stream is not depleted. - if (_streams[streamId].isDepleted) { - revert Errors.SablierLockupBase_StreamDepleted(streamId); - } - - // Check: the withdrawal address is not zero. - if (to == address(0)) { - revert Errors.SablierLockupBase_WithdrawToZeroAddress(streamId); - } - - // Retrieve the recipient from storage. - address recipient = _ownerOf(streamId); - - // Check: `msg.sender` is neither the stream's recipient nor an approved third party, the withdrawal address - // must be the recipient. - if (to != recipient && !_isCallerStreamRecipientOrApproved(streamId, recipient)) { - revert Errors.SablierLockupBase_WithdrawalAddressNotRecipient(streamId, msg.sender, to); - } - - // Check: the withdraw amount is not zero. - if (amount == 0) { - revert Errors.SablierLockupBase_WithdrawAmountZero(streamId); - } - - // Check: the withdraw amount is not greater than the withdrawable amount. - uint128 withdrawableAmount = _withdrawableAmountOf(streamId); - if (amount > withdrawableAmount) { - revert Errors.SablierLockupBase_Overdraw(streamId, amount, withdrawableAmount); - } - - // Effects and Interactions: make the withdrawal. - _withdraw(streamId, to, amount); - - // Emit an ERC-4906 event to trigger an update of the NFT metadata. - emit MetadataUpdate({ _tokenId: streamId }); - - // Interaction: if `msg.sender` is not the recipient and the recipient is on the allowlist, run the hook. - if (msg.sender != recipient && _allowedToHook[recipient]) { - bytes4 selector = ISablierLockupRecipient(recipient).onSablierLockupWithdraw({ - streamId: streamId, - caller: msg.sender, - to: to, - amount: amount - }); - - // Check: the recipient's hook returned the correct selector. - if (selector != ISablierLockupRecipient.onSablierLockupWithdraw.selector) { - revert Errors.SablierLockupBase_InvalidHookSelector(recipient); - } - } - } - - /// @inheritdoc ISablierLockupBase - function withdrawMax(uint256 streamId, address to) external payable override returns (uint128 withdrawnAmount) { - withdrawnAmount = _withdrawableAmountOf(streamId); - withdraw({ streamId: streamId, to: to, amount: withdrawnAmount }); - } - - /// @inheritdoc ISablierLockupBase - function withdrawMaxAndTransfer( - uint256 streamId, - address newRecipient - ) - external - payable - override - noDelegateCall - notNull(streamId) - returns (uint128 withdrawnAmount) - { - // Retrieve the current owner. This also checks that the NFT was not burned. - address currentRecipient = _ownerOf(streamId); - - // Check: `msg.sender` is neither the stream's recipient nor an approved third party. - if (!_isCallerStreamRecipientOrApproved(streamId, currentRecipient)) { - revert Errors.SablierLockupBase_Unauthorized(streamId, msg.sender); - } - - // Skip the withdrawal if the withdrawable amount is zero. - withdrawnAmount = _withdrawableAmountOf(streamId); - if (withdrawnAmount > 0) { - withdraw({ streamId: streamId, to: currentRecipient, amount: withdrawnAmount }); - } - - // Checks and Effects: transfer the NFT. - _transfer({ from: currentRecipient, to: newRecipient, tokenId: streamId }); - } - - /// @inheritdoc ISablierLockupBase - function withdrawMultiple( - uint256[] calldata streamIds, - uint128[] calldata amounts - ) - external - payable - override - noDelegateCall - { - // Check: there is an equal number of `streamIds` and `amounts`. - uint256 streamIdsCount = streamIds.length; - uint256 amountsCount = amounts.length; - if (streamIdsCount != amountsCount) { - revert Errors.SablierLockupBase_WithdrawArrayCountsNotEqual(streamIdsCount, amountsCount); - } - - // Iterate over the provided array of stream IDs and withdraw from each stream to the recipient. - for (uint256 i = 0; i < streamIdsCount; ++i) { - // Checks, Effects and Interactions: withdraw using delegatecall. - (bool success, bytes memory result) = address(this).delegatecall( - abi.encodeCall(ISablierLockupBase.withdraw, (streamIds[i], _ownerOf(streamIds[i]), amounts[i])) - ); - // If the withdrawal reverts, log it using an event, and continue with the next stream. - if (!success) { - emit InvalidWithdrawalInWithdrawMultiple(streamIds[i], result); - } - } - } - - /*////////////////////////////////////////////////////////////////////////// - INTERNAL CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Calculates the streamed amount of the stream without looking up the stream's status. - /// @dev This function is implemented by child contracts, so the logic varies depending on the model. - function _calculateStreamedAmount(uint256 streamId) internal view virtual returns (uint128); - - /// @notice Checks whether `msg.sender` is the stream's recipient or an approved third party, when the - /// `recipient` is known in advance. - /// @param streamId The stream ID for the query. - /// @param recipient The address of the stream's recipient. - function _isCallerStreamRecipientOrApproved(uint256 streamId, address recipient) internal view returns (bool) { - return msg.sender == recipient || isApprovedForAll({ owner: recipient, operator: msg.sender }) - || getApproved(streamId) == msg.sender; - } - - /// @notice Checks whether `msg.sender` is the stream's sender. - /// @param streamId The stream ID for the query. - function _isCallerStreamSender(uint256 streamId) internal view returns (bool) { - return msg.sender == _streams[streamId].sender; - } - - /// @dev Retrieves the stream's status without performing a null check. - function _statusOf(uint256 streamId) internal view returns (Lockup.Status) { - if (_streams[streamId].isDepleted) { - return Lockup.Status.DEPLETED; - } else if (_streams[streamId].wasCanceled) { - return Lockup.Status.CANCELED; - } - - if (block.timestamp < _streams[streamId].startTime) { - return Lockup.Status.PENDING; - } - - if (_calculateStreamedAmount(streamId) < _streams[streamId].amounts.deposited) { - return Lockup.Status.STREAMING; - } else { - return Lockup.Status.SETTLED; - } - } - - /// @dev See the documentation for the user-facing functions that call this internal function. - function _streamedAmountOf(uint256 streamId) internal view returns (uint128) { - Lockup.Amounts memory amounts = _streams[streamId].amounts; - - if (_streams[streamId].isDepleted) { - return amounts.withdrawn; - } else if (_streams[streamId].wasCanceled) { - return amounts.deposited - amounts.refunded; - } - - return _calculateStreamedAmount(streamId); - } - - /// @dev See the documentation for the user-facing functions that call this internal function. - function _withdrawableAmountOf(uint256 streamId) internal view returns (uint128) { - return _streamedAmountOf(streamId) - _streams[streamId].amounts.withdrawn; - } - - /*////////////////////////////////////////////////////////////////////////// - INTERNAL NON-CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @dev See the documentation for the user-facing functions that call this internal function. - function _cancel(uint256 streamId) internal { - // Calculate the streamed amount. - uint128 streamedAmount = _calculateStreamedAmount(streamId); - - // Retrieve the amounts from storage. - Lockup.Amounts memory amounts = _streams[streamId].amounts; - - // Check: the stream is not settled. - if (streamedAmount >= amounts.deposited) { - revert Errors.SablierLockupBase_StreamSettled(streamId); - } - - // Check: the stream is cancelable. - if (!_streams[streamId].isCancelable) { - revert Errors.SablierLockupBase_StreamNotCancelable(streamId); - } - - // Calculate the sender's amount. - uint128 senderAmount; - unchecked { - senderAmount = amounts.deposited - streamedAmount; - } - - // Calculate the recipient's amount. - uint128 recipientAmount = streamedAmount - amounts.withdrawn; - - // Effect: mark the stream as canceled. - _streams[streamId].wasCanceled = true; - - // Effect: make the stream not cancelable anymore, because a stream can only be canceled once. - _streams[streamId].isCancelable = false; - - // Effect: if there are no tokens left for the recipient to withdraw, mark the stream as depleted. - if (recipientAmount == 0) { - _streams[streamId].isDepleted = true; - } - - // Effect: set the refunded amount. - _streams[streamId].amounts.refunded = senderAmount; - - // Retrieve the sender and the recipient from storage. - address sender = _streams[streamId].sender; - address recipient = _ownerOf(streamId); - - // Retrieve the ERC-20 token from storage. - IERC20 token = _streams[streamId].token; - - // Interaction: refund the sender. - token.safeTransfer({ to: sender, value: senderAmount }); - - // Log the cancellation. - emit ISablierLockupBase.CancelLockupStream(streamId, sender, recipient, token, senderAmount, recipientAmount); - - // Emit an ERC-4906 event to trigger an update of the NFT metadata. - emit MetadataUpdate({ _tokenId: streamId }); - - // Interaction: if the recipient is on the allowlist, run the hook. - if (_allowedToHook[recipient]) { - bytes4 selector = ISablierLockupRecipient(recipient).onSablierLockupCancel({ - streamId: streamId, - sender: sender, - senderAmount: senderAmount, - recipientAmount: recipientAmount - }); - - // Check: the recipient's hook returned the correct selector. - if (selector != ISablierLockupRecipient.onSablierLockupCancel.selector) { - revert Errors.SablierLockupBase_InvalidHookSelector(recipient); - } - } - } - - /// @dev See the documentation for the user-facing functions that call this internal function. - function _renounce(uint256 streamId) internal { - // Check: the stream is cancelable. - if (!_streams[streamId].isCancelable) { - revert Errors.SablierLockupBase_StreamNotCancelable(streamId); - } - - // Effect: renounce the stream by making it not cancelable. - _streams[streamId].isCancelable = false; - } - - /// @notice Overrides the {ERC-721._update} function to check that the stream is transferable, and emits an - /// ERC-4906 event. - /// @dev There are two cases when the transferable flag is ignored: - /// - If the current owner is 0, then the update is a mint and is allowed. - /// - If `to` is 0, then the update is a burn and is also allowed. - /// @param to The address of the new recipient of the stream. - /// @param streamId ID of the stream to update. - /// @param auth Optional parameter. If the value is not zero, the overridden implementation will check that - /// `auth` is either the recipient of the stream, or an approved third party. - /// @return The original recipient of the `streamId` before the update. - function _update(address to, uint256 streamId, address auth) internal override returns (address) { - address from = _ownerOf(streamId); - - if (from != address(0) && to != address(0) && !_streams[streamId].isTransferable) { - revert Errors.SablierLockupBase_NotTransferable(streamId); - } - - // Emit an ERC-4906 event to trigger an update of the NFT metadata. - emit MetadataUpdate({ _tokenId: streamId }); - - return super._update(to, streamId, auth); - } - - /// @dev See the documentation for the user-facing functions that call this internal function. - function _withdraw(uint256 streamId, address to, uint128 amount) internal { - // Effect: update the withdrawn amount. - _streams[streamId].amounts.withdrawn = _streams[streamId].amounts.withdrawn + amount; - - // Retrieve the amounts from storage. - Lockup.Amounts memory amounts = _streams[streamId].amounts; - - // Using ">=" instead of "==" for additional safety reasons. In the event of an unforeseen increase in the - // withdrawn amount, the stream will still be marked as depleted. - if (amounts.withdrawn >= amounts.deposited - amounts.refunded) { - // Effect: mark the stream as depleted. - _streams[streamId].isDepleted = true; - - // Effect: make the stream not cancelable anymore, because a depleted stream cannot be canceled. - _streams[streamId].isCancelable = false; - } - - // Retrieve the ERC-20 token from storage. - IERC20 token = _streams[streamId].token; - - // Interaction: perform the ERC-20 transfer. - token.safeTransfer({ to: to, value: amount }); - - // Log the withdrawal. - emit ISablierLockupBase.WithdrawFromLockupStream(streamId, to, token, amount); - } -} diff --git a/src/abstracts/SablierLockupDynamic.sol b/src/abstracts/SablierLockupDynamic.sol new file mode 100644 index 000000000..51fb6f3bb --- /dev/null +++ b/src/abstracts/SablierLockupDynamic.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.22; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { NoDelegateCall } from "@sablier/evm-utils/src/NoDelegateCall.sol"; + +import { ISablierLockupDynamic } from "../interfaces/ISablierLockupDynamic.sol"; +import { Helpers } from "../libraries/Helpers.sol"; +import { Lockup } from "../types/Lockup.sol"; +import { LockupDynamic } from "../types/LockupDynamic.sol"; +import { SablierLockupState } from "./SablierLockupState.sol"; + +/// @title SablierLockupDynamic +/// @notice See the documentation in {ISablierLockupDynamic}. +abstract contract SablierLockupDynamic is + ISablierLockupDynamic, // 1 inherited component + NoDelegateCall, // 0 inherited components + SablierLockupState // 1 inherited component +{ + /*////////////////////////////////////////////////////////////////////////// + USER-FACING STATE-CHANGING FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc ISablierLockupDynamic + function createWithDurationsLD( + Lockup.CreateWithDurations calldata params, + LockupDynamic.SegmentWithDuration[] calldata segmentsWithDuration + ) + external + payable + override + noDelegateCall + returns (uint256 streamId) + { + // Use the block timestamp as the start time. + uint40 startTime = uint40(block.timestamp); + + // Generate the canonical segments. + LockupDynamic.Segment[] memory segments = Helpers.calculateSegmentTimestamps(segmentsWithDuration, startTime); + + // Declare the timestamps for the stream. + Lockup.Timestamps memory timestamps = + Lockup.Timestamps({ start: startTime, end: segments[segments.length - 1].timestamp }); + + // Checks, Effects and Interactions: create the stream. + streamId = _createLD({ + cancelable: params.cancelable, + depositAmount: params.depositAmount, + recipient: params.recipient, + segments: segments, + sender: params.sender, + shape: params.shape, + timestamps: timestamps, + token: params.token, + transferable: params.transferable + }); + } + + /// @inheritdoc ISablierLockupDynamic + function createWithTimestampsLD( + Lockup.CreateWithTimestamps calldata params, + LockupDynamic.Segment[] calldata segments + ) + external + payable + override + noDelegateCall + returns (uint256 streamId) + { + // Checks, Effects and Interactions: create the stream. + streamId = _createLD({ + cancelable: params.cancelable, + depositAmount: params.depositAmount, + recipient: params.recipient, + segments: segments, + sender: params.sender, + shape: params.shape, + timestamps: params.timestamps, + token: params.token, + transferable: params.transferable + }); + } + + /*////////////////////////////////////////////////////////////////////////// + PRIVATE STATE-CHANGING FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev See the documentation for the user-facing functions that call this private function. + function _createLD( + bool cancelable, + uint128 depositAmount, + address recipient, + LockupDynamic.Segment[] memory segments, + address sender, + string memory shape, + Lockup.Timestamps memory timestamps, + IERC20 token, + bool transferable + ) + private + returns (uint256 streamId) + { + // Check: validate the user-provided parameters and segments. + Helpers.checkCreateLD({ + sender: sender, + timestamps: timestamps, + depositAmount: depositAmount, + segments: segments, + token: address(token), + nativeToken: nativeToken, + shape: shape + }); + + // Load the stream ID in a variable. + streamId = nextStreamId; + + // Effect: store the segments. Since Solidity lacks a syntax for copying arrays of structs directly from + // memory to storage, a manual approach is necessary. See https://github.com/ethereum/solidity/issues/12783. + uint256 segmentCount = segments.length; + for (uint256 i = 0; i < segmentCount; ++i) { + _segments[streamId].push(segments[i]); + } + + // Effect: create the stream, mint the NFT and transfer the deposit amount. + _create({ + cancelable: cancelable, + depositAmount: depositAmount, + lockupModel: Lockup.Model.LOCKUP_DYNAMIC, + recipient: recipient, + sender: sender, + streamId: streamId, + timestamps: timestamps, + token: token, + transferable: transferable + }); + + // Log the newly created stream. + emit ISablierLockupDynamic.CreateLockupDynamicStream({ + streamId: streamId, + commonParams: Lockup.CreateEventCommon({ + funder: msg.sender, + sender: sender, + recipient: recipient, + depositAmount: depositAmount, + token: token, + cancelable: cancelable, + transferable: transferable, + timestamps: timestamps, + shape: shape + }), + segments: segments + }); + } +} diff --git a/src/abstracts/SablierLockupLinear.sol b/src/abstracts/SablierLockupLinear.sol new file mode 100644 index 000000000..b6de443e2 --- /dev/null +++ b/src/abstracts/SablierLockupLinear.sol @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.22; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { NoDelegateCall } from "@sablier/evm-utils/src/NoDelegateCall.sol"; + +import { ISablierLockupLinear } from "../interfaces/ISablierLockupLinear.sol"; +import { Helpers } from "../libraries/Helpers.sol"; +import { Lockup } from "../types/Lockup.sol"; +import { LockupLinear } from "../types/LockupLinear.sol"; +import { SablierLockupState } from "./SablierLockupState.sol"; + +/// @title SablierLockupLinear +/// @notice See the documentation in {ISablierLockupLinear}. +abstract contract SablierLockupLinear is + ISablierLockupLinear, // 1 inherited component + NoDelegateCall, // 0 inherited components + SablierLockupState // 1 inherited component +{ + /*////////////////////////////////////////////////////////////////////////// + USER-FACING STATE-CHANGING FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc ISablierLockupLinear + function createWithDurationsLL( + Lockup.CreateWithDurations calldata params, + LockupLinear.UnlockAmounts calldata unlockAmounts, + LockupLinear.Durations calldata durations + ) + external + payable + override + noDelegateCall + returns (uint256 streamId) + { + // Set the current block timestamp as the stream's start time. + Lockup.Timestamps memory timestamps = Lockup.Timestamps({ start: uint40(block.timestamp), end: 0 }); + + uint40 cliffTime; + + // Calculate the cliff time and the end time. + if (durations.cliff > 0) { + cliffTime = timestamps.start + durations.cliff; + } + timestamps.end = timestamps.start + durations.total; + + // Checks, Effects and Interactions: create the stream. + streamId = _createLL({ + cancelable: params.cancelable, + cliffTime: cliffTime, + depositAmount: params.depositAmount, + recipient: params.recipient, + sender: params.sender, + shape: params.shape, + timestamps: timestamps, + token: params.token, + transferable: params.transferable, + unlockAmounts: unlockAmounts + }); + } + + /// @inheritdoc ISablierLockupLinear + function createWithTimestampsLL( + Lockup.CreateWithTimestamps calldata params, + LockupLinear.UnlockAmounts calldata unlockAmounts, + uint40 cliffTime + ) + external + payable + override + noDelegateCall + returns (uint256 streamId) + { + // Checks, Effects and Interactions: create the stream. + streamId = _createLL({ + cancelable: params.cancelable, + cliffTime: cliffTime, + depositAmount: params.depositAmount, + recipient: params.recipient, + sender: params.sender, + shape: params.shape, + timestamps: params.timestamps, + token: params.token, + transferable: params.transferable, + unlockAmounts: unlockAmounts + }); + } + + /*////////////////////////////////////////////////////////////////////////// + PRIVATE STATE-CHANGING FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev See the documentation for the user-facing functions that call this private function. + function _createLL( + bool cancelable, + uint40 cliffTime, + uint128 depositAmount, + address recipient, + address sender, + string memory shape, + Lockup.Timestamps memory timestamps, + IERC20 token, + bool transferable, + LockupLinear.UnlockAmounts memory unlockAmounts + ) + private + returns (uint256 streamId) + { + // Check: validate the user-provided parameters and cliff time. + Helpers.checkCreateLL({ + sender: sender, + timestamps: timestamps, + cliffTime: cliffTime, + depositAmount: depositAmount, + unlockAmounts: unlockAmounts, + token: address(token), + nativeToken: nativeToken, + shape: shape + }); + + // Load the stream ID in a variable. + streamId = nextStreamId; + + // Effect: set the start and cliff unlock amounts. + _unlockAmounts[streamId] = unlockAmounts; + + // Effect: update cliff time. + _cliffs[streamId] = cliffTime; + + // Effect: create the stream, mint the NFT and transfer the deposit amount. + _create({ + cancelable: cancelable, + depositAmount: depositAmount, + lockupModel: Lockup.Model.LOCKUP_LINEAR, + recipient: recipient, + sender: sender, + streamId: streamId, + timestamps: timestamps, + token: token, + transferable: transferable + }); + + // Log the newly created stream. + emit ISablierLockupLinear.CreateLockupLinearStream({ + streamId: streamId, + commonParams: Lockup.CreateEventCommon({ + funder: msg.sender, + sender: sender, + recipient: recipient, + depositAmount: depositAmount, + token: token, + cancelable: cancelable, + transferable: transferable, + timestamps: timestamps, + shape: shape + }), + cliffTime: cliffTime, + unlockAmounts: unlockAmounts + }); + } +} diff --git a/src/abstracts/SablierLockupState.sol b/src/abstracts/SablierLockupState.sol new file mode 100644 index 000000000..63b236ae5 --- /dev/null +++ b/src/abstracts/SablierLockupState.sol @@ -0,0 +1,301 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.22; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { ILockupNFTDescriptor } from "../interfaces/ILockupNFTDescriptor.sol"; +import { ISablierLockupState } from "../interfaces/ISablierLockupState.sol"; +import { Errors } from "../libraries/Errors.sol"; +import { Lockup } from "../types/Lockup.sol"; +import { LockupDynamic } from "../types/LockupDynamic.sol"; +import { LockupLinear } from "../types/LockupLinear.sol"; +import { LockupTranched } from "../types/LockupTranched.sol"; + +/// @title SablierLockupState +/// @notice See the documentation in {ISablierLockupState}. +abstract contract SablierLockupState is ISablierLockupState { + /*////////////////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc ISablierLockupState + mapping(IERC20 token => uint256 amount) public override aggregateAmount; + + /// @inheritdoc ISablierLockupState + address public override nativeToken; + + /// @inheritdoc ISablierLockupState + uint256 public override nextStreamId; + + /// @inheritdoc ISablierLockupState + ILockupNFTDescriptor public override nftDescriptor; + + /// @dev Mapping of contracts allowed to hook to Sablier when a stream is canceled or when tokens are withdrawn. + mapping(address recipient => bool allowed) internal _allowedToHook; + + /// @dev Cliff timestamp mapped by stream IDs, used in LL streams. + mapping(uint256 streamId => uint40 cliffTime) internal _cliffs; + + /// @dev Stream segments mapped by stream IDs, used in LD streams. + mapping(uint256 streamId => LockupDynamic.Segment[] segments) internal _segments; + + /// @dev Lockup streams mapped by unsigned integers. + mapping(uint256 id => Lockup.Stream stream) internal _streams; + + /// @dev Stream tranches mapped by stream IDs, used in LT streams. + mapping(uint256 streamId => LockupTranched.Tranche[] tranches) internal _tranches; + + /// @dev Unlock amounts mapped by stream IDs, used in LL streams. + mapping(uint256 streamId => LockupLinear.UnlockAmounts unlockAmounts) internal _unlockAmounts; + + /*////////////////////////////////////////////////////////////////////////// + MODIFIERS + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Checks that `streamId` does not reference a null stream. + modifier notNull(uint256 streamId) { + _notNull(streamId); + _; + } + + /*////////////////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////////////////*/ + + /// @param initialNFTDescriptor The address of the initial NFT descriptor. + constructor(address initialNFTDescriptor) { + // Set the next stream to 1. + nextStreamId = 1; + + // Set the NFT Descriptor. + nftDescriptor = ILockupNFTDescriptor(initialNFTDescriptor); + } + + /*////////////////////////////////////////////////////////////////////////// + USER-FACING READ-ONLY FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc ISablierLockupState + function getCliffTime(uint256 streamId) external view override notNull(streamId) returns (uint40 cliffTime) { + if (_streams[streamId].lockupModel != Lockup.Model.LOCKUP_LINEAR) { + revert Errors.SablierLockupState_NotExpectedModel( + _streams[streamId].lockupModel, Lockup.Model.LOCKUP_LINEAR + ); + } + + cliffTime = _cliffs[streamId]; + } + + /// @inheritdoc ISablierLockupState + function getDepositedAmount(uint256 streamId) + external + view + override + notNull(streamId) + returns (uint128 depositedAmount) + { + depositedAmount = _streams[streamId].amounts.deposited; + } + + /// @inheritdoc ISablierLockupState + function getEndTime(uint256 streamId) external view override notNull(streamId) returns (uint40 endTime) { + endTime = _streams[streamId].endTime; + } + + /// @inheritdoc ISablierLockupState + function getLockupModel(uint256 streamId) + external + view + override + notNull(streamId) + returns (Lockup.Model lockupModel) + { + lockupModel = _streams[streamId].lockupModel; + } + + /// @inheritdoc ISablierLockupState + function getRefundedAmount(uint256 streamId) + external + view + override + notNull(streamId) + returns (uint128 refundedAmount) + { + refundedAmount = _streams[streamId].amounts.refunded; + } + + /// @inheritdoc ISablierLockupState + function getSegments(uint256 streamId) + external + view + override + notNull(streamId) + returns (LockupDynamic.Segment[] memory segments) + { + if (_streams[streamId].lockupModel != Lockup.Model.LOCKUP_DYNAMIC) { + revert Errors.SablierLockupState_NotExpectedModel( + _streams[streamId].lockupModel, Lockup.Model.LOCKUP_DYNAMIC + ); + } + + segments = _segments[streamId]; + } + + /// @inheritdoc ISablierLockupState + function getSender(uint256 streamId) external view override notNull(streamId) returns (address sender) { + sender = _streams[streamId].sender; + } + + /// @inheritdoc ISablierLockupState + function getStartTime(uint256 streamId) external view override notNull(streamId) returns (uint40 startTime) { + startTime = _streams[streamId].startTime; + } + + /// @inheritdoc ISablierLockupState + function getTranches(uint256 streamId) + external + view + override + notNull(streamId) + returns (LockupTranched.Tranche[] memory tranches) + { + if (_streams[streamId].lockupModel != Lockup.Model.LOCKUP_TRANCHED) { + revert Errors.SablierLockupState_NotExpectedModel( + _streams[streamId].lockupModel, Lockup.Model.LOCKUP_TRANCHED + ); + } + + tranches = _tranches[streamId]; + } + + /// @inheritdoc ISablierLockupState + function getUnderlyingToken(uint256 streamId) external view override notNull(streamId) returns (IERC20 token) { + token = _streams[streamId].token; + } + + /// @inheritdoc ISablierLockupState + function getUnlockAmounts(uint256 streamId) + external + view + override + notNull(streamId) + returns (LockupLinear.UnlockAmounts memory unlockAmounts) + { + if (_streams[streamId].lockupModel != Lockup.Model.LOCKUP_LINEAR) { + revert Errors.SablierLockupState_NotExpectedModel( + _streams[streamId].lockupModel, Lockup.Model.LOCKUP_LINEAR + ); + } + + unlockAmounts = _unlockAmounts[streamId]; + } + + /// @inheritdoc ISablierLockupState + function getWithdrawnAmount(uint256 streamId) + external + view + override + notNull(streamId) + returns (uint128 withdrawnAmount) + { + withdrawnAmount = _streams[streamId].amounts.withdrawn; + } + + /// @inheritdoc ISablierLockupState + function isAllowedToHook(address recipient) external view returns (bool result) { + result = _allowedToHook[recipient]; + } + + /// @inheritdoc ISablierLockupState + function isCancelable(uint256 streamId) external view override notNull(streamId) returns (bool result) { + if (_statusOf(streamId) != Lockup.Status.SETTLED) { + result = _streams[streamId].isCancelable; + } + } + + /// @inheritdoc ISablierLockupState + function isDepleted(uint256 streamId) external view override notNull(streamId) returns (bool result) { + result = _streams[streamId].isDepleted; + } + + /// @inheritdoc ISablierLockupState + function isStream(uint256 streamId) external view override returns (bool result) { + // Since {Helpers._checkCreateStream} reverts if the sender address is zero, this can be used to check whether + // the stream exists. + result = _streams[streamId].sender != address(0); + } + + /// @inheritdoc ISablierLockupState + function isTransferable(uint256 streamId) external view override notNull(streamId) returns (bool result) { + result = _streams[streamId].isTransferable; + } + + /// @inheritdoc ISablierLockupState + function wasCanceled(uint256 streamId) external view override notNull(streamId) returns (bool result) { + result = _streams[streamId].wasCanceled; + } + + /*////////////////////////////////////////////////////////////////////////// + INTERNAL READ-ONLY FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Retrieves the stream's status without performing a null check. + function _statusOf(uint256 streamId) internal view returns (Lockup.Status) { + if (_streams[streamId].isDepleted) { + return Lockup.Status.DEPLETED; + } else if (_streams[streamId].wasCanceled) { + return Lockup.Status.CANCELED; + } + + if (block.timestamp < _streams[streamId].startTime) { + return Lockup.Status.PENDING; + } + + if (_streamedAmountOf(streamId) < _streams[streamId].amounts.deposited) { + return Lockup.Status.STREAMING; + } else { + return Lockup.Status.SETTLED; + } + } + + /// @notice Calculates the streamed amount of the stream. + /// @dev This function is implemented by child contract. The logic varies according to the distribution model. + function _streamedAmountOf(uint256 streamId) internal view virtual returns (uint128); + + /*////////////////////////////////////////////////////////////////////////// + INTERNAL STATE-CHANGING FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice This function is implemented by {SablierLockup} and is used in the {SablierLockupDynamic}, + /// {SablierLockupLinear} and {SablierLockupTranched} contracts. + /// @dev It updates state variables based on the stream parameters, mints an NFT to the recipient, bumps stream ID, + /// and transfers the deposit amount. + function _create( + bool cancelable, + uint128 depositAmount, + Lockup.Model lockupModel, + address recipient, + address sender, + uint256 streamId, + Lockup.Timestamps memory timestamps, + IERC20 token, + bool transferable + ) + internal + virtual + { } + + /*////////////////////////////////////////////////////////////////////////// + PRIVATE READ-ONLY FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev A private function is used instead of inlining this logic in a modifier because Solidity copies modifiers + /// into every function that uses them. + function _notNull(uint256 streamId) private view { + // Since {Helpers._checkCreateStream} reverts if the sender address is zero, this can be used to check whether + // the stream exists. + if (_streams[streamId].sender == address(0)) { + revert Errors.SablierLockupState_Null(streamId); + } + } +} diff --git a/src/abstracts/SablierLockupTranched.sol b/src/abstracts/SablierLockupTranched.sol new file mode 100644 index 000000000..e5781779d --- /dev/null +++ b/src/abstracts/SablierLockupTranched.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.22; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { NoDelegateCall } from "@sablier/evm-utils/src/NoDelegateCall.sol"; + +import { ISablierLockupTranched } from "../interfaces/ISablierLockupTranched.sol"; +import { Helpers } from "../libraries/Helpers.sol"; +import { Lockup } from "../types/Lockup.sol"; +import { LockupTranched } from "../types/LockupTranched.sol"; +import { SablierLockupState } from "./SablierLockupState.sol"; + +/// @title SablierLockupTranched +/// @notice See the documentation in {ISablierLockupTranched}. +abstract contract SablierLockupTranched is + ISablierLockupTranched, // 1 inherited component + NoDelegateCall, // 0 inherited components + SablierLockupState // 1 inherited component +{ + /*////////////////////////////////////////////////////////////////////////// + USER-FACING STATE-CHANGING FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc ISablierLockupTranched + function createWithDurationsLT( + Lockup.CreateWithDurations calldata params, + LockupTranched.TrancheWithDuration[] calldata tranchesWithDuration + ) + external + payable + override + noDelegateCall + returns (uint256 streamId) + { + // Use the block timestamp as the start time. + uint40 startTime = uint40(block.timestamp); + + // Generate the canonical tranches. + LockupTranched.Tranche[] memory tranches = Helpers.calculateTrancheTimestamps(tranchesWithDuration, startTime); + + // Declare the timestamps for the stream. + Lockup.Timestamps memory timestamps = + Lockup.Timestamps({ start: startTime, end: tranches[tranches.length - 1].timestamp }); + + // Checks, Effects and Interactions: create the stream. + streamId = _createLT({ + cancelable: params.cancelable, + depositAmount: params.depositAmount, + recipient: params.recipient, + sender: params.sender, + shape: params.shape, + timestamps: timestamps, + token: params.token, + tranches: tranches, + transferable: params.transferable + }); + } + + /// @inheritdoc ISablierLockupTranched + function createWithTimestampsLT( + Lockup.CreateWithTimestamps calldata params, + LockupTranched.Tranche[] calldata tranches + ) + external + payable + override + noDelegateCall + returns (uint256 streamId) + { + // Checks, Effects and Interactions: create the stream. + streamId = _createLT({ + cancelable: params.cancelable, + depositAmount: params.depositAmount, + recipient: params.recipient, + sender: params.sender, + shape: params.shape, + timestamps: params.timestamps, + token: params.token, + tranches: tranches, + transferable: params.transferable + }); + } + + /*////////////////////////////////////////////////////////////////////////// + PRIVATE STATE-CHANGING FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev See the documentation for the user-facing functions that call this private function. + function _createLT( + bool cancelable, + uint128 depositAmount, + address recipient, + address sender, + string memory shape, + Lockup.Timestamps memory timestamps, + IERC20 token, + bool transferable, + LockupTranched.Tranche[] memory tranches + ) + private + returns (uint256 streamId) + { + // Check: validate the user-provided parameters and tranches. + Helpers.checkCreateLT({ + sender: sender, + timestamps: timestamps, + depositAmount: depositAmount, + tranches: tranches, + token: address(token), + nativeToken: nativeToken, + shape: shape + }); + + // Load the stream ID in a variable. + streamId = nextStreamId; + + // Effect: store the tranches. Since Solidity lacks a syntax for copying arrays of structs directly from + // memory to storage, a manual approach is necessary. See https://github.com/ethereum/solidity/issues/12783. + uint256 trancheCount = tranches.length; + for (uint256 i = 0; i < trancheCount; ++i) { + _tranches[streamId].push(tranches[i]); + } + + // Effect: create the stream, mint the NFT and transfer the deposit amount. + _create({ + cancelable: cancelable, + depositAmount: depositAmount, + lockupModel: Lockup.Model.LOCKUP_TRANCHED, + recipient: recipient, + sender: sender, + streamId: streamId, + timestamps: timestamps, + token: token, + transferable: transferable + }); + + // Log the newly created stream. + emit ISablierLockupTranched.CreateLockupTranchedStream({ + streamId: streamId, + commonParams: Lockup.CreateEventCommon({ + funder: msg.sender, + sender: sender, + recipient: recipient, + depositAmount: depositAmount, + token: token, + cancelable: cancelable, + transferable: transferable, + timestamps: timestamps, + shape: shape + }), + tranches: tranches + }); + } +} diff --git a/src/interfaces/IAdminable.sol b/src/interfaces/IAdminable.sol deleted file mode 100644 index 62a13d1a7..000000000 --- a/src/interfaces/IAdminable.sol +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.22; - -/// @title IAdminable -/// @notice Contract module that provides a basic access control mechanism, with an admin that can be -/// granted exclusive access to specific functions. The inheriting contract must set the initial admin -/// in the constructor. -interface IAdminable { - /*////////////////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Emitted when the admin is transferred. - /// @param oldAdmin The address of the old admin. - /// @param newAdmin The address of the new admin. - event TransferAdmin(address indexed oldAdmin, address indexed newAdmin); - - /*////////////////////////////////////////////////////////////////////////// - CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice The address of the admin account or contract. - function admin() external view returns (address); - - /*////////////////////////////////////////////////////////////////////////// - NON-CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Transfers the contract admin to a new address. - /// - /// @dev Notes: - /// - Does not revert if the admin is the same. - /// - This function can potentially leave the contract without an admin, thereby removing any - /// functionality that is only available to the admin. - /// - /// Requirements: - /// - `msg.sender` must be the contract admin. - /// - /// @param newAdmin The address of the new admin. - function transferAdmin(address newAdmin) external; -} diff --git a/src/interfaces/IBatch.sol b/src/interfaces/IBatch.sol deleted file mode 100644 index dd6de1ac8..000000000 --- a/src/interfaces/IBatch.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.22; - -/// @notice This contract implements logic to batch call any function. -interface IBatch { - /// @notice Allows batched calls to self, i.e., `this` contract. - /// @dev Since `msg.value` can be reused across calls, be VERY CAREFUL when using it. Refer to - /// https://paradigm.xyz/2021/08/two-rights-might-make-a-wrong for more information. - /// @param calls An array of inputs for each call. - /// @return results An array of results from each call. Empty when the calls do not return anything. - function batch(bytes[] calldata calls) external payable returns (bytes[] memory results); -} diff --git a/src/interfaces/ILockupNFTDescriptor.sol b/src/interfaces/ILockupNFTDescriptor.sol index d80d7f38f..7c631a600 100644 --- a/src/interfaces/ILockupNFTDescriptor.sol +++ b/src/interfaces/ILockupNFTDescriptor.sol @@ -7,6 +7,10 @@ import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions /// @notice This contract generates the URI describing the Sablier stream NFTs. /// @dev Inspired by Uniswap V3 Positions NFTs. interface ILockupNFTDescriptor { + /*////////////////////////////////////////////////////////////////////////// + USER-FACING READ-ONLY FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + /// @notice Produces the URI describing a particular stream NFT. /// @dev This is a data URI with the JSON contents directly inlined. /// @param sablier The address of the Sablier contract the stream was created in. diff --git a/src/interfaces/ISablierBatchLockup.sol b/src/interfaces/ISablierBatchLockup.sol index 2c9795860..140d4d02c 100644 --- a/src/interfaces/ISablierBatchLockup.sol +++ b/src/interfaces/ISablierBatchLockup.sol @@ -3,27 +3,36 @@ pragma solidity >=0.8.22; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { ISablierLockup } from "../interfaces/ISablierLockup.sol"; - -import { BatchLockup } from "../types/DataTypes.sol"; +import { BatchLockup } from "../types/BatchLockup.sol"; +import { ISablierLockup } from "./ISablierLockup.sol"; /// @title ISablierBatchLockup /// @notice Helper to batch create Lockup streams. interface ISablierBatchLockup { /*////////////////////////////////////////////////////////////////////////// - SABLIER-LOCKUP-DYNAMIC + EVENTS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when a batch of Lockup streams are created. + /// @param funder The address funding the streams. + /// @param lockup The address of the {SablierLockup} contract used to create the streams. + /// @param streamIds The ids of the newly created streams, the ones that were successfully created. + event CreateLockupBatch(address indexed funder, ISablierLockup indexed lockup, uint256[] streamIds); + + /*////////////////////////////////////////////////////////////////////////// + USER-FACING STATE-CHANGING FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @notice Creates a batch of Lockup Dynamic streams using `createWithDurationsLD`. + /// @notice Creates a batch of LD streams using `createWithDurationsLD`. /// /// @dev Requirements: /// - There must be at least one element in `batch`. - /// - All requirements from {ISablierLockup.createWithDurationsLD} must be met for each stream. + /// - All requirements from {ISablierLockupDynamic.createWithDurationsLD} must be met for each stream. /// /// @param lockup The address of the {SablierLockup} contract. /// @param token The contract address of the ERC-20 token to be distributed. /// @param batch An array of structs, each encapsulating a subset of the parameters of - /// {SablierLockup.createWithDurationsLD}. + /// {ISablierLockupDynamic.createWithDurationsLD}. /// @return streamIds The ids of the newly created streams. function createWithDurationsLD( ISablierLockup lockup, @@ -33,16 +42,16 @@ interface ISablierBatchLockup { external returns (uint256[] memory streamIds); - /// @notice Creates a batch of Lockup Dynamic streams using `createWithTimestampsLD`. + /// @notice Creates a batch of LD streams using `createWithTimestampsLD`. /// /// @dev Requirements: /// - There must be at least one element in `batch`. - /// - All requirements from {ISablierLockup.createWithTimestampsLD} must be met for each stream. + /// - All requirements from {ISablierLockupDynamic.createWithTimestampsLD} must be met for each stream. /// /// @param lockup The address of the {SablierLockup} contract. /// @param token The contract address of the ERC-20 token to be distributed. /// @param batch An array of structs, each encapsulating a subset of the parameters of - /// {SablierLockup.createWithTimestampsLD}. + /// {ISablierLockupDynamic.createWithTimestampsLD}. /// @return streamIds The ids of the newly created streams. function createWithTimestampsLD( ISablierLockup lockup, @@ -52,20 +61,16 @@ interface ISablierBatchLockup { external returns (uint256[] memory streamIds); - /*////////////////////////////////////////////////////////////////////////// - SABLIER-LOCKUP-LINEAR - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Creates a batch of Lockup Linear streams using `createWithDurationsLL`. + /// @notice Creates a batch of LL streams using `createWithDurationsLL`. /// /// @dev Requirements: /// - There must be at least one element in `batch`. - /// - All requirements from {ISablierLockup.createWithDurationsLL} must be met for each stream. + /// - All requirements from {ISablierLockupLinear.createWithDurationsLL} must be met for each stream. /// /// @param lockup The address of the {SablierLockup} contract. /// @param token The contract address of the ERC-20 token to be distributed. /// @param batch An array of structs, each encapsulating a subset of the parameters of - /// {SablierLockup.createWithDurationsLL}. + /// {ISablierLockupLinear.createWithDurationsLL}. /// @return streamIds The ids of the newly created streams. function createWithDurationsLL( ISablierLockup lockup, @@ -75,16 +80,16 @@ interface ISablierBatchLockup { external returns (uint256[] memory streamIds); - /// @notice Creates a batch of Lockup Linear streams using `createWithTimestampsLL`. + /// @notice Creates a batch of LL streams using `createWithTimestampsLL`. /// /// @dev Requirements: /// - There must be at least one element in `batch`. - /// - All requirements from {ISablierLockup.createWithTimestampsLL} must be met for each stream. + /// - All requirements from {ISablierLockupLinear.createWithTimestampsLL} must be met for each stream. /// /// @param lockup The address of the {SablierLockup} contract. /// @param token The contract address of the ERC-20 token to be distributed. /// @param batch An array of structs, each encapsulating a subset of the parameters of - /// {SablierLockup.createWithTimestampsLL}. + /// {ISablierLockupLinear.createWithTimestampsLL}. /// @return streamIds The ids of the newly created streams. function createWithTimestampsLL( ISablierLockup lockup, @@ -94,20 +99,16 @@ interface ISablierBatchLockup { external returns (uint256[] memory streamIds); - /*////////////////////////////////////////////////////////////////////////// - SABLIER-LOCKUP-TRANCHED - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Creates a batch of Lockup Tranched streams using `createWithDurationsLT`. + /// @notice Creates a batch of LT streams using `createWithDurationsLT`. /// /// @dev Requirements: /// - There must be at least one element in `batch`. - /// - All requirements from {ISablierLockup.createWithDurationsLT} must be met for each stream. + /// - All requirements from {ISablierLockupTranched.createWithDurationsLT} must be met for each stream. /// /// @param lockup The address of the {SablierLockup} contract. /// @param token The contract address of the ERC-20 token to be distributed. /// @param batch An array of structs, each encapsulating a subset of the parameters of - /// {SablierLockup.createWithDurationsLT}. + /// {ISablierLockupTranched.createWithDurationsLT}. /// @return streamIds The ids of the newly created streams. function createWithDurationsLT( ISablierLockup lockup, @@ -117,16 +118,16 @@ interface ISablierBatchLockup { external returns (uint256[] memory streamIds); - /// @notice Creates a batch of Lockup Tranched streams using `createWithTimestampsLT`. + /// @notice Creates a batch of LT streams using `createWithTimestampsLT`. /// /// @dev Requirements: /// - There must be at least one element in `batch`. - /// - All requirements from {ISablierLockup.createWithTimestampsLT} must be met for each stream. + /// - All requirements from {ISablierLockupTranched.createWithTimestampsLT} must be met for each stream. /// /// @param lockup The address of the {SablierLockup} contract. /// @param token The contract address of the ERC-20 token to be distributed. /// @param batch An array of structs, each encapsulating a subset of the parameters of - /// {SablierLockup.createWithTimestampsLT}. + /// {ISablierLockupTranched.createWithTimestampsLT}. /// @return streamIds The ids of the newly created streams. function createWithTimestampsLT( ISablierLockup lockup, diff --git a/src/interfaces/ISablierLockup.sol b/src/interfaces/ISablierLockup.sol index 263af259b..554ebb6ba 100644 --- a/src/interfaces/ISablierLockup.sol +++ b/src/interfaces/ISablierLockup.sol @@ -1,251 +1,337 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity >=0.8.22; -import { Lockup, LockupDynamic, LockupLinear, LockupTranched } from "../types/DataTypes.sol"; -import { ISablierLockupBase } from "./ISablierLockupBase.sol"; +import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; +import { IBatch } from "@sablier/evm-utils/src/interfaces/IBatch.sol"; +import { IComptrollerable } from "@sablier/evm-utils/src/interfaces/IComptrollerable.sol"; +import { ISablierComptroller } from "@sablier/evm-utils/src/interfaces/ISablierComptroller.sol"; + +import { Lockup } from "../types/Lockup.sol"; +import { ILockupNFTDescriptor } from "./ILockupNFTDescriptor.sol"; +import { ISablierLockupDynamic } from "./ISablierLockupDynamic.sol"; +import { ISablierLockupLinear } from "./ISablierLockupLinear.sol"; +import { ISablierLockupTranched } from "./ISablierLockupTranched.sol"; /// @title ISablierLockup -/// @notice Creates and manages Lockup streams with various distribution models. -interface ISablierLockup is ISablierLockupBase { +/// @notice Interface to manage Lockup streams with various distribution models. +interface ISablierLockup is + IBatch, // 0 inherited components + IComptrollerable, // 0 inherited components + IERC4906, // 2 inherited components + IERC721Metadata, // 2 inherited components + ISablierLockupDynamic, // 1 inherited component + ISablierLockupLinear, // 1 inherited component + ISablierLockupTranched // 1 inherited component +{ /*////////////////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////////////////*/ - /// @notice Emitted when a stream is created using Lockup dynamic model. - /// @param streamId The ID of the newly created stream. - /// @param commonParams Common parameters emitted in Create events across all Lockup models. - /// @param segments The segments the protocol uses to compose the dynamic distribution function. - event CreateLockupDynamicStream( - uint256 indexed streamId, Lockup.CreateEventCommon commonParams, LockupDynamic.Segment[] segments - ); + /// @notice Emitted when the comptroller allows a new recipient contract to hook to Sablier. + /// @param comptroller The address of the current comptroller. + /// @param recipient The address of the recipient contract put on the allowlist. + event AllowToHook(ISablierComptroller indexed comptroller, address indexed recipient); - /// @notice Emitted when a stream is created using Lockup linear model. - /// @param streamId The ID of the newly created stream. - /// @param commonParams Common parameters emitted in Create events across all Lockup models. - /// @param cliffTime The Unix timestamp for the cliff period's end. A value of zero means there is no cliff. - /// @param unlockAmounts Struct encapsulating (i) the amount to unlock at the start time and (ii) the amount to - /// unlock at the cliff time. - event CreateLockupLinearStream( - uint256 indexed streamId, - Lockup.CreateEventCommon commonParams, - uint40 cliffTime, - LockupLinear.UnlockAmounts unlockAmounts + /// @notice Emitted when a stream is canceled. + /// @param streamId The ID of the stream. + /// @param sender The address of the stream's sender. + /// @param recipient The address of the stream's recipient. + /// @param token The contract address of the ERC-20 token that has been distributed. + /// @param senderAmount The amount of tokens refunded to the stream's sender, denoted in units of the token's + /// decimals. + /// @param recipientAmount The amount of tokens left for the stream's recipient to withdraw, denoted in units of the + /// token's decimals. + event CancelLockupStream( + uint256 streamId, + address indexed sender, + address indexed recipient, + IERC20 indexed token, + uint128 senderAmount, + uint128 recipientAmount ); - /// @notice Emitted when a stream is created using Lockup tranched model. - /// @param streamId The ID of the newly created stream. - /// @param commonParams Common parameters emitted in Create events across all Lockup models. - /// @param tranches The tranches the protocol uses to compose the tranched distribution function. - event CreateLockupTranchedStream( - uint256 indexed streamId, Lockup.CreateEventCommon commonParams, LockupTranched.Tranche[] tranches + /// @notice Emitted when canceling multiple streams and one particular cancellation reverts. + /// @param streamId The ID of the stream that reverted the cancellation. + /// @param revertData The error data returned by the reverted cancel. + event InvalidStreamInCancelMultiple(uint256 indexed streamId, bytes revertData); + + /// @notice Emitted when withdrawing from multiple streams and one particular withdrawal reverts. + /// @param streamId The ID of the stream that reverted the withdrawal. + /// @param revertData The error data returned by the reverted withdraw. + event InvalidWithdrawalInWithdrawMultiple(uint256 indexed streamId, bytes revertData); + + /// @notice Emitted when a sender gives up the right to cancel a stream. + /// @param streamId The ID of the stream. + event RenounceLockupStream(uint256 indexed streamId); + + /// @notice Emitted when the comptroller sets a new NFT descriptor contract. + /// @param comptroller The address of the current comptroller. + /// @param oldNFTDescriptor The address of the old NFT descriptor contract. + /// @param newNFTDescriptor The address of the new NFT descriptor contract. + event SetNFTDescriptor( + ISablierComptroller indexed comptroller, + ILockupNFTDescriptor indexed oldNFTDescriptor, + ILockupNFTDescriptor indexed newNFTDescriptor ); + /// @notice Emitted when tokens are withdrawn from a stream. + /// @param streamId The ID of the stream. + /// @param to The address that has received the withdrawn tokens. + /// @param token The contract address of the ERC-20 token that has been withdrawn. + /// @param amount The amount of tokens withdrawn, denoted in units of the token's decimals. + event WithdrawFromLockupStream(uint256 indexed streamId, address indexed to, IERC20 indexed token, uint128 amount); + /*////////////////////////////////////////////////////////////////////////// - CONSTANT FUNCTIONS + USER-FACING READ-ONLY FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @notice The maximum number of segments and tranches allowed in Dynamic and Tranched streams respectively. - /// @dev This is initialized at construction time and cannot be changed later. - function MAX_COUNT() external view returns (uint256); + /// @notice Calculates the minimum fee in wei required to withdraw from the given stream ID. + /// @dev Reverts if `streamId` references a null stream. + /// @param streamId The stream ID for the query. + function calculateMinFeeWei(uint256 streamId) external view returns (uint256 minFeeWei); - /// @notice Retrieves the stream's cliff time, which is a Unix timestamp. A value of zero means there is no cliff. - /// @dev Reverts if `streamId` references a null stream or a non Lockup Linear stream. + /// @notice Retrieves the stream's recipient. + /// @dev Reverts if the NFT has been burned. /// @param streamId The stream ID for the query. - function getCliffTime(uint256 streamId) external view returns (uint40 cliffTime); + function getRecipient(uint256 streamId) external view returns (address recipient); - /// @notice Retrieves the segments used to compose the dynamic distribution function. - /// @dev Reverts if `streamId` references a null stream or a non Lockup Dynamic stream. + /// @notice Retrieves a flag indicating whether the stream is cold, i.e. settled, canceled, or depleted. + /// @dev Reverts if `streamId` references a null stream. /// @param streamId The stream ID for the query. - /// @return segments See the documentation in {DataTypes}. - function getSegments(uint256 streamId) external view returns (LockupDynamic.Segment[] memory segments); + function isCold(uint256 streamId) external view returns (bool result); - /// @notice Retrieves the tranches used to compose the tranched distribution function. - /// @dev Reverts if `streamId` references a null stream or a non Lockup Tranched stream. + /// @notice Retrieves a flag indicating whether the stream is warm, i.e. either pending or streaming. + /// @dev Reverts if `streamId` references a null stream. /// @param streamId The stream ID for the query. - /// @return tranches See the documentation in {DataTypes}. - function getTranches(uint256 streamId) external view returns (LockupTranched.Tranche[] memory tranches); + function isWarm(uint256 streamId) external view returns (bool result); - /// @notice Retrieves the unlock amounts used to compose the linear distribution function. - /// @dev Reverts if `streamId` references a null stream or a non Lockup Linear stream. + /// @notice Calculates the amount that the sender would be refunded if the stream were canceled, denoted in units + /// of the token's decimals. + /// @dev Reverts if `streamId` references a null stream. /// @param streamId The stream ID for the query. - /// @return unlockAmounts See the documentation in {DataTypes}. - function getUnlockAmounts(uint256 streamId) - external - view - returns (LockupLinear.UnlockAmounts memory unlockAmounts); + function refundableAmountOf(uint256 streamId) external view returns (uint128 refundableAmount); + + /// @notice Retrieves the stream's status. + /// @dev Reverts if `streamId` references a null stream. + /// @param streamId The stream ID for the query. + function statusOf(uint256 streamId) external view returns (Lockup.Status status); + + /// @notice Calculates the amount streamed to the recipient, denoted in units of the token's decimals. + /// @dev Reverts if `streamId` references a null stream. + /// + /// Notes: + /// - Upon cancellation of the stream, the amount streamed is calculated as the difference between the deposited + /// amount and the refunded amount. Ultimately, when the stream becomes depleted, the streamed amount is equivalent + /// to the total amount withdrawn. + /// + /// @param streamId The stream ID for the query. + function streamedAmountOf(uint256 streamId) external view returns (uint128 streamedAmount); + + /// @notice Calculates the amount that the recipient can withdraw from the stream, denoted in units of the token's + /// decimals. + /// @dev Reverts if `streamId` references a null stream. + /// @param streamId The stream ID for the query. + function withdrawableAmountOf(uint256 streamId) external view returns (uint128 withdrawableAmount); /*////////////////////////////////////////////////////////////////////////// - NON-CONSTANT FUNCTIONS + USER-FACING STATE-CHANGING FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @notice Creates a stream by setting the start time to `block.timestamp`, and the end time to the sum of - /// `block.timestamp` and all specified time durations. The segment timestamps are derived from these - /// durations. The stream is funded by `msg.sender` and is wrapped in an ERC-721 NFT. + /// @notice Allows a recipient contract to hook to Sablier when a stream is canceled or when tokens are withdrawn. + /// Useful for implementing contracts that hold streams on behalf of users, such as vaults or staking contracts. /// - /// @dev Emits a {Transfer}, {CreateLockupDynamicStream} and {MetadataUpdate} event. + /// @dev Emits an {AllowToHook} event. + /// + /// Notes: + /// - Does not revert if the contract is already on the allowlist. + /// - This is an irreversible operation. The contract cannot be removed from the allowlist. /// /// Requirements: - /// - All requirements in {createWithTimestampsLD} must be met for the calculated parameters. - /// - /// @param params Struct encapsulating the function parameters, which are documented in {DataTypes}. - /// @param segmentsWithDuration Segments with durations used to compose the dynamic distribution function. Timestamps - /// are calculated by starting from `block.timestamp` and adding each duration to the previous timestamp. - /// @return streamId The ID of the newly created stream. - function createWithDurationsLD( - Lockup.CreateWithDurations calldata params, - LockupDynamic.SegmentWithDuration[] calldata segmentsWithDuration - ) - external - payable - returns (uint256 streamId); + /// - `msg.sender` must be the comptroller contract. + /// - `recipient` must implement {ISablierLockupRecipient}. + /// + /// @param recipient The address of the contract to allow for hooks. + function allowToHook(address recipient) external; - /// @notice Creates a stream by setting the start time to `block.timestamp`, and the end time to - /// the sum of `block.timestamp` and `durations.total`. The stream is funded by `msg.sender` and is wrapped in an - /// ERC-721 NFT. + /// @notice Burns the NFT associated with the stream. /// - /// @dev Emits a {Transfer}, {CreateLockupLinearStream} and {MetadataUpdate} event. + /// @dev Emits a {Transfer} and {MetadataUpdate} event. /// /// Requirements: - /// - All requirements in {createWithTimestampsLL} must be met for the calculated parameters. - /// - /// @param params Struct encapsulating the function parameters, which are documented in {DataTypes}. - /// @param durations Struct encapsulating (i) cliff period duration and (ii) total stream duration, both in seconds. - /// @param unlockAmounts Struct encapsulating (i) the amount to unlock at the start time and (ii) the amount to - /// unlock at the cliff time. - /// @return streamId The ID of the newly created stream. - function createWithDurationsLL( - Lockup.CreateWithDurations calldata params, - LockupLinear.UnlockAmounts calldata unlockAmounts, - LockupLinear.Durations calldata durations - ) - external - payable - returns (uint256 streamId); + /// - Must not be delegate called. + /// - `streamId` must reference a depleted stream. + /// - The NFT must exist. + /// - `msg.sender` must be either the NFT owner or an approved third party. + /// + /// @param streamId The ID of the stream NFT to burn. + function burn(uint256 streamId) external payable; - /// @notice Creates a stream by setting the start time to `block.timestamp`, and the end time to the sum of - /// `block.timestamp` and all specified time durations. The tranche timestamps are derived from these - /// durations. The stream is funded by `msg.sender` and is wrapped in an ERC-721 NFT. + /// @notice Cancels the stream and refunds any remaining tokens to the sender. /// - /// @dev Emits a {Transfer}, {CreateLockupTrancheStream} and {MetadataUpdate} event. + /// @dev Emits a {Transfer}, {CancelLockupStream} and {MetadataUpdate} event. + /// + /// Notes: + /// - If there any tokens left for the recipient to withdraw, the stream is marked as canceled. Otherwise, the + /// stream is marked as depleted. + /// - If the address is on the allowlist, this function will invoke a hook on the recipient. /// /// Requirements: - /// - All requirements in {createWithTimestampsLT} must be met for the calculated parameters. - /// - /// @param params Struct encapsulating the function parameters, which are documented in {DataTypes}. - /// @param tranchesWithDuration Tranches with durations used to compose the tranched distribution function. - /// Timestamps are calculated by starting from `block.timestamp` and adding each duration to the previous timestamp. - /// @return streamId The ID of the newly created stream. - function createWithDurationsLT( - Lockup.CreateWithDurations calldata params, - LockupTranched.TrancheWithDuration[] calldata tranchesWithDuration - ) - external - payable - returns (uint256 streamId); + /// - Must not be delegate called. + /// - The stream must be warm and cancelable. + /// - `msg.sender` must be the stream's sender. + /// + /// @param streamId The ID of the stream to cancel. + /// @return refundedAmount The amount refunded to the sender, denoted in units of the token's decimals. + function cancel(uint256 streamId) external payable returns (uint128 refundedAmount); + + /// @notice Cancels multiple streams and refunds any remaining tokens to the sender. + /// + /// @dev Emits multiple {Transfer}, {CancelLockupStream} and {MetadataUpdate} events. For each reverted + /// cancellation, it emits an {InvalidStreamInCancelMultiple} event. + /// + /// Notes: + /// - This function as a whole will not revert if one or more cancellations revert. A zero amount is returned for + /// reverted streams. + /// - Refer to the notes and requirements from {cancel}. + /// + /// @param streamIds The IDs of the streams to cancel. + /// @return refundedAmounts The amounts refunded to the sender, denoted in units of the token's decimals. + function cancelMultiple(uint256[] calldata streamIds) external payable returns (uint128[] memory refundedAmounts); - /// @notice Creates a stream with the provided segment timestamps, implying the end time from the last timestamp. - /// The stream is funded by `msg.sender` and is wrapped in an ERC-721 NFT. + /// @notice Recover the surplus amount of tokens. /// - /// @dev Emits a {Transfer}, {CreateLockupDynamicStream} and {MetadataUpdate} event. + /// @dev Notes: + /// - The surplus amount is defined as the difference between the total balance of the contract for the provided + /// ERC-20 token and the sum of balances of all streams created using the same ERC-20 token. + /// + /// Requirements: + /// - `msg.sender` must be the comptroller contract. + /// - The surplus amount must be greater than zero. + /// + /// @param token The contract address of the ERC-20 token to recover for. + /// @param to The address to send the surplus amount. + function recover(IERC20 token, address to) external; + + /// @notice Removes the right of the stream's sender to cancel the stream. + /// + /// @dev Emits a {RenounceLockupStream} event. /// /// Notes: - /// - As long as the segment timestamps are arranged in ascending order, it is not an error for some - /// of them to be in the past. + /// - This is an irreversible operation. /// /// Requirements: /// - Must not be delegate called. - /// - `params.totalAmount` must be greater than zero. - /// - If set, `params.broker.fee` must not be greater than `MAX_BROKER_FEE`. - /// - `params.timestamps.start` must be greater than zero and less than the first segment's timestamp. - /// - `segments` must have at least one segment, but not more than `MAX_COUNT`. - /// - The segment timestamps must be arranged in ascending order. - /// - `params.timestamps.end` must be equal to the last segment's timestamp. - /// - The sum of the segment amounts must equal the deposit amount. - /// - `params.recipient` must not be the zero address. - /// - `params.sender` must not be the zero address. - /// - `msg.sender` must have allowed this contract to spend at least `params.totalAmount` tokens. - /// - `params.shape.length` must not be greater than 32 characters. - /// - /// @param params Struct encapsulating the function parameters, which are documented in {DataTypes}. - /// @param segments Segments used to compose the dynamic distribution function. - /// @return streamId The ID of the newly created stream. - function createWithTimestampsLD( - Lockup.CreateWithTimestamps calldata params, - LockupDynamic.Segment[] calldata segments - ) - external - payable - returns (uint256 streamId); + /// - `streamId` must reference a warm stream. + /// - `msg.sender` must be the stream's sender. + /// - The stream must be cancelable. + /// + /// @param streamId The ID of the stream to renounce. + function renounce(uint256 streamId) external payable; + + /// @notice Sets the native token address. Once set, it cannot be changed. + /// @dev For more information, see the documentation for {nativeToken}. + /// + /// Notes: + /// - If `newNativeToken` is zero address, the function does not revert. + /// + /// Requirements: + /// - `msg.sender` must be the comptroller contract. + /// - The current native token must be zero address. + /// @param newNativeToken The address of the native token. + function setNativeToken(address newNativeToken) external; + + /// @notice Sets a new NFT descriptor contract, which produces the URI describing the Sablier stream NFTs. + /// + /// @dev Emits a {SetNFTDescriptor} and {BatchMetadataUpdate} event. + /// + /// Notes: + /// - Does not revert if the NFT descriptor is the same. + /// + /// Requirements: + /// - `msg.sender` must be the comptroller contract. + /// + /// @param newNFTDescriptor The address of the new NFT descriptor contract. + function setNFTDescriptor(ILockupNFTDescriptor newNFTDescriptor) external; - /// @notice Creates a stream with the provided start time and end time. The stream is funded by `msg.sender` and is - /// wrapped in an ERC-721 NFT. + /// @notice Withdraws the provided amount of tokens from the stream to the `to` address. /// - /// @dev Emits a {Transfer}, {CreateLockupLinearStream} and {MetadataUpdate} event. + /// @dev Emits a {Transfer}, {WithdrawFromLockupStream} and {MetadataUpdate} event. /// /// Notes: - /// - A cliff time of zero means there is no cliff. - /// - As long as the times are ordered, it is not an error for the start or the cliff time to be in the past. + /// - If `msg.sender` is not the recipient and the address is on the allowlist, this function will invoke a hook on + /// the recipient. + /// - The minimum fee in wei is calculated for the stream's sender using the {SablierComptroller} contract. /// /// Requirements: /// - Must not be delegate called. - /// - `params.totalAmount` must be greater than zero. - /// - If set, `params.broker.fee` must not be greater than `MAX_BROKER_FEE`. - /// - `params.timestamps.start` must be greater than zero and less than `params.timestamps.end`. - /// - If set, `cliffTime` must be greater than `params.timestamps.start` and less than - /// `params.timestamps.end`. - /// - `params.recipient` must not be the zero address. - /// - `params.sender` must not be the zero address. - /// - The sum of `params.unlockAmounts.start` and `params.unlockAmounts.cliff` must be less than or equal to - /// deposit amount. - /// - If `params.timestamps.cliff` not set, the `params.unlockAmounts.cliff` must be zero. - /// - `msg.sender` must have allowed this contract to spend at least `params.totalAmount` tokens. - /// - `params.shape.length` must not be greater than 32 characters. - /// - /// @param params Struct encapsulating the function parameters, which are documented in {DataTypes}. - /// @param cliffTime The Unix timestamp for the cliff period's end. A value of zero means there is no cliff. - /// @param unlockAmounts Struct encapsulating (i) the amount to unlock at the start time and (ii) the amount to - /// unlock at the cliff time. - /// @return streamId The ID of the newly created stream. - function createWithTimestampsLL( - Lockup.CreateWithTimestamps calldata params, - LockupLinear.UnlockAmounts calldata unlockAmounts, - uint40 cliffTime + /// - `streamId` must not reference a null or depleted stream. + /// - `to` must not be the zero address. + /// - `amount` must be greater than zero and must not exceed the withdrawable amount. + /// - `to` must be the recipient if `msg.sender` is not the stream's recipient or an approved third party. + /// - `msg.value` must be greater than or equal to the minimum fee in wei for the stream's sender. + /// + /// @param streamId The ID of the stream to withdraw from. + /// @param to The address receiving the withdrawn tokens. + /// @param amount The amount to withdraw, denoted in units of the token's decimals. + function withdraw(uint256 streamId, address to, uint128 amount) external payable; + + /// @notice Withdraws the maximum withdrawable amount from the stream to the provided address `to`. + /// + /// @dev Emits a {Transfer}, {WithdrawFromLockupStream} and {MetadataUpdate} event. + /// + /// Notes: + /// - Refer to the notes in {withdraw}. + /// + /// Requirements: + /// - Refer to the requirements in {withdraw}. + /// + /// @param streamId The ID of the stream to withdraw from. + /// @param to The address receiving the withdrawn tokens. + /// @return withdrawnAmount The amount withdrawn, denoted in units of the token's decimals. + function withdrawMax(uint256 streamId, address to) external payable returns (uint128 withdrawnAmount); + + /// @notice Withdraws the maximum withdrawable amount from the stream to the current recipient, and transfers the + /// NFT to `newRecipient`. + /// + /// @dev Emits a {WithdrawFromLockupStream}, {Transfer} and {MetadataUpdate} event. + /// + /// Notes: + /// - If the withdrawable amount is zero, the withdrawal is skipped. + /// - Refer to the notes in {withdraw}. + /// + /// Requirements: + /// - `msg.sender` must be either the NFT owner or an approved third party. + /// - Refer to the requirements in {withdraw}. + /// - Refer to the requirements in {IERC721.transferFrom}. + /// + /// @param streamId The ID of the stream NFT to transfer. + /// @param newRecipient The address of the new owner of the stream NFT. + /// @return withdrawnAmount The amount withdrawn, denoted in units of the token's decimals. + function withdrawMaxAndTransfer( + uint256 streamId, + address newRecipient ) external payable - returns (uint256 streamId); + returns (uint128 withdrawnAmount); - /// @notice Creates a stream with the provided tranche timestamps, implying the end time from the last timestamp. - /// The stream is funded by `msg.sender` and is wrapped in an ERC-721 NFT. + /// @notice Withdraws tokens from streams to the recipient of each stream. /// - /// @dev Emits a {Transfer}, {CreateLockupTrancheStream} and {MetadataUpdate} event. + /// @dev Emits multiple {Transfer}, {WithdrawFromLockupStream} and {MetadataUpdate} events. For each reverting + /// withdrawal, it emits an {InvalidWithdrawalInWithdrawMultiple} event. /// /// Notes: - /// - As long as the tranche timestamps are arranged in ascending order, it is not an error for some - /// of them to be in the past. + /// - This function as a whole will not revert if one or more withdrawals revert. + /// - This function attempts to call a hook on the recipient of each stream, unless `msg.sender` is the recipient. + /// - Refer to the notes and requirements from {withdraw}. /// /// Requirements: /// - Must not be delegate called. - /// - `params.totalAmount` must be greater than zero. - /// - If set, `params.broker.fee` must not be greater than `MAX_BROKER_FEE`. - /// - `params.timestamps.start` must be greater than zero and less than the first tranche's timestamp. - /// - `tranches` must have at least one tranche, but not more than `MAX_COUNT`. - /// - The tranche timestamps must be arranged in ascending order. - /// - `params.timestamps.end` must be equal to the last tranche's timestamp. - /// - The sum of the tranche amounts must equal the deposit amount. - /// - `params.recipient` must not be the zero address. - /// - `params.sender` must not be the zero address. - /// - `msg.sender` must have allowed this contract to spend at least `params.totalAmount` tokens. - /// - `params.shape.length` must not be greater than 32 characters. - /// - /// @param params Struct encapsulating the function parameters, which are documented in {DataTypes}. - /// @param tranches Tranches used to compose the tranched distribution function. - /// @return streamId The ID of the newly created stream. - function createWithTimestampsLT( - Lockup.CreateWithTimestamps calldata params, - LockupTranched.Tranche[] calldata tranches - ) - external - payable - returns (uint256 streamId); + /// - There must be an equal number of `streamIds` and `amounts`. + /// + /// @param streamIds The IDs of the streams to withdraw from. + /// @param amounts The amounts to withdraw, denoted in units of the token's decimals. + function withdrawMultiple(uint256[] calldata streamIds, uint128[] calldata amounts) external payable; } diff --git a/src/interfaces/ISablierLockupBase.sol b/src/interfaces/ISablierLockupBase.sol deleted file mode 100644 index 75cd97ef1..000000000 --- a/src/interfaces/ISablierLockupBase.sol +++ /dev/null @@ -1,399 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.22; - -import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; -import { UD60x18 } from "@prb/math/src/UD60x18.sol"; - -import { Lockup } from "../types/DataTypes.sol"; -import { IAdminable } from "./IAdminable.sol"; -import { IBatch } from "./IBatch.sol"; -import { ILockupNFTDescriptor } from "./ILockupNFTDescriptor.sol"; - -/// @title ISablierLockupBase -/// @notice Common logic between all Sablier Lockup contracts. -interface ISablierLockupBase is - IAdminable, // 0 inherited components - IBatch, // 0 inherited components - IERC4906, // 2 inherited components - IERC721Metadata // 2 inherited components -{ - /*////////////////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Emitted when the admin allows a new recipient contract to hook to Sablier. - /// @param admin The address of the current contract admin. - /// @param recipient The address of the recipient contract put on the allowlist. - event AllowToHook(address indexed admin, address recipient); - - /// @notice Emitted when a stream is canceled. - /// @param streamId The ID of the stream. - /// @param sender The address of the stream's sender. - /// @param recipient The address of the stream's recipient. - /// @param token The contract address of the ERC-20 token that has been distributed. - /// @param senderAmount The amount of tokens refunded to the stream's sender, denoted in units of the token's - /// decimals. - /// @param recipientAmount The amount of tokens left for the stream's recipient to withdraw, denoted in units of the - /// token's decimals. - event CancelLockupStream( - uint256 streamId, - address indexed sender, - address indexed recipient, - IERC20 indexed token, - uint128 senderAmount, - uint128 recipientAmount - ); - - /// @notice Emitted when the accrued fees are collected. - /// @param admin The address of the current contract admin, which has received the fees. - /// @param feeAmount The amount of collected fees. - event CollectFees(address indexed admin, uint256 indexed feeAmount); - - /// @notice Emitted when withdrawing from multiple streams and one particular withdrawal reverts. - /// @param streamId The stream ID that reverted during withdraw. - /// @param revertData The error data returned by the reverted withdraw. - event InvalidWithdrawalInWithdrawMultiple(uint256 streamId, bytes revertData); - - /// @notice Emitted when a sender gives up the right to cancel a stream. - /// @param streamId The ID of the stream. - event RenounceLockupStream(uint256 indexed streamId); - - /// @notice Emitted when the admin sets a new NFT descriptor contract. - /// @param admin The address of the current contract admin. - /// @param oldNFTDescriptor The address of the old NFT descriptor contract. - /// @param newNFTDescriptor The address of the new NFT descriptor contract. - event SetNFTDescriptor( - address indexed admin, ILockupNFTDescriptor oldNFTDescriptor, ILockupNFTDescriptor newNFTDescriptor - ); - - /// @notice Emitted when tokens are withdrawn from a stream. - /// @param streamId The ID of the stream. - /// @param to The address that has received the withdrawn tokens. - /// @param token The contract address of the ERC-20 token that has been withdrawn. - /// @param amount The amount of tokens withdrawn, denoted in units of the token's decimals. - event WithdrawFromLockupStream(uint256 indexed streamId, address indexed to, IERC20 indexed token, uint128 amount); - - /*////////////////////////////////////////////////////////////////////////// - CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Retrieves the maximum broker fee that can be charged by the broker, denoted as a fixed-point - /// number where 1e18 is 100%. - /// @dev This value is hard coded as a constant. - function MAX_BROKER_FEE() external view returns (UD60x18); - - /// @notice Retrieves the amount deposited in the stream, denoted in units of the token's decimals. - /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream ID for the query. - function getDepositedAmount(uint256 streamId) external view returns (uint128 depositedAmount); - - /// @notice Retrieves the stream's end time, which is a Unix timestamp. - /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream ID for the query. - function getEndTime(uint256 streamId) external view returns (uint40 endTime); - - /// @notice Retrieves the distribution models used to create the stream. - /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream ID for the query. - function getLockupModel(uint256 streamId) external view returns (Lockup.Model lockupModel); - - /// @notice Retrieves the stream's recipient. - /// @dev Reverts if the NFT has been burned. - /// @param streamId The stream ID for the query. - function getRecipient(uint256 streamId) external view returns (address recipient); - - /// @notice Retrieves the amount refunded to the sender after a cancellation, denoted in units of the token's - /// decimals. This amount is always zero unless the stream was canceled. - /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream ID for the query. - function getRefundedAmount(uint256 streamId) external view returns (uint128 refundedAmount); - - /// @notice Retrieves the stream's sender. - /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream ID for the query. - function getSender(uint256 streamId) external view returns (address sender); - - /// @notice Retrieves the stream's start time, which is a Unix timestamp. - /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream ID for the query. - function getStartTime(uint256 streamId) external view returns (uint40 startTime); - - /// @notice Retrieves the address of the underlying ERC-20 token being distributed. - /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream ID for the query. - function getUnderlyingToken(uint256 streamId) external view returns (IERC20 token); - - /// @notice Retrieves the amount withdrawn from the stream, denoted in units of the token's decimals. - /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream ID for the query. - function getWithdrawnAmount(uint256 streamId) external view returns (uint128 withdrawnAmount); - - /// @notice Retrieves a flag indicating whether the provided address is a contract allowed to hook to Sablier - /// when a stream is canceled or when tokens are withdrawn. - /// @dev See {ISablierLockupRecipient} for more information. - function isAllowedToHook(address recipient) external view returns (bool result); - - /// @notice Retrieves a flag indicating whether the stream can be canceled. When the stream is cold, this - /// flag is always `false`. - /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream ID for the query. - function isCancelable(uint256 streamId) external view returns (bool result); - - /// @notice Retrieves a flag indicating whether the stream is cold, i.e. settled, canceled, or depleted. - /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream ID for the query. - function isCold(uint256 streamId) external view returns (bool result); - - /// @notice Retrieves a flag indicating whether the stream is depleted. - /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream ID for the query. - function isDepleted(uint256 streamId) external view returns (bool result); - - /// @notice Retrieves a flag indicating whether the stream exists. - /// @dev Does not revert if `streamId` references a null stream. - /// @param streamId The stream ID for the query. - function isStream(uint256 streamId) external view returns (bool result); - - /// @notice Retrieves a flag indicating whether the stream NFT can be transferred. - /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream ID for the query. - function isTransferable(uint256 streamId) external view returns (bool result); - - /// @notice Retrieves a flag indicating whether the stream is warm, i.e. either pending or streaming. - /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream ID for the query. - function isWarm(uint256 streamId) external view returns (bool result); - - /// @notice Counter for stream IDs, used in the create functions. - function nextStreamId() external view returns (uint256); - - /// @notice Contract that generates the non-fungible token URI. - function nftDescriptor() external view returns (ILockupNFTDescriptor); - - /// @notice Calculates the amount that the sender would be refunded if the stream were canceled, denoted in units - /// of the token's decimals. - /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream ID for the query. - function refundableAmountOf(uint256 streamId) external view returns (uint128 refundableAmount); - - /// @notice Retrieves the stream's status. - /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream ID for the query. - function statusOf(uint256 streamId) external view returns (Lockup.Status status); - - /// @notice Calculates the amount streamed to the recipient, denoted in units of the token's decimals. - /// @dev Reverts if `streamId` references a null stream. - /// - /// Notes: - /// - Upon cancellation of the stream, the amount streamed is calculated as the difference between the deposited - /// amount and the refunded amount. Ultimately, when the stream becomes depleted, the streamed amount is equivalent - /// to the total amount withdrawn. - /// - /// @param streamId The stream ID for the query. - function streamedAmountOf(uint256 streamId) external view returns (uint128 streamedAmount); - - /// @notice Retrieves a flag indicating whether the stream was canceled. - /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream ID for the query. - function wasCanceled(uint256 streamId) external view returns (bool result); - - /// @notice Calculates the amount that the recipient can withdraw from the stream, denoted in units of the token's - /// decimals. - /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream ID for the query. - function withdrawableAmountOf(uint256 streamId) external view returns (uint128 withdrawableAmount); - - /*////////////////////////////////////////////////////////////////////////// - NON-CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Allows a recipient contract to hook to Sablier when a stream is canceled or when tokens are withdrawn. - /// Useful for implementing contracts that hold streams on behalf of users, such as vaults or staking contracts. - /// - /// @dev Emits an {AllowToHook} event. - /// - /// Notes: - /// - Does not revert if the contract is already on the allowlist. - /// - This is an irreversible operation. The contract cannot be removed from the allowlist. - /// - /// Requirements: - /// - `msg.sender` must be the contract admin. - /// - `recipient` must have a non-zero code size. - /// - `recipient` must implement {ISablierLockupRecipient}. - /// - /// @param recipient The address of the contract to allow for hooks. - function allowToHook(address recipient) external; - - /// @notice Burns the NFT associated with the stream. - /// - /// @dev Emits a {Transfer} and {MetadataUpdate} event. - /// - /// Requirements: - /// - Must not be delegate called. - /// - `streamId` must reference a depleted stream. - /// - The NFT must exist. - /// - `msg.sender` must be either the NFT owner or an approved third party. - /// - /// @param streamId The ID of the stream NFT to burn. - function burn(uint256 streamId) external payable; - - /// @notice Cancels the stream and refunds any remaining tokens to the sender. - /// - /// @dev Emits a {Transfer}, {CancelLockupStream} and {MetadataUpdate} event. - /// - /// Notes: - /// - If there any tokens left for the recipient to withdraw, the stream is marked as canceled. Otherwise, the - /// stream is marked as depleted. - /// - If the address is on the allowlist, this function will invoke a hook on the recipient. - /// - /// Requirements: - /// - Must not be delegate called. - /// - The stream must be warm and cancelable. - /// - `msg.sender` must be the stream's sender. - /// - /// @param streamId The ID of the stream to cancel. - function cancel(uint256 streamId) external payable; - - /// @notice Cancels multiple streams and refunds any remaining tokens to the sender. - /// - /// @dev Emits multiple {Transfer}, {CancelLockupStream} and {MetadataUpdate} events. - /// - /// Notes: - /// - Refer to the notes in {cancel}. - /// - /// Requirements: - /// - All requirements from {cancel} must be met for each stream. - /// - /// @param streamIds The IDs of the streams to cancel. - function cancelMultiple(uint256[] calldata streamIds) external payable; - - /// @notice Collects the accrued fees by transferring them to the contract admin. - /// - /// @dev Emits a {CollectFees} event. - /// - /// Notes: - /// - If the admin is a contract, it must be able to receive native token payments, e.g., ETH for Ethereum Mainnet. - function collectFees() external; - - /// @notice Removes the right of the stream's sender to cancel the stream. - /// - /// @dev Emits a {RenounceLockupStream} event. - /// - /// Notes: - /// - This is an irreversible operation. - /// - /// Requirements: - /// - Must not be delegate called. - /// - `streamId` must reference a warm stream. - /// - `msg.sender` must be the stream's sender. - /// - The stream must be cancelable. - /// - /// @param streamId The ID of the stream to renounce. - function renounce(uint256 streamId) external payable; - - /// @notice Renounces multiple streams. - /// - /// @dev Emits multiple {RenounceLockupStream} events. - /// - /// Notes: - /// - Refer to the notes in {renounce}. - /// - /// Requirements: - /// - All requirements from {renounce} must be met for each stream. - /// - /// @param streamIds An array of stream IDs to renounce. - function renounceMultiple(uint256[] calldata streamIds) external payable; - - /// @notice Sets a new NFT descriptor contract, which produces the URI describing the Sablier stream NFTs. - /// - /// @dev Emits a {SetNFTDescriptor} and {BatchMetadataUpdate} event. - /// - /// Notes: - /// - Does not revert if the NFT descriptor is the same. - /// - /// Requirements: - /// - `msg.sender` must be the contract admin. - /// - /// @param newNFTDescriptor The address of the new NFT descriptor contract. - function setNFTDescriptor(ILockupNFTDescriptor newNFTDescriptor) external; - - /// @notice Withdraws the provided amount of tokens from the stream to the `to` address. - /// - /// @dev Emits a {Transfer}, {WithdrawFromLockupStream} and {MetadataUpdate} event. - /// - /// Notes: - /// - If `msg.sender` is not the recipient and the address is on the allowlist, this function will invoke a hook on - /// the recipient. - /// - /// Requirements: - /// - Must not be delegate called. - /// - `streamId` must not reference a null or depleted stream. - /// - `to` must not be the zero address. - /// - `amount` must be greater than zero and must not exceed the withdrawable amount. - /// - `to` must be the recipient if `msg.sender` is not the stream's recipient or an approved third party. - /// - /// @param streamId The ID of the stream to withdraw from. - /// @param to The address receiving the withdrawn tokens. - /// @param amount The amount to withdraw, denoted in units of the token's decimals. - function withdraw(uint256 streamId, address to, uint128 amount) external payable; - - /// @notice Withdraws the maximum withdrawable amount from the stream to the provided address `to`. - /// - /// @dev Emits a {Transfer}, {WithdrawFromLockupStream} and {MetadataUpdate} event. - /// - /// Notes: - /// - Refer to the notes in {withdraw}. - /// - /// Requirements: - /// - Refer to the requirements in {withdraw}. - /// - /// @param streamId The ID of the stream to withdraw from. - /// @param to The address receiving the withdrawn tokens. - /// @return withdrawnAmount The amount withdrawn, denoted in units of the token's decimals. - function withdrawMax(uint256 streamId, address to) external payable returns (uint128 withdrawnAmount); - - /// @notice Withdraws the maximum withdrawable amount from the stream to the current recipient, and transfers the - /// NFT to `newRecipient`. - /// - /// @dev Emits a {WithdrawFromLockupStream}, {Transfer} and {MetadataUpdate} event. - /// - /// Notes: - /// - If the withdrawable amount is zero, the withdrawal is skipped. - /// - Refer to the notes in {withdraw}. - /// - /// Requirements: - /// - `msg.sender` must be either the NFT owner or an approved third party. - /// - Refer to the requirements in {withdraw}. - /// - Refer to the requirements in {IERC721.transferFrom}. - /// - /// @param streamId The ID of the stream NFT to transfer. - /// @param newRecipient The address of the new owner of the stream NFT. - /// @return withdrawnAmount The amount withdrawn, denoted in units of the token's decimals. - function withdrawMaxAndTransfer( - uint256 streamId, - address newRecipient - ) - external - payable - returns (uint128 withdrawnAmount); - - /// @notice Withdraws tokens from streams to the recipient of each stream. - /// - /// @dev Emits multiple {Transfer}, {WithdrawFromLockupStream} and {MetadataUpdate} events. For each stream that - /// reverted the withdrawal, it emits an {InvalidWithdrawalInWithdrawMultiple} event. - /// - /// Notes: - /// - This function attempts to call a hook on the recipient of each stream, unless `msg.sender` is the recipient. - /// - /// Requirements: - /// - Must not be delegate called. - /// - There must be an equal number of `streamIds` and `amounts`. - /// - Each stream ID in the array must not reference a null or depleted stream. - /// - Each amount in the array must be greater than zero and must not exceed the withdrawable amount. - /// - /// @param streamIds The IDs of the streams to withdraw from. - /// @param amounts The amounts to withdraw, denoted in units of the token's decimals. - function withdrawMultiple(uint256[] calldata streamIds, uint128[] calldata amounts) external payable; -} diff --git a/src/interfaces/ISablierLockupDynamic.sol b/src/interfaces/ISablierLockupDynamic.sol new file mode 100644 index 000000000..c475e68c3 --- /dev/null +++ b/src/interfaces/ISablierLockupDynamic.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { Lockup } from "../types/Lockup.sol"; +import { LockupDynamic } from "../types/LockupDynamic.sol"; +import { ISablierLockupState } from "./ISablierLockupState.sol"; + +/// @title ISablierLockupDynamic +/// @notice Creates Lockup streams with dynamic distribution model. +interface ISablierLockupDynamic is ISablierLockupState { + /*////////////////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when an LD stream is created. + /// @param streamId The ID of the newly created stream. + /// @param commonParams Common parameters emitted in Create events across all Lockup models. + /// @param segments The segments the protocol uses to compose the dynamic distribution function. + event CreateLockupDynamicStream( + uint256 indexed streamId, Lockup.CreateEventCommon commonParams, LockupDynamic.Segment[] segments + ); + + /*////////////////////////////////////////////////////////////////////////// + USER-FACING STATE-CHANGING FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Creates a stream by setting the start time to `block.timestamp`, and the end time to the sum of + /// `block.timestamp` and all specified time durations. The segment timestamps are derived from these + /// durations. The stream is funded by `msg.sender` and is wrapped in an ERC-721 NFT. + /// + /// @dev Emits a {Transfer}, {CreateLockupDynamicStream} and {MetadataUpdate} event. + /// + /// Requirements: + /// - All requirements in {createWithTimestampsLD} must be met for the calculated parameters. + /// + /// @param params Struct encapsulating the function parameters, which are documented in {Lockup} type. + /// @param segmentsWithDuration Segments with durations used to compose the dynamic distribution function. Timestamps + /// are calculated by starting from `block.timestamp` and adding each duration to the previous timestamp. + /// @return streamId The ID of the newly created stream. + function createWithDurationsLD( + Lockup.CreateWithDurations calldata params, + LockupDynamic.SegmentWithDuration[] calldata segmentsWithDuration + ) + external + payable + returns (uint256 streamId); + + /// @notice Creates a stream with the provided segment timestamps, implying the end time from the last timestamp. + /// The stream is funded by `msg.sender` and is wrapped in an ERC-721 NFT. + /// + /// @dev Emits a {Transfer}, {CreateLockupDynamicStream} and {MetadataUpdate} event. + /// + /// Notes: + /// - As long as the segment timestamps are arranged in ascending order, it is not an error for some + /// of them to be in the past. + /// + /// Requirements: + /// - Must not be delegate called. + /// - `params.depositAmount` must be greater than zero. + /// - `params.timestamps.start` must be greater than zero and less than the first segment's timestamp. + /// - `segments` must have at least one segment. + /// - The segment timestamps must be arranged in ascending order. + /// - `params.timestamps.end` must be equal to the last segment's timestamp. + /// - The sum of the segment amounts must equal the deposit amount. + /// - `params.recipient` must not be the zero address. + /// - `params.sender` must not be the zero address. + /// - `msg.sender` must have allowed this contract to spend at least `params.depositAmount` tokens. + /// - `params.token` must not be the native token. + /// - `params.shape.length` must not be greater than 32 characters. + /// + /// @param params Struct encapsulating the function parameters, which are documented in {Lockup} type. + /// @param segments Segments used to compose the dynamic distribution function. + /// @return streamId The ID of the newly created stream. + function createWithTimestampsLD( + Lockup.CreateWithTimestamps calldata params, + LockupDynamic.Segment[] calldata segments + ) + external + payable + returns (uint256 streamId); +} diff --git a/src/interfaces/ISablierLockupLinear.sol b/src/interfaces/ISablierLockupLinear.sol new file mode 100644 index 000000000..4c0b2d2a7 --- /dev/null +++ b/src/interfaces/ISablierLockupLinear.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { Lockup } from "../types/Lockup.sol"; +import { LockupLinear } from "../types/LockupLinear.sol"; +import { ISablierLockupState } from "./ISablierLockupState.sol"; + +/// @title ISablierLockupLinear +/// @notice Creates Lockup streams with linear distribution model. +interface ISablierLockupLinear is ISablierLockupState { + /*////////////////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when an LL stream is created. + /// @param streamId The ID of the newly created stream. + /// @param commonParams Common parameters emitted in Create events across all Lockup models. + /// @param cliffTime The Unix timestamp for the cliff period's end. A value of zero means there is no cliff. + /// @param unlockAmounts Struct encapsulating (i) the amount to unlock at the start time and (ii) the amount to + /// unlock at the cliff time. + event CreateLockupLinearStream( + uint256 indexed streamId, + Lockup.CreateEventCommon commonParams, + uint40 cliffTime, + LockupLinear.UnlockAmounts unlockAmounts + ); + + /*////////////////////////////////////////////////////////////////////////// + USER-FACING STATE-CHANGING FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Creates a stream by setting the start time to `block.timestamp`, and the end time to + /// the sum of `block.timestamp` and `durations.total`. The stream is funded by `msg.sender` and is wrapped in an + /// ERC-721 NFT. + /// + /// @dev Emits a {Transfer}, {CreateLockupLinearStream} and {MetadataUpdate} event. + /// + /// Requirements: + /// - All requirements in {createWithTimestampsLL} must be met for the calculated parameters. + /// + /// @param params Struct encapsulating the function parameters, which are documented in {Lockup} type. + /// @param durations Struct encapsulating (i) cliff period duration and (ii) total stream duration, both in seconds. + /// @param unlockAmounts Struct encapsulating (i) the amount to unlock at the start time and (ii) the amount to + /// unlock at the cliff time. + /// @return streamId The ID of the newly created stream. + function createWithDurationsLL( + Lockup.CreateWithDurations calldata params, + LockupLinear.UnlockAmounts calldata unlockAmounts, + LockupLinear.Durations calldata durations + ) + external + payable + returns (uint256 streamId); + + /// @notice Creates a stream with the provided start time and end time. The stream is funded by `msg.sender` and is + /// wrapped in an ERC-721 NFT. + /// + /// @dev Emits a {Transfer}, {CreateLockupLinearStream} and {MetadataUpdate} event. + /// + /// Notes: + /// - A cliff time of zero means there is no cliff. + /// - As long as the times are ordered, it is not an error for the start or the cliff time to be in the past. + /// + /// Requirements: + /// - Must not be delegate called. + /// - `params.depositAmount` must be greater than zero. + /// - `params.timestamps.start` must be greater than zero and less than `params.timestamps.end`. + /// - If set, `cliffTime` must be greater than `params.timestamps.start` and less than + /// `params.timestamps.end`. + /// - `params.recipient` must not be the zero address. + /// - `params.sender` must not be the zero address. + /// - The sum of `params.unlockAmounts.start` and `params.unlockAmounts.cliff` must be less than or equal to + /// deposit amount. + /// - If `params.timestamps.cliff` not set, the `params.unlockAmounts.cliff` must be zero. + /// - `msg.sender` must have allowed this contract to spend at least `params.depositAmount` tokens. + /// - `params.token` must not be the native token. + /// - `params.shape.length` must not be greater than 32 characters. + /// + /// @param params Struct encapsulating the function parameters, which are documented in {Lockup} type. + /// @param cliffTime The Unix timestamp for the cliff period's end. A value of zero means there is no cliff. + /// @param unlockAmounts Struct encapsulating (i) the amount to unlock at the start time and (ii) the amount to + /// unlock at the cliff time. + /// @return streamId The ID of the newly created stream. + function createWithTimestampsLL( + Lockup.CreateWithTimestamps calldata params, + LockupLinear.UnlockAmounts calldata unlockAmounts, + uint40 cliffTime + ) + external + payable + returns (uint256 streamId); +} diff --git a/src/interfaces/ISablierLockupRecipient.sol b/src/interfaces/ISablierLockupRecipient.sol index 4dc33f0e4..defb8d26a 100644 --- a/src/interfaces/ISablierLockupRecipient.sol +++ b/src/interfaces/ISablierLockupRecipient.sol @@ -5,12 +5,16 @@ import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol /// @title ISablierLockupRecipient /// @notice Interface for recipient contracts capable of reacting to cancellations and withdrawals. For this to be able -/// to hook into Sablier, it must fully implement this interface and it must have been allowlisted by the Lockup -/// contract's admin. +/// to hook into Sablier, it must fully implement this interface and it must have been allowlisted in the Lockup +/// contract. /// @dev See {IERC165-supportsInterface}. /// The implementation MUST implement the {IERC165-supportsInterface} method, which MUST return `true` when called with /// `0xf8ee98d3`, i.e. `type(ISablierLockupRecipient).interfaceId`. interface ISablierLockupRecipient is IERC165 { + /*////////////////////////////////////////////////////////////////////////// + USER-FACING STATE-CHANGING FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + /// @notice Responds to cancellations. /// /// @dev Notes: diff --git a/src/interfaces/ISablierLockupState.sol b/src/interfaces/ISablierLockupState.sol new file mode 100644 index 000000000..4181f57d3 --- /dev/null +++ b/src/interfaces/ISablierLockupState.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { Lockup } from "../types/Lockup.sol"; +import { LockupDynamic } from "../types/LockupDynamic.sol"; +import { LockupLinear } from "../types/LockupLinear.sol"; +import { LockupTranched } from "../types/LockupTranched.sol"; +import { ILockupNFTDescriptor } from "./ILockupNFTDescriptor.sol"; + +/// @title ISablierLockupState +/// @notice Contract with state variables (storage and constants) for the {SablierLockup} contract, their respective +/// getters and helpful modifiers. +interface ISablierLockupState { + /*////////////////////////////////////////////////////////////////////////// + USER-FACING READ-ONLY FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Retrieves the aggregate amount across all streams, denoted in units of the token's decimals. + /// @dev If tokens are directly transferred to the contract without using the stream creation functions, the + /// ERC-20 balance may be greater than the aggregate amount. + /// @param token The ERC-20 token for the query. + function aggregateAmount(IERC20 token) external view returns (uint256); + + /// @notice Retrieves the stream's cliff time, which is a Unix timestamp. A value of zero means there is no cliff. + /// @dev Reverts if `streamId` references either a null stream or a non-LL stream. + /// @param streamId The stream ID for the query. + function getCliffTime(uint256 streamId) external view returns (uint40 cliffTime); + + /// @notice Retrieves the amount deposited in the stream, denoted in units of the token's decimals. + /// @dev Reverts if `streamId` references a null stream. + /// @param streamId The stream ID for the query. + function getDepositedAmount(uint256 streamId) external view returns (uint128 depositedAmount); + + /// @notice Retrieves the stream's end time, which is a Unix timestamp. + /// @dev Reverts if `streamId` references a null stream. + /// @param streamId The stream ID for the query. + function getEndTime(uint256 streamId) external view returns (uint40 endTime); + + /// @notice Retrieves the distribution models used to create the stream. + /// @dev Reverts if `streamId` references a null stream. + /// @param streamId The stream ID for the query. + function getLockupModel(uint256 streamId) external view returns (Lockup.Model lockupModel); + + /// @notice Retrieves the amount refunded to the sender after a cancellation, denoted in units of the token's + /// decimals. This amount is always zero unless the stream was canceled. + /// @dev Reverts if `streamId` references a null stream. + /// @param streamId The stream ID for the query. + function getRefundedAmount(uint256 streamId) external view returns (uint128 refundedAmount); + + /// @notice Retrieves the segments used to compose the dynamic distribution function. + /// @dev Reverts if `streamId` references either a null stream or a non-LD stream. + /// @param streamId The stream ID for the query. + /// @return segments See the documentation in {LockupDynamic} type. + function getSegments(uint256 streamId) external view returns (LockupDynamic.Segment[] memory segments); + + /// @notice Retrieves the stream's sender. + /// @dev Reverts if `streamId` references a null stream. + /// @param streamId The stream ID for the query. + function getSender(uint256 streamId) external view returns (address sender); + + /// @notice Retrieves the stream's start time, which is a Unix timestamp. + /// @dev Reverts if `streamId` references a null stream. + /// @param streamId The stream ID for the query. + function getStartTime(uint256 streamId) external view returns (uint40 startTime); + + /// @notice Retrieves the tranches used to compose the tranched distribution function. + /// @dev Reverts if `streamId` references either a null stream or a non-LT stream. + /// @param streamId The stream ID for the query. + /// @return tranches See the documentation in {LockupTranched} type. + function getTranches(uint256 streamId) external view returns (LockupTranched.Tranche[] memory tranches); + + /// @notice Retrieves the address of the underlying ERC-20 token being distributed. + /// @dev Reverts if `streamId` references a null stream. + /// @param streamId The stream ID for the query. + function getUnderlyingToken(uint256 streamId) external view returns (IERC20 token); + + /// @notice Retrieves the unlock amounts used to compose the linear distribution function. + /// @dev Reverts if `streamId` references either a null stream or a non-LL stream. + /// @param streamId The stream ID for the query. + /// @return unlockAmounts See the documentation in {LockupLinear} type. + function getUnlockAmounts(uint256 streamId) + external + view + returns (LockupLinear.UnlockAmounts memory unlockAmounts); + + /// @notice Retrieves the amount withdrawn from the stream, denoted in units of the token's decimals. + /// @dev Reverts if `streamId` references a null stream. + /// @param streamId The stream ID for the query. + function getWithdrawnAmount(uint256 streamId) external view returns (uint128 withdrawnAmount); + + /// @notice Retrieves a flag indicating whether the provided address is a contract allowed to hook to Sablier + /// when a stream is canceled or when tokens are withdrawn. + /// @dev See {ISablierLockupRecipient} for more information. + function isAllowedToHook(address recipient) external view returns (bool result); + + /// @notice Retrieves a flag indicating whether the stream can be canceled. When the stream is cold, this + /// flag is always `false`. + /// @dev Reverts if `streamId` references a null stream. + /// @param streamId The stream ID for the query. + function isCancelable(uint256 streamId) external view returns (bool result); + + /// @notice Retrieves a flag indicating whether the stream is depleted. + /// @dev Reverts if `streamId` references a null stream. + /// @param streamId The stream ID for the query. + function isDepleted(uint256 streamId) external view returns (bool result); + + /// @notice Retrieves a flag indicating whether the stream exists. + /// @dev Does not revert if `streamId` references a null stream. + /// @param streamId The stream ID for the query. + function isStream(uint256 streamId) external view returns (bool result); + + /// @notice Retrieves a flag indicating whether the stream NFT can be transferred. + /// @dev Reverts if `streamId` references a null stream. + /// @param streamId The stream ID for the query. + function isTransferable(uint256 streamId) external view returns (bool result); + + /// @notice Retrieves the address of the ERC-20 interface of the native token, if it exists. + /// @dev The native tokens on some chains have a dual interface as ERC-20. For example, on Polygon the $POL token + /// is the native token and has an ERC-20 version at 0x0000000000000000000000000000000000001010. This means + /// that `address(this).balance` returns the same value as `balanceOf(address(this))`. To avoid any unintended + /// behavior, these tokens cannot be used in Sablier. As an alternative, users can use the Wrapped version of the + /// token, i.e. WMATIC, which is a standard ERC-20 token. + function nativeToken() external view returns (address); + + /// @notice Counter for stream IDs, used in the create functions. + function nextStreamId() external view returns (uint256); + + /// @notice Contract that generates the non-fungible token URI. + function nftDescriptor() external view returns (ILockupNFTDescriptor); + + /// @notice Retrieves a flag indicating whether the stream was canceled. + /// @dev Reverts if `streamId` references a null stream. + /// @param streamId The stream ID for the query. + function wasCanceled(uint256 streamId) external view returns (bool result); +} diff --git a/src/interfaces/ISablierLockupTranched.sol b/src/interfaces/ISablierLockupTranched.sol new file mode 100644 index 000000000..3befb1ffb --- /dev/null +++ b/src/interfaces/ISablierLockupTranched.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { Lockup } from "../types/Lockup.sol"; +import { LockupTranched } from "../types/LockupTranched.sol"; +import { ISablierLockupState } from "./ISablierLockupState.sol"; + +/// @title ISablierLockupTranched +/// @notice Creates Lockup streams with tranched distribution model. +interface ISablierLockupTranched is ISablierLockupState { + /*////////////////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when an LT stream is created. + /// @param streamId The ID of the newly created stream. + /// @param commonParams Common parameters emitted in Create events across all Lockup models. + /// @param tranches The tranches the protocol uses to compose the tranched distribution function. + event CreateLockupTranchedStream( + uint256 indexed streamId, Lockup.CreateEventCommon commonParams, LockupTranched.Tranche[] tranches + ); + + /*////////////////////////////////////////////////////////////////////////// + USER-FACING STATE-CHANGING FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Creates a stream by setting the start time to `block.timestamp`, and the end time to the sum of + /// `block.timestamp` and all specified time durations. The tranche timestamps are derived from these + /// durations. The stream is funded by `msg.sender` and is wrapped in an ERC-721 NFT. + /// + /// @dev Emits a {Transfer}, {CreateLockupTrancheStream} and {MetadataUpdate} event. + /// + /// Requirements: + /// - All requirements in {createWithTimestampsLT} must be met for the calculated parameters. + /// + /// @param params Struct encapsulating the function parameters, which are documented in {Lockup} type. + /// @param tranchesWithDuration Tranches with durations used to compose the tranched distribution function. + /// Timestamps are calculated by starting from `block.timestamp` and adding each duration to the previous timestamp. + /// @return streamId The ID of the newly created stream. + function createWithDurationsLT( + Lockup.CreateWithDurations calldata params, + LockupTranched.TrancheWithDuration[] calldata tranchesWithDuration + ) + external + payable + returns (uint256 streamId); + + /// @notice Creates a stream with the provided tranche timestamps, implying the end time from the last timestamp. + /// The stream is funded by `msg.sender` and is wrapped in an ERC-721 NFT. + /// + /// @dev Emits a {Transfer}, {CreateLockupTrancheStream} and {MetadataUpdate} event. + /// + /// Notes: + /// - As long as the tranche timestamps are arranged in ascending order, it is not an error for some + /// of them to be in the past. + /// + /// Requirements: + /// - Must not be delegate called. + /// - `params.depositAmount` must be greater than zero. + /// - `params.timestamps.start` must be greater than zero and less than the first tranche's timestamp. + /// - `tranches` must have at least one tranche. + /// - The tranche timestamps must be arranged in ascending order. + /// - `params.timestamps.end` must be equal to the last tranche's timestamp. + /// - The sum of the tranche amounts must equal the deposit amount. + /// - `params.recipient` must not be the zero address. + /// - `params.sender` must not be the zero address. + /// - `msg.sender` must have allowed this contract to spend at least `params.depositAmount` tokens. + /// - `params.token` must not be the native token. + /// - `params.shape.length` must not be greater than 32 characters. + /// + /// @param params Struct encapsulating the function parameters, which are documented in {Lockup} type. + /// @param tranches Tranches used to compose the tranched distribution function. + /// @return streamId The ID of the newly created stream. + function createWithTimestampsLT( + Lockup.CreateWithTimestamps calldata params, + LockupTranched.Tranche[] calldata tranches + ) + external + payable + returns (uint256 streamId); +} diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index 3d647cb52..f93e88c25 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -1,53 +1,30 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity >=0.8.22; -import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; -import { UD60x18 } from "@prb/math/src/UD60x18.sol"; - -import { Lockup } from "../types/DataTypes.sol"; +import { Lockup } from "../types/Lockup.sol"; /// @title Errors /// @notice Library containing all custom errors the protocol may revert with. library Errors { - /*////////////////////////////////////////////////////////////////////////// - GENERICS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Thrown when an unexpected error occurs during a batch call. - error BatchError(bytes errorData); - - /// @notice Thrown when `msg.sender` is not the admin. - error CallerNotAdmin(address admin, address caller); - - /// @notice Thrown when trying to delegate call to a function that disallows delegate calls. - error DelegateCall(); - /*////////////////////////////////////////////////////////////////////////// SABLIER-BATCH-LOCKUP //////////////////////////////////////////////////////////////////////////*/ error SablierBatchLockup_BatchSizeZero(); - /*////////////////////////////////////////////////////////////////////////// - LOCKUP-NFT-DESCRIPTOR - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Thrown when trying to generate the token URI for an unknown ERC-721 NFT contract. - error LockupNFTDescriptor_UnknownNFT(IERC721Metadata nft, string symbol); - /*////////////////////////////////////////////////////////////////////////// HELPERS //////////////////////////////////////////////////////////////////////////*/ - /// @notice Thrown when the broker fee exceeds the maximum allowed fee. - error SablierHelpers_BrokerFeeTooHigh(UD60x18 brokerFee, UD60x18 maxBrokerFee); - /// @notice Thrown when trying to create a linear stream with a cliff time not strictly less than the end time. error SablierHelpers_CliffTimeNotLessThanEndTime(uint40 cliffTime, uint40 endTime); /// @notice Thrown when trying to create a stream with a non zero cliff unlock amount when the cliff time is zero. error SablierHelpers_CliffTimeZeroUnlockAmountNotZero(uint128 cliffUnlockAmount); + /// @notice Thrown when trying to create a stream with the native token. + error SablierHelpers_CreateNativeToken(address nativeToken); + /// @notice Thrown when trying to create a dynamic stream with a deposit amount not equal to the sum of the segment /// amounts. error SablierHelpers_DepositAmountNotEqualToSegmentAmountsSum(uint128 depositAmount, uint128 segmentAmountsSum); @@ -65,9 +42,6 @@ library Errors { /// @notice Thrown when trying to create a tranched stream with end time not equal to the last tranche's timestamp. error SablierHelpers_EndTimeNotEqualToLastTrancheTimestamp(uint40 endTime, uint40 lastTrancheTimestamp); - /// @notice Thrown when trying to create a dynamic stream with more segments than the maximum allowed. - error SablierHelpers_SegmentCountTooHigh(uint256 count); - /// @notice Thrown when trying to create a dynamic stream with no segments. error SablierHelpers_SegmentCountZero(); @@ -98,9 +72,6 @@ library Errors { /// @notice Thrown when trying to create a stream with a zero start time. error SablierHelpers_StartTimeZero(); - /// @notice Thrown when trying to create a tranched stream with more tranches than the maximum allowed. - error SablierHelpers_TrancheCountTooHigh(uint256 count); - /// @notice Thrown when trying to create a tranched stream with no tranches. error SablierHelpers_TrancheCountZero(); @@ -114,65 +85,71 @@ library Errors { ); /*////////////////////////////////////////////////////////////////////////// - SABLIER-LOCKUP-BASE + SABLIER-LOCKUP //////////////////////////////////////////////////////////////////////////*/ /// @notice Thrown when trying to allow to hook a contract that doesn't implement the interface correctly. - error SablierLockupBase_AllowToHookUnsupportedInterface(address recipient); + error SablierLockup_AllowToHookUnsupportedInterface(address recipient); /// @notice Thrown when trying to allow to hook an address with no code. - error SablierLockupBase_AllowToHookZeroCodeSize(address recipient); + error SablierLockup_AllowToHookZeroCodeSize(address recipient); + + /// @notice Thrown when trying to withdraw with a fee amount less than the minimum fee. + error SablierLockup_InsufficientFeePayment(uint256 feePaid, uint256 minFeeWei); /// @notice Thrown when the fee transfer fails. - error SablierLockupBase_FeeTransferFail(address admin, uint256 feeAmount); + error SablierLockup_FeeTransferFailed(address comptroller, uint256 feeAmount); /// @notice Thrown when the hook does not return the correct selector. - error SablierLockupBase_InvalidHookSelector(address recipient); + error SablierLockup_InvalidHookSelector(address recipient); - /// @notice Thrown when trying to transfer Stream NFT when transferability is disabled. - error SablierLockupBase_NotTransferable(uint256 tokenId); + /// @notice Thrown when trying to set the native token address when it is already set. + error SablierLockup_NativeTokenAlreadySet(address nativeToken); - /// @notice Thrown when the ID references a null stream. - error SablierLockupBase_Null(uint256 streamId); + /// @notice Thrown when trying to transfer Stream NFT when transferability is disabled. + error SablierLockup_NotTransferable(uint256 tokenId); /// @notice Thrown when trying to withdraw an amount greater than the withdrawable amount. - error SablierLockupBase_Overdraw(uint256 streamId, uint128 amount, uint128 withdrawableAmount); + error SablierLockup_Overdraw(uint256 streamId, uint128 amount, uint128 withdrawableAmount); /// @notice Thrown when trying to cancel or renounce a canceled stream. - error SablierLockupBase_StreamCanceled(uint256 streamId); + error SablierLockup_StreamCanceled(uint256 streamId); /// @notice Thrown when trying to cancel, renounce, or withdraw from a depleted stream. - error SablierLockupBase_StreamDepleted(uint256 streamId); + error SablierLockup_StreamDepleted(uint256 streamId); /// @notice Thrown when trying to cancel or renounce a stream that is not cancelable. - error SablierLockupBase_StreamNotCancelable(uint256 streamId); + error SablierLockup_StreamNotCancelable(uint256 streamId); /// @notice Thrown when trying to burn a stream that is not depleted. - error SablierLockupBase_StreamNotDepleted(uint256 streamId); + error SablierLockup_StreamNotDepleted(uint256 streamId); /// @notice Thrown when trying to cancel or renounce a settled stream. - error SablierLockupBase_StreamSettled(uint256 streamId); + error SablierLockup_StreamSettled(uint256 streamId); /// @notice Thrown when `msg.sender` lacks authorization to perform an action. - error SablierLockupBase_Unauthorized(uint256 streamId, address caller); + error SablierLockup_Unauthorized(uint256 streamId, address caller); /// @notice Thrown when trying to withdraw to an address other than the recipient's. - error SablierLockupBase_WithdrawalAddressNotRecipient(uint256 streamId, address caller, address to); + error SablierLockup_WithdrawalAddressNotRecipient(uint256 streamId, address caller, address to); /// @notice Thrown when trying to withdraw zero tokens from a stream. - error SablierLockupBase_WithdrawAmountZero(uint256 streamId); + error SablierLockup_WithdrawAmountZero(uint256 streamId); /// @notice Thrown when trying to withdraw from multiple streams and the number of stream IDs does /// not match the number of withdraw amounts. - error SablierLockupBase_WithdrawArrayCountsNotEqual(uint256 streamIdsCount, uint256 amountsCount); + error SablierLockup_WithdrawArrayCountsNotEqual(uint256 streamIdsCount, uint256 amountsCount); /// @notice Thrown when trying to withdraw to the zero address. - error SablierLockupBase_WithdrawToZeroAddress(uint256 streamId); + error SablierLockup_WithdrawToZeroAddress(uint256 streamId); /*////////////////////////////////////////////////////////////////////////// - SABLIER-LOCKUP + SABLIER-LOCKUP-STATE //////////////////////////////////////////////////////////////////////////*/ /// @notice Thrown when a function is called on a stream that does not use the expected Lockup model. - error SablierLockup_NotExpectedModel(Lockup.Model actualLockupModel, Lockup.Model expectedLockupModel); + error SablierLockupState_NotExpectedModel(Lockup.Model actualLockupModel, Lockup.Model expectedLockupModel); + + /// @notice Thrown when the ID references a null stream. + error SablierLockupState_Null(uint256 streamId); } diff --git a/src/libraries/Helpers.sol b/src/libraries/Helpers.sol index eda3e1a2e..b462913a9 100644 --- a/src/libraries/Helpers.sol +++ b/src/libraries/Helpers.sol @@ -1,15 +1,17 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity >=0.8.22; -import { UD60x18, ud } from "@prb/math/src/UD60x18.sol"; -import { Lockup, LockupDynamic, LockupLinear, LockupTranched } from "./../types/DataTypes.sol"; +import { Lockup } from "../types/Lockup.sol"; +import { LockupDynamic } from "../types/LockupDynamic.sol"; +import { LockupLinear } from "../types/LockupLinear.sol"; +import { LockupTranched } from "../types/LockupTranched.sol"; import { Errors } from "./Errors.sol"; /// @title Helpers -/// @notice Library with functions needed to validate input parameters across lockup streams. +/// @notice Library with functions needed to validate input parameters across Lockup streams. library Helpers { /*////////////////////////////////////////////////////////////////////////// - CONSTANT FUNCTIONS + USER-FACING READ-ONLY FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ /// @dev Calculate the timestamps and return the segments. @@ -77,121 +79,71 @@ library Helpers { } /// @dev Checks the parameters of the {SablierLockup-_createLD} function. - function checkCreateLockupDynamic( + function checkCreateLD( address sender, Lockup.Timestamps memory timestamps, - uint128 totalAmount, + uint128 depositAmount, LockupDynamic.Segment[] memory segments, - uint256 maxCount, - UD60x18 brokerFee, - string memory shape, - UD60x18 maxBrokerFee + address token, + address nativeToken, + string memory shape ) public pure - returns (Lockup.CreateAmounts memory createAmounts) { - // Check: verify the broker fee and calculate the amounts. - createAmounts = _checkAndCalculateBrokerFee(totalAmount, brokerFee, maxBrokerFee); - // Check: validate the user-provided common parameters. - _checkCreateStream(sender, createAmounts.deposit, timestamps.start, shape); + _checkCreateStream(sender, depositAmount, timestamps.start, token, nativeToken, shape); // Check: validate the user-provided segments. - _checkSegments(segments, createAmounts.deposit, timestamps, maxCount); + _checkSegments(segments, depositAmount, timestamps); } /// @dev Checks the parameters of the {SablierLockup-_createLL} function. - function checkCreateLockupLinear( + function checkCreateLL( address sender, Lockup.Timestamps memory timestamps, uint40 cliffTime, - uint128 totalAmount, + uint128 depositAmount, LockupLinear.UnlockAmounts memory unlockAmounts, - UD60x18 brokerFee, - string memory shape, - UD60x18 maxBrokerFee + address token, + address nativeToken, + string memory shape ) public pure - returns (Lockup.CreateAmounts memory createAmounts) { - // Check: verify the broker fee and calculate the amounts. - createAmounts = _checkAndCalculateBrokerFee(totalAmount, brokerFee, maxBrokerFee); - // Check: validate the user-provided common parameters. - _checkCreateStream(sender, createAmounts.deposit, timestamps.start, shape); + _checkCreateStream(sender, depositAmount, timestamps.start, token, nativeToken, shape); // Check: validate the user-provided cliff and end times. - _checkTimestampsAndUnlockAmounts(createAmounts.deposit, timestamps, cliffTime, unlockAmounts); + _checkTimestampsAndUnlockAmounts(depositAmount, timestamps, cliffTime, unlockAmounts); } /// @dev Checks the parameters of the {SablierLockup-_createLT} function. - function checkCreateLockupTranched( + function checkCreateLT( address sender, Lockup.Timestamps memory timestamps, - uint128 totalAmount, + uint128 depositAmount, LockupTranched.Tranche[] memory tranches, - uint256 maxCount, - UD60x18 brokerFee, - string memory shape, - UD60x18 maxBrokerFee + address token, + address nativeToken, + string memory shape ) public pure - returns (Lockup.CreateAmounts memory createAmounts) { - // Check: verify the broker fee and calculate the amounts. - createAmounts = _checkAndCalculateBrokerFee(totalAmount, brokerFee, maxBrokerFee); - // Check: validate the user-provided common parameters. - _checkCreateStream(sender, createAmounts.deposit, timestamps.start, shape); + _checkCreateStream(sender, depositAmount, timestamps.start, token, nativeToken, shape); // Check: validate the user-provided segments. - _checkTranches(tranches, createAmounts.deposit, timestamps, maxCount); + _checkTranches(tranches, depositAmount, timestamps); } /*////////////////////////////////////////////////////////////////////////// - PRIVATE CONSTANT FUNCTIONS + PRIVATE READ-ONLY FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @dev Checks the broker fee is not greater than `maxBrokerFee`, and then calculates the broker fee amount and - /// the deposit amount from the total amount. - function _checkAndCalculateBrokerFee( - uint128 totalAmount, - UD60x18 brokerFee, - UD60x18 maxBrokerFee - ) - private - pure - returns (Lockup.CreateAmounts memory amounts) - { - // When the total amount is zero, the broker fee is also zero. - if (totalAmount == 0) { - return Lockup.CreateAmounts(0, 0); - } - - // If the broker fee is zero, the deposit amount is the total amount. - if (brokerFee.isZero()) { - return Lockup.CreateAmounts(totalAmount, 0); - } - - // Check: the broker fee is not greater than `maxBrokerFee`. - if (brokerFee.gt(maxBrokerFee)) { - revert Errors.SablierHelpers_BrokerFeeTooHigh(brokerFee, maxBrokerFee); - } - - // Calculate the broker fee amount. - amounts.brokerFee = ud(totalAmount).mul(brokerFee).intoUint128(); - - // Assert that the total amount is strictly greater than the broker fee amount. - assert(totalAmount > amounts.brokerFee); - - // Calculate the deposit amount (the amount to stream, net of the broker fee). - amounts.deposit = totalAmount - amounts.brokerFee; - } - - /// @dev Checks the user-provided cliff, end times and unlock amounts of a lockup linear stream. + /// @dev Checks the user-provided cliff, end times, and unlock amounts of an LL stream. function _checkTimestampsAndUnlockAmounts( uint128 depositAmount, Lockup.Timestamps memory timestamps, @@ -231,11 +183,13 @@ library Helpers { } } - /// @dev Checks the user-provided common parameters across lockup streams. + /// @dev Checks the user-provided common parameters across Lockup streams. function _checkCreateStream( address sender, uint128 depositAmount, uint40 startTime, + address token, + address nativeToken, string memory shape ) private @@ -256,6 +210,11 @@ library Helpers { revert Errors.SablierHelpers_StartTimeZero(); } + // Check: the token is not the native token. + if (token == nativeToken) { + revert Errors.SablierHelpers_CreateNativeToken(nativeToken); + } + // Check: the shape is not greater than 32 bytes. if (bytes(shape).length > 32) { revert Errors.SablierHelpers_ShapeExceeds32Bytes(bytes(shape).length); @@ -272,8 +231,7 @@ library Helpers { function _checkSegments( LockupDynamic.Segment[] memory segments, uint128 depositAmount, - Lockup.Timestamps memory timestamps, - uint256 maxSegmentCount + Lockup.Timestamps memory timestamps ) private pure @@ -284,11 +242,6 @@ library Helpers { revert Errors.SablierHelpers_SegmentCountZero(); } - // Check: the segment count is not greater than the maximum allowed. - if (segmentCount > maxSegmentCount) { - revert Errors.SablierHelpers_SegmentCountTooHigh(segmentCount); - } - // Check: the start time is strictly less than the first segment timestamp. if (timestamps.start >= segments[0].timestamp) { revert Errors.SablierHelpers_StartTimeNotLessThanFirstSegmentTimestamp( @@ -344,8 +297,7 @@ library Helpers { function _checkTranches( LockupTranched.Tranche[] memory tranches, uint128 depositAmount, - Lockup.Timestamps memory timestamps, - uint256 maxTrancheCount + Lockup.Timestamps memory timestamps ) private pure @@ -356,11 +308,6 @@ library Helpers { revert Errors.SablierHelpers_TrancheCountZero(); } - // Check: the tranche count is not greater than the maximum allowed. - if (trancheCount > maxTrancheCount) { - revert Errors.SablierHelpers_TrancheCountTooHigh(trancheCount); - } - // Check: the start time is strictly less than the first tranche timestamp. if (timestamps.start >= tranches[0].timestamp) { revert Errors.SablierHelpers_StartTimeNotLessThanFirstTrancheTimestamp( diff --git a/src/libraries/VestingMath.sol b/src/libraries/LockupMath.sol similarity index 79% rename from src/libraries/VestingMath.sol rename to src/libraries/LockupMath.sol index 629aa7c6f..572a35a74 100644 --- a/src/libraries/VestingMath.sol +++ b/src/libraries/LockupMath.sol @@ -6,16 +6,23 @@ import { PRBMathCastingUint40 as CastingUint40 } from "@prb/math/src/casting/Uin import { SD59x18 } from "@prb/math/src/SD59x18.sol"; import { UD60x18, ud } from "@prb/math/src/UD60x18.sol"; -import { Lockup, LockupDynamic, LockupLinear, LockupTranched } from "./../types/DataTypes.sol"; +import { LockupDynamic } from "../types/LockupDynamic.sol"; +import { LockupLinear } from "../types/LockupLinear.sol"; +import { LockupTranched } from "../types/LockupTranched.sol"; -/// @title VestingMath -/// @notice Library with functions needed to calculate vested amount across lockup streams. -library VestingMath { +/// @title LockupMath +/// @notice Provides functions for calculating the streamed amounts in Lockup streams. Note that 'streamed' is +/// synonymous with 'vested'. +library LockupMath { using CastingUint128 for uint128; using CastingUint40 for uint40; - /// @notice Calculates the streamed amount for a Lockup dynamic stream. - /// @dev Lockup dynamic model uses the following distribution function: + /*////////////////////////////////////////////////////////////////////////// + USER-FACING READ-ONLY FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Calculates the streamed amount of LD streams. + /// @dev The LD streaming model uses the following distribution function: /// /// $$ /// f(x) = x^{exp} * csa + \Sigma(esa) @@ -26,7 +33,7 @@ library VestingMath { /// - $x$ is the elapsed time divided by the total duration of the current segment. /// - $exp$ is the current segment exponent. /// - $csa$ is the current segment amount. - /// - $\Sigma(esa)$ is the sum of all vested segments' amounts. + /// - $\Sigma(esa)$ is the sum of all streamed segments' amounts. /// /// Notes: /// 1. Normalization to 18 decimals is not needed because there is no mix of amounts with different decimals. @@ -39,24 +46,26 @@ library VestingMath { /// 2. The first segment's timestamp is greater than the start time. /// 3. The last segment's timestamp equals the end time. /// 4. The segment timestamps are arranged in ascending order. - function calculateLockupDynamicStreamedAmount( + function calculateStreamedAmountLD( uint128 depositedAmount, - LockupDynamic.Segment[] memory segments, - uint40 blockTimestamp, - Lockup.Timestamps memory timestamps, + uint40 endTime, + LockupDynamic.Segment[] calldata segments, + uint40 startTime, uint128 withdrawnAmount ) - public - pure + external + view returns (uint128) { + uint40 blockTimestamp = uint40(block.timestamp); + // If the start time is in the future, return zero. - if (timestamps.start > blockTimestamp) { + if (startTime > blockTimestamp) { return 0; } // If the end time is not in the future, return the deposited amount. - if (timestamps.end <= blockTimestamp) { + if (endTime <= blockTimestamp) { return depositedAmount; } @@ -80,7 +89,7 @@ library VestingMath { if (index == 0) { // When the current segment's index is equal to 0, the current segment is the first, so use the start // time as the previous timestamp. - previousTimestamp = timestamps.start; + previousTimestamp = startTime; } else { // Otherwise, when the current segment's index is greater than zero, it means that the segment is not // the first. In this case, use the previous segment's timestamp. @@ -112,8 +121,8 @@ library VestingMath { } } - /// @notice Calculates the streamed amount for a Lockup linear stream. - /// @dev Lockup linear model uses the following distribution function: + /// @notice Calculates the streamed amount of LL streams. + /// @dev The LL streaming model uses the following distribution function: /// /// $$ /// ( x * sa + s, block timestamp < cliff time @@ -133,33 +142,35 @@ library VestingMath { /// the deposit amount. /// 2. The start time is before the end time. /// 3. If the cliff time is not zero, it is after the start time and before the end time. - function calculateLockupLinearStreamedAmount( - uint128 depositedAmount, - uint40 blockTimestamp, - Lockup.Timestamps memory timestamps, + function calculateStreamedAmountLL( uint40 cliffTime, - LockupLinear.UnlockAmounts memory unlockAmounts, + uint128 depositedAmount, + uint40 endTime, + uint40 startTime, + LockupLinear.UnlockAmounts calldata unlockAmounts, uint128 withdrawnAmount ) - public - pure + external + view returns (uint128) { + uint40 blockTimestamp = uint40(block.timestamp); + // If the start time is in the future, return zero. - if (timestamps.start > blockTimestamp) { + if (startTime > blockTimestamp) { return 0; } - // If the end time is not in the future, return the deposited amount. - if (timestamps.end <= blockTimestamp) { - return depositedAmount; - } - // If the cliff time is in the future, return the start unlock amount. if (cliffTime > blockTimestamp) { return unlockAmounts.start; } + // If the end time is not in the future, return the deposited amount. + if (endTime <= blockTimestamp) { + return depositedAmount; + } + unchecked { uint128 unlockAmountsSum = unlockAmounts.start + unlockAmounts.cliff; @@ -175,11 +186,11 @@ library VestingMath { // Calculate the streamable range. if (cliffTime == 0) { - elapsedTime = ud(blockTimestamp - timestamps.start); - streamableRange = ud(timestamps.end - timestamps.start); + elapsedTime = ud(blockTimestamp - startTime); + streamableRange = ud(endTime - startTime); } else { elapsedTime = ud(blockTimestamp - cliffTime); - streamableRange = ud(timestamps.end - cliffTime); + streamableRange = ud(endTime - cliffTime); } UD60x18 elapsedTimePercentage = elapsedTime.div(streamableRange); @@ -200,8 +211,8 @@ library VestingMath { } } - /// @notice Calculates the streamed amount for a Lockup tranched stream. - /// @dev Lockup tranched model uses the following distribution function: + /// @notice Calculates the streamed amount of LT streams. + /// @dev The LT streaming model uses the following distribution function: /// /// $$ /// f(x) = \Sigma(eta) @@ -209,45 +220,47 @@ library VestingMath { /// /// Where: /// - /// - $\Sigma(eta)$ is the sum of all vested tranches' amounts. + /// - $\Sigma(eta)$ is the sum of all streamed tranches' amounts. /// /// Assumptions: /// 1. The sum of all tranche amounts does not overflow uint128, and equals the deposited amount. /// 2. The first tranche's timestamp is greater than the start time. /// 3. The last tranche's timestamp equals the end time. /// 4. The tranche timestamps are arranged in ascending order. - function calculateLockupTranchedStreamedAmount( + function calculateStreamedAmountLT( uint128 depositedAmount, - uint40 blockTimestamp, - Lockup.Timestamps memory timestamps, - LockupTranched.Tranche[] memory tranches + uint40 endTime, + uint40 startTime, + LockupTranched.Tranche[] calldata tranches ) - public - pure + external + view returns (uint128) { + uint40 blockTimestamp = uint40(block.timestamp); + // If the start time is in the future, return zero. - if (timestamps.start > blockTimestamp) { + if (startTime > blockTimestamp) { return 0; } - // If the end time is not in the future, return the deposited amount. - if (timestamps.end <= blockTimestamp) { - return depositedAmount; - } - // If the first tranche's timestamp is in the future, return zero. if (tranches[0].timestamp > blockTimestamp) { return 0; } - // Sum the amounts in all tranches that have already been vested. + // If the end time is not in the future, return the deposited amount. + if (endTime <= blockTimestamp) { + return depositedAmount; + } + + // Sum the amounts in all tranches that have already been streamed. // Using unchecked arithmetic is safe because the sum of the tranche amounts is equal to the total amount // at this point. uint128 streamedAmount = tranches[0].amount; uint256 tranchesCount = tranches.length; for (uint256 i = 1; i < tranchesCount; ++i) { - // The loop breaks at the first tranche with a timestamp in the future. A tranche is considered vested if + // The loop breaks at the first tranche with a timestamp in the future. A tranche is considered streamed if // its timestamp is less than or equal to the block timestamp. if (tranches[i].timestamp > blockTimestamp) { break; diff --git a/src/libraries/NFTSVG.sol b/src/libraries/NFTSVG.sol index d6e233743..d424b0e36 100644 --- a/src/libraries/NFTSVG.sol +++ b/src/libraries/NFTSVG.sol @@ -9,8 +9,16 @@ import { SVGElements } from "./SVGElements.sol"; library NFTSVG { using Strings for uint256; + /*////////////////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////////////////*/ + uint256 internal constant CARD_MARGIN = 16; + /*////////////////////////////////////////////////////////////////////////// + DATA TYPES + //////////////////////////////////////////////////////////////////////////*/ + struct SVGParams { string accentColor; string amount; @@ -40,6 +48,10 @@ library NFTSVG { uint256 statusXPosition; } + /*////////////////////////////////////////////////////////////////////////// + INTERNAL READ-ONLY FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + function generateSVG(SVGParams memory params) internal pure returns (string memory) { SVGVars memory vars; diff --git a/src/libraries/SVGElements.sol b/src/libraries/SVGElements.sol index 92f1bbdd0..68e1841d0 100644 --- a/src/libraries/SVGElements.sol +++ b/src/libraries/SVGElements.sol @@ -10,7 +10,7 @@ library SVGElements { using Strings for uint256; /*////////////////////////////////////////////////////////////////////////// - CONSTANTS + STATE VARIABLES //////////////////////////////////////////////////////////////////////////*/ string internal constant BACKGROUND = diff --git a/src/types/BatchLockup.sol b/src/types/BatchLockup.sol new file mode 100644 index 000000000..a38b128ca --- /dev/null +++ b/src/types/BatchLockup.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { Lockup } from "./Lockup.sol"; +import { LockupDynamic } from "./LockupDynamic.sol"; +import { LockupLinear } from "./LockupLinear.sol"; +import { LockupTranched } from "./LockupTranched.sol"; + +/// @dev Namespace for the structs used in `SablierBatchLockup` contract. +library BatchLockup { + /// @notice A struct encapsulating all parameters of {SablierLockupDynamic.createWithDurationsLD} except for the + /// token. + struct CreateWithDurationsLD { + address sender; + address recipient; + uint128 depositAmount; + bool cancelable; + bool transferable; + LockupDynamic.SegmentWithDuration[] segmentsWithDuration; + string shape; + } + + /// @notice A struct encapsulating all parameters of {SablierLockupLinear.createWithDurationsLL} except for the + /// token. + struct CreateWithDurationsLL { + address sender; + address recipient; + uint128 depositAmount; + bool cancelable; + bool transferable; + LockupLinear.Durations durations; + LockupLinear.UnlockAmounts unlockAmounts; + string shape; + } + + /// @notice A struct encapsulating all parameters of {SablierLockupTranched.createWithDurationsLT} except for the + /// token. + struct CreateWithDurationsLT { + address sender; + address recipient; + uint128 depositAmount; + bool cancelable; + bool transferable; + LockupTranched.TrancheWithDuration[] tranchesWithDuration; + string shape; + } + + /// @notice A struct encapsulating all parameters of {SablierLockupDynamic.createWithTimestampsLD} except for the + /// token. + struct CreateWithTimestampsLD { + address sender; + address recipient; + uint128 depositAmount; + bool cancelable; + bool transferable; + uint40 startTime; + LockupDynamic.Segment[] segments; + string shape; + } + + /// @notice A struct encapsulating all parameters of {SablierLockupLinear.createWithTimestampsLL} except for the + /// token. + struct CreateWithTimestampsLL { + address sender; + address recipient; + uint128 depositAmount; + bool cancelable; + bool transferable; + Lockup.Timestamps timestamps; + uint40 cliffTime; + LockupLinear.UnlockAmounts unlockAmounts; + string shape; + } + + /// @notice A struct encapsulating all parameters of {SablierLockupTranched.createWithTimestampsLT} except for the + /// token. + struct CreateWithTimestampsLT { + address sender; + address recipient; + uint128 depositAmount; + bool cancelable; + bool transferable; + uint40 startTime; + LockupTranched.Tranche[] tranches; + string shape; + } +} diff --git a/src/types/DataTypes.sol b/src/types/DataTypes.sol index c5d87b101..c1de65af3 100644 --- a/src/types/DataTypes.sol +++ b/src/types/DataTypes.sol @@ -1,341 +1,19 @@ // SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable imports-order,no-unused-import pragma solidity >=0.8.22; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { UD2x18 } from "@prb/math/src/UD2x18.sol"; -import { UD60x18 } from "@prb/math/src/UD60x18.sol"; - -// This file defines all structs used in Lockup, most of which are organized under three namespaces: -// -// - BatchLockup -// - Lockup -// - LockupDynamic -// - LockupLinear -// - LockupTranched -// -// You will notice that some structs contain "slot" annotations - they are used to indicate the -// storage layout of the struct. It is more gas efficient to group small data types together so -// that they fit in a single 32-byte slot. - -/// @dev Namespace for the structs used in `BatchLockup` contract. -library BatchLockup { - /// @notice A struct encapsulating all parameters of {SablierLockup.createWithDurationsLD} except for the token. - struct CreateWithDurationsLD { - address sender; - address recipient; - uint128 totalAmount; - bool cancelable; - bool transferable; - LockupDynamic.SegmentWithDuration[] segmentsWithDuration; - string shape; - Broker broker; - } - - /// @notice A struct encapsulating all parameters of {SablierLockup.createWithDurationsLL} except for the token. - struct CreateWithDurationsLL { - address sender; - address recipient; - uint128 totalAmount; - bool cancelable; - bool transferable; - LockupLinear.Durations durations; - LockupLinear.UnlockAmounts unlockAmounts; - string shape; - Broker broker; - } - - /// @notice A struct encapsulating all parameters of {SablierLockup.createWithDurationsLT} except for the token. - struct CreateWithDurationsLT { - address sender; - address recipient; - uint128 totalAmount; - bool cancelable; - bool transferable; - LockupTranched.TrancheWithDuration[] tranchesWithDuration; - string shape; - Broker broker; - } - - /// @notice A struct encapsulating all parameters of {SablierLockup.createWithTimestampsLD} except for the token. - struct CreateWithTimestampsLD { - address sender; - address recipient; - uint128 totalAmount; - bool cancelable; - bool transferable; - uint40 startTime; - LockupDynamic.Segment[] segments; - string shape; - Broker broker; - } - - /// @notice A struct encapsulating all parameters of {SablierLockup.createWithTimestampsLL} except for the token. - struct CreateWithTimestampsLL { - address sender; - address recipient; - uint128 totalAmount; - bool cancelable; - bool transferable; - Lockup.Timestamps timestamps; - uint40 cliffTime; - LockupLinear.UnlockAmounts unlockAmounts; - string shape; - Broker broker; - } - - /// @notice A struct encapsulating all parameters of {SablierLockup.createWithTimestampsLT} except for the token. - struct CreateWithTimestampsLT { - address sender; - address recipient; - uint128 totalAmount; - bool cancelable; - bool transferable; - uint40 startTime; - LockupTranched.Tranche[] tranches; - string shape; - Broker broker; - } -} - -/// @notice Struct encapsulating the broker parameters passed to the create functions. Both can be set to zero. -/// @param account The address receiving the broker's fee. -/// @param fee The broker's percentage fee from the total amount, denoted as a fixed-point number where 1e18 is 100%. -struct Broker { - address account; - UD60x18 fee; -} - -/// @notice Namespace for the structs used in all Lockup models. -library Lockup { - /// @notice Struct encapsulating the deposit, withdrawn, and refunded amounts, all denoted in units of the token's - /// decimals. - /// @dev Because the deposited and the withdrawn amount are often read together, declaring them in the same slot - /// saves gas. - /// @param deposited The initial amount deposited in the stream, net of broker fee. - /// @param withdrawn The cumulative amount withdrawn from the stream. - /// @param refunded The amount refunded to the sender. Unless the stream was canceled, this is always zero. - struct Amounts { - // slot 0 - uint128 deposited; - uint128 withdrawn; - // slot 1 - uint128 refunded; - } - - /// @notice Struct encapsulating (i) the deposit amount and (ii) the broker fee amount, both denoted in units of the - /// token's decimals. - /// @param deposit The amount to deposit in the stream. - /// @param brokerFee The broker fee amount. - struct CreateAmounts { - uint128 deposit; - uint128 brokerFee; - } - - /// @notice Struct encapsulating the common parameters emitted in the `Create` event. - /// @param funder The address which has funded the stream. - /// @param sender The address distributing the tokens, which is able to cancel the stream. - /// @param recipient The address receiving the tokens, as well as the NFT owner. - /// @param amounts Struct encapsulating (i) the deposit amount, and (ii) the broker fee amount, both denoted - /// in units of the token's decimals. - /// @param token The contract address of the ERC-20 token to be distributed. - /// @param cancelable Boolean indicating whether the stream is cancelable or not. - /// @param transferable Boolean indicating whether the stream NFT is transferable or not. - /// @param timestamps Struct encapsulating (i) the stream's start time and (ii) end time, all as Unix timestamps. - /// @param shape An optional parameter to specify the shape of the distribution function. This helps differentiate - /// streams in the UI. - /// @param broker The address of the broker who has helped create the stream, e.g. a front-end website. - struct CreateEventCommon { - address funder; - address sender; - address recipient; - Lockup.CreateAmounts amounts; - IERC20 token; - bool cancelable; - bool transferable; - Lockup.Timestamps timestamps; - string shape; - address broker; - } - - /// @notice Struct encapsulating the parameters of the `createWithDurations` functions. - /// @param sender The address distributing the tokens, with the ability to cancel the stream. It doesn't have to be - /// the same as `msg.sender`. - /// @param recipient The address receiving the tokens, as well as the NFT owner. - /// @param totalAmount The total amount, including the deposit and any broker fee, denoted in units of the token's - /// decimals. - /// @param token The contract address of the ERC-20 token to be distributed. - /// @param cancelable Indicates if the stream is cancelable. - /// @param transferable Indicates if the stream NFT is transferable. - /// @param shape An optional parameter to specify the shape of the distribution function. This helps differentiate - /// streams in the UI. - /// @param broker Struct encapsulating (i) the address of the broker assisting in creating the stream, and (ii) the - /// percentage fee paid to the broker from `totalAmount`, denoted as a fixed-point number. Both can be set to zero. - struct CreateWithDurations { - address sender; - address recipient; - uint128 totalAmount; - IERC20 token; - bool cancelable; - bool transferable; - string shape; - Broker broker; - } - - /// @notice Struct encapsulating the parameters of the `createWithTimestamps` functions. - /// @param sender The address distributing the tokens, with the ability to cancel the stream. It doesn't have to be - /// the same as `msg.sender`. - /// @param recipient The address receiving the tokens, as well as the NFT owner. - /// @param totalAmount The total amount, including the deposit and any broker fee, denoted in units of the token's - /// decimals. - /// @param token The contract address of the ERC-20 token to be distributed. - /// @param cancelable Indicates if the stream is cancelable. - /// @param transferable Indicates if the stream NFT is transferable. - /// @param timestamps Struct encapsulating (i) the stream's start time and (ii) end time, both as Unix timestamps. - /// @param shape An optional parameter to specify the shape of the distribution function. This helps differentiate - /// streams in the UI. - /// @param broker Struct encapsulating (i) the address of the broker assisting in creating the stream, and (ii) the - /// percentage fee paid to the broker from `totalAmount`, denoted as a fixed-point number. Both can be set to zero. - struct CreateWithTimestamps { - address sender; - address recipient; - uint128 totalAmount; - IERC20 token; - bool cancelable; - bool transferable; - Timestamps timestamps; - string shape; - Broker broker; - } - - /// @notice Enum representing the different distribution models used to create lockup streams. - /// @dev These distribution models determine the vesting function used in the calculations of the unlocked tokens. - enum Model { - LOCKUP_LINEAR, - LOCKUP_DYNAMIC, - LOCKUP_TRANCHED - } - - /// @notice Enum representing the different statuses of a stream. - /// @dev The status can have a "temperature": - /// 1. Warm: Pending, Streaming. The passage of time alone can change the status. - /// 2. Cold: Settled, Canceled, Depleted. The passage of time alone cannot change the status. - /// @custom:value0 PENDING Stream created but not started; tokens are in a pending state. - /// @custom:value1 STREAMING Active stream where tokens are currently being streamed. - /// @custom:value2 SETTLED All tokens have been streamed; recipient is due to withdraw them. - /// @custom:value3 CANCELED Canceled stream; remaining tokens await recipient's withdrawal. - /// @custom:value4 DEPLETED Depleted stream; all tokens have been withdrawn and/or refunded. - enum Status { - // Warm - PENDING, - STREAMING, - // Cold - SETTLED, - CANCELED, - DEPLETED - } - - /// @notice A common data structure to be stored in all Lockup models. - /// @dev The fields are arranged like this to save gas via tight variable packing. - /// @param sender The address distributing the tokens, with the ability to cancel the stream. - /// @param startTime The Unix timestamp indicating the stream's start. - /// @param endTime The Unix timestamp indicating the stream's end. - /// @param isCancelable Boolean indicating if the stream is cancelable. - /// @param wasCanceled Boolean indicating if the stream was canceled. - /// @param token The contract address of the ERC-20 token to be distributed. - /// @param isDepleted Boolean indicating if the stream is depleted. - /// @param isStream Boolean indicating if the struct entity exists. - /// @param isTransferable Boolean indicating if the stream NFT is transferable. - /// @param lockupModel The distribution model of the stream. - /// @param amounts Struct encapsulating the deposit, withdrawn, and refunded amounts, both denoted in units of the - /// token's decimals. - struct Stream { - // slot 0 - address sender; - uint40 startTime; - uint40 endTime; - bool isCancelable; - bool wasCanceled; - // slot 1 - IERC20 token; - bool isDepleted; - bool isStream; - bool isTransferable; - Model lockupModel; - // slot 2 and 3 - Amounts amounts; - } - - /// @notice Struct encapsulating the Lockup timestamps. - /// @param start The Unix timestamp for the stream's start. - /// @param end The Unix timestamp for the stream's end. - struct Timestamps { - uint40 start; - uint40 end; - } -} - -/// @notice Namespace for the structs used only in Lockup Dynamic model. -library LockupDynamic { - /// @notice Segment struct to be stored in the Lockup Dynamic model. - /// @param amount The amount of tokens streamed in the segment, denoted in units of the token's decimals. - /// @param exponent The exponent of the segment, denoted as a fixed-point number. - /// @param timestamp The Unix timestamp indicating the segment's end. - struct Segment { - // slot 0 - uint128 amount; - UD2x18 exponent; - uint40 timestamp; - } - - /// @notice Segment struct used at runtime in {SablierLockup.createWithDurationsLD} function. - /// @param amount The amount of tokens streamed in the segment, denoted in units of the token's decimals. - /// @param exponent The exponent of the segment, denoted as a fixed-point number. - /// @param duration The time difference in seconds between the segment and the previous one. - struct SegmentWithDuration { - uint128 amount; - UD2x18 exponent; - uint40 duration; - } -} - -/// @notice Namespace for the structs used only in Lockup Linear model. -library LockupLinear { - /// @notice Struct encapsulating the cliff duration and the total duration used at runtime in - /// {SablierLockup.createWithDurationsLL} function. - /// @param cliff The cliff duration in seconds. - /// @param total The total duration in seconds. - struct Durations { - uint40 cliff; - uint40 total; - } - - /// @notice Struct encapsulating the unlock amounts for the stream. - /// @dev The sum of `start` and `cliff` must be less than or equal to deposit amount. Both amounts can be zero. - /// @param start The amount to be unlocked at the start time. - /// @param cliff The amount to be unlocked at the cliff time. - struct UnlockAmounts { - // slot 0 - uint128 start; - uint128 cliff; - } -} - -/// @notice Namespace for the structs used only in Lockup Tranched model. -library LockupTranched { - /// @notice Tranche struct to be stored in the Lockup Tranched model. - /// @param amount The amount of tokens to be unlocked in the tranche, denoted in units of the token's decimals. - /// @param timestamp The Unix timestamp indicating the tranche's end. - struct Tranche { - // slot 0 - uint128 amount; - uint40 timestamp; - } - - /// @notice Tranche struct used at runtime in {SablierLockup.createWithDurationsLT} function. - /// @param amount The amount of tokens to be unlocked in the tranche, denoted in units of the token's decimals. - /// @param duration The time difference in seconds between the tranche and the previous one. - struct TrancheWithDuration { - uint128 amount; - uint40 duration; - } -} +import { BatchLockup } from "./BatchLockup.sol"; +import { Lockup } from "./Lockup.sol"; +import { LockupDynamic } from "./LockupDynamic.sol"; +import { LockupLinear } from "./LockupLinear.sol"; +import { LockupTranched } from "./LockupTranched.sol"; + +/** + * This file has been deprecated and is only kept for backward compatibility. Please use the below types instead: + * + * - BatchLockup + * - Lockup + * - LockupDynamic + * - LockupLinear + * - LockupTranched + */ diff --git a/src/types/Lockup.sol b/src/types/Lockup.sol new file mode 100644 index 000000000..947791d0c --- /dev/null +++ b/src/types/Lockup.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/// @notice Namespace for the structs shared by all Lockup models. +library Lockup { + /// @notice Struct encapsulating the deposit, withdrawn, and refunded amounts, all denoted in units of the token's + /// decimals. + /// @dev The deposited and withdrawn amount are often read together, so declaring them in the same slot saves gas. + /// @param deposited The amount deposited in the stream. + /// @param withdrawn The cumulative amount withdrawn from the stream. + /// @param refunded The amount refunded to the sender. Unless the stream was canceled, this is always zero. + struct Amounts { + // slot 0 + uint128 deposited; + uint128 withdrawn; + // slot 1 + uint128 refunded; + } + + /// @notice Struct encapsulating the common parameters emitted in the stream creation events. + /// @param funder The address funding the stream. + /// @param sender The address distributing the tokens, which is able to cancel the stream. + /// @param recipient The address receiving the tokens, as well as the NFT owner. + /// @param depositAmount The deposit amount, denoted in units of the token's decimals. + /// @param token The contract address of the ERC-20 token to be distributed. + /// @param cancelable Boolean indicating whether the stream is cancelable or not. + /// @param transferable Boolean indicating whether the stream NFT is transferable or not. + /// @param timestamps Struct encapsulating (i) the stream's start time and (ii) end time, all as Unix timestamps. + /// @param shape An optional parameter to specify the shape of the distribution function. This helps differentiate + /// streams in the UI. + struct CreateEventCommon { + address funder; + address sender; + address recipient; + uint128 depositAmount; + IERC20 token; + bool cancelable; + bool transferable; + Lockup.Timestamps timestamps; + string shape; + } + + /// @notice Struct encapsulating the parameters of the `createWithDurations` functions. + /// @param sender The address distributing the tokens, with the ability to cancel the stream. It doesn't have to be + /// the same as `msg.sender`. + /// @param recipient The address receiving the tokens, as well as the NFT owner. + /// @param depositAmount The deposit amount, denoted in units of the token's decimals. + /// @param token The contract address of the ERC-20 token to be distributed. + /// @param cancelable Indicates if the stream is cancelable. + /// @param transferable Indicates if the stream NFT is transferable. + /// @param shape An optional parameter to specify the shape of the distribution function. This helps differentiate + /// streams in the UI. + struct CreateWithDurations { + address sender; + address recipient; + uint128 depositAmount; + IERC20 token; + bool cancelable; + bool transferable; + string shape; + } + + /// @notice Struct encapsulating the parameters of the `createWithTimestamps` functions. + /// @param sender The address distributing the tokens, with the ability to cancel the stream. It doesn't have to be + /// the same as `msg.sender`. + /// @param recipient The address receiving the tokens, as well as the NFT owner. + /// @param depositAmount The deposit amount, denoted in units of the token's decimals. + /// @param token The contract address of the ERC-20 token to be distributed. + /// @param cancelable Indicates if the stream is cancelable. + /// @param transferable Indicates if the stream NFT is transferable. + /// @param timestamps Struct encapsulating (i) the stream's start time and (ii) end time, both as Unix timestamps. + /// @param shape An optional parameter to specify the shape of the distribution function. This helps differentiate + /// streams in the UI. + struct CreateWithTimestamps { + address sender; + address recipient; + uint128 depositAmount; + IERC20 token; + bool cancelable; + bool transferable; + Timestamps timestamps; + string shape; + } + + /// @notice Enum representing the different distribution models used to create Lockup streams. + /// @dev This determines the streaming function used in the calculations of the unlocked tokens. + enum Model { + LOCKUP_LINEAR, + LOCKUP_DYNAMIC, + LOCKUP_TRANCHED + } + + /// @notice Enum representing the different statuses of a stream. + /// @dev The status can have a "temperature": + /// 1. Warm: Pending, Streaming. The passage of time alone can change the status. + /// 2. Cold: Settled, Canceled, Depleted. The passage of time alone cannot change the status. + /// @custom:value0 PENDING Stream created but not started; tokens are in a pending state. + /// @custom:value1 STREAMING Active stream where tokens are currently being streamed. + /// @custom:value2 SETTLED All tokens have been streamed; recipient is due to withdraw them. + /// @custom:value3 CANCELED Canceled stream; remaining tokens await recipient's withdrawal. + /// @custom:value4 DEPLETED Depleted stream; all tokens have been withdrawn and/or refunded. + enum Status { + // Warm + PENDING, + STREAMING, + // Cold + SETTLED, + CANCELED, + DEPLETED + } + + /// @notice A common data structure to be stored in all Lockup models. + /// @dev The fields are arranged like this to save gas via tight variable packing. + /// @param sender The address distributing the tokens, with the ability to cancel the stream. + /// @param startTime The Unix timestamp indicating the stream's start. + /// @param endTime The Unix timestamp indicating the stream's end. + /// @param isCancelable Boolean indicating if the stream is cancelable. + /// @param wasCanceled Boolean indicating if the stream was canceled. + /// @param token The contract address of the ERC-20 token to be distributed. + /// @param isDepleted Boolean indicating if the stream is depleted. + /// @param isTransferable Boolean indicating if the stream NFT is transferable. + /// @param lockupModel The distribution model of the stream. + /// @param amounts Struct encapsulating the deposit, withdrawn, and refunded amounts, both denoted in units of the + /// token's decimals. + struct Stream { + // slot 0 + address sender; + uint40 startTime; + uint40 endTime; + bool isCancelable; + bool wasCanceled; + // slot 1 + IERC20 token; + bool isDepleted; + bool isTransferable; + Model lockupModel; + // slot 2 and 3 + Amounts amounts; + } + + /// @notice Struct encapsulating the Lockup timestamps. + /// @param start The Unix timestamp for the stream's start. + /// @param end The Unix timestamp for the stream's end. + struct Timestamps { + uint40 start; + uint40 end; + } +} diff --git a/src/types/LockupDynamic.sol b/src/types/LockupDynamic.sol new file mode 100644 index 000000000..c9abcd131 --- /dev/null +++ b/src/types/LockupDynamic.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { UD2x18 } from "@prb/math/src/UD2x18.sol"; + +/// @notice Namespace for the structs used only in LD streams. +library LockupDynamic { + /// @notice Segment struct stored to represent LD streams. + /// @param amount The amount of tokens streamed in the segment, denoted in units of the token's decimals. + /// @param exponent The exponent of the segment, denoted as a fixed-point number. + /// @param timestamp The Unix timestamp indicating the segment's end. + struct Segment { + // slot 0 + uint128 amount; + UD2x18 exponent; + uint40 timestamp; + } + + /// @notice Segment struct used at runtime in {SablierLockupDynamic.createWithDurationsLD} function. + /// @param amount The amount of tokens streamed in the segment, denoted in units of the token's decimals. + /// @param exponent The exponent of the segment, denoted as a fixed-point number. + /// @param duration The time difference in seconds between the segment and the previous one. + struct SegmentWithDuration { + uint128 amount; + UD2x18 exponent; + uint40 duration; + } +} diff --git a/src/types/LockupLinear.sol b/src/types/LockupLinear.sol new file mode 100644 index 000000000..d57d39373 --- /dev/null +++ b/src/types/LockupLinear.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +/// @notice Namespace for the structs used only in LL streams. +library LockupLinear { + /// @notice Struct encapsulating the cliff duration and the total duration used at runtime in + /// {SablierLockupLinear.createWithDurationsLL} function. + /// @param cliff The cliff duration in seconds. + /// @param total The total duration in seconds. + struct Durations { + uint40 cliff; + uint40 total; + } + + /// @notice Struct encapsulating the unlock amounts for the stream. + /// @dev The sum of `start` and `cliff` must be less than or equal to deposit amount. Both amounts can be zero. + /// @param start The amount to be unlocked at the start time. + /// @param cliff The amount to be unlocked at the cliff time. + struct UnlockAmounts { + // slot 0 + uint128 start; + uint128 cliff; + } +} diff --git a/src/types/LockupTranched.sol b/src/types/LockupTranched.sol new file mode 100644 index 000000000..82602ad57 --- /dev/null +++ b/src/types/LockupTranched.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +/// @notice Namespace for the structs used only in LT streams. +library LockupTranched { + /// @notice Tranche struct stored to represent LT streams. + /// @param amount The amount of tokens to be unlocked in the tranche, denoted in units of the token's decimals. + /// @param timestamp The Unix timestamp indicating the tranche's end. + struct Tranche { + // slot 0 + uint128 amount; + uint40 timestamp; + } + + /// @notice Tranche struct used at runtime in {SablierLockupTranched.createWithDurationsLT} function. + /// @param amount The amount of tokens to be unlocked in the tranche, denoted in units of the token's decimals. + /// @param duration The time difference in seconds between the tranche and the previous one. + struct TrancheWithDuration { + uint128 amount; + uint40 duration; + } +} diff --git a/tests/.solhint.json b/tests/.solhint.json new file mode 100644 index 000000000..66bef156c --- /dev/null +++ b/tests/.solhint.json @@ -0,0 +1,11 @@ +{ + "rules": { + "const-name-snakecase": "off", + "contract-name-capwords": "off", + "func-name-mixedcase": "off", + "gas-calldata-parameters": "off", + "gas-custom-errors": "off", + "one-contract-per-file": "off", + "no-console": "off" + } +} diff --git a/tests/Base.t.sol b/tests/Base.t.sol index e8447c165..50404d6ba 100644 --- a/tests/Base.t.sol +++ b/tests/Base.t.sol @@ -1,28 +1,31 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { BaseTest as EvmBase } from "@sablier/evm-utils/src/tests/BaseTest.sol"; import { ILockupNFTDescriptor } from "src/interfaces/ILockupNFTDescriptor.sol"; import { ISablierBatchLockup } from "src/interfaces/ISablierBatchLockup.sol"; import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; +import { ISablierLockupDynamic } from "src/interfaces/ISablierLockupDynamic.sol"; +import { ISablierLockupLinear } from "src/interfaces/ISablierLockupLinear.sol"; +import { ISablierLockupTranched } from "src/interfaces/ISablierLockupTranched.sol"; import { LockupNFTDescriptor } from "src/LockupNFTDescriptor.sol"; import { SablierBatchLockup } from "src/SablierBatchLockup.sol"; import { SablierLockup } from "src/SablierLockup.sol"; -import { Lockup, LockupDynamic, LockupLinear, LockupTranched } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; +import { LockupDynamic } from "src/types/LockupDynamic.sol"; +import { LockupLinear } from "src/types/LockupLinear.sol"; +import { LockupTranched } from "src/types/LockupTranched.sol"; -import { ERC20MissingReturn } from "./mocks/erc20/ERC20MissingReturn.sol"; -import { ERC20Mock } from "./mocks/erc20/ERC20Mock.sol"; import { RecipientGood } from "./mocks/Hooks.sol"; import { NFTDescriptorMock } from "./mocks/NFTDescriptorMock.sol"; import { Noop } from "./mocks/Noop.sol"; -import { ContractWithoutReceive, ContractWithReceive } from "./mocks/Receive.sol"; import { Assertions } from "./utils/Assertions.sol"; import { Calculations } from "./utils/Calculations.sol"; import { Defaults } from "./utils/Defaults.sol"; import { DeployOptimized } from "./utils/DeployOptimized.t.sol"; import { Modifiers } from "./utils/Modifiers.sol"; -import { Users } from "./utils/Types.sol"; +import { StreamIds, Users } from "./utils/Types.sol"; /// @notice Base test contract with common logic needed by all tests. abstract contract Base_Test is Assertions, Calculations, DeployOptimized, Modifiers { @@ -30,6 +33,7 @@ abstract contract Base_Test is Assertions, Calculations, DeployOptimized, Modifi VARIABLES //////////////////////////////////////////////////////////////////////////*/ + StreamIds internal ids; Users internal users; /*////////////////////////////////////////////////////////////////////////// @@ -37,42 +41,27 @@ abstract contract Base_Test is Assertions, Calculations, DeployOptimized, Modifi //////////////////////////////////////////////////////////////////////////*/ ISablierBatchLockup internal batchLockup; - ContractWithoutReceive internal contractWithoutReceive; - ContractWithReceive internal contractWithReceive; - ERC20Mock internal dai; Defaults internal defaults; ISablierLockup internal lockup; ILockupNFTDescriptor internal nftDescriptor; NFTDescriptorMock internal nftDescriptorMock; Noop internal noop; RecipientGood internal recipientGood; - ERC20MissingReturn internal usdt; /*////////////////////////////////////////////////////////////////////////// SET-UP FUNCTION //////////////////////////////////////////////////////////////////////////*/ - function setUp() public virtual { + function setUp() public virtual override { + EvmBase.setUp(); + // Deploy the base test contracts. - contractWithoutReceive = new ContractWithoutReceive(); - contractWithReceive = new ContractWithReceive(); - dai = new ERC20Mock("Dai Stablecoin", "DAI"); noop = new Noop(); recipientGood = new RecipientGood(); - usdt = new ERC20MissingReturn("Tether USD", "USDT", 6); // Label the base test contracts. - vm.label({ account: address(contractWithoutReceive), newLabel: "Contract without Receive" }); - vm.label({ account: address(contractWithReceive), newLabel: "Contract with Receive" }); - vm.label({ account: address(dai), newLabel: "DAI" }); vm.label({ account: address(recipientGood), newLabel: "Good Recipient" }); vm.label({ account: address(noop), newLabel: "Noop" }); - vm.label({ account: address(usdt), newLabel: "USDT" }); - - // Create the protocol admin. - users.admin = payable(makeAddr({ name: "Admin" })); - vm.deal({ account: users.admin, newBalance: 100 ether }); - vm.startPrank({ msgSender: users.admin }); // Deploy the defaults contract. defaults = new Defaults(); @@ -84,13 +73,8 @@ abstract contract Base_Test is Assertions, Calculations, DeployOptimized, Modifi // Deploy the NFT descriptor mock. nftDescriptorMock = new NFTDescriptorMock(); - // Create users for testing. Note that due to ERC-20 approvals, this has to go after the protocol deployment. - users.alice = createUser("Alice"); - users.broker = createUser("Broker"); - users.eve = createUser("Eve"); - users.operator = createUser("Operator"); - users.recipient = createUser("Recipient"); - users.sender = createUser("Sender"); + // Create users for testing. + createTestUsers(); defaults.setUsers(users); @@ -98,44 +82,33 @@ abstract contract Base_Test is Assertions, Calculations, DeployOptimized, Modifi setVariables(defaults, users); // Approve `users.operator` to operate over lockup on behalf of the `users.recipient`. - resetPrank({ msgSender: users.recipient }); + setMsgSender(users.recipient); lockup.setApprovalForAll(users.operator, true); // Set sender as the default caller for the tests. - resetPrank({ msgSender: users.sender }); + setMsgSender(users.sender); - // Warp to July 1, 2024 at 00:00 UTC to provide a more realistic testing environment. - vm.warp({ newTimestamp: JULY_1_2024 }); + // Warp to Feb 1, 2025 at 00:00 UTC to provide a more realistic testing environment. + vm.warp({ newTimestamp: defaults.FEB_1_2025() }); } /*////////////////////////////////////////////////////////////////////////// HELPERS //////////////////////////////////////////////////////////////////////////*/ - /// @dev Approve `spender` to spend tokens from `from`. - function approveContract(IERC20 token_, address from, address spender) internal { - resetPrank({ msgSender: from }); - (bool success,) = address(token_).call(abi.encodeCall(IERC20.approve, (spender, MAX_UINT256))); - success; - } - - /// @dev Approves all contracts to spend tokens from the address passed. - function approveProtocol(address from) internal { - resetPrank({ msgSender: from }); - dai.approve({ spender: address(batchLockup), value: MAX_UINT256 }); - dai.approve({ spender: address(lockup), value: MAX_UINT256 }); - usdt.approve({ spender: address(batchLockup), value: MAX_UINT256 }); - usdt.approve({ spender: address(lockup), value: MAX_UINT256 }); - } + /// @dev Create users for testing and assign roles if applicable. + function createTestUsers() internal { + // Create users for testing. Note that due to ERC-20 approvals, this has to go after the protocol deployment. + address[] memory spenders = new address[](2); + spenders[0] = address(batchLockup); + spenders[1] = address(lockup); - /// @dev Generates a user, labels its address, funds it with test tokens, and approves the protocol contracts. - function createUser(string memory name) internal returns (address payable) { - address payable user = payable(makeAddr(name)); - vm.deal({ account: user, newBalance: 100 ether }); - deal({ token: address(dai), to: user, give: 1_000_000e18 }); - deal({ token: address(usdt), to: user, give: 1_000_000e18 }); - approveProtocol({ from: user }); - return user; + // Create test users. + users.alice = createUser("Alice", spenders); + users.eve = createUser("Eve", spenders); + users.operator = createUser("Operator", spenders); + users.recipient = createUser("Recipient", spenders); + users.sender = createUser("Sender", spenders); } /// @dev Conditionally deploys the protocol normally or from an optimized source compiled with `--via-ir`. @@ -144,74 +117,23 @@ abstract contract Base_Test is Assertions, Calculations, DeployOptimized, Modifi /// deployer's nonce, which would in turn lead to different addresses (recall that the addresses /// for contracts deployed via `CREATE` are based on the caller-and-nonce-hash). function deployProtocolConditionally() internal { - if (!isBenchmarkProfile() && !isTestOptimizedProfile()) { + if (!isTestOptimizedProfile()) { batchLockup = new SablierBatchLockup(); nftDescriptor = new LockupNFTDescriptor(); - lockup = new SablierLockup(users.admin, nftDescriptor, defaults.MAX_COUNT()); + lockup = new SablierLockup(address(comptroller), address(nftDescriptor)); } else { - (nftDescriptor, lockup, batchLockup) = deployOptimizedProtocol(users.admin, defaults.MAX_COUNT()); + (nftDescriptor, lockup, batchLockup) = deployOptimizedProtocol(address(comptroller)); } vm.label({ account: address(batchLockup), newLabel: "BatchLockup" }); vm.label({ account: address(lockup), newLabel: "Lockup" }); vm.label({ account: address(nftDescriptor), newLabel: "NFTDescriptor" }); } - /*////////////////////////////////////////////////////////////////////////// - CALL EXPECTS - IERC20 - //////////////////////////////////////////////////////////////////////////*/ - - /// @dev Expects a call to {IERC20.transfer}. - function expectCallToTransfer(address to, uint256 value) internal { - vm.expectCall({ callee: address(dai), data: abi.encodeCall(IERC20.transfer, (to, value)) }); - } - - /// @dev Expects a call to {IERC20.transfer}. - function expectCallToTransfer(IERC20 token, address to, uint256 value) internal { - vm.expectCall({ callee: address(token), data: abi.encodeCall(IERC20.transfer, (to, value)) }); - } - - /// @dev Expects a call to {IERC20.transferFrom}. - function expectCallToTransferFrom(address from, address to, uint256 value) internal { - vm.expectCall({ callee: address(dai), data: abi.encodeCall(IERC20.transferFrom, (from, to, value)) }); - } - - /// @dev Expects a call to {IERC20.transferFrom}. - function expectCallToTransferFrom(IERC20 token, address from, address to, uint256 value) internal { - vm.expectCall({ callee: address(token), data: abi.encodeCall(IERC20.transferFrom, (from, to, value)) }); - } - - /// @dev Expects multiple calls to {IERC20.transfer}. - function expectMultipleCallsToTransfer(uint64 count, address to, uint256 value) internal { - vm.expectCall({ callee: address(dai), count: count, data: abi.encodeCall(IERC20.transfer, (to, value)) }); - } - - /// @dev Expects multiple calls to {IERC20.transferFrom}. - function expectMultipleCallsToTransferFrom(uint64 count, address from, address to, uint256 value) internal { - expectMultipleCallsToTransferFrom(dai, count, from, to, value); - } - - /// @dev Expects multiple calls to {IERC20.transferFrom}. - function expectMultipleCallsToTransferFrom( - IERC20 token, - uint64 count, - address from, - address to, - uint256 value - ) - internal - { - vm.expectCall({ - callee: address(token), - count: count, - data: abi.encodeCall(IERC20.transferFrom, (from, to, value)) - }); - } - /*////////////////////////////////////////////////////////////////////////// CALL EXPECTS - LOCKUP //////////////////////////////////////////////////////////////////////////*/ - /// @dev Expects multiple calls to {ISablierLockup.createWithDurationsLD}. + /// @dev Expects multiple calls to {ISablierLockupDynamic.createWithDurationsLD}. function expectMultipleCallsToCreateWithDurationsLD( uint64 count, Lockup.CreateWithDurations memory params, @@ -222,11 +144,11 @@ abstract contract Base_Test is Assertions, Calculations, DeployOptimized, Modifi vm.expectCall({ callee: address(lockup), count: count, - data: abi.encodeCall(ISablierLockup.createWithDurationsLD, (params, segmentsWithDuration)) + data: abi.encodeCall(ISablierLockupDynamic.createWithDurationsLD, (params, segmentsWithDuration)) }); } - /// @dev Expects multiple calls to {ISablierLockup.createWithDurationsLL}. + /// @dev Expects multiple calls to {ISablierLockupLinear.createWithDurationsLL}. function expectMultipleCallsToCreateWithDurationsLL( uint64 count, Lockup.CreateWithDurations memory params, @@ -238,11 +160,11 @@ abstract contract Base_Test is Assertions, Calculations, DeployOptimized, Modifi vm.expectCall({ callee: address(lockup), count: count, - data: abi.encodeCall(ISablierLockup.createWithDurationsLL, (params, unlockAmounts, durations)) + data: abi.encodeCall(ISablierLockupLinear.createWithDurationsLL, (params, unlockAmounts, durations)) }); } - /// @dev Expects multiple calls to {ISablierLockup.createWithDurationsLT}. + /// @dev Expects multiple calls to {ISablierLockupTranched.createWithDurationsLT}. function expectMultipleCallsToCreateWithDurationsLT( uint64 count, Lockup.CreateWithDurations memory params, @@ -253,11 +175,11 @@ abstract contract Base_Test is Assertions, Calculations, DeployOptimized, Modifi vm.expectCall({ callee: address(lockup), count: count, - data: abi.encodeCall(ISablierLockup.createWithDurationsLT, (params, tranches)) + data: abi.encodeCall(ISablierLockupTranched.createWithDurationsLT, (params, tranches)) }); } - /// @dev Expects multiple calls to {ISablierLockup.createWithTimestampsLD}. + /// @dev Expects multiple calls to {ISablierLockupDynamic.createWithTimestampsLD}. function expectMultipleCallsToCreateWithTimestampsLD( uint64 count, Lockup.CreateWithTimestamps memory params, @@ -268,11 +190,11 @@ abstract contract Base_Test is Assertions, Calculations, DeployOptimized, Modifi vm.expectCall({ callee: address(lockup), count: count, - data: abi.encodeCall(ISablierLockup.createWithTimestampsLD, (params, segments)) + data: abi.encodeCall(ISablierLockupDynamic.createWithTimestampsLD, (params, segments)) }); } - /// @dev Expects multiple calls to {ISablierLockup.createWithTimestampsLL}. + /// @dev Expects multiple calls to {ISablierLockupLinear.createWithTimestampsLL}. function expectMultipleCallsToCreateWithTimestampsLL( uint64 count, Lockup.CreateWithTimestamps memory params, @@ -284,11 +206,11 @@ abstract contract Base_Test is Assertions, Calculations, DeployOptimized, Modifi vm.expectCall({ callee: address(lockup), count: count, - data: abi.encodeCall(ISablierLockup.createWithTimestampsLL, (params, unlockAmounts, cliffTime)) + data: abi.encodeCall(ISablierLockupLinear.createWithTimestampsLL, (params, unlockAmounts, cliffTime)) }); } - /// @dev Expects multiple calls to {ISablierLockup.createWithTimestampsLT}. + /// @dev Expects multiple calls to {ISablierLockupTranched.createWithTimestampsLT}. function expectMultipleCallsToCreateWithTimestampsLT( uint64 count, Lockup.CreateWithTimestamps memory params, @@ -299,7 +221,7 @@ abstract contract Base_Test is Assertions, Calculations, DeployOptimized, Modifi vm.expectCall({ callee: address(lockup), count: count, - data: abi.encodeCall(ISablierLockup.createWithTimestampsLT, (params, tranches)) + data: abi.encodeCall(ISablierLockupTranched.createWithTimestampsLT, (params, tranches)) }); } } diff --git a/tests/fork/Fork.t.sol b/tests/fork/Fork.t.sol index 01f9bbe2c..178eb8c10 100644 --- a/tests/fork/Fork.t.sol +++ b/tests/fork/Fork.t.sol @@ -3,13 +3,15 @@ pragma solidity >=0.8.22 <0.9.0; import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + import { ILockupNFTDescriptor } from "src/interfaces/ILockupNFTDescriptor.sol"; import { ISablierBatchLockup } from "src/interfaces/ISablierBatchLockup.sol"; import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; import { Base_Test } from "./../Base.t.sol"; +import { Defaults } from "./../utils/Defaults.sol"; -/// @notice Common logic needed by all fork tests. +/// @notice Base logic needed by the fork tests. abstract contract Fork_Test is Base_Test { /*////////////////////////////////////////////////////////////////////////// STATE VARIABLES @@ -17,7 +19,7 @@ abstract contract Fork_Test is Base_Test { IERC20 internal immutable FORK_TOKEN; address internal forkTokenHolder; - uint256 internal initialHolderBalance; + uint128 internal initialHolderBalance; /*////////////////////////////////////////////////////////////////////////// CONSTRUCTOR @@ -32,25 +34,34 @@ abstract contract Fork_Test is Base_Test { //////////////////////////////////////////////////////////////////////////*/ function setUp() public virtual override { - // Fork Ethereum Mainnet at a specific block number. - vm.createSelectFork({ blockNumber: 21_719_029, urlOrAlias: "mainnet" }); + // Fork Ethereum Mainnet at the latest block number. + vm.createSelectFork({ urlOrAlias: "ethereum" }); // Load deployed addresses from Ethereum mainnet. - batchLockup = ISablierBatchLockup(0x3F6E8a8Cffe377c4649aCeB01e6F20c60fAA356c); + batchLockup = ISablierBatchLockup(0x0636D83B184D65C242c43de6AAd10535BFb9D45a); nftDescriptor = ILockupNFTDescriptor(0xA9dC6878C979B5cc1d98a1803F0664ad725A1f56); - lockup = ISablierLockup(0x7C01AA3783577E15fD7e272443D44B92d5b21056); + lockup = ISablierLockup(0xcF8ce57fa442ba50aCbC57147a62aD03873FfA73); + + defaults = new Defaults(); + + // We need these in case we work on a new iteration. + // Base_Test.setUp(); + // vm.etch(address(FORK_TOKEN), address(usdc).code); - // Create a custom user for this test suite. - forkTokenHolder = payable(makeAddr(string.concat(IERC20Metadata(address(FORK_TOKEN)).symbol(), "_HOLDER"))); + // Create a random user for this test suite. + forkTokenHolder = vm.randomAddress(); // Label the addresses. labelContracts(); - // Deal token balance to the user. - initialHolderBalance = 1e7 * 10 ** IERC20Metadata(address(FORK_TOKEN)).decimals(); + // Deal 1M tokens to the user. + initialHolderBalance = uint128(1e6 * (10 ** IERC20Metadata(address(FORK_TOKEN)).decimals())); deal({ token: address(FORK_TOKEN), to: forkTokenHolder, give: initialHolderBalance }); - resetPrank({ msgSender: forkTokenHolder }); + setMsgSender(forkTokenHolder); + + // Approve {SablierLockup} to transfer the holder's tokens. + approveContract({ token_: address(FORK_TOKEN), from: forkTokenHolder, spender: address(lockup) }); } /*////////////////////////////////////////////////////////////////////////// @@ -58,19 +69,21 @@ abstract contract Fork_Test is Base_Test { //////////////////////////////////////////////////////////////////////////*/ /// @dev Checks the user assumptions. - function checkUsers(address sender, address recipient, address broker, address lockupContract) internal virtual { + function checkUsers(address sender, address recipient, address lockupContract) internal virtual { // The protocol does not allow the zero address to interact with it. - vm.assume(sender != address(0) && recipient != address(0) && broker != address(0)); + vm.assume(sender != address(0) && recipient != address(0)); // The goal is to not have overlapping users because the forked token balance tests would fail otherwise. - vm.assume(sender != recipient && sender != broker && recipient != broker); - vm.assume(sender != forkTokenHolder && recipient != forkTokenHolder && broker != forkTokenHolder); - vm.assume(sender != lockupContract && recipient != lockupContract && broker != lockupContract); + vm.assume(sender != recipient); + vm.assume(sender != forkTokenHolder && recipient != forkTokenHolder); + vm.assume(sender != lockupContract && recipient != lockupContract); // Avoid users blacklisted by USDC or USDT. assumeNoBlacklisted(address(FORK_TOKEN), sender); assumeNoBlacklisted(address(FORK_TOKEN), recipient); - assumeNoBlacklisted(address(FORK_TOKEN), broker); + + // Make the holder the caller. + setMsgSender(forkTokenHolder); } /// @dev Labels the most relevant addresses. @@ -78,4 +91,20 @@ abstract contract Fork_Test is Base_Test { vm.label({ account: address(FORK_TOKEN), newLabel: IERC20Metadata(address(FORK_TOKEN)).symbol() }); vm.label({ account: forkTokenHolder, newLabel: "Fork Token Holder" }); } + + // We need this function in case we work on a new iteration. + // function getTokenBalances( + // address token, + // address[] memory addresses + // ) + // internal + // view + // override + // returns (uint256[] memory balances) + // { + // balances = new uint256[](addresses.length); + // for (uint256 i = 0; i < addresses.length; ++i) { + // balances[i] = IERC20(token).balanceOf(addresses[i]); + // } + // } } diff --git a/tests/fork/Lockup.t.sol b/tests/fork/Lockup.t.sol new file mode 100644 index 000000000..6da9ef01c --- /dev/null +++ b/tests/fork/Lockup.t.sol @@ -0,0 +1,349 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { Solarray } from "solarray/src/Solarray.sol"; +import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; +import { Lockup } from "src/types/Lockup.sol"; +import { LockupDynamic } from "src/types/LockupDynamic.sol"; +import { LockupLinear } from "src/types/LockupLinear.sol"; +import { LockupTranched } from "src/types/LockupTranched.sol"; + +import { Fork_Test } from "./Fork.t.sol"; + +/// @notice Common Lockup logic needed by all the fork tests. +abstract contract Lockup_Fork_Test is Fork_Test { + /*////////////////////////////////////////////////////////////////////////// + STRUCTS + //////////////////////////////////////////////////////////////////////////*/ + + // Struct with parameters to be fuzzed during the fork tests. + struct Params { + Lockup.CreateWithTimestamps create; + uint40 cliffTime; + LockupDynamic.Segment[] segments; + LockupTranched.Tranche[] tranches; + LockupLinear.UnlockAmounts unlockAmounts; + uint40 warpTimestamp; + uint128 withdrawAmount; + } + + // Struct to manage storage variables to be used across contracts. + struct Vars { + // Initial values + uint256 initialComptrollerBalanceETH; + uint256 initialLockupBalance; + uint256 initialRecipientBalance; + uint256 initialSenderBalance; + // Final values + uint256 actualHolderBalance; + uint256 actualLockupBalance; + uint256 actualRecipientBalance; + uint256 actualSenderBalance; + // Expected values + Lockup.Status expectedStatus; + // Generics + bool hasCliff; + bool isDepleted; + bool isSettled; + uint128 recipientAmount; + uint128 senderAmount; + uint128 streamedAmount; + uint256 streamId; + } + + /*////////////////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////////////////*/ + + Vars internal vars; + Lockup.Model internal lockupModel; + + /*////////////////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////////////////*/ + + constructor(IERC20 forkToken) Fork_Test(forkToken) { } + + /*////////////////////////////////////////////////////////////////////////// + CREATE HELPERS + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev A pre-create helper function to set up the parameters for the stream creation. + function preCreateStream(Params memory params) internal { + checkUsers(params.create.sender, params.create.recipient, address(lockup)); + + // Store the pre-create token balances of Lockup and Holder. + uint256[] memory balances = + getTokenBalances(address(FORK_TOKEN), Solarray.addresses(address(lockup), forkTokenHolder)); + vars.initialLockupBalance = balances[0]; + initialHolderBalance = uint128(balances[1]); + + // Store the next stream ID. + vars.streamId = lockup.nextStreamId(); + + // Bound the start time. + params.create.timestamps.start = boundUint40( + params.create.timestamps.start, getBlockTimestamp() - 1000 seconds, getBlockTimestamp() + 10_000 seconds + ); + + vars.hasCliff = params.cliffTime > 0; + // Bound the cliff time. Since it is only relevant to the Linear model, it will be ignored for the Dynamic + // and Tranched models. + params.cliffTime = vars.hasCliff + ? boundUint40( + params.cliffTime, params.create.timestamps.start + 1 seconds, params.create.timestamps.start + 52 weeks + ) + : 0; + + // Set fixed values for shape name and token. + params.create.shape = "Custom shape"; + params.create.token = FORK_TOKEN; + + // Make the stream cancelable so that the cancel tests can be run. + params.create.cancelable = true; + + if (lockupModel == Lockup.Model.LOCKUP_LINEAR) { + // Bound the deposit amount. + params.create.depositAmount = boundUint128(params.create.depositAmount, 1, initialHolderBalance); + + // Bound the minimum value of end time so that it is always greater than the start time, and the cliff time. + uint40 endTimeLowerBound = maxOfTwo(params.create.timestamps.start, params.cliffTime); + + // Bound the end time of the stream. + params.create.timestamps.end = + boundUint40(params.create.timestamps.end, endTimeLowerBound + 1 seconds, MAX_UNIX_TIMESTAMP); + + // Bound the unlock amounts. + params.unlockAmounts.start = boundUint128(params.unlockAmounts.start, 0, params.create.depositAmount); + // Bound the cliff unlock amount only if the cliff is set. + params.unlockAmounts.cliff = vars.hasCliff + ? boundUint128(params.unlockAmounts.cliff, 0, params.create.depositAmount - params.unlockAmounts.start) + : 0; + } + + if (lockupModel == Lockup.Model.LOCKUP_DYNAMIC) { + fuzzSegmentTimestamps(params.segments, params.create.timestamps.start); + + // Fuzz the segment amounts and calculate the deposit. + params.create.depositAmount = + fuzzDynamicStreamAmounts({ upperBound: initialHolderBalance, segments: params.segments }); + + // Bound the end time of the stream. + params.create.timestamps.end = params.segments[params.segments.length - 1].timestamp; + } + + if (lockupModel == Lockup.Model.LOCKUP_TRANCHED) { + fuzzTrancheTimestamps(params.tranches, params.create.timestamps.start); + + // Fuzz the tranche amounts and calculate the deposit. + params.create.depositAmount = + fuzzTranchedStreamAmounts({ upperBound: initialHolderBalance, tranches: params.tranches }); + + // Bound the end time of the stream. + params.create.timestamps.end = params.tranches[params.tranches.length - 1].timestamp; + } + } + + /// @dev A post-create helper function to compare values and set up the parameters for withdraw and cancel tests. + function postCreateStream(Params memory params) internal { + // Check if the stream is settled. It is possible for a Lockup stream to settle at the time of creation in the + // following cases: + // 1. The streamed amount equals the deposited amount. + // 2. The end time is in the past. + vars.isSettled = vars.streamedAmount >= params.create.depositAmount + || lockup.getEndTime(vars.streamId) <= getBlockTimestamp(); + + // Check that the stream status is correct. + if (lockup.getStartTime(vars.streamId) > getBlockTimestamp()) { + vars.expectedStatus = Lockup.Status.PENDING; + } else if (vars.isSettled) { + vars.expectedStatus = Lockup.Status.SETTLED; + } else { + vars.expectedStatus = Lockup.Status.STREAMING; + } + assertEq(lockup.statusOf(vars.streamId), vars.expectedStatus, "post-create stream status"); + + if (vars.isSettled) { + // If the stream is settled, it should not be cancelable. + assertFalse(lockup.isCancelable(vars.streamId), "isCancelable"); + } else { + // Otherwise, it should match the parameter value. + assertEq(lockup.isCancelable(vars.streamId), params.create.cancelable, "isCancelable"); + } + + // Assert that the aggregate amount has been updated. + assertEq( + lockup.aggregateAmount(FORK_TOKEN), + vars.initialLockupBalance + params.create.depositAmount, + "aggregateAmount" + ); + + // Store the post-create token balances of Lockup and Holder. + uint256[] memory balances = + getTokenBalances(address(FORK_TOKEN), Solarray.addresses(address(lockup), forkTokenHolder)); + vars.actualLockupBalance = balances[0]; + vars.actualHolderBalance = balances[1]; + + // Assert that the Lockup contract's balance has been updated. + uint256 expectedLockupBalance = vars.initialLockupBalance + params.create.depositAmount; + assertEq(vars.actualLockupBalance, expectedLockupBalance, "post-create Lockup balance"); + + // Assert that the holder's balance has been updated. + uint128 expectedHolderBalance = initialHolderBalance - params.create.depositAmount; + assertEq(vars.actualHolderBalance, expectedHolderBalance, "post-create Holder balance"); + } + + /*////////////////////////////////////////////////////////////////////////// + WITHDRAW HELPERS + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev A shared withdraw function to be used by all the fork tests. + function withdraw(Params memory params) internal { + // Bound the withdraw amount. + params.withdrawAmount = boundUint128(params.withdrawAmount, 0, lockup.withdrawableAmountOf(vars.streamId)); + + // Check if the stream has settled or will get depleted. It is possible for the stream to be just settled + // and not depleted because the withdraw amount is fuzzed. + vars.isSettled = vars.streamedAmount >= params.create.depositAmount + || lockup.getEndTime(vars.streamId) <= getBlockTimestamp(); + vars.isDepleted = params.withdrawAmount == params.create.depositAmount; + + // Run the withdraw tests only if the withdraw amount is not zero. + if (params.withdrawAmount > 0) { + // Load the pre-withdraw token balances. + vars.initialComptrollerBalanceETH = address(lockup).balance; + vars.initialLockupBalance = vars.actualLockupBalance; + vars.initialRecipientBalance = FORK_TOKEN.balanceOf(params.create.recipient); + + // Expect the relevant events to be emitted. + vm.expectEmit({ emitter: address(lockup) }); + emit ISablierLockup.WithdrawFromLockupStream({ + streamId: vars.streamId, + to: params.create.recipient, + token: FORK_TOKEN, + amount: params.withdrawAmount + }); + vm.expectEmit({ emitter: address(lockup) }); + emit IERC4906.MetadataUpdate({ _tokenId: vars.streamId }); + + // Make the withdrawal and pay a fee. + setMsgSender(params.create.recipient); + vm.deal({ account: params.create.recipient, newBalance: 100 ether }); + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: vars.streamId, + to: params.create.recipient, + amount: params.withdrawAmount + }); + + // Assert that the stream's status is correct. + if (vars.isDepleted) { + vars.expectedStatus = Lockup.Status.DEPLETED; + } else if (vars.isSettled) { + vars.expectedStatus = Lockup.Status.SETTLED; + } else { + vars.expectedStatus = Lockup.Status.STREAMING; + } + assertEq(lockup.statusOf(vars.streamId), vars.expectedStatus, "post-withdraw stream status"); + + // Assert that the withdrawn amount has been updated. + assertEq(lockup.getWithdrawnAmount(vars.streamId), params.withdrawAmount, "post-withdraw withdrawnAmount"); + + // Assert that the aggregate amount has been updated. + assertEq( + lockup.aggregateAmount(FORK_TOKEN), vars.initialLockupBalance - params.withdrawAmount, "aggregateAmount" + ); + + // Load the post-withdraw token balances. + uint256[] memory balances = + getTokenBalances(address(FORK_TOKEN), Solarray.addresses(address(lockup), params.create.recipient)); + vars.actualLockupBalance = balances[0]; + vars.actualRecipientBalance = balances[1]; + + // Assert that the contract's balance has been updated. + uint256 expectedLockupBalance = vars.initialLockupBalance - params.withdrawAmount; + assertEq(vars.actualLockupBalance, expectedLockupBalance, "post-withdraw Lockup balance"); + + // Assert that the contract's ETH balance has been updated. + assertEq( + address(lockup).balance, + vars.initialComptrollerBalanceETH + LOCKUP_MIN_FEE_WEI, + "post-withdraw Lockup balance ETH" + ); + + // Assert that the Recipient's balance has been updated. + uint256 expectedRecipientBalance = vars.initialRecipientBalance + params.withdrawAmount; + assertEq(vars.actualRecipientBalance, expectedRecipientBalance, "post-withdraw Recipient balance"); + } + } + + /*////////////////////////////////////////////////////////////////////////// + CANCEL HELPERS + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev A shared cancel function to be used by all the fork tests. + function cancel(Params memory params) internal { + // Run the cancel tests only if the stream is cancelable and is neither depleted nor settled. + if (params.create.cancelable && !vars.isDepleted && !vars.isSettled) { + // Load the pre-cancel token balances. + uint256[] memory balances = getTokenBalances( + address(FORK_TOKEN), Solarray.addresses(address(lockup), params.create.sender, params.create.recipient) + ); + vars.initialLockupBalance = balances[0]; + vars.initialSenderBalance = balances[1]; + vars.initialRecipientBalance = balances[2]; + + // Expect the relevant events to be emitted. + vm.expectEmit({ emitter: address(lockup) }); + vars.senderAmount = lockup.refundableAmountOf(vars.streamId); + vars.recipientAmount = lockup.withdrawableAmountOf(vars.streamId); + emit ISablierLockup.CancelLockupStream( + vars.streamId, + params.create.sender, + params.create.recipient, + FORK_TOKEN, + vars.senderAmount, + vars.recipientAmount + ); + vm.expectEmit({ emitter: address(lockup) }); + emit IERC4906.MetadataUpdate({ _tokenId: vars.streamId }); + + // Cancel the stream. + setMsgSender(params.create.sender); + uint128 refundedAmount = lockup.cancel(vars.streamId); + + // Assert that the refunded amount is correct. + assertEq(refundedAmount, vars.senderAmount, "refundedAmount"); + + // Assert that the stream's status is correct. + vars.expectedStatus = vars.recipientAmount > 0 ? Lockup.Status.CANCELED : Lockup.Status.DEPLETED; + assertEq(lockup.statusOf(vars.streamId), vars.expectedStatus, "post-cancel stream status"); + + // Assert that the aggregate amount has been updated. + assertEq(lockup.aggregateAmount(FORK_TOKEN), vars.initialLockupBalance - refundedAmount, "aggregateAmount"); + + // Load the post-cancel token balances. + balances = getTokenBalances( + address(FORK_TOKEN), Solarray.addresses(address(lockup), params.create.sender, params.create.recipient) + ); + vars.actualLockupBalance = balances[0]; + vars.actualSenderBalance = balances[1]; + vars.actualRecipientBalance = balances[2]; + + // Assert that the contract's balance has been updated. + uint256 expectedLockupBalance = vars.initialLockupBalance - vars.senderAmount; + assertEq(vars.actualLockupBalance, expectedLockupBalance, "post-cancel Lockup balance"); + + // Assert that the Sender's balance has been updated. + uint256 expectedSenderBalance = vars.initialSenderBalance + vars.senderAmount; + assertEq(vars.actualSenderBalance, expectedSenderBalance, "post-cancel Sender balance"); + + // Assert that the Recipient's balance has not changed. + assertEq(vars.actualRecipientBalance, vars.initialRecipientBalance, "post-cancel Recipient balance"); + } + + // Assert that the not burned NFT. + assertEq(lockup.ownerOf(vars.streamId), params.create.recipient, "post-cancel NFT owner"); + } +} diff --git a/tests/fork/LockupDynamic.t.sol b/tests/fork/LockupDynamic.t.sol index 146787974..5c592189c 100644 --- a/tests/fork/LockupDynamic.t.sol +++ b/tests/fork/LockupDynamic.t.sol @@ -3,87 +3,23 @@ pragma solidity >=0.8.22 <0.9.0; import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { Solarray } from "solarray/src/Solarray.sol"; -import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; -import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; -import { Broker, Lockup, LockupDynamic } from "src/types/DataTypes.sol"; -import { Fork_Test } from "./Fork.t.sol"; +import { ISablierLockupDynamic } from "src/interfaces/ISablierLockupDynamic.sol"; +import { Lockup } from "src/types/Lockup.sol"; +import { Lockup_Fork_Test } from "./Lockup.t.sol"; -abstract contract Lockup_Dynamic_Fork_Test is Fork_Test { +abstract contract Lockup_Dynamic_Fork_Test is Lockup_Fork_Test { /*////////////////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////////////////*/ - constructor(IERC20 forkToken) Fork_Test(forkToken) { } - - /*////////////////////////////////////////////////////////////////////////// - SET-UP FUNCTION - //////////////////////////////////////////////////////////////////////////*/ - - function setUp() public virtual override { - Fork_Test.setUp(); - - // Approve {SablierLockup} to transfer the holder's tokens. - // We use a low-level call to ignore reverts because the token can have the missing return value bug. - (bool success,) = address(FORK_TOKEN).call(abi.encodeCall(IERC20.approve, (address(lockup), MAX_UINT256))); - success; + constructor(IERC20 forkToken) Lockup_Fork_Test(forkToken) { + lockupModel = Lockup.Model.LOCKUP_DYNAMIC; } /*////////////////////////////////////////////////////////////////////////// TEST FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - struct Params { - address sender; - address recipient; - uint128 withdrawAmount; - uint40 startTime; - uint40 warpTimestamp; - LockupDynamic.Segment[] segments; - Broker broker; - } - - struct Vars { - // Generic vars - address actualNFTOwner; - uint256 actualLockupBalance; - uint256 actualRecipientBalance; - Lockup.Status actualStatus; - uint256[] balances; - address expectedNFTOwner; - uint256 expectedLockupBalance; - uint256 expectedRecipientBalance; - Lockup.Status expectedStatus; - uint256 initialLockupBalance; - uint256 initialRecipientBalance; - bool isCancelable; - bool isDepleted; - bool isSettled; - uint256 streamId; - Lockup.Timestamps timestamps; - // Create vars - uint256 actualBrokerBalance; - uint256 actualHolderBalance; - uint256 actualNextStreamId; - Lockup.CreateAmounts createAmounts; - uint256 expectedBrokerBalance; - uint256 expectedHolderBalance; - uint256 expectedNextStreamId; - uint256 initialBrokerBalance; - uint128 totalAmount; - // Withdraw vars - uint128 actualWithdrawnAmount; - uint128 expectedWithdrawnAmount; - uint256 initialLockupBalanceETH; - uint128 withdrawableAmount; - // Cancel vars - uint256 actualSenderBalance; - uint256 expectedSenderBalance; - uint256 initialSenderBalance; - uint128 recipientAmount; - uint128 senderAmount; - } - /// @dev Checklist: /// /// - It should perform all expected ERC-20 transfers @@ -99,281 +35,72 @@ abstract contract Lockup_Dynamic_Fork_Test is Fork_Test { /// /// Given enough fuzz runs, all of the following scenarios will be fuzzed: /// - /// - Multiple values for the funder, recipient, sender, and broker - /// - Multiple values for the total amount + /// - Multiple values for the recipient and the sender + /// - Multiple values for the deposit amount /// - Start time in the past /// - Start time in the present /// - Start time in the future /// - Start time equal and not equal to the first segment timestamp - /// - Multiple values for the broker fee, including zero /// - Multiple values for the withdraw amount, including zero /// - The whole gamut of stream statuses function testForkFuzz_CreateWithdrawCancel(Params memory params) external { - checkUsers(params.sender, params.recipient, params.broker.account, address(lockup)); - vm.assume(params.segments.length != 0); - params.broker.fee = _bound(params.broker.fee, 0, MAX_BROKER_FEE); - params.startTime = boundUint40(params.startTime, 1, getBlockTimestamp() + 2 days); - - // Fuzz the segment timestamps. - fuzzSegmentTimestamps(params.segments, params.startTime); - - // Fuzz the segment amounts and calculate the total and create amounts (deposit and broker fee). - Vars memory vars; - (vars.totalAmount, vars.createAmounts) = fuzzDynamicStreamAmounts({ - upperBound: uint128(initialHolderBalance), - segments: params.segments, - brokerFee: params.broker.fee - }); - - // Make the holder the caller. - resetPrank(forkTokenHolder); - /*////////////////////////////////////////////////////////////////////////// CREATE //////////////////////////////////////////////////////////////////////////*/ - // Load the pre-create token balances. - vars.balances = - getTokenBalances(address(FORK_TOKEN), Solarray.addresses(address(lockup), params.broker.account)); - vars.initialLockupBalance = vars.balances[0]; - vars.initialBrokerBalance = vars.balances[1]; + vm.assume(params.segments.length != 0); - vars.streamId = lockup.nextStreamId(); - vars.timestamps = - Lockup.Timestamps({ start: params.startTime, end: params.segments[params.segments.length - 1].timestamp }); + // Bound the fuzzed parameters and load values into `vars`. + preCreateStream(params); // Expect the relevant events to be emitted. vm.expectEmit({ emitter: address(lockup) }); emit IERC4906.MetadataUpdate({ _tokenId: vars.streamId }); vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockup.CreateLockupDynamicStream({ + emit ISablierLockupDynamic.CreateLockupDynamicStream({ streamId: vars.streamId, - commonParams: Lockup.CreateEventCommon({ - funder: forkTokenHolder, - sender: params.sender, - recipient: params.recipient, - amounts: vars.createAmounts, - token: FORK_TOKEN, - cancelable: true, - transferable: true, - timestamps: vars.timestamps, - shape: "Dynamic Shape", - broker: params.broker.account - }), + commonParams: defaults.lockupCreateEvent({ caller: forkTokenHolder, params: params.create, token_: FORK_TOKEN }), segments: params.segments }); // Create the stream. - lockup.createWithTimestampsLD( - Lockup.CreateWithTimestamps({ - sender: params.sender, - recipient: params.recipient, - totalAmount: vars.totalAmount, - token: FORK_TOKEN, - cancelable: true, - transferable: true, - timestamps: vars.timestamps, - shape: "Dynamic Shape", - broker: params.broker - }), - params.segments - ); + lockup.createWithTimestampsLD(params.create, params.segments); - // Check if the stream is settled. It is possible for a Lockup Dynamic stream to settle at the time of creation - // because some segment amounts can be zero or the last segment timestamp can be in the past. - vars.isSettled = lockup.refundableAmountOf(vars.streamId) == 0 || vars.timestamps.end <= getBlockTimestamp(); - vars.isCancelable = vars.isSettled ? false : true; - - // Assert that the stream has been created. - assertEq(lockup.getDepositedAmount(vars.streamId), vars.createAmounts.deposit, "depositedAmount"); - assertEq(lockup.getEndTime(vars.streamId), vars.timestamps.end, "endTime"); - assertEq(lockup.isCancelable(vars.streamId), vars.isCancelable, "isCancelable"); - assertFalse(lockup.isDepleted(vars.streamId), "isDepleted"); - assertTrue(lockup.isStream(vars.streamId), "isStream"); - assertTrue(lockup.isTransferable(vars.streamId), "isTransferable"); - assertEq(lockup.getRecipient(vars.streamId), params.recipient, "recipient"); - assertEq(lockup.getSender(vars.streamId), params.sender, "sender"); + // Assert that the stream is created with the correct parameters. + assertEq({ lockup: lockup, streamId: vars.streamId, expectedLockup: params.create }); assertEq(lockup.getSegments(vars.streamId), params.segments); - assertEq(lockup.getStartTime(vars.streamId), params.startTime, "startTime"); - assertEq(lockup.getUnderlyingToken(vars.streamId), FORK_TOKEN, "underlyingToken"); - assertFalse(lockup.wasCanceled(vars.streamId), "wasCanceled"); - - // Assert that the stream's status is correct. - vars.actualStatus = lockup.statusOf(vars.streamId); - if (params.startTime > getBlockTimestamp()) { - vars.expectedStatus = Lockup.Status.PENDING; - } else if (vars.isSettled) { - vars.expectedStatus = Lockup.Status.SETTLED; - } else { - vars.expectedStatus = Lockup.Status.STREAMING; - } - assertEq(vars.actualStatus, vars.expectedStatus, "post-create stream status"); - - // Assert that the next stream ID has been bumped. - vars.actualNextStreamId = lockup.nextStreamId(); - vars.expectedNextStreamId = vars.streamId + 1; - assertEq(vars.actualNextStreamId, vars.expectedNextStreamId, "post-create nextStreamId"); - // Assert that the NFT has been minted. - vars.actualNFTOwner = lockup.ownerOf({ tokenId: vars.streamId }); - vars.expectedNFTOwner = params.recipient; - assertEq(vars.actualNFTOwner, vars.expectedNFTOwner, "post-create NFT owner"); - - // Load the post-create token balances. - vars.balances = getTokenBalances( - address(FORK_TOKEN), Solarray.addresses(address(lockup), forkTokenHolder, params.broker.account) - ); - vars.actualLockupBalance = vars.balances[0]; - vars.actualHolderBalance = vars.balances[1]; - vars.actualBrokerBalance = vars.balances[2]; + // Update the streamed amount. + vars.streamedAmount = + calculateStreamedAmountLD(params.segments, params.create.timestamps.start, params.create.depositAmount); - // Assert that the contract's balance has been updated. - vars.expectedLockupBalance = vars.initialLockupBalance + vars.createAmounts.deposit; - assertEq(vars.actualLockupBalance, vars.expectedLockupBalance, "post-create Lockup contract balance"); - - // Assert that the holder's balance has been updated. - vars.expectedHolderBalance = initialHolderBalance - vars.totalAmount; - assertEq(vars.actualHolderBalance, vars.expectedHolderBalance, "post-create Holder balance"); - - // Assert that the broker's balance has been updated. - vars.expectedBrokerBalance = vars.initialBrokerBalance + vars.createAmounts.brokerFee; - assertEq(vars.actualBrokerBalance, vars.expectedBrokerBalance, "post-create Broker balance"); + // Run post-create assertions and update token balances in `vars`. + postCreateStream(params); /*////////////////////////////////////////////////////////////////////////// WITHDRAW //////////////////////////////////////////////////////////////////////////*/ + // Bound the warp timestamp. + params.warpTimestamp = boundUint40( + params.warpTimestamp, params.create.timestamps.start + 1 seconds, params.create.timestamps.end + 100 seconds + ); + // Simulate the passage of time. - params.warpTimestamp = - boundUint40(params.warpTimestamp, vars.timestamps.start, vars.timestamps.end + 100 seconds); vm.warp({ newTimestamp: params.warpTimestamp }); - // Bound the withdraw amount. - vars.withdrawableAmount = lockup.withdrawableAmountOf(vars.streamId); - params.withdrawAmount = boundUint128(params.withdrawAmount, 0, vars.withdrawableAmount); - - // Check if the stream has settled or will get depleted. It is possible for the stream to be just settled - // and not depleted because the withdraw amount is fuzzed. - vars.isDepleted = params.withdrawAmount == vars.createAmounts.deposit; - vars.isSettled = lockup.refundableAmountOf(vars.streamId) == 0; - - // Only run the withdraw tests if the withdraw amount is not zero. - if (params.withdrawAmount > 0) { - // Load the pre-withdraw token balances. - vars.initialLockupBalance = vars.actualLockupBalance; - vars.initialLockupBalanceETH = address(lockup).balance; - vars.initialRecipientBalance = FORK_TOKEN.balanceOf(params.recipient); - - // Expect the relevant events to be emitted. - vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.WithdrawFromLockupStream({ - streamId: vars.streamId, - to: params.recipient, - token: FORK_TOKEN, - amount: params.withdrawAmount - }); - vm.expectEmit({ emitter: address(lockup) }); - emit IERC4906.MetadataUpdate({ _tokenId: vars.streamId }); - - // Make the withdrawal. - resetPrank({ msgSender: params.recipient }); - vm.deal({ account: params.recipient, newBalance: 100 ether }); - lockup.withdraw{ value: FEE }({ - streamId: vars.streamId, - to: params.recipient, - amount: params.withdrawAmount - }); - - // Assert that the stream's status is correct. - vars.actualStatus = lockup.statusOf(vars.streamId); - if (vars.isDepleted) { - vars.expectedStatus = Lockup.Status.DEPLETED; - } else if (vars.isSettled) { - vars.expectedStatus = Lockup.Status.SETTLED; - } else { - vars.expectedStatus = Lockup.Status.STREAMING; - } - assertEq(vars.actualStatus, vars.expectedStatus, "post-withdraw stream status"); - - // Assert that the withdrawn amount has been updated. - vars.actualWithdrawnAmount = lockup.getWithdrawnAmount(vars.streamId); - vars.expectedWithdrawnAmount = params.withdrawAmount; - assertEq(vars.actualWithdrawnAmount, vars.expectedWithdrawnAmount, "post-withdraw withdrawnAmount"); - - // Load the post-withdraw token balances. - vars.balances = getTokenBalances(address(FORK_TOKEN), Solarray.addresses(address(lockup), params.recipient)); - vars.actualLockupBalance = vars.balances[0]; - vars.actualRecipientBalance = vars.balances[1]; + // Update the streamed amount. + vars.streamedAmount = + calculateStreamedAmountLD(params.segments, params.create.timestamps.start, params.create.depositAmount); - // Assert that the contract's balance has been updated. - vars.expectedLockupBalance = vars.initialLockupBalance - uint256(params.withdrawAmount); - assertEq(vars.actualLockupBalance, vars.expectedLockupBalance, "post-withdraw Lockup contract balance"); - - // Assert that the contract's ETH balance has been updated. - assertEq(address(lockup).balance, vars.initialLockupBalanceETH + FEE, "post-withdraw Lockup balance ETH"); - - // Assert that the Recipient's balance has been updated. - vars.expectedRecipientBalance = vars.initialRecipientBalance + uint256(params.withdrawAmount); - assertEq(vars.actualRecipientBalance, vars.expectedRecipientBalance, "post-withdraw Recipient balance"); - } + // Run the fork test for withdraw function and update the parameters. + withdraw(params); /*////////////////////////////////////////////////////////////////////////// CANCEL //////////////////////////////////////////////////////////////////////////*/ - // Only run the cancel tests if the stream is neither depleted nor settled. - if (!vars.isDepleted && !vars.isSettled) { - // Load the pre-cancel token balances. - vars.balances = getTokenBalances( - address(FORK_TOKEN), Solarray.addresses(address(lockup), params.sender, params.recipient) - ); - vars.initialLockupBalance = vars.balances[0]; - vars.initialSenderBalance = vars.balances[1]; - vars.initialRecipientBalance = vars.balances[2]; - - // Expect the relevant events to be emitted. - vm.expectEmit({ emitter: address(lockup) }); - vars.senderAmount = lockup.refundableAmountOf(vars.streamId); - vars.recipientAmount = lockup.withdrawableAmountOf(vars.streamId); - emit ISablierLockupBase.CancelLockupStream( - vars.streamId, params.sender, params.recipient, FORK_TOKEN, vars.senderAmount, vars.recipientAmount - ); - vm.expectEmit({ emitter: address(lockup) }); - emit IERC4906.MetadataUpdate({ _tokenId: vars.streamId }); - - // Cancel the stream. - resetPrank({ msgSender: params.sender }); - lockup.cancel(vars.streamId); - - // Assert that the stream's status is correct. - vars.actualStatus = lockup.statusOf(vars.streamId); - vars.expectedStatus = vars.recipientAmount > 0 ? Lockup.Status.CANCELED : Lockup.Status.DEPLETED; - assertEq(vars.actualStatus, vars.expectedStatus, "post-cancel stream status"); - - // Load the post-cancel token balances. - vars.balances = getTokenBalances( - address(FORK_TOKEN), Solarray.addresses(address(lockup), params.sender, params.recipient) - ); - vars.actualLockupBalance = vars.balances[0]; - vars.actualSenderBalance = vars.balances[1]; - vars.actualRecipientBalance = vars.balances[2]; - - // Assert that the contract's balance has been updated. - vars.expectedLockupBalance = vars.initialLockupBalance - uint256(vars.senderAmount); - assertEq(vars.actualLockupBalance, vars.expectedLockupBalance, "post-cancel lockup contract balance"); - - // Assert that the Sender's balance has been updated. - vars.expectedSenderBalance = vars.initialSenderBalance + uint256(vars.senderAmount); - assertEq(vars.actualSenderBalance, vars.expectedSenderBalance, "post-cancel Sender balance"); - - // Assert that the Recipient's balance has not changed. - vars.expectedRecipientBalance = vars.initialRecipientBalance; - assertEq(vars.actualRecipientBalance, vars.expectedRecipientBalance, "post-cancel Recipient balance"); - } - - // Assert that the not burned NFT. - vars.actualNFTOwner = lockup.ownerOf({ tokenId: vars.streamId }); - vars.expectedNFTOwner = params.recipient; - assertEq(vars.actualNFTOwner, vars.expectedNFTOwner, "post-cancel NFT owner"); + // Run the cancel test. + cancel(params); } } diff --git a/tests/fork/LockupLinear.t.sol b/tests/fork/LockupLinear.t.sol index 8919cfb91..157f27327 100644 --- a/tests/fork/LockupLinear.t.sol +++ b/tests/fork/LockupLinear.t.sol @@ -3,92 +3,24 @@ pragma solidity >=0.8.22 <0.9.0; import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { ud } from "@prb/math/src/UD60x18.sol"; -import { Solarray } from "solarray/src/Solarray.sol"; -import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; -import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; -import { Broker, Lockup, LockupLinear } from "src/types/DataTypes.sol"; -import { Fork_Test } from "./Fork.t.sol"; -abstract contract Lockup_Linear_Fork_Test is Fork_Test { - /*////////////////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////////////////*/ - - constructor(IERC20 forkToken) Fork_Test(forkToken) { } +import { ISablierLockupLinear } from "src/interfaces/ISablierLockupLinear.sol"; +import { Lockup } from "src/types/Lockup.sol"; +import { Lockup_Fork_Test } from "./Lockup.t.sol"; +abstract contract Lockup_Linear_Fork_Test is Lockup_Fork_Test { /*////////////////////////////////////////////////////////////////////////// - SET-UP FUNCTION + CONSTRUCTOR //////////////////////////////////////////////////////////////////////////*/ - function setUp() public virtual override { - Fork_Test.setUp(); - - // Approve {SablierLockup} to transfer the token holder's tokens. - // We use a low-level call to ignore reverts because the token can have the missing return value bug. - (bool success,) = address(FORK_TOKEN).call(abi.encodeCall(IERC20.approve, (address(lockup), MAX_UINT256))); - success; + constructor(IERC20 forkToken) Lockup_Fork_Test(forkToken) { + lockupModel = Lockup.Model.LOCKUP_LINEAR; } /*////////////////////////////////////////////////////////////////////////// TEST FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - struct Params { - address sender; - address recipient; - uint128 totalAmount; - uint128 withdrawAmount; - uint40 warpTimestamp; - Lockup.Timestamps timestamps; - LockupLinear.UnlockAmounts unlockAmounts; - uint40 cliffTime; - Broker broker; - } - - struct Vars { - // Generic vars - uint256 actualLockupBalance; - uint256 actualHolderBalance; - address actualNFTOwner; - uint256 actualRecipientBalance; - Lockup.Status actualStatus; - uint256[] balances; - uint40 blockTimestamp; - uint40 endTimeLowerBound; - uint256 expectedLockupBalance; - uint256 expectedHolderBalance; - address expectedNFTOwner; - uint256 expectedRecipientBalance; - Lockup.Status expectedStatus; - bool hasCliff; - uint256 initialLockupBalance; - uint256 initialRecipientBalance; - bool isCancelable; - bool isDepleted; - bool isSettled; - uint256 streamId; - uint128 streamedAmount; - // Create vars - uint256 actualBrokerBalance; - uint256 actualNextStreamId; - Lockup.CreateAmounts createAmounts; - uint256 expectedBrokerBalance; - uint256 expectedNextStreamId; - uint256 initialBrokerBalance; - // Withdraw vars - uint128 actualWithdrawnAmount; - uint128 expectedWithdrawnAmount; - uint256 initialLockupBalanceETH; - uint128 withdrawableAmount; - // Cancel vars - uint256 actualSenderBalance; - uint256 expectedSenderBalance; - uint256 initialSenderBalance; - uint128 recipientAmount; - uint128 senderAmount; - } - /// @dev Checklist: /// /// - It should perform all expected ERC-20 transfers @@ -105,315 +37,91 @@ abstract contract Lockup_Linear_Fork_Test is Fork_Test { /// /// Given enough fuzz runs, all of the following scenarios will be fuzzed: /// - /// - Multiple values for the sender, recipient, and broker - /// - Multiple values for the total amount + /// - Multiple values for the sender, and recipient + /// - Multiple values for the deposit amount /// - Multiple values for the withdraw amount, including zero /// - Start time in the past /// - Start time in the present /// - Start time in the future /// - Multiple values for the cliff time and the end time /// - Cliff time zero and not zero - /// - Multiple values for the broker fee, including zero /// - The whole gamut of stream statuses function testForkFuzz_CreateWithdrawCancel(Params memory params) external { - checkUsers(params.sender, params.recipient, params.broker.account, address(lockup)); - - // Bound the parameters. - Vars memory vars; - vars.blockTimestamp = getBlockTimestamp(); - params.broker.fee = _bound(params.broker.fee, 0, MAX_BROKER_FEE); - params.timestamps.start = boundUint40( - params.timestamps.start, vars.blockTimestamp - 1000 seconds, vars.blockTimestamp + 10_000 seconds - ); - params.totalAmount = boundUint128(params.totalAmount, 1, uint128(initialHolderBalance)); - - // The cliff time must be either zero or greater than the start time. - vars.hasCliff = params.cliffTime > 0; - params.cliffTime = vars.hasCliff - ? boundUint40(params.cliffTime, params.timestamps.start + 1 seconds, params.timestamps.start + 52 weeks) - : 0; - - // Bound the end time so that it is always greater than the start time, and the cliff time. - vars.endTimeLowerBound = maxOfTwo(params.timestamps.start, params.cliffTime); - params.timestamps.end = - boundUint40(params.timestamps.end, vars.endTimeLowerBound + 1 seconds, MAX_UNIX_TIMESTAMP); - - // Calculate the broker fee amount and the deposit amount. - vars.createAmounts.brokerFee = ud(params.totalAmount).mul(params.broker.fee).intoUint128(); - vars.createAmounts.deposit = params.totalAmount - vars.createAmounts.brokerFee; - - // Bound the unlock amounts. - params.unlockAmounts.start = boundUint128(params.unlockAmounts.start, 0, vars.createAmounts.deposit); - params.unlockAmounts.cliff = vars.hasCliff - ? boundUint128(params.unlockAmounts.cliff, 0, vars.createAmounts.deposit - params.unlockAmounts.start) - : 0; - - // Make the holder the caller. - resetPrank(forkTokenHolder); - /*////////////////////////////////////////////////////////////////////////// CREATE //////////////////////////////////////////////////////////////////////////*/ - // Load the pre-create token balances. - vars.balances = - getTokenBalances(address(FORK_TOKEN), Solarray.addresses(address(lockup), params.broker.account)); - vars.initialLockupBalance = vars.balances[0]; - vars.initialBrokerBalance = vars.balances[1]; - - vars.streamId = lockup.nextStreamId(); + // Bound the fuzzed parameters and load values into `vars`. + preCreateStream(params); // Expect the relevant events to be emitted. vm.expectEmit({ emitter: address(lockup) }); emit IERC4906.MetadataUpdate({ _tokenId: vars.streamId }); vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockup.CreateLockupLinearStream({ + emit ISablierLockupLinear.CreateLockupLinearStream({ streamId: vars.streamId, - commonParams: Lockup.CreateEventCommon({ - funder: forkTokenHolder, - sender: params.sender, - recipient: params.recipient, - amounts: vars.createAmounts, - token: FORK_TOKEN, - cancelable: true, - transferable: true, - timestamps: params.timestamps, - shape: "Linear Shape", - broker: params.broker.account - }), + commonParams: defaults.lockupCreateEvent({ caller: forkTokenHolder, params: params.create, token_: FORK_TOKEN }), cliffTime: params.cliffTime, unlockAmounts: params.unlockAmounts }); // Create the stream. - lockup.createWithTimestampsLL( - Lockup.CreateWithTimestamps({ - sender: params.sender, - recipient: params.recipient, - totalAmount: params.totalAmount, - token: FORK_TOKEN, - cancelable: true, - transferable: true, - timestamps: params.timestamps, - shape: "Linear Shape", - broker: params.broker - }), - params.unlockAmounts, - params.cliffTime - ); - - vars.streamedAmount = calculateLockupLinearStreamedAmount( - params.timestamps.start, - params.cliffTime, - params.timestamps.end, - vars.createAmounts.deposit, - params.unlockAmounts - ); - - // Check if the stream is settled. It is possible for a Lockup Linear stream to settle at the time of creation - // in case 1. the start unlock amount equals the deposited amount 2. end time is in the past. - if (vars.streamedAmount == vars.createAmounts.deposit) { - vars.isSettled = true; - } else { - vars.isSettled = false; - } - vars.isCancelable = vars.isSettled ? false : true; + lockup.createWithTimestampsLL(params.create, params.unlockAmounts, params.cliffTime); - // Assert that the stream has been created. + // Assert that the stream is created with the correct parameters. + assertEq({ lockup: lockup, streamId: vars.streamId, expectedLockup: params.create }); assertEq(lockup.getCliffTime(vars.streamId), params.cliffTime, "cliffTime"); - assertEq(lockup.getDepositedAmount(vars.streamId), vars.createAmounts.deposit, "depositedAmount"); - assertEq(lockup.isCancelable(vars.streamId), vars.isCancelable, "isCancelable"); - assertFalse(lockup.isDepleted(vars.streamId), "isDepleted"); - assertTrue(lockup.isStream(vars.streamId), "isStream"); - assertTrue(lockup.isTransferable(vars.streamId), "isTransferable"); - assertEq(lockup.getEndTime(vars.streamId), params.timestamps.end, "endTime"); - assertEq(lockup.getRecipient(vars.streamId), params.recipient, "recipient"); - assertEq(lockup.getSender(vars.streamId), params.sender, "sender"); - assertEq(lockup.getStartTime(vars.streamId), params.timestamps.start, "startTime"); - assertEq(lockup.getUnderlyingToken(vars.streamId), FORK_TOKEN, "underlyingToken"); - assertEq(lockup.getUnlockAmounts(vars.streamId).start, params.unlockAmounts.start, "unlockAmounts.start"); - assertEq(lockup.getUnlockAmounts(vars.streamId).cliff, params.unlockAmounts.cliff, "unlockAmounts.cliff"); - assertFalse(lockup.wasCanceled(vars.streamId), "wasCanceled"); - - // Assert that the stream's status is correct. - vars.actualStatus = lockup.statusOf(vars.streamId); - if (vars.streamedAmount == vars.createAmounts.deposit) { - vars.expectedStatus = Lockup.Status.SETTLED; - } else if (params.timestamps.start > vars.blockTimestamp) { - vars.expectedStatus = Lockup.Status.PENDING; - } else { - vars.expectedStatus = Lockup.Status.STREAMING; - } - assertEq(vars.actualStatus, vars.expectedStatus, "post-create stream status"); - - // Assert that the next stream ID has been bumped. - vars.actualNextStreamId = lockup.nextStreamId(); - vars.expectedNextStreamId = vars.streamId + 1; - assertEq(vars.actualNextStreamId, vars.expectedNextStreamId, "post-create nextStreamId"); + assertEq(lockup.getUnlockAmounts(vars.streamId), params.unlockAmounts); + assertEq(lockup.getLockupModel(vars.streamId), Lockup.Model.LOCKUP_LINEAR); - // Assert that the NFT has been minted. - vars.actualNFTOwner = lockup.ownerOf({ tokenId: vars.streamId }); - vars.expectedNFTOwner = params.recipient; - assertEq(vars.actualNFTOwner, vars.expectedNFTOwner, "post-create NFT owner"); - - // Load the post-create token balances. - vars.balances = getTokenBalances( - address(FORK_TOKEN), Solarray.addresses(address(lockup), forkTokenHolder, params.broker.account) + // Update the streamed amount. + vars.streamedAmount = calculateStreamedAmountLL( + params.create.timestamps.start, + params.cliffTime, + params.create.timestamps.end, + params.create.depositAmount, + params.unlockAmounts ); - vars.actualLockupBalance = vars.balances[0]; - vars.actualHolderBalance = vars.balances[1]; - vars.actualBrokerBalance = vars.balances[2]; - - // Assert that the Lockup contract's balance has been updated. - vars.expectedLockupBalance = vars.initialLockupBalance + vars.createAmounts.deposit; - assertEq(vars.actualLockupBalance, vars.expectedLockupBalance, "post-create Lockup balance"); - - // Assert that the holder's balance has been updated. - vars.expectedHolderBalance = initialHolderBalance - params.totalAmount; - assertEq(vars.actualHolderBalance, vars.expectedHolderBalance, "post-create Holder balance"); - // Assert that the broker's balance has been updated. - vars.expectedBrokerBalance = vars.initialBrokerBalance + vars.createAmounts.brokerFee; - assertEq(vars.actualBrokerBalance, vars.expectedBrokerBalance, "post-create Broker balance"); + // Run post-create assertions and update token balances in `vars`. + postCreateStream(params); /*////////////////////////////////////////////////////////////////////////// WITHDRAW //////////////////////////////////////////////////////////////////////////*/ + // Bound the warp timestamp according to the cliff status, if it exists. + if (vars.hasCliff) { + params.warpTimestamp = + boundUint40(params.warpTimestamp, params.cliffTime, params.create.timestamps.end + 100 seconds); + } else { + params.warpTimestamp = boundUint40( + params.warpTimestamp, + params.create.timestamps.start + 1 seconds, + params.create.timestamps.end + 100 seconds + ); + } + // Simulate the passage of time. - params.warpTimestamp = boundUint40( - params.warpTimestamp, - vars.hasCliff ? params.cliffTime : params.timestamps.start + 1 seconds, - params.timestamps.end + 100 seconds - ); vm.warp({ newTimestamp: params.warpTimestamp }); - // Bound the withdraw amount. - vars.withdrawableAmount = lockup.withdrawableAmountOf(vars.streamId); - params.withdrawAmount = boundUint128(params.withdrawAmount, 0, vars.withdrawableAmount); - - // Check if the stream has settled or will get depleted. It is possible for the stream to be just settled - // and not depleted because the withdraw amount is fuzzed. - vars.isSettled = lockup.refundableAmountOf(vars.streamId) == 0; - vars.isDepleted = params.withdrawAmount == vars.createAmounts.deposit; - - // Only run the withdraw tests if the withdraw amount is not zero. - if (params.withdrawAmount > 0) { - // Load the pre-withdraw token balances. - vars.initialLockupBalance = vars.actualLockupBalance; - vars.initialLockupBalanceETH = address(lockup).balance; - vars.initialRecipientBalance = FORK_TOKEN.balanceOf(params.recipient); - - // Expect the relevant events to be emitted. - vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.WithdrawFromLockupStream({ - streamId: vars.streamId, - to: params.recipient, - token: FORK_TOKEN, - amount: params.withdrawAmount - }); - vm.expectEmit({ emitter: address(lockup) }); - emit IERC4906.MetadataUpdate({ _tokenId: vars.streamId }); - - // Make the withdrawal and pay a fee. - resetPrank({ msgSender: params.recipient }); - vm.deal({ account: params.recipient, newBalance: 100 ether }); - lockup.withdraw{ value: FEE }({ - streamId: vars.streamId, - to: params.recipient, - amount: params.withdrawAmount - }); - - // Assert that the stream's status is correct. - vars.actualStatus = lockup.statusOf(vars.streamId); - if (vars.isDepleted) { - vars.expectedStatus = Lockup.Status.DEPLETED; - } else if (vars.isSettled) { - vars.expectedStatus = Lockup.Status.SETTLED; - } else { - vars.expectedStatus = Lockup.Status.STREAMING; - } - assertEq(vars.actualStatus, vars.expectedStatus, "post-withdraw stream status"); - - // Assert that the withdrawn amount has been updated. - vars.actualWithdrawnAmount = lockup.getWithdrawnAmount(vars.streamId); - vars.expectedWithdrawnAmount = params.withdrawAmount; - assertEq(vars.actualWithdrawnAmount, vars.expectedWithdrawnAmount, "post-withdraw withdrawnAmount"); - - // Load the post-withdraw token balances. - vars.balances = getTokenBalances(address(FORK_TOKEN), Solarray.addresses(address(lockup), params.recipient)); - vars.actualLockupBalance = vars.balances[0]; - vars.actualRecipientBalance = vars.balances[1]; - - // Assert that the contract's balance has been updated. - vars.expectedLockupBalance = vars.initialLockupBalance - uint256(params.withdrawAmount); - assertEq(vars.actualLockupBalance, vars.expectedLockupBalance, "post-withdraw Lockup balance"); - - // Assert that the contract's ETH balance has been updated. - assertEq(address(lockup).balance, vars.initialLockupBalanceETH + FEE, "post-withdraw Lockup balance ETH"); + // Update the streamed amount. + vars.streamedAmount = calculateStreamedAmountLL( + params.create.timestamps.start, + params.cliffTime, + params.create.timestamps.end, + params.create.depositAmount, + params.unlockAmounts + ); - // Assert that the Recipient's balance has been updated. - vars.expectedRecipientBalance = vars.initialRecipientBalance + uint256(params.withdrawAmount); - assertEq(vars.actualRecipientBalance, vars.expectedRecipientBalance, "post-withdraw Recipient balance"); - } + // Run the fork test for withdraw function and update the parameters. + withdraw(params); /*////////////////////////////////////////////////////////////////////////// CANCEL //////////////////////////////////////////////////////////////////////////*/ - // Only run the cancel tests if the stream is neither depleted nor settled. - if (!vars.isDepleted && !vars.isSettled) { - // Load the pre-cancel token balances. - vars.balances = getTokenBalances( - address(FORK_TOKEN), Solarray.addresses(address(lockup), params.sender, params.recipient) - ); - vars.initialLockupBalance = vars.balances[0]; - vars.initialSenderBalance = vars.balances[1]; - vars.initialRecipientBalance = vars.balances[2]; - - // Expect the relevant events to be emitted. - vm.expectEmit({ emitter: address(lockup) }); - vars.senderAmount = lockup.refundableAmountOf(vars.streamId); - vars.recipientAmount = lockup.withdrawableAmountOf(vars.streamId); - emit ISablierLockupBase.CancelLockupStream( - vars.streamId, params.sender, params.recipient, FORK_TOKEN, vars.senderAmount, vars.recipientAmount - ); - vm.expectEmit({ emitter: address(lockup) }); - emit IERC4906.MetadataUpdate({ _tokenId: vars.streamId }); - - // Cancel the stream. - resetPrank({ msgSender: params.sender }); - lockup.cancel(vars.streamId); - - // Assert that the stream's status is correct. - vars.actualStatus = lockup.statusOf(vars.streamId); - vars.expectedStatus = vars.recipientAmount > 0 ? Lockup.Status.CANCELED : Lockup.Status.DEPLETED; - assertEq(vars.actualStatus, vars.expectedStatus, "post-cancel stream status"); - - // Load the post-cancel token balances. - vars.balances = getTokenBalances( - address(FORK_TOKEN), Solarray.addresses(address(lockup), params.sender, params.recipient) - ); - vars.actualLockupBalance = vars.balances[0]; - vars.actualSenderBalance = vars.balances[1]; - vars.actualRecipientBalance = vars.balances[2]; - - // Assert that the contract's balance has been updated. - vars.expectedLockupBalance = vars.initialLockupBalance - uint256(vars.senderAmount); - assertEq(vars.actualLockupBalance, vars.expectedLockupBalance, "post-cancel Lockup balance"); - - // Assert that the Sender's balance has been updated. - vars.expectedSenderBalance = vars.initialSenderBalance + uint256(vars.senderAmount); - assertEq(vars.actualSenderBalance, vars.expectedSenderBalance, "post-cancel Sender balance"); - - // Assert that the Recipient's balance has not changed. - vars.expectedRecipientBalance = vars.initialRecipientBalance; - assertEq(vars.actualRecipientBalance, vars.expectedRecipientBalance, "post-cancel Recipient balance"); - } - - // Assert that the not burned NFT. - vars.actualNFTOwner = lockup.ownerOf({ tokenId: vars.streamId }); - vars.expectedNFTOwner = params.recipient; - assertEq(vars.actualNFTOwner, vars.expectedNFTOwner, "post-cancel NFT owner"); + // Run the cancel test. + cancel(params); } } diff --git a/tests/fork/LockupTranched.t.sol b/tests/fork/LockupTranched.t.sol index c1023612d..4e7656f35 100644 --- a/tests/fork/LockupTranched.t.sol +++ b/tests/fork/LockupTranched.t.sol @@ -3,87 +3,23 @@ pragma solidity >=0.8.22 <0.9.0; import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { Solarray } from "solarray/src/Solarray.sol"; -import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; -import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; -import { Broker, Lockup, LockupTranched } from "src/types/DataTypes.sol"; -import { Fork_Test } from "./Fork.t.sol"; +import { ISablierLockupTranched } from "src/interfaces/ISablierLockupTranched.sol"; +import { Lockup } from "src/types/Lockup.sol"; +import { Lockup_Fork_Test } from "./Lockup.t.sol"; -abstract contract Lockup_Tranched_Fork_Test is Fork_Test { +abstract contract Lockup_Tranched_Fork_Test is Lockup_Fork_Test { /*////////////////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////////////////*/ - constructor(IERC20 forkToken) Fork_Test(forkToken) { } - - /*////////////////////////////////////////////////////////////////////////// - SET-UP FUNCTION - //////////////////////////////////////////////////////////////////////////*/ - - function setUp() public virtual override { - Fork_Test.setUp(); - - // Approve {SablierLockup} to transfer the holder's tokens. - // We use a low-level call to ignore reverts because the token can have the missing return value bug. - (bool success,) = address(FORK_TOKEN).call(abi.encodeCall(IERC20.approve, (address(lockup), MAX_UINT256))); - success; + constructor(IERC20 forkToken) Lockup_Fork_Test(forkToken) { + lockupModel = Lockup.Model.LOCKUP_TRANCHED; } /*////////////////////////////////////////////////////////////////////////// TEST FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - struct Params { - address sender; - address recipient; - uint128 withdrawAmount; - uint40 startTime; - uint40 warpTimestamp; - LockupTranched.Tranche[] tranches; - Broker broker; - } - - struct Vars { - // Generic vars - address actualNFTOwner; - uint256 actualLockupBalance; - uint256 actualRecipientBalance; - Lockup.Status actualStatus; - uint256[] balances; - address expectedNFTOwner; - uint256 expectedLockupBalance; - uint256 expectedRecipientBalance; - Lockup.Status expectedStatus; - uint256 initialLockupBalance; - uint256 initialRecipientBalance; - bool isCancelable; - bool isDepleted; - bool isSettled; - uint256 streamId; - Lockup.Timestamps timestamps; - // Create vars - uint256 actualBrokerBalance; - uint256 actualHolderBalance; - uint256 actualNextStreamId; - Lockup.CreateAmounts createAmounts; - uint256 expectedBrokerBalance; - uint256 expectedHolderBalance; - uint256 expectedNextStreamId; - uint256 initialBrokerBalance; - uint128 totalAmount; - // Withdraw vars - uint128 actualWithdrawnAmount; - uint128 expectedWithdrawnAmount; - uint256 initialLockupBalanceETH; - uint128 withdrawableAmount; - // Cancel vars - uint256 actualSenderBalance; - uint256 expectedSenderBalance; - uint256 initialSenderBalance; - uint128 recipientAmount; - uint128 senderAmount; - } - /// @dev Checklist: /// /// - It should perform all expected ERC-20 transfers @@ -99,281 +35,71 @@ abstract contract Lockup_Tranched_Fork_Test is Fork_Test { /// /// Given enough fuzz runs, all of the following scenarios will be fuzzed: /// - /// - Multiple values for the funder, recipient, sender, and broker + /// - Multiple values for the recipient, and sender /// - Multiple values for the total amount /// - Start time in the past /// - Start time in the present /// - Start time in the future /// - Start time equal and not equal to the first tranche timestamp - /// - Multiple values for the broker fee, including zero. /// - Multiple values for the withdraw amount, including zero /// - The whole gamut of stream statuses function testForkFuzz_CreateWithdrawCancel(Params memory params) external { - checkUsers(params.sender, params.recipient, params.broker.account, address(lockup)); - vm.assume(params.tranches.length != 0); - params.broker.fee = _bound(params.broker.fee, 0, MAX_BROKER_FEE); - params.startTime = boundUint40(params.startTime, 1, getBlockTimestamp() + 2 days); - - // Fuzz the tranche timestamps. - fuzzTrancheTimestamps(params.tranches, params.startTime); - - // Fuzz the tranche amounts and calculate the total and create amounts (deposit and broker fee). - Vars memory vars; - (vars.totalAmount, vars.createAmounts) = fuzzTranchedStreamAmounts({ - upperBound: uint128(initialHolderBalance), - tranches: params.tranches, - brokerFee: params.broker.fee - }); - - // Make the holder the caller. - resetPrank(forkTokenHolder); - /*////////////////////////////////////////////////////////////////////////// CREATE //////////////////////////////////////////////////////////////////////////*/ - // Load the pre-create token balances. - vars.balances = - getTokenBalances(address(FORK_TOKEN), Solarray.addresses(address(lockup), params.broker.account)); - vars.initialLockupBalance = vars.balances[0]; - vars.initialBrokerBalance = vars.balances[1]; + // Fuzz the tranche timestamps. + vm.assume(params.tranches.length != 0); - vars.streamId = lockup.nextStreamId(); - vars.timestamps = - Lockup.Timestamps({ start: params.startTime, end: params.tranches[params.tranches.length - 1].timestamp }); + // Bound the fuzzed parameters and load values into `vars`. + preCreateStream(params); // Expect the relevant events to be emitted. vm.expectEmit({ emitter: address(lockup) }); emit IERC4906.MetadataUpdate({ _tokenId: vars.streamId }); vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockup.CreateLockupTranchedStream({ + emit ISablierLockupTranched.CreateLockupTranchedStream({ streamId: vars.streamId, - commonParams: Lockup.CreateEventCommon({ - funder: forkTokenHolder, - sender: params.sender, - recipient: params.recipient, - amounts: vars.createAmounts, - token: FORK_TOKEN, - cancelable: true, - transferable: true, - timestamps: vars.timestamps, - shape: "Tranched Shape", - broker: params.broker.account - }), + commonParams: defaults.lockupCreateEvent({ caller: forkTokenHolder, params: params.create, token_: FORK_TOKEN }), tranches: params.tranches }); // Create the stream. - lockup.createWithTimestampsLT( - Lockup.CreateWithTimestamps({ - sender: params.sender, - recipient: params.recipient, - totalAmount: vars.totalAmount, - token: FORK_TOKEN, - cancelable: true, - transferable: true, - timestamps: vars.timestamps, - shape: "Tranched Shape", - broker: params.broker - }), - params.tranches - ); - - // Check if the stream is settled. It is possible for a Lockup Tranched stream to settle at the time of creation - // because some tranche amounts can be zero or the last tranche timestamp can be in the past - vars.isSettled = lockup.refundableAmountOf(vars.streamId) == 0 || vars.timestamps.end <= getBlockTimestamp(); - vars.isCancelable = vars.isSettled ? false : true; + lockup.createWithTimestampsLT(params.create, params.tranches); - // Assert that the stream has been created. - assertEq(lockup.isCancelable(vars.streamId), vars.isCancelable, "isCancelable"); - assertFalse(lockup.isDepleted(vars.streamId), "isDepleted"); - assertTrue(lockup.isStream(vars.streamId), "isStream"); - assertTrue(lockup.isTransferable(vars.streamId), "isTransferable"); - assertEq(lockup.getDepositedAmount(vars.streamId), vars.createAmounts.deposit, "depositedAmount"); - assertEq(lockup.getEndTime(vars.streamId), vars.timestamps.end, "endTime"); - assertEq(lockup.getRecipient(vars.streamId), params.recipient, "recipient"); - assertEq(lockup.getSender(vars.streamId), params.sender, "sender"); - assertEq(lockup.getStartTime(vars.streamId), params.startTime, "startTime"); + // Assert that the stream is created with the correct parameters. + assertEq({ streamId: vars.streamId, lockup: lockup, expectedLockup: params.create }); assertEq(lockup.getTranches(vars.streamId), params.tranches); - assertEq(lockup.getUnderlyingToken(vars.streamId), FORK_TOKEN, "underlyingToken"); - assertFalse(lockup.wasCanceled(vars.streamId), "wasCanceled"); - - // Assert that the stream's status is correct. - vars.actualStatus = lockup.statusOf(vars.streamId); - if (params.startTime > getBlockTimestamp()) { - vars.expectedStatus = Lockup.Status.PENDING; - } else if (vars.isSettled) { - vars.expectedStatus = Lockup.Status.SETTLED; - } else { - vars.expectedStatus = Lockup.Status.STREAMING; - } - assertEq(vars.actualStatus, vars.expectedStatus, "post-create stream status"); - // Assert that the next stream ID has been bumped. - vars.actualNextStreamId = lockup.nextStreamId(); - vars.expectedNextStreamId = vars.streamId + 1; - assertEq(vars.actualNextStreamId, vars.expectedNextStreamId, "post-create nextStreamId"); + // Update the streamed amount. + vars.streamedAmount = calculateStreamedAmountLT(params.tranches, params.create.depositAmount); - // Assert that the NFT has been minted. - vars.actualNFTOwner = lockup.ownerOf({ tokenId: vars.streamId }); - vars.expectedNFTOwner = params.recipient; - assertEq(vars.actualNFTOwner, vars.expectedNFTOwner, "post-create NFT owner"); - - // Load the post-create token balances. - vars.balances = getTokenBalances( - address(FORK_TOKEN), Solarray.addresses(address(lockup), forkTokenHolder, params.broker.account) - ); - vars.actualLockupBalance = vars.balances[0]; - vars.actualHolderBalance = vars.balances[1]; - vars.actualBrokerBalance = vars.balances[2]; - - // Assert that the contract's balance has been updated. - vars.expectedLockupBalance = vars.initialLockupBalance + vars.createAmounts.deposit; - assertEq(vars.actualLockupBalance, vars.expectedLockupBalance, "post-create Lockup contract balance"); - - // Assert that the holder's balance has been updated. - vars.expectedHolderBalance = initialHolderBalance - vars.totalAmount; - assertEq(vars.actualHolderBalance, vars.expectedHolderBalance, "post-create Holder balance"); - - // Assert that the broker's balance has been updated. - vars.expectedBrokerBalance = vars.initialBrokerBalance + vars.createAmounts.brokerFee; - assertEq(vars.actualBrokerBalance, vars.expectedBrokerBalance, "post-create Broker balance"); + // Run post-create assertions and update token balances in `vars`. + postCreateStream(params); /*////////////////////////////////////////////////////////////////////////// WITHDRAW //////////////////////////////////////////////////////////////////////////*/ + // Bound the warp timestamp. + params.warpTimestamp = boundUint40( + params.warpTimestamp, params.create.timestamps.start, params.create.timestamps.end + 100 seconds + ); + // Simulate the passage of time. - params.warpTimestamp = - boundUint40(params.warpTimestamp, vars.timestamps.start, vars.timestamps.end + 100 seconds); vm.warp({ newTimestamp: params.warpTimestamp }); - // Bound the withdraw amount. - vars.withdrawableAmount = lockup.withdrawableAmountOf(vars.streamId); - params.withdrawAmount = boundUint128(params.withdrawAmount, 0, vars.withdrawableAmount); - - // Check if the stream has settled or will get depleted. It is possible for the stream to be just settled - // and not depleted because the withdraw amount is fuzzed. - vars.isDepleted = params.withdrawAmount == vars.createAmounts.deposit; - vars.isSettled = lockup.refundableAmountOf(vars.streamId) == 0; - - // Only run the withdraw tests if the withdraw amount is not zero. - if (params.withdrawAmount > 0) { - // Load the pre-withdraw token balances. - vars.initialLockupBalance = vars.actualLockupBalance; - vars.initialLockupBalanceETH = address(lockup).balance; - vars.initialRecipientBalance = FORK_TOKEN.balanceOf(params.recipient); - - // Expect the relevant events to be emitted. - vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.WithdrawFromLockupStream({ - streamId: vars.streamId, - to: params.recipient, - token: FORK_TOKEN, - amount: params.withdrawAmount - }); - vm.expectEmit({ emitter: address(lockup) }); - emit IERC4906.MetadataUpdate({ _tokenId: vars.streamId }); - - // Make the withdrawal. - resetPrank({ msgSender: params.recipient }); - vm.deal({ account: params.recipient, newBalance: 100 ether }); - lockup.withdraw{ value: FEE }({ - streamId: vars.streamId, - to: params.recipient, - amount: params.withdrawAmount - }); - - // Assert that the stream's status is correct. - vars.actualStatus = lockup.statusOf(vars.streamId); - if (vars.isDepleted) { - vars.expectedStatus = Lockup.Status.DEPLETED; - } else if (vars.isSettled) { - vars.expectedStatus = Lockup.Status.SETTLED; - } else { - vars.expectedStatus = Lockup.Status.STREAMING; - } - assertEq(vars.actualStatus, vars.expectedStatus, "post-withdraw stream status"); - - // Assert that the withdrawn amount has been updated. - vars.actualWithdrawnAmount = lockup.getWithdrawnAmount(vars.streamId); - vars.expectedWithdrawnAmount = params.withdrawAmount; - assertEq(vars.actualWithdrawnAmount, vars.expectedWithdrawnAmount, "post-withdraw withdrawnAmount"); + // Update the streamed amount. + vars.streamedAmount = calculateStreamedAmountLT(params.tranches, params.create.depositAmount); - // Load the post-withdraw token balances. - vars.balances = getTokenBalances(address(FORK_TOKEN), Solarray.addresses(address(lockup), params.recipient)); - vars.actualLockupBalance = vars.balances[0]; - vars.actualRecipientBalance = vars.balances[1]; - - // Assert that the contract's balance has been updated. - vars.expectedLockupBalance = vars.initialLockupBalance - uint256(params.withdrawAmount); - assertEq(vars.actualLockupBalance, vars.expectedLockupBalance, "post-withdraw Lockup contract balance"); - - // Assert that the contract's ETH balance has been updated. - assertEq(address(lockup).balance, vars.initialLockupBalanceETH + FEE, "post-withdraw Lockup balance ETH"); - - // Assert that the Recipient's balance has been updated. - vars.expectedRecipientBalance = vars.initialRecipientBalance + uint256(params.withdrawAmount); - assertEq(vars.actualRecipientBalance, vars.expectedRecipientBalance, "post-withdraw Recipient balance"); - } + // Run the fork test for withdraw function and update the parameters. + withdraw(params); /*////////////////////////////////////////////////////////////////////////// CANCEL //////////////////////////////////////////////////////////////////////////*/ - // Only run the cancel tests if the stream is neither depleted nor settled. - if (!vars.isDepleted && !vars.isSettled) { - // Load the pre-cancel token balances. - vars.balances = getTokenBalances( - address(FORK_TOKEN), Solarray.addresses(address(lockup), params.sender, params.recipient) - ); - vars.initialLockupBalance = vars.balances[0]; - vars.initialSenderBalance = vars.balances[1]; - vars.initialRecipientBalance = vars.balances[2]; - - // Expect the relevant events to be emitted. - vm.expectEmit({ emitter: address(lockup) }); - vars.senderAmount = lockup.refundableAmountOf(vars.streamId); - vars.recipientAmount = lockup.withdrawableAmountOf(vars.streamId); - emit ISablierLockupBase.CancelLockupStream( - vars.streamId, params.sender, params.recipient, FORK_TOKEN, vars.senderAmount, vars.recipientAmount - ); - vm.expectEmit({ emitter: address(lockup) }); - emit IERC4906.MetadataUpdate({ _tokenId: vars.streamId }); - - // Cancel the stream. - resetPrank({ msgSender: params.sender }); - lockup.cancel(vars.streamId); - - // Assert that the stream's status is correct. - vars.actualStatus = lockup.statusOf(vars.streamId); - vars.expectedStatus = vars.recipientAmount > 0 ? Lockup.Status.CANCELED : Lockup.Status.DEPLETED; - assertEq(vars.actualStatus, vars.expectedStatus, "post-cancel stream status"); - - // Load the post-cancel token balances. - vars.balances = getTokenBalances( - address(FORK_TOKEN), Solarray.addresses(address(lockup), params.sender, params.recipient) - ); - vars.actualLockupBalance = vars.balances[0]; - vars.actualSenderBalance = vars.balances[1]; - vars.actualRecipientBalance = vars.balances[2]; - - // Assert that the contract's balance has been updated. - vars.expectedLockupBalance = vars.initialLockupBalance - uint256(vars.senderAmount); - assertEq(vars.actualLockupBalance, vars.expectedLockupBalance, "post-cancel lockup contract balance"); - - // Assert that the Sender's balance has been updated. - vars.expectedSenderBalance = vars.initialSenderBalance + uint256(vars.senderAmount); - assertEq(vars.actualSenderBalance, vars.expectedSenderBalance, "post-cancel Sender balance"); - - // Assert that the Recipient's balance has not changed. - vars.expectedRecipientBalance = vars.initialRecipientBalance; - assertEq(vars.actualRecipientBalance, vars.expectedRecipientBalance, "post-cancel Recipient balance"); - } - - // Assert that the not burned NFT. - vars.actualNFTOwner = lockup.ownerOf({ tokenId: vars.streamId }); - vars.expectedNFTOwner = params.recipient; - assertEq(vars.actualNFTOwner, vars.expectedNFTOwner, "post-cancel NFT owner"); + // Run the cancel test. + cancel(params); } } diff --git a/tests/integration/Integration.t.sol b/tests/integration/Integration.t.sol index b88389284..20afda82d 100644 --- a/tests/integration/Integration.t.sol +++ b/tests/integration/Integration.t.sol @@ -1,8 +1,13 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; +import { Errors as EvmUtilsErrors } from "@sablier/evm-utils/src/libraries/Errors.sol"; + import { Errors } from "src/libraries/Errors.sol"; -import { Lockup, LockupDynamic, LockupLinear, LockupTranched } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; +import { LockupDynamic } from "src/types/LockupDynamic.sol"; +import { LockupLinear } from "src/types/LockupLinear.sol"; +import { LockupTranched } from "src/types/LockupTranched.sol"; import { Base_Test } from "../Base.t.sol"; import { @@ -21,26 +26,6 @@ abstract contract Integration_Test is Base_Test { Lockup.Model internal lockupModel; - // Common stream IDs to be used across the tests. - // Default stream ID. - uint256 internal defaultStreamId; - // A stream with a recipient contract that is not allowed to hook. - uint256 internal notAllowedtoHookStreamId; - // A non-cancelable stream ID. - uint256 internal notCancelableStreamId; - // A non-transferable stream ID. - uint256 internal notTransferableStreamId; - // A stream ID that does not exist. - uint256 internal nullStreamId = 1729; - // A stream with a recipient contract that implements {ISablierLockupRecipient}. - uint256 internal recipientGoodStreamId; - // A stream with a recipient contract that returns invalid selector bytes on the hook call. - uint256 internal recipientInvalidSelectorStreamId; - // A stream with a reentrant contract as the recipient. - uint256 internal recipientReentrantStreamId; - // A stream with a reverting contract as the stream's recipient. - uint256 internal recipientRevertStreamId; - struct CreateParams { Lockup.CreateWithTimestamps createWithTimestamps; Lockup.CreateWithDurations createWithDurations; @@ -110,14 +95,15 @@ abstract contract Integration_Test is Base_Test { //////////////////////////////////////////////////////////////////////////*/ function initializeDefaultStreams() internal { - defaultStreamId = createDefaultStream(); - notAllowedtoHookStreamId = createDefaultStreamWithRecipient(address(recipientInterfaceIDIncorrect)); - notCancelableStreamId = createDefaultStreamNonCancelable(); - notTransferableStreamId = createDefaultStreamNonTransferable(); - recipientGoodStreamId = createDefaultStreamWithRecipient(address(recipientGood)); - recipientInvalidSelectorStreamId = createDefaultStreamWithRecipient(address(recipientInvalidSelector)); - recipientReentrantStreamId = createDefaultStreamWithRecipient(address(recipientReentrant)); - recipientRevertStreamId = createDefaultStreamWithRecipient(address(recipientReverting)); + ids.defaultStream = createDefaultStream(); + ids.notAllowedToHookStream = createDefaultStreamWithRecipient(address(recipientInterfaceIDIncorrect)); + ids.notCancelableStream = createDefaultStreamNonCancelable(); + ids.notTransferableStream = createDefaultStreamNonTransferable(); + ids.nullStream = 1729; + ids.recipientGoodStream = createDefaultStreamWithRecipient(address(recipientGood)); + ids.recipientInvalidSelectorStream = createDefaultStreamWithRecipient(address(recipientInvalidSelector)); + ids.recipientReentrantStream = createDefaultStreamWithRecipient(address(recipientReentrant)); + ids.recipientRevertStream = createDefaultStreamWithRecipient(address(recipientReverting)); } function initializeRecipientsWithHooks() internal { @@ -132,13 +118,16 @@ abstract contract Integration_Test is Base_Test { vm.label({ account: address(recipientReentrant), newLabel: "Recipient Reentrant" }); vm.label({ account: address(recipientReverting), newLabel: "Recipient Reverting" }); + // Deal some ETH to the `recipientReentrant` because its used in reentrant tests. + vm.deal({ account: address(recipientReentrant), newBalance: 100 ether }); + // Allow the selected recipients to hook. - resetPrank({ msgSender: users.admin }); + setMsgSender(address(comptroller)); lockup.allowToHook(address(recipientGood)); lockup.allowToHook(address(recipientInvalidSelector)); lockup.allowToHook(address(recipientReentrant)); lockup.allowToHook(address(recipientReverting)); - resetPrank({ msgSender: users.sender }); + setMsgSender(users.sender); } /*////////////////////////////////////////////////////////////////////////// @@ -217,25 +206,25 @@ abstract contract Integration_Test is Base_Test { //////////////////////////////////////////////////////////////////////////*/ function expectRevert_CallerMaliciousThirdParty(bytes memory callData) internal { - resetPrank({ msgSender: users.eve }); + setMsgSender(users.eve); (bool success, bytes memory returnData) = address(lockup).call(callData); assertFalse(success, "malicious call success"); assertEq( returnData, - abi.encodeWithSelector(Errors.SablierLockupBase_Unauthorized.selector, defaultStreamId, users.eve), + abi.encodeWithSelector(Errors.SablierLockup_Unauthorized.selector, ids.defaultStream, users.eve), "malicious call return data" ); } function expectRevert_CANCELEDStatus(bytes memory callData) internal { vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); - lockup.cancel(defaultStreamId); + lockup.cancel(ids.defaultStream); (bool success, bytes memory returnData) = address(lockup).call(callData); assertFalse(success, "canceled status call success"); assertEq( returnData, - abi.encodeWithSelector(Errors.SablierLockupBase_StreamCanceled.selector, defaultStreamId), + abi.encodeWithSelector(Errors.SablierLockup_StreamCanceled.selector, ids.defaultStream), "canceled status call return data" ); } @@ -243,18 +232,18 @@ abstract contract Integration_Test is Base_Test { function expectRevert_DelegateCall(bytes memory callData) internal { (bool success, bytes memory returnData) = address(lockup).delegatecall(callData); assertFalse(success, "delegatecall success"); - assertEq(returnData, abi.encodeWithSelector(Errors.DelegateCall.selector), "delegatecall return data"); + assertEq(returnData, abi.encodeWithSelector(EvmUtilsErrors.DelegateCall.selector), "delegatecall return data"); } function expectRevert_DEPLETEDStatus(bytes memory callData) internal { vm.warp({ newTimestamp: defaults.END_TIME() }); - lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); + lockup.withdrawMax{ value: LOCKUP_MIN_FEE_WEI }({ streamId: ids.defaultStream, to: users.recipient }); (bool success, bytes memory returnData) = address(lockup).call(callData); assertFalse(success, "depleted status call success"); assertEq( returnData, - abi.encodeWithSelector(Errors.SablierLockupBase_StreamDepleted.selector, defaultStreamId), + abi.encodeWithSelector(Errors.SablierLockup_StreamDepleted.selector, ids.defaultStream), "depleted status call return data" ); } @@ -264,7 +253,7 @@ abstract contract Integration_Test is Base_Test { assertFalse(success, "null call success"); assertEq( returnData, - abi.encodeWithSelector(Errors.SablierLockupBase_Null.selector, nullStreamId), + abi.encodeWithSelector(Errors.SablierLockupState_Null.selector, ids.nullStream), "null call return data" ); } @@ -276,7 +265,7 @@ abstract contract Integration_Test is Base_Test { assertFalse(success, "settled status call success"); assertEq( returnData, - abi.encodeWithSelector(Errors.SablierLockupBase_StreamSettled.selector, defaultStreamId), + abi.encodeWithSelector(Errors.SablierLockup_StreamSettled.selector, ids.defaultStream), "settled status call return data" ); } diff --git a/tests/integration/concrete/batch-lockup/create-with-durations-ld/createWithDurationsLD.t.sol b/tests/integration/concrete/batch-lockup/create-with-durations-ld/createWithDurationsLD.t.sol index 0da7fc786..96b0b1dcb 100644 --- a/tests/integration/concrete/batch-lockup/create-with-durations-ld/createWithDurationsLD.t.sol +++ b/tests/integration/concrete/batch-lockup/create-with-durations-ld/createWithDurationsLD.t.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; +import { ISablierBatchLockup } from "src/interfaces/ISablierBatchLockup.sol"; import { Errors } from "src/libraries/Errors.sol"; -import { BatchLockup } from "src/types/DataTypes.sol"; +import { BatchLockup } from "src/types/BatchLockup.sol"; import { Integration_Test } from "../../../Integration.t.sol"; @@ -14,6 +15,8 @@ contract CreateWithDurationsLD_Integration_Test is Integration_Test { } function test_WhenBatchSizeNotZero() external { + uint256[] memory expectedStreamIds = defaults.incrementalStreamIds({ firstStreamId: lockup.nextStreamId() }); + // Token flow: Sender → batchLockup → SablierLockup // Expect transfers from Alice to the batchLockup, and then from the batchLockup to the Lockup contract. expectCallToTransferFrom({ @@ -24,7 +27,7 @@ contract CreateWithDurationsLD_Integration_Test is Integration_Test { expectMultipleCallsToCreateWithDurationsLD({ count: defaults.BATCH_SIZE(), - params: defaults.createWithDurationsBrokerNull(), + params: defaults.createWithDurations(), segmentsWithDuration: defaults.segmentsWithDurations() }); expectMultipleCallsToTransferFrom({ @@ -34,12 +37,13 @@ contract CreateWithDurationsLD_Integration_Test is Integration_Test { value: defaults.DEPOSIT_AMOUNT() }); - uint256 firstStreamId = lockup.nextStreamId(); + // It should emit a {CreateLockupBatch} event. + vm.expectEmit({ emitter: address(batchLockup) }); + emit ISablierBatchLockup.CreateLockupBatch({ funder: users.sender, lockup: lockup, streamIds: expectedStreamIds }); // Assert that the batch of streams has been created successfully. uint256[] memory actualStreamIds = batchLockup.createWithDurationsLD(lockup, dai, defaults.batchCreateWithDurationsLD()); - uint256[] memory expectedStreamIds = defaults.incrementalStreamIds({ firstStreamId: firstStreamId }); assertEq(actualStreamIds, expectedStreamIds, "stream ids mismatch"); } } diff --git a/tests/integration/concrete/batch-lockup/create-with-durations-ld/createWithDurationsLD.tree b/tests/integration/concrete/batch-lockup/create-with-durations-ld/createWithDurationsLD.tree index a95c1f97a..dfb526157 100644 --- a/tests/integration/concrete/batch-lockup/create-with-durations-ld/createWithDurationsLD.tree +++ b/tests/integration/concrete/batch-lockup/create-with-durations-ld/createWithDurationsLD.tree @@ -3,4 +3,5 @@ CreateWithDurationsLD_Integration_Test │ └── it should revert └── when batch size not zero ├── it should create a batch of streams with durations - └── it should perform the ERC-20 transfers + ├── it should perform the ERC-20 transfers + └── it should emit a {CreateLockupBatch} event diff --git a/tests/integration/concrete/batch-lockup/create-with-durations-ll/createWithDurationsLL.t.sol b/tests/integration/concrete/batch-lockup/create-with-durations-ll/createWithDurationsLL.t.sol index 93dd78e7f..41c60150a 100644 --- a/tests/integration/concrete/batch-lockup/create-with-durations-ll/createWithDurationsLL.t.sol +++ b/tests/integration/concrete/batch-lockup/create-with-durations-ll/createWithDurationsLL.t.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; +import { ISablierBatchLockup } from "src/interfaces/ISablierBatchLockup.sol"; import { Errors } from "src/libraries/Errors.sol"; -import { BatchLockup } from "src/types/DataTypes.sol"; +import { BatchLockup } from "src/types/BatchLockup.sol"; import { Integration_Test } from "../../../Integration.t.sol"; @@ -14,6 +15,8 @@ contract CreateWithDurationsLL_Integration_Test is Integration_Test { } function test_WhenBatchSizeNotZero() external { + uint256[] memory expectedStreamIds = defaults.incrementalStreamIds({ firstStreamId: lockup.nextStreamId() }); + // Token flow: Sender → batchLockup → SablierLockup // Expect transfers from Alice to the batchLockup, and then from the batchLockup to the Lockup contract. expectCallToTransferFrom({ @@ -24,7 +27,7 @@ contract CreateWithDurationsLL_Integration_Test is Integration_Test { expectMultipleCallsToCreateWithDurationsLL({ count: defaults.BATCH_SIZE(), - params: defaults.createWithDurationsBrokerNull(), + params: defaults.createWithDurations(), unlockAmounts: defaults.unlockAmounts(), durations: defaults.durations() }); @@ -35,12 +38,13 @@ contract CreateWithDurationsLL_Integration_Test is Integration_Test { value: defaults.DEPOSIT_AMOUNT() }); - uint256 firstStreamId = lockup.nextStreamId(); + // It should emit a {CreateLockupBatch} event. + vm.expectEmit({ emitter: address(batchLockup) }); + emit ISablierBatchLockup.CreateLockupBatch({ funder: users.sender, lockup: lockup, streamIds: expectedStreamIds }); // Assert that the batch of streams has been created successfully. uint256[] memory actualStreamIds = batchLockup.createWithDurationsLL(lockup, dai, defaults.batchCreateWithDurationsLL()); - uint256[] memory expectedStreamIds = defaults.incrementalStreamIds({ firstStreamId: firstStreamId }); assertEq(actualStreamIds, expectedStreamIds, "stream ids mismatch"); } } diff --git a/tests/integration/concrete/batch-lockup/create-with-durations-ll/createWithDurationsLL.tree b/tests/integration/concrete/batch-lockup/create-with-durations-ll/createWithDurationsLL.tree index a7b721652..aabb3a5b4 100644 --- a/tests/integration/concrete/batch-lockup/create-with-durations-ll/createWithDurationsLL.tree +++ b/tests/integration/concrete/batch-lockup/create-with-durations-ll/createWithDurationsLL.tree @@ -3,4 +3,5 @@ CreateWithDurationsLL_Integration_Test │ └── it should revert └── when batch size not zero ├── it should create a batch of streams with durations - └── it should perform the ERC-20 transfers + ├── it should perform the ERC-20 transfers + └── it should emit a {CreateLockupBatch} event diff --git a/tests/integration/concrete/batch-lockup/create-with-durations-lt/createWithDurationsLT.t.sol b/tests/integration/concrete/batch-lockup/create-with-durations-lt/createWithDurationsLT.t.sol index 5af1c72a0..69f23f7fb 100644 --- a/tests/integration/concrete/batch-lockup/create-with-durations-lt/createWithDurationsLT.t.sol +++ b/tests/integration/concrete/batch-lockup/create-with-durations-lt/createWithDurationsLT.t.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; +import { ISablierBatchLockup } from "src/interfaces/ISablierBatchLockup.sol"; import { Errors } from "src/libraries/Errors.sol"; -import { BatchLockup } from "src/types/DataTypes.sol"; +import { BatchLockup } from "src/types/BatchLockup.sol"; import { Integration_Test } from "../../../Integration.t.sol"; @@ -14,6 +15,8 @@ contract CreateWithDurationsLT_Integration_Test is Integration_Test { } function test_WhenBatchSizeNotZero() external { + uint256[] memory expectedStreamIds = defaults.incrementalStreamIds({ firstStreamId: lockup.nextStreamId() }); + // Token flow: Sender → batchLockup → SablierLockup // Expect transfers from Alice to the batchLockup, and then from the batchLockup to the Lockup contract. expectCallToTransferFrom({ @@ -24,7 +27,7 @@ contract CreateWithDurationsLT_Integration_Test is Integration_Test { expectMultipleCallsToCreateWithDurationsLT({ count: defaults.BATCH_SIZE(), - params: defaults.createWithDurationsBrokerNull(), + params: defaults.createWithDurations(), tranches: defaults.tranchesWithDurations() }); expectMultipleCallsToTransferFrom({ @@ -34,12 +37,13 @@ contract CreateWithDurationsLT_Integration_Test is Integration_Test { value: defaults.DEPOSIT_AMOUNT() }); - uint256 firstStreamId = lockup.nextStreamId(); + // It should emit a {CreateLockupBatch} event. + vm.expectEmit({ emitter: address(batchLockup) }); + emit ISablierBatchLockup.CreateLockupBatch({ funder: users.sender, lockup: lockup, streamIds: expectedStreamIds }); // Assert that the batch of streams has been created successfully. uint256[] memory actualStreamIds = batchLockup.createWithDurationsLT(lockup, dai, defaults.batchCreateWithDurationsLT()); - uint256[] memory expectedStreamIds = defaults.incrementalStreamIds({ firstStreamId: firstStreamId }); assertEq(actualStreamIds, expectedStreamIds, "stream ids mismatch"); } } diff --git a/tests/integration/concrete/batch-lockup/create-with-durations-lt/createWithDurationsLT.tree b/tests/integration/concrete/batch-lockup/create-with-durations-lt/createWithDurationsLT.tree index 38d919121..a6f81c935 100644 --- a/tests/integration/concrete/batch-lockup/create-with-durations-lt/createWithDurationsLT.tree +++ b/tests/integration/concrete/batch-lockup/create-with-durations-lt/createWithDurationsLT.tree @@ -3,4 +3,5 @@ CreateWithDurationsLT_Integration_Test │ └── it should revert └── when batch size not zero ├── it should create a batch of streams with durations - └── it should perform the ERC-20 transfers + ├── it should perform the ERC-20 transfers + └── it should emit a {CreateLockupBatch} event diff --git a/tests/integration/concrete/batch-lockup/create-with-timestamps-ld/createWithTimestampsLD.t.sol b/tests/integration/concrete/batch-lockup/create-with-timestamps-ld/createWithTimestampsLD.t.sol index a03d0c5b7..e97461f31 100644 --- a/tests/integration/concrete/batch-lockup/create-with-timestamps-ld/createWithTimestampsLD.t.sol +++ b/tests/integration/concrete/batch-lockup/create-with-timestamps-ld/createWithTimestampsLD.t.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; +import { ISablierBatchLockup } from "src/interfaces/ISablierBatchLockup.sol"; import { Errors } from "src/libraries/Errors.sol"; -import { BatchLockup } from "src/types/DataTypes.sol"; +import { BatchLockup } from "src/types/BatchLockup.sol"; import { Integration_Test } from "../../../Integration.t.sol"; @@ -14,6 +15,8 @@ contract CreateWithTimestampsLD_Integration_Test is Integration_Test { } function test_WhenBatchSizeNotZero() external { + uint256[] memory expectedStreamIds = defaults.incrementalStreamIds({ firstStreamId: lockup.nextStreamId() }); + // Token flow: Sender → batchLockup → SablierLockup // Expect transfers from Alice to the batchLockup, and then from the batchLockup to the Lockup contract. expectCallToTransferFrom({ @@ -24,7 +27,7 @@ contract CreateWithTimestampsLD_Integration_Test is Integration_Test { expectMultipleCallsToCreateWithTimestampsLD({ count: defaults.BATCH_SIZE(), - params: defaults.createWithTimestampsBrokerNull(), + params: defaults.createWithTimestamps(), segments: defaults.segments() }); expectMultipleCallsToTransferFrom({ @@ -34,12 +37,13 @@ contract CreateWithTimestampsLD_Integration_Test is Integration_Test { value: defaults.DEPOSIT_AMOUNT() }); - uint256 firstStreamId = lockup.nextStreamId(); + // It should emit a {CreateLockupBatch} event. + vm.expectEmit({ emitter: address(batchLockup) }); + emit ISablierBatchLockup.CreateLockupBatch({ funder: users.sender, lockup: lockup, streamIds: expectedStreamIds }); // Assert that the batch of streams has been created successfully. uint256[] memory actualStreamIds = batchLockup.createWithTimestampsLD(lockup, dai, defaults.batchCreateWithTimestampsLD()); - uint256[] memory expectedStreamIds = defaults.incrementalStreamIds({ firstStreamId: firstStreamId }); assertEq(actualStreamIds, expectedStreamIds, "stream ids mismatch"); } } diff --git a/tests/integration/concrete/batch-lockup/create-with-timestamps-ld/createWithTimestampsLD.tree b/tests/integration/concrete/batch-lockup/create-with-timestamps-ld/createWithTimestampsLD.tree index c5a0fb3bd..acfa65ef8 100644 --- a/tests/integration/concrete/batch-lockup/create-with-timestamps-ld/createWithTimestampsLD.tree +++ b/tests/integration/concrete/batch-lockup/create-with-timestamps-ld/createWithTimestampsLD.tree @@ -3,4 +3,5 @@ CreateWithTimestampsLD_Integration_Test │ └── it should revert └── when batch size not zero ├── it should create a batch of streams with timestamps - └── it should perform the ERC-20 transfers + ├── it should perform the ERC-20 transfers + └── it should emit a {CreateLockupBatch} event diff --git a/tests/integration/concrete/batch-lockup/create-with-timestamps-ll/createWithTimestamps.t.sol b/tests/integration/concrete/batch-lockup/create-with-timestamps-ll/createWithTimestamps.t.sol index 05ef622cf..3a4658c27 100644 --- a/tests/integration/concrete/batch-lockup/create-with-timestamps-ll/createWithTimestamps.t.sol +++ b/tests/integration/concrete/batch-lockup/create-with-timestamps-ll/createWithTimestamps.t.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; +import { ISablierBatchLockup } from "src/interfaces/ISablierBatchLockup.sol"; import { Errors } from "src/libraries/Errors.sol"; -import { BatchLockup } from "src/types/DataTypes.sol"; +import { BatchLockup } from "src/types/BatchLockup.sol"; import { Integration_Test } from "../../../Integration.t.sol"; @@ -14,6 +15,8 @@ contract CreateWithTimestampsLL_Integration_Test is Integration_Test { } function test_WhenBatchSizeNotZero() external { + uint256[] memory expectedStreamIds = defaults.incrementalStreamIds({ firstStreamId: lockup.nextStreamId() }); + // Token flow: Sender → batchLockup → SablierLockup // Expect transfers from Alice to the batchLockup, and then from the batchLockup to the Lockup contract. expectCallToTransferFrom({ @@ -24,7 +27,7 @@ contract CreateWithTimestampsLL_Integration_Test is Integration_Test { expectMultipleCallsToCreateWithTimestampsLL({ count: defaults.BATCH_SIZE(), - params: defaults.createWithTimestampsBrokerNull(), + params: defaults.createWithTimestamps(), unlockAmounts: defaults.unlockAmounts(), cliffTime: defaults.CLIFF_TIME() }); @@ -35,12 +38,13 @@ contract CreateWithTimestampsLL_Integration_Test is Integration_Test { value: defaults.DEPOSIT_AMOUNT() }); - uint256 firstStreamId = lockup.nextStreamId(); + // It should emit a {CreateLockupBatch} event. + vm.expectEmit({ emitter: address(batchLockup) }); + emit ISablierBatchLockup.CreateLockupBatch({ funder: users.sender, lockup: lockup, streamIds: expectedStreamIds }); // Assert that the batch of streams has been created successfully. uint256[] memory actualStreamIds = batchLockup.createWithTimestampsLL(lockup, dai, defaults.batchCreateWithTimestampsLL()); - uint256[] memory expectedStreamIds = defaults.incrementalStreamIds({ firstStreamId: firstStreamId }); assertEq(actualStreamIds, expectedStreamIds, "stream ids mismatch"); } } diff --git a/tests/integration/concrete/batch-lockup/create-with-timestamps-ll/createWithTimestamps.tree b/tests/integration/concrete/batch-lockup/create-with-timestamps-ll/createWithTimestamps.tree index 436a0f62b..dd6446865 100644 --- a/tests/integration/concrete/batch-lockup/create-with-timestamps-ll/createWithTimestamps.tree +++ b/tests/integration/concrete/batch-lockup/create-with-timestamps-ll/createWithTimestamps.tree @@ -3,4 +3,5 @@ CreateWithTimestampsLL_Integration_Test │ └── it should revert └── when batch size not zero ├── it should create a batch of streams with timestamps - └── it should perform the ERC-20 transfers + ├── it should perform the ERC-20 transfers + └── it should emit a {CreateLockupBatch} event diff --git a/tests/integration/concrete/batch-lockup/create-with-timestamps-lt/createWithTimestampsLT.t.sol b/tests/integration/concrete/batch-lockup/create-with-timestamps-lt/createWithTimestampsLT.t.sol index 8cf557884..3d91168f8 100644 --- a/tests/integration/concrete/batch-lockup/create-with-timestamps-lt/createWithTimestampsLT.t.sol +++ b/tests/integration/concrete/batch-lockup/create-with-timestamps-lt/createWithTimestampsLT.t.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; +import { ISablierBatchLockup } from "src/interfaces/ISablierBatchLockup.sol"; import { Errors } from "src/libraries/Errors.sol"; -import { BatchLockup } from "src/types/DataTypes.sol"; +import { BatchLockup } from "src/types/BatchLockup.sol"; import { Integration_Test } from "../../../Integration.t.sol"; @@ -14,6 +15,8 @@ contract CreateWithTimestampsLT_Integration_Test is Integration_Test { } function test_WhenBatchSizeNotZero() external { + uint256[] memory expectedStreamIds = defaults.incrementalStreamIds({ firstStreamId: lockup.nextStreamId() }); + // Token flow: Sender → batchLockup → SablierLockup // Expect transfers from Alice to the batchLockup, and then from the batchLockup to the Lockup contract. expectCallToTransferFrom({ @@ -24,7 +27,7 @@ contract CreateWithTimestampsLT_Integration_Test is Integration_Test { expectMultipleCallsToCreateWithTimestampsLT({ count: defaults.BATCH_SIZE(), - params: defaults.createWithTimestampsBrokerNull(), + params: defaults.createWithTimestamps(), tranches: defaults.tranches() }); expectMultipleCallsToTransferFrom({ @@ -34,12 +37,13 @@ contract CreateWithTimestampsLT_Integration_Test is Integration_Test { value: defaults.DEPOSIT_AMOUNT() }); - uint256 firstStreamId = lockup.nextStreamId(); + // It should emit a {CreateLockupBatch} event. + vm.expectEmit({ emitter: address(batchLockup) }); + emit ISablierBatchLockup.CreateLockupBatch({ funder: users.sender, lockup: lockup, streamIds: expectedStreamIds }); // Assert that the batch of streams has been created successfully. uint256[] memory actualStreamIds = batchLockup.createWithTimestampsLT(lockup, dai, defaults.batchCreateWithTimestampsLT()); - uint256[] memory expectedStreamIds = defaults.incrementalStreamIds({ firstStreamId: firstStreamId }); assertEq(actualStreamIds, expectedStreamIds, "stream ids mismatch"); } } diff --git a/tests/integration/concrete/batch-lockup/create-with-timestamps-lt/createWithTimestampsLT.tree b/tests/integration/concrete/batch-lockup/create-with-timestamps-lt/createWithTimestampsLT.tree index a22565816..7e4cf773f 100644 --- a/tests/integration/concrete/batch-lockup/create-with-timestamps-lt/createWithTimestampsLT.tree +++ b/tests/integration/concrete/batch-lockup/create-with-timestamps-lt/createWithTimestampsLT.tree @@ -3,4 +3,5 @@ CreateWithTimestampsLT_Integration_Test │ └── it should revert └── when batch size not zero ├── it should create a batch of streams with timestamps - └── it should perform the ERC-20 transfers + ├── it should perform the ERC-20 transfers + └── it should emit a {CreateLockupBatch} event diff --git a/tests/integration/concrete/batch/batch.t.sol b/tests/integration/concrete/batch/batch.t.sol index e6d808a14..a95e64e0a 100644 --- a/tests/integration/concrete/batch/batch.t.sol +++ b/tests/integration/concrete/batch/batch.t.sol @@ -14,12 +14,12 @@ contract Batch_Integration_Concrete_Test is Integration_Test { /// @dev The batch call cancels a non-cancelable stream. function test_RevertWhen_LockupThrows() external { bytes[] memory calls = new bytes[](2); - calls[0] = abi.encodeCall(lockup.cancel, (defaultStreamId)); - calls[1] = abi.encodeCall(lockup.cancel, (notCancelableStreamId)); + calls[0] = abi.encodeCall(lockup.cancel, (ids.defaultStream)); + calls[1] = abi.encodeCall(lockup.cancel, (ids.notCancelableStream)); - // Expect revert on notCancelableStreamId. + // Expect revert on ids.notCancelableStream. vm.expectRevert( - abi.encodeWithSelector(Errors.SablierLockupBase_StreamNotCancelable.selector, notCancelableStreamId) + abi.encodeWithSelector(Errors.SablierLockup_StreamNotCancelable.selector, ids.notCancelableStream) ); lockup.batch(calls); } @@ -32,31 +32,32 @@ contract Batch_Integration_Concrete_Test is Integration_Test { uint256 expectedNextStreamId = lockup.nextStreamId(); vm.warp(defaults.WARP_26_PERCENT()); - bytes[] memory calls = new bytes[](6); + bytes[] memory calls = new bytes[](5); // It should return True. - calls[0] = abi.encodeCall(lockup.isCancelable, (defaultStreamId)); - // It should return the withdrawn amount. - calls[1] = abi.encodeCall(lockup.withdrawMax, (notCancelableStreamId, users.recipient)); - // It should return nothing. - calls[2] = abi.encodeCall(lockup.cancel, (defaultStreamId)); + calls[0] = abi.encodeCall(lockup.isCancelable, (ids.defaultStream)); + // It should return the refunded amount. + calls[1] = abi.encodeCall(lockup.cancel, (ids.defaultStream)); // It should return the next stream ID. - calls[3] = abi.encodeCall(lockup.nextStreamId, ()); + calls[2] = abi.encodeCall(lockup.nextStreamId, ()); // It should return the stream ID created. - calls[4] = abi.encodeCall( + calls[3] = abi.encodeCall( lockup.createWithTimestampsLL, (defaults.createWithTimestamps(), defaults.unlockAmounts(), defaults.CLIFF_TIME()) ); // It should return nothing. - calls[5] = abi.encodeCall(lockup.renounce, (notTransferableStreamId)); + calls[4] = abi.encodeCall(lockup.renounce, (ids.notTransferableStream)); bytes[] memory results = lockup.batch(calls); - assertEq(results.length, 6, "batch results length"); - assertTrue(abi.decode(results[0], (bool)), "batch results[0]"); - assertEq(abi.decode(results[1], (uint128)), defaults.WITHDRAW_AMOUNT(), "batch results[1]"); - assertEq(results[2], "", "batch results[2]"); - assertEq(abi.decode(results[3], (uint256)), expectedNextStreamId, "batch results[3]"); - assertEq(abi.decode(results[4], (uint256)), expectedNextStreamId, "batch results[4]"); - assertEq(results[5], "", "batch results[5]"); + assertEq(results.length, 5, "batch results length"); + assertTrue(abi.decode(results[0], (bool)), "batch results[0]: isCancelable"); + assertEq( + abi.decode(results[1], (uint128)), + defaults.DEPOSIT_AMOUNT() - defaults.WITHDRAW_AMOUNT(), + "batch results[2]: cancel" + ); + assertEq(abi.decode(results[2], (uint256)), expectedNextStreamId, "batch results[3]: nextStreamId"); + assertEq(abi.decode(results[3], (uint256)), expectedNextStreamId, "batch results[4]: createWithTimestampsLL"); + assertEq(results[4], "", "batch results[5]: renounce"); } /// @dev The batch call includes: @@ -85,15 +86,21 @@ contract Batch_Integration_Concrete_Test is Integration_Test { calls[5] = abi.encodeCall(lockup.createWithTimestampsLT, (defaults.createWithTimestamps(), defaults.tranches())); // It should return the stream IDs created. - bytes[] memory results = lockup.batch{ value: 1 wei }(calls); + bytes[] memory results = lockup.batch{ value: LOCKUP_MIN_FEE_WEI }(calls); assertEq(results.length, 6, "batch results length"); - assertEq(abi.decode(results[0], (uint256)), expectedNextStreamId, "batch results[0]"); - assertEq(abi.decode(results[1], (uint256)), expectedNextStreamId + 1, "batch results[1]"); - assertEq(abi.decode(results[2], (uint256)), expectedNextStreamId + 2, "batch results[2]"); - assertEq(abi.decode(results[3], (uint256)), expectedNextStreamId + 3, "batch results[3]"); - assertEq(abi.decode(results[4], (uint256)), expectedNextStreamId + 4, "batch results[4]"); - assertEq(abi.decode(results[5], (uint256)), expectedNextStreamId + 5, "batch results[5]"); - assertEq(address(lockup).balance, initialEthBalance + 1 wei, "lockup contract balance"); + assertEq(abi.decode(results[0], (uint256)), expectedNextStreamId, "batch results[0]: createWithDurationsLD"); + assertEq(abi.decode(results[1], (uint256)), expectedNextStreamId + 1, "batch results[1]: createWithDurationsLL"); + assertEq(abi.decode(results[2], (uint256)), expectedNextStreamId + 2, "batch results[2]: createWithDurationsLT"); + assertEq( + abi.decode(results[3], (uint256)), expectedNextStreamId + 3, "batch results[3]: createWithTimestampsLD" + ); + assertEq( + abi.decode(results[4], (uint256)), expectedNextStreamId + 4, "batch results[4]: createWithTimestampsLL" + ); + assertEq( + abi.decode(results[5], (uint256)), expectedNextStreamId + 5, "batch results[5]: createWithTimestampsLT" + ); + assertEq(address(lockup).balance, initialEthBalance + LOCKUP_MIN_FEE_WEI, "lockup contract balance"); } /// @dev The batch call includes: @@ -103,28 +110,30 @@ contract Batch_Integration_Concrete_Test is Integration_Test { uint256 initialEthBalance = address(lockup).balance; vm.warp(defaults.WARP_26_PERCENT()); - bytes[] memory calls = new bytes[](4); - calls[0] = abi.encodeCall(lockup.cancel, (defaultStreamId)); + bytes[] memory calls = new bytes[](3); + + // It should return the refunded amount. + calls[0] = abi.encodeCall(lockup.cancel, (ids.defaultStream)); uint256[] memory streamIds = new uint256[](2); - streamIds[0] = recipientGoodStreamId; - streamIds[1] = notTransferableStreamId; + streamIds[0] = ids.recipientGoodStream; + streamIds[1] = ids.notTransferableStream; + // It should return the array of refunded amounts. calls[1] = abi.encodeCall(lockup.cancelMultiple, (streamIds)); - calls[2] = abi.encodeCall(lockup.renounce, (recipientReentrantStreamId)); - - streamIds = new uint256[](1); - streamIds[0] = recipientRevertStreamId; - calls[3] = abi.encodeCall(lockup.renounceMultiple, (streamIds)); - - bytes[] memory results = lockup.batch{ value: 1 wei }(calls); - - assertEq(results.length, 4, "batch results length"); - assertEq(results[0], "", "batch results[0]"); - assertEq(results[1], "", "batch results[1]"); - assertEq(results[2], "", "batch results[2]"); - assertEq(results[3], "", "batch results[3]"); - assertEq(address(lockup).balance, initialEthBalance + 1 wei, "lockup contract balance"); + // It should return nothing. + calls[2] = abi.encodeCall(lockup.renounce, (ids.recipientReentrantStream)); + + bytes[] memory results = lockup.batch{ value: LOCKUP_MIN_FEE_WEI }(calls); + + uint128 expectedRefundedAmount = defaults.REFUND_AMOUNT(); + assertEq(results.length, 3, "batch results length"); + assertEq(abi.decode(results[0], (uint128)), expectedRefundedAmount, "batch results[0]: cancel"); + uint128[] memory refundedAmounts = abi.decode(results[1], (uint128[])); + assertEq(refundedAmounts[0], expectedRefundedAmount, "batch results[1][0]: cancelMultiple"); + assertEq(refundedAmounts[1], expectedRefundedAmount, "batch results[1][1]: cancelMultiple"); + assertEq(results[2], "", "batch results[2]: renounce"); + assertEq(address(lockup).balance, initialEthBalance + LOCKUP_MIN_FEE_WEI, "lockup contract balance"); } /// @dev The batch call includes: @@ -137,29 +146,31 @@ contract Batch_Integration_Concrete_Test is Integration_Test { bytes[] memory calls = new bytes[](5); // It should return nothing. - calls[0] = abi.encodeCall(lockup.withdraw, (defaultStreamId, users.recipient, 1)); + calls[0] = abi.encodeCall(lockup.withdraw, (ids.defaultStream, users.recipient, 1)); // It should return the withdrawn amount. - calls[1] = abi.encodeCall(lockup.withdrawMax, (defaultStreamId, users.recipient)); + calls[1] = abi.encodeCall(lockup.withdrawMax, (ids.defaultStream, users.recipient)); - uint256[] memory streamIds = Solarray.uint256s(notCancelableStreamId, notCancelableStreamId); + uint256[] memory streamIds = Solarray.uint256s(ids.notCancelableStream, ids.notCancelableStream); uint128[] memory amounts = Solarray.uint128s(1, 1); // It should return nothing. calls[2] = abi.encodeCall(lockup.withdrawMultiple, (streamIds, amounts)); // It should return the withdrawn amount. - calls[3] = abi.encodeCall(lockup.withdrawMaxAndTransfer, (notCancelableStreamId, users.recipient)); + calls[3] = abi.encodeCall(lockup.withdrawMaxAndTransfer, (ids.notCancelableStream, users.recipient)); // It should return nothing. - calls[4] = abi.encodeCall(lockup.burn, (defaultStreamId)); + calls[4] = abi.encodeCall(lockup.burn, (ids.defaultStream)); - resetPrank({ msgSender: users.recipient }); - bytes[] memory results = lockup.batch{ value: 1 wei }(calls); + setMsgSender(users.recipient); + bytes[] memory results = lockup.batch{ value: LOCKUP_MIN_FEE_WEI }(calls); assertEq(results.length, 5, "batch results length"); - assertEq(results[0], "", "batch results[0]"); - assertEq(abi.decode(results[1], (uint128)), defaults.DEPOSIT_AMOUNT() - 1, "batch results[1]"); - assertEq(results[2], "", "batch results[2]"); - assertEq(abi.decode(results[3], (uint128)), defaults.DEPOSIT_AMOUNT() - 2, "batch results[3]"); - assertEq(results[4], "", "batch results[4]"); - assertEq(address(lockup).balance, initialEthBalance + 1 wei, "lockup contract balance"); + assertEq(results[0], "", "batch results[0]: withdraw"); + assertEq(abi.decode(results[1], (uint128)), defaults.DEPOSIT_AMOUNT() - 1, "batch results[1]: withdrawMax"); + assertEq(results[2], "", "batch results[2]: withdrawMultiple"); + assertEq( + abi.decode(results[3], (uint128)), defaults.DEPOSIT_AMOUNT() - 2, "batch results[3]: withdrawMaxAndTransfer" + ); + assertEq(results[4], "", "batch results[4]: burn"); + assertEq(address(lockup).balance, initialEthBalance + LOCKUP_MIN_FEE_WEI, "lockup contract balance"); } } diff --git a/tests/integration/concrete/constructor.t.sol b/tests/integration/concrete/constructor.t.sol index 34f97b8b1..a63028548 100644 --- a/tests/integration/concrete/constructor.t.sol +++ b/tests/integration/concrete/constructor.t.sol @@ -1,9 +1,8 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { UD60x18 } from "@prb/math/src/UD60x18.sol"; - -import { IAdminable } from "src/interfaces/IAdminable.sol"; +import { IComptrollerable } from "@sablier/evm-utils/src/interfaces/IComptrollerable.sol"; +import { ISablierComptroller } from "@sablier/evm-utils/src/interfaces/ISablierComptroller.sol"; import { SablierLockup } from "src/SablierLockup.sol"; import { Integration_Test } from "../Integration.t.sol"; @@ -12,41 +11,33 @@ contract Constructor_Integration_Concrete_Test is Integration_Test { function test_Constructor() external { // Expect the relevant event to be emitted. vm.expectEmit(); - emit IAdminable.TransferAdmin({ oldAdmin: address(0), newAdmin: users.admin }); + emit IComptrollerable.SetComptroller({ + newComptroller: comptroller, + oldComptroller: ISablierComptroller(address(0)) + }); // Construct the contract. SablierLockup constructedLockup = new SablierLockup({ - initialAdmin: users.admin, - initialNFTDescriptor: nftDescriptor, - maxCount: defaults.MAX_COUNT() + initialComptroller: address(comptroller), + initialNFTDescriptor: address(nftDescriptor) }); - // {SablierLockupBase.constant} - UD60x18 actualMaxBrokerFee = constructedLockup.MAX_BROKER_FEE(); - UD60x18 expectedMaxBrokerFee = UD60x18.wrap(0.1e18); - assertEq(actualMaxBrokerFee, expectedMaxBrokerFee, "MAX_BROKER_FEE"); + // {Comptrollerable.constructor} + address actualComptroller = address(constructedLockup.comptroller()); + address expectedComptroller = address(comptroller); + assertEq(actualComptroller, expectedComptroller, "comptroller"); - // {Adminable.constructor} - address actualAdmin = constructedLockup.admin(); - address expectedAdmin = users.admin; - assertEq(actualAdmin, expectedAdmin, "admin"); - - // {SablierLockupBase.constructor} + // {SablierLockupState.constructor} uint256 actualStreamId = constructedLockup.nextStreamId(); uint256 expectedStreamId = 1; assertEq(actualStreamId, expectedStreamId, "nextStreamId"); - // {SablierLockupBase.constructor} + // {SablierLockupState.constructor} address actualNFTDescriptor = address(constructedLockup.nftDescriptor()); address expectedNFTDescriptor = address(nftDescriptor); assertEq(actualNFTDescriptor, expectedNFTDescriptor, "nftDescriptor"); - // {SablierLockupBase.supportsInterface} + // {SablierLockup.supportsInterface} assertTrue(constructedLockup.supportsInterface(0x49064906), "ERC-4906 interface ID"); - - // {SablierLockup.constructor} - uint256 actualMaxCount = constructedLockup.MAX_COUNT(); - uint256 expectedMaxCount = defaults.MAX_COUNT(); - assertEq(actualMaxCount, expectedMaxCount, "MAX_COUNT"); } } diff --git a/tests/integration/concrete/lockup-base/cancel-multiple/cancelMultiple.t.sol b/tests/integration/concrete/lockup-base/cancel-multiple/cancelMultiple.t.sol deleted file mode 100644 index 92add76ae..000000000 --- a/tests/integration/concrete/lockup-base/cancel-multiple/cancelMultiple.t.sol +++ /dev/null @@ -1,144 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.22 <0.9.0; - -import { Solarray } from "solarray/src/Solarray.sol"; - -import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; -import { Errors } from "src/libraries/Errors.sol"; -import { Lockup } from "src/types/DataTypes.sol"; - -import { Integration_Test } from "../../../Integration.t.sol"; - -contract CancelMultiple_Integration_Concrete_Test is Integration_Test { - // An array of stream IDs to be canceled. - uint256[] internal streamIds; - - function setUp() public virtual override { - Integration_Test.setUp(); - - streamIds.push(createDefaultStream()); - - // Create the second stream with an end time double that of the default stream so that the refund amounts are - // different. - streamIds.push(createDefaultStreamWithEndTime(defaults.END_TIME() + defaults.TOTAL_DURATION())); - } - - function test_RevertWhen_DelegateCall() external { - expectRevert_DelegateCall({ callData: abi.encodeCall(lockup.cancelMultiple, streamIds) }); - } - - function test_WhenZeroArrayLength() external whenNoDelegateCall { - // It should do nothing. - uint256[] memory nullStreamIds = new uint256[](0); - lockup.cancelMultiple(nullStreamIds); - } - - function test_RevertGiven_AtleastOneNullStream() external whenNoDelegateCall whenNonZeroArrayLength { - expectRevert_Null({ - callData: abi.encodeCall(lockup.cancelMultiple, Solarray.uint256s(streamIds[0], nullStreamId)) - }); - } - - function test_RevertGiven_AtleastOneColdStream() - external - whenNoDelegateCall - whenNonZeroArrayLength - givenNoNullStreams - { - uint40 earlyEndTime = defaults.END_TIME() - 10; - uint256 earlyEndtimeStreamId = createDefaultStreamWithEndTime(earlyEndTime); - vm.warp({ newTimestamp: earlyEndTime + 1 seconds }); - vm.expectRevert(abi.encodeWithSelector(Errors.SablierLockupBase_StreamSettled.selector, earlyEndtimeStreamId)); - lockup.cancelMultiple({ streamIds: Solarray.uint256s(streamIds[0], earlyEndtimeStreamId) }); - } - - function test_RevertWhen_CallerUnauthorizedForAny() - external - whenNoDelegateCall - whenNonZeroArrayLength - givenNoNullStreams - givenNoColdStreams - { - // Make the Recipient the caller in this test. - resetPrank({ msgSender: users.recipient }); - - // Run the test. - vm.expectRevert( - abi.encodeWithSelector(Errors.SablierLockupBase_Unauthorized.selector, streamIds[0], users.recipient) - ); - lockup.cancelMultiple(streamIds); - } - - function test_RevertGiven_AtleastOneNonCancelableStream() - external - whenNoDelegateCall - whenNonZeroArrayLength - givenNoNullStreams - givenNoColdStreams - whenCallerAuthorizedForAllStreams - { - vm.expectRevert( - abi.encodeWithSelector(Errors.SablierLockupBase_StreamNotCancelable.selector, notCancelableStreamId) - ); - lockup.cancelMultiple({ streamIds: Solarray.uint256s(streamIds[0], notCancelableStreamId) }); - } - - function test_GivenAllStreamsCancelable() - external - whenNoDelegateCall - whenNonZeroArrayLength - givenNoNullStreams - givenNoColdStreams - whenCallerAuthorizedForAllStreams - { - // Simulate the passage of time. - vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); - - // It should refund the sender. - uint128 senderAmount0 = lockup.refundableAmountOf(streamIds[0]); - expectCallToTransfer({ to: users.sender, value: senderAmount0 }); - uint128 senderAmount1 = lockup.refundableAmountOf(streamIds[1]); - expectCallToTransfer({ to: users.sender, value: senderAmount1 }); - - // It should emit {CancelLockupStream} events for all streams. - vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.CancelLockupStream({ - streamId: streamIds[0], - sender: users.sender, - recipient: users.recipient, - token: dai, - senderAmount: senderAmount0, - recipientAmount: defaults.DEPOSIT_AMOUNT() - senderAmount0 - }); - vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.CancelLockupStream({ - streamId: streamIds[1], - sender: users.sender, - recipient: users.recipient, - token: dai, - senderAmount: senderAmount1, - recipientAmount: defaults.DEPOSIT_AMOUNT() - senderAmount1 - }); - - // Cancel the streams. - lockup.cancelMultiple(streamIds); - - // It should mark the streams as canceled. - Lockup.Status expectedStatus = Lockup.Status.CANCELED; - assertEq(lockup.statusOf(streamIds[0]), expectedStatus, "status0"); - assertEq(lockup.statusOf(streamIds[1]), expectedStatus, "status1"); - - // It should make the streams as non cancelable. - assertFalse(lockup.isCancelable(streamIds[0]), "isCancelable0"); - assertFalse(lockup.isCancelable(streamIds[1]), "isCancelable1"); - - // It should update the refunded amounts. - assertEq(lockup.getRefundedAmount(streamIds[0]), senderAmount0, "refundedAmount0"); - assertEq(lockup.getRefundedAmount(streamIds[1]), senderAmount1, "refundedAmount1"); - - // It should not burn the NFT for all streams. - address expectedNFTOwner = users.recipient; - assertEq(lockup.getRecipient(streamIds[0]), expectedNFTOwner, "NFT owner0"); - assertEq(lockup.getRecipient(streamIds[1]), expectedNFTOwner, "NFT owner1"); - } -} diff --git a/tests/integration/concrete/lockup-base/cancel-multiple/cancelMultiple.tree b/tests/integration/concrete/lockup-base/cancel-multiple/cancelMultiple.tree deleted file mode 100644 index 7a6f0f1a0..000000000 --- a/tests/integration/concrete/lockup-base/cancel-multiple/cancelMultiple.tree +++ /dev/null @@ -1,25 +0,0 @@ -CancelMultiple_Integration_Concrete_Test -├── when delegate call -│ └── it should revert -└── when no delegate call - ├── when zero array length - │ └── it should do nothing - └── when non zero array length - ├── given atleast one null stream - │ └── it should revert - └── given no null streams - ├── given atleast one cold stream - │ └── it should revert - └── given no cold streams - ├── when caller unauthorized for any - │ └── it should revert - └── when caller authorized for all streams - ├── given atleast one non cancelable stream - │ └── it should revert - └── given all streams cancelable - ├── it should mark the streams as canceled - ├── it should make the streams as non cancelable - ├── it should refund the sender - ├── it should update the refunded amounts - ├── it should not burn the NFT for all streams - └── it should emit {CancelLockupStream} events for all streams diff --git a/tests/integration/concrete/lockup-base/collect-fees/collectFees.t.sol b/tests/integration/concrete/lockup-base/collect-fees/collectFees.t.sol deleted file mode 100644 index 2b454088a..000000000 --- a/tests/integration/concrete/lockup-base/collect-fees/collectFees.t.sol +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.22 <0.9.0; - -import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; -import { Errors } from "src/libraries/Errors.sol"; - -import { Integration_Test } from "../../../Integration.t.sol"; - -contract CollectFees_Integration_Concrete_Test is Integration_Test { - function test_GivenAdminIsNotContract() external { - _test_CollectFees(users.admin); - } - - function test_RevertGiven_AdminDoesNotImplementReceiveFunction() external givenAdminIsContract { - // Transfer the admin to a contract that does not implement the receive function. - resetPrank({ msgSender: users.admin }); - lockup.transferAdmin(address(contractWithoutReceive)); - - // Make the contract the caller. - resetPrank({ msgSender: address(contractWithoutReceive) }); - - // Expect a revert. - vm.expectRevert( - abi.encodeWithSelector( - Errors.SablierLockupBase_FeeTransferFail.selector, - address(contractWithoutReceive), - address(lockup).balance - ) - ); - - // Collect the fees. - lockup.collectFees(); - } - - function test_GivenAdminImplementsReceiveFunction() external givenAdminIsContract { - // Transfer the admin to a contract that implements the receive function. - resetPrank({ msgSender: users.admin }); - lockup.transferAdmin(address(contractWithReceive)); - - // Make the contract the caller. - resetPrank({ msgSender: address(contractWithReceive) }); - - // Run the tests. - _test_CollectFees(address(contractWithReceive)); - } - - function _test_CollectFees(address admin) private { - vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); - - // Load the initial ETH balance of the admin. - uint256 initialAdminBalance = admin.balance; - - // Make Alice the caller. - resetPrank({ msgSender: users.alice }); - - // Make a withdrawal and pay the fee. - lockup.withdrawMax{ value: FEE }({ streamId: defaultStreamId, to: users.recipient }); - - // It should emit a {CollectFees} event. - vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.CollectFees({ admin: admin, feeAmount: FEE }); - - lockup.collectFees(); - - // It should transfer the fee. - assertEq(admin.balance, initialAdminBalance + FEE, "admin ETH balance"); - - // It should decrease contract balance to zero. - assertEq(address(lockup).balance, 0, "lockup ETH balance"); - } -} diff --git a/tests/integration/concrete/lockup-base/collect-fees/collectFees.tree b/tests/integration/concrete/lockup-base/collect-fees/collectFees.tree deleted file mode 100644 index 6023588ef..000000000 --- a/tests/integration/concrete/lockup-base/collect-fees/collectFees.tree +++ /dev/null @@ -1,12 +0,0 @@ -CollectFees_Integration_Concrete_Test -├── given admin is not contract -│ ├── it should transfer fee -│ ├── it should decrease contract balance to zero -│ └── it should emit a {CollectFees} event -└── given admin is contract - ├── given admin does not implement receive function - │ └── it should revert - └── given admin implements receive function - ├── it should transfer fee - ├── it should decrease contract balance to zero - └── it should emit a {CollectFees} event diff --git a/tests/integration/concrete/lockup-base/renounce-multiple/renounceMultiple.t.sol b/tests/integration/concrete/lockup-base/renounce-multiple/renounceMultiple.t.sol deleted file mode 100644 index 299882e2f..000000000 --- a/tests/integration/concrete/lockup-base/renounce-multiple/renounceMultiple.t.sol +++ /dev/null @@ -1,105 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.22 <0.9.0; - -import { Solarray } from "solarray/src/Solarray.sol"; - -import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; -import { Errors } from "src/libraries/Errors.sol"; - -import { Integration_Test } from "../../../Integration.t.sol"; - -contract RenounceMultiple_Integration_Concrete_Test is Integration_Test { - // An array of stream IDs to be renounces. - uint256[] internal streamIds; - - function setUp() public virtual override { - Integration_Test.setUp(); - - // Create test streams. - streamIds.push(defaultStreamId); - streamIds.push(createDefaultStream()); - } - - function test_RevertWhen_DelegateCall() external { - expectRevert_DelegateCall({ callData: abi.encodeCall(lockup.renounceMultiple, streamIds) }); - } - - function test_WhenZeroArrayLength() external whenNoDelegateCall { - // It should do nothing. - uint256[] memory nullStreamIds = new uint256[](0); - lockup.renounceMultiple(nullStreamIds); - } - - function test_RevertGiven_AtLeastOneNullStream() external whenNoDelegateCall whenNonZeroArrayLength { - expectRevert_Null({ - callData: abi.encodeCall(lockup.renounceMultiple, Solarray.uint256s(streamIds[0], nullStreamId)) - }); - } - - function test_RevertGiven_AtLeastOneColdStream() - external - whenNoDelegateCall - whenNonZeroArrayLength - givenNoNullStreams - { - uint40 earlyEndTime = defaults.END_TIME() - 10; - uint256 earlyEndtimeStreamId = createDefaultStreamWithEndTime(earlyEndTime); - vm.warp({ newTimestamp: earlyEndTime + 1 seconds }); - - vm.expectRevert(abi.encodeWithSelector(Errors.SablierLockupBase_StreamSettled.selector, earlyEndtimeStreamId)); - lockup.renounceMultiple({ streamIds: Solarray.uint256s(streamIds[0], earlyEndtimeStreamId) }); - } - - function test_RevertWhen_CallerUnauthorizedForAny() - external - whenNoDelegateCall - whenNonZeroArrayLength - givenNoNullStreams - givenNoColdStreams - { - // Make the Recipient the caller in this test. - resetPrank({ msgSender: users.recipient }); - - // Run the test. - vm.expectRevert( - abi.encodeWithSelector(Errors.SablierLockupBase_Unauthorized.selector, streamIds[0], users.recipient) - ); - lockup.renounceMultiple(streamIds); - } - - function test_RevertGiven_AtLeastOneNonCancelableStream() - external - whenNoDelegateCall - whenNonZeroArrayLength - givenNoNullStreams - givenNoColdStreams - whenCallerAuthorizedForAllStreams - { - vm.expectRevert( - abi.encodeWithSelector(Errors.SablierLockupBase_StreamNotCancelable.selector, notCancelableStreamId) - ); - lockup.renounceMultiple({ streamIds: Solarray.uint256s(streamIds[0], notCancelableStreamId) }); - } - - function test_GivenAllStreamsCancelable() - external - whenNoDelegateCall - whenNonZeroArrayLength - givenNoNullStreams - givenNoColdStreams - whenCallerAuthorizedForAllStreams - { - // It should emit {RenounceLockupStream} events for both streams. - vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.RenounceLockupStream(streamIds[0]); - vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.RenounceLockupStream(streamIds[1]); - - // Renounce the streams. - lockup.renounceMultiple(streamIds); - - // It should make streams non cancelable. - assertFalse(lockup.isCancelable(streamIds[0]), "isCancelable0"); - assertFalse(lockup.isCancelable(streamIds[1]), "isCancelable1"); - } -} diff --git a/tests/integration/concrete/lockup-base/renounce-multiple/renounceMultiple.tree b/tests/integration/concrete/lockup-base/renounce-multiple/renounceMultiple.tree deleted file mode 100644 index f585d0458..000000000 --- a/tests/integration/concrete/lockup-base/renounce-multiple/renounceMultiple.tree +++ /dev/null @@ -1,21 +0,0 @@ -RenounceMultiple_Integration_Concrete_Test -├── when delegate call -│ └── it should revert -└── when no delegate call - ├── when zero array length - │ └── it should do nothing - └── when non zero array length - ├── given at least one null stream - │ └── it should revert - └── given no null streams - ├── given at least one cold stream - │ └── it should revert - └── given no cold streams - ├── when caller unauthorized for any - │ └── it should revert - └── when caller authorized for all streams - ├── given at least one non cancelable stream - │ └── it should revert - └── given all streams cancelable - ├── it should emit {RenounceLockupStream} events - └── it should make streams non cancelable diff --git a/tests/integration/concrete/lockup-base/withdraw/withdraw.tree b/tests/integration/concrete/lockup-base/withdraw/withdraw.tree deleted file mode 100644 index 48f13661e..000000000 --- a/tests/integration/concrete/lockup-base/withdraw/withdraw.tree +++ /dev/null @@ -1,65 +0,0 @@ -Withdraw_Integration_Concrete_Test -├── when delegate call -│ └── it should revert -└── when no delegate call - ├── given null - │ └── it should revert - └── given not null - ├── given DEPLETED status - │ └── it should revert - └── given not DEPLETED status - ├── when withdrawal address zero - │ └── it should revert - └── when withdrawal address not zero - ├── when zero withdraw amount - │ └── it should revert - └── when non zero withdraw amount - ├── when withdraw amount overdraws - │ └── it should revert - └── when withdraw amount not overdraw - ├── when withdrawal address not recipient - │ ├── when caller not approved third party or recipient - │ │ └── it should revert - │ └── when caller approved third party or recipient - │ ├── it should make the withdrawal - │ ├── it should update the withdrawn amount - │ └── it should emit {WithdrawFromLockupStream} and {MetadataUpdate} events - └── when withdrawal address recipient - ├── when caller unknown - │ ├── it should make the withdrawal - │ └── it should update the withdrawn amount - ├── when caller recipient - │ ├── it should make the withdrawal - │ └── it should update the withdrawn amount - └── when caller sender - ├── given end time not in future - │ ├── it should make the withdrawal - │ ├── it should mark the stream as depleted - │ └── it should make the stream not cancelable - └── given end time in future - ├── given canceled stream - │ ├── it should make the withdrawal - │ ├── it should mark the stream as depleted - │ ├── it should update the withdrawn amount - │ └── it should emit {WithdrawFromLockupStream} and {MetadataUpdate} events - └── given not canceled stream - ├── given recipient not allowed to hook - │ ├── it should make the withdrawal - │ ├── it should update the withdrawn amount - │ └── it should not make Sablier run the recipient hook - └── given recipient allowed to hook - ├── when reverting recipient - │ └── it should revert - └── when non reverting recipient - ├── when hook returns invalid selector - │ └── it should revert - └── when hook returns valid selector - ├── when reentrancy - │ ├── it should make multiple withdrawals - │ ├── it should update the withdrawn amounts - │ └── it should make Sablier run the recipient hook - └── when no reentrancy - ├── it should make the withdrawal - ├── it should update the withdrawn amount - ├── it should make Sablier run the recipient hook - └── it should emit {WithdrawFromLockupStream} and {MetadataUpdate} events diff --git a/tests/integration/concrete/lockup-dynamic/LockupDynamic.t.sol b/tests/integration/concrete/lockup-dynamic/LockupDynamic.t.sol index c080d91aa..d47d1f8e1 100644 --- a/tests/integration/concrete/lockup-dynamic/LockupDynamic.t.sol +++ b/tests/integration/concrete/lockup-dynamic/LockupDynamic.t.sol @@ -1,14 +1,13 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { Lockup } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; import { Integration_Test } from "../../Integration.t.sol"; -import { Cancel_Integration_Concrete_Test } from "../lockup-base/cancel/cancel.t.sol"; -import { RefundableAmountOf_Integration_Concrete_Test } from - "../lockup-base/refundable-amount-of/refundableAmountOf.t.sol"; -import { Renounce_Integration_Concrete_Test } from "../lockup-base/renounce/renounce.t.sol"; -import { Withdraw_Integration_Concrete_Test } from "../lockup-base/withdraw/withdraw.t.sol"; +import { Cancel_Integration_Concrete_Test } from "../lockup/cancel/cancel.t.sol"; +import { RefundableAmountOf_Integration_Concrete_Test } from "../lockup/refundable-amount-of/refundableAmountOf.t.sol"; +import { Renounce_Integration_Concrete_Test } from "../lockup/renounce/renounce.t.sol"; +import { Withdraw_Integration_Concrete_Test } from "../lockup/withdraw/withdraw.t.sol"; abstract contract Lockup_Dynamic_Integration_Concrete_Test is Integration_Test { function setUp() public virtual override { diff --git a/tests/integration/concrete/lockup-dynamic/create-with-durations-ld/createWithDurationsLD.t.sol b/tests/integration/concrete/lockup-dynamic/create-with-durations-ld/createWithDurationsLD.t.sol index 493ade52d..a1481cfb9 100644 --- a/tests/integration/concrete/lockup-dynamic/create-with-durations-ld/createWithDurationsLD.t.sol +++ b/tests/integration/concrete/lockup-dynamic/create-with-durations-ld/createWithDurationsLD.t.sol @@ -4,9 +4,10 @@ pragma solidity >=0.8.22 <0.9.0; import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; import { ud2x18 } from "@prb/math/src/UD2x18.sol"; -import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; +import { ISablierLockupDynamic } from "src/interfaces/ISablierLockupDynamic.sol"; import { Errors } from "src/libraries/Errors.sol"; -import { Lockup, LockupDynamic } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; +import { LockupDynamic } from "src/types/LockupDynamic.sol"; import { Lockup_Dynamic_Integration_Concrete_Test } from "../LockupDynamic.t.sol"; @@ -35,20 +36,7 @@ contract CreateWithDurationsLD_Integration_Concrete_Test is Lockup_Dynamic_Integ }); } - function test_RevertWhen_SegmentCountExceedsMaxValue() external whenNoDelegateCall { - LockupDynamic.SegmentWithDuration[] memory segmentsWithDurations = - new LockupDynamic.SegmentWithDuration[](25_000); - - // Set the default segments with duration. - vm.expectRevert(abi.encodeWithSelector(Errors.SablierHelpers_SegmentCountTooHigh.selector, 25_000)); - createDefaultStreamWithDurations(segmentsWithDurations); - } - - function test_RevertWhen_FirstIndexHasZeroDuration() - external - whenNoDelegateCall - whenSegmentCountNotExceedMaxValue - { + function test_RevertWhen_FirstIndexHasZeroDuration() external whenNoDelegateCall { uint40 startTime = getBlockTimestamp(); LockupDynamic.SegmentWithDuration[] memory segmentsWithDurations = _defaultParams.segmentsWithDurations; segmentsWithDurations[1].duration = 0; @@ -69,7 +57,6 @@ contract CreateWithDurationsLD_Integration_Concrete_Test is Lockup_Dynamic_Integ function test_RevertWhen_StartTimeExceedsFirstTimestamp() external whenNoDelegateCall - whenSegmentCountNotExceedMaxValue whenFirstIndexHasNonZeroDuration whenTimestampsCalculationOverflows { @@ -93,7 +80,6 @@ contract CreateWithDurationsLD_Integration_Concrete_Test is Lockup_Dynamic_Integ function test_RevertWhen_TimestampsNotStrictlyIncreasing() external whenNoDelegateCall - whenSegmentCountNotExceedMaxValue whenFirstIndexHasNonZeroDuration whenTimestampsCalculationOverflows whenStartTimeNotExceedsFirstTimestamp @@ -126,14 +112,7 @@ contract CreateWithDurationsLD_Integration_Concrete_Test is Lockup_Dynamic_Integ } } - function test_WhenTimestampsCalculationNotOverflow() - external - whenNoDelegateCall - whenSegmentCountNotExceedMaxValue - whenFirstIndexHasNonZeroDuration - { - // Make the Sender the stream's funder - address funder = users.sender; + function test_WhenTimestampsCalculationNotOverflow() external whenNoDelegateCall whenFirstIndexHasNonZeroDuration { uint256 expectedStreamId = lockup.nextStreamId(); // Declare the timestamps. @@ -147,16 +126,13 @@ contract CreateWithDurationsLD_Integration_Concrete_Test is Lockup_Dynamic_Integ segments[1].timestamp = segments[0].timestamp + _defaultParams.segmentsWithDurations[1].duration; // It should perform the ERC-20 transfers. - expectCallToTransferFrom({ from: funder, to: address(lockup), value: defaults.DEPOSIT_AMOUNT() }); - - // Expect the broker fee to be paid to the broker. - expectCallToTransferFrom({ from: funder, to: users.broker, value: defaults.BROKER_FEE_AMOUNT() }); + expectCallToTransferFrom({ from: users.sender, to: address(lockup), value: defaults.DEPOSIT_AMOUNT() }); // It should emit {CreateLockupDynamicStream} and {MetadataUpdate} events. vm.expectEmit({ emitter: address(lockup) }); emit IERC4906.MetadataUpdate({ _tokenId: expectedStreamId }); vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockup.CreateLockupDynamicStream({ + emit ISablierLockupDynamic.CreateLockupDynamicStream({ streamId: expectedStreamId, commonParams: defaults.lockupCreateEvent(timestamps), segments: segments diff --git a/tests/integration/concrete/lockup-dynamic/create-with-durations-ld/createWithDurationsLD.tree b/tests/integration/concrete/lockup-dynamic/create-with-durations-ld/createWithDurationsLD.tree index 44d129842..9d1f981d1 100644 --- a/tests/integration/concrete/lockup-dynamic/create-with-durations-ld/createWithDurationsLD.tree +++ b/tests/integration/concrete/lockup-dynamic/create-with-durations-ld/createWithDurationsLD.tree @@ -2,21 +2,18 @@ CreateWithDurationsLD_Integration_Concrete_Test ├── when delegate call │ └── it should revert └── when no delegate call - ├── when segment count exceeds max value + ├── when first index has zero duration │ └── it should revert - └── when segment count not exceed max value - ├── when first index has zero duration - │ └── it should revert - └── when first index has non zero duration - ├── when timestamps calculation overflows - │ ├── when start time exceeds first timestamp - │ │ └── it should revert - │ └── when start time not exceeds first timestamp - │ └── when timestamps not strictly increasing - │ └── it should revert - └── when timestamps calculation not overflow - ├── it should create the stream - ├── it should bump the next stream ID - ├── it should mint the NFT - ├── it should emit {CreateLockupDynamicStream} and {MetadataUpdate} events - └── it should perform the ERC-20 transfers + └── when first index has non zero duration + ├── when timestamps calculation overflows + │ ├── when start time exceeds first timestamp + │ │ └── it should revert + │ └── when start time not exceeds first timestamp + │ └── when timestamps not strictly increasing + │ └── it should revert + └── when timestamps calculation not overflow + ├── it should create the stream + ├── it should bump the next stream ID + ├── it should mint the NFT + ├── it should emit {CreateLockupDynamicStream} and {MetadataUpdate} events + └── it should perform the ERC-20 transfers diff --git a/tests/integration/concrete/lockup-dynamic/create-with-timestamps-ld/createWithTimestampsLD.t.sol b/tests/integration/concrete/lockup-dynamic/create-with-timestamps-ld/createWithTimestampsLD.t.sol index bbfce6ba8..51102e411 100644 --- a/tests/integration/concrete/lockup-dynamic/create-with-timestamps-ld/createWithTimestampsLD.t.sol +++ b/tests/integration/concrete/lockup-dynamic/create-with-timestamps-ld/createWithTimestampsLD.t.sol @@ -5,14 +5,15 @@ import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { stdError } from "forge-std/src/StdError.sol"; -import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; +import { ISablierLockupDynamic } from "src/interfaces/ISablierLockupDynamic.sol"; import { Errors } from "src/libraries/Errors.sol"; -import { Lockup, LockupDynamic } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; +import { LockupDynamic } from "src/types/LockupDynamic.sol"; import { CreateWithTimestamps_Integration_Concrete_Test, Integration_Test -} from "../../lockup-base/create-with-timestamps/createWithTimestamps.t.sol"; +} from "../../lockup/create-with-timestamps/createWithTimestamps.t.sol"; contract CreateWithTimestampsLD_Integration_Concrete_Test is CreateWithTimestamps_Integration_Concrete_Test { function setUp() public virtual override { @@ -32,11 +33,11 @@ contract CreateWithTimestampsLD_Integration_Concrete_Test is CreateWithTimestamp external whenNoDelegateCall whenShapeNotExceed32Bytes - whenBrokerFeeNotExceedMaxValue whenSenderNotZeroAddress whenRecipientNotZeroAddress whenDepositAmountNotZero whenStartTimeNotZero + whenTokenNotNativeToken whenTokenContract { LockupDynamic.Segment[] memory segments; @@ -44,37 +45,17 @@ contract CreateWithTimestampsLD_Integration_Concrete_Test is CreateWithTimestamp createDefaultStreamWithSegments(segments); } - function test_RevertWhen_SegmentCountExceedsMaxValue() - external - whenNoDelegateCall - whenShapeNotExceed32Bytes - whenBrokerFeeNotExceedMaxValue - whenSenderNotZeroAddress - whenRecipientNotZeroAddress - whenDepositAmountNotZero - whenStartTimeNotZero - whenTokenContract - whenSegmentCountNotZero - { - uint256 segmentCount = defaults.MAX_COUNT() + 1; - LockupDynamic.Segment[] memory segments = new LockupDynamic.Segment[](segmentCount); - segments[segmentCount - 1].timestamp = defaults.END_TIME(); - vm.expectRevert(abi.encodeWithSelector(Errors.SablierHelpers_SegmentCountTooHigh.selector, segmentCount)); - createDefaultStreamWithSegments(segments); - } - function test_RevertWhen_SegmentAmountsSumOverflows() external whenNoDelegateCall whenShapeNotExceed32Bytes - whenBrokerFeeNotExceedMaxValue whenSenderNotZeroAddress whenRecipientNotZeroAddress whenDepositAmountNotZero whenStartTimeNotZero + whenTokenNotNativeToken whenTokenContract whenSegmentCountNotZero - whenSegmentCountNotExceedMaxValue { LockupDynamic.Segment[] memory segments = defaults.segments(); segments[0].amount = MAX_UINT128; @@ -87,14 +68,13 @@ contract CreateWithTimestampsLD_Integration_Concrete_Test is CreateWithTimestamp external whenNoDelegateCall whenShapeNotExceed32Bytes - whenBrokerFeeNotExceedMaxValue whenSenderNotZeroAddress whenRecipientNotZeroAddress whenDepositAmountNotZero whenStartTimeNotZero + whenTokenNotNativeToken whenTokenContract whenSegmentCountNotZero - whenSegmentCountNotExceedMaxValue whenSegmentAmountsSumNotOverflow { // Change the timestamp of the first segment. @@ -116,14 +96,13 @@ contract CreateWithTimestampsLD_Integration_Concrete_Test is CreateWithTimestamp external whenNoDelegateCall whenShapeNotExceed32Bytes - whenBrokerFeeNotExceedMaxValue whenSenderNotZeroAddress whenRecipientNotZeroAddress whenDepositAmountNotZero whenStartTimeNotZero + whenTokenNotNativeToken whenTokenContract whenSegmentCountNotZero - whenSegmentCountNotExceedMaxValue whenSegmentAmountsSumNotOverflow { // Change the timestamp of the first segment. @@ -141,20 +120,45 @@ contract CreateWithTimestampsLD_Integration_Concrete_Test is CreateWithTimestamp createDefaultStreamWithSegments(segments); } + function test_RevertWhen_EndTimeNotEqualLastTimestamp() + external + whenNoDelegateCall + whenShapeNotExceed32Bytes + whenSenderNotZeroAddress + whenRecipientNotZeroAddress + whenDepositAmountNotZero + whenStartTimeNotZero + whenTokenNotNativeToken + whenTokenContract + whenSegmentCountNotZero + whenSegmentAmountsSumNotOverflow + whenStartTimeLessThanFirstTimestamp + { + _defaultParams.createWithTimestamps.timestamps.end = defaults.END_TIME() + 1 seconds; + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierHelpers_EndTimeNotEqualToLastSegmentTimestamp.selector, + _defaultParams.createWithTimestamps.timestamps.end, + _defaultParams.createWithTimestamps.timestamps.end - 1 + ) + ); + createDefaultStream(); + } + function test_RevertWhen_TimestampsNotStrictlyIncreasing() external whenNoDelegateCall whenShapeNotExceed32Bytes - whenBrokerFeeNotExceedMaxValue whenSenderNotZeroAddress whenRecipientNotZeroAddress whenDepositAmountNotZero whenStartTimeNotZero + whenTokenNotNativeToken whenTokenContract whenSegmentCountNotZero - whenSegmentCountNotExceedMaxValue whenSegmentAmountsSumNotOverflow whenStartTimeLessThanFirstTimestamp + whenEndTimeEqualsLastTimestamp { // Swap the segment timestamps. LockupDynamic.Segment[] memory segments = defaults.segments(); @@ -183,20 +187,19 @@ contract CreateWithTimestampsLD_Integration_Concrete_Test is CreateWithTimestamp whenDepositAmountNotZero whenStartTimeNotZero whenSegmentCountNotZero - whenSegmentCountNotExceedMaxValue whenSegmentAmountsSumNotOverflow whenStartTimeLessThanFirstTimestamp + whenEndTimeEqualsLastTimestamp whenTimestampsStrictlyIncreasing { - resetPrank({ msgSender: users.sender }); + setMsgSender(users.sender); // Adjust the default deposit amount. uint128 defaultDepositAmount = defaults.DEPOSIT_AMOUNT(); uint128 depositAmount = defaultDepositAmount + 100; // Prepare the params. - _defaultParams.createWithTimestamps.broker = defaults.brokerNull(); - _defaultParams.createWithTimestamps.totalAmount = depositAmount; + _defaultParams.createWithTimestamps.depositAmount = depositAmount; // Expect the relevant error to be thrown. vm.expectRevert( @@ -213,20 +216,61 @@ contract CreateWithTimestampsLD_Integration_Concrete_Test is CreateWithTimestamp external whenNoDelegateCall whenShapeNotExceed32Bytes - whenBrokerFeeNotExceedMaxValue whenSenderNotZeroAddress whenRecipientNotZeroAddress whenDepositAmountNotZero whenStartTimeNotZero + whenTokenNotNativeToken whenTokenContract whenSegmentCountNotZero - whenSegmentCountNotExceedMaxValue whenSegmentAmountsSumNotOverflow whenStartTimeLessThanFirstTimestamp + whenEndTimeEqualsLastTimestamp whenTimestampsStrictlyIncreasing whenDepositAmountEqualsSegmentAmountsSum { - _testCreateWithTimestampsLD(address(usdt)); + IERC20 _usdt = IERC20(address(usdt)); + + uint256 previousAggregateAmount = lockup.aggregateAmount(_usdt); + + // Update the default params. + _defaultParams.createWithTimestamps.depositAmount = defaults.DEPOSIT_AMOUNT_6D(); + _defaultParams.createWithTimestamps.token = _usdt; + _defaultParams.segments[0].amount = 2600e6; + _defaultParams.segments[1].amount = 7400e6; + + uint256 expectedStreamId = lockup.nextStreamId(); + + // It should perform the ERC-20 transfers. + expectCallToTransferFrom({ + token: _usdt, + from: users.sender, + to: address(lockup), + value: _defaultParams.createWithTimestamps.depositAmount + }); + + // It should emit {CreateLockupDynamicStream} and {MetadataUpdate} events. + vm.expectEmit({ emitter: address(lockup) }); + emit IERC4906.MetadataUpdate({ _tokenId: expectedStreamId }); + vm.expectEmit({ emitter: address(lockup) }); + emit ISablierLockupDynamic.CreateLockupDynamicStream({ + streamId: expectedStreamId, + commonParams: defaults.lockupCreateEvent(_usdt, _defaultParams.createWithTimestamps.depositAmount), + segments: _defaultParams.segments + }); + + // Create the stream. + uint256 streamId = createDefaultStream(); + + // It should create the stream. + assertEqStream(streamId, _usdt); + assertEq(lockup.getSegments(streamId), _defaultParams.segments); + assertEq(lockup.getLockupModel(streamId), Lockup.Model.LOCKUP_DYNAMIC); + assertEq( + lockup.aggregateAmount(_usdt), + previousAggregateAmount + _defaultParams.createWithTimestamps.depositAmount, + "aggregateAmount" + ); } function test_WhenTokenNotMissERC20ReturnValue() @@ -238,57 +282,43 @@ contract CreateWithTimestampsLD_Integration_Concrete_Test is CreateWithTimestamp whenDepositAmountNotZero whenStartTimeNotZero whenSegmentCountNotZero - whenSegmentCountNotExceedMaxValue whenSegmentAmountsSumNotOverflow whenStartTimeLessThanFirstTimestamp + whenEndTimeEqualsLastTimestamp whenTimestampsStrictlyIncreasing whenDepositAmountNotEqualSegmentAmountsSum - whenBrokerFeeNotExceedMaxValue + whenTokenNotNativeToken whenTokenContract { - _testCreateWithTimestampsLD(address(dai)); - } - - function _testCreateWithTimestampsLD(address token) private { - // Make the Sender the stream's funder. - address funder = users.sender; + uint256 previousAggregateAmount = lockup.aggregateAmount(dai); uint256 expectedStreamId = lockup.nextStreamId(); // It should perform the ERC-20 transfers. expectCallToTransferFrom({ - token: IERC20(token), - from: funder, + token: dai, + from: users.sender, to: address(lockup), value: defaults.DEPOSIT_AMOUNT() }); - // Expect the broker fee to be paid to the broker. - expectCallToTransferFrom({ - token: IERC20(token), - from: funder, - to: users.broker, - value: defaults.BROKER_FEE_AMOUNT() - }); - // It should emit {CreateLockupDynamicStream} and {MetadataUpdate} events. vm.expectEmit({ emitter: address(lockup) }); emit IERC4906.MetadataUpdate({ _tokenId: expectedStreamId }); vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockup.CreateLockupDynamicStream({ + emit ISablierLockupDynamic.CreateLockupDynamicStream({ streamId: expectedStreamId, - commonParams: defaults.lockupCreateEvent(IERC20(token)), + commonParams: defaults.lockupCreateEvent(dai, defaults.DEPOSIT_AMOUNT()), segments: defaults.segments() }); // Create the stream. - _defaultParams.createWithTimestamps.token = IERC20(token); uint256 streamId = createDefaultStream(); // It should create the stream. - assertEqStream(streamId); - assertEq(lockup.getUnderlyingToken(streamId), IERC20(token), "underlyingToken"); + assertEqStream(streamId, dai); assertEq(lockup.getSegments(streamId), defaults.segments()); assertEq(lockup.getLockupModel(streamId), Lockup.Model.LOCKUP_DYNAMIC); + assertEq(lockup.aggregateAmount(dai), previousAggregateAmount + defaults.DEPOSIT_AMOUNT(), "aggregateAmount"); } } diff --git a/tests/integration/concrete/lockup-dynamic/create-with-timestamps-ld/createWithTimestampsLD.tree b/tests/integration/concrete/lockup-dynamic/create-with-timestamps-ld/createWithTimestampsLD.tree index 9f98db7b2..9aa1ac5ea 100644 --- a/tests/integration/concrete/lockup-dynamic/create-with-timestamps-ld/createWithTimestampsLD.tree +++ b/tests/integration/concrete/lockup-dynamic/create-with-timestamps-ld/createWithTimestampsLD.tree @@ -3,17 +3,17 @@ CreateWithTimestampsLD_Integration_Concrete_Test ├── when segment count zero │ └── it should revert └── when segment count not zero - ├── when segment count exceeds max value + ├── when segment amounts sum overflows │ └── it should revert - └── when segment count not exceed max value - ├── when segment amounts sum overflows + └── when segment amounts sum not overflow + ├── when start time greater than first timestamp │ └── it should revert - └── when segment amounts sum not overflow - ├── when start time greater than first timestamp - │ └── it should revert - ├── when start time equals first timestamp + ├── when start time equals first timestamp + │ └── it should revert + └── when start time less than first timestamp + ├── when end time not equal last timestamp │ └── it should revert - └── when start time less than first timestamp + └── when end time equals last timestamp ├── when timestamps not strictly increasing │ └── it should revert └── when timestamps strictly increasing @@ -23,12 +23,14 @@ CreateWithTimestampsLD_Integration_Concrete_Test ├── when token misses ERC20 return value │ ├── it should create the stream │ ├── it should bump the next stream ID + │ ├── it should increase the aggregate amount │ ├── it should mint the NFT │ ├── it should emit {CreateLockupDynamicStream} and {MetadataUpdate} events │ └── it should perform the ERC-20 transfers └── when token not miss ERC20 return value ├── it should create the stream ├── it should bump the next stream ID + ├── it should increase the aggregate amount ├── it should mint the NFT ├── it should emit {CreateLockupDynamicStream} and {MetadataUpdate} events - └── it should perform the ERC-20 transfers \ No newline at end of file + └── it should perform the ERC-20 transfers diff --git a/tests/integration/concrete/lockup-dynamic/get-segments/getSegments.t.sol b/tests/integration/concrete/lockup-dynamic/get-segments/getSegments.t.sol index 39c94a7cc..8e19355a1 100644 --- a/tests/integration/concrete/lockup-dynamic/get-segments/getSegments.t.sol +++ b/tests/integration/concrete/lockup-dynamic/get-segments/getSegments.t.sol @@ -2,13 +2,14 @@ pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; -import { Lockup, LockupDynamic } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; +import { LockupDynamic } from "src/types/LockupDynamic.sol"; import { Lockup_Dynamic_Integration_Concrete_Test } from "../LockupDynamic.t.sol"; contract GetSegments_Integration_Concrete_Test is Lockup_Dynamic_Integration_Concrete_Test { function test_RevertGiven_Null() external { - expectRevert_Null({ callData: abi.encodeCall(lockup.getSegments, nullStreamId) }); + expectRevert_Null({ callData: abi.encodeCall(lockup.getSegments, ids.nullStream) }); } function test_RevertGiven_NotDynamicModel() external givenNotNull { @@ -16,14 +17,16 @@ contract GetSegments_Integration_Concrete_Test is Lockup_Dynamic_Integration_Con uint256 streamId = createDefaultStream(); vm.expectRevert( abi.encodeWithSelector( - Errors.SablierLockup_NotExpectedModel.selector, Lockup.Model.LOCKUP_LINEAR, Lockup.Model.LOCKUP_DYNAMIC + Errors.SablierLockupState_NotExpectedModel.selector, + Lockup.Model.LOCKUP_LINEAR, + Lockup.Model.LOCKUP_DYNAMIC ) ); lockup.getSegments(streamId); } function test_GivenDynamicModel() external givenNotNull { - LockupDynamic.Segment[] memory actualSegments = lockup.getSegments(defaultStreamId); + LockupDynamic.Segment[] memory actualSegments = lockup.getSegments(ids.defaultStream); LockupDynamic.Segment[] memory expectedSegments = defaults.segments(); assertEq(actualSegments, expectedSegments); } diff --git a/tests/integration/concrete/lockup-dynamic/streamed-amount-of/streamedAmountOf.t.sol b/tests/integration/concrete/lockup-dynamic/streamed-amount-of/streamedAmountOf.t.sol index ae3073043..17afe053f 100644 --- a/tests/integration/concrete/lockup-dynamic/streamed-amount-of/streamedAmountOf.t.sol +++ b/tests/integration/concrete/lockup-dynamic/streamed-amount-of/streamedAmountOf.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { LockupDynamic } from "src/types/DataTypes.sol"; -import { StreamedAmountOf_Integration_Concrete_Test } from "../../lockup-base/streamed-amount-of/streamedAmountOf.t.sol"; +import { LockupDynamic } from "src/types/LockupDynamic.sol"; +import { StreamedAmountOf_Integration_Concrete_Test } from "../../lockup/streamed-amount-of/streamedAmountOf.t.sol"; import { Lockup_Dynamic_Integration_Concrete_Test, Integration_Test } from "../LockupDynamic.t.sol"; contract StreamedAmountOf_Lockup_Dynamic_Integration_Concrete_Test is @@ -39,7 +39,7 @@ contract StreamedAmountOf_Lockup_Dynamic_Integration_Concrete_Test is vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() + 740 seconds }); // It should return the correct streamed amount. - uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); + uint128 actualStreamedAmount = lockup.streamedAmountOf(ids.defaultStream); uint128 expectedStreamedAmount = defaults.segments()[0].amount + 2340.0854685246007116e18; // ~7,400*0.1^{0.5} assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } diff --git a/tests/integration/concrete/lockup-dynamic/withdrawable-amount-of/withdrawableAmountOf.t.sol b/tests/integration/concrete/lockup-dynamic/withdrawable-amount-of/withdrawableAmountOf.t.sol index b10aff093..5cf5ba9cc 100644 --- a/tests/integration/concrete/lockup-dynamic/withdrawable-amount-of/withdrawableAmountOf.t.sol +++ b/tests/integration/concrete/lockup-dynamic/withdrawable-amount-of/withdrawableAmountOf.t.sol @@ -2,7 +2,7 @@ pragma solidity >=0.8.22 <0.9.0; import { WithdrawableAmountOf_Integration_Concrete_Test } from - "../../lockup-base/withdrawable-amount-of/withdrawableAmountOf.t.sol"; + "../../lockup/withdrawable-amount-of/withdrawableAmountOf.t.sol"; import { Lockup_Dynamic_Integration_Concrete_Test, Integration_Test } from "../LockupDynamic.t.sol"; contract WithdrawableAmountOf_Lockup_Dynamic_Integration_Concrete_Test is @@ -15,7 +15,7 @@ contract WithdrawableAmountOf_Lockup_Dynamic_Integration_Concrete_Test is function test_GivenStartTimeInPresent() external givenSTREAMINGStatus { vm.warp({ newTimestamp: defaults.START_TIME() }); - uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); + uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(ids.defaultStream); uint128 expectedWithdrawableAmount = 0; assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); } @@ -25,7 +25,7 @@ contract WithdrawableAmountOf_Lockup_Dynamic_Integration_Concrete_Test is vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() + 3750 seconds }); // Run the test. - uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); + uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(ids.defaultStream); // The second term is 7,400*0.5^{0.5} uint128 expectedWithdrawableAmount = defaults.segments()[0].amount + 5267.8268764263694426e18; assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); @@ -36,10 +36,14 @@ contract WithdrawableAmountOf_Lockup_Dynamic_Integration_Concrete_Test is vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() + 3750 seconds }); // Make the withdrawal. - lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: defaults.STREAMED_AMOUNT_26_PERCENT() }); + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: ids.defaultStream, + to: users.recipient, + amount: defaults.STREAMED_AMOUNT_26_PERCENT() + }); // Run the test. - uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); + uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(ids.defaultStream); // The second term is 7,500*0.5^{0.5} uint128 expectedWithdrawableAmount = diff --git a/tests/integration/concrete/lockup-linear/LockupLinear.t.sol b/tests/integration/concrete/lockup-linear/LockupLinear.t.sol index 6bc4d61c8..49f342f9f 100644 --- a/tests/integration/concrete/lockup-linear/LockupLinear.t.sol +++ b/tests/integration/concrete/lockup-linear/LockupLinear.t.sol @@ -1,14 +1,13 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { Lockup } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; import { Integration_Test } from "../../Integration.t.sol"; -import { Cancel_Integration_Concrete_Test } from "../lockup-base/cancel/cancel.t.sol"; -import { RefundableAmountOf_Integration_Concrete_Test } from - "../lockup-base/refundable-amount-of/refundableAmountOf.t.sol"; -import { Renounce_Integration_Concrete_Test } from "../lockup-base/renounce/renounce.t.sol"; -import { Withdraw_Integration_Concrete_Test } from "../lockup-base/withdraw/withdraw.t.sol"; +import { Cancel_Integration_Concrete_Test } from "../lockup/cancel/cancel.t.sol"; +import { RefundableAmountOf_Integration_Concrete_Test } from "../lockup/refundable-amount-of/refundableAmountOf.t.sol"; +import { Renounce_Integration_Concrete_Test } from "../lockup/renounce/renounce.t.sol"; +import { Withdraw_Integration_Concrete_Test } from "../lockup/withdraw/withdraw.t.sol"; abstract contract Lockup_Linear_Integration_Concrete_Test is Integration_Test { function setUp() public virtual override { diff --git a/tests/integration/concrete/lockup-linear/create-with-durations-ll/createWithDurationsLL.t.sol b/tests/integration/concrete/lockup-linear/create-with-durations-ll/createWithDurationsLL.t.sol index 8fab541e8..252d16467 100644 --- a/tests/integration/concrete/lockup-linear/create-with-durations-ll/createWithDurationsLL.t.sol +++ b/tests/integration/concrete/lockup-linear/create-with-durations-ll/createWithDurationsLL.t.sol @@ -3,8 +3,9 @@ pragma solidity >=0.8.22 <0.9.0; import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; -import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; -import { Lockup, LockupLinear } from "src/types/DataTypes.sol"; +import { ISablierLockupLinear } from "src/interfaces/ISablierLockupLinear.sol"; +import { Lockup } from "src/types/Lockup.sol"; +import { LockupLinear } from "src/types/LockupLinear.sol"; import { Lockup_Linear_Integration_Concrete_Test } from "../LockupLinear.t.sol"; @@ -28,8 +29,6 @@ contract CreateWithDurationsLL_Integration_Concrete_Test is Lockup_Linear_Integr } function _test_CreateWithDurations(LockupLinear.Durations memory durations) private { - // Make the Sender the stream's funder - address funder = users.sender; uint256 expectedStreamId = lockup.nextStreamId(); // Declare the timestamps. @@ -45,16 +44,13 @@ contract CreateWithDurationsLL_Integration_Concrete_Test is Lockup_Linear_Integr } // It should perform the ERC-20 transfers. - expectCallToTransferFrom({ from: funder, to: address(lockup), value: defaults.DEPOSIT_AMOUNT() }); - - // Expect the broker fee to be paid to the broker. - expectCallToTransferFrom({ from: funder, to: users.broker, value: defaults.BROKER_FEE_AMOUNT() }); + expectCallToTransferFrom({ from: users.sender, to: address(lockup), value: defaults.DEPOSIT_AMOUNT() }); // It should emit {CreateLockupLinearStream} and {MetadataUpdate} events. vm.expectEmit({ emitter: address(lockup) }); emit IERC4906.MetadataUpdate({ _tokenId: expectedStreamId }); vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockup.CreateLockupLinearStream({ + emit ISablierLockupLinear.CreateLockupLinearStream({ streamId: expectedStreamId, commonParams: defaults.lockupCreateEvent(timestamps), cliffTime: cliffTime, @@ -77,8 +73,7 @@ contract CreateWithDurationsLL_Integration_Concrete_Test is Lockup_Linear_Integr assertEq(lockup.getSender(streamId), users.sender, "sender"); assertEq(lockup.getStartTime(streamId), timestamps.start, "startTime"); assertEq(lockup.getUnderlyingToken(streamId), dai, "underlyingToken"); - assertEq(lockup.getUnlockAmounts(streamId).start, _defaultParams.unlockAmounts.start, "unlockAmounts.start"); - assertEq(lockup.getUnlockAmounts(streamId).cliff, _defaultParams.unlockAmounts.cliff, "unlockAmounts.cliff"); + assertEq(lockup.getUnlockAmounts(streamId), _defaultParams.unlockAmounts); assertFalse(lockup.wasCanceled(streamId), "wasCanceled"); // Assert that the stream's status is "STREAMING". diff --git a/tests/integration/concrete/lockup-linear/create-with-timestamps-ll/createWithTimestampsLL.t.sol b/tests/integration/concrete/lockup-linear/create-with-timestamps-ll/createWithTimestampsLL.t.sol index b6904e05e..f4c501b18 100644 --- a/tests/integration/concrete/lockup-linear/create-with-timestamps-ll/createWithTimestampsLL.t.sol +++ b/tests/integration/concrete/lockup-linear/create-with-timestamps-ll/createWithTimestampsLL.t.sol @@ -4,14 +4,14 @@ pragma solidity >=0.8.22 <0.9.0; import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; +import { ISablierLockupLinear } from "src/interfaces/ISablierLockupLinear.sol"; import { Errors } from "src/libraries/Errors.sol"; -import { Lockup } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; import { CreateWithTimestamps_Integration_Concrete_Test, Integration_Test -} from "../../lockup-base/create-with-timestamps/createWithTimestamps.t.sol"; +} from "../../lockup/create-with-timestamps/createWithTimestamps.t.sol"; contract CreateWithTimestampsLL_Integration_Concrete_Test is CreateWithTimestamps_Integration_Concrete_Test { function setUp() public override { @@ -23,11 +23,11 @@ contract CreateWithTimestampsLL_Integration_Concrete_Test is CreateWithTimestamp external whenNoDelegateCall whenShapeNotExceed32Bytes - whenBrokerFeeNotExceedMaxValue whenSenderNotZeroAddress whenRecipientNotZeroAddress whenDepositAmountNotZero whenStartTimeNotZero + whenTokenNotNativeToken whenTokenContract whenCliffTimeZero { @@ -44,11 +44,11 @@ contract CreateWithTimestampsLL_Integration_Concrete_Test is CreateWithTimestamp external whenNoDelegateCall whenShapeNotExceed32Bytes - whenBrokerFeeNotExceedMaxValue whenSenderNotZeroAddress whenRecipientNotZeroAddress whenDepositAmountNotZero whenStartTimeNotZero + whenTokenNotNativeToken whenTokenContract whenCliffTimeZero { @@ -69,27 +69,27 @@ contract CreateWithTimestampsLL_Integration_Concrete_Test is CreateWithTimestamp external whenNoDelegateCall whenShapeNotExceed32Bytes - whenBrokerFeeNotExceedMaxValue whenSenderNotZeroAddress whenRecipientNotZeroAddress whenDepositAmountNotZero whenStartTimeNotZero + whenTokenNotNativeToken whenTokenContract whenCliffTimeZero { uint40 cliffTime = 0; - _testCreateWithTimestampsLL(address(dai), cliffTime); + _testCreateWithTimestampsLL(cliffTime); } function test_RevertWhen_StartTimeNotLessThanCliffTime() external whenNoDelegateCall whenShapeNotExceed32Bytes - whenBrokerFeeNotExceedMaxValue whenSenderNotZeroAddress whenRecipientNotZeroAddress whenDepositAmountNotZero whenStartTimeNotZero + whenTokenNotNativeToken whenTokenContract whenCliffTimeNotZero { @@ -111,11 +111,11 @@ contract CreateWithTimestampsLL_Integration_Concrete_Test is CreateWithTimestamp external whenNoDelegateCall whenShapeNotExceed32Bytes - whenBrokerFeeNotExceedMaxValue whenSenderNotZeroAddress whenRecipientNotZeroAddress whenDepositAmountNotZero whenStartTimeNotZero + whenTokenNotNativeToken whenTokenContract whenCliffTimeNotZero whenStartTimeLessThanCliffTime @@ -136,11 +136,11 @@ contract CreateWithTimestampsLL_Integration_Concrete_Test is CreateWithTimestamp external whenNoDelegateCall whenShapeNotExceed32Bytes - whenBrokerFeeNotExceedMaxValue whenSenderNotZeroAddress whenRecipientNotZeroAddress whenDepositAmountNotZero whenStartTimeNotZero + whenTokenNotNativeToken whenTokenContract whenCliffTimeNotZero whenStartTimeLessThanCliffTime @@ -162,73 +162,101 @@ contract CreateWithTimestampsLL_Integration_Concrete_Test is CreateWithTimestamp external whenNoDelegateCall whenShapeNotExceed32Bytes - whenBrokerFeeNotExceedMaxValue whenSenderNotZeroAddress whenRecipientNotZeroAddress whenDepositAmountNotZero whenStartTimeNotZero + whenTokenNotNativeToken whenTokenContract whenCliffTimeNotZero whenStartTimeLessThanCliffTime whenCliffTimeLessThanEndTime whenUnlockAmountsSumNotExceedDepositAmount { - _testCreateWithTimestampsLL(address(usdt), _defaultParams.cliffTime); + IERC20 _usdt = IERC20(address(usdt)); + + // Update the default parameters. + _defaultParams.createWithTimestamps.token = _usdt; + _defaultParams.createWithTimestamps.depositAmount = defaults.DEPOSIT_AMOUNT_6D(); + _defaultParams.unlockAmounts.cliff = defaults.CLIFF_AMOUNT_6D(); + + uint256 previousAggregateAmount = lockup.aggregateAmount(_usdt); + uint256 expectedStreamId = lockup.nextStreamId(); + + // It should perform the ERC-20 transfers. + expectCallToTransferFrom({ + token: _usdt, + from: users.sender, + to: address(lockup), + value: defaults.DEPOSIT_AMOUNT_6D() + }); + + // It should emit {MetadataUpdate} and {CreateLockupLinearStream} events. + vm.expectEmit({ emitter: address(lockup) }); + emit IERC4906.MetadataUpdate({ _tokenId: expectedStreamId }); + vm.expectEmit({ emitter: address(lockup) }); + emit ISablierLockupLinear.CreateLockupLinearStream({ + streamId: expectedStreamId, + commonParams: defaults.lockupCreateEvent(_usdt, defaults.DEPOSIT_AMOUNT_6D()), + cliffTime: _defaultParams.cliffTime, + unlockAmounts: _defaultParams.unlockAmounts + }); + + // Create the stream. + uint256 streamId = createDefaultStream(); + + // It should create the stream. + assertEqStream(streamId, _usdt); + assertEq(lockup.getCliffTime(streamId), _defaultParams.cliffTime, "cliffTime"); + assertEq(lockup.getLockupModel(streamId), Lockup.Model.LOCKUP_LINEAR); + assertEq(lockup.getUnlockAmounts(streamId), _defaultParams.unlockAmounts); + assertEq( + lockup.aggregateAmount(_usdt), previousAggregateAmount + defaults.DEPOSIT_AMOUNT_6D(), "aggregateAmount" + ); } function test_WhenTokenNotMissERC20ReturnValue() external whenNoDelegateCall whenShapeNotExceed32Bytes - whenBrokerFeeNotExceedMaxValue whenSenderNotZeroAddress whenRecipientNotZeroAddress whenDepositAmountNotZero whenStartTimeNotZero + whenTokenNotNativeToken whenTokenContract whenCliffTimeNotZero whenStartTimeLessThanCliffTime whenCliffTimeLessThanEndTime whenUnlockAmountsSumNotExceedDepositAmount { - _testCreateWithTimestampsLL(address(dai), _defaultParams.cliffTime); + _testCreateWithTimestampsLL(_defaultParams.cliffTime); } - /// @dev Shared logic between {test_WhenStartTimeLessThanEndTime}, {test_WhenTokenMissesERC20ReturnValue} and - /// {test_WhenTokenNotMissERC20ReturnValue}. - function _testCreateWithTimestampsLL(address token, uint40 cliffTime) private { - // Make the Sender the stream's funder. - address funder = users.sender; - uint256 expectedStreamId = lockup.nextStreamId(); - - // Set the default parameters. - _defaultParams.createWithTimestamps.token = IERC20(token); + /// @dev Shared logic between {test_WhenStartTimeLessThanEndTime} and {test_WhenTokenMissesERC20ReturnValue}. + function _testCreateWithTimestampsLL(uint40 cliffTime) private { + // Update the default parameters. _defaultParams.unlockAmounts.cliff = cliffTime == 0 ? 0 : _defaultParams.unlockAmounts.cliff; _defaultParams.cliffTime = cliffTime; + uint256 previousAggregateAmount = lockup.aggregateAmount(dai); + uint256 expectedStreamId = lockup.nextStreamId(); + // It should perform the ERC-20 transfers. expectCallToTransferFrom({ - token: IERC20(token), - from: funder, + token: dai, + from: users.sender, to: address(lockup), value: defaults.DEPOSIT_AMOUNT() }); - // Expect the broker fee to be paid to the broker. - expectCallToTransferFrom({ - token: IERC20(token), - from: funder, - to: users.broker, - value: defaults.BROKER_FEE_AMOUNT() - }); - // It should emit {MetadataUpdate} and {CreateLockupLinearStream} events. vm.expectEmit({ emitter: address(lockup) }); emit IERC4906.MetadataUpdate({ _tokenId: expectedStreamId }); vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockup.CreateLockupLinearStream({ + emit ISablierLockupLinear.CreateLockupLinearStream({ streamId: expectedStreamId, - commonParams: defaults.lockupCreateEvent(IERC20(token)), + commonParams: defaults.lockupCreateEvent(dai, defaults.DEPOSIT_AMOUNT()), cliffTime: cliffTime, unlockAmounts: _defaultParams.unlockAmounts }); @@ -237,11 +265,10 @@ contract CreateWithTimestampsLL_Integration_Concrete_Test is CreateWithTimestamp uint256 streamId = createDefaultStream(); // It should create the stream. - assertEqStream(streamId); + assertEqStream(streamId, dai); assertEq(lockup.getCliffTime(streamId), cliffTime, "cliffTime"); assertEq(lockup.getLockupModel(streamId), Lockup.Model.LOCKUP_LINEAR); - assertEq(lockup.getUnderlyingToken(streamId), IERC20(token), "underlyingToken"); - assertEq(lockup.getUnlockAmounts(streamId).start, _defaultParams.unlockAmounts.start, "unlockAmounts.start"); - assertEq(lockup.getUnlockAmounts(streamId).cliff, _defaultParams.unlockAmounts.cliff, "unlockAmounts.cliff"); + assertEq(lockup.getUnlockAmounts(streamId), _defaultParams.unlockAmounts); + assertEq(lockup.aggregateAmount(dai), previousAggregateAmount + defaults.DEPOSIT_AMOUNT(), "aggregateAmount"); } } diff --git a/tests/integration/concrete/lockup-linear/create-with-timestamps-ll/createWithTimestampsLL.tree b/tests/integration/concrete/lockup-linear/create-with-timestamps-ll/createWithTimestampsLL.tree index a2f021ad6..56d61d4ee 100644 --- a/tests/integration/concrete/lockup-linear/create-with-timestamps-ll/createWithTimestampsLL.tree +++ b/tests/integration/concrete/lockup-linear/create-with-timestamps-ll/createWithTimestampsLL.tree @@ -20,12 +20,14 @@ CreateWithTimestampsLL_Integration_Concrete_Test ├── when token misses ERC20 return value │ ├── it should create the stream │ ├── it should bump the next stream ID + │ ├── it should increase the aggregate amount │ ├── it should mint the NFT │ ├── it should emit {MetadataUpdate} and {CreateLockupLinearStream} events │ └── it should perform the ERC-20 transfers └── when token not miss ERC20 return value ├── it should create the stream ├── it should bump the next stream ID + ├── it should increase the aggregate amount ├── it should mint the NFT ├── it should emit {MetadataUpdate} and {CreateLockupLinearStream} events - └── it should perform the ERC-20 transfers \ No newline at end of file + └── it should perform the ERC-20 transfers diff --git a/tests/integration/concrete/lockup-linear/get-cliff-time/getCliffTime.t.sol b/tests/integration/concrete/lockup-linear/get-cliff-time/getCliffTime.t.sol index 62e370f51..a07a22614 100644 --- a/tests/integration/concrete/lockup-linear/get-cliff-time/getCliffTime.t.sol +++ b/tests/integration/concrete/lockup-linear/get-cliff-time/getCliffTime.t.sol @@ -2,13 +2,13 @@ pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; -import { Lockup } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; import { Lockup_Linear_Integration_Concrete_Test } from "../LockupLinear.t.sol"; contract GetCliffTime_Integration_Concrete_Test is Lockup_Linear_Integration_Concrete_Test { function test_RevertGiven_Null() external { - expectRevert_Null({ callData: abi.encodeCall(lockup.getCliffTime, nullStreamId) }); + expectRevert_Null({ callData: abi.encodeCall(lockup.getCliffTime, ids.nullStream) }); } function test_RevertGiven_NotLinearModel() external givenNotNull { @@ -16,14 +16,16 @@ contract GetCliffTime_Integration_Concrete_Test is Lockup_Linear_Integration_Con uint256 streamId = createDefaultStream(); vm.expectRevert( abi.encodeWithSelector( - Errors.SablierLockup_NotExpectedModel.selector, Lockup.Model.LOCKUP_TRANCHED, Lockup.Model.LOCKUP_LINEAR + Errors.SablierLockupState_NotExpectedModel.selector, + Lockup.Model.LOCKUP_TRANCHED, + Lockup.Model.LOCKUP_LINEAR ) ); lockup.getCliffTime(streamId); } function test_GivenLinearModel() external view givenNotNull { - uint40 actualCliffTime = lockup.getCliffTime(defaultStreamId); + uint40 actualCliffTime = lockup.getCliffTime(ids.defaultStream); uint40 expectedCliffTime = defaults.CLIFF_TIME(); assertEq(actualCliffTime, expectedCliffTime, "cliffTime"); } diff --git a/tests/integration/concrete/lockup-linear/get-unlock-amounts/getUnlockAmounts.t.sol b/tests/integration/concrete/lockup-linear/get-unlock-amounts/getUnlockAmounts.t.sol index 9b96fb96f..87bdba8f7 100644 --- a/tests/integration/concrete/lockup-linear/get-unlock-amounts/getUnlockAmounts.t.sol +++ b/tests/integration/concrete/lockup-linear/get-unlock-amounts/getUnlockAmounts.t.sol @@ -2,13 +2,14 @@ pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; -import { Lockup, LockupLinear } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; +import { LockupLinear } from "src/types/LockupLinear.sol"; import { Lockup_Linear_Integration_Concrete_Test } from "../LockupLinear.t.sol"; contract GetUnlockAmounts_Integration_Concrete_Test is Lockup_Linear_Integration_Concrete_Test { function test_RevertGiven_Null() external { - expectRevert_Null({ callData: abi.encodeCall(lockup.getUnlockAmounts, nullStreamId) }); + expectRevert_Null({ callData: abi.encodeCall(lockup.getUnlockAmounts, ids.nullStream) }); } function test_RevertGiven_NotLinearModel() external givenNotNull { @@ -16,7 +17,9 @@ contract GetUnlockAmounts_Integration_Concrete_Test is Lockup_Linear_Integration uint256 streamId = createDefaultStream(); vm.expectRevert( abi.encodeWithSelector( - Errors.SablierLockup_NotExpectedModel.selector, Lockup.Model.LOCKUP_TRANCHED, Lockup.Model.LOCKUP_LINEAR + Errors.SablierLockupState_NotExpectedModel.selector, + Lockup.Model.LOCKUP_TRANCHED, + Lockup.Model.LOCKUP_LINEAR ) ); lockup.getUnlockAmounts(streamId); @@ -26,14 +29,12 @@ contract GetUnlockAmounts_Integration_Concrete_Test is Lockup_Linear_Integration _defaultParams.unlockAmounts = defaults.unlockAmountsZero(); uint256 streamId = createDefaultStream(); LockupLinear.UnlockAmounts memory unlockAmounts = lockup.getUnlockAmounts(streamId); - assertEq(unlockAmounts.start, 0, "unlockAmounts.start"); - assertEq(unlockAmounts.cliff, 0, "unlockAmounts.cliff"); + assertEq(unlockAmounts, _defaultParams.unlockAmounts); } function test_GivenStartUnlockAmountZero() external view givenNotNull givenLinearModel givenOnlyOneAmountZero { - LockupLinear.UnlockAmounts memory unlockAmounts = lockup.getUnlockAmounts(defaultStreamId); - assertEq(unlockAmounts.start, 0, "unlockAmounts.start"); - assertEq(unlockAmounts.cliff, defaults.CLIFF_AMOUNT(), "unlockAmounts.cliff"); + LockupLinear.UnlockAmounts memory unlockAmounts = lockup.getUnlockAmounts(ids.defaultStream); + assertEq(unlockAmounts, _defaultParams.unlockAmounts); } function test_GivenStartUnlockAmountNotZero() external givenNotNull givenLinearModel givenOnlyOneAmountZero { @@ -41,15 +42,13 @@ contract GetUnlockAmounts_Integration_Concrete_Test is Lockup_Linear_Integration _defaultParams.unlockAmounts.cliff = 0; uint256 streamId = createDefaultStream(); LockupLinear.UnlockAmounts memory unlockAmounts = lockup.getUnlockAmounts(streamId); - assertEq(unlockAmounts.start, 1, "unlockAmounts.start"); - assertEq(unlockAmounts.cliff, 0, "unlockAmounts.cliff"); + assertEq(unlockAmounts, _defaultParams.unlockAmounts); } function test_GivenBothAmountsNotZero() external givenNotNull givenLinearModel { _defaultParams.unlockAmounts.start = 1; uint256 streamId = createDefaultStream(); LockupLinear.UnlockAmounts memory unlockAmounts = lockup.getUnlockAmounts(streamId); - assertEq(unlockAmounts.start, 1, "unlockAmounts.start"); - assertEq(unlockAmounts.cliff, defaults.CLIFF_AMOUNT(), "unlockAmounts.cliff"); + assertEq(unlockAmounts, _defaultParams.unlockAmounts); } } diff --git a/tests/integration/concrete/lockup-linear/streamed-amount-of/streamedAmountOf.t.sol b/tests/integration/concrete/lockup-linear/streamed-amount-of/streamedAmountOf.t.sol index 128039f68..a45aca9e7 100644 --- a/tests/integration/concrete/lockup-linear/streamed-amount-of/streamedAmountOf.t.sol +++ b/tests/integration/concrete/lockup-linear/streamed-amount-of/streamedAmountOf.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { StreamedAmountOf_Integration_Concrete_Test } from "../../lockup-base/streamed-amount-of/streamedAmountOf.t.sol"; +import { StreamedAmountOf_Integration_Concrete_Test } from "../../lockup/streamed-amount-of/streamedAmountOf.t.sol"; import { Lockup_Linear_Integration_Concrete_Test, Integration_Test } from "./../LockupLinear.t.sol"; contract StreamedAmountOf_Lockup_Linear_Integration_Concrete_Test is @@ -33,14 +33,14 @@ contract StreamedAmountOf_Lockup_Linear_Integration_Concrete_Test is function test_GivenCliffTimeInFuture_Zero() external givenSTREAMINGStatus givenCliffTimeNotZero { vm.warp({ newTimestamp: defaults.CLIFF_TIME() - 1 }); - uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); + uint128 actualStreamedAmount = lockup.streamedAmountOf(ids.defaultStream); uint128 expectedStreamedAmount = 0; assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } function test_GivenCliffTimeInPresent() external givenSTREAMINGStatus givenCliffTimeNotZero { vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); - uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); + uint128 actualStreamedAmount = lockup.streamedAmountOf(ids.defaultStream); uint128 expectedStreamedAmount = defaults.CLIFF_AMOUNT(); assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } @@ -66,7 +66,7 @@ contract StreamedAmountOf_Lockup_Linear_Integration_Concrete_Test is vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); uint128 actualStreamedAmount = lockup.streamedAmountOf(streamId); - uint128 expectedStreamedAmount = calculateLockupLinearStreamedAmount( + uint128 expectedStreamedAmount = calculateStreamedAmountLL( _defaultParams.createWithTimestamps.timestamps.start, _defaultParams.cliffTime, _defaultParams.createWithTimestamps.timestamps.end, @@ -85,7 +85,7 @@ contract StreamedAmountOf_Lockup_Linear_Integration_Concrete_Test is givenNoStartAmount { vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); - uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); + uint128 actualStreamedAmount = lockup.streamedAmountOf(ids.defaultStream); uint128 expectedStreamedAmount = defaults.STREAMED_AMOUNT_26_PERCENT(); assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } diff --git a/tests/integration/concrete/lockup-linear/withdrawable-amount-of/withdrawableAmountOf.t.sol b/tests/integration/concrete/lockup-linear/withdrawable-amount-of/withdrawableAmountOf.t.sol index c4c57167b..3bfd101ef 100644 --- a/tests/integration/concrete/lockup-linear/withdrawable-amount-of/withdrawableAmountOf.t.sol +++ b/tests/integration/concrete/lockup-linear/withdrawable-amount-of/withdrawableAmountOf.t.sol @@ -2,7 +2,7 @@ pragma solidity >=0.8.22 <0.9.0; import { WithdrawableAmountOf_Integration_Concrete_Test } from - "../../lockup-base/withdrawable-amount-of/withdrawableAmountOf.t.sol"; + "../../lockup/withdrawable-amount-of/withdrawableAmountOf.t.sol"; import { Lockup_Linear_Integration_Concrete_Test, Integration_Test } from "./../LockupLinear.t.sol"; contract WithdrawableAmountOf_Lockup_Linear_Integration_Concrete_Test is @@ -15,21 +15,25 @@ contract WithdrawableAmountOf_Lockup_Linear_Integration_Concrete_Test is function test_GivenCliffTimeInFuture() external givenSTREAMINGStatus { vm.warp({ newTimestamp: defaults.CLIFF_TIME() - 1 }); - uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); + uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(ids.defaultStream); uint128 expectedWithdrawableAmount = 0; assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); } function test_GivenNoPreviousWithdrawals() external givenSTREAMINGStatus givenCliffTimeNotInFuture { - uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); + uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(ids.defaultStream); uint128 expectedWithdrawableAmount = defaults.STREAMED_AMOUNT_26_PERCENT(); assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); } function test_GivenPreviousWithdrawal() external givenSTREAMINGStatus givenCliffTimeNotInFuture { - lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: defaults.STREAMED_AMOUNT_26_PERCENT() }); + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: ids.defaultStream, + to: users.recipient, + amount: defaults.STREAMED_AMOUNT_26_PERCENT() + }); - uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); + uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(ids.defaultStream); uint128 expectedWithdrawableAmount = 0; assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); } diff --git a/tests/integration/concrete/lockup-tranched/LockupTranched.t.sol b/tests/integration/concrete/lockup-tranched/LockupTranched.t.sol index f1f55da2a..a54f59274 100644 --- a/tests/integration/concrete/lockup-tranched/LockupTranched.t.sol +++ b/tests/integration/concrete/lockup-tranched/LockupTranched.t.sol @@ -1,14 +1,13 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { Lockup } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; import { Integration_Test } from "../../Integration.t.sol"; -import { Cancel_Integration_Concrete_Test } from "../lockup-base/cancel/cancel.t.sol"; -import { RefundableAmountOf_Integration_Concrete_Test } from - "../lockup-base/refundable-amount-of/refundableAmountOf.t.sol"; -import { Renounce_Integration_Concrete_Test } from "../lockup-base/renounce/renounce.t.sol"; -import { Withdraw_Integration_Concrete_Test } from "../lockup-base/withdraw/withdraw.t.sol"; +import { Cancel_Integration_Concrete_Test } from "../lockup/cancel/cancel.t.sol"; +import { RefundableAmountOf_Integration_Concrete_Test } from "../lockup/refundable-amount-of/refundableAmountOf.t.sol"; +import { Renounce_Integration_Concrete_Test } from "../lockup/renounce/renounce.t.sol"; +import { Withdraw_Integration_Concrete_Test } from "../lockup/withdraw/withdraw.t.sol"; abstract contract Lockup_Tranched_Integration_Concrete_Test is Integration_Test { function setUp() public virtual override { diff --git a/tests/integration/concrete/lockup-tranched/create-with-durations-lt/createWithDurationsLT.t.sol b/tests/integration/concrete/lockup-tranched/create-with-durations-lt/createWithDurationsLT.t.sol index 3848fe673..166f866fb 100644 --- a/tests/integration/concrete/lockup-tranched/create-with-durations-lt/createWithDurationsLT.t.sol +++ b/tests/integration/concrete/lockup-tranched/create-with-durations-lt/createWithDurationsLT.t.sol @@ -3,9 +3,10 @@ pragma solidity >=0.8.22 <0.9.0; import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; -import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; +import { ISablierLockupTranched } from "src/interfaces/ISablierLockupTranched.sol"; import { Errors } from "src/libraries/Errors.sol"; -import { Lockup, LockupTranched } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; +import { LockupTranched } from "src/types/LockupTranched.sol"; import { Lockup_Tranched_Integration_Concrete_Test } from "./../LockupTranched.t.sol"; @@ -38,17 +39,7 @@ contract CreateWithDurationsLT_Integration_Concrete_Test is Lockup_Tranched_Inte }); } - function test_RevertWhen_TrancheCountExceedsMaxValue() external whenNoDelegateCall { - LockupTranched.TrancheWithDuration[] memory tranches = new LockupTranched.TrancheWithDuration[](25_000); - vm.expectRevert(abi.encodeWithSelector(Errors.SablierHelpers_TrancheCountTooHigh.selector, 25_000)); - createDefaultStreamWithDurations(tranches); - } - - function test_RevertWhen_FirstIndexHasZeroDuration() - external - whenNoDelegateCall - whenTrancheCountNotExceedMaxValue - { + function test_RevertWhen_FirstIndexHasZeroDuration() external whenNoDelegateCall { uint40 startTime = getBlockTimestamp(); LockupTranched.TrancheWithDuration[] memory tranches = defaults.tranchesWithDurations(); uint256 index = 1; @@ -67,7 +58,6 @@ contract CreateWithDurationsLT_Integration_Concrete_Test is Lockup_Tranched_Inte function test_RevertWhen_StartTimeExceedsFirstTimestamp() external whenNoDelegateCall - whenTrancheCountNotExceedMaxValue whenFirstIndexHasNonZeroDuration whenTimestampsCalculationOverflows { @@ -89,7 +79,6 @@ contract CreateWithDurationsLT_Integration_Concrete_Test is Lockup_Tranched_Inte function test_RevertWhen_TimestampsNotStrictlyIncreasing() external whenNoDelegateCall - whenTrancheCountNotExceedMaxValue whenFirstIndexHasNonZeroDuration whenTimestampsCalculationOverflows whenStartTimeNotExceedsFirstTimestamp @@ -118,14 +107,7 @@ contract CreateWithDurationsLT_Integration_Concrete_Test is Lockup_Tranched_Inte } } - function test_WhenTimestampsCalculationNotOverflow() - external - whenNoDelegateCall - whenTrancheCountNotExceedMaxValue - whenFirstIndexHasNonZeroDuration - { - // Make the Sender the stream's funder - address funder = users.sender; + function test_WhenTimestampsCalculationNotOverflow() external whenNoDelegateCall whenFirstIndexHasNonZeroDuration { uint256 expectedStreamId = lockup.nextStreamId(); // Declare the timestamps. @@ -139,16 +121,13 @@ contract CreateWithDurationsLT_Integration_Concrete_Test is Lockup_Tranched_Inte tranches[1].timestamp = tranches[0].timestamp + tranchesWithDurations[1].duration; // It should perform the ERC-20 transfers. - expectCallToTransferFrom({ from: funder, to: address(lockup), value: defaults.DEPOSIT_AMOUNT() }); - - // Expect the broker fee to be paid to the broker. - expectCallToTransferFrom({ from: funder, to: users.broker, value: defaults.BROKER_FEE_AMOUNT() }); + expectCallToTransferFrom({ from: users.sender, to: address(lockup), value: defaults.DEPOSIT_AMOUNT() }); // It should emit {MetadataUpdate} and {CreateLockupTranchedStream} events. vm.expectEmit({ emitter: address(lockup) }); emit IERC4906.MetadataUpdate({ _tokenId: expectedStreamId }); vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockup.CreateLockupTranchedStream({ + emit ISablierLockupTranched.CreateLockupTranchedStream({ streamId: expectedStreamId, commonParams: defaults.lockupCreateEvent(timestamps), tranches: tranches @@ -168,10 +147,11 @@ contract CreateWithDurationsLT_Integration_Concrete_Test is Lockup_Tranched_Inte assertEq(lockup.getRecipient(streamId), users.recipient, "recipient"); assertEq(lockup.getSender(streamId), users.sender, "sender"); assertEq(lockup.getStartTime(streamId), timestamps.start, "startTime"); - assertEq(lockup.getTranches(streamId), tranches); assertEq(lockup.getUnderlyingToken(streamId), dai, "underlyingToken"); assertFalse(lockup.wasCanceled(streamId), "wasCanceled"); + assertEq(lockup.getTranches(streamId), tranches); + // Assert that the stream's status is "STREAMING". Lockup.Status actualStatus = lockup.statusOf(streamId); Lockup.Status expectedStatus = Lockup.Status.STREAMING; diff --git a/tests/integration/concrete/lockup-tranched/create-with-durations-lt/createWithDurationsLT.tree b/tests/integration/concrete/lockup-tranched/create-with-durations-lt/createWithDurationsLT.tree index 375ee923e..0dc22f525 100644 --- a/tests/integration/concrete/lockup-tranched/create-with-durations-lt/createWithDurationsLT.tree +++ b/tests/integration/concrete/lockup-tranched/create-with-durations-lt/createWithDurationsLT.tree @@ -2,21 +2,18 @@ CreateWithDurationsLT_Integration_Concrete_Test ├── when delegate call │ └── it should revert └── when no delegate call - ├── when tranche count exceeds max value + ├── when first index has zero duration │ └── it should revert - └── when tranche count not exceed max value - ├── when first index has zero duration - │ └── it should revert - └── when first index has non zero duration - ├── when timestamps calculation overflows - │ ├── when start time exceeds first timestamp - │ │ └── it should revert - │ └── when start time not exceeds first timestamp - │ └── when timestamps not strictly increasing - │ └── it should revert - └── when timestamps calculation not overflow - ├── it should create the stream - ├── it should bump the next stream ID - ├── it should mint the NFT - ├── it should emit {CreateLockupDynamicStream} and {MetadataUpdate} events - └── it should perform the ERC-20 transfers + └── when first index has non zero duration + ├── when timestamps calculation overflows + │ ├── when start time exceeds first timestamp + │ │ └── it should revert + │ └── when start time not exceeds first timestamp + │ └── when timestamps not strictly increasing + │ └── it should revert + └── when timestamps calculation not overflow + ├── it should create the stream + ├── it should bump the next stream ID + ├── it should mint the NFT + ├── it should emit {CreateLockupDynamicStream} and {MetadataUpdate} events + └── it should perform the ERC-20 transfers diff --git a/tests/integration/concrete/lockup-tranched/create-with-timestamps-lt/createWithTimestampsLT.t.sol b/tests/integration/concrete/lockup-tranched/create-with-timestamps-lt/createWithTimestampsLT.t.sol index c90811270..8f3c0213e 100644 --- a/tests/integration/concrete/lockup-tranched/create-with-timestamps-lt/createWithTimestampsLT.t.sol +++ b/tests/integration/concrete/lockup-tranched/create-with-timestamps-lt/createWithTimestampsLT.t.sol @@ -5,14 +5,15 @@ import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { stdError } from "forge-std/src/StdError.sol"; -import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; +import { ISablierLockupTranched } from "src/interfaces/ISablierLockupTranched.sol"; import { Errors } from "src/libraries/Errors.sol"; -import { Lockup, LockupTranched } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; +import { LockupTranched } from "src/types/LockupTranched.sol"; import { CreateWithTimestamps_Integration_Concrete_Test, Integration_Test -} from "../../lockup-base/create-with-timestamps/createWithTimestamps.t.sol"; +} from "../../lockup/create-with-timestamps/createWithTimestamps.t.sol"; contract CreateWithTimestampsLT_Integration_Concrete_Test is CreateWithTimestamps_Integration_Concrete_Test { function setUp() public virtual override { @@ -28,42 +29,25 @@ contract CreateWithTimestampsLT_Integration_Concrete_Test is CreateWithTimestamp whenRecipientNotZeroAddress whenDepositAmountNotZero whenStartTimeNotZero + whenTokenNotNativeToken + whenTokenContract { LockupTranched.Tranche[] memory tranches; vm.expectRevert(Errors.SablierHelpers_TrancheCountZero.selector); lockup.createWithTimestampsLT(_defaultParams.createWithTimestamps, tranches); } - function test_RevertWhen_TrancheCountExceedsMaxValue() - external - whenNoDelegateCall - whenShapeNotExceed32Bytes - whenBrokerFeeNotExceedMaxValue - whenSenderNotZeroAddress - whenRecipientNotZeroAddress - whenDepositAmountNotZero - whenStartTimeNotZero - whenTokenContract - whenTrancheCountNotZero - { - uint256 trancheCount = defaults.MAX_COUNT() + 1; - LockupTranched.Tranche[] memory tranches = new LockupTranched.Tranche[](trancheCount); - vm.expectRevert(abi.encodeWithSelector(Errors.SablierHelpers_TrancheCountTooHigh.selector, trancheCount)); - lockup.createWithTimestampsLT(_defaultParams.createWithTimestamps, tranches); - } - function test_RevertWhen_TrancheAmountsSumOverflows() external whenNoDelegateCall whenShapeNotExceed32Bytes - whenBrokerFeeNotExceedMaxValue whenSenderNotZeroAddress whenRecipientNotZeroAddress whenDepositAmountNotZero whenStartTimeNotZero + whenTokenNotNativeToken whenTokenContract whenTrancheCountNotZero - whenTrancheCountNotExceedMaxValue { _defaultParams.tranches[0].amount = MAX_UINT128; _defaultParams.tranches[1].amount = 1; @@ -75,14 +59,13 @@ contract CreateWithTimestampsLT_Integration_Concrete_Test is CreateWithTimestamp external whenNoDelegateCall whenShapeNotExceed32Bytes - whenBrokerFeeNotExceedMaxValue whenSenderNotZeroAddress whenRecipientNotZeroAddress whenDepositAmountNotZero whenStartTimeNotZero + whenTokenNotNativeToken whenTokenContract whenTrancheCountNotZero - whenTrancheCountNotExceedMaxValue whenTrancheAmountsSumNotOverflow { // Change the timestamp of the first tranche. @@ -106,14 +89,13 @@ contract CreateWithTimestampsLT_Integration_Concrete_Test is CreateWithTimestamp external whenNoDelegateCall whenShapeNotExceed32Bytes - whenBrokerFeeNotExceedMaxValue whenSenderNotZeroAddress whenRecipientNotZeroAddress whenDepositAmountNotZero whenStartTimeNotZero + whenTokenNotNativeToken whenTokenContract whenTrancheCountNotZero - whenTrancheCountNotExceedMaxValue whenTrancheAmountsSumNotOverflow { // Change the timestamp of the first tranche. @@ -130,20 +112,45 @@ contract CreateWithTimestampsLT_Integration_Concrete_Test is CreateWithTimestamp createDefaultStream(); } + function test_RevertWhen_EndTimeNotEqualLastTimestamp() + external + whenNoDelegateCall + whenShapeNotExceed32Bytes + whenSenderNotZeroAddress + whenRecipientNotZeroAddress + whenDepositAmountNotZero + whenStartTimeNotZero + whenTokenNotNativeToken + whenTokenContract + whenTrancheCountNotZero + whenTrancheAmountsSumNotOverflow + whenStartTimeLessThanFirstTimestamp + { + _defaultParams.createWithTimestamps.timestamps.end = defaults.END_TIME() + 1 seconds; + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierHelpers_EndTimeNotEqualToLastTrancheTimestamp.selector, + _defaultParams.createWithTimestamps.timestamps.end, + _defaultParams.createWithTimestamps.timestamps.end - 1 + ) + ); + createDefaultStream(); + } + function test_RevertWhen_TimestampsNotStrictlyIncreasing() external whenNoDelegateCall whenShapeNotExceed32Bytes - whenBrokerFeeNotExceedMaxValue whenSenderNotZeroAddress whenRecipientNotZeroAddress whenDepositAmountNotZero whenStartTimeNotZero + whenTokenNotNativeToken whenTokenContract whenTrancheCountNotZero - whenTrancheCountNotExceedMaxValue whenTrancheAmountsSumNotOverflow whenStartTimeLessThanFirstTimestamp + whenEndTimeEqualsLastTimestamp { // Swap the tranche timestamps. // LockupTranched.Tranche[] memory tranches = defaults.tranches(); @@ -168,25 +175,24 @@ contract CreateWithTimestampsLT_Integration_Concrete_Test is CreateWithTimestamp external whenNoDelegateCall whenShapeNotExceed32Bytes - whenBrokerFeeNotExceedMaxValue whenSenderNotZeroAddress whenRecipientNotZeroAddress whenDepositAmountNotZero whenStartTimeNotZero + whenTokenNotNativeToken whenTokenContract whenTrancheCountNotZero - whenTrancheCountNotExceedMaxValue whenTrancheAmountsSumNotOverflow whenStartTimeLessThanFirstTimestamp + whenEndTimeEqualsLastTimestamp whenTimestampsStrictlyIncreasing { - resetPrank({ msgSender: users.sender }); + setMsgSender(users.sender); // Adjust the default deposit amount. uint128 defaultDepositAmount = defaults.DEPOSIT_AMOUNT(); uint128 depositAmount = defaultDepositAmount + 100; - _defaultParams.createWithTimestamps.broker = defaults.brokerNull(); - _defaultParams.createWithTimestamps.totalAmount = depositAmount; + _defaultParams.createWithTimestamps.depositAmount = depositAmount; // Expect the relevant error to be thrown. vm.expectRevert( @@ -203,82 +209,106 @@ contract CreateWithTimestampsLT_Integration_Concrete_Test is CreateWithTimestamp external whenNoDelegateCall whenShapeNotExceed32Bytes - whenBrokerFeeNotExceedMaxValue whenSenderNotZeroAddress whenRecipientNotZeroAddress whenDepositAmountNotZero whenStartTimeNotZero + whenTokenNotNativeToken whenTokenContract whenTrancheCountNotZero - whenTrancheCountNotExceedMaxValue whenTrancheAmountsSumNotOverflow whenStartTimeLessThanFirstTimestamp + whenEndTimeEqualsLastTimestamp whenTimestampsStrictlyIncreasing whenDepositAmountEqualsTrancheAmountsSum { - _testCreateWithTimestampsLT(address(usdt)); + IERC20 _usdt = IERC20(address(usdt)); + + // Update the default parameters. + _defaultParams.createWithTimestamps.depositAmount = defaults.DEPOSIT_AMOUNT_6D(); + _defaultParams.createWithTimestamps.token = _usdt; + _defaultParams.tranches[0].amount = 2600e6; + _defaultParams.tranches[1].amount = 7400e6; + + uint256 previousAggregateAmount = lockup.aggregateAmount(_usdt); + uint256 expectedStreamId = lockup.nextStreamId(); + + // It should perform the ERC-20 transfers. + expectCallToTransferFrom({ + token: _usdt, + from: users.sender, + to: address(lockup), + value: defaults.DEPOSIT_AMOUNT_6D() + }); + + // It should emit {CreateLockupTranchedStream} and {MetadataUpdate} events. + vm.expectEmit({ emitter: address(lockup) }); + emit IERC4906.MetadataUpdate({ _tokenId: expectedStreamId }); + vm.expectEmit({ emitter: address(lockup) }); + emit ISablierLockupTranched.CreateLockupTranchedStream({ + streamId: expectedStreamId, + commonParams: defaults.lockupCreateEvent(_usdt, defaults.DEPOSIT_AMOUNT_6D()), + tranches: _defaultParams.tranches + }); + + // It should create the stream. + uint256 streamId = createDefaultStream(); + + // It should create the stream. + assertEqStream(streamId, _usdt); + assertEq(lockup.getLockupModel(streamId), Lockup.Model.LOCKUP_TRANCHED); + assertEq( + lockup.aggregateAmount(_usdt), previousAggregateAmount + defaults.DEPOSIT_AMOUNT_6D(), "aggregateAmount" + ); + assertEq(lockup.getTranches(streamId), _defaultParams.tranches); } function test_WhenTokenNotMissERC20ReturnValue() external whenNoDelegateCall whenShapeNotExceed32Bytes - whenBrokerFeeNotExceedMaxValue whenSenderNotZeroAddress whenRecipientNotZeroAddress whenDepositAmountNotZero whenStartTimeNotZero + whenTokenNotNativeToken whenTokenContract whenTrancheCountNotZero - whenTrancheCountNotExceedMaxValue whenTrancheAmountsSumNotOverflow whenStartTimeLessThanFirstTimestamp + whenEndTimeEqualsLastTimestamp whenTimestampsStrictlyIncreasing whenDepositAmountEqualsTrancheAmountsSum { - _testCreateWithTimestampsLT(address(dai)); - } - - /// @dev Shared logic between {test_CreateWithTimestamps_TokenMissingReturnValue} and {test_CreateWithTimestamps}. - function _testCreateWithTimestampsLT(address token) internal { - // Make the Sender the stream's funder. - address funder = users.sender; + uint256 previousAggregateAmount = lockup.aggregateAmount(dai); uint256 expectedStreamId = lockup.nextStreamId(); // It should perform the ERC-20 transfers. expectCallToTransferFrom({ - token: IERC20(token), - from: funder, + token: dai, + from: users.sender, to: address(lockup), value: defaults.DEPOSIT_AMOUNT() }); - // Expect the broker fee to be paid to the broker. - expectCallToTransferFrom({ - token: IERC20(token), - from: funder, - to: users.broker, - value: defaults.BROKER_FEE_AMOUNT() - }); - // It should emit {CreateLockupTranchedStream} and {MetadataUpdate} events. vm.expectEmit({ emitter: address(lockup) }); emit IERC4906.MetadataUpdate({ _tokenId: expectedStreamId }); vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockup.CreateLockupTranchedStream({ + emit ISablierLockupTranched.CreateLockupTranchedStream({ streamId: expectedStreamId, - commonParams: defaults.lockupCreateEvent(IERC20(token)), + commonParams: defaults.lockupCreateEvent(dai, defaults.DEPOSIT_AMOUNT()), tranches: defaults.tranches() }); // It should create the stream. - _defaultParams.createWithTimestamps.token = IERC20(token); uint256 streamId = createDefaultStream(); // It should create the stream. - assertEqStream(streamId); + assertEqStream(streamId, dai); assertEq(lockup.getLockupModel(streamId), Lockup.Model.LOCKUP_TRANCHED); + assertEq(lockup.aggregateAmount(dai), previousAggregateAmount + defaults.DEPOSIT_AMOUNT(), "aggregateAmount"); assertEq(lockup.getTranches(streamId), defaults.tranches()); - assertEq(lockup.getUnderlyingToken(streamId), IERC20(token), "underlyingToken"); + assertEq(lockup.getUnderlyingToken(streamId), dai, "underlyingToken"); } } diff --git a/tests/integration/concrete/lockup-tranched/create-with-timestamps-lt/createWithTimestampsLT.tree b/tests/integration/concrete/lockup-tranched/create-with-timestamps-lt/createWithTimestampsLT.tree index 4fc1f102b..9b7b01d69 100644 --- a/tests/integration/concrete/lockup-tranched/create-with-timestamps-lt/createWithTimestampsLT.tree +++ b/tests/integration/concrete/lockup-tranched/create-with-timestamps-lt/createWithTimestampsLT.tree @@ -3,17 +3,17 @@ CreateWithTimestampsLT_Integration_Concrete_Test ├── when tranche count zero │ └── it should revert └── when tranche count not zero - ├── when tranche count exceeds max value + ├── when tranche amounts sum overflows │ └── it should revert - └── when tranche count not exceed max value - ├── when tranche amounts sum overflows + └── when tranche amounts sum not overflow + ├── when start time greater than first timestamp │ └── it should revert - └── when tranche amounts sum not overflow - ├── when start time greater than first timestamp - │ └── it should revert - ├── when start time equals first timestamp + ├── when start time equals first timestamp + │ └── it should revert + └── when start time less than first timestamp + ├── when end time not equal last timestamp │ └── it should revert - └── when start time less than first timestamp + └── when end time equals last timestamp ├── when timestamps not strictly increasing │ └── it should revert └── when timestamps strictly increasing @@ -23,12 +23,14 @@ CreateWithTimestampsLT_Integration_Concrete_Test ├── when token misses ERC20 return value │ ├── it should create the stream │ ├── it should bump the next stream ID + │ ├── it should increase the aggregate amount │ ├── it should mint the NFT │ ├── it should emit {CreateLockupTranchedStream} and {MetadataUpdate} events │ └── it should perform the ERC-20 transfers └── when token not miss ERC20 return value ├── it should create the stream ├── it should bump the next stream ID + ├── it should increase the aggregate amount ├── it should mint the NFT ├── it should emit {CreateLockupTranchedStream} and {MetadataUpdate} events - └── it should perform the ERC-20 transfers \ No newline at end of file + └── it should perform the ERC-20 transfers diff --git a/tests/integration/concrete/lockup-tranched/get-tranches/getTranches.t.sol b/tests/integration/concrete/lockup-tranched/get-tranches/getTranches.t.sol index 92d00d34c..63a3336fd 100644 --- a/tests/integration/concrete/lockup-tranched/get-tranches/getTranches.t.sol +++ b/tests/integration/concrete/lockup-tranched/get-tranches/getTranches.t.sol @@ -2,13 +2,14 @@ pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; -import { Lockup, LockupTranched } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; +import { LockupTranched } from "src/types/LockupTranched.sol"; import { Lockup_Tranched_Integration_Concrete_Test } from "../LockupTranched.t.sol"; contract GetTranches_Integration_Concrete_Test is Lockup_Tranched_Integration_Concrete_Test { function test_RevertGiven_Null() external { - expectRevert_Null({ callData: abi.encodeCall(lockup.getTranches, nullStreamId) }); + expectRevert_Null({ callData: abi.encodeCall(lockup.getTranches, ids.nullStream) }); } function test_RevertGiven_NotTranchedModel() external givenNotNull { @@ -16,14 +17,16 @@ contract GetTranches_Integration_Concrete_Test is Lockup_Tranched_Integration_Co uint256 streamId = createDefaultStream(); vm.expectRevert( abi.encodeWithSelector( - Errors.SablierLockup_NotExpectedModel.selector, Lockup.Model.LOCKUP_LINEAR, Lockup.Model.LOCKUP_TRANCHED + Errors.SablierLockupState_NotExpectedModel.selector, + Lockup.Model.LOCKUP_LINEAR, + Lockup.Model.LOCKUP_TRANCHED ) ); lockup.getTranches(streamId); } function test_GivenTranchedModel() external givenNotNull { - LockupTranched.Tranche[] memory actualTranches = lockup.getTranches(defaultStreamId); + LockupTranched.Tranche[] memory actualTranches = lockup.getTranches(ids.defaultStream); LockupTranched.Tranche[] memory expectedTranches = defaults.tranches(); assertEq(actualTranches, expectedTranches); } diff --git a/tests/integration/concrete/lockup-tranched/streamed-amount-of/streamedAmountOf.t.sol b/tests/integration/concrete/lockup-tranched/streamed-amount-of/streamedAmountOf.t.sol index 08701b044..6059bacb8 100644 --- a/tests/integration/concrete/lockup-tranched/streamed-amount-of/streamedAmountOf.t.sol +++ b/tests/integration/concrete/lockup-tranched/streamed-amount-of/streamedAmountOf.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { StreamedAmountOf_Integration_Concrete_Test } from "../../lockup-base/streamed-amount-of/streamedAmountOf.t.sol"; +import { StreamedAmountOf_Integration_Concrete_Test } from "../../lockup/streamed-amount-of/streamedAmountOf.t.sol"; import { Lockup_Tranched_Integration_Concrete_Test, Integration_Test } from "./../LockupTranched.t.sol"; contract StreamedAmountOf_Lockup_Tranched_Integration_Concrete_Test is @@ -14,7 +14,7 @@ contract StreamedAmountOf_Lockup_Tranched_Integration_Concrete_Test is function test_GivenStartTimeInPresent() external givenSTREAMINGStatus { vm.warp({ newTimestamp: defaults.START_TIME() }); - uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); + uint128 actualStreamedAmount = lockup.streamedAmountOf(ids.defaultStream); uint128 expectedStreamedAmount = 0; assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } @@ -23,7 +23,7 @@ contract StreamedAmountOf_Lockup_Tranched_Integration_Concrete_Test is vm.warp({ newTimestamp: defaults.END_TIME() + 1 seconds }); // It should return the deposited amount. - uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); + uint128 actualStreamedAmount = lockup.streamedAmountOf(ids.defaultStream); uint128 expectedStreamedAmount = defaults.DEPOSIT_AMOUNT(); assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } @@ -37,7 +37,7 @@ contract StreamedAmountOf_Lockup_Tranched_Integration_Concrete_Test is vm.warp({ newTimestamp: defaults.START_TIME() + 1 seconds }); // It should return 0. - uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); + uint128 actualStreamedAmount = lockup.streamedAmountOf(ids.defaultStream); uint128 expectedStreamedAmount = 0; assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } @@ -51,7 +51,7 @@ contract StreamedAmountOf_Lockup_Tranched_Integration_Concrete_Test is vm.warp({ newTimestamp: defaults.END_TIME() - 1 seconds }); // It should return the correct streamed amount. - uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); + uint128 actualStreamedAmount = lockup.streamedAmountOf(ids.defaultStream); uint128 expectedStreamedAmount = defaults.tranches()[0].amount; assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } diff --git a/tests/integration/concrete/lockup-tranched/withdrawable-amount-of/withdrawableAmountOf.t.sol b/tests/integration/concrete/lockup-tranched/withdrawable-amount-of/withdrawableAmountOf.t.sol index ac159a91a..b5c83217f 100644 --- a/tests/integration/concrete/lockup-tranched/withdrawable-amount-of/withdrawableAmountOf.t.sol +++ b/tests/integration/concrete/lockup-tranched/withdrawable-amount-of/withdrawableAmountOf.t.sol @@ -2,7 +2,7 @@ pragma solidity >=0.8.22 <0.9.0; import { WithdrawableAmountOf_Integration_Concrete_Test } from - "../../lockup-base/withdrawable-amount-of/withdrawableAmountOf.t.sol"; + "../../lockup/withdrawable-amount-of/withdrawableAmountOf.t.sol"; import { Lockup_Tranched_Integration_Concrete_Test, Integration_Test } from "./../LockupTranched.t.sol"; contract WithdrawableAmountOf_Lockup_Tranched_Integration_Concrete_Test is @@ -15,7 +15,7 @@ contract WithdrawableAmountOf_Lockup_Tranched_Integration_Concrete_Test is function test_GivenStartTimeInPresent() external givenSTREAMINGStatus { vm.warp({ newTimestamp: defaults.START_TIME() }); - uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); + uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(ids.defaultStream); uint128 expectedWithdrawableAmount = 0; assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); } @@ -25,7 +25,7 @@ contract WithdrawableAmountOf_Lockup_Tranched_Integration_Concrete_Test is vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); // Run the test. - uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); + uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(ids.defaultStream); uint128 expectedWithdrawableAmount = defaults.tranches()[0].amount; assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); } @@ -35,10 +35,14 @@ contract WithdrawableAmountOf_Lockup_Tranched_Integration_Concrete_Test is vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); // Make the withdrawal. - lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: defaults.STREAMED_AMOUNT_26_PERCENT() }); + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: ids.defaultStream, + to: users.recipient, + amount: defaults.STREAMED_AMOUNT_26_PERCENT() + }); // Run the test. - uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); + uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(ids.defaultStream); uint128 expectedWithdrawableAmount = defaults.tranches()[0].amount - defaults.STREAMED_AMOUNT_26_PERCENT(); assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); diff --git a/tests/integration/concrete/lockup-base/allow-to-hook/allowToHook.t.sol b/tests/integration/concrete/lockup/allow-to-hook/allowToHook.t.sol similarity index 61% rename from tests/integration/concrete/lockup-base/allow-to-hook/allowToHook.t.sol rename to tests/integration/concrete/lockup/allow-to-hook/allowToHook.t.sol index 62323931b..a2ebefa50 100644 --- a/tests/integration/concrete/lockup-base/allow-to-hook/allowToHook.t.sol +++ b/tests/integration/concrete/lockup/allow-to-hook/allowToHook.t.sol @@ -1,37 +1,50 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; +import { Errors as EvmUtilsErrors } from "@sablier/evm-utils/src/libraries/Errors.sol"; + +import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; import { Errors } from "src/libraries/Errors.sol"; import { RecipientGood } from "../../../../mocks/Hooks.sol"; import { Integration_Test } from "../../../Integration.t.sol"; contract AllowToHook_Integration_Concrete_Test is Integration_Test { - function test_RevertWhen_CallerNotAdmin() external { + function setUp() public override { + Integration_Test.setUp(); + + // Set the comptroller as the caller for this test. + setMsgSender(address(comptroller)); + } + + function test_RevertWhen_CallerNotComptroller() external { // Make Eve the caller in this test. - resetPrank({ msgSender: users.eve }); + setMsgSender(users.eve); // Run the test. - vm.expectRevert(abi.encodeWithSelector(Errors.CallerNotAdmin.selector, users.admin, users.eve)); + vm.expectRevert( + abi.encodeWithSelector( + EvmUtilsErrors.Comptrollerable_CallerNotComptroller.selector, address(comptroller), users.eve + ) + ); lockup.allowToHook(users.eve); } - function test_RevertWhen_ProvidedAddressNotContract() external whenCallerAdmin { + function test_RevertWhen_ProvidedAddressNotContract() external whenCallerComptroller { address eoa = vm.addr({ privateKey: 1 }); - vm.expectRevert(abi.encodeWithSelector(Errors.SablierLockupBase_AllowToHookZeroCodeSize.selector, eoa)); + vm.expectRevert(); lockup.allowToHook(eoa); } function test_RevertWhen_ProvidedAddressNotReturnInterfaceId() external - whenCallerAdmin + whenCallerComptroller whenProvidedAddressContract { // Incorrect interface ID. address recipient = address(recipientInterfaceIDIncorrect); vm.expectRevert( - abi.encodeWithSelector(Errors.SablierLockupBase_AllowToHookUnsupportedInterface.selector, recipient) + abi.encodeWithSelector(Errors.SablierLockup_AllowToHookUnsupportedInterface.selector, recipient) ); lockup.allowToHook(recipient); @@ -41,13 +54,13 @@ contract AllowToHook_Integration_Concrete_Test is Integration_Test { lockup.allowToHook(recipient); } - function test_WhenProvidedAddressReturnsInterfaceId() external whenCallerAdmin whenProvidedAddressContract { - // Define a recipient that implementes the interface correctly. + function test_WhenProvidedAddressReturnsInterfaceId() external whenCallerComptroller whenProvidedAddressContract { + // Define a recipient that implements the interface correctly. RecipientGood recipientWithInterfaceId = new RecipientGood(); // It should emit a {AllowToHook} event. vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.AllowToHook(users.admin, address(recipientWithInterfaceId)); + emit ISablierLockup.AllowToHook(comptroller, address(recipientWithInterfaceId)); // Allow the provided address to hook. lockup.allowToHook(address(recipientWithInterfaceId)); diff --git a/tests/integration/concrete/lockup-base/allow-to-hook/allowToHook.tree b/tests/integration/concrete/lockup/allow-to-hook/allowToHook.tree similarity index 86% rename from tests/integration/concrete/lockup-base/allow-to-hook/allowToHook.tree rename to tests/integration/concrete/lockup/allow-to-hook/allowToHook.tree index 038794581..4d65da84e 100644 --- a/tests/integration/concrete/lockup-base/allow-to-hook/allowToHook.tree +++ b/tests/integration/concrete/lockup/allow-to-hook/allowToHook.tree @@ -1,7 +1,7 @@ AllowToHook_Integration_Concrete_Test -├── when caller not admin +├── when caller not comptroller │ └── it should revert -└── when caller admin +└── when caller comptroller ├── when provided address not contract │ └── it should revert └── when provided address contract diff --git a/tests/integration/concrete/lockup-base/burn/burn.t.sol b/tests/integration/concrete/lockup/burn/burn.t.sol similarity index 67% rename from tests/integration/concrete/lockup-base/burn/burn.t.sol rename to tests/integration/concrete/lockup/burn/burn.t.sol index 3d0df5184..3ba23fe69 100644 --- a/tests/integration/concrete/lockup-base/burn/burn.t.sol +++ b/tests/integration/concrete/lockup/burn/burn.t.sol @@ -9,113 +9,115 @@ import { Integration_Test } from "../../../Integration.t.sol"; contract Burn_Integration_Concrete_Test is Integration_Test { function test_RevertWhen_DelegateCall() external { - expectRevert_DelegateCall({ callData: abi.encodeCall(lockup.burn, defaultStreamId) }); + expectRevert_DelegateCall({ callData: abi.encodeCall(lockup.burn, ids.defaultStream) }); } function test_RevertGiven_Null() external whenNoDelegateCall { - expectRevert_Null({ callData: abi.encodeCall(lockup.burn, nullStreamId) }); + expectRevert_Null({ callData: abi.encodeCall(lockup.burn, ids.nullStream) }); } function test_RevertGiven_PENDINGStatus() external whenNoDelegateCall givenNotNull givenNotDepletedStream { - vm.warp({ newTimestamp: getBlockTimestamp() - 1 seconds }); - vm.expectRevert(abi.encodeWithSelector(Errors.SablierLockupBase_StreamNotDepleted.selector, defaultStreamId)); - lockup.burn(defaultStreamId); + rewind(1 seconds); + vm.expectRevert(abi.encodeWithSelector(Errors.SablierLockup_StreamNotDepleted.selector, ids.defaultStream)); + lockup.burn(ids.defaultStream); } function test_RevertGiven_STREAMINGStatus() external whenNoDelegateCall givenNotNull givenNotDepletedStream { vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); - vm.expectRevert(abi.encodeWithSelector(Errors.SablierLockupBase_StreamNotDepleted.selector, defaultStreamId)); - lockup.burn(defaultStreamId); + vm.expectRevert(abi.encodeWithSelector(Errors.SablierLockup_StreamNotDepleted.selector, ids.defaultStream)); + lockup.burn(ids.defaultStream); } function test_RevertGiven_SETTLEDStatus() external whenNoDelegateCall givenNotNull givenNotDepletedStream { vm.warp({ newTimestamp: defaults.END_TIME() }); - vm.expectRevert(abi.encodeWithSelector(Errors.SablierLockupBase_StreamNotDepleted.selector, defaultStreamId)); - lockup.burn(defaultStreamId); + vm.expectRevert(abi.encodeWithSelector(Errors.SablierLockup_StreamNotDepleted.selector, ids.defaultStream)); + lockup.burn(ids.defaultStream); } function test_RevertGiven_CANCELEDStatus() external whenNoDelegateCall givenNotNull givenNotDepletedStream { vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); - resetPrank({ msgSender: users.sender }); - lockup.cancel(defaultStreamId); - resetPrank({ msgSender: users.recipient }); - vm.expectRevert(abi.encodeWithSelector(Errors.SablierLockupBase_StreamNotDepleted.selector, defaultStreamId)); - lockup.burn(defaultStreamId); + setMsgSender(users.sender); + lockup.cancel(ids.defaultStream); + setMsgSender(users.recipient); + vm.expectRevert(abi.encodeWithSelector(Errors.SablierLockup_StreamNotDepleted.selector, ids.defaultStream)); + lockup.burn(ids.defaultStream); } function test_RevertWhen_CallerMaliciousThirdParty() external whenNoDelegateCall givenNotNull - givenDepletedStream(lockup, defaultStreamId) + givenDepletedStream(lockup, ids.defaultStream) whenCallerNotRecipient { - expectRevert_CallerMaliciousThirdParty({ callData: abi.encodeCall(lockup.burn, defaultStreamId) }); + expectRevert_CallerMaliciousThirdParty({ callData: abi.encodeCall(lockup.burn, ids.defaultStream) }); } function test_RevertWhen_CallerSender() external whenNoDelegateCall givenNotNull - givenDepletedStream(lockup, defaultStreamId) + givenDepletedStream(lockup, ids.defaultStream) whenCallerNotRecipient { - resetPrank({ msgSender: users.sender }); + setMsgSender(users.sender); vm.expectRevert( - abi.encodeWithSelector(Errors.SablierLockupBase_Unauthorized.selector, defaultStreamId, users.sender) + abi.encodeWithSelector(Errors.SablierLockup_Unauthorized.selector, ids.defaultStream, users.sender) ); - lockup.burn(defaultStreamId); + lockup.burn(ids.defaultStream); } function test_WhenCallerApprovedThirdParty() external whenNoDelegateCall givenNotNull - givenDepletedStream(lockup, defaultStreamId) + givenDepletedStream(lockup, ids.defaultStream) whenCallerNotRecipient { // Make the third party the caller in this test. - resetPrank({ msgSender: users.operator }); + setMsgSender(users.operator); // It should burn the NFT. - _test_Burn(defaultStreamId); + _test_Burn(ids.defaultStream); } function test_RevertGiven_NFTNotExist() external whenNoDelegateCall givenNotNull - givenDepletedStream(lockup, defaultStreamId) + givenDepletedStream(lockup, ids.defaultStream) whenCallerRecipient { // Burn the NFT so that it no longer exists. - lockup.burn(defaultStreamId); + lockup.burn(ids.defaultStream); // Run the test. - vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, defaultStreamId)); - lockup.burn(defaultStreamId); + vm.expectRevert( + abi.encodeWithSelector(Errors.SablierLockup_Unauthorized.selector, ids.defaultStream, users.recipient) + ); + lockup.burn(ids.defaultStream); } function test_GivenNonTransferableNFT() external whenNoDelegateCall givenNotNull - givenDepletedStream(lockup, notTransferableStreamId) + givenDepletedStream(lockup, ids.notTransferableStream) whenCallerRecipient givenNFTExists { - _test_Burn(notTransferableStreamId); + _test_Burn(ids.notTransferableStream); } function test_GivenTransferableNFT() external whenNoDelegateCall givenNotNull - givenDepletedStream(lockup, defaultStreamId) + givenDepletedStream(lockup, ids.defaultStream) whenCallerRecipient givenNFTExists { - _test_Burn(defaultStreamId); + _test_Burn(ids.defaultStream); } function _test_Burn(uint256 streamId) private { diff --git a/tests/integration/concrete/lockup-base/burn/burn.tree b/tests/integration/concrete/lockup/burn/burn.tree similarity index 100% rename from tests/integration/concrete/lockup-base/burn/burn.tree rename to tests/integration/concrete/lockup/burn/burn.tree diff --git a/tests/integration/concrete/lockup/calculate-min-fee-wei-for/calculateMinFeeWeiFor.t.sol b/tests/integration/concrete/lockup/calculate-min-fee-wei-for/calculateMinFeeWeiFor.t.sol new file mode 100644 index 000000000..15cbedd1e --- /dev/null +++ b/tests/integration/concrete/lockup/calculate-min-fee-wei-for/calculateMinFeeWeiFor.t.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { ISablierComptroller } from "@sablier/evm-utils/src/interfaces/ISablierComptroller.sol"; + +import { Integration_Test } from "../../../Integration.t.sol"; + +contract CalculateMinFeeWeiFor_Integration_Concrete_Test is Integration_Test { + function test_RevertGiven_Null() external { + expectRevert_Null({ callData: abi.encodeCall(lockup.calculateMinFeeWei, ids.nullStream) }); + } + + function test_GivenCustomFeeSet() external givenNotNull { + setMsgSender(admin); + + uint256 customFeeUSD = 100e8; // 100 USD. + + // Set the custom fee. + comptroller.setCustomFeeUSDFor({ + protocol: ISablierComptroller.Protocol.Lockup, + user: users.sender, + customFeeUSD: customFeeUSD + }); + + uint256 expectedFeeWei = (1e18 * customFeeUSD) / ETH_PRICE_USD; + + // It should return the custom fee in wei. + assertEq(lockup.calculateMinFeeWei(ids.defaultStream), expectedFeeWei, "customFeeWei"); + } + + function test_GivenCustomFeeNotSet() external view givenNotNull { + // It should return the minimum fee in wei. + assertEq(lockup.calculateMinFeeWei(ids.defaultStream), LOCKUP_MIN_FEE_WEI, "minFeeWei"); + } +} diff --git a/tests/integration/concrete/lockup/calculate-min-fee-wei-for/calculateMinFeeWeiFor.tree b/tests/integration/concrete/lockup/calculate-min-fee-wei-for/calculateMinFeeWeiFor.tree new file mode 100644 index 000000000..e41e077fe --- /dev/null +++ b/tests/integration/concrete/lockup/calculate-min-fee-wei-for/calculateMinFeeWeiFor.tree @@ -0,0 +1,8 @@ +CalculateMinFeeWeiFor_Integration_Concrete_Test +├── given null +│ └── it should revert +└── given not null + ├── given custom fee set + │ └── it should return the custom fee in wei + └── given custom fee not set + └── it should return the minimum fee in wei diff --git a/tests/integration/concrete/lockup/cancel-multiple/cancelMultiple.t.sol b/tests/integration/concrete/lockup/cancel-multiple/cancelMultiple.t.sol new file mode 100644 index 000000000..289b44b50 --- /dev/null +++ b/tests/integration/concrete/lockup/cancel-multiple/cancelMultiple.t.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; +import { Errors } from "src/libraries/Errors.sol"; +import { Lockup } from "src/types/Lockup.sol"; + +import { Integration_Test } from "../../../Integration.t.sol"; + +contract CancelMultiple_Integration_Concrete_Test is Integration_Test { + // An array of stream IDs to be canceled. + uint256[] internal cancelIds; + + function setUp() public virtual override { + Integration_Test.setUp(); + + cancelIds.push(ids.defaultStream); + // Create the second stream with an end time double that of the default stream so that the refund amounts are + // different. + cancelIds.push(createDefaultStreamWithEndTime(defaults.END_TIME() + defaults.TOTAL_DURATION())); + } + + function test_RevertWhen_DelegateCall() external { + expectRevert_DelegateCall({ callData: abi.encodeCall(lockup.cancelMultiple, cancelIds) }); + } + + function test_WhenZeroArrayLength() external whenNoDelegateCall { + // It should do nothing. + uint256[] memory nullStreamIds = new uint256[](0); + uint128[] memory refundedAmounts = lockup.cancelMultiple(nullStreamIds); + + assertEq(refundedAmounts.length, 0, "refundedAmounts.length"); + } + + function test_WhenOneStreamReverts() external whenNoDelegateCall whenNonZeroArrayLength { + // Create a cancelable stream using a different sender so that users.sender cannot cancel it. + uint256 revertingStreamId = createDefaultStreamWithUsers(users.recipient, users.alice); + cancelIds.push(revertingStreamId); + + // Simulate the passage of time. + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); + + uint128 expectedSenderAmount = defaults.DEPOSIT_AMOUNT() - defaults.WITHDRAW_AMOUNT(); + + // It should emit {CancelLockupStream} events for non-reverting streams. + vm.expectEmit({ emitter: address(lockup) }); + emit ISablierLockup.CancelLockupStream({ + streamId: cancelIds[0], + sender: users.sender, + recipient: users.recipient, + token: dai, + senderAmount: expectedSenderAmount, + recipientAmount: defaults.WITHDRAW_AMOUNT() + }); + vm.expectEmit({ emitter: address(lockup) }); + emit ISablierLockup.CancelLockupStream({ + streamId: cancelIds[1], + sender: users.sender, + recipient: users.recipient, + token: dai, + senderAmount: expectedSenderAmount, + recipientAmount: defaults.WITHDRAW_AMOUNT() + }); + + // It should emit {InvalidStreamInCancelMultiple} event for reverting stream. + vm.expectEmit({ emitter: address(lockup) }); + emit ISablierLockup.InvalidStreamInCancelMultiple({ + streamId: revertingStreamId, + revertData: abi.encodeWithSelector(Errors.SablierLockup_Unauthorized.selector, revertingStreamId, users.sender) + }); + + // Cancel the streams. + uint128[] memory refundedAmounts = lockup.cancelMultiple(cancelIds); + + // It should return the expected refunded amounts. + assertEq(refundedAmounts.length, 3, "refundedAmounts.length"); + assertEq(refundedAmounts[0], expectedSenderAmount, "refundedAmount0"); + assertEq(refundedAmounts[1], expectedSenderAmount, "refundedAmount1"); + assertEq(refundedAmounts[2], 0, "refundedAmount2"); + + // It should mark the streams as canceled only for non-reverting streams. + assertEq(lockup.statusOf(cancelIds[0]), Lockup.Status.CANCELED, "status0"); + assertEq(lockup.statusOf(cancelIds[1]), Lockup.Status.CANCELED, "status1"); + assertEq(lockup.statusOf(cancelIds[2]), Lockup.Status.STREAMING, "status2"); + + // It should mark the streams as non cancelable only for non-reverting streams. + assertFalse(lockup.isCancelable(cancelIds[0]), "isCancelable0"); + assertFalse(lockup.isCancelable(cancelIds[1]), "isCancelable1"); + assertTrue(lockup.isCancelable(cancelIds[2]), "isCancelable2"); + + // It should update the refunded amounts only for non-reverting streams. + assertEq(lockup.getRefundedAmount(cancelIds[0]), expectedSenderAmount, "refundedAmount0"); + assertEq(lockup.getRefundedAmount(cancelIds[1]), expectedSenderAmount, "refundedAmount1"); + assertEq(lockup.getRefundedAmount(cancelIds[2]), 0, "refundedAmount2"); + } + + function test_WhenNoStreamsRevert() external whenNoDelegateCall whenNonZeroArrayLength { + // Simulate the passage of time. + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); + + // It should refund the sender. + uint128 senderAmount0 = lockup.refundableAmountOf(cancelIds[0]); + expectCallToTransfer({ to: users.sender, value: senderAmount0 }); + uint128 senderAmount1 = lockup.refundableAmountOf(cancelIds[1]); + expectCallToTransfer({ to: users.sender, value: senderAmount1 }); + + // It should emit {CancelLockupStream} events for all streams. + vm.expectEmit({ emitter: address(lockup) }); + emit ISablierLockup.CancelLockupStream({ + streamId: cancelIds[0], + sender: users.sender, + recipient: users.recipient, + token: dai, + senderAmount: senderAmount0, + recipientAmount: defaults.DEPOSIT_AMOUNT() - senderAmount0 + }); + vm.expectEmit({ emitter: address(lockup) }); + emit ISablierLockup.CancelLockupStream({ + streamId: cancelIds[1], + sender: users.sender, + recipient: users.recipient, + token: dai, + senderAmount: senderAmount1, + recipientAmount: defaults.DEPOSIT_AMOUNT() - senderAmount1 + }); + + // Cancel the streams. + uint128[] memory refundedAmounts = lockup.cancelMultiple(cancelIds); + + // It should return the expected refunded amounts. + assertEq(refundedAmounts.length, 2, "refundedAmounts.length"); + assertEq(refundedAmounts[0], senderAmount0, "refundedAmount0"); + assertEq(refundedAmounts[1], senderAmount1, "refundedAmount1"); + + // It should mark the streams as canceled. + Lockup.Status expectedStatus = Lockup.Status.CANCELED; + assertEq(lockup.statusOf(cancelIds[0]), expectedStatus, "status0"); + assertEq(lockup.statusOf(cancelIds[1]), expectedStatus, "status1"); + + // It should make the streams as non cancelable. + assertFalse(lockup.isCancelable(cancelIds[0]), "isCancelable0"); + assertFalse(lockup.isCancelable(cancelIds[1]), "isCancelable1"); + + // It should update the refunded amounts. + assertEq(lockup.getRefundedAmount(cancelIds[0]), senderAmount0, "refundedAmount0"); + assertEq(lockup.getRefundedAmount(cancelIds[1]), senderAmount1, "refundedAmount1"); + + // It should not burn the NFT for any stream. + address expectedNFTOwner = users.recipient; + assertEq(lockup.getRecipient(cancelIds[0]), expectedNFTOwner, "NFT owner0"); + assertEq(lockup.getRecipient(cancelIds[1]), expectedNFTOwner, "NFT owner1"); + } +} diff --git a/tests/integration/concrete/lockup/cancel-multiple/cancelMultiple.tree b/tests/integration/concrete/lockup/cancel-multiple/cancelMultiple.tree new file mode 100644 index 000000000..c14706473 --- /dev/null +++ b/tests/integration/concrete/lockup/cancel-multiple/cancelMultiple.tree @@ -0,0 +1,22 @@ +CancelMultiple_Integration_Concrete_Test +├── when delegate call +│ └── it should revert +└── when no delegate call + ├── when zero array length + │ └── it should do nothing + └── when non zero array length + ├── when one stream reverts + │ ├── it should emit {CancelLockupStream} events for non-reverting streams + │ ├── it should emit {InvalidStreamInCancelMultiple} event for reverting stream + │ ├── it should return the expected refunded amounts + │ ├── it should mark the streams as canceled only for non-reverting streams + │ ├── it should mark the streams as non cancelable only for non-reverting streams + │ └── it should update the refunded amounts only for non-reverting streams + └── when no streams revert + ├── it should return the expected refunded amounts + ├── it should mark the streams as canceled + ├── it should make the streams as non cancelable + ├── it should refund the sender + ├── it should update the refunded amounts + ├── it should not burn the NFT for any stream + └── it should emit {CancelLockupStream} events for all streams diff --git a/tests/integration/concrete/lockup-base/cancel/cancel.t.sol b/tests/integration/concrete/lockup/cancel/cancel.t.sol similarity index 64% rename from tests/integration/concrete/lockup-base/cancel/cancel.t.sol rename to tests/integration/concrete/lockup/cancel/cancel.t.sol index 7dc8733ff..517db883a 100644 --- a/tests/integration/concrete/lockup-base/cancel/cancel.t.sol +++ b/tests/integration/concrete/lockup/cancel/cancel.t.sol @@ -3,32 +3,32 @@ pragma solidity >=0.8.22 <0.9.0; import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; -import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; +import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; import { ISablierLockupRecipient } from "src/interfaces/ISablierLockupRecipient.sol"; import { Errors } from "src/libraries/Errors.sol"; -import { Lockup } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; import { Integration_Test } from "../../../Integration.t.sol"; abstract contract Cancel_Integration_Concrete_Test is Integration_Test { function test_RevertWhen_DelegateCall() external { - expectRevert_DelegateCall({ callData: abi.encodeCall(lockup.cancel, defaultStreamId) }); + expectRevert_DelegateCall({ callData: abi.encodeCall(lockup.cancel, ids.defaultStream) }); } function test_RevertGiven_Null() external whenNoDelegateCall { - expectRevert_Null({ callData: abi.encodeCall(lockup.cancel, nullStreamId) }); + expectRevert_Null({ callData: abi.encodeCall(lockup.cancel, ids.nullStream) }); } function test_RevertGiven_DEPLETEDStatus() external whenNoDelegateCall givenNotNull givenColdStream { - expectRevert_DEPLETEDStatus({ callData: abi.encodeCall(lockup.cancel, defaultStreamId) }); + expectRevert_DEPLETEDStatus({ callData: abi.encodeCall(lockup.cancel, ids.defaultStream) }); } function test_RevertGiven_CANCELEDStatus() external whenNoDelegateCall givenNotNull givenColdStream { - expectRevert_CANCELEDStatus({ callData: abi.encodeCall(lockup.cancel, defaultStreamId) }); + expectRevert_CANCELEDStatus({ callData: abi.encodeCall(lockup.cancel, ids.defaultStream) }); } function test_RevertGiven_SETTLEDStatus() external whenNoDelegateCall givenNotNull givenColdStream { - expectRevert_SETTLEDStatus({ callData: abi.encodeCall(lockup.cancel, defaultStreamId) }); + expectRevert_SETTLEDStatus({ callData: abi.encodeCall(lockup.cancel, ids.defaultStream) }); } function test_RevertWhen_CallerMaliciousThirdParty() @@ -38,7 +38,7 @@ abstract contract Cancel_Integration_Concrete_Test is Integration_Test { givenWarmStream whenCallerNotSender { - expectRevert_CallerMaliciousThirdParty({ callData: abi.encodeCall(lockup.cancel, defaultStreamId) }); + expectRevert_CallerMaliciousThirdParty({ callData: abi.encodeCall(lockup.cancel, ids.defaultStream) }); } function test_RevertWhen_CallerRecipient() @@ -49,13 +49,13 @@ abstract contract Cancel_Integration_Concrete_Test is Integration_Test { whenCallerNotSender { // Make the Recipient the caller in this test. - resetPrank({ msgSender: users.recipient }); + setMsgSender(users.recipient); // Run the test. vm.expectRevert( - abi.encodeWithSelector(Errors.SablierLockupBase_Unauthorized.selector, defaultStreamId, users.recipient) + abi.encodeWithSelector(Errors.SablierLockup_Unauthorized.selector, ids.defaultStream, users.recipient) ); - lockup.cancel(defaultStreamId); + lockup.cancel(ids.defaultStream); } function test_RevertGiven_NonCancelableStream() @@ -66,9 +66,9 @@ abstract contract Cancel_Integration_Concrete_Test is Integration_Test { whenCallerSender { vm.expectRevert( - abi.encodeWithSelector(Errors.SablierLockupBase_StreamNotCancelable.selector, notCancelableStreamId) + abi.encodeWithSelector(Errors.SablierLockup_StreamNotCancelable.selector, ids.notCancelableStream) ); - lockup.cancel(notCancelableStreamId); + lockup.cancel(ids.notCancelableStream); } function test_GivenPENDINGStatus() @@ -79,19 +79,19 @@ abstract contract Cancel_Integration_Concrete_Test is Integration_Test { whenCallerSender givenCancelableStream { - // Warp to the past. - vm.warp({ newTimestamp: getBlockTimestamp() - 1 seconds }); + // Rewind time by 1 second. + rewind(1 seconds); // Cancel the stream. - lockup.cancel(defaultStreamId); + lockup.cancel(ids.defaultStream); // It should mark the stream as depleted. - Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); + Lockup.Status actualStatus = lockup.statusOf(ids.defaultStream); Lockup.Status expectedStatus = Lockup.Status.DEPLETED; assertEq(actualStatus, expectedStatus); // It should make the stream not cancelable. - bool isCancelable = lockup.isCancelable(defaultStreamId); + bool isCancelable = lockup.isCancelable(ids.defaultStream); assertFalse(isCancelable, "isCancelable"); } @@ -105,22 +105,25 @@ abstract contract Cancel_Integration_Concrete_Test is Integration_Test { givenSTREAMINGStatus { // It should not make Sablier run the recipient hook. - uint128 senderAmount = lockup.refundableAmountOf(notAllowedtoHookStreamId); - uint128 recipientAmount = lockup.withdrawableAmountOf(notAllowedtoHookStreamId); + uint128 senderAmount = lockup.refundableAmountOf(ids.notAllowedToHookStream); + uint128 recipientAmount = lockup.withdrawableAmountOf(ids.notAllowedToHookStream); vm.expectCall({ callee: address(recipientGood), data: abi.encodeCall( ISablierLockupRecipient.onSablierLockupCancel, - (notAllowedtoHookStreamId, users.sender, senderAmount, recipientAmount) + (ids.notAllowedToHookStream, users.sender, senderAmount, recipientAmount) ), count: 0 }); // Cancel the stream. - lockup.cancel(notAllowedtoHookStreamId); + uint128 refundedAmount = lockup.cancel(ids.notAllowedToHookStream); + + // It should return the correct refunded amount. + assertEq(refundedAmount, senderAmount, "refundedAmount"); // It should mark the stream as canceled. - Lockup.Status actualStatus = lockup.statusOf(notAllowedtoHookStreamId); + Lockup.Status actualStatus = lockup.statusOf(ids.notAllowedToHookStream); Lockup.Status expectedStatus = Lockup.Status.CANCELED; assertEq(actualStatus, expectedStatus); } @@ -139,7 +142,7 @@ abstract contract Cancel_Integration_Concrete_Test is Integration_Test { vm.expectRevert("You shall not pass"); // Cancel the stream. - lockup.cancel(recipientRevertStreamId); + lockup.cancel(ids.recipientRevertStream); } function test_RevertWhen_RecipientReturnsInvalidSelector() @@ -155,13 +158,11 @@ abstract contract Cancel_Integration_Concrete_Test is Integration_Test { { // It should revert. vm.expectRevert( - abi.encodeWithSelector( - Errors.SablierLockupBase_InvalidHookSelector.selector, address(recipientInvalidSelector) - ) + abi.encodeWithSelector(Errors.SablierLockup_InvalidHookSelector.selector, address(recipientInvalidSelector)) ); // Cancel the stream. - lockup.cancel(recipientInvalidSelectorStreamId); + lockup.cancel(ids.recipientInvalidSelectorStream); } function test_WhenReentrancy() @@ -177,13 +178,13 @@ abstract contract Cancel_Integration_Concrete_Test is Integration_Test { whenRecipientReturnsValidSelector { // It should make Sablier run the recipient hook. - uint128 senderAmount = lockup.refundableAmountOf(recipientReentrantStreamId); - uint128 recipientAmount = lockup.withdrawableAmountOf(recipientReentrantStreamId); + uint128 senderAmount = lockup.refundableAmountOf(ids.recipientReentrantStream); + uint128 recipientAmount = lockup.withdrawableAmountOf(ids.recipientReentrantStream); vm.expectCall( address(recipientReentrant), abi.encodeCall( ISablierLockupRecipient.onSablierLockupCancel, - (recipientReentrantStreamId, users.sender, senderAmount, recipientAmount) + (ids.recipientReentrantStream, users.sender, senderAmount, recipientAmount) ) ); @@ -191,20 +192,23 @@ abstract contract Cancel_Integration_Concrete_Test is Integration_Test { vm.expectCall( address(lockup), abi.encodeCall( - ISablierLockupBase.withdraw, (recipientReentrantStreamId, address(recipientReentrant), recipientAmount) + ISablierLockup.withdraw, (ids.recipientReentrantStream, address(recipientReentrant), recipientAmount) ) ); // Cancel the stream. - lockup.cancel(recipientReentrantStreamId); + uint128 refundedAmount = lockup.cancel(ids.recipientReentrantStream); + + // It should return the correct refunded amount. + assertEq(refundedAmount, senderAmount, "refundedAmount"); // It should mark the stream as depleted. The reentrant recipient withdrew all the funds. - Lockup.Status actualStatus = lockup.statusOf(recipientReentrantStreamId); + Lockup.Status actualStatus = lockup.statusOf(ids.recipientReentrantStream); Lockup.Status expectedStatus = Lockup.Status.DEPLETED; assertEq(actualStatus, expectedStatus); // It should make the withdrawal via the reentrancy. - uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(recipientReentrantStreamId); + uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(ids.recipientReentrantStream); assertEq(actualWithdrawnAmount, recipientAmount, "withdrawnAmount"); } @@ -220,47 +224,57 @@ abstract contract Cancel_Integration_Concrete_Test is Integration_Test { whenNonRevertingRecipient whenRecipientReturnsValidSelector { + uint256 previousAggregateAmount = lockup.aggregateAmount(dai); + // It should refund the sender. - uint128 senderAmount = lockup.refundableAmountOf(recipientGoodStreamId); + uint128 senderAmount = lockup.refundableAmountOf(ids.recipientGoodStream); expectCallToTransfer({ to: users.sender, value: senderAmount }); // It should make Sablier run the recipient hook. - uint128 recipientAmount = lockup.withdrawableAmountOf(recipientGoodStreamId); + uint128 recipientAmount = lockup.withdrawableAmountOf(ids.recipientGoodStream); vm.expectCall( address(recipientGood), abi.encodeCall( ISablierLockupRecipient.onSablierLockupCancel, - (recipientGoodStreamId, users.sender, senderAmount, recipientAmount) + (ids.recipientGoodStream, users.sender, senderAmount, recipientAmount) ) ); // It should emit {MetadataUpdate} and {CancelLockupStream} events. vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.CancelLockupStream( - recipientGoodStreamId, users.sender, address(recipientGood), dai, senderAmount, recipientAmount + emit ISablierLockup.CancelLockupStream( + ids.recipientGoodStream, users.sender, address(recipientGood), dai, senderAmount, recipientAmount ); vm.expectEmit({ emitter: address(lockup) }); - emit IERC4906.MetadataUpdate({ _tokenId: recipientGoodStreamId }); + emit IERC4906.MetadataUpdate({ _tokenId: ids.recipientGoodStream }); // Cancel the stream. - lockup.cancel(recipientGoodStreamId); + uint128 refundedAmount = lockup.cancel(ids.recipientGoodStream); + + // It should return the correct refunded amount. + assertEq(refundedAmount, senderAmount, "refundedAmount"); // It should mark the stream as canceled. - Lockup.Status actualStatus = lockup.statusOf(recipientGoodStreamId); + Lockup.Status actualStatus = lockup.statusOf(ids.recipientGoodStream); Lockup.Status expectedStatus = Lockup.Status.CANCELED; assertEq(actualStatus, expectedStatus); // It should make the stream as non cancelable. - bool isCancelable = lockup.isCancelable(recipientGoodStreamId); + bool isCancelable = lockup.isCancelable(ids.recipientGoodStream); assertFalse(isCancelable, "isCancelable"); // It should update the refunded amount. - uint128 actualRefundedAmount = lockup.getRefundedAmount(recipientGoodStreamId); + uint128 actualRefundedAmount = lockup.getRefundedAmount(ids.recipientGoodStream); uint128 expectedRefundedAmount = senderAmount; assertEq(actualRefundedAmount, expectedRefundedAmount, "refundedAmount"); + // It should update the aggregate amount. + uint256 actualAggregateAmount = lockup.aggregateAmount(dai); + uint256 expectedAggregateAmount = previousAggregateAmount - senderAmount; + assertEq(actualAggregateAmount, expectedAggregateAmount, "aggregateAmount"); + // It should not burn the NFT. - address actualNFTOwner = lockup.ownerOf({ tokenId: recipientGoodStreamId }); + address actualNFTOwner = lockup.ownerOf({ tokenId: ids.recipientGoodStream }); address expectedNFTOwner = address(recipientGood); assertEq(actualNFTOwner, expectedNFTOwner, "NFT owner"); } diff --git a/tests/integration/concrete/lockup-base/cancel/cancel.tree b/tests/integration/concrete/lockup/cancel/cancel.tree similarity index 92% rename from tests/integration/concrete/lockup-base/cancel/cancel.tree rename to tests/integration/concrete/lockup/cancel/cancel.tree index 4842ecf8a..e62957587 100644 --- a/tests/integration/concrete/lockup-base/cancel/cancel.tree +++ b/tests/integration/concrete/lockup/cancel/cancel.tree @@ -37,14 +37,17 @@ Cancel_Integration_Concrete_Test │ └── it should revert └── when recipient returns valid selector ├── when reentrancy + │ ├── it should return the correct refunded amount │ ├── it should mark the stream as depleted │ ├── it should make Sablier run the recipient hook │ ├── it should perform a reentrancy call to the Lockup contract │ └── it should make the withdrawal via the reentrancy └── when no reentrancy + ├── it should return the correct refunded amount ├── it should mark the stream as canceled ├── it should make the stream as non cancelable ├── it should update the refunded amount + ├── it should update the aggregate amount ├── it should refund the sender ├── it should make Sablier run the recipient hook ├── it should not burn the NFT diff --git a/tests/integration/concrete/lockup-base/create-with-timestamps/createWithTimestamps.t.sol b/tests/integration/concrete/lockup/create-with-timestamps/createWithTimestamps.t.sol similarity index 82% rename from tests/integration/concrete/lockup-base/create-with-timestamps/createWithTimestamps.t.sol rename to tests/integration/concrete/lockup/create-with-timestamps/createWithTimestamps.t.sol index 8de6c30c7..4daacdcd0 100644 --- a/tests/integration/concrete/lockup-base/create-with-timestamps/createWithTimestamps.t.sol +++ b/tests/integration/concrete/lockup/create-with-timestamps/createWithTimestamps.t.sol @@ -3,10 +3,10 @@ pragma solidity >=0.8.22 <0.9.0; import { IERC721Errors } from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { Address } from "@openzeppelin/contracts/utils/Address.sol"; -import { UD60x18, ud } from "@prb/math/src/UD60x18.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + import { Errors } from "src/libraries/Errors.sol"; -import { Lockup } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; import { Integration_Test } from "../../../Integration.t.sol"; abstract contract CreateWithTimestamps_Integration_Concrete_Test is Integration_Test { @@ -15,12 +15,15 @@ abstract contract CreateWithTimestamps_Integration_Concrete_Test is Integration_ //////////////////////////////////////////////////////////////////////////*/ /// @dev Helpers function to assert the common storaged values of a stream between all Lockup models. - function assertEqStream(uint256 streamId) internal view { - assertEq(lockup.getDepositedAmount(streamId), defaults.DEPOSIT_AMOUNT(), "depositedAmount"); + function assertEqStream(uint256 streamId, IERC20 token) internal view { + assertEq( + lockup.getDepositedAmount(streamId), _defaultParams.createWithTimestamps.depositAmount, "depositedAmount" + ); assertEq(lockup.getSender(streamId), users.sender, "sender"); assertEq(lockup.getRecipient(streamId), users.recipient, "recipient"); assertEq(lockup.getStartTime(streamId), defaults.START_TIME(), "startTime"); assertEq(lockup.getEndTime(streamId), defaults.END_TIME(), "endTime"); + assertEq(lockup.getUnderlyingToken(streamId), token, "underlyingToken"); assertFalse(lockup.isDepleted(streamId), "isDepleted"); assertFalse(lockup.wasCanceled(streamId), "wasCanceled"); assertTrue(lockup.isStream(streamId), "isStream"); @@ -74,21 +77,7 @@ abstract contract CreateWithTimestamps_Integration_Concrete_Test is Integration_ createDefaultStream(); } - function test_RevertWhen_BrokerFeeExceedsMaxValue() external whenNoDelegateCall whenShapeNotExceed32Bytes { - UD60x18 brokerFee = MAX_BROKER_FEE + ud(1); - _defaultParams.createWithTimestamps.broker.fee = brokerFee; - vm.expectRevert( - abi.encodeWithSelector(Errors.SablierHelpers_BrokerFeeTooHigh.selector, brokerFee, MAX_BROKER_FEE) - ); - createDefaultStream(); - } - - function test_RevertWhen_SenderZeroAddress() - external - whenNoDelegateCall - whenShapeNotExceed32Bytes - whenBrokerFeeNotExceedMaxValue - { + function test_RevertWhen_SenderZeroAddress() external whenNoDelegateCall whenShapeNotExceed32Bytes { _defaultParams.createWithTimestamps.sender = address(0); vm.expectRevert(abi.encodeWithSelector(Errors.SablierHelpers_SenderZeroAddress.selector)); createDefaultStream(); @@ -98,7 +87,6 @@ abstract contract CreateWithTimestamps_Integration_Concrete_Test is Integration_ external whenNoDelegateCall whenShapeNotExceed32Bytes - whenBrokerFeeNotExceedMaxValue whenSenderNotZeroAddress { _defaultParams.createWithTimestamps.recipient = address(0); @@ -110,11 +98,10 @@ abstract contract CreateWithTimestamps_Integration_Concrete_Test is Integration_ external whenNoDelegateCall whenShapeNotExceed32Bytes - whenBrokerFeeNotExceedMaxValue whenSenderNotZeroAddress whenRecipientNotZeroAddress { - _defaultParams.createWithTimestamps.totalAmount = 0; + _defaultParams.createWithTimestamps.depositAmount = 0; vm.expectRevert(abi.encodeWithSelector(Errors.SablierHelpers_DepositAmountZero.selector)); createDefaultStream(); } @@ -123,7 +110,6 @@ abstract contract CreateWithTimestamps_Integration_Concrete_Test is Integration_ external whenNoDelegateCall whenShapeNotExceed32Bytes - whenBrokerFeeNotExceedMaxValue whenSenderNotZeroAddress whenRecipientNotZeroAddress whenDepositAmountNotZero @@ -133,19 +119,34 @@ abstract contract CreateWithTimestamps_Integration_Concrete_Test is Integration_ createDefaultStream(); } + function test_RevertWhen_TokenNativeToken() + external + whenNoDelegateCall + whenShapeNotExceed32Bytes + whenSenderNotZeroAddress + whenRecipientNotZeroAddress + whenDepositAmountNotZero + whenStartTimeNotZero + { + setMsgSender(address(comptroller)); + lockup.setNativeToken(address(dai)); + vm.expectRevert(abi.encodeWithSelector(Errors.SablierHelpers_CreateNativeToken.selector, address(dai))); + createDefaultStream(); + } + function test_RevertWhen_TokenNotContract() external whenNoDelegateCall whenShapeNotExceed32Bytes - whenBrokerFeeNotExceedMaxValue whenSenderNotZeroAddress whenRecipientNotZeroAddress whenDepositAmountNotZero whenStartTimeNotZero + whenTokenNotNativeToken { address nonContract = address(8128); _defaultParams.createWithTimestamps.token = IERC20(nonContract); - vm.expectRevert(abi.encodeWithSelector(Address.AddressEmptyCode.selector, nonContract)); + vm.expectRevert(abi.encodeWithSelector(SafeERC20.SafeERC20FailedOperation.selector, nonContract)); createDefaultStream(); } } diff --git a/tests/integration/concrete/lockup-base/create-with-timestamps/createWithTimestamps.tree b/tests/integration/concrete/lockup/create-with-timestamps/createWithTimestamps.tree similarity index 53% rename from tests/integration/concrete/lockup-base/create-with-timestamps/createWithTimestamps.tree rename to tests/integration/concrete/lockup/create-with-timestamps/createWithTimestamps.tree index e8149d97a..cc102e5d9 100644 --- a/tests/integration/concrete/lockup-base/create-with-timestamps/createWithTimestamps.tree +++ b/tests/integration/concrete/lockup/create-with-timestamps/createWithTimestamps.tree @@ -5,20 +5,20 @@ CreateWithTimestamps_Integration_Concrete_Test ├── when shape exceeds 32 bytes │ └── it should revert └── when shape not exceed 32 bytes - ├── when broker fee exceeds max value + ├── when sender zero address │ └── it should revert - └── when broker fee not exceed max value - ├── when sender zero address + └── when sender not zero address + ├── when recipient zero address │ └── it should revert - └── when sender not zero address - ├── when recipient zero address + └── when recipient not zero address + ├── when deposit amount zero │ └── it should revert - └── when recipient not zero address - ├── when deposit amount zero + └── when deposit amount not zero + ├── when start time zero │ └── it should revert - └── when deposit amount not zero - ├── when start time zero + └── when start time not zero + ├── when token native token │ └── it should revert - └── when start time not zero + └── when token not native token └── when token not contract └── it should revert diff --git a/tests/integration/concrete/lockup-base/getters/getters.t.sol b/tests/integration/concrete/lockup/getters/getters.t.sol similarity index 55% rename from tests/integration/concrete/lockup-base/getters/getters.t.sol rename to tests/integration/concrete/lockup/getters/getters.t.sol index 7042c2d3c..9b6799294 100644 --- a/tests/integration/concrete/lockup-base/getters/getters.t.sol +++ b/tests/integration/concrete/lockup/getters/getters.t.sol @@ -2,7 +2,6 @@ pragma solidity >=0.8.22 <0.9.0; import { IERC721Errors } from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { Integration_Test } from "../../../Integration.t.sol"; @@ -11,12 +10,12 @@ contract Getters_Integration_Concrete_Test is Integration_Test { GET-DEPOSITED-AMOUNT //////////////////////////////////////////////////////////////////////////*/ - function test_GetDepositedAmountRevertGiven_Null() external { - expectRevert_Null({ callData: abi.encodeCall(lockup.getDepositedAmount, nullStreamId) }); + function test_GetDepositedAmount_RevertGiven_Null() external { + expectRevert_Null({ callData: abi.encodeCall(lockup.getDepositedAmount, ids.nullStream) }); } - function test_GetDepositedAmountGivenNotNull() external view { - uint128 actualDepositedAmount = lockup.getDepositedAmount(defaultStreamId); + function test_GetDepositedAmount_GivenNotNull() external view { + uint128 actualDepositedAmount = lockup.getDepositedAmount(ids.defaultStream); uint128 expectedDepositedAmount = defaults.DEPOSIT_AMOUNT(); assertEq(actualDepositedAmount, expectedDepositedAmount, "depositedAmount"); } @@ -25,12 +24,12 @@ contract Getters_Integration_Concrete_Test is Integration_Test { GET-END-TIME //////////////////////////////////////////////////////////////////////////*/ - function test_GetEndTimeRevertGiven_Null() external { - expectRevert_Null({ callData: abi.encodeCall(lockup.getEndTime, nullStreamId) }); + function test_GetEndTime_RevertGiven_Null() external { + expectRevert_Null({ callData: abi.encodeCall(lockup.getEndTime, ids.nullStream) }); } - function test_GetEndTimeGivenNotNull() external view { - uint40 actualEndTime = lockup.getEndTime(defaultStreamId); + function test_GetEndTime_GivenNotNull() external view { + uint40 actualEndTime = lockup.getEndTime(ids.defaultStream); uint40 expectedEndTime = defaults.END_TIME(); assertEq(actualEndTime, expectedEndTime, "endTime"); } @@ -39,31 +38,31 @@ contract Getters_Integration_Concrete_Test is Integration_Test { GET-RECIPIENT //////////////////////////////////////////////////////////////////////////*/ - function test_GetRecipientRevertGiven_Null() external { - vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, nullStreamId)); - lockup.getRecipient(nullStreamId); + function test_GetRecipient_RevertGiven_Null() external { + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, ids.nullStream)); + lockup.getRecipient(ids.nullStream); } - function test_GetRecipientRevertGiven_BurnedNFT() external givenNotNull { + function test_GetRecipient_RevertGiven_BurnedNFT() external givenNotNull { // Simulate the passage of time. vm.warp({ newTimestamp: defaults.END_TIME() }); // Make the Recipient the caller. - resetPrank({ msgSender: users.recipient }); + setMsgSender(users.recipient); // Deplete the stream. - lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); + lockup.withdrawMax{ value: LOCKUP_MIN_FEE_WEI }({ streamId: ids.defaultStream, to: users.recipient }); // Burn the NFT. - lockup.burn(defaultStreamId); + lockup.burn(ids.defaultStream); // Expect the relevant error when retrieving the recipient. - vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, defaultStreamId)); - lockup.getRecipient(defaultStreamId); + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, ids.defaultStream)); + lockup.getRecipient(ids.defaultStream); } - function test_GetRecipientGivenNotBurnedNFT() external view givenNotNull { - address actualRecipient = lockup.getRecipient(defaultStreamId); + function test_GetRecipient_GivenNotBurnedNFT() external view givenNotNull { + address actualRecipient = lockup.getRecipient(ids.defaultStream); address expectedRecipient = users.recipient; assertEq(actualRecipient, expectedRecipient, "recipient"); } @@ -72,60 +71,60 @@ contract Getters_Integration_Concrete_Test is Integration_Test { GET-REFUNDED-AMOUNT //////////////////////////////////////////////////////////////////////////*/ - function test_GetRefundedAmountRevertGiven_Null() external { - expectRevert_Null({ callData: abi.encodeCall(lockup.getRefundedAmount, nullStreamId) }); + function test_GetRefundedAmount_RevertGiven_Null() external { + expectRevert_Null({ callData: abi.encodeCall(lockup.getRefundedAmount, ids.nullStream) }); } - function test_GetRefundedAmountGivenCanceledStreamAndCANCELEDStatus() external givenNotNull { + function test_GetRefundedAmount_GivenCanceledStreamAndCANCELEDStatus() external givenNotNull { vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); // Cancel the stream. - lockup.cancel(defaultStreamId); + lockup.cancel(ids.defaultStream); // It should return the correct refunded amount. - uint128 actualRefundedAmount = lockup.getRefundedAmount(defaultStreamId); + uint128 actualRefundedAmount = lockup.getRefundedAmount(ids.defaultStream); uint128 expectedRefundedAmount = defaults.REFUND_AMOUNT(); assertEq(actualRefundedAmount, expectedRefundedAmount, "refundedAmount"); } - function test_GetRefundedAmountGivenCanceledStreamAndDEPLETEDStatus() external givenNotNull { + function test_GetRefundedAmount_GivenCanceledStreamAndDEPLETEDStatus() external givenNotNull { vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); // Cancel the stream. - lockup.cancel(defaultStreamId); + lockup.cancel(ids.defaultStream); // Withdraw the maximum amount to deplete the stream. - lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); + lockup.withdrawMax{ value: LOCKUP_MIN_FEE_WEI }({ streamId: ids.defaultStream, to: users.recipient }); // It should return the correct refunded amount. - uint128 actualRefundedAmount = lockup.getRefundedAmount(defaultStreamId); + uint128 actualRefundedAmount = lockup.getRefundedAmount(ids.defaultStream); uint128 expectedRefundedAmount = defaults.REFUND_AMOUNT(); assertEq(actualRefundedAmount, expectedRefundedAmount, "refundedAmount"); } - function test_GetRefundedAmountGivenPENDINGStatus() external givenNotNull givenNotCanceledStream { - vm.warp({ newTimestamp: getBlockTimestamp() - 1 seconds }); - uint128 actualRefundedAmount = lockup.getRefundedAmount(defaultStreamId); + function test_GetRefundedAmount_GivenPENDINGStatus() external givenNotNull givenNotCanceledStream { + rewind(1 seconds); + uint128 actualRefundedAmount = lockup.getRefundedAmount(ids.defaultStream); uint128 expectedRefundedAmount = 0; assertEq(actualRefundedAmount, expectedRefundedAmount, "refundedAmount"); } - function test_GetRefundedAmountGivenSETTLEDStatus() external givenNotNull givenNotCanceledStream { + function test_GetRefundedAmount_GivenSETTLEDStatus() external givenNotNull givenNotCanceledStream { vm.warp({ newTimestamp: defaults.END_TIME() }); - uint128 actualRefundedAmount = lockup.getRefundedAmount(defaultStreamId); + uint128 actualRefundedAmount = lockup.getRefundedAmount(ids.defaultStream); uint128 expectedRefundedAmount = 0; assertEq(actualRefundedAmount, expectedRefundedAmount, "refundedAmount"); } - function test_GetRefundedAmountGivenDEPLETEDStatus() external givenNotNull givenNotCanceledStream { + function test_GetRefundedAmount_GivenDEPLETEDStatus() external givenNotNull givenNotCanceledStream { vm.warp({ newTimestamp: defaults.END_TIME() }); - lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); - uint128 actualRefundedAmount = lockup.getRefundedAmount(defaultStreamId); + lockup.withdrawMax{ value: LOCKUP_MIN_FEE_WEI }({ streamId: ids.defaultStream, to: users.recipient }); + uint128 actualRefundedAmount = lockup.getRefundedAmount(ids.defaultStream); uint128 expectedRefundedAmount = 0; assertEq(actualRefundedAmount, expectedRefundedAmount, "refundedAmount"); } - function test_GetRefundedAmountGivenSTREAMINGStatus() external givenNotNull givenNotCanceledStream { + function test_GetRefundedAmount_GivenSTREAMINGStatus() external givenNotNull givenNotCanceledStream { vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); - uint128 actualRefundedAmount = lockup.getRefundedAmount(defaultStreamId); + uint128 actualRefundedAmount = lockup.getRefundedAmount(ids.defaultStream); uint128 expectedRefundedAmount = 0; assertEq(actualRefundedAmount, expectedRefundedAmount, "refundedAmount"); } @@ -134,12 +133,12 @@ contract Getters_Integration_Concrete_Test is Integration_Test { GET-SENDER //////////////////////////////////////////////////////////////////////////*/ - function test_GetSenderRevertGiven_Null() external { - expectRevert_Null({ callData: abi.encodeCall(lockup.getSender, nullStreamId) }); + function test_GetSender_RevertGiven_Null() external { + expectRevert_Null({ callData: abi.encodeCall(lockup.getSender, ids.nullStream) }); } - function test_GetSenderGivenNotNull() external view { - address actualSender = lockup.getSender(defaultStreamId); + function test_GetSender_GivenNotNull() external view { + address actualSender = lockup.getSender(ids.defaultStream); address expectedSender = users.sender; assertEq(actualSender, expectedSender, "sender"); } @@ -148,12 +147,12 @@ contract Getters_Integration_Concrete_Test is Integration_Test { GET-START-TIME //////////////////////////////////////////////////////////////////////////*/ - function test_GetStartTimeRevertGiven_Null() external { - expectRevert_Null({ callData: abi.encodeCall(lockup.getStartTime, nullStreamId) }); + function test_GetStartTime_RevertGiven_Null() external { + expectRevert_Null({ callData: abi.encodeCall(lockup.getStartTime, ids.nullStream) }); } - function test_GetStartTimeGivenNotNull() external view { - uint40 actualStartTime = lockup.getStartTime(defaultStreamId); + function test_GetStartTime_GivenNotNull() external view { + uint40 actualStartTime = lockup.getStartTime(ids.defaultStream); uint40 expectedStartTime = defaults.START_TIME(); assertEq(actualStartTime, expectedStartTime, "startTime"); } @@ -162,46 +161,48 @@ contract Getters_Integration_Concrete_Test is Integration_Test { GET-UNDERLYING-TOKEN //////////////////////////////////////////////////////////////////////////*/ - function test_GetUnderlyingTokenRevertGiven_Null() external { - expectRevert_Null({ callData: abi.encodeCall(lockup.getUnderlyingToken, nullStreamId) }); + function test_GetUnderlyingToken_RevertGiven_Null() external { + expectRevert_Null({ callData: abi.encodeCall(lockup.getUnderlyingToken, ids.nullStream) }); } - function test_GetUnderlyingTokenGivenNotNull() external view { - IERC20 actualUnderlyingToken = lockup.getUnderlyingToken(defaultStreamId); - IERC20 expectedUnderlyingToken = dai; - assertEq(actualUnderlyingToken, expectedUnderlyingToken, "underlyingToken"); + function test_GetUnderlyingToken_GivenNotNull() external view { + assertEq(lockup.getUnderlyingToken(ids.defaultStream), dai, "underlyingToken"); } /*////////////////////////////////////////////////////////////////////////// GET-WITHDRAWN-AMOUNT //////////////////////////////////////////////////////////////////////////*/ - function test_GetWithdrawnAmountRevertGiven_Null() external { - expectRevert_Null({ callData: abi.encodeCall(lockup.getWithdrawnAmount, nullStreamId) }); + function test_GetWithdrawnAmount_RevertGiven_Null() external { + expectRevert_Null({ callData: abi.encodeCall(lockup.getWithdrawnAmount, ids.nullStream) }); } - function test_GetWithdrawnAmountGivenNoPreviousWithdrawals() external givenNotNull { + function test_GetWithdrawnAmount_GivenNoPreviousWithdrawals() external givenNotNull { // Simulate the passage of time. vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); // It should return zero. - uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(defaultStreamId); + uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(ids.defaultStream); uint128 expectedWithdrawnAmount = 0; assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); } - function test_GetWithdrawnAmountGivenPreviousWithdrawal() external givenNotNull { + function test_GetWithdrawnAmount_GivenPreviousWithdrawal() external givenNotNull { // Simulate the passage of time. vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); // Set the withdraw amount to the streamed amount. - uint128 withdrawAmount = lockup.streamedAmountOf(defaultStreamId); + uint128 withdrawAmount = lockup.streamedAmountOf(ids.defaultStream); // Make the withdrawal. - lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: withdrawAmount }); + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: ids.defaultStream, + to: users.recipient, + amount: withdrawAmount + }); // It should return the correct withdrawn amount. - uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(defaultStreamId); + uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(ids.defaultStream); uint128 expectedWithdrawnAmount = withdrawAmount; assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); } @@ -210,12 +211,12 @@ contract Getters_Integration_Concrete_Test is Integration_Test { IS-ALLOWED-TO-HOOK //////////////////////////////////////////////////////////////////////////*/ - function test_IsAllowedToHookGivenProvidedAddressNotAllowedToHook() external view { + function test_IsAllowedToHook_GivenProvidedAddressNotAllowedToHook() external view { bool result = lockup.isAllowedToHook(address(recipientInterfaceIDIncorrect)); assertFalse(result, "isAllowedToHook"); } - function test_IsAllowedToHookGivenProvidedAddressAllowedToHook() external view { + function test_IsAllowedToHook_GivenProvidedAddressAllowedToHook() external view { bool result = lockup.isAllowedToHook(address(recipientGood)); assertTrue(result, "isAllowedToHook"); } @@ -224,153 +225,153 @@ contract Getters_Integration_Concrete_Test is Integration_Test { IS-CANCELABLE //////////////////////////////////////////////////////////////////////////*/ - function test_IsCancelableRevertGiven_Null() external { - expectRevert_Null({ callData: abi.encodeCall(lockup.isCancelable, nullStreamId) }); + function test_IsCancelable_RevertGiven_Null() external { + expectRevert_Null({ callData: abi.encodeCall(lockup.isCancelable, ids.nullStream) }); } - function test_IsCancelableGivenColdStream() external givenNotNull { + function test_IsCancelable_GivenColdStream() external givenNotNull { vm.warp({ newTimestamp: defaults.END_TIME() }); // settled status - assertFalse(lockup.isCancelable(defaultStreamId), "isCancelable"); + assertFalse(lockup.isCancelable(ids.defaultStream), "isCancelable"); } - function test_IsCancelableGivenCancelableStream() external view givenNotNull givenWarmStream { - assertTrue(lockup.isCancelable(defaultStreamId), "isCancelable"); + function test_IsCancelable_GivenCancelableStream() external view givenNotNull givenWarmStream { + assertTrue(lockup.isCancelable(ids.defaultStream), "isCancelable"); } - function test_IsCancelableGivenNonCancelableStream() external view givenNotNull givenWarmStream { - assertFalse(lockup.isCancelable(notCancelableStreamId), "isCancelable"); + function test_IsCancelable_GivenNonCancelableStream() external view givenNotNull givenWarmStream { + assertFalse(lockup.isCancelable(ids.notCancelableStream), "isCancelable"); } /*////////////////////////////////////////////////////////////////////////// IS-COLD //////////////////////////////////////////////////////////////////////////*/ - function test_IsColdRevertGiven_Null() external { - expectRevert_Null({ callData: abi.encodeCall(lockup.isCold, nullStreamId) }); + function test_IsCold_RevertGiven_Null() external { + expectRevert_Null({ callData: abi.encodeCall(lockup.isCold, ids.nullStream) }); } - function test_IsColdGivenPENDINGStatus() external givenNotNull { - vm.warp({ newTimestamp: getBlockTimestamp() - 1 seconds }); - assertFalse(lockup.isCold(defaultStreamId), "isCold"); + function test_IsCold_GivenPENDINGStatus() external givenNotNull { + rewind(1 seconds); + assertFalse(lockup.isCold(ids.defaultStream), "isCold"); } - function test_IsColdGivenSTREAMINGStatus() external givenNotNull { + function test_IsCold_GivenSTREAMINGStatus() external givenNotNull { vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); - assertFalse(lockup.isCold(defaultStreamId), "isCold"); + assertFalse(lockup.isCold(ids.defaultStream), "isCold"); } - function test_IsColdGivenSETTLEDStatus() external givenNotNull { + function test_IsCold_GivenSETTLEDStatus() external givenNotNull { vm.warp({ newTimestamp: defaults.END_TIME() }); - assertTrue(lockup.isCold(defaultStreamId), "isCold"); + assertTrue(lockup.isCold(ids.defaultStream), "isCold"); } - function test_IsColdGivenCANCELEDStatus() external givenNotNull { + function test_IsCold_GivenCANCELEDStatus() external givenNotNull { vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); - lockup.cancel(defaultStreamId); - assertTrue(lockup.isCold(defaultStreamId), "isCold"); + lockup.cancel(ids.defaultStream); + assertTrue(lockup.isCold(ids.defaultStream), "isCold"); } - function test_IsColdGivenDEPLETEDStatus() external givenNotNull { + function test_IsCold_GivenDEPLETEDStatus() external givenNotNull { vm.warp({ newTimestamp: defaults.END_TIME() }); - lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); - assertTrue(lockup.isCold(defaultStreamId), "isCold"); + lockup.withdrawMax{ value: LOCKUP_MIN_FEE_WEI }({ streamId: ids.defaultStream, to: users.recipient }); + assertTrue(lockup.isCold(ids.defaultStream), "isCold"); } /*////////////////////////////////////////////////////////////////////////// IS-DEPLETED //////////////////////////////////////////////////////////////////////////*/ - function test_IsDepletedRevertGiven_Null() external { - expectRevert_Null({ callData: abi.encodeCall(lockup.isDepleted, nullStreamId) }); + function test_IsDepleted_RevertGiven_Null() external { + expectRevert_Null({ callData: abi.encodeCall(lockup.isDepleted, ids.nullStream) }); } - function test_IsDepletedGivenNotDepletedStream() external view givenNotNull { - assertFalse(lockup.isDepleted(defaultStreamId), "isDepleted"); + function test_IsDepleted_GivenNotDepletedStream() external view givenNotNull { + assertFalse(lockup.isDepleted(ids.defaultStream), "isDepleted"); } - function test_IsDepletedGivenDepletedStream() external givenNotNull { + function test_IsDepleted_GivenDepletedStream() external givenNotNull { vm.warp({ newTimestamp: defaults.END_TIME() }); - lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); - assertTrue(lockup.isDepleted(defaultStreamId), "isDepleted"); + lockup.withdrawMax{ value: LOCKUP_MIN_FEE_WEI }({ streamId: ids.defaultStream, to: users.recipient }); + assertTrue(lockup.isDepleted(ids.defaultStream), "isDepleted"); } /*////////////////////////////////////////////////////////////////////////// IS-STREAM //////////////////////////////////////////////////////////////////////////*/ - function test_IsStreamGivenNull() external view { - assertFalse(lockup.isStream(nullStreamId), "isStream"); + function test_IsStream_GivenNull() external view { + assertFalse(lockup.isStream(ids.nullStream), "isStream"); } - function test_IsStreamGivenNotNull() external view { - assertTrue(lockup.isStream(defaultStreamId), "isStream"); + function test_IsStream_GivenNotNull() external view { + assertTrue(lockup.isStream(ids.defaultStream), "isStream"); } /*////////////////////////////////////////////////////////////////////////// IS-TRANSFERABLE //////////////////////////////////////////////////////////////////////////*/ - function test_IsTransferableRevertGiven_Null() external { - expectRevert_Null({ callData: abi.encodeCall(lockup.isTransferable, nullStreamId) }); + function test_IsTransferable_RevertGiven_Null() external { + expectRevert_Null({ callData: abi.encodeCall(lockup.isTransferable, ids.nullStream) }); } - function test_IsTransferableGivenNonTransferableStream() external view givenNotNull { - assertFalse(lockup.isTransferable(notTransferableStreamId), "isTransferable"); + function test_IsTransferable_GivenNonTransferableStream() external view givenNotNull { + assertFalse(lockup.isTransferable(ids.notTransferableStream), "isTransferable"); } - function test_IsTransferableGivenTransferableStream() external view givenNotNull { - assertTrue(lockup.isTransferable(defaultStreamId), "isTransferable"); + function test_IsTransferable_GivenTransferableStream() external view givenNotNull { + assertTrue(lockup.isTransferable(ids.defaultStream), "isTransferable"); } /*////////////////////////////////////////////////////////////////////////// IS-WARM //////////////////////////////////////////////////////////////////////////*/ - function test_IsWarmRevertGiven_Null() external { - expectRevert_Null({ callData: abi.encodeCall(lockup.isWarm, nullStreamId) }); + function test_IsWarm_RevertGiven_Null() external { + expectRevert_Null({ callData: abi.encodeCall(lockup.isWarm, ids.nullStream) }); } - function test_IsWarmGivenPENDINGStatus() external givenNotNull { - vm.warp({ newTimestamp: getBlockTimestamp() - 1 seconds }); - assertTrue(lockup.isWarm(defaultStreamId), "isWarm"); + function test_IsWarm_GivenPENDINGStatus() external givenNotNull { + rewind(1 seconds); + assertTrue(lockup.isWarm(ids.defaultStream), "isWarm"); } - function test_IsWarmGivenSTREAMINGStatus() external givenNotNull { + function test_IsWarm_GivenSTREAMINGStatus() external givenNotNull { vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); - assertTrue(lockup.isWarm(defaultStreamId), "isWarm"); + assertTrue(lockup.isWarm(ids.defaultStream), "isWarm"); } - function test_IsWarmGivenSETTLEDStatus() external givenNotNull { + function test_IsWarm_GivenSETTLEDStatus() external givenNotNull { vm.warp({ newTimestamp: defaults.END_TIME() }); - assertFalse(lockup.isWarm(defaultStreamId), "isWarm"); + assertFalse(lockup.isWarm(ids.defaultStream), "isWarm"); } - function test_IsWarmGivenCANCELEDStatus() external givenNotNull { + function test_IsWarm_GivenCANCELEDStatus() external givenNotNull { vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); - lockup.cancel(defaultStreamId); - assertFalse(lockup.isWarm(defaultStreamId), "isWarm"); + lockup.cancel(ids.defaultStream); + assertFalse(lockup.isWarm(ids.defaultStream), "isWarm"); } - function test_IsWarmGivenDEPLETEDStatus() external givenNotNull { + function test_IsWarm_GivenDEPLETEDStatus() external givenNotNull { vm.warp({ newTimestamp: defaults.END_TIME() }); - lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); - assertFalse(lockup.isWarm(defaultStreamId), "isWarm"); + lockup.withdrawMax{ value: LOCKUP_MIN_FEE_WEI }({ streamId: ids.defaultStream, to: users.recipient }); + assertFalse(lockup.isWarm(ids.defaultStream), "isWarm"); } /*////////////////////////////////////////////////////////////////////////// WAS-CANCELED //////////////////////////////////////////////////////////////////////////*/ - function test_WasCanceledRevertGiven_Null() external { - expectRevert_Null({ callData: abi.encodeCall(lockup.wasCanceled, nullStreamId) }); + function test_WasCanceled_RevertGiven_Null() external { + expectRevert_Null({ callData: abi.encodeCall(lockup.wasCanceled, ids.nullStream) }); } - function test_WasCanceledGivenCanceledStream() external view givenNotNull { - assertFalse(lockup.wasCanceled(defaultStreamId), "wasCanceled"); + function test_WasCanceled_GivenCanceledStream() external view givenNotNull { + assertFalse(lockup.wasCanceled(ids.defaultStream), "wasCanceled"); } - function test_WasCanceledGivenNotCanceledStream() external givenNotNull { - lockup.cancel(defaultStreamId); - assertTrue(lockup.wasCanceled(defaultStreamId), "wasCanceled"); + function test_WasCanceled_GivenNotCanceledStream() external givenNotNull { + lockup.cancel(ids.defaultStream); + assertTrue(lockup.wasCanceled(ids.defaultStream), "wasCanceled"); } } diff --git a/tests/integration/concrete/lockup-base/getters/getters.tree b/tests/integration/concrete/lockup/getters/getters.tree similarity index 100% rename from tests/integration/concrete/lockup-base/getters/getters.tree rename to tests/integration/concrete/lockup/getters/getters.tree diff --git a/tests/integration/concrete/lockup/recover/recover.t.sol b/tests/integration/concrete/lockup/recover/recover.t.sol new file mode 100644 index 000000000..bf1788234 --- /dev/null +++ b/tests/integration/concrete/lockup/recover/recover.t.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { Errors as EvmUtilsErrors } from "@sablier/evm-utils/src/libraries/Errors.sol"; + +import { Integration_Test } from "../../../Integration.t.sol"; + +contract Recover_Integration_Concrete_Test is Integration_Test { + function test_RevertWhen_CallerNotComptroller() external { + setMsgSender(users.eve); + vm.expectRevert( + abi.encodeWithSelector(EvmUtilsErrors.Comptrollerable_CallerNotComptroller.selector, comptroller, users.eve) + ); + lockup.recover(dai, users.eve); + } + + function test_WhenCallerComptroller() external { + setMsgSender(address(comptroller)); + uint256 surplusAmount = 1e18; + + // Increase the lockup contract balance in order to have a surplus. + deal({ token: address(dai), to: address(lockup), give: dai.balanceOf(address(lockup)) + surplusAmount }); + + // It should emit {Transfer} event. + vm.expectEmit({ emitter: address(dai) }); + emit IERC20.Transfer({ from: address(lockup), to: address(comptroller), value: surplusAmount }); + + // Recover the surplus. + lockup.recover(dai, address(comptroller)); + + // It should lead to token balance same as aggregate amount. + assertEq(dai.balanceOf(address(lockup)), lockup.aggregateAmount(dai)); + } +} diff --git a/tests/integration/concrete/lockup/recover/recover.tree b/tests/integration/concrete/lockup/recover/recover.tree new file mode 100644 index 000000000..477400a11 --- /dev/null +++ b/tests/integration/concrete/lockup/recover/recover.tree @@ -0,0 +1,7 @@ +Recover_Integration_Concrete_Test +├── when caller not comptroller +│ └── it should revert +└── when caller comptroller + ├── it should transfer the surplus to provided address + ├── it should emit {Transfer} event + └── it should lead to token balance same as aggregate amount diff --git a/tests/integration/concrete/lockup-base/refundable-amount-of/refundableAmountOf.t.sol b/tests/integration/concrete/lockup/refundable-amount-of/refundableAmountOf.t.sol similarity index 84% rename from tests/integration/concrete/lockup-base/refundable-amount-of/refundableAmountOf.t.sol rename to tests/integration/concrete/lockup/refundable-amount-of/refundableAmountOf.t.sol index 2fb5f383c..61b103fc3 100644 --- a/tests/integration/concrete/lockup-base/refundable-amount-of/refundableAmountOf.t.sol +++ b/tests/integration/concrete/lockup/refundable-amount-of/refundableAmountOf.t.sol @@ -5,59 +5,59 @@ import { Integration_Test } from "../../../Integration.t.sol"; abstract contract RefundableAmountOf_Integration_Concrete_Test is Integration_Test { function test_RevertGiven_Null() external { - expectRevert_Null({ callData: abi.encodeCall(lockup.refundableAmountOf, nullStreamId) }); + expectRevert_Null({ callData: abi.encodeCall(lockup.refundableAmountOf, ids.nullStream) }); } function test_GivenNonCancelableStream() external givenNotNull { vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); - uint128 actualRefundableAmount = lockup.refundableAmountOf(notCancelableStreamId); + uint128 actualRefundableAmount = lockup.refundableAmountOf(ids.notCancelableStream); uint128 expectedRefundableAmount = 0; assertEq(actualRefundableAmount, expectedRefundableAmount, "refundableAmount"); } function test_GivenCanceledStreamAndCANCELEDStatus() external givenNotNull givenCancelableStream { vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); - lockup.cancel(defaultStreamId); - uint128 actualRefundableAmount = lockup.refundableAmountOf(defaultStreamId); + lockup.cancel(ids.defaultStream); + uint128 actualRefundableAmount = lockup.refundableAmountOf(ids.defaultStream); uint128 expectedRefundableAmount = 0; assertEq(actualRefundableAmount, expectedRefundableAmount, "refundableAmount"); } function test_GivenCanceledStreamAndDEPLETEDStatus() external givenNotNull givenCancelableStream { vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); - lockup.cancel(defaultStreamId); - lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); + lockup.cancel(ids.defaultStream); + lockup.withdrawMax{ value: LOCKUP_MIN_FEE_WEI }({ streamId: ids.defaultStream, to: users.recipient }); vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() + 10 seconds }); - uint128 actualRefundableAmount = lockup.refundableAmountOf(defaultStreamId); + uint128 actualRefundableAmount = lockup.refundableAmountOf(ids.defaultStream); uint128 expectedRefundableAmount = 0; assertEq(actualRefundableAmount, expectedRefundableAmount, "refundableAmount"); } function test_GivenPENDINGStatus() external givenNotNull givenCancelableStream givenNotCanceledStream { - vm.warp({ newTimestamp: getBlockTimestamp() - 1 seconds }); - uint128 actualRefundableAmount = lockup.refundableAmountOf(defaultStreamId); + rewind(1 seconds); + uint128 actualRefundableAmount = lockup.refundableAmountOf(ids.defaultStream); uint128 expectedReturnableAmount = defaults.DEPOSIT_AMOUNT(); assertEq(actualRefundableAmount, expectedReturnableAmount, "refundableAmount"); } function test_GivenSTREAMINGStatus() external givenNotNull givenCancelableStream givenNotCanceledStream { vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); - uint128 actualRefundableAmount = lockup.refundableAmountOf(defaultStreamId); + uint128 actualRefundableAmount = lockup.refundableAmountOf(ids.defaultStream); uint128 expectedReturnableAmount = defaults.REFUND_AMOUNT(); assertEq(actualRefundableAmount, expectedReturnableAmount, "refundableAmount"); } function test_GivenSETTLEDStatus() external givenNotNull givenCancelableStream givenNotCanceledStream { vm.warp({ newTimestamp: defaults.END_TIME() }); - uint128 actualRefundableAmount = lockup.refundableAmountOf(defaultStreamId); + uint128 actualRefundableAmount = lockup.refundableAmountOf(ids.defaultStream); uint128 expectedReturnableAmount = 0; assertEq(actualRefundableAmount, expectedReturnableAmount, "refundableAmount"); } function test_GivenDEPLETEDStatus() external givenNotNull givenCancelableStream givenNotCanceledStream { vm.warp({ newTimestamp: defaults.END_TIME() }); - lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); - uint128 actualRefundableAmount = lockup.refundableAmountOf(defaultStreamId); + lockup.withdrawMax{ value: LOCKUP_MIN_FEE_WEI }({ streamId: ids.defaultStream, to: users.recipient }); + uint128 actualRefundableAmount = lockup.refundableAmountOf(ids.defaultStream); uint128 expectedReturnableAmount = 0; assertEq(actualRefundableAmount, expectedReturnableAmount, "refundableAmount"); } diff --git a/tests/integration/concrete/lockup-base/refundable-amount-of/refundableAmountOf.tree b/tests/integration/concrete/lockup/refundable-amount-of/refundableAmountOf.tree similarity index 100% rename from tests/integration/concrete/lockup-base/refundable-amount-of/refundableAmountOf.tree rename to tests/integration/concrete/lockup/refundable-amount-of/refundableAmountOf.tree diff --git a/tests/integration/concrete/lockup-base/renounce/renounce.t.sol b/tests/integration/concrete/lockup/renounce/renounce.t.sol similarity index 78% rename from tests/integration/concrete/lockup-base/renounce/renounce.t.sol rename to tests/integration/concrete/lockup/renounce/renounce.t.sol index 771bb3492..b1b10038f 100644 --- a/tests/integration/concrete/lockup-base/renounce/renounce.t.sol +++ b/tests/integration/concrete/lockup/renounce/renounce.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; +import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; import { Errors } from "src/libraries/Errors.sol"; import { Integration_Test } from "../../../Integration.t.sol"; @@ -10,37 +10,37 @@ abstract contract Renounce_Integration_Concrete_Test is Integration_Test { uint256 internal streamId; function test_RevertWhen_DelegateCall() external { - expectRevert_DelegateCall({ callData: abi.encodeCall(lockup.renounce, defaultStreamId) }); + expectRevert_DelegateCall({ callData: abi.encodeCall(lockup.renounce, ids.defaultStream) }); } function test_RevertGiven_Null() external whenNoDelegateCall { - expectRevert_Null({ callData: abi.encodeCall(lockup.renounce, nullStreamId) }); + expectRevert_Null({ callData: abi.encodeCall(lockup.renounce, ids.nullStream) }); } function test_RevertGiven_DEPLETEDStatus() external whenNoDelegateCall givenNotNull givenColdStream { - expectRevert_DEPLETEDStatus({ callData: abi.encodeCall(lockup.renounce, defaultStreamId) }); + expectRevert_DEPLETEDStatus({ callData: abi.encodeCall(lockup.renounce, ids.defaultStream) }); } function test_RevertGiven_CANCELEDStatus() external whenNoDelegateCall givenNotNull givenColdStream { - expectRevert_CANCELEDStatus({ callData: abi.encodeCall(lockup.renounce, defaultStreamId) }); + expectRevert_CANCELEDStatus({ callData: abi.encodeCall(lockup.renounce, ids.defaultStream) }); } function test_RevertGiven_SETTLEDStatus() external whenNoDelegateCall givenNotNull givenColdStream { - expectRevert_SETTLEDStatus({ callData: abi.encodeCall(lockup.renounce, defaultStreamId) }); + expectRevert_SETTLEDStatus({ callData: abi.encodeCall(lockup.renounce, ids.defaultStream) }); } modifier givenWarmStreamRenounce() { vm.warp({ newTimestamp: defaults.START_TIME() - 1 seconds }); - streamId = defaultStreamId; + streamId = ids.defaultStream; _; vm.warp({ newTimestamp: defaults.START_TIME() }); - streamId = recipientGoodStreamId; + streamId = ids.recipientGoodStream; _; } function test_RevertWhen_CallerNotSender() external whenNoDelegateCall givenNotNull givenWarmStreamRenounce { - expectRevert_CallerMaliciousThirdParty({ callData: abi.encodeCall(lockup.renounce, defaultStreamId) }); + expectRevert_CallerMaliciousThirdParty({ callData: abi.encodeCall(lockup.renounce, ids.defaultStream) }); } function test_RevertGiven_NonCancelableStream() @@ -52,9 +52,9 @@ abstract contract Renounce_Integration_Concrete_Test is Integration_Test { { // Run the test. vm.expectRevert( - abi.encodeWithSelector(Errors.SablierLockupBase_StreamNotCancelable.selector, notCancelableStreamId) + abi.encodeWithSelector(Errors.SablierLockup_StreamNotCancelable.selector, ids.notCancelableStream) ); - lockup.renounce(notCancelableStreamId); + lockup.renounce(ids.notCancelableStream); } function test_GivenCancelableStream() @@ -66,7 +66,7 @@ abstract contract Renounce_Integration_Concrete_Test is Integration_Test { { // It should emit {RenounceLockupStream} event. vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.RenounceLockupStream(streamId); + emit ISablierLockup.RenounceLockupStream(streamId); // Renounce the stream. lockup.renounce(streamId); diff --git a/tests/integration/concrete/lockup-base/renounce/renounce.tree b/tests/integration/concrete/lockup/renounce/renounce.tree similarity index 100% rename from tests/integration/concrete/lockup-base/renounce/renounce.tree rename to tests/integration/concrete/lockup/renounce/renounce.tree diff --git a/tests/integration/concrete/lockup/set-native-token/setNativeToken.t.sol b/tests/integration/concrete/lockup/set-native-token/setNativeToken.t.sol new file mode 100644 index 000000000..7ff0df6d7 --- /dev/null +++ b/tests/integration/concrete/lockup/set-native-token/setNativeToken.t.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { Errors as EvmUtilsErrors } from "@sablier/evm-utils/src/libraries/Errors.sol"; +import { Errors } from "src/libraries/Errors.sol"; + +import { Integration_Test } from "../../../Integration.t.sol"; + +contract SetNativeToken_Integration_Test is Integration_Test { + function test_RevertWhen_CallerNotComptroller() external { + setMsgSender(users.eve); + vm.expectRevert( + abi.encodeWithSelector( + EvmUtilsErrors.Comptrollerable_CallerNotComptroller.selector, address(comptroller), users.eve + ) + ); + lockup.setNativeToken(address(dai)); + } + + function test_RevertGiven_NativeTokenAlreadySet() external whenCallerComptroller { + // Already set the native token for this test. + address nativeToken = address(dai); + lockup.setNativeToken(nativeToken); + + // It should revert. + vm.expectRevert(abi.encodeWithSelector(Errors.SablierLockup_NativeTokenAlreadySet.selector, nativeToken)); + + // Set native token again with a different address. + lockup.setNativeToken(address(usdc)); + } + + function test_GivenNativeTokenNotSet() external whenCallerComptroller { + address nativeToken = address(dai); + + // Set native token. + lockup.setNativeToken(nativeToken); + + // It should set native token. + assertEq(lockup.nativeToken(), nativeToken, "native token"); + } +} diff --git a/tests/integration/concrete/lockup/set-native-token/setNativeToken.tree b/tests/integration/concrete/lockup/set-native-token/setNativeToken.tree new file mode 100644 index 000000000..248ae4f67 --- /dev/null +++ b/tests/integration/concrete/lockup/set-native-token/setNativeToken.tree @@ -0,0 +1,8 @@ +SetNativeToken_Integration_Test +├── when caller not comptroller +│ └── it should revert +└── when caller comptroller + ├── given native token already set + │ └── it should revert + └── given native token not set + └── it should set native token diff --git a/tests/integration/concrete/lockup-base/set-nft-descriptor/setNFTDescriptor.t.sol b/tests/integration/concrete/lockup/set-nft-descriptor/setNFTDescriptor.t.sol similarity index 65% rename from tests/integration/concrete/lockup-base/set-nft-descriptor/setNFTDescriptor.t.sol rename to tests/integration/concrete/lockup/set-nft-descriptor/setNFTDescriptor.t.sol index 431017b39..1e311337f 100644 --- a/tests/integration/concrete/lockup-base/set-nft-descriptor/setNFTDescriptor.t.sol +++ b/tests/integration/concrete/lockup/set-nft-descriptor/setNFTDescriptor.t.sol @@ -2,26 +2,31 @@ pragma solidity >=0.8.22 <0.9.0; import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; +import { Errors as EvmUtilsErrors } from "@sablier/evm-utils/src/libraries/Errors.sol"; + import { ILockupNFTDescriptor } from "src/interfaces/ILockupNFTDescriptor.sol"; -import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; -import { Errors } from "src/libraries/Errors.sol"; +import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; import { LockupNFTDescriptor } from "src/LockupNFTDescriptor.sol"; import { Integration_Test } from "../../../Integration.t.sol"; contract SetNFTDescriptor_Integration_Concrete_Test is Integration_Test { - function test_RevertWhen_CallerNotAdmin() external { + function test_RevertWhen_CallerNotComptroller() external { // Make Eve the caller in this test. - resetPrank({ msgSender: users.eve }); + setMsgSender(users.eve); // Run the test. - vm.expectRevert(abi.encodeWithSelector(Errors.CallerNotAdmin.selector, users.admin, users.eve)); + vm.expectRevert( + abi.encodeWithSelector( + EvmUtilsErrors.Comptrollerable_CallerNotComptroller.selector, address(comptroller), users.eve + ) + ); lockup.setNFTDescriptor(ILockupNFTDescriptor(users.eve)); } - function test_WhenProvidedAddressMatchesCurrentNFTDescriptor() external whenCallerAdmin { + function test_WhenProvidedAddressMatchesCurrentNFTDescriptor() external whenCallerComptroller { // It should emit {SetNFTDescriptor} and {BatchMetadataUpdate} events. vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.SetNFTDescriptor(users.admin, nftDescriptor, nftDescriptor); + emit ISablierLockup.SetNFTDescriptor(comptroller, nftDescriptor, nftDescriptor); vm.expectEmit({ emitter: address(lockup) }); emit IERC4906.BatchMetadataUpdate({ _fromTokenId: 1, _toTokenId: lockup.nextStreamId() - 1 }); @@ -29,17 +34,19 @@ contract SetNFTDescriptor_Integration_Concrete_Test is Integration_Test { lockup.setNFTDescriptor(nftDescriptor); // It should re-set the NFT descriptor. - vm.expectCall(address(nftDescriptor), abi.encodeCall(ILockupNFTDescriptor.tokenURI, (lockup, defaultStreamId))); - lockup.tokenURI({ tokenId: defaultStreamId }); + vm.expectCall( + address(nftDescriptor), abi.encodeCall(ILockupNFTDescriptor.tokenURI, (lockup, ids.defaultStream)) + ); + lockup.tokenURI({ tokenId: ids.defaultStream }); } - function test_WhenProvidedAddressNotMatchCurrentNFTDescriptor() external whenCallerAdmin { + function test_WhenProvidedAddressNotMatchCurrentNFTDescriptor() external whenCallerComptroller { // Deploy another NFT descriptor. ILockupNFTDescriptor newNFTDescriptor = new LockupNFTDescriptor(); // It should emit {SetNFTDescriptor} and {BatchMetadataUpdate} events. vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.SetNFTDescriptor(users.admin, nftDescriptor, newNFTDescriptor); + emit ISablierLockup.SetNFTDescriptor(comptroller, nftDescriptor, newNFTDescriptor); vm.expectEmit({ emitter: address(lockup) }); emit IERC4906.BatchMetadataUpdate({ _fromTokenId: 1, _toTokenId: lockup.nextStreamId() - 1 }); @@ -48,6 +55,6 @@ contract SetNFTDescriptor_Integration_Concrete_Test is Integration_Test { // It should set the new NFT descriptor. vm.expectCall(address(newNFTDescriptor), abi.encodeCall(ILockupNFTDescriptor.tokenURI, (lockup, 1))); - lockup.tokenURI({ tokenId: defaultStreamId }); + lockup.tokenURI({ tokenId: ids.defaultStream }); } } diff --git a/tests/integration/concrete/lockup-base/set-nft-descriptor/setNFTDescriptor.tree b/tests/integration/concrete/lockup/set-nft-descriptor/setNFTDescriptor.tree similarity index 87% rename from tests/integration/concrete/lockup-base/set-nft-descriptor/setNFTDescriptor.tree rename to tests/integration/concrete/lockup/set-nft-descriptor/setNFTDescriptor.tree index e50cdba48..e2996e11e 100644 --- a/tests/integration/concrete/lockup-base/set-nft-descriptor/setNFTDescriptor.tree +++ b/tests/integration/concrete/lockup/set-nft-descriptor/setNFTDescriptor.tree @@ -1,7 +1,7 @@ SetNFTDescriptor_Integration_Concrete_Test -├── when caller not admin +├── when caller not comptroller │ └── it should revert -└── when caller admin +└── when caller comptroller ├── when provided address matches current NFT descriptor │ ├── it should re-set the NFT descriptor │ └── it should emit {SetNFTDescriptor} and {BatchMetadataUpdate} events diff --git a/tests/integration/concrete/lockup-base/status-of/statusOf.t.sol b/tests/integration/concrete/lockup/status-of/statusOf.t.sol similarity index 76% rename from tests/integration/concrete/lockup-base/status-of/statusOf.t.sol rename to tests/integration/concrete/lockup/status-of/statusOf.t.sol index 499bc7a80..96b798949 100644 --- a/tests/integration/concrete/lockup-base/status-of/statusOf.t.sol +++ b/tests/integration/concrete/lockup/status-of/statusOf.t.sol @@ -1,40 +1,40 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { Lockup } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; import { Integration_Test } from "../../../Integration.t.sol"; contract StatusOf_Integration_Concrete_Test is Integration_Test { function test_RevertGiven_Null() external { - expectRevert_Null({ callData: abi.encodeCall(lockup.statusOf, nullStreamId) }); + expectRevert_Null({ callData: abi.encodeCall(lockup.statusOf, ids.nullStream) }); } function test_GivenTokensFullyWithdrawn() external givenNotNull { vm.warp({ newTimestamp: defaults.END_TIME() }); - lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); + lockup.withdrawMax{ value: LOCKUP_MIN_FEE_WEI }({ streamId: ids.defaultStream, to: users.recipient }); // It should return DEPLETED. - Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); + Lockup.Status actualStatus = lockup.statusOf(ids.defaultStream); Lockup.Status expectedStatus = Lockup.Status.DEPLETED; assertEq(actualStatus, expectedStatus); } function test_GivenCanceledStream() external givenNotNull givenTokensNotFullyWithdrawn { vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); - lockup.cancel(defaultStreamId); + lockup.cancel(ids.defaultStream); // It should return CANCELED. - Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); + Lockup.Status actualStatus = lockup.statusOf(ids.defaultStream); Lockup.Status expectedStatus = Lockup.Status.CANCELED; assertEq(actualStatus, expectedStatus); } function test_GivenStartTimeInFuture() external givenNotNull givenTokensNotFullyWithdrawn givenNotCanceledStream { - vm.warp({ newTimestamp: getBlockTimestamp() - 1 seconds }); + rewind(1 seconds); // It should return PENDING. - Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); + Lockup.Status actualStatus = lockup.statusOf(ids.defaultStream); Lockup.Status expectedStatus = Lockup.Status.PENDING; assertEq(actualStatus, expectedStatus); } @@ -49,7 +49,7 @@ contract StatusOf_Integration_Concrete_Test is Integration_Test { vm.warp({ newTimestamp: defaults.END_TIME() }); // It should return SETTLED. - Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); + Lockup.Status actualStatus = lockup.statusOf(ids.defaultStream); Lockup.Status expectedStatus = Lockup.Status.SETTLED; assertEq(actualStatus, expectedStatus); } @@ -64,7 +64,7 @@ contract StatusOf_Integration_Concrete_Test is Integration_Test { vm.warp({ newTimestamp: defaults.START_TIME() + 1 seconds }); // It should return STREAMING. - Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); + Lockup.Status actualStatus = lockup.statusOf(ids.defaultStream); Lockup.Status expectedStatus = Lockup.Status.STREAMING; assertEq(actualStatus, expectedStatus); } diff --git a/tests/integration/concrete/lockup-base/status-of/statusOf.tree b/tests/integration/concrete/lockup/status-of/statusOf.tree similarity index 100% rename from tests/integration/concrete/lockup-base/status-of/statusOf.tree rename to tests/integration/concrete/lockup/status-of/statusOf.tree diff --git a/tests/integration/concrete/lockup-base/streamed-amount-of/streamedAmountOf.t.sol b/tests/integration/concrete/lockup/streamed-amount-of/streamedAmountOf.t.sol similarity index 75% rename from tests/integration/concrete/lockup-base/streamed-amount-of/streamedAmountOf.t.sol rename to tests/integration/concrete/lockup/streamed-amount-of/streamedAmountOf.t.sol index 14cef8fd8..8acd5943b 100644 --- a/tests/integration/concrete/lockup-base/streamed-amount-of/streamedAmountOf.t.sol +++ b/tests/integration/concrete/lockup/streamed-amount-of/streamedAmountOf.t.sol @@ -5,15 +5,15 @@ import { Integration_Test } from "../../../Integration.t.sol"; abstract contract StreamedAmountOf_Integration_Concrete_Test is Integration_Test { function test_RevertGiven_Null() external { - expectRevert_Null({ callData: abi.encodeCall(lockup.streamedAmountOf, nullStreamId) }); + expectRevert_Null({ callData: abi.encodeCall(lockup.streamedAmountOf, ids.nullStream) }); } function test_GivenCanceledStreamAndCANCELEDStatus() external givenNotNull { vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); - lockup.cancel(defaultStreamId); + lockup.cancel(ids.defaultStream); // It should return the correct streamed amount. - uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); + uint128 actualStreamedAmount = lockup.streamedAmountOf(ids.defaultStream); uint256 expectedStreamedAmount = defaults.STREAMED_AMOUNT_26_PERCENT(); assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } @@ -21,23 +21,23 @@ abstract contract StreamedAmountOf_Integration_Concrete_Test is Integration_Test /// @dev This test warps a second time to ensure that {streamedAmountOf} ignores the current time. function test_GivenCanceledStreamAndDEPLETEDStatus() external givenNotNull { vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); - lockup.cancel(defaultStreamId); + lockup.cancel(ids.defaultStream); // Withdraw max to deplete the stream. - lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); + lockup.withdrawMax{ value: LOCKUP_MIN_FEE_WEI }({ streamId: ids.defaultStream, to: users.recipient }); vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() + 10 seconds }); // It should return the correct streamed amount. - uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); + uint128 actualStreamedAmount = lockup.streamedAmountOf(ids.defaultStream); uint128 expectedStreamedAmount = defaults.STREAMED_AMOUNT_26_PERCENT(); assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } function test_GivenPENDINGStatus() external givenNotNull givenNotCanceledStream { - vm.warp({ newTimestamp: getBlockTimestamp() - 1 seconds }); + rewind(1 seconds); // It should return zero. - uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); + uint128 actualStreamedAmount = lockup.streamedAmountOf(ids.defaultStream); uint128 expectedStreamedAmount = 0; assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } @@ -46,7 +46,7 @@ abstract contract StreamedAmountOf_Integration_Concrete_Test is Integration_Test vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); // It should return the correct streamed amount. - uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); + uint128 actualStreamedAmount = lockup.streamedAmountOf(ids.defaultStream); uint128 expectedStreamedAmount = defaults.STREAMED_AMOUNT_26_PERCENT(); assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } @@ -55,7 +55,7 @@ abstract contract StreamedAmountOf_Integration_Concrete_Test is Integration_Test vm.warp({ newTimestamp: defaults.END_TIME() }); // It should return the deposited amount. - uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); + uint128 actualStreamedAmount = lockup.streamedAmountOf(ids.defaultStream); uint128 expectedStreamedAmount = defaults.DEPOSIT_AMOUNT(); assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } @@ -63,10 +63,10 @@ abstract contract StreamedAmountOf_Integration_Concrete_Test is Integration_Test function test_GivenDEPLETEDStatus() external givenNotNull givenNotCanceledStream { vm.warp({ newTimestamp: defaults.END_TIME() }); // Withdraw max to deplete the stream. - lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); + lockup.withdrawMax{ value: LOCKUP_MIN_FEE_WEI }({ streamId: ids.defaultStream, to: users.recipient }); // It should return the deposited amount. - uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); + uint128 actualStreamedAmount = lockup.streamedAmountOf(ids.defaultStream); uint128 expectedStreamedAmount = defaults.DEPOSIT_AMOUNT(); assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } diff --git a/tests/integration/concrete/lockup-base/streamed-amount-of/streamedAmountOf.tree b/tests/integration/concrete/lockup/streamed-amount-of/streamedAmountOf.tree similarity index 100% rename from tests/integration/concrete/lockup-base/streamed-amount-of/streamedAmountOf.tree rename to tests/integration/concrete/lockup/streamed-amount-of/streamedAmountOf.tree diff --git a/tests/integration/concrete/lockup-base/token-uri/tokenURI.t.sol b/tests/integration/concrete/lockup/token-uri/tokenURI.t.sol similarity index 99% rename from tests/integration/concrete/lockup-base/token-uri/tokenURI.t.sol rename to tests/integration/concrete/lockup/token-uri/tokenURI.t.sol index 5cc891e43..4b77eadb8 100644 --- a/tests/integration/concrete/lockup-base/token-uri/tokenURI.t.sol +++ b/tests/integration/concrete/lockup/token-uri/tokenURI.t.sol @@ -29,8 +29,8 @@ contract TokenURI_Lockup_Integration_Concrete_Test is Integration_Test { } function test_RevertGiven_NFTNotExist() external { - vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, nullStreamId)); - lockup.tokenURI({ tokenId: nullStreamId }); + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, ids.nullStream)); + lockup.tokenURI({ tokenId: ids.nullStream }); } /// @dev If you need to update the hard-coded token URI: @@ -39,7 +39,7 @@ contract TokenURI_Lockup_Integration_Concrete_Test is Integration_Test { function test_WhenTokenURIDecoded() external skipOnMismatch givenNFTExists { vm.warp({ newTimestamp: defaults.START_TIME() + defaults.TOTAL_DURATION() / 4 }); - string memory tokenURI = lockup.tokenURI(defaultStreamId); + string memory tokenURI = lockup.tokenURI(ids.defaultStream); tokenURI = vm.replace({ input: tokenURI, from: "data:application/json;base64,", to: "" }); string memory actualDecodedTokenURI = string(Base64.decode(tokenURI)); string memory expectedDecodedTokenURI = @@ -50,7 +50,7 @@ contract TokenURI_Lockup_Integration_Concrete_Test is Integration_Test { function test_WhenTokenURINotDecoded() external skipOnMismatch givenNFTExists { vm.warp({ newTimestamp: defaults.START_TIME() + defaults.TOTAL_DURATION() / 4 }); - string memory actualTokenURI = lockup.tokenURI(defaultStreamId); + string memory actualTokenURI = lockup.tokenURI(ids.defaultStream); console2.log("actualTokenURI", actualTokenURI); string memory expectedTokenURI = "data:application/json;base64,eyJhdHRyaWJ1dGVzIjpbeyJ0cmFpdF90eXBlIjoiVG9rZW4iLCJ2YWx1ZSI6IkRBSSJ9LHsidHJhaXRfdHlwZSI6IlNlbmRlciIsInZhbHVlIjoiMHg2MzMyZTdiMWRlYjFmMWEwYjc3YjJiYjE4YjE0NDMzMGM3MjkxYmNhIn0seyJ0cmFpdF90eXBlIjoiU3RhdHVzIiwidmFsdWUiOiJTdHJlYW1pbmcifV0sImRlc2NyaXB0aW9uIjoiVGhpcyBORlQgcmVwcmVzZW50cyBhIHN0cmVhbSBpbiBhIFNhYmxpZXIgTG9ja3VwIGNvbnRyYWN0LiBUaGUgb3duZXIgb2YgdGhpcyBORlQgY2FuIHdpdGhkcmF3IHRoZSBzdHJlYW1lZCB0b2tlbnMsIHdoaWNoIGFyZSBkZW5vbWluYXRlZCBpbiBEQUkuXG5cbi0gU3RyZWFtIElEOiAxXG4tIFNhYmxpZXIgTG9ja3VwIEFkZHJlc3M6IDB4OTIzYjVhYjM3MTRmZDM0MzMxNmFmNWE1NDM0NTgyZmQxNjcyMjUyM1xuLSBEQUkgQWRkcmVzczogMHhmNjI4NDlmOWEwYjViZjI5MTNiMzk2MDk4ZjdjNzAxOWI1MWE4MjBhXG5cbuKaoO+4jyBXQVJOSU5HOiBUcmFuc2ZlcnJpbmcgdGhlIE5GVCBtYWtlcyB0aGUgbmV3IG93bmVyIHRoZSByZWNpcGllbnQgb2YgdGhlIHN0cmVhbS4gVGhlIGZ1bmRzIGFyZSBub3QgYXV0b21hdGljYWxseSB3aXRoZHJhd24gZm9yIHRoZSBwcmV2aW91cyByZWNpcGllbnQuIiwiZXh0ZXJuYWxfdXJsIjoiaHR0cHM6Ly9zYWJsaWVyLmNvbSIsIm5hbWUiOiJTYWJsaWVyIExvY2t1cCAjMSIsImltYWdlIjoiZGF0YTppbWFnZS9zdmcreG1sO2Jhc2U2NCxQSE4yWnlCNGJXeHVjejBpYUhSMGNEb3ZMM2QzZHk1M015NXZjbWN2TWpBd01DOXpkbWNpSUhkcFpIUm9QU0l4TURBd0lpQm9aV2xuYUhROUlqRXdNREFpSUhacFpYZENiM2c5SWpBZ01DQXhNREF3SURFd01EQWlQanh5WldOMElIZHBaSFJvUFNJeE1EQWxJaUJvWldsbmFIUTlJakV3TUNVaUlHWnBiSFJsY2owaWRYSnNLQ05PYjJselpTa2lMejQ4Y21WamRDQjRQU0kzTUNJZ2VUMGlOekFpSUhkcFpIUm9QU0k0TmpBaUlHaGxhV2RvZEQwaU9EWXdJaUJtYVd4c1BTSWpabVptSWlCbWFXeHNMVzl3WVdOcGRIazlJaTR3TXlJZ2NuZzlJalExSWlCeWVUMGlORFVpSUhOMGNtOXJaVDBpSTJabVppSWdjM1J5YjJ0bExXOXdZV05wZEhrOUlpNHhJaUJ6ZEhKdmEyVXRkMmxrZEdnOUlqUWlMejQ4WkdWbWN6NDhZMmx5WTJ4bElHbGtQU0pIYkc5M0lpQnlQU0kxTURBaUlHWnBiR3c5SW5WeWJDZ2pVbUZrYVdGc1IyeHZkeWtpTHo0OFptbHNkR1Z5SUdsa1BTSk9iMmx6WlNJK1BHWmxSbXh2YjJRZ2VEMGlNQ0lnZVQwaU1DSWdkMmxrZEdnOUlqRXdNQ1VpSUdobGFXZG9kRDBpTVRBd0pTSWdabXh2YjJRdFkyOXNiM0k5SW1oemJDZ3lNekFzTWpFbExERXhKU2tpSUdac2IyOWtMVzl3WVdOcGRIazlJakVpSUhKbGMzVnNkRDBpWm14dmIyUkdhV3hzSWk4K1BHWmxWSFZ5WW5Wc1pXNWpaU0JpWVhObFJuSmxjWFZsYm1ONVBTSXVOQ0lnYm5WdFQyTjBZWFpsY3owaU15SWdjbVZ6ZFd4MFBTSk9iMmx6WlNJZ2RIbHdaVDBpWm5KaFkzUmhiRTV2YVhObElpOCtQR1psUW14bGJtUWdhVzQ5SWs1dmFYTmxJaUJwYmpJOUltWnNiMjlrUm1sc2JDSWdiVzlrWlQwaWMyOW1kQzFzYVdkb2RDSXZQand2Wm1sc2RHVnlQanh3WVhSb0lHbGtQU0pNYjJkdklpQm1hV3hzUFNJalptWm1JaUJtYVd4c0xXOXdZV05wZEhrOUlpNHhJaUJrUFNKdE1UTXpMalUxT1N3eE1qUXVNRE0wWXkwdU1ERXpMREl1TkRFeUxURXVNRFU1TERRdU9EUTRMVEl1T1RJekxEWXVOREF5TFRJdU5UVTRMREV1T0RFNUxUVXVNVFk0TERNdU5ETTVMVGN1T0RnNExEUXVPVGsyTFRFMExqUTBMRGd1TWpZeUxUTXhMakEwTnl3eE1pNDFOalV0TkRjdU5qYzBMREV5TGpVMk9TMDRMamcxT0M0d016WXRNVGN1T0RNNExURXVNamN5TFRJMkxqTXlPQzB6TGpZMk15MDVMamd3TmkweUxqYzJOaTB4T1M0d09EY3ROeTR4TVRNdE1qY3VOVFl5TFRFeUxqYzNPQzB4TXk0NE5ESXRPQzR3TWpVc09TNDBOamd0TWpndU5qQTJMREUyTGpFMU15MHpOUzR5TmpWb01HTXlMakF6TlMweExqZ3pPQ3cwTGpJMU1pMHpMalUwTml3MkxqUTJNeTAxTGpJeU5HZ3dZell1TkRJNUxUVXVOalUxTERFMkxqSXhPQzB5TGpnek5Td3lNQzR6TlRnc05DNHhOeXcwTGpFME15dzFMakExTnl3NExqZ3hOaXc1TGpZME9Td3hNeTQ1TWl3eE15NDNNelJvTGpBek4yTTFMamN6Tml3MkxqUTJNU3d4TlM0ek5UY3RNaTR5TlRNc09TNHpPQzA0TGpRNExEQXNNQzB6TGpVeE5TMHpMalV4TlMwekxqVXhOUzB6TGpVeE5TMHhNUzQwT1MweE1TNDBOemd0TlRJdU5qVTJMVFV5TGpZMk5DMDJOQzQ0TXpjdE5qUXVPRE0zYkM0d05Ea3RMakF6TjJNdE1TNDNNalV0TVM0Mk1EWXRNaTQzTVRrdE15NDRORGN0TWk0M05URXROaTR5TURSb01HTXRMakEwTmkweUxqTTNOU3d4TGpBMk1pMDBMalU0TWl3eUxqY3lOaTAyTGpJeU9XZ3diQzR4T0RVdExqRTBPR2d3WXk0d09Ua3RMakEyTWl3dU1qSXlMUzR4TkRnc0xqTTNMUzR5TlRsb01HTXlMakEyTFRFdU16WXlMRE11T1RVeExUSXVOakl4TERZdU1EUTBMVE11T0RReVF6VTNMamMyTXkwekxqUTNNeXc1Tnk0M05pMHlMak0wTVN3eE1qZ3VOak0zTERFNExqTXpNbU14Tmk0Mk56RXNPUzQ1TkRZdE1qWXVNelEwTERVMExqZ3hNeTB6T0M0Mk5URXNOREF1TVRrNUxUWXVNams1TFRZdU1EazJMVEU0TGpBMk15MHhOeTQzTkRNdE1Ua3VOalk0TFRFNExqZ3hNUzAyTGpBeE5pMDBMakEwTnkweE15NHdOakVzTkM0M056WXROeTQzTlRJc09TNDNOVEZzTmpndU1qVTBMRFk0TGpNM01XTXhMamN5TkN3eExqWXdNU3d5TGpjeE5Dd3pMamcwTERJdU56TTRMRFl1TVRreVdpSXZQanh3WVhSb0lHbGtQU0pHYkc5aGRHbHVaMVJsZUhRaUlHWnBiR3c5SW01dmJtVWlJR1E5SWsweE1qVWdORFZvTnpVd2N6Z3dJREFnT0RBZ09EQjJOelV3Y3pBZ09EQWdMVGd3SURnd2FDMDNOVEJ6TFRnd0lEQWdMVGd3SUMwNE1IWXROelV3Y3pBZ0xUZ3dJRGd3SUMwNE1DSXZQanh5WVdScFlXeEhjbUZrYVdWdWRDQnBaRDBpVW1Ga2FXRnNSMnh2ZHlJK1BITjBiM0FnYjJabWMyVjBQU0l3SlNJZ2MzUnZjQzFqYjJ4dmNqMGlhSE5zS0RZekxEazFKU3cxTnlVcElpQnpkRzl3TFc5d1lXTnBkSGs5SWk0MklpOCtQSE4wYjNBZ2IyWm1jMlYwUFNJeE1EQWxJaUJ6ZEc5d0xXTnZiRzl5UFNKb2Myd29Nak13TERJeEpTd3hNU1VwSWlCemRHOXdMVzl3WVdOcGRIazlJakFpTHo0OEwzSmhaR2xoYkVkeVlXUnBaVzUwUGp4c2FXNWxZWEpIY21Ga2FXVnVkQ0JwWkQwaVUyRnVaRlJ2Y0NJZ2VERTlJakFsSWlCNU1UMGlNQ1VpUGp4emRHOXdJRzltWm5ObGREMGlNQ1VpSUhOMGIzQXRZMjlzYjNJOUltaHpiQ2cyTXl3NU5TVXNOVGNsS1NJdlBqeHpkRzl3SUc5bVpuTmxkRDBpTVRBd0pTSWdjM1J2Y0MxamIyeHZjajBpYUhOc0tESXpNQ3d5TVNVc01URWxLU0l2UGp3dmJHbHVaV0Z5UjNKaFpHbGxiblErUEd4cGJtVmhja2R5WVdScFpXNTBJR2xrUFNKVFlXNWtRbTkwZEc5dElpQjRNVDBpTVRBd0pTSWdlVEU5SWpFd01DVWlQanh6ZEc5d0lHOW1abk5sZEQwaU1UQWxJaUJ6ZEc5d0xXTnZiRzl5UFNKb2Myd29Nak13TERJeEpTd3hNU1VwSWk4K1BITjBiM0FnYjJabWMyVjBQU0l4TURBbElpQnpkRzl3TFdOdmJHOXlQU0pvYzJ3b05qTXNPVFVsTERVM0pTa2lMejQ4WVc1cGJXRjBaU0JoZEhSeWFXSjFkR1ZPWVcxbFBTSjRNU0lnWkhWeVBTSTJjeUlnY21Wd1pXRjBRMjkxYm5ROUltbHVaR1ZtYVc1cGRHVWlJSFpoYkhWbGN6MGlNekFsT3pZd0pUc3hNakFsT3pZd0pUc3pNQ1U3SWk4K1BDOXNhVzVsWVhKSGNtRmthV1Z1ZEQ0OGJHbHVaV0Z5UjNKaFpHbGxiblFnYVdROUlraHZkWEpuYkdGemMxTjBjbTlyWlNJZ1ozSmhaR2xsYm5SVWNtRnVjMlp2Y20wOUluSnZkR0YwWlNnNU1Da2lJR2R5WVdScFpXNTBWVzVwZEhNOUluVnpaWEpUY0dGalpVOXVWWE5sSWo0OGMzUnZjQ0J2Wm1aelpYUTlJalV3SlNJZ2MzUnZjQzFqYjJ4dmNqMGlhSE5zS0RZekxEazFKU3cxTnlVcElpOCtQSE4wYjNBZ2IyWm1jMlYwUFNJNE1DVWlJSE4wYjNBdFkyOXNiM0k5SW1oemJDZ3lNekFzTWpFbExERXhKU2tpTHo0OEwyeHBibVZoY2tkeVlXUnBaVzUwUGp4bklHbGtQU0pJYjNWeVoyeGhjM01pUGp4d1lYUm9JR1E5SWswZ05UQXNNell3SUdFZ016QXdMRE13TUNBd0lERXNNU0EyTURBc01DQmhJRE13TUN3ek1EQWdNQ0F4TERFZ0xUWXdNQ3d3SWlCbWFXeHNQU0lqWm1abUlpQm1hV3hzTFc5d1lXTnBkSGs5SWk0d01pSWdjM1J5YjJ0bFBTSjFjbXdvSTBodmRYSm5iR0Z6YzFOMGNtOXJaU2tpSUhOMGNtOXJaUzEzYVdSMGFEMGlOQ0l2UGp4d1lYUm9JR1E5SW0wMU5qWXNNVFl4TGpJd01YWXROVE11T1RJMFl6QXRNVGt1TXpneUxUSXlMalV4TXkwek55NDFOak10TmpNdU16azRMVFV4TGpFNU9DMDBNQzQzTlRZdE1UTXVOVGt5TFRrMExqazBOaTB5TVM0d056a3RNVFV5TGpVNE55MHlNUzR3TnpsekxURXhNUzQ0TXpnc055NDBPRGN0TVRVeUxqWXdNaXd5TVM0d056bGpMVFF3TGpnNU15d3hNeTQyTXpZdE5qTXVOREV6TERNeExqZ3hOaTAyTXk0ME1UTXNOVEV1TVRrNGRqVXpMamt5TkdNd0xERTNMakU0TVN3eE55NDNNRFFzTXpNdU5ESTNMRFV3TGpJeU15dzBOaTR6T1RSMk1qZzBMamd3T1dNdE16SXVOVEU1TERFeUxqazJMVFV3TGpJeU15d3lPUzR5TURZdE5UQXVNakl6TERRMkxqTTVOSFkxTXk0NU1qUmpNQ3d4T1M0ek9ESXNNakl1TlRJc016Y3VOVFl6TERZekxqUXhNeXcxTVM0eE9UZ3NOREF1TnpZekxERXpMalU1TWl3NU5DNDVOVFFzTWpFdU1EYzVMREUxTWk0Mk1ESXNNakV1TURjNWN6RXhNUzQ0TXpFdE55NDBPRGNzTVRVeUxqVTROeTB5TVM0d056bGpOREF1T0RnMkxURXpMall6Tml3Mk15NHpPVGd0TXpFdU9ERTJMRFl6TGpNNU9DMDFNUzR4T1RoMkxUVXpMamt5TkdNd0xURTNMakU1TmkweE55NDNNRFF0TXpNdU5ETTFMVFV3TGpJeU15MDBOaTQwTURGV01qQTNMall3TTJNek1pNDFNVGt0TVRJdU9UWTNMRFV3TGpJeU15MHlPUzR5TURZc05UQXVNakl6TFRRMkxqUXdNVnB0TFRNME55NDBOaklzTlRjdU56a3piREV6TUM0NU5Ua3NNVE14TGpBeU55MHhNekF1T1RVNUxERXpNUzR3TVROV01qRTRMams1TkZwdE1qWXlMamt5TkM0d01qSjJNall5TGpBeE9Hd3RNVE13TGprek55MHhNekV1TURBMkxERXpNQzQ1TXpjdE1UTXhMakF4TTFvaUlHWnBiR3c5SWlNeE5qRTRNaklpUGp3dmNHRjBhRDQ4Y0c5c2VXZHZiaUJ3YjJsdWRITTlJak0xTUNBek5UQXVNREkySURReE5TNHdNeUF5T0RRdU9UYzRJREk0TlNBeU9EUXVPVGM0SURNMU1DQXpOVEF1TURJMklpQm1hV3hzUFNKMWNtd29JMU5oYm1SQ2IzUjBiMjBwSWk4K1BIQmhkR2dnWkQwaWJUUXhOaTR6TkRFc01qZ3hMamszTldNd0xDNDVNVFF0TGpNMU5Dd3hMamd3T1MweExqQXpOU3d5TGpZNExUVXVOVFF5TERjdU1EYzJMVE15TGpZMk1Td3hNaTQwTlMwMk5TNHlPQ3d4TWk0ME5TMHpNaTQyTWpRc01DMDFPUzQzTXpndE5TNHpOelF0TmpVdU1qZ3RNVEl1TkRVdExqWTRNUzB1T0RjeUxURXVNRE0xTFRFdU56WTNMVEV1TURNMUxUSXVOamdzTUMwdU9URTBMak0xTkMweExqZ3dPQ3d4TGpBek5TMHlMalkzTml3MUxqVTBNaTAzTGpBM05pd3pNaTQyTlRZdE1USXVORFVzTmpVdU1qZ3RNVEl1TkRVc016SXVOakU1TERBc05Ua3VOek00TERVdU16YzBMRFkxTGpJNExERXlMalExTGpZNE1TNDROamNzTVM0d016VXNNUzQzTmpJc01TNHdNelVzTWk0Mk56WmFJaUJtYVd4c1BTSjFjbXdvSTFOaGJtUlViM0FwSWk4K1BIQmhkR2dnWkQwaWJUUTRNUzQwTml3MU1EUXVNVEF4ZGpVNExqUTBPV010TWk0ek5TNDNOeTAwTGpneUxERXVOVEV0Tnk0ek9Td3lMakl6TFRNd0xqTXNPQzQxTkMwM05DNDJOU3d4TXk0NU1pMHhNalF1TURZc01UTXVPVEl0TlRNdU5pd3dMVEV3TVM0eU5DMDJMak16TFRFek1TNDBOeTB4Tmk0eE5uWXROVGd1TkRNNWFESTJNaTQ1TWxvaUlHWnBiR3c5SW5WeWJDZ2pVMkZ1WkVKdmRIUnZiU2tpTHo0OFpXeHNhWEJ6WlNCamVEMGlNelV3SWlCamVUMGlOVEEwTGpFd01TSWdjbmc5SWpFek1TNDBOaklpSUhKNVBTSXlPQzR4TURnaUlHWnBiR3c5SW5WeWJDZ2pVMkZ1WkZSdmNDa2lMejQ4WnlCbWFXeHNQU0p1YjI1bElpQnpkSEp2YTJVOUluVnliQ2dqU0c5MWNtZHNZWE56VTNSeWIydGxLU0lnYzNSeWIydGxMV3hwYm1WallYQTlJbkp2ZFc1a0lpQnpkSEp2YTJVdGJXbDBaWEpzYVcxcGREMGlNVEFpSUhOMGNtOXJaUzEzYVdSMGFEMGlOQ0krUEhCaGRHZ2daRDBpYlRVMk5TNDJOREVzTVRBM0xqSTRZekFzT1M0MU16Y3ROUzQxTml3eE9DNDJNamt0TVRVdU5qYzJMREkyTGprM00yZ3RMakF5TTJNdE9TNHlNRFFzTnk0MU9UWXRNakl1TVRrMExERTBMalUyTWkwek9DNHhPVGNzTWpBdU5Ua3lMVE01TGpVd05Dd3hOQzQ1TXpZdE9UY3VNekkxTERJMExqTTFOUzB4TmpFdU56TXpMREkwTGpNMU5TMDVNQzQwT0N3d0xURTJOeTQ1TkRndE1UZ3VOVGd5TFRFNU9TNDVOVE10TkRRdU9UUTRhQzB1TURJell5MHhNQzR4TVRVdE9DNHpORFF0TVRVdU5qYzJMVEUzTGpRek55MHhOUzQyTnpZdE1qWXVPVGN6TERBdE16a3VOek0xTERrMkxqVTFOQzAzTVM0NU1qRXNNakUxTGpZMU1pMDNNUzQ1TWpGek1qRTFMall5T1N3ek1pNHhPRFVzTWpFMUxqWXlPU3czTVM0NU1qRmFJaTgrUEhCaGRHZ2daRDBpYlRFek5DNHpOaXd4TmpFdU1qQXpZekFzTXprdU56TTFMRGsyTGpVMU5DdzNNUzQ1TWpFc01qRTFMalkxTWl3M01TNDVNakZ6TWpFMUxqWXlPUzB6TWk0eE9EWXNNakUxTGpZeU9TMDNNUzQ1TWpFaUx6NDhiR2x1WlNCNE1UMGlNVE0wTGpNMklpQjVNVDBpTVRZeExqSXdNeUlnZURJOUlqRXpOQzR6TmlJZ2VUSTlJakV3Tnk0eU9DSXZQanhzYVc1bElIZ3hQU0kxTmpVdU5qUWlJSGt4UFNJeE5qRXVNakF6SWlCNE1qMGlOVFkxTGpZMElpQjVNajBpTVRBM0xqSTRJaTgrUEd4cGJtVWdlREU5SWpFNE5DNDFPRFFpSUhreFBTSXlNRFl1T0RJeklpQjRNajBpTVRnMExqVTROU0lnZVRJOUlqVXpOeTQxTnpraUx6NDhiR2x1WlNCNE1UMGlNakU0TGpFNE1TSWdlVEU5SWpJeE9DNHhNVGdpSUhneVBTSXlNVGd1TVRneElpQjVNajBpTlRZeUxqVXpOeUl2UGp4c2FXNWxJSGd4UFNJME9ERXVPREU0SWlCNU1UMGlNakU0TGpFME1pSWdlREk5SWpRNE1TNDRNVGtpSUhreVBTSTFOakl1TkRJNElpOCtQR3hwYm1VZ2VERTlJalV4TlM0ME1UVWlJSGt4UFNJeU1EY3VNelV5SWlCNE1qMGlOVEUxTGpReE5pSWdlVEk5SWpVek55NDFOemtpTHo0OGNHRjBhQ0JrUFNKdE1UZzBMalU0TERVek55NDFPR013TERVdU5EVXNOQzR5Tnl3eE1DNDJOU3d4TWk0d015d3hOUzQwTW1ndU1ESmpOUzQxTVN3ekxqTTVMREV5TGpjNUxEWXVOVFVzTWpFdU5UVXNPUzQwTWl3ek1DNHlNU3c1TGprc056Z3VNRElzTVRZdU1qZ3NNVE14TGpnekxERTJMakk0TERRNUxqUXhMREFzT1RNdU56WXROUzR6T0N3eE1qUXVNRFl0TVRNdU9USXNNaTQzTFM0M05pdzFMakk1TFRFdU5UUXNOeTQzTlMweUxqTTFMRGd1TnpjdE1pNDROeXd4Tmk0d05TMDJMakEwTERJeExqVTJMVGt1TkROb01HTTNMamMyTFRRdU56Y3NNVEl1TURRdE9TNDVOeXd4TWk0d05DMHhOUzQwTWlJdlBqeHdZWFJvSUdROUltMHhPRFF1TlRneUxEUTVNaTQyTlRaakxUTXhMak0xTkN3eE1pNDBPRFV0TlRBdU1qSXpMREk0TGpVNExUVXdMakl5TXl3ME5pNHhORElzTUN3NUxqVXpOaXcxTGpVMk5Dd3hPQzQyTWpjc01UVXVOamMzTERJMkxqazJPV2d1TURJeVl6Z3VOVEF6TERjdU1EQTFMREl3TGpJeE15d3hNeTQwTmpNc016UXVOVEkwTERFNUxqRTFPU3c1TGprNU9Td3pMams1TVN3eU1TNHlOamtzTnk0Mk1Ea3NNek11TlRrM0xERXdMamM0T0N3ek5pNDBOU3c1TGpRd055dzRNaTR4T0RFc01UVXVNREF5TERFek1TNDRNelVzTVRVdU1EQXljemsxTGpNMk15MDFMalU1TlN3eE16RXVPREEzTFRFMUxqQXdNbU14TUM0NE5EY3RNaTQzT1N3eU1DNDROamN0TlM0NU1qWXNNamt1T1RJMExUa3VNelE1TERFdU1qUTBMUzQwTmpjc01pNDBOek10TGprME1pd3pMalkzTXkweExqUXlOQ3d4TkM0ek1qWXROUzQyT1RZc01qWXVNRE0xTFRFeUxqRTJNU3d6TkM0MU1qUXRNVGt1TVRjemFDNHdNakpqTVRBdU1URTBMVGd1TXpReUxERTFMalkzTnkweE55NDBNek1zTVRVdU5qYzNMVEkyTGprMk9Td3dMVEUzTGpVMk1pMHhPQzQ0TmprdE16TXVOalkxTFRVd0xqSXlNeTAwTmk0eE5TSXZQanh3WVhSb0lHUTlJbTB4TXpRdU16WXNOVGt5TGpjeVl6QXNNemt1TnpNMUxEazJMalUxTkN3M01TNDVNakVzTWpFMUxqWTFNaXczTVM0NU1qRnpNakUxTGpZeU9TMHpNaTR4T0RZc01qRTFMall5T1MwM01TNDVNakVpTHo0OGJHbHVaU0I0TVQwaU1UTTBMak0ySWlCNU1UMGlOVGt5TGpjeUlpQjRNajBpTVRNMExqTTJJaUI1TWowaU5UTTRMamM1TnlJdlBqeHNhVzVsSUhneFBTSTFOalV1TmpRaUlIa3hQU0kxT1RJdU56SWlJSGd5UFNJMU5qVXVOalFpSUhreVBTSTFNemd1TnprM0lpOCtQSEJ2Ykhsc2FXNWxJSEJ2YVc1MGN6MGlORGd4TGpneU1pQTBPREV1T1RBeElEUTRNUzQzT1RnZ05EZ3hMamczTnlBME9ERXVOemMxSURRNE1TNDROVFFnTXpVd0xqQXhOU0F6TlRBdU1ESTJJREl4T0M0eE9EVWdNakU0TGpFeU9TSXZQanh3YjJ4NWJHbHVaU0J3YjJsdWRITTlJakl4T0M0eE9EVWdORGd4TGprd01TQXlNVGd1TWpNeElEUTRNUzQ0TlRRZ016VXdMakF4TlNBek5UQXVNREkySURRNE1TNDRNaklnTWpFNExqRTFNaUl2UGp3dlp6NDhMMmMrUEdjZ2FXUTlJbEJ5YjJkeVpYTnpJaUJtYVd4c1BTSWpabVptSWo0OGNtVmpkQ0IzYVdSMGFEMGlNakE0SWlCb1pXbG5hSFE5SWpFd01DSWdabWxzYkMxdmNHRmphWFI1UFNJdU1ETWlJSEo0UFNJeE5TSWdjbms5SWpFMUlpQnpkSEp2YTJVOUlpTm1abVlpSUhOMGNtOXJaUzF2Y0dGamFYUjVQU0l1TVNJZ2MzUnliMnRsTFhkcFpIUm9QU0kwSWk4K1BIUmxlSFFnZUQwaU1qQWlJSGs5SWpNMElpQm1iMjUwTFdaaGJXbHNlVDBpSjBOdmRYSnBaWElnVG1WM0p5eEJjbWxoYkN4dGIyNXZjM0JoWTJVaUlHWnZiblF0YzJsNlpUMGlNakp3ZUNJK1VISnZaM0psYzNNOEwzUmxlSFErUEhSbGVIUWdlRDBpTWpBaUlIazlJamN5SWlCbWIyNTBMV1poYldsc2VUMGlKME52ZFhKcFpYSWdUbVYzSnl4QmNtbGhiQ3h0YjI1dmMzQmhZMlVpSUdadmJuUXRjMmw2WlQwaU1qWndlQ0krTWpJdU9UZ2xQQzkwWlhoMFBqeG5JR1pwYkd3OUltNXZibVVpUGp4amFYSmpiR1VnWTNnOUlqRTJOaUlnWTNrOUlqVXdJaUJ5UFNJeU1pSWdjM1J5YjJ0bFBTSm9jMndvTWpNd0xESXhKU3d4TVNVcElpQnpkSEp2YTJVdGQybGtkR2c5SWpFd0lpOCtQR05wY21Oc1pTQmplRDBpTVRZMklpQmplVDBpTlRBaUlIQmhkR2hNWlc1bmRHZzlJakV3TURBd0lpQnlQU0l5TWlJZ2MzUnliMnRsUFNKb2Myd29Oak1zT1RVbExEVTNKU2tpSUhOMGNtOXJaUzFrWVhOb1lYSnlZWGs5SWpFd01EQXdJaUJ6ZEhKdmEyVXRaR0Z6YUc5bVpuTmxkRDBpTnpjd01pSWdjM1J5YjJ0bExXeHBibVZqWVhBOUluSnZkVzVrSWlCemRISnZhMlV0ZDJsa2RHZzlJalVpSUhSeVlXNXpabTl5YlQwaWNtOTBZWFJsS0MwNU1Da2lJSFJ5WVc1elptOXliUzF2Y21sbmFXNDlJakUyTmlBMU1DSXZQand2Wno0OEwyYytQR2NnYVdROUlsTjBZWFIxY3lJZ1ptbHNiRDBpSTJabVppSStQSEpsWTNRZ2QybGtkR2c5SWpFNE5DSWdhR1ZwWjJoMFBTSXhNREFpSUdacGJHd3RiM0JoWTJsMGVUMGlMakF6SWlCeWVEMGlNVFVpSUhKNVBTSXhOU0lnYzNSeWIydGxQU0lqWm1abUlpQnpkSEp2YTJVdGIzQmhZMmwwZVQwaUxqRWlJSE4wY205clpTMTNhV1IwYUQwaU5DSXZQangwWlhoMElIZzlJakl3SWlCNVBTSXpOQ0lnWm05dWRDMW1ZVzFwYkhrOUlpZERiM1Z5YVdWeUlFNWxkeWNzUVhKcFlXd3NiVzl1YjNOd1lXTmxJaUJtYjI1MExYTnBlbVU5SWpJeWNIZ2lQbE4wWVhSMWN6d3ZkR1Y0ZEQ0OGRHVjRkQ0I0UFNJeU1DSWdlVDBpTnpJaUlHWnZiblF0Wm1GdGFXeDVQU0luUTI5MWNtbGxjaUJPWlhjbkxFRnlhV0ZzTEcxdmJtOXpjR0ZqWlNJZ1ptOXVkQzF6YVhwbFBTSXlObkI0SWo1VGRISmxZVzFwYm1jOEwzUmxlSFErUEM5blBqeG5JR2xrUFNKQmJXOTFiblFpSUdacGJHdzlJaU5tWm1ZaVBqeHlaV04wSUhkcFpIUm9QU0l4TWpBaUlHaGxhV2RvZEQwaU1UQXdJaUJtYVd4c0xXOXdZV05wZEhrOUlpNHdNeUlnY25nOUlqRTFJaUJ5ZVQwaU1UVWlJSE4wY205clpUMGlJMlptWmlJZ2MzUnliMnRsTFc5d1lXTnBkSGs5SWk0eElpQnpkSEp2YTJVdGQybGtkR2c5SWpRaUx6NDhkR1Y0ZENCNFBTSXlNQ0lnZVQwaU16UWlJR1p2Ym5RdFptRnRhV3g1UFNJblEyOTFjbWxsY2lCT1pYY25MRUZ5YVdGc0xHMXZibTl6Y0dGalpTSWdabTl1ZEMxemFYcGxQU0l5TW5CNElqNUJiVzkxYm5ROEwzUmxlSFErUEhSbGVIUWdlRDBpTWpBaUlIazlJamN5SWlCbWIyNTBMV1poYldsc2VUMGlKME52ZFhKcFpYSWdUbVYzSnl4QmNtbGhiQ3h0YjI1dmMzQmhZMlVpSUdadmJuUXRjMmw2WlQwaU1qWndlQ0krSmlNNE9EQTFPeUF4TUVzOEwzUmxlSFErUEM5blBqeG5JR2xrUFNKRWRYSmhkR2x2YmlJZ1ptbHNiRDBpSTJabVppSStQSEpsWTNRZ2QybGtkR2c5SWpFMU1pSWdhR1ZwWjJoMFBTSXhNREFpSUdacGJHd3RiM0JoWTJsMGVUMGlMakF6SWlCeWVEMGlNVFVpSUhKNVBTSXhOU0lnYzNSeWIydGxQU0lqWm1abUlpQnpkSEp2YTJVdGIzQmhZMmwwZVQwaUxqRWlJSE4wY205clpTMTNhV1IwYUQwaU5DSXZQangwWlhoMElIZzlJakl3SWlCNVBTSXpOQ0lnWm05dWRDMW1ZVzFwYkhrOUlpZERiM1Z5YVdWeUlFNWxkeWNzUVhKcFlXd3NiVzl1YjNOd1lXTmxJaUJtYjI1MExYTnBlbVU5SWpJeWNIZ2lQa1IxY21GMGFXOXVQQzkwWlhoMFBqeDBaWGgwSUhnOUlqSXdJaUI1UFNJM01pSWdabTl1ZEMxbVlXMXBiSGs5SWlkRGIzVnlhV1Z5SUU1bGR5Y3NRWEpwWVd3c2JXOXViM053WVdObElpQm1iMjUwTFhOcGVtVTlJakkyY0hnaVBpWnNkRHNnTVNCRVlYazhMM1JsZUhRK1BDOW5Qand2WkdWbWN6NDhkR1Y0ZENCMFpYaDBMWEpsYm1SbGNtbHVaejBpYjNCMGFXMXBlbVZUY0dWbFpDSStQSFJsZUhSUVlYUm9JSE4wWVhKMFQyWm1jMlYwUFNJdE1UQXdKU0lnYUhKbFpqMGlJMFpzYjJGMGFXNW5WR1Y0ZENJZ1ptbHNiRDBpSTJabVppSWdabTl1ZEMxbVlXMXBiSGs5SWlkRGIzVnlhV1Z5SUU1bGR5Y3NRWEpwWVd3c2JXOXViM053WVdObElpQm1hV3hzTFc5d1lXTnBkSGs5SWk0NElpQm1iMjUwTFhOcGVtVTlJakkyY0hnaVBqeGhibWx0WVhSbElHRmtaR2wwYVhabFBTSnpkVzBpSUdGMGRISnBZblYwWlU1aGJXVTlJbk4wWVhKMFQyWm1jMlYwSWlCaVpXZHBiajBpTUhNaUlHUjFjajBpTlRCeklpQm1jbTl0UFNJd0pTSWdjbVZ3WldGMFEyOTFiblE5SW1sdVpHVm1hVzVwZEdVaUlIUnZQU0l4TURBbElpOCtNSGc1TWpOaU5XRmlNemN4Tkdaa016UXpNekUyWVdZMVlUVTBNelExT0RKbVpERTJOekl5TlRJeklPS0FvaUJUWVdKc2FXVnlJRXh2WTJ0MWNEd3ZkR1Y0ZEZCaGRHZytQSFJsZUhSUVlYUm9JSE4wWVhKMFQyWm1jMlYwUFNJd0pTSWdhSEpsWmowaUkwWnNiMkYwYVc1blZHVjRkQ0lnWm1sc2JEMGlJMlptWmlJZ1ptOXVkQzFtWVcxcGJIazlJaWREYjNWeWFXVnlJRTVsZHljc1FYSnBZV3dzYlc5dWIzTndZV05sSWlCbWFXeHNMVzl3WVdOcGRIazlJaTQ0SWlCbWIyNTBMWE5wZW1VOUlqSTJjSGdpUGp4aGJtbHRZWFJsSUdGa1pHbDBhWFpsUFNKemRXMGlJR0YwZEhKcFluVjBaVTVoYldVOUluTjBZWEowVDJabWMyVjBJaUJpWldkcGJqMGlNSE1pSUdSMWNqMGlOVEJ6SWlCbWNtOXRQU0l3SlNJZ2NtVndaV0YwUTI5MWJuUTlJbWx1WkdWbWFXNXBkR1VpSUhSdlBTSXhNREFsSWk4K01IZzVNak5pTldGaU16Y3hOR1prTXpRek16RTJZV1kxWVRVME16UTFPREptWkRFMk56SXlOVEl6SU9LQW9pQlRZV0pzYVdWeUlFeHZZMnQxY0R3dmRHVjRkRkJoZEdnK1BIUmxlSFJRWVhSb0lITjBZWEowVDJabWMyVjBQU0l0TlRBbElpQm9jbVZtUFNJalJteHZZWFJwYm1kVVpYaDBJaUJtYVd4c1BTSWpabVptSWlCbWIyNTBMV1poYldsc2VUMGlKME52ZFhKcFpYSWdUbVYzSnl4QmNtbGhiQ3h0YjI1dmMzQmhZMlVpSUdacGJHd3RiM0JoWTJsMGVUMGlMamdpSUdadmJuUXRjMmw2WlQwaU1qWndlQ0krUEdGdWFXMWhkR1VnWVdSa2FYUnBkbVU5SW5OMWJTSWdZWFIwY21saWRYUmxUbUZ0WlQwaWMzUmhjblJQWm1aelpYUWlJR0psWjJsdVBTSXdjeUlnWkhWeVBTSTFNSE1pSUdaeWIyMDlJakFsSWlCeVpYQmxZWFJEYjNWdWREMGlhVzVrWldacGJtbDBaU0lnZEc4OUlqRXdNQ1VpTHo0d2VHWTJNamcwT1dZNVlUQmlOV0ptTWpreE0ySXpPVFl3T1RobU4yTTNNREU1WWpVeFlUZ3lNR0VnNG9DaUlFUkJTVHd2ZEdWNGRGQmhkR2crUEhSbGVIUlFZWFJvSUhOMFlYSjBUMlptYzJWMFBTSTFNQ1VpSUdoeVpXWTlJaU5HYkc5aGRHbHVaMVJsZUhRaUlHWnBiR3c5SWlObVptWWlJR1p2Ym5RdFptRnRhV3g1UFNJblEyOTFjbWxsY2lCT1pYY25MRUZ5YVdGc0xHMXZibTl6Y0dGalpTSWdabWxzYkMxdmNHRmphWFI1UFNJdU9DSWdabTl1ZEMxemFYcGxQU0l5Tm5CNElqNDhZVzVwYldGMFpTQmhaR1JwZEdsMlpUMGljM1Z0SWlCaGRIUnlhV0oxZEdWT1lXMWxQU0p6ZEdGeWRFOW1abk5sZENJZ1ltVm5hVzQ5SWpCeklpQmtkWEk5SWpVd2N5SWdabkp2YlQwaU1DVWlJSEpsY0dWaGRFTnZkVzUwUFNKcGJtUmxabWx1YVhSbElpQjBiejBpTVRBd0pTSXZQakI0WmpZeU9EUTVaamxoTUdJMVltWXlPVEV6WWpNNU5qQTVPR1kzWXpjd01UbGlOVEZoT0RJd1lTRGlnS0lnUkVGSlBDOTBaWGgwVUdGMGFENDhMM1JsZUhRK1BIVnpaU0JvY21WbVBTSWpSMnh2ZHlJZ1ptbHNiQzF2Y0dGamFYUjVQU0l1T1NJdlBqeDFjMlVnYUhKbFpqMGlJMGRzYjNjaUlIZzlJakV3TURBaUlIazlJakV3TURBaUlHWnBiR3d0YjNCaFkybDBlVDBpTGpraUx6NDhkWE5sSUdoeVpXWTlJaU5NYjJkdklpQjRQU0l4TnpBaUlIazlJakUzTUNJZ2RISmhibk5tYjNKdFBTSnpZMkZzWlNndU5pa2lMejQ4ZFhObElHaHlaV1k5SWlOSWIzVnlaMnhoYzNNaUlIZzlJakUxTUNJZ2VUMGlPVEFpSUhSeVlXNXpabTl5YlQwaWNtOTBZWFJsS0RFd0tTSWdkSEpoYm5ObWIzSnRMVzl5YVdkcGJqMGlOVEF3SURVd01DSXZQangxYzJVZ2FISmxaajBpSTFCeWIyZHlaWE56SWlCNFBTSXhORFFpSUhrOUlqYzVNQ0l2UGp4MWMyVWdhSEpsWmowaUkxTjBZWFIxY3lJZ2VEMGlNelk0SWlCNVBTSTNPVEFpTHo0OGRYTmxJR2h5WldZOUlpTkJiVzkxYm5RaUlIZzlJalUyT0NJZ2VUMGlOemt3SWk4K1BIVnpaU0JvY21WbVBTSWpSSFZ5WVhScGIyNGlJSGc5SWpjd05DSWdlVDBpTnprd0lpOCtQQzl6ZG1jKyJ9"; diff --git a/tests/integration/concrete/lockup-base/token-uri/tokenURI.tree b/tests/integration/concrete/lockup/token-uri/tokenURI.tree similarity index 100% rename from tests/integration/concrete/lockup-base/token-uri/tokenURI.tree rename to tests/integration/concrete/lockup/token-uri/tokenURI.tree diff --git a/tests/integration/concrete/lockup-base/transfer-from/transferFrom.t.sol b/tests/integration/concrete/lockup/transfer-from/transferFrom.t.sol similarity index 75% rename from tests/integration/concrete/lockup-base/transfer-from/transferFrom.t.sol rename to tests/integration/concrete/lockup/transfer-from/transferFrom.t.sol index 6ceb88a94..5f8fe66a8 100644 --- a/tests/integration/concrete/lockup-base/transfer-from/transferFrom.t.sol +++ b/tests/integration/concrete/lockup/transfer-from/transferFrom.t.sol @@ -13,28 +13,28 @@ contract TransferFrom_Integration_Concrete_Test is Integration_Test { Integration_Test.setUp(); // Set recipient as caller for this test. - resetPrank({ msgSender: users.recipient }); + setMsgSender(users.recipient); } function test_RevertGiven_NonTransferableStream() external { vm.expectRevert( - abi.encodeWithSelector(Errors.SablierLockupBase_NotTransferable.selector, notTransferableStreamId) + abi.encodeWithSelector(Errors.SablierLockup_NotTransferable.selector, ids.notTransferableStream) ); - lockup.transferFrom({ from: users.recipient, to: users.alice, tokenId: notTransferableStreamId }); + lockup.transferFrom({ from: users.recipient, to: users.alice, tokenId: ids.notTransferableStream }); } function test_GivenTransferableStream() external { // It should emit {MetadataUpdate} and {Transfer} events. vm.expectEmit({ emitter: address(lockup) }); - emit IERC4906.MetadataUpdate({ _tokenId: defaultStreamId }); + emit IERC4906.MetadataUpdate({ _tokenId: ids.defaultStream }); vm.expectEmit({ emitter: address(lockup) }); - emit IERC721.Transfer({ from: users.recipient, to: users.alice, tokenId: defaultStreamId }); + emit IERC721.Transfer({ from: users.recipient, to: users.alice, tokenId: ids.defaultStream }); // Transfer the NFT. - lockup.transferFrom({ from: users.recipient, to: users.alice, tokenId: defaultStreamId }); + lockup.transferFrom({ from: users.recipient, to: users.alice, tokenId: ids.defaultStream }); // It should change the stream recipient (and NFT owner). - address actualRecipient = lockup.getRecipient(defaultStreamId); + address actualRecipient = lockup.getRecipient(ids.defaultStream); address expectedRecipient = users.alice; assertEq(actualRecipient, expectedRecipient, "recipient"); } diff --git a/tests/integration/concrete/lockup-base/transfer-from/transferFrom.tree b/tests/integration/concrete/lockup/transfer-from/transferFrom.tree similarity index 100% rename from tests/integration/concrete/lockup-base/transfer-from/transferFrom.tree rename to tests/integration/concrete/lockup/transfer-from/transferFrom.tree diff --git a/tests/integration/concrete/lockup-base/withdraw-hooks/withdrawHooks.t.sol b/tests/integration/concrete/lockup/withdraw-hooks/withdrawHooks.t.sol similarity index 88% rename from tests/integration/concrete/lockup-base/withdraw-hooks/withdrawHooks.t.sol rename to tests/integration/concrete/lockup/withdraw-hooks/withdrawHooks.t.sol index 09bcbeab5..ab90f4f5d 100644 --- a/tests/integration/concrete/lockup-base/withdraw-hooks/withdrawHooks.t.sol +++ b/tests/integration/concrete/lockup/withdraw-hooks/withdrawHooks.t.sol @@ -35,13 +35,17 @@ contract WithdrawHooks_Integration_Concrete_Test is Integration_Test { }); // Make the withdrawal. - lockup.withdraw({ streamId: identicalSenderRecipientStreamId, to: users.sender, amount: withdrawAmount }); + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: identicalSenderRecipientStreamId, + to: users.sender, + amount: withdrawAmount + }); } function test_WhenCallerUnknown() external givenRecipientNotSameAsSender { // Make the unknown address the caller in this test. address unknownCaller = address(0xCAFE); - resetPrank({ msgSender: unknownCaller }); + setMsgSender(unknownCaller); // Simulate the passage of time. vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); @@ -57,7 +61,7 @@ contract WithdrawHooks_Integration_Concrete_Test is Integration_Test { }); // Make the withdrawal. - lockup.withdraw({ + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ streamId: differentSenderRecipientStreamId, to: address(recipientGood), amount: withdrawAmount @@ -66,11 +70,11 @@ contract WithdrawHooks_Integration_Concrete_Test is Integration_Test { function test_WhenCallerApprovedThirdParty() external givenRecipientNotSameAsSender { // Approve the operator to handle the stream. - resetPrank({ msgSender: address(recipientGood) }); + setMsgSender(address(recipientGood)); lockup.approve({ to: users.operator, tokenId: differentSenderRecipientStreamId }); // Make the operator the caller in this test. - resetPrank({ msgSender: users.operator }); + setMsgSender(users.operator); // Simulate the passage of time. vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); @@ -86,7 +90,7 @@ contract WithdrawHooks_Integration_Concrete_Test is Integration_Test { }); // Make the withdrawal. - lockup.withdraw({ + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ streamId: differentSenderRecipientStreamId, to: address(recipientGood), amount: withdrawAmount @@ -95,7 +99,7 @@ contract WithdrawHooks_Integration_Concrete_Test is Integration_Test { function test_WhenCallerSender() external givenRecipientNotSameAsSender { // Make the Sender the caller in this test. - resetPrank({ msgSender: users.sender }); + setMsgSender(users.sender); // Simulate the passage of time. vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); @@ -111,7 +115,7 @@ contract WithdrawHooks_Integration_Concrete_Test is Integration_Test { }); // Make the withdrawal. - lockup.withdraw({ + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ streamId: differentSenderRecipientStreamId, to: address(recipientGood), amount: withdrawAmount @@ -120,7 +124,7 @@ contract WithdrawHooks_Integration_Concrete_Test is Integration_Test { function test_WhenCallerRecipient() external givenRecipientNotSameAsSender { // Make the recipient contract the caller in this test. - resetPrank({ msgSender: address(recipientGood) }); + setMsgSender(address(recipientGood)); // Simulate the passage of time. vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); @@ -136,7 +140,7 @@ contract WithdrawHooks_Integration_Concrete_Test is Integration_Test { }); // Make the withdrawal. - lockup.withdraw({ + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ streamId: differentSenderRecipientStreamId, to: address(recipientGood), amount: withdrawAmount diff --git a/tests/integration/concrete/lockup-base/withdraw-hooks/withdrawHooks.tree b/tests/integration/concrete/lockup/withdraw-hooks/withdrawHooks.tree similarity index 100% rename from tests/integration/concrete/lockup-base/withdraw-hooks/withdrawHooks.tree rename to tests/integration/concrete/lockup/withdraw-hooks/withdrawHooks.tree diff --git a/tests/integration/concrete/lockup-base/withdraw-max-and-transfer/withdrawMaxAndTransfer.t.sol b/tests/integration/concrete/lockup/withdraw-max-and-transfer/withdrawMaxAndTransfer.t.sol similarity index 66% rename from tests/integration/concrete/lockup-base/withdraw-max-and-transfer/withdrawMaxAndTransfer.t.sol rename to tests/integration/concrete/lockup/withdraw-max-and-transfer/withdrawMaxAndTransfer.t.sol index 8ec61ae93..d8dae7adf 100644 --- a/tests/integration/concrete/lockup-base/withdraw-max-and-transfer/withdrawMaxAndTransfer.t.sol +++ b/tests/integration/concrete/lockup/withdraw-max-and-transfer/withdrawMaxAndTransfer.t.sol @@ -1,30 +1,32 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { IERC721Errors } from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; -import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; +import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; import { Errors } from "src/libraries/Errors.sol"; import { Integration_Test } from "../../../Integration.t.sol"; contract WithdrawMaxAndTransfer_Integration_Concrete_Test is Integration_Test { function test_RevertWhen_DelegateCall() external { expectRevert_DelegateCall({ - callData: abi.encodeCall(lockup.withdrawMaxAndTransfer, (defaultStreamId, users.alice)) + callData: abi.encodeCall(lockup.withdrawMaxAndTransfer, (ids.defaultStream, users.alice)) }); } function test_RevertGiven_Null() external whenNoDelegateCall { - expectRevert_Null({ callData: abi.encodeCall(lockup.withdrawMaxAndTransfer, (nullStreamId, users.alice)) }); + expectRevert_Null({ callData: abi.encodeCall(lockup.withdrawMaxAndTransfer, (ids.nullStream, users.alice)) }); } function test_RevertGiven_NonTransferableStream() external whenCallerRecipient whenNoDelegateCall givenNotNull { vm.expectRevert( - abi.encodeWithSelector(Errors.SablierLockupBase_NotTransferable.selector, notTransferableStreamId) + abi.encodeWithSelector(Errors.SablierLockup_NotTransferable.selector, ids.notTransferableStream) ); - lockup.withdrawMaxAndTransfer({ streamId: notTransferableStreamId, newRecipient: users.recipient }); + lockup.withdrawMaxAndTransfer{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: ids.notTransferableStream, + newRecipient: users.recipient + }); } function test_RevertGiven_BurnedNFT() @@ -36,14 +38,19 @@ contract WithdrawMaxAndTransfer_Integration_Concrete_Test is Integration_Test { { // Deplete the stream. vm.warp({ newTimestamp: defaults.END_TIME() }); - lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); + lockup.withdrawMax{ value: LOCKUP_MIN_FEE_WEI }({ streamId: ids.defaultStream, to: users.recipient }); // Burn the NFT. - lockup.burn({ streamId: defaultStreamId }); + lockup.burn({ streamId: ids.defaultStream }); // Run the test. - vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, defaultStreamId)); - lockup.withdrawMaxAndTransfer({ streamId: defaultStreamId, newRecipient: users.alice }); + vm.expectRevert( + abi.encodeWithSelector(Errors.SablierLockup_Unauthorized.selector, ids.defaultStream, users.recipient) + ); + lockup.withdrawMaxAndTransfer{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: ids.defaultStream, + newRecipient: users.alice + }); } function test_GivenZeroWithdrawableAmount() @@ -55,16 +62,19 @@ contract WithdrawMaxAndTransfer_Integration_Concrete_Test is Integration_Test { whenCallerRecipient { vm.warp({ newTimestamp: defaults.END_TIME() }); - lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); + lockup.withdrawMax{ value: LOCKUP_MIN_FEE_WEI }({ streamId: ids.defaultStream, to: users.recipient }); // It should not expect a transfer call on token. vm.expectCall({ callee: address(dai), data: abi.encodeCall(IERC20.transfer, (users.recipient, 0)), count: 0 }); // It should emit {Transfer} event on NFT. vm.expectEmit({ emitter: address(lockup) }); - emit IERC721.Transfer({ from: users.recipient, to: users.alice, tokenId: defaultStreamId }); + emit IERC721.Transfer({ from: users.recipient, to: users.alice, tokenId: ids.defaultStream }); - lockup.withdrawMaxAndTransfer({ streamId: defaultStreamId, newRecipient: users.alice }); + lockup.withdrawMaxAndTransfer{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: ids.defaultStream, + newRecipient: users.alice + }); } function test_RevertWhen_CallerNotCurrentRecipient() @@ -76,13 +86,16 @@ contract WithdrawMaxAndTransfer_Integration_Concrete_Test is Integration_Test { givenNonZeroWithdrawableAmount { // Make Eve the caller in this test. - resetPrank({ msgSender: users.eve }); + setMsgSender(users.eve); // It should revert. vm.expectRevert( - abi.encodeWithSelector(Errors.SablierLockupBase_Unauthorized.selector, defaultStreamId, users.eve) + abi.encodeWithSelector(Errors.SablierLockup_Unauthorized.selector, ids.defaultStream, users.eve) ); - lockup.withdrawMaxAndTransfer({ streamId: defaultStreamId, newRecipient: users.eve }); + lockup.withdrawMaxAndTransfer{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: ids.defaultStream, + newRecipient: users.eve + }); } function test_WhenCallerApprovedThirdParty() @@ -94,37 +107,39 @@ contract WithdrawMaxAndTransfer_Integration_Concrete_Test is Integration_Test { givenNonZeroWithdrawableAmount { // Make the operator the caller in this test. - resetPrank({ msgSender: users.operator }); + setMsgSender(users.operator); // Simulate the passage of time. vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); // Get the withdraw amount. - uint128 expectedWithdrawnAmount = lockup.withdrawableAmountOf(defaultStreamId); + uint128 expectedWithdrawnAmount = lockup.withdrawableAmountOf(ids.defaultStream); // Expect the tokens to be transferred to the Recipient. expectCallToTransfer({ to: users.recipient, value: expectedWithdrawnAmount }); // It should emit {Transfer} and {WithdrawFromLockupStream} events. vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.WithdrawFromLockupStream({ - streamId: defaultStreamId, + emit ISablierLockup.WithdrawFromLockupStream({ + streamId: ids.defaultStream, to: users.recipient, amount: expectedWithdrawnAmount, token: dai }); vm.expectEmit({ emitter: address(lockup) }); - emit IERC721.Transfer({ from: users.recipient, to: users.alice, tokenId: defaultStreamId }); + emit IERC721.Transfer({ from: users.recipient, to: users.alice, tokenId: ids.defaultStream }); // Make the max withdrawal and transfer the NFT. - uint128 actualWithdrawnAmount = - lockup.withdrawMaxAndTransfer({ streamId: defaultStreamId, newRecipient: users.alice }); + uint128 actualWithdrawnAmount = lockup.withdrawMaxAndTransfer{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: ids.defaultStream, + newRecipient: users.alice + }); // Assert that the withdrawn amount has been updated. assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); // Assert that the operator is the new stream recipient (and NFT owner). - address actualRecipient = lockup.getRecipient(defaultStreamId); + address actualRecipient = lockup.getRecipient(ids.defaultStream); address expectedRecipient = users.alice; assertEq(actualRecipient, expectedRecipient, "recipient"); } @@ -137,39 +152,41 @@ contract WithdrawMaxAndTransfer_Integration_Concrete_Test is Integration_Test { givenNotBurnedNFT givenNonZeroWithdrawableAmount { - resetPrank(users.recipient); + setMsgSender(users.recipient); // Simulate the passage of time. vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); // Get the withdraw amount. - uint128 expectedWithdrawnAmount = lockup.withdrawableAmountOf(defaultStreamId); + uint128 expectedWithdrawnAmount = lockup.withdrawableAmountOf(ids.defaultStream); // Expect the tokens to be transferred to the Recipient. expectCallToTransfer({ to: users.recipient, value: expectedWithdrawnAmount }); // It should emit {Transfer}, {WithdrawFromLockupStream} and {MetadataUpdate} events. vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.WithdrawFromLockupStream({ - streamId: defaultStreamId, + emit ISablierLockup.WithdrawFromLockupStream({ + streamId: ids.defaultStream, to: users.recipient, amount: expectedWithdrawnAmount, token: dai }); vm.expectEmit({ emitter: address(lockup) }); - emit IERC4906.MetadataUpdate({ _tokenId: defaultStreamId }); + emit IERC4906.MetadataUpdate({ _tokenId: ids.defaultStream }); vm.expectEmit({ emitter: address(lockup) }); - emit IERC721.Transfer({ from: users.recipient, to: users.alice, tokenId: defaultStreamId }); + emit IERC721.Transfer({ from: users.recipient, to: users.alice, tokenId: ids.defaultStream }); // Make the max withdrawal and transfer the NFT. - uint128 actualWithdrawnAmount = - lockup.withdrawMaxAndTransfer({ streamId: defaultStreamId, newRecipient: users.alice }); + uint128 actualWithdrawnAmount = lockup.withdrawMaxAndTransfer{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: ids.defaultStream, + newRecipient: users.alice + }); // it should update the withdrawn amount.abi assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); // It should transfer the NFT. - address actualRecipient = lockup.getRecipient(defaultStreamId); + address actualRecipient = lockup.getRecipient(ids.defaultStream); address expectedRecipient = users.alice; assertEq(actualRecipient, expectedRecipient, "recipient"); } diff --git a/tests/integration/concrete/lockup-base/withdraw-max-and-transfer/withdrawMaxAndTransfer.tree b/tests/integration/concrete/lockup/withdraw-max-and-transfer/withdrawMaxAndTransfer.tree similarity index 100% rename from tests/integration/concrete/lockup-base/withdraw-max-and-transfer/withdrawMaxAndTransfer.tree rename to tests/integration/concrete/lockup/withdraw-max-and-transfer/withdrawMaxAndTransfer.tree diff --git a/tests/integration/concrete/lockup-base/withdraw-max/withdrawMax.t.sol b/tests/integration/concrete/lockup/withdraw-max/withdrawMax.t.sol similarity index 73% rename from tests/integration/concrete/lockup-base/withdraw-max/withdrawMax.t.sol rename to tests/integration/concrete/lockup/withdraw-max/withdrawMax.t.sol index c98b318dc..370806ea3 100644 --- a/tests/integration/concrete/lockup-base/withdraw-max/withdrawMax.t.sol +++ b/tests/integration/concrete/lockup/withdraw-max/withdrawMax.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; -import { Lockup } from "src/types/DataTypes.sol"; +import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; +import { Lockup } from "src/types/Lockup.sol"; import { Integration_Test } from "../../../Integration.t.sol"; @@ -16,36 +16,37 @@ contract WithdrawMax_Integration_Concrete_Test is Integration_Test { // It should emit a {WithdrawFromLockupStream} event. vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.WithdrawFromLockupStream({ - streamId: defaultStreamId, + emit ISablierLockup.WithdrawFromLockupStream({ + streamId: ids.defaultStream, to: users.recipient, amount: defaults.DEPOSIT_AMOUNT(), token: dai }); // Make the max withdrawal. - uint128 actualReturnedValue = lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); + uint128 actualReturnedValue = + lockup.withdrawMax{ value: LOCKUP_MIN_FEE_WEI }({ streamId: ids.defaultStream, to: users.recipient }); // It should return the withdrawn amount. uint128 expectedReturnedValue = defaults.DEPOSIT_AMOUNT(); assertEq(actualReturnedValue, expectedReturnedValue, "returnValue"); // It should update the withdrawn amount. - uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(defaultStreamId); + uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(ids.defaultStream); uint128 expectedWithdrawnAmount = defaults.DEPOSIT_AMOUNT(); assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); // It should mark the stream as depleted. - Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); + Lockup.Status actualStatus = lockup.statusOf(ids.defaultStream); Lockup.Status expectedStatus = Lockup.Status.DEPLETED; assertEq(actualStatus, expectedStatus); // It should make the stream not cancelable. - bool isCancelable = lockup.isCancelable(defaultStreamId); + bool isCancelable = lockup.isCancelable(ids.defaultStream); assertFalse(isCancelable, "isCancelable"); // Assert that the not burned NFT. - address actualNFTowner = lockup.ownerOf({ tokenId: defaultStreamId }); + address actualNFTowner = lockup.ownerOf({ tokenId: ids.defaultStream }); address expectedNFTOwner = users.recipient; assertEq(actualNFTowner, expectedNFTOwner, "NFT owner"); } @@ -55,28 +56,29 @@ contract WithdrawMax_Integration_Concrete_Test is Integration_Test { vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); // Get the withdraw amount. - uint128 expectedWithdrawnAmount = lockup.withdrawableAmountOf(defaultStreamId); + uint128 expectedWithdrawnAmount = lockup.withdrawableAmountOf(ids.defaultStream); // Expect the tokens to be transferred to the Recipient. expectCallToTransfer({ to: users.recipient, value: expectedWithdrawnAmount }); // It should emit a {WithdrawFromLockupStream} event. vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.WithdrawFromLockupStream({ - streamId: defaultStreamId, + emit ISablierLockup.WithdrawFromLockupStream({ + streamId: ids.defaultStream, to: users.recipient, amount: expectedWithdrawnAmount, token: dai }); // Make the max withdrawal. - uint128 actualWithdrawnAmount = lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); + uint128 actualWithdrawnAmount = + lockup.withdrawMax{ value: LOCKUP_MIN_FEE_WEI }({ streamId: ids.defaultStream, to: users.recipient }); // It should return the withdrawable amount. assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); // Assert that the stream's status is still "STREAMING". - Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); + Lockup.Status actualStatus = lockup.statusOf(ids.defaultStream); Lockup.Status expectedStatus = Lockup.Status.STREAMING; assertEq(actualStatus, expectedStatus); } diff --git a/tests/integration/concrete/lockup-base/withdraw-max/withdrawMax.tree b/tests/integration/concrete/lockup/withdraw-max/withdrawMax.tree similarity index 100% rename from tests/integration/concrete/lockup-base/withdraw-max/withdrawMax.tree rename to tests/integration/concrete/lockup/withdraw-max/withdrawMax.tree diff --git a/tests/integration/concrete/lockup-base/withdraw-multiple/withdrawMultiple.t.sol b/tests/integration/concrete/lockup/withdraw-multiple/withdrawMultiple.t.sol similarity index 60% rename from tests/integration/concrete/lockup-base/withdraw-multiple/withdrawMultiple.t.sol rename to tests/integration/concrete/lockup/withdraw-multiple/withdrawMultiple.t.sol index 65506c53e..6372c597d 100644 --- a/tests/integration/concrete/lockup-base/withdraw-multiple/withdrawMultiple.t.sol +++ b/tests/integration/concrete/lockup/withdraw-multiple/withdrawMultiple.t.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; +import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; import { Errors } from "src/libraries/Errors.sol"; -import { Lockup } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; import { Integration_Test } from "../../../Integration.t.sol"; @@ -17,7 +17,7 @@ contract WithdrawMultiple_Integration_Concrete_Test is Integration_Test { uint128[] internal withdrawAmounts; // An array of stream IDs to be withdrawn from. - uint256[] internal withdrawMultipleStreamIds; + uint256[] internal withdrawMultipleIds; function setUp() public virtual override { Integration_Test.setUp(); @@ -31,7 +31,7 @@ contract WithdrawMultiple_Integration_Concrete_Test is Integration_Test { function test_RevertWhen_DelegateCall() external { expectRevert_DelegateCall({ - callData: abi.encodeCall(lockup.withdrawMultiple, (withdrawMultipleStreamIds, withdrawAmounts)) + callData: abi.encodeCall(lockup.withdrawMultiple, (withdrawMultipleIds, withdrawAmounts)) }); } @@ -40,10 +40,10 @@ contract WithdrawMultiple_Integration_Concrete_Test is Integration_Test { uint128[] memory amounts = new uint128[](1); vm.expectRevert( abi.encodeWithSelector( - Errors.SablierLockupBase_WithdrawArrayCountsNotEqual.selector, streamIds.length, amounts.length + Errors.SablierLockup_WithdrawArrayCountsNotEqual.selector, streamIds.length, amounts.length ) ); - lockup.withdrawMultiple(streamIds, amounts); + lockup.withdrawMultiple{ value: streamIds.length * LOCKUP_MIN_FEE_WEI }(streamIds, amounts); } function test_WhenZeroArrayLength() external whenNoDelegateCall whenEqualArraysLength { @@ -51,7 +51,7 @@ contract WithdrawMultiple_Integration_Concrete_Test is Integration_Test { uint128[] memory amounts = new uint128[](0); // It should do nothing. - lockup.withdrawMultiple(streamIds, amounts); + lockup.withdrawMultiple{ value: LOCKUP_MIN_FEE_WEI }(streamIds, amounts); } /// @dev This modifier runs the test in four different modes: @@ -60,19 +60,19 @@ contract WithdrawMultiple_Integration_Concrete_Test is Integration_Test { /// - Approved NFT operator as caller /// - Random caller (Alice) modifier whenCallerAuthorizedForAllStreams() override { - withdrawMultipleStreamIds = _warpAndCreateStreams({ warpTime: originalTime }); + withdrawMultipleIds = _warpAndCreateStreams({ warpTime: originalTime }); caller = users.sender; _; - withdrawMultipleStreamIds = _warpAndCreateStreams({ warpTime: originalTime }); + withdrawMultipleIds = _warpAndCreateStreams({ warpTime: originalTime }); caller = users.recipient; _; - withdrawMultipleStreamIds = _warpAndCreateStreams({ warpTime: originalTime }); + withdrawMultipleIds = _warpAndCreateStreams({ warpTime: originalTime }); caller = users.operator; _; - withdrawMultipleStreamIds = _warpAndCreateStreams({ warpTime: originalTime }); + withdrawMultipleIds = _warpAndCreateStreams({ warpTime: originalTime }); caller = users.alice; _; } @@ -88,19 +88,19 @@ contract WithdrawMultiple_Integration_Concrete_Test is Integration_Test { vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() + 1 }); // Run the test with the caller provided in {whenCallerAuthorizedForAllStreams}. - resetPrank({ msgSender: caller }); + setMsgSender(caller); // It should emit {WithdrawFromLockupStream} events for non-reverting streams. vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.WithdrawFromLockupStream({ - streamId: withdrawMultipleStreamIds[0], + emit ISablierLockup.WithdrawFromLockupStream({ + streamId: withdrawMultipleIds[0], to: users.recipient, token: dai, amount: withdrawAmounts[0] }); vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.WithdrawFromLockupStream({ - streamId: withdrawMultipleStreamIds[1], + emit ISablierLockup.WithdrawFromLockupStream({ + streamId: withdrawMultipleIds[1], to: users.recipient, token: dai, amount: withdrawAmounts[1] @@ -108,24 +108,27 @@ contract WithdrawMultiple_Integration_Concrete_Test is Integration_Test { // It should emit {InvalidWithdrawalInWithdrawMultiple} event for reverting stream. vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.InvalidWithdrawalInWithdrawMultiple({ - streamId: withdrawMultipleStreamIds[2], + emit ISablierLockup.InvalidWithdrawalInWithdrawMultiple({ + streamId: withdrawMultipleIds[2], revertData: abi.encodeWithSelector( - Errors.SablierLockupBase_Overdraw.selector, - withdrawMultipleStreamIds[2], + Errors.SablierLockup_Overdraw.selector, + withdrawMultipleIds[2], MAX_UINT128, - lockup.withdrawableAmountOf(withdrawMultipleStreamIds[2]) + lockup.withdrawableAmountOf(withdrawMultipleIds[2]) ) }); // Make the withdrawals with overdrawn withdraw amount for reverting stream. withdrawAmounts[2] = MAX_UINT128; - lockup.withdrawMultiple({ streamIds: withdrawMultipleStreamIds, amounts: withdrawAmounts }); + lockup.withdrawMultiple{ value: withdrawMultipleIds.length * LOCKUP_MIN_FEE_WEI }({ + streamIds: withdrawMultipleIds, + amounts: withdrawAmounts + }); // It should update the withdrawn amounts only for non-reverting streams. - assertEq(lockup.getWithdrawnAmount(withdrawMultipleStreamIds[0]), withdrawAmounts[0], "withdrawnAmount0"); - assertEq(lockup.getWithdrawnAmount(withdrawMultipleStreamIds[1]), withdrawAmounts[1], "withdrawnAmount1"); - assertEq(lockup.getWithdrawnAmount(withdrawMultipleStreamIds[2]), 0, "withdrawnAmount2"); + assertEq(lockup.getWithdrawnAmount(withdrawMultipleIds[0]), withdrawAmounts[0], "withdrawnAmount0"); + assertEq(lockup.getWithdrawnAmount(withdrawMultipleIds[1]), withdrawAmounts[1], "withdrawnAmount1"); + assertEq(lockup.getWithdrawnAmount(withdrawMultipleIds[2]), 0, "withdrawnAmount2"); } function test_WhenNoStreamsRevert() @@ -139,52 +142,55 @@ contract WithdrawMultiple_Integration_Concrete_Test is Integration_Test { vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() + 1 }); // Cancel the 3rd stream. - resetPrank({ msgSender: users.sender }); - lockup.cancel(withdrawMultipleStreamIds[2]); + setMsgSender(users.sender); + lockup.cancel(withdrawMultipleIds[2]); // Run the test with the caller provided in {whenCallerAuthorizedForAllStreams}. - resetPrank({ msgSender: caller }); + setMsgSender(caller); // It should emit {WithdrawFromLockupStream} events for all streams. vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.WithdrawFromLockupStream({ - streamId: withdrawMultipleStreamIds[0], + emit ISablierLockup.WithdrawFromLockupStream({ + streamId: withdrawMultipleIds[0], to: users.recipient, token: dai, amount: withdrawAmounts[0] }); vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.WithdrawFromLockupStream({ - streamId: withdrawMultipleStreamIds[1], + emit ISablierLockup.WithdrawFromLockupStream({ + streamId: withdrawMultipleIds[1], to: users.recipient, token: dai, amount: withdrawAmounts[1] }); vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.WithdrawFromLockupStream({ - streamId: withdrawMultipleStreamIds[2], + emit ISablierLockup.WithdrawFromLockupStream({ + streamId: withdrawMultipleIds[2], to: users.recipient, token: dai, amount: withdrawAmounts[2] }); // Make the withdrawals. - lockup.withdrawMultiple({ streamIds: withdrawMultipleStreamIds, amounts: withdrawAmounts }); + lockup.withdrawMultiple{ value: withdrawMultipleIds.length * LOCKUP_MIN_FEE_WEI }({ + streamIds: withdrawMultipleIds, + amounts: withdrawAmounts + }); // It should update the statuses. - assertEq(lockup.statusOf(withdrawMultipleStreamIds[0]), Lockup.Status.STREAMING, "status0"); - assertEq(lockup.statusOf(withdrawMultipleStreamIds[1]), Lockup.Status.DEPLETED, "status1"); - assertEq(lockup.statusOf(withdrawMultipleStreamIds[2]), Lockup.Status.CANCELED, "status2"); + assertEq(lockup.statusOf(withdrawMultipleIds[0]), Lockup.Status.STREAMING, "status0"); + assertEq(lockup.statusOf(withdrawMultipleIds[1]), Lockup.Status.DEPLETED, "status1"); + assertEq(lockup.statusOf(withdrawMultipleIds[2]), Lockup.Status.CANCELED, "status2"); // It should update the withdrawn amounts. - assertEq(lockup.getWithdrawnAmount(withdrawMultipleStreamIds[0]), withdrawAmounts[0], "withdrawnAmount0"); - assertEq(lockup.getWithdrawnAmount(withdrawMultipleStreamIds[1]), withdrawAmounts[1], "withdrawnAmount1"); - assertEq(lockup.getWithdrawnAmount(withdrawMultipleStreamIds[2]), withdrawAmounts[2], "withdrawnAmount2"); + assertEq(lockup.getWithdrawnAmount(withdrawMultipleIds[0]), withdrawAmounts[0], "withdrawnAmount0"); + assertEq(lockup.getWithdrawnAmount(withdrawMultipleIds[1]), withdrawAmounts[1], "withdrawnAmount1"); + assertEq(lockup.getWithdrawnAmount(withdrawMultipleIds[2]), withdrawAmounts[2], "withdrawnAmount2"); // Assert that the stream NFTs have not been burned. - assertEq(lockup.getRecipient(withdrawMultipleStreamIds[0]), users.recipient, "NFT owner0"); - assertEq(lockup.getRecipient(withdrawMultipleStreamIds[1]), users.recipient, "NFT owner1"); - assertEq(lockup.getRecipient(withdrawMultipleStreamIds[2]), users.recipient, "NFT owner2"); + assertEq(lockup.getRecipient(withdrawMultipleIds[0]), users.recipient, "NFT owner0"); + assertEq(lockup.getRecipient(withdrawMultipleIds[1]), users.recipient, "NFT owner1"); + assertEq(lockup.getRecipient(withdrawMultipleIds[2]), users.recipient, "NFT owner2"); } // A helper function to warp to the original time and create test streams. diff --git a/tests/integration/concrete/lockup-base/withdraw-multiple/withdrawMultiple.tree b/tests/integration/concrete/lockup/withdraw-multiple/withdrawMultiple.tree similarity index 100% rename from tests/integration/concrete/lockup-base/withdraw-multiple/withdrawMultiple.tree rename to tests/integration/concrete/lockup/withdraw-multiple/withdrawMultiple.tree diff --git a/tests/integration/concrete/lockup-base/withdraw/withdraw.t.sol b/tests/integration/concrete/lockup/withdraw/withdraw.t.sol similarity index 65% rename from tests/integration/concrete/lockup-base/withdraw/withdraw.t.sol rename to tests/integration/concrete/lockup/withdraw/withdraw.t.sol index bf02e308a..3a9293038 100644 --- a/tests/integration/concrete/lockup-base/withdraw/withdraw.t.sol +++ b/tests/integration/concrete/lockup/withdraw/withdraw.t.sol @@ -3,10 +3,10 @@ pragma solidity >=0.8.22 <0.9.0; import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; -import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; +import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; import { ISablierLockupRecipient } from "src/interfaces/ISablierLockupRecipient.sol"; import { Errors } from "src/libraries/Errors.sol"; -import { Lockup } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; import { Integration_Test } from "../../../Integration.t.sol"; @@ -16,27 +16,31 @@ abstract contract Withdraw_Integration_Concrete_Test is Integration_Test { function test_RevertWhen_DelegateCall() external { uint128 withdrawAmount = defaults.WITHDRAW_AMOUNT(); expectRevert_DelegateCall({ - callData: abi.encodeCall(lockup.withdraw, (defaultStreamId, users.recipient, withdrawAmount)) + callData: abi.encodeCall(lockup.withdraw, (ids.defaultStream, users.recipient, withdrawAmount)) }); } function test_RevertGiven_Null() external whenNoDelegateCall { uint128 withdrawAmount = defaults.WITHDRAW_AMOUNT(); - expectRevert_Null({ callData: abi.encodeCall(lockup.withdraw, (nullStreamId, users.recipient, withdrawAmount)) }); + expectRevert_Null({ + callData: abi.encodeCall(lockup.withdraw, (ids.nullStream, users.recipient, withdrawAmount)) + }); } function test_RevertGiven_DEPLETEDStatus() external whenNoDelegateCall givenNotNull { expectRevert_DEPLETEDStatus({ - callData: abi.encodeCall(lockup.withdraw, (defaultStreamId, users.recipient, defaults.WITHDRAW_AMOUNT())) + callData: abi.encodeCall(lockup.withdraw, (ids.defaultStream, users.recipient, defaults.WITHDRAW_AMOUNT())) }); } function test_RevertWhen_WithdrawalAddressZero() external whenNoDelegateCall givenNotNull givenNotDEPLETEDStatus { uint128 withdrawAmount = defaults.WITHDRAW_AMOUNT(); - vm.expectRevert( - abi.encodeWithSelector(Errors.SablierLockupBase_WithdrawToZeroAddress.selector, defaultStreamId) - ); - lockup.withdraw({ streamId: defaultStreamId, to: address(0), amount: withdrawAmount }); + vm.expectRevert(abi.encodeWithSelector(Errors.SablierLockup_WithdrawToZeroAddress.selector, ids.defaultStream)); + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: ids.defaultStream, + to: address(0), + amount: withdrawAmount + }); } function test_RevertWhen_ZeroWithdrawAmount() @@ -46,8 +50,8 @@ abstract contract Withdraw_Integration_Concrete_Test is Integration_Test { givenNotDEPLETEDStatus whenWithdrawalAddressNotZero { - vm.expectRevert(abi.encodeWithSelector(Errors.SablierLockupBase_WithdrawAmountZero.selector, defaultStreamId)); - lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: 0 }); + vm.expectRevert(abi.encodeWithSelector(Errors.SablierLockup_WithdrawAmountZero.selector, ids.defaultStream)); + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ streamId: ids.defaultStream, to: users.recipient, amount: 0 }); } function test_RevertWhen_WithdrawAmountOverdraws() @@ -61,38 +65,42 @@ abstract contract Withdraw_Integration_Concrete_Test is Integration_Test { uint128 withdrawableAmount = 0; vm.expectRevert( abi.encodeWithSelector( - Errors.SablierLockupBase_Overdraw.selector, defaultStreamId, MAX_UINT128, withdrawableAmount + Errors.SablierLockup_Overdraw.selector, ids.defaultStream, MAX_UINT128, withdrawableAmount ) ); - lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: MAX_UINT128 }); + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: ids.defaultStream, + to: users.recipient, + amount: MAX_UINT128 + }); } modifier whenWithdrawalAddressNotRecipient(bool isCallerRecipient) { if (!isCallerRecipient) { // When caller is unknown. caller = users.eve; - resetPrank({ msgSender: caller }); + setMsgSender(caller); _; // When caller is sender. caller = users.sender; - resetPrank({ msgSender: caller }); + setMsgSender(caller); _; // When caller is a former recipient. caller = users.recipient; - resetPrank({ msgSender: caller }); - lockup.transferFrom(caller, users.eve, defaultStreamId); + setMsgSender(caller); + lockup.transferFrom(caller, users.eve, ids.defaultStream); _; } else { // When caller is approved third party. caller = users.operator; - resetPrank({ msgSender: caller }); + setMsgSender(caller); _; // When caller is recipient. caller = users.recipient; - resetPrank({ msgSender: caller }); + setMsgSender(caller); _; } } @@ -116,10 +124,14 @@ abstract contract Withdraw_Integration_Concrete_Test is Integration_Test { // It should revert. vm.expectRevert( abi.encodeWithSelector( - Errors.SablierLockupBase_WithdrawalAddressNotRecipient.selector, defaultStreamId, caller, users.alice + Errors.SablierLockup_WithdrawalAddressNotRecipient.selector, ids.defaultStream, caller, users.alice ) ); - lockup.withdraw({ streamId: defaultStreamId, to: users.alice, amount: withdrawAmount }); + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: ids.defaultStream, + to: users.alice, + amount: withdrawAmount + }); } function test_WhenCallerApprovedThirdPartyOrRecipient() @@ -138,24 +150,28 @@ abstract contract Withdraw_Integration_Concrete_Test is Integration_Test { // Set the withdraw amount to the default amount. uint128 withdrawAmount = defaults.WITHDRAW_AMOUNT() / 2; - uint128 previousWithdrawnAmount = lockup.getWithdrawnAmount(defaultStreamId); + uint128 previousWithdrawnAmount = lockup.getWithdrawnAmount(ids.defaultStream); // It should emit {WithdrawFromLockupStream} and {MetadataUpdate} events. vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.WithdrawFromLockupStream({ - streamId: defaultStreamId, + emit ISablierLockup.WithdrawFromLockupStream({ + streamId: ids.defaultStream, to: users.alice, token: dai, amount: withdrawAmount }); vm.expectEmit({ emitter: address(lockup) }); - emit IERC4906.MetadataUpdate({ _tokenId: defaultStreamId }); + emit IERC4906.MetadataUpdate({ _tokenId: ids.defaultStream }); // Make the withdrawal. - lockup.withdraw({ streamId: defaultStreamId, to: users.alice, amount: withdrawAmount }); + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: ids.defaultStream, + to: users.alice, + amount: withdrawAmount + }); // It should update the withdrawn amount. - uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(defaultStreamId); + uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(ids.defaultStream); uint128 expectedWithdrawnAmount = previousWithdrawnAmount + withdrawAmount; assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); } @@ -171,16 +187,20 @@ abstract contract Withdraw_Integration_Concrete_Test is Integration_Test { whenWithdrawalAddressRecipient { // Make the unknown address the caller in this test. - resetPrank({ msgSender: address(0xCAFE) }); + setMsgSender(address(0xCAFE)); // Simulate the passage of time. vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); // Make the withdrawal. - lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: defaults.WITHDRAW_AMOUNT() }); + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: ids.defaultStream, + to: users.recipient, + amount: defaults.WITHDRAW_AMOUNT() + }); // It should update the withdrawn amount. - uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(defaultStreamId); + uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(ids.defaultStream); uint128 expectedWithdrawnAmount = defaults.WITHDRAW_AMOUNT(); assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); } @@ -195,20 +215,48 @@ abstract contract Withdraw_Integration_Concrete_Test is Integration_Test { whenWithdrawAmountNotOverdraw whenWithdrawalAddressRecipient { - resetPrank({ msgSender: users.recipient }); + setMsgSender(users.recipient); // Simulate the passage of time. vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); // Make the withdrawal. - lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: defaults.WITHDRAW_AMOUNT() }); + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: ids.defaultStream, + to: users.recipient, + amount: defaults.WITHDRAW_AMOUNT() + }); // It should update the withdrawn amount. - uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(defaultStreamId); + uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(ids.defaultStream); uint128 expectedWithdrawnAmount = defaults.WITHDRAW_AMOUNT(); assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); } + function test_RevertWhen_FeeLessThanMinFee() + external + whenNoDelegateCall + givenNotNull + givenNotDEPLETEDStatus + whenWithdrawalAddressNotZero + whenNonZeroWithdrawAmount + whenWithdrawAmountNotOverdraw + whenWithdrawalAddressRecipient + whenCallerSender + { + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); + + uint256 fee = LOCKUP_MIN_FEE_WEI - 1; + uint128 withdrawAmount = defaults.WITHDRAW_AMOUNT(); + + vm.expectRevert( + abi.encodeWithSelector(Errors.SablierLockup_InsufficientFeePayment.selector, fee, LOCKUP_MIN_FEE_WEI) + ); + + // Make the withdrawal. + lockup.withdraw{ value: fee }({ streamId: ids.defaultStream, to: users.recipient, amount: withdrawAmount }); + } + function test_GivenEndTimeNotInFuture() external whenNoDelegateCall @@ -219,24 +267,29 @@ abstract contract Withdraw_Integration_Concrete_Test is Integration_Test { whenWithdrawAmountNotOverdraw whenWithdrawalAddressRecipient whenCallerSender + whenFeeNotLessThanMinFee { // Warp to the stream's end. vm.warp({ newTimestamp: defaults.END_TIME() }); // Make the withdrawal. - lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: defaults.DEPOSIT_AMOUNT() }); + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: ids.defaultStream, + to: users.recipient, + amount: defaults.DEPOSIT_AMOUNT() + }); // It should mark the stream as depleted. - Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); + Lockup.Status actualStatus = lockup.statusOf(ids.defaultStream); Lockup.Status expectedStatus = Lockup.Status.DEPLETED; assertEq(actualStatus, expectedStatus); // It should make the stream not cancelable. - bool isCancelable = lockup.isCancelable(defaultStreamId); + bool isCancelable = lockup.isCancelable(ids.defaultStream); assertFalse(isCancelable, "isCancelable"); // Assert that the not burned NFT. - address actualNFTowner = lockup.ownerOf({ tokenId: defaultStreamId }); + address actualNFTowner = lockup.ownerOf({ tokenId: ids.defaultStream }); address expectedNFTOwner = users.recipient; assertEq(actualNFTowner, expectedNFTOwner, "NFT owner"); } @@ -251,40 +304,45 @@ abstract contract Withdraw_Integration_Concrete_Test is Integration_Test { whenWithdrawAmountNotOverdraw whenWithdrawalAddressRecipient whenCallerSender + whenFeeNotLessThanMinFee givenEndTimeInFuture { // Cancel the stream. - lockup.cancel(defaultStreamId); + lockup.cancel(ids.defaultStream); // Set the withdraw amount to the withdrawable amount. - uint128 withdrawAmount = lockup.withdrawableAmountOf(defaultStreamId); + uint128 withdrawAmount = lockup.withdrawableAmountOf(ids.defaultStream); // It should emit {WithdrawFromLockupStream} and {MetadataUpdate} events. vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.WithdrawFromLockupStream({ - streamId: defaultStreamId, + emit ISablierLockup.WithdrawFromLockupStream({ + streamId: ids.defaultStream, to: users.recipient, token: dai, amount: withdrawAmount }); vm.expectEmit({ emitter: address(lockup) }); - emit IERC4906.MetadataUpdate({ _tokenId: defaultStreamId }); + emit IERC4906.MetadataUpdate({ _tokenId: ids.defaultStream }); // Make the withdrawal. - lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: withdrawAmount }); + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: ids.defaultStream, + to: users.recipient, + amount: withdrawAmount + }); // It should mark the stream as depleted. - Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); + Lockup.Status actualStatus = lockup.statusOf(ids.defaultStream); Lockup.Status expectedStatus = Lockup.Status.DEPLETED; assertEq(actualStatus, expectedStatus); // It should update the withdrawn amount. - uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(defaultStreamId); + uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(ids.defaultStream); uint128 expectedWithdrawnAmount = withdrawAmount; assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); // Assert that the not burned NFT. - address actualNFTowner = lockup.ownerOf({ tokenId: defaultStreamId }); + address actualNFTowner = lockup.ownerOf({ tokenId: ids.defaultStream }); address expectedNFTOwner = users.recipient; assertEq(actualNFTowner, expectedNFTOwner, "NFT owner"); } @@ -299,29 +357,30 @@ abstract contract Withdraw_Integration_Concrete_Test is Integration_Test { whenWithdrawAmountNotOverdraw whenWithdrawalAddressRecipient whenCallerSender + whenFeeNotLessThanMinFee givenEndTimeInFuture givenNotCanceledStream { // It should not make Sablier run the recipient hook. - uint128 withdrawAmount = lockup.withdrawableAmountOf(notAllowedtoHookStreamId); + uint128 withdrawAmount = lockup.withdrawableAmountOf(ids.notAllowedToHookStream); vm.expectCall({ callee: address(recipientGood), data: abi.encodeCall( ISablierLockupRecipient.onSablierLockupWithdraw, - (notAllowedtoHookStreamId, users.sender, address(recipientInterfaceIDIncorrect), withdrawAmount) + (ids.notAllowedToHookStream, users.sender, address(recipientInterfaceIDIncorrect), withdrawAmount) ), count: 0 }); // Make the withdrawal. - lockup.withdraw({ - streamId: notAllowedtoHookStreamId, + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: ids.notAllowedToHookStream, to: address(recipientInterfaceIDIncorrect), amount: withdrawAmount }); // It should update the withdrawn amount. - uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(notAllowedtoHookStreamId); + uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(ids.notAllowedToHookStream); uint128 expectedWithdrawnAmount = withdrawAmount; assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); } @@ -336,6 +395,7 @@ abstract contract Withdraw_Integration_Concrete_Test is Integration_Test { whenWithdrawAmountNotOverdraw whenWithdrawalAddressRecipient whenCallerSender + whenFeeNotLessThanMinFee givenEndTimeInFuture givenNotCanceledStream givenRecipientAllowedToHook @@ -345,7 +405,11 @@ abstract contract Withdraw_Integration_Concrete_Test is Integration_Test { vm.expectRevert("You shall not pass"); // Make the withdrawal. - lockup.withdraw({ streamId: recipientRevertStreamId, to: address(recipientReverting), amount: withdrawAmount }); + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: ids.recipientRevertStream, + to: address(recipientReverting), + amount: withdrawAmount + }); } function test_RevertWhen_HookReturnsInvalidSelector() @@ -358,6 +422,7 @@ abstract contract Withdraw_Integration_Concrete_Test is Integration_Test { whenWithdrawAmountNotOverdraw whenWithdrawalAddressRecipient whenCallerSender + whenFeeNotLessThanMinFee givenEndTimeInFuture givenNotCanceledStream givenRecipientAllowedToHook @@ -366,14 +431,12 @@ abstract contract Withdraw_Integration_Concrete_Test is Integration_Test { // Expect a revert. uint128 withdrawAmount = defaults.WITHDRAW_AMOUNT(); vm.expectRevert( - abi.encodeWithSelector( - Errors.SablierLockupBase_InvalidHookSelector.selector, address(recipientInvalidSelector) - ) + abi.encodeWithSelector(Errors.SablierLockup_InvalidHookSelector.selector, address(recipientInvalidSelector)) ); // Cancel the stream. - lockup.withdraw({ - streamId: recipientInvalidSelectorStreamId, + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: ids.recipientInvalidSelectorStream, to: address(recipientInvalidSelector), amount: withdrawAmount }); @@ -389,12 +452,15 @@ abstract contract Withdraw_Integration_Concrete_Test is Integration_Test { whenWithdrawAmountNotOverdraw whenWithdrawalAddressRecipient whenCallerSender + whenFeeNotLessThanMinFee givenEndTimeInFuture givenNotCanceledStream givenRecipientAllowedToHook whenNonRevertingRecipient whenHookReturnsValidSelector { + uint256 previousAggregateAmount = lockup.aggregateAmount(dai); + // Halve the withdraw amount so that the recipient can re-entry and make another withdrawal. uint128 withdrawAmount = defaults.WITHDRAW_AMOUNT() / 2; @@ -403,22 +469,31 @@ abstract contract Withdraw_Integration_Concrete_Test is Integration_Test { address(recipientReentrant), abi.encodeCall( ISablierLockupRecipient.onSablierLockupWithdraw, - (recipientReentrantStreamId, users.sender, address(recipientReentrant), withdrawAmount) + (ids.recipientReentrantStream, users.sender, address(recipientReentrant), withdrawAmount) ) ); // It should make multiple withdrawals. - lockup.withdraw({ streamId: recipientReentrantStreamId, to: address(recipientReentrant), amount: withdrawAmount }); + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: ids.recipientReentrantStream, + to: address(recipientReentrant), + amount: withdrawAmount + }); // Assert that the stream's status is still "STREAMING". - Lockup.Status actualStatus = lockup.statusOf(recipientReentrantStreamId); + Lockup.Status actualStatus = lockup.statusOf(ids.recipientReentrantStream); Lockup.Status expectedStatus = Lockup.Status.STREAMING; assertEq(actualStatus, expectedStatus); // It should update the withdrawn amounts. - uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(recipientReentrantStreamId); + uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(ids.recipientReentrantStream); uint128 expectedWithdrawnAmount = defaults.WITHDRAW_AMOUNT(); assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); + + // It should update the aggregate amount. + uint256 actualAggregateAmount = lockup.aggregateAmount(dai); + uint256 expectedAggregateAmount = previousAggregateAmount - defaults.WITHDRAW_AMOUNT(); + assertEq(actualAggregateAmount, expectedAggregateAmount, "aggregateAmount"); } function test_WhenNoReentrancy() @@ -431,12 +506,15 @@ abstract contract Withdraw_Integration_Concrete_Test is Integration_Test { whenWithdrawAmountNotOverdraw whenWithdrawalAddressRecipient whenCallerSender + whenFeeNotLessThanMinFee givenEndTimeInFuture givenNotCanceledStream givenRecipientAllowedToHook whenNonRevertingRecipient whenHookReturnsValidSelector { + uint256 previousAggregateAmount = lockup.aggregateAmount(dai); + // Set the withdraw amount to the default amount. uint128 withdrawAmount = defaults.WITHDRAW_AMOUNT(); @@ -448,32 +526,41 @@ abstract contract Withdraw_Integration_Concrete_Test is Integration_Test { address(recipientGood), abi.encodeCall( ISablierLockupRecipient.onSablierLockupWithdraw, - (recipientGoodStreamId, users.sender, address(recipientGood), withdrawAmount) + (ids.recipientGoodStream, users.sender, address(recipientGood), withdrawAmount) ) ); // It should emit {WithdrawFromLockupStream} and {MetadataUpdate} events. vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.WithdrawFromLockupStream({ - streamId: recipientGoodStreamId, + emit ISablierLockup.WithdrawFromLockupStream({ + streamId: ids.recipientGoodStream, to: address(recipientGood), token: dai, amount: withdrawAmount }); vm.expectEmit({ emitter: address(lockup) }); - emit IERC4906.MetadataUpdate({ _tokenId: recipientGoodStreamId }); + emit IERC4906.MetadataUpdate({ _tokenId: ids.recipientGoodStream }); // Make the withdrawal. - lockup.withdraw({ streamId: recipientGoodStreamId, to: address(recipientGood), amount: withdrawAmount }); + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: ids.recipientGoodStream, + to: address(recipientGood), + amount: withdrawAmount + }); // Assert that the stream's status is still "STREAMING". - Lockup.Status actualStatus = lockup.statusOf(recipientGoodStreamId); + Lockup.Status actualStatus = lockup.statusOf(ids.recipientGoodStream); Lockup.Status expectedStatus = Lockup.Status.STREAMING; assertEq(actualStatus, expectedStatus); // It should update the withdrawn amount. - uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(recipientGoodStreamId); + uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(ids.recipientGoodStream); uint128 expectedWithdrawnAmount = withdrawAmount; assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); + + // It should update the aggregate amount. + uint256 actualAggregateAmount = lockup.aggregateAmount(dai); + uint256 expectedAggregateAmount = previousAggregateAmount - defaults.WITHDRAW_AMOUNT(); + assertEq(actualAggregateAmount, expectedAggregateAmount, "aggregateAmount"); } } diff --git a/tests/integration/concrete/lockup/withdraw/withdraw.tree b/tests/integration/concrete/lockup/withdraw/withdraw.tree new file mode 100644 index 000000000..5382040ce --- /dev/null +++ b/tests/integration/concrete/lockup/withdraw/withdraw.tree @@ -0,0 +1,69 @@ +Withdraw_Integration_Concrete_Test +├── when delegate call +│ └── it should revert +└── when no delegate call + ├── given null + │ └── it should revert + └── given not null + ├── given DEPLETED status + │ └── it should revert + └── given not DEPLETED status + ├── when withdrawal address zero + │ └── it should revert + └── when withdrawal address not zero + ├── when zero withdraw amount + │ └── it should revert + └── when non zero withdraw amount + ├── when withdraw amount overdraws + │ └── it should revert + └── when withdraw amount not overdraw + ├── when withdrawal address not recipient + │ ├── when caller not approved third party or recipient + │ │ └── it should revert + │ └── when caller approved third party or recipient + │ ├── it should make the withdrawal + │ ├── it should update the withdrawn amount + │ └── it should emit {WithdrawFromLockupStream} and {MetadataUpdate} events + └── when withdrawal address recipient + ├── when caller unknown + │ ├── it should make the withdrawal + │ └── it should update the withdrawn amount + ├── when caller recipient + │ ├── it should make the withdrawal + │ └── it should update the withdrawn amount + └── when caller sender + ├── when fee less than min fee + │ └── it should revert + └── when fee not less than min fee + ├── given end time not in future + │ ├── it should make the withdrawal + │ ├── it should mark the stream as depleted + │ └── it should make the stream not cancelable + └── given end time in future + ├── given canceled stream + │ ├── it should make the withdrawal + │ ├── it should mark the stream as depleted + │ ├── it should update the withdrawn amount + │ └── it should emit {WithdrawFromLockupStream} and {MetadataUpdate} events + └── given not canceled stream + ├── given recipient not allowed to hook + │ ├── it should make the withdrawal + │ ├── it should update the withdrawn amount + │ └── it should not make Sablier run the recipient hook + └── given recipient allowed to hook + ├── when reverting recipient + │ └── it should revert + └── when non reverting recipient + ├── when hook returns invalid selector + │ └── it should revert + └── when hook returns valid selector + ├── when reentrancy + │ ├── it should make multiple withdrawals + │ ├── it should update the withdrawn amounts + │ └── it should make Sablier run the recipient hook + └── when no reentrancy + ├── it should make the withdrawal + ├── it should update the withdrawn amount + ├── it should update the aggregate amount + ├── it should make Sablier run the recipient hook + └── it should emit {WithdrawFromLockupStream} and {MetadataUpdate} events diff --git a/tests/integration/concrete/lockup-base/withdrawable-amount-of/withdrawableAmountOf.t.sol b/tests/integration/concrete/lockup/withdrawable-amount-of/withdrawableAmountOf.t.sol similarity index 82% rename from tests/integration/concrete/lockup-base/withdrawable-amount-of/withdrawableAmountOf.t.sol rename to tests/integration/concrete/lockup/withdrawable-amount-of/withdrawableAmountOf.t.sol index b9566614c..3307ae2bc 100644 --- a/tests/integration/concrete/lockup-base/withdrawable-amount-of/withdrawableAmountOf.t.sol +++ b/tests/integration/concrete/lockup/withdrawable-amount-of/withdrawableAmountOf.t.sol @@ -5,36 +5,36 @@ import { Integration_Test } from "../../../Integration.t.sol"; abstract contract WithdrawableAmountOf_Integration_Concrete_Test is Integration_Test { function test_RevertGiven_Null() external { - expectRevert_Null({ callData: abi.encodeCall(lockup.withdrawableAmountOf, nullStreamId) }); + expectRevert_Null({ callData: abi.encodeCall(lockup.withdrawableAmountOf, ids.nullStream) }); } function test_GivenCanceledStreamAndCANCELEDStatus() external givenNotNull { vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); - lockup.cancel(defaultStreamId); + lockup.cancel(ids.defaultStream); // It should return the correct withdrawable amount. - uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); + uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(ids.defaultStream); uint256 expectedWithdrawableAmount = defaults.STREAMED_AMOUNT_26_PERCENT(); assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); } function test_GivenCanceledStreamAndDEPLETEDStatus() external givenNotNull { vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); - lockup.cancel(defaultStreamId); - lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); + lockup.cancel(ids.defaultStream); + lockup.withdrawMax{ value: LOCKUP_MIN_FEE_WEI }({ streamId: ids.defaultStream, to: users.recipient }); vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() + 10 seconds }); // It should return zero. - uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); + uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(ids.defaultStream); uint128 expectedWithdrawableAmount = 0; assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); } function test_GivenPENDINGStatus() external givenNotNull givenNotCanceledStream { - vm.warp({ newTimestamp: getBlockTimestamp() - 1 seconds }); + rewind(1 seconds); // It should return zero. - uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); + uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(ids.defaultStream); uint128 expectedWithdrawableAmount = 0; assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); } @@ -43,17 +43,17 @@ abstract contract WithdrawableAmountOf_Integration_Concrete_Test is Integration_ vm.warp({ newTimestamp: defaults.END_TIME() }); // It should return the correct withdrawable amount. - uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); + uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(ids.defaultStream); uint128 expectedWithdrawableAmount = defaults.DEPOSIT_AMOUNT(); assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); } function test_GivenDEPLETEDStatus() external givenNotNull givenNotCanceledStream { vm.warp({ newTimestamp: defaults.END_TIME() }); - lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); + lockup.withdrawMax{ value: LOCKUP_MIN_FEE_WEI }({ streamId: ids.defaultStream, to: users.recipient }); // It should return zero. - uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); + uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(ids.defaultStream); uint128 expectedWithdrawableAmount = 0; assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); } diff --git a/tests/integration/concrete/lockup-base/withdrawable-amount-of/withdrawableAmountOf.tree b/tests/integration/concrete/lockup/withdrawable-amount-of/withdrawableAmountOf.tree similarity index 100% rename from tests/integration/concrete/lockup-base/withdrawable-amount-of/withdrawableAmountOf.tree rename to tests/integration/concrete/lockup/withdrawable-amount-of/withdrawableAmountOf.tree diff --git a/tests/integration/concrete/nft-descriptor/generateAccentColor.t.sol b/tests/integration/concrete/nft-descriptor/generateAccentColor.t.sol index 71d0b2152..9b8174320 100644 --- a/tests/integration/concrete/nft-descriptor/generateAccentColor.t.sol +++ b/tests/integration/concrete/nft-descriptor/generateAccentColor.t.sol @@ -8,7 +8,7 @@ contract GenerateAccentColor_Integration_Concrete_Test is Base_Test { // Passing a dummy contract instead of a real Lockup contract to make this test easy to maintain. // Note: the address of `noop` depends on the order of the state variables in {Base_Test}. string memory actualColor = nftDescriptorMock.generateAccentColor_({ sablier: address(noop), streamId: 1337 }); - string memory expectedColor = "hsl(115,39%,48%)"; + string memory expectedColor = "hsl(128,58%,62%)"; assertEq(actualColor, expectedColor, "accentColor"); } } diff --git a/tests/integration/concrete/nft-descriptor/safe-token-symbol/safeTokenSymbol.t.sol b/tests/integration/concrete/nft-descriptor/safe-token-symbol/safeTokenSymbol.t.sol index c782d95cc..075007974 100644 --- a/tests/integration/concrete/nft-descriptor/safe-token-symbol/safeTokenSymbol.t.sol +++ b/tests/integration/concrete/nft-descriptor/safe-token-symbol/safeTokenSymbol.t.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; +import { ERC20Bytes32 } from "@sablier/evm-utils/src/mocks/erc20/ERC20Bytes32.sol"; +import { ERC20Mock } from "@sablier/evm-utils/src/mocks/erc20/ERC20Mock.sol"; import { Base_Test } from "tests/Base.t.sol"; -import { ERC20Bytes32 } from "tests/mocks/erc20/ERC20Bytes32.sol"; -import { ERC20Mock } from "tests/mocks/erc20/ERC20Mock.sol"; contract SafeTokenSymbol_Integration_Concrete_Test is Base_Test { function test_WhenTokenNotContract() external view { @@ -33,8 +33,9 @@ contract SafeTokenSymbol_Integration_Concrete_Test is Base_Test { givenSymbolAsString { ERC20Mock token = new ERC20Mock({ - name: "Token", - symbol: "This symbol is has more than 30 characters and it should be ignored" + name_: "Token", + symbol_: "This symbol is has more than 30 characters and it should be ignored", + decimals_: 18 }); string memory actualSymbol = nftDescriptorMock.safeTokenSymbol_(address(token)); string memory expectedSymbol = "Long Symbol"; @@ -48,7 +49,7 @@ contract SafeTokenSymbol_Integration_Concrete_Test is Base_Test { givenSymbolAsString givenSymbolNotLongerThan30Chars { - ERC20Mock token = new ERC20Mock({ name: "Token", symbol: "" }); + ERC20Mock token = new ERC20Mock({ name_: "Token", symbol_: "", decimals_: 18 }); string memory actualSymbol = nftDescriptorMock.safeTokenSymbol_(address(token)); string memory expectedSymbol = "Unsupported Symbol"; assertEq(actualSymbol, expectedSymbol, "symbol"); diff --git a/tests/integration/fuzz/lockup-dynamic/LockupDynamic.t.sol b/tests/integration/fuzz/lockup-dynamic/LockupDynamic.t.sol index d3c0dd784..28dd99f18 100644 --- a/tests/integration/fuzz/lockup-dynamic/LockupDynamic.t.sol +++ b/tests/integration/fuzz/lockup-dynamic/LockupDynamic.t.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { Lockup } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; import { Integration_Test } from "../../Integration.t.sol"; -import { Cancel_Integration_Fuzz_Test } from "./../lockup-base/cancel.t.sol"; -import { RefundableAmountOf_Integration_Fuzz_Test } from "./../lockup-base/refundableAmountOf.t.sol"; +import { Cancel_Integration_Fuzz_Test } from "./../lockup/cancel.t.sol"; +import { RefundableAmountOf_Integration_Fuzz_Test } from "./../lockup/refundableAmountOf.t.sol"; abstract contract Lockup_Dynamic_Integration_Fuzz_Test is Integration_Test { function setUp() public virtual override { diff --git a/tests/integration/fuzz/lockup-dynamic/createWithDurationsLD.t.sol b/tests/integration/fuzz/lockup-dynamic/createWithDurationsLD.t.sol index 6b3fe1cfa..4ab4382b3 100644 --- a/tests/integration/fuzz/lockup-dynamic/createWithDurationsLD.t.sol +++ b/tests/integration/fuzz/lockup-dynamic/createWithDurationsLD.t.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; -import { Lockup, LockupDynamic } from "src/types/DataTypes.sol"; +import { ISablierLockupDynamic } from "src/interfaces/ISablierLockupDynamic.sol"; +import { Lockup } from "src/types/Lockup.sol"; +import { LockupDynamic } from "src/types/LockupDynamic.sol"; import { Lockup_Dynamic_Integration_Fuzz_Test } from "./LockupDynamic.t.sol"; contract CreateWithDurationsLD_Integration_Fuzz_Test is Lockup_Dynamic_Integration_Fuzz_Test { @@ -10,21 +11,18 @@ contract CreateWithDurationsLD_Integration_Fuzz_Test is Lockup_Dynamic_Integrati uint256 actualNextStreamId; address actualNFTOwner; Lockup.Status actualStatus; - Lockup.CreateAmounts createAmounts; uint256 expectedNextStreamId; address expectedNFTOwner; Lockup.Status expectedStatus; - address funder; bool isCancelable; bool isSettled; LockupDynamic.Segment[] segmentsWithTimestamps; - uint128 totalAmount; + uint128 depositAmount; } function testFuzz_CreateWithDurationsLD(LockupDynamic.SegmentWithDuration[] memory segments) external whenNoDelegateCall - whenSegmentCountNotExceedMaxValue whenTimestampsCalculationNotOverflow { vm.assume(segments.length != 0); @@ -33,24 +31,16 @@ contract CreateWithDurationsLD_Integration_Fuzz_Test is Lockup_Dynamic_Integrati Vars memory vars; fuzzSegmentDurations(segments); - // Fuzz the segment amounts and calculate the total and create amounts (deposit and broker fee). - (vars.totalAmount, vars.createAmounts) = fuzzDynamicStreamAmounts(segments, defaults.BROKER_FEE()); - - // Make the Sender the stream's funder (recall that the Sender is the default caller). - vars.funder = users.sender; + // Fuzz the segment amounts and calculate the deposit amount. + vars.depositAmount = fuzzDynamicStreamAmounts(segments); uint256 expectedStreamId = lockup.nextStreamId(); - // Mint enough tokens to the fuzzed funder. - deal({ token: address(dai), to: vars.funder, give: vars.totalAmount }); - - // Expect the tokens to be transferred from the funder to {SablierLockup}. - expectCallToTransferFrom({ from: vars.funder, to: address(lockup), value: vars.createAmounts.deposit }); + // Mint enough tokens to the sender. + deal({ token: address(dai), to: users.sender, give: vars.depositAmount }); - // Expect the broker fee to be paid to the broker, if not zero. - if (vars.createAmounts.brokerFee > 0) { - expectCallToTransferFrom({ from: vars.funder, to: users.broker, value: vars.createAmounts.brokerFee }); - } + // Expect the tokens to be transferred from the sender to {SablierLockup}. + expectCallToTransferFrom({ from: users.sender, to: address(lockup), value: vars.depositAmount }); // Create the timestamps struct. vars.segmentsWithTimestamps = getSegmentsWithTimestamps(segments); @@ -61,24 +51,24 @@ contract CreateWithDurationsLD_Integration_Fuzz_Test is Lockup_Dynamic_Integrati // Expect the relevant event to be emitted. vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockup.CreateLockupDynamicStream({ + emit ISablierLockupDynamic.CreateLockupDynamicStream({ streamId: expectedStreamId, - commonParams: defaults.lockupCreateEvent(vars.createAmounts, timestamps), + commonParams: defaults.lockupCreateEvent(vars.depositAmount, timestamps), segments: vars.segmentsWithTimestamps }); // Create the stream. - _defaultParams.createWithDurations.totalAmount = vars.totalAmount; + _defaultParams.createWithDurations.depositAmount = vars.depositAmount; _defaultParams.createWithDurations.transferable = true; uint256 streamId = lockup.createWithDurationsLD(_defaultParams.createWithDurations, segments); - // Check if the stream is settled. It is possible for a Lockup Dynamic stream to settle at the time of creation - // because some segment amounts can be zero. + // Check if the stream is settled. It is possible for a stream to settle at the time of creation because some + // segment amounts can be zero. vars.isSettled = lockup.refundableAmountOf(streamId) == 0; vars.isCancelable = vars.isSettled ? false : true; // It should create the stream. - assertEq(lockup.getDepositedAmount(streamId), vars.createAmounts.deposit, "depositedAmount"); + assertEq(lockup.getDepositedAmount(streamId), vars.depositAmount, "depositedAmount"); assertEq(lockup.getEndTime(streamId), timestamps.end, "endTime"); assertEq(lockup.isCancelable(streamId), vars.isCancelable, "isCancelable"); assertFalse(lockup.isDepleted(streamId), "isDepleted"); diff --git a/tests/integration/fuzz/lockup-dynamic/createWithTimestampsLD.t.sol b/tests/integration/fuzz/lockup-dynamic/createWithTimestampsLD.t.sol index 6bbdd639e..72e4962f7 100644 --- a/tests/integration/fuzz/lockup-dynamic/createWithTimestampsLD.t.sol +++ b/tests/integration/fuzz/lockup-dynamic/createWithTimestampsLD.t.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { MAX_UD60x18, ud } from "@prb/math/src/UD60x18.sol"; import { stdError } from "forge-std/src/StdError.sol"; -import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; +import { ISablierLockupDynamic } from "src/interfaces/ISablierLockupDynamic.sol"; import { Errors } from "src/libraries/Errors.sol"; -import { Broker, Lockup, LockupDynamic } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; +import { LockupDynamic } from "src/types/LockupDynamic.sol"; import { Lockup_Dynamic_Integration_Fuzz_Test } from "./LockupDynamic.t.sol"; contract CreateWithTimestampsLD_Integration_Fuzz_Test is Lockup_Dynamic_Integration_Fuzz_Test { @@ -25,22 +25,6 @@ contract CreateWithTimestampsLD_Integration_Fuzz_Test is Lockup_Dynamic_Integrat createDefaultStream(); } - function testFuzz_RevertWhen_SegmentCountTooHigh(uint256 segmentCount) - external - whenNoDelegateCall - whenShapeNotExceed32Bytes - whenSenderNotZeroAddress - whenRecipientNotZeroAddress - whenDepositAmountNotZero - whenSegmentCountNotZero - { - uint256 defaultMax = defaults.MAX_COUNT(); - segmentCount = _bound(segmentCount, defaultMax + 1, defaultMax * 2); - LockupDynamic.Segment[] memory segments = new LockupDynamic.Segment[](segmentCount); - vm.expectRevert(abi.encodeWithSelector(Errors.SablierHelpers_SegmentCountTooHigh.selector, segmentCount)); - lockup.createWithTimestampsLD(_defaultParams.createWithTimestamps, segments); - } - function testFuzz_RevertWhen_SegmentAmountsSumOverflows( uint128 amount0, uint128 amount1 @@ -52,7 +36,6 @@ contract CreateWithTimestampsLD_Integration_Fuzz_Test is Lockup_Dynamic_Integrat whenRecipientNotZeroAddress whenDepositAmountNotZero whenSegmentCountNotZero - whenSegmentCountNotExceedMaxValue { amount0 = boundUint128(amount0, MAX_UINT128 / 2 + 1, MAX_UINT128); amount1 = boundUint128(amount0, MAX_UINT128 / 2 + 1, MAX_UINT128); @@ -70,7 +53,6 @@ contract CreateWithTimestampsLD_Integration_Fuzz_Test is Lockup_Dynamic_Integrat whenRecipientNotZeroAddress whenDepositAmountNotZero whenSegmentCountNotZero - whenSegmentCountNotExceedMaxValue whenSegmentAmountsSumNotOverflow { firstTimestamp = boundUint40(firstTimestamp, 0, defaults.START_TIME()); @@ -97,22 +79,20 @@ contract CreateWithTimestampsLD_Integration_Fuzz_Test is Lockup_Dynamic_Integrat whenRecipientNotZeroAddress whenDepositAmountNotZero whenSegmentCountNotZero - whenSegmentCountNotExceedMaxValue whenSegmentAmountsSumNotOverflow whenStartTimeLessThanFirstTimestamp whenTimestampsStrictlyIncreasing { - depositDiff = boundUint128(depositDiff, 100, defaults.TOTAL_AMOUNT()); + depositDiff = boundUint128(depositDiff, 100, defaults.DEPOSIT_AMOUNT()); - resetPrank({ msgSender: users.sender }); + setMsgSender(users.sender); // Adjust the default deposit amount. uint128 defaultDepositAmount = defaults.DEPOSIT_AMOUNT(); uint128 depositAmount = defaultDepositAmount + depositDiff; // Prepare the params. - _defaultParams.createWithTimestamps.totalAmount = depositAmount; - _defaultParams.createWithTimestamps.broker = defaults.brokerNull(); + _defaultParams.createWithTimestamps.depositAmount = depositAmount; // Expect the relevant error to be thrown. vm.expectRevert( @@ -125,52 +105,26 @@ contract CreateWithTimestampsLD_Integration_Fuzz_Test is Lockup_Dynamic_Integrat createDefaultStream(); } - function testFuzz_RevertWhen_BrokerFeeTooHigh(Broker memory broker) - external - whenNoDelegateCall - whenShapeNotExceed32Bytes - whenSenderNotZeroAddress - whenRecipientNotZeroAddress - whenDepositAmountNotZero - whenSegmentCountNotZero - whenSegmentCountNotExceedMaxValue - whenSegmentAmountsSumNotOverflow - whenStartTimeLessThanFirstTimestamp - whenTimestampsStrictlyIncreasing - whenDepositAmountNotEqualSegmentAmountsSum - { - vm.assume(broker.account != address(0)); - broker.fee = _bound(broker.fee, MAX_BROKER_FEE + ud(1), MAX_UD60x18); - _defaultParams.createWithTimestamps.broker = broker; - vm.expectRevert( - abi.encodeWithSelector(Errors.SablierHelpers_BrokerFeeTooHigh.selector, broker.fee, MAX_BROKER_FEE) - ); - createDefaultStream(); - } - struct Vars { uint256 actualNextStreamId; address actualNFTOwner; Lockup.Status actualStatus; - Lockup.CreateAmounts createAmounts; uint256 expectedNextStreamId; address expectedNFTOwner; Lockup.Status expectedStatus; bool isCancelable; bool isSettled; - uint128 totalAmount; } /// @dev Given enough fuzz runs, all of the following scenarios will be fuzzed: /// - /// - All possible permutations for the funder, sender, recipient, and broker + /// - All possible permutations for the funder, sender and recipient /// - Multiple values for the segment amounts, exponents, and timestamps /// - Cancelable and not cancelable /// - Start time in the past /// - Start time in the present /// - Start time in the future /// - Start time equal and not equal to the first segment timestamp - /// - Multiple values for the broker fee, including zero function testFuzz_CreateWithTimestampsLD( address funder, Lockup.CreateWithTimestamps memory params, @@ -184,21 +138,15 @@ contract CreateWithTimestampsLD_Integration_Fuzz_Test is Lockup_Dynamic_Integrat whenDepositAmountNotZero whenStartTimeNotZero whenSegmentCountNotZero - whenSegmentCountNotExceedMaxValue whenSegmentAmountsSumNotOverflow whenStartTimeLessThanFirstTimestamp whenTimestampsStrictlyIncreasing whenDepositAmountNotEqualSegmentAmountsSum - whenBrokerFeeNotExceedMaxValue whenTokenContract whenTokenERC20 { - vm.assume( - funder != address(0) && params.sender != address(0) && params.recipient != address(0) - && params.broker.account != address(0) - ); + vm.assume(funder != address(0) && params.sender != address(0) && params.recipient != address(0)); vm.assume(segments.length != 0); - params.broker.fee = _bound(params.broker.fee, 0, MAX_BROKER_FEE); params.token = dai; params.timestamps.start = boundUint40(params.timestamps.start, 1, defaults.START_TIME()); @@ -208,64 +156,47 @@ contract CreateWithTimestampsLD_Integration_Fuzz_Test is Lockup_Dynamic_Integrat fuzzSegmentTimestamps(segments, params.timestamps.start); params.timestamps.end = segments[segments.length - 1].timestamp; - // If shape exceeds 32 bytes, use the default value. + // If the shape exceeds 32 bytes, use the default value. if (bytes(params.shape).length > 32) params.shape = defaults.SHAPE(); - // Fuzz the segment amounts and calculate the total and create amounts (deposit and broker fee). - Vars memory vars; - (vars.totalAmount, vars.createAmounts) = - fuzzDynamicStreamAmounts({ upperBound: MAX_UINT128, segments: segments, brokerFee: params.broker.fee }); + uint256 previousAggregateAmount = lockup.aggregateAmount(dai); - params.totalAmount = vars.totalAmount; + // Fuzz the segment amounts and calculate the deposit amount + Vars memory vars; + params.depositAmount = fuzzDynamicStreamAmounts({ upperBound: MAX_UINT128, segments: segments }); // Make the fuzzed funder the caller in the rest of this test. - resetPrank(funder); + setMsgSender(funder); uint256 expectedStreamId = lockup.nextStreamId(); // Mint enough tokens to the fuzzed funder. - deal({ token: address(dai), to: funder, give: vars.totalAmount }); + deal({ token: address(dai), to: funder, give: params.depositAmount }); // Approve {SablierLockup} to transfer the tokens from the fuzzed funder. dai.approve({ spender: address(lockup), value: MAX_UINT256 }); // Expect the tokens to be transferred from the funder to {SablierLockup}. - expectCallToTransferFrom({ from: funder, to: address(lockup), value: vars.createAmounts.deposit }); - - // Expect the broker fee to be paid to the broker, if not zero. - if (vars.createAmounts.brokerFee > 0) { - expectCallToTransferFrom({ from: funder, to: params.broker.account, value: vars.createAmounts.brokerFee }); - } + expectCallToTransferFrom({ from: funder, to: address(lockup), value: params.depositAmount }); // Expect the relevant event to be emitted. vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockup.CreateLockupDynamicStream({ + emit ISablierLockupDynamic.CreateLockupDynamicStream({ streamId: expectedStreamId, - commonParams: Lockup.CreateEventCommon({ - funder: funder, - sender: params.sender, - recipient: params.recipient, - amounts: vars.createAmounts, - token: dai, - cancelable: params.cancelable, - transferable: params.transferable, - timestamps: params.timestamps, - shape: params.shape, - broker: params.broker.account - }), + commonParams: defaults.lockupCreateEvent(funder, params, dai), segments: segments }); // Create the stream. uint256 streamId = lockup.createWithTimestampsLD(params, segments); - // Check if the stream is settled. It is possible for a Lockup Dynamic stream to settle at the time of creation - // because some segment amounts can be zero. + // Check if the stream is settled. It is possible for a stream to settle at the time of creation because some + // segment amounts can be zero. vars.isSettled = (lockup.getDepositedAmount(streamId) - lockup.streamedAmountOf(streamId)) == 0; vars.isCancelable = vars.isSettled ? false : params.cancelable; // It should create the stream. - assertEq(lockup.getDepositedAmount(streamId), vars.createAmounts.deposit, "depositedAmount"); + assertEq(lockup.getDepositedAmount(streamId), params.depositAmount, "depositedAmount"); assertEq(lockup.getEndTime(streamId), params.timestamps.end, "endTime"); assertEq(lockup.isCancelable(streamId), vars.isCancelable, "isCancelable"); assertFalse(lockup.isDepleted(streamId), "isDepleted"); @@ -299,5 +230,8 @@ contract CreateWithTimestampsLD_Integration_Fuzz_Test is Lockup_Dynamic_Integrat vars.actualNFTOwner = lockup.ownerOf({ tokenId: streamId }); vars.expectedNFTOwner = params.recipient; assertEq(vars.actualNFTOwner, vars.expectedNFTOwner, "NFT owner"); + + // Assert that the aggregate amount has been updated. + assertEq(lockup.aggregateAmount(dai), previousAggregateAmount + params.depositAmount); } } diff --git a/tests/integration/fuzz/lockup-dynamic/streamedAmountOf.t.sol b/tests/integration/fuzz/lockup-dynamic/streamedAmountOf.t.sol index 08bc533dd..55a9dfda8 100644 --- a/tests/integration/fuzz/lockup-dynamic/streamedAmountOf.t.sol +++ b/tests/integration/fuzz/lockup-dynamic/streamedAmountOf.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { ZERO } from "@prb/math/src/UD60x18.sol"; -import { Lockup, LockupDynamic } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; +import { LockupDynamic } from "src/types/LockupDynamic.sol"; import { Lockup_Dynamic_Integration_Fuzz_Test } from "./LockupDynamic.t.sol"; @@ -35,8 +35,8 @@ contract StreamedAmountOf_Lockup_Dynamic_Integration_Fuzz_Test is Lockup_Dynamic deal({ token: address(dai), to: users.sender, give: segment.amount }); // Create the stream with the fuzzed segment. - Lockup.CreateWithTimestamps memory params = defaults.createWithTimestampsBrokerNull(); - params.totalAmount = segment.amount; + Lockup.CreateWithTimestamps memory params = defaults.createWithTimestamps(); + params.depositAmount = segment.amount; params.timestamps.end = segment.timestamp; uint256 streamId = lockup.createWithTimestampsLD(params, segments); @@ -46,7 +46,7 @@ contract StreamedAmountOf_Lockup_Dynamic_Integration_Fuzz_Test is Lockup_Dynamic // Run the test. uint128 actualStreamedAmount = lockup.streamedAmountOf(streamId); uint128 expectedStreamedAmount = - calculateLockupDynamicStreamedAmount(segments, defaults.START_TIME(), params.totalAmount); + calculateStreamedAmountLD(segments, defaults.START_TIME(), params.depositAmount); assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } @@ -75,8 +75,7 @@ contract StreamedAmountOf_Lockup_Dynamic_Integration_Fuzz_Test is Lockup_Dynamic fuzzSegmentTimestamps(segments, defaults.START_TIME()); // Fuzz the segment amounts. - (uint128 totalAmount,) = - fuzzDynamicStreamAmounts({ upperBound: MAX_UINT128, segments: segments, brokerFee: ZERO }); + uint128 depositAmount = fuzzDynamicStreamAmounts({ upperBound: MAX_UINT128, segments: segments }); // Bound the time jump. uint40 firstSegmentDuration = segments[1].timestamp - segments[0].timestamp; @@ -84,11 +83,11 @@ contract StreamedAmountOf_Lockup_Dynamic_Integration_Fuzz_Test is Lockup_Dynamic timeJump = boundUint40(timeJump, firstSegmentDuration, totalDuration + 100 seconds); // Mint enough tokens to the Sender. - deal({ token: address(dai), to: users.sender, give: totalAmount }); + deal({ token: address(dai), to: users.sender, give: depositAmount }); // Create the stream with the fuzzed segments. - Lockup.CreateWithTimestamps memory params = defaults.createWithTimestampsBrokerNull(); - params.totalAmount = totalAmount; + Lockup.CreateWithTimestamps memory params = defaults.createWithTimestamps(); + params.depositAmount = depositAmount; params.timestamps.end = segments[segments.length - 1].timestamp; uint256 streamId = lockup.createWithTimestampsLD(params, segments); @@ -97,8 +96,7 @@ contract StreamedAmountOf_Lockup_Dynamic_Integration_Fuzz_Test is Lockup_Dynamic // Run the test. uint128 actualStreamedAmount = lockup.streamedAmountOf(streamId); - uint128 expectedStreamedAmount = - calculateLockupDynamicStreamedAmount(segments, defaults.START_TIME(), totalAmount); + uint128 expectedStreamedAmount = calculateStreamedAmountLD(segments, defaults.START_TIME(), depositAmount); assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } @@ -121,8 +119,7 @@ contract StreamedAmountOf_Lockup_Dynamic_Integration_Fuzz_Test is Lockup_Dynamic fuzzSegmentTimestamps(segments, defaults.START_TIME()); // Fuzz the segment amounts. - (uint128 totalAmount,) = - fuzzDynamicStreamAmounts({ upperBound: MAX_UINT128, segments: segments, brokerFee: ZERO }); + uint128 depositAmount = fuzzDynamicStreamAmounts({ upperBound: MAX_UINT128, segments: segments }); // Bound the time warps. uint40 firstSegmentDuration = segments[1].timestamp - segments[0].timestamp; @@ -131,11 +128,11 @@ contract StreamedAmountOf_Lockup_Dynamic_Integration_Fuzz_Test is Lockup_Dynamic timeWarp1 = boundUint40(timeWarp1, timeWarp0, totalDuration); // Mint enough tokens to the Sender. - deal({ token: address(dai), to: users.sender, give: totalAmount }); + deal({ token: address(dai), to: users.sender, give: depositAmount }); // Create the stream with the fuzzed segments. - Lockup.CreateWithTimestamps memory params = defaults.createWithTimestampsBrokerNull(); - params.totalAmount = totalAmount; + Lockup.CreateWithTimestamps memory params = defaults.createWithTimestamps(); + params.depositAmount = depositAmount; params.timestamps.end = segments[segments.length - 1].timestamp; uint256 streamId = lockup.createWithTimestampsLD(params, segments); diff --git a/tests/integration/fuzz/lockup-dynamic/withdraw.t.sol b/tests/integration/fuzz/lockup-dynamic/withdraw.t.sol index ff04b5c68..9783200eb 100644 --- a/tests/integration/fuzz/lockup-dynamic/withdraw.t.sol +++ b/tests/integration/fuzz/lockup-dynamic/withdraw.t.sol @@ -3,11 +3,12 @@ pragma solidity >=0.8.22 <0.9.0; import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; -import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; -import { Lockup, LockupDynamic } from "src/types/DataTypes.sol"; +import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; +import { Lockup } from "src/types/Lockup.sol"; +import { LockupDynamic } from "src/types/LockupDynamic.sol"; import { Integration_Test } from "../../Integration.t.sol"; -import { Withdraw_Integration_Fuzz_Test } from "./../lockup-base/withdraw.t.sol"; +import { Withdraw_Integration_Fuzz_Test } from "./../lockup/withdraw.t.sol"; import { Lockup_Dynamic_Integration_Fuzz_Test } from "./LockupDynamic.t.sol"; /// @dev This contract complements the tests in {Withdraw_Integration_Fuzz_Test} by testing the withdraw function @@ -29,14 +30,12 @@ contract Withdraw_Lockup_Dynamic_Integration_Fuzz_Test is struct Vars { Lockup.Status actualStatus; uint256 actualWithdrawnAmount; - Lockup.CreateAmounts createAmounts; Lockup.Status expectedStatus; uint256 expectedWithdrawnAmount; bool isDepleted; bool isSettled; - address funder; uint256 streamId; - uint128 totalAmount; + uint128 depositAmount; uint40 totalDuration; uint128 withdrawAmount; uint128 withdrawableAmount; @@ -53,29 +52,27 @@ contract Withdraw_Lockup_Dynamic_Integration_Fuzz_Test is vm.assume(params.segments.length != 0); vm.assume(params.to != address(0)); - // Make the Sender the stream's funder (recall that the Sender is the default caller). Vars memory vars; - vars.funder = users.sender; // Fuzz the segment timestamps. fuzzSegmentTimestamps(params.segments, defaults.START_TIME()); // Fuzz the segment amounts. - (vars.totalAmount, vars.createAmounts) = fuzzDynamicStreamAmounts(params.segments, defaults.BROKER_FEE()); + vars.depositAmount = fuzzDynamicStreamAmounts(params.segments); // Bound the time jump. vars.totalDuration = params.segments[params.segments.length - 1].timestamp - defaults.START_TIME(); params.timeJump = _bound(params.timeJump, 1 seconds, vars.totalDuration + 100 seconds); - // Mint enough tokens to the funder. - deal({ token: address(dai), to: vars.funder, give: vars.totalAmount }); + // Mint enough tokens to the sender. + deal({ token: address(dai), to: users.sender, give: vars.depositAmount }); // Make the Sender the caller. - resetPrank({ msgSender: users.sender }); + setMsgSender(users.sender); // Create the stream with the fuzzed segments. Lockup.CreateWithTimestamps memory createParams = defaults.createWithTimestamps(); - createParams.totalAmount = vars.totalAmount; + createParams.depositAmount = vars.depositAmount; createParams.timestamps.end = params.segments[params.segments.length - 1].timestamp; vars.streamId = lockup.createWithTimestampsLD(createParams, params.segments); @@ -94,12 +91,14 @@ contract Withdraw_Lockup_Dynamic_Integration_Fuzz_Test is // Bound the withdraw amount. vars.withdrawAmount = boundUint128(vars.withdrawAmount, 1, vars.withdrawableAmount); + uint256 previousAggregateAmount = lockup.aggregateAmount(dai); + // Expect the tokens to be transferred to the fuzzed `to` address. expectCallToTransfer({ to: params.to, value: vars.withdrawAmount }); // Expect the relevant events to be emitted. vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.WithdrawFromLockupStream({ + emit ISablierLockup.WithdrawFromLockupStream({ streamId: vars.streamId, to: params.to, amount: vars.withdrawAmount, @@ -109,14 +108,18 @@ contract Withdraw_Lockup_Dynamic_Integration_Fuzz_Test is emit IERC4906.MetadataUpdate({ _tokenId: vars.streamId }); // Make the Recipient the caller. - resetPrank({ msgSender: users.recipient }); + setMsgSender(users.recipient); // Make the withdrawal. - lockup.withdraw({ streamId: vars.streamId, to: params.to, amount: vars.withdrawAmount }); + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: vars.streamId, + to: params.to, + amount: vars.withdrawAmount + }); // Check if the stream is depleted or settled. It is possible for the stream to be just settled // and not depleted because the withdraw amount is fuzzed. - vars.isDepleted = vars.withdrawAmount == vars.createAmounts.deposit; + vars.isDepleted = vars.withdrawAmount == vars.depositAmount; vars.isSettled = lockup.refundableAmountOf(vars.streamId) == 0; // Assert that the stream's status is correct. @@ -134,5 +137,8 @@ contract Withdraw_Lockup_Dynamic_Integration_Fuzz_Test is vars.actualWithdrawnAmount = lockup.getWithdrawnAmount(vars.streamId); vars.expectedWithdrawnAmount = vars.withdrawAmount; assertEq(vars.actualWithdrawnAmount, vars.expectedWithdrawnAmount, "withdrawnAmount"); + + // Assert that the aggregate amount has been updated. + assertEq(lockup.aggregateAmount(dai), previousAggregateAmount - vars.actualWithdrawnAmount, "aggregateAmount"); } } diff --git a/tests/integration/fuzz/lockup-dynamic/withdrawableAmountOf.t.sol b/tests/integration/fuzz/lockup-dynamic/withdrawableAmountOf.t.sol index e01e6aa44..9c3ec8346 100644 --- a/tests/integration/fuzz/lockup-dynamic/withdrawableAmountOf.t.sol +++ b/tests/integration/fuzz/lockup-dynamic/withdrawableAmountOf.t.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { Lockup } from "src/types/DataTypes.sol"; - import { Lockup_Dynamic_Integration_Fuzz_Test } from "./LockupDynamic.t.sol"; contract WithdrawableAmountOf_Lockup_Dynamic_Integration_Fuzz_Test is Lockup_Dynamic_Integration_Fuzz_Test { @@ -16,19 +14,14 @@ contract WithdrawableAmountOf_Lockup_Dynamic_Integration_Fuzz_Test is Lockup_Dyn function testFuzz_WithdrawableAmountOf_NoPreviousWithdrawals(uint40 timeJump) external givenStartTimeInPast { timeJump = boundUint40(timeJump, defaults.CLIFF_DURATION(), defaults.TOTAL_DURATION() * 2); - // Create the stream with a custom total amount. The broker fee is disabled so that it doesn't interfere with - // the calculations. - Lockup.CreateWithTimestamps memory params = defaults.createWithTimestampsBrokerNull(); - uint256 streamId = lockup.createWithTimestampsLD(params, defaults.segments()); - // Simulate the passage of time. uint40 blockTimestamp = defaults.START_TIME() + timeJump; vm.warp({ newTimestamp: blockTimestamp }); // Run the test. - uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(streamId); + uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(ids.defaultStream); uint128 expectedWithdrawableAmount = - calculateLockupDynamicStreamedAmount(defaults.segments(), defaults.START_TIME(), defaults.DEPOSIT_AMOUNT()); + calculateStreamedAmountLD(defaults.segments(), defaults.START_TIME(), defaults.DEPOSIT_AMOUNT()); assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); } @@ -50,12 +43,6 @@ contract WithdrawableAmountOf_Lockup_Dynamic_Integration_Fuzz_Test is Lockup_Dyn givenStartTimeInPast givenPreviousWithdrawal { - // Create the stream with a custom total amount. The broker fee is disabled so that it doesn't interfere with - // the calculations. - Lockup.CreateWithTimestamps memory params = defaults.createWithTimestampsBrokerNull(); - params.totalAmount = defaults.DEPOSIT_AMOUNT(); - uint256 streamId = lockup.createWithTimestampsLD(params, defaults.segments()); - timeJump = boundUint40(timeJump, defaults.CLIFF_DURATION(), defaults.TOTAL_DURATION() * 2); // Simulate the passage of time. @@ -63,14 +50,18 @@ contract WithdrawableAmountOf_Lockup_Dynamic_Integration_Fuzz_Test is Lockup_Dyn // Bound the withdraw amount. uint128 streamedAmount = - calculateLockupDynamicStreamedAmount(defaults.segments(), defaults.START_TIME(), defaults.DEPOSIT_AMOUNT()); + calculateStreamedAmountLD(defaults.segments(), defaults.START_TIME(), defaults.DEPOSIT_AMOUNT()); withdrawAmount = boundUint128(withdrawAmount, 1, streamedAmount); // Make the withdrawal. - lockup.withdraw({ streamId: streamId, to: users.recipient, amount: withdrawAmount }); + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: ids.defaultStream, + to: users.recipient, + amount: withdrawAmount + }); // Run the test. - uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(streamId); + uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(ids.defaultStream); uint128 expectedWithdrawableAmount = streamedAmount - withdrawAmount; assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); } diff --git a/tests/integration/fuzz/lockup-linear/LockupLinear.t.sol b/tests/integration/fuzz/lockup-linear/LockupLinear.t.sol index 450cdc972..1b45f3a3a 100644 --- a/tests/integration/fuzz/lockup-linear/LockupLinear.t.sol +++ b/tests/integration/fuzz/lockup-linear/LockupLinear.t.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { Lockup } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; import { Integration_Test } from "../../Integration.t.sol"; -import { Cancel_Integration_Fuzz_Test } from "./../lockup-base/cancel.t.sol"; -import { RefundableAmountOf_Integration_Fuzz_Test } from "./../lockup-base/refundableAmountOf.t.sol"; -import { Withdraw_Integration_Fuzz_Test } from "./../lockup-base/withdraw.t.sol"; +import { Cancel_Integration_Fuzz_Test } from "./../lockup/cancel.t.sol"; +import { RefundableAmountOf_Integration_Fuzz_Test } from "./../lockup/refundableAmountOf.t.sol"; +import { Withdraw_Integration_Fuzz_Test } from "./../lockup/withdraw.t.sol"; abstract contract Lockup_Linear_Integration_Fuzz_Test is Integration_Test { function setUp() public virtual override { diff --git a/tests/integration/fuzz/lockup-linear/createWithDurationsLL.t.sol b/tests/integration/fuzz/lockup-linear/createWithDurationsLL.t.sol index 8823059e4..dc7352e7a 100644 --- a/tests/integration/fuzz/lockup-linear/createWithDurationsLL.t.sol +++ b/tests/integration/fuzz/lockup-linear/createWithDurationsLL.t.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; -import { Lockup, LockupLinear } from "src/types/DataTypes.sol"; +import { ISablierLockupLinear } from "src/interfaces/ISablierLockupLinear.sol"; +import { Lockup } from "src/types/Lockup.sol"; +import { LockupLinear } from "src/types/LockupLinear.sol"; import { Lockup_Linear_Integration_Fuzz_Test } from "./LockupLinear.t.sol"; @@ -11,15 +12,10 @@ contract CreateWithDurationsLL_Integration_Fuzz_Test is Lockup_Linear_Integratio durations.total = boundUint40(durations.total, 1 seconds, MAX_UNIX_TIMESTAMP); vm.assume(durations.cliff < durations.total); - // Make the Sender the stream's funder (recall that the Sender is the default caller). - address funder = users.sender; uint256 expectedStreamId = lockup.nextStreamId(); - // Expect the tokens to be transferred from the funder to {SablierLockup}. - expectCallToTransferFrom({ from: funder, to: address(lockup), value: defaults.DEPOSIT_AMOUNT() }); - - // Expect the broker fee to be paid to the broker. - expectCallToTransferFrom({ from: funder, to: users.broker, value: defaults.BROKER_FEE_AMOUNT() }); + // Expect the tokens to be transferred from the sender to {SablierLockup}. + expectCallToTransferFrom({ from: users.sender, to: address(lockup), value: defaults.DEPOSIT_AMOUNT() }); // Create the timestamps struct by calculating the start time, cliff time and the end time. Lockup.Timestamps memory timestamps = @@ -30,7 +26,7 @@ contract CreateWithDurationsLL_Integration_Fuzz_Test is Lockup_Linear_Integratio // Expect the relevant event to be emitted. vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockup.CreateLockupLinearStream({ + emit ISablierLockupLinear.CreateLockupLinearStream({ streamId: expectedStreamId, commonParams: defaults.lockupCreateEvent(timestamps), cliffTime: cliffTime, @@ -56,8 +52,7 @@ contract CreateWithDurationsLL_Integration_Fuzz_Test is Lockup_Linear_Integratio assertEq(lockup.getStartTime(streamId), timestamps.start, "startTime"); assertFalse(lockup.wasCanceled(streamId), "wasCanceled"); assertEq(lockup.getUnderlyingToken(streamId), dai, "underlyingToken"); - assertEq(lockup.getUnlockAmounts(streamId).start, unlockAmounts.start, "unlockAmounts.start"); - assertEq(lockup.getUnlockAmounts(streamId).cliff, unlockAmounts.cliff, "unlockAmounts.cliff"); + assertEq(lockup.getUnlockAmounts(streamId), unlockAmounts); // Assert that the stream's status is "STREAMING". Lockup.Status actualStatus = lockup.statusOf(streamId); diff --git a/tests/integration/fuzz/lockup-linear/createWithTimestampsLL.t.sol b/tests/integration/fuzz/lockup-linear/createWithTimestampsLL.t.sol index 2ddbb064a..c0e90ac92 100644 --- a/tests/integration/fuzz/lockup-linear/createWithTimestampsLL.t.sol +++ b/tests/integration/fuzz/lockup-linear/createWithTimestampsLL.t.sol @@ -1,11 +1,10 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { MAX_UD60x18, ud } from "@prb/math/src/UD60x18.sol"; - -import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; +import { ISablierLockupLinear } from "src/interfaces/ISablierLockupLinear.sol"; import { Errors } from "src/libraries/Errors.sol"; -import { Broker, Lockup, LockupLinear } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; +import { LockupLinear } from "src/types/LockupLinear.sol"; import { Lockup_Linear_Integration_Fuzz_Test } from "./LockupLinear.t.sol"; @@ -26,24 +25,6 @@ contract CreateWithTimestampsLL_Integration_Fuzz_Test is Lockup_Linear_Integrati createDefaultStream(); } - function testFuzz_RevertWhen_BrokerFeeTooHigh(Broker memory broker) - external - whenNoDelegateCall - whenShapeNotExceed32Bytes - whenSenderNotZeroAddress - whenRecipientNotZeroAddress - whenDepositAmountNotZero - { - vm.assume(broker.account != address(0)); - broker.fee = _bound(broker.fee, MAX_BROKER_FEE + ud(1), MAX_UD60x18); - vm.expectRevert( - abi.encodeWithSelector(Errors.SablierHelpers_BrokerFeeTooHigh.selector, broker.fee, MAX_BROKER_FEE) - ); - - _defaultParams.createWithTimestamps.broker = broker; - createDefaultStream(); - } - function testFuzz_RevertWhen_StartTimeNotLessThanCliffTime(uint40 startTime) external whenNoDelegateCall @@ -94,7 +75,6 @@ contract CreateWithTimestampsLL_Integration_Fuzz_Test is Lockup_Linear_Integrati uint256 actualNextStreamId; address actualNFTOwner; Lockup.Status actualStatus; - Lockup.CreateAmounts createAmounts; uint256 expectedNextStreamId; address expectedNFTOwner; Lockup.Status expectedStatus; @@ -102,7 +82,7 @@ contract CreateWithTimestampsLL_Integration_Fuzz_Test is Lockup_Linear_Integrati /// @dev Given enough fuzz runs, all of the following scenarios will be fuzzed: /// - /// - All possible permutations for the funder, sender, recipient, and broker + /// - All possible permutations for the funder, sender and recipient /// - Multiple values for the total amount /// - Cancelable and not cancelable /// - Start time in the past @@ -112,7 +92,6 @@ contract CreateWithTimestampsLL_Integration_Fuzz_Test is Lockup_Linear_Integrati /// - Cliff time zero and not zero /// - Multiple values for the cliff time and the end time /// - Multiple values for start unlock amount and cliff unlock amount - /// - Multiple values for the broker fee, including zero function testFuzz_CreateWithTimestampsLL( address funder, Lockup.CreateWithTimestamps memory params, @@ -127,18 +106,13 @@ contract CreateWithTimestampsLL_Integration_Fuzz_Test is Lockup_Linear_Integrati whenDepositAmountNotZero whenStartTimeNotZero whenCliffTimeLessThanEndTime - whenBrokerFeeNotExceedMaxValue whenTokenContract whenTokenERC20 { - vm.assume( - funder != address(0) && params.sender != address(0) && params.recipient != address(0) - && params.broker.account != address(0) - ); - vm.assume(params.totalAmount != 0); + vm.assume(funder != address(0) && params.sender != address(0) && params.recipient != address(0)); + vm.assume(params.depositAmount != 0); params.timestamps.start = boundUint40(params.timestamps.start, defaults.START_TIME(), defaults.START_TIME() + 10_000 seconds); - params.broker.fee = _bound(params.broker.fee, 0, MAX_BROKER_FEE); params.transferable = true; // The cliff time must be either zero or greater than the start time. @@ -150,53 +124,36 @@ contract CreateWithTimestampsLL_Integration_Fuzz_Test is Lockup_Linear_Integrati boundUint40(params.timestamps.end, params.timestamps.start + 1 seconds, MAX_UNIX_TIMESTAMP); } - // If shape exceeds 32 bytes, use the default value. + // If the shape exceeds 32 bytes, use the default value. if (bytes(params.shape).length > 32) params.shape = defaults.SHAPE(); + uint256 previousAggregateAmount = lockup.aggregateAmount(dai); + // Calculate the fee amounts and the deposit amount. Vars memory vars; - vars.createAmounts.brokerFee = ud(params.totalAmount).mul(params.broker.fee).intoUint128(); - vars.createAmounts.deposit = params.totalAmount - vars.createAmounts.brokerFee; - - unlockAmounts.start = boundUint128(unlockAmounts.start, 0, vars.createAmounts.deposit); + unlockAmounts.start = boundUint128(unlockAmounts.start, 0, params.depositAmount); unlockAmounts.cliff = - cliffTime > 0 ? boundUint128(unlockAmounts.cliff, 0, vars.createAmounts.deposit - unlockAmounts.start) : 0; + cliffTime > 0 ? boundUint128(unlockAmounts.cliff, 0, params.depositAmount - unlockAmounts.start) : 0; // Make the fuzzed funder the caller in this test. - resetPrank(funder); + setMsgSender(funder); vars.expectedStreamId = lockup.nextStreamId(); // Mint enough tokens to the funder. - deal({ token: address(dai), to: funder, give: params.totalAmount }); + deal({ token: address(dai), to: funder, give: params.depositAmount }); // Approve {SablierLockup} to transfer the tokens from the fuzzed funder. dai.approve({ spender: address(lockup), value: MAX_UINT256 }); // Expect the tokens to be transferred from the funder to {SablierLockup}. - expectCallToTransferFrom({ from: funder, to: address(lockup), value: vars.createAmounts.deposit }); - - // Expect the broker fee to be paid to the broker, if not zero. - if (vars.createAmounts.brokerFee > 0) { - expectCallToTransferFrom({ from: funder, to: params.broker.account, value: vars.createAmounts.brokerFee }); - } + expectCallToTransferFrom({ from: funder, to: address(lockup), value: params.depositAmount }); // Expect the relevant event to be emitted. vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockup.CreateLockupLinearStream({ + emit ISablierLockupLinear.CreateLockupLinearStream({ streamId: vars.expectedStreamId, - commonParams: Lockup.CreateEventCommon({ - funder: funder, - sender: params.sender, - recipient: params.recipient, - amounts: vars.createAmounts, - token: dai, - cancelable: params.cancelable, - transferable: params.transferable, - timestamps: Lockup.Timestamps({ start: params.timestamps.start, end: params.timestamps.end }), - shape: params.shape, - broker: params.broker.account - }), + commonParams: defaults.lockupCreateEvent(funder, params, dai), cliffTime: cliffTime, unlockAmounts: unlockAmounts }); @@ -208,7 +165,7 @@ contract CreateWithTimestampsLL_Integration_Fuzz_Test is Lockup_Linear_Integrati // It should create the stream. assertEq(lockup.getCliffTime(vars.actualStreamId), cliffTime, "cliffTime"); - assertEq(lockup.getDepositedAmount(vars.actualStreamId), vars.createAmounts.deposit, "depositedAmount"); + assertEq(lockup.getDepositedAmount(vars.actualStreamId), params.depositAmount, "depositedAmount"); assertEq(lockup.getEndTime(vars.actualStreamId), params.timestamps.end, "endTime"); assertEq(lockup.isCancelable(vars.actualStreamId), params.cancelable, "isCancelable"); assertFalse(lockup.isDepleted(vars.actualStreamId), "isDepleted"); @@ -219,8 +176,7 @@ contract CreateWithTimestampsLL_Integration_Fuzz_Test is Lockup_Linear_Integrati assertEq(lockup.getSender(vars.actualStreamId), params.sender, "sender"); assertEq(lockup.getStartTime(vars.actualStreamId), params.timestamps.start, "startTime"); assertEq(lockup.getUnderlyingToken(vars.actualStreamId), dai, "underlyingToken"); - assertEq(lockup.getUnlockAmounts(vars.actualStreamId).start, unlockAmounts.start, "unlockAmounts.start"); - assertEq(lockup.getUnlockAmounts(vars.actualStreamId).cliff, unlockAmounts.cliff, "unlockAmounts.cliff"); + assertEq(lockup.getUnlockAmounts(vars.actualStreamId), unlockAmounts); assertFalse(lockup.wasCanceled(vars.actualStreamId), "wasCanceled"); // Assert that the stream's status is correct. @@ -233,5 +189,13 @@ contract CreateWithTimestampsLL_Integration_Fuzz_Test is Lockup_Linear_Integrati vars.actualNextStreamId = lockup.nextStreamId(); vars.expectedNextStreamId = vars.actualStreamId + 1; assertEq(vars.actualNextStreamId, vars.expectedNextStreamId, "nextStreamId"); + + // Assert that the NFT has been minted. + vars.actualNFTOwner = lockup.ownerOf({ tokenId: vars.actualStreamId }); + vars.expectedNFTOwner = params.recipient; + assertEq(vars.actualNFTOwner, vars.expectedNFTOwner, "NFT owner"); + + // Assert that the aggregate amount has been updated. + assertEq(lockup.aggregateAmount(dai), previousAggregateAmount + params.depositAmount); } } diff --git a/tests/integration/fuzz/lockup-linear/streamedAmountOf.t.sol b/tests/integration/fuzz/lockup-linear/streamedAmountOf.t.sol index ecbfe9498..64576f387 100644 --- a/tests/integration/fuzz/lockup-linear/streamedAmountOf.t.sol +++ b/tests/integration/fuzz/lockup-linear/streamedAmountOf.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { LockupLinear } from "src/types/DataTypes.sol"; +import { LockupLinear } from "src/types/LockupLinear.sol"; import { Lockup_Linear_Integration_Fuzz_Test } from "./LockupLinear.t.sol"; @@ -29,8 +29,7 @@ contract StreamedAmountOf_Lockup_Linear_Integration_Fuzz_Test is Lockup_Linear_I dai.approve(address(lockup), depositAmount); // Create the stream with the fuzzed deposit amount. - _defaultParams.createWithTimestamps.broker = defaults.brokerNull(); - _defaultParams.createWithTimestamps.totalAmount = depositAmount; + _defaultParams.createWithTimestamps.depositAmount = depositAmount; _defaultParams.unlockAmounts = unlockAmounts; uint256 streamId = createDefaultStream(); @@ -72,8 +71,7 @@ contract StreamedAmountOf_Lockup_Linear_Integration_Fuzz_Test is Lockup_Linear_I dai.approve(address(lockup), depositAmount); // Create the stream with the fuzzed deposit amount. - _defaultParams.createWithTimestamps.totalAmount = depositAmount; - _defaultParams.createWithTimestamps.broker = defaults.brokerNull(); + _defaultParams.createWithTimestamps.depositAmount = depositAmount; _defaultParams.unlockAmounts = unlockAmounts; uint256 streamId = createDefaultStream(); @@ -82,7 +80,7 @@ contract StreamedAmountOf_Lockup_Linear_Integration_Fuzz_Test is Lockup_Linear_I // Run the test. uint128 actualStreamedAmount = lockup.streamedAmountOf(streamId); - uint128 expectedStreamedAmount = calculateLockupLinearStreamedAmount( + uint128 expectedStreamedAmount = calculateStreamedAmountLL( defaults.START_TIME(), defaults.CLIFF_TIME(), defaults.END_TIME(), depositAmount, unlockAmounts ); assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); @@ -116,8 +114,7 @@ contract StreamedAmountOf_Lockup_Linear_Integration_Fuzz_Test is Lockup_Linear_I // Create the stream with the fuzzed deposit amount. _defaultParams.unlockAmounts = unlockAmounts; - _defaultParams.createWithTimestamps.totalAmount = depositAmount; - _defaultParams.createWithTimestamps.broker = defaults.brokerNull(); + _defaultParams.createWithTimestamps.depositAmount = depositAmount; uint256 streamId = createDefaultStream(); // Warp to the future for the first time. diff --git a/tests/integration/fuzz/lockup-linear/withdrawableAmountOf.t.sol b/tests/integration/fuzz/lockup-linear/withdrawableAmountOf.t.sol index 45d33051f..3f0e8a3cc 100644 --- a/tests/integration/fuzz/lockup-linear/withdrawableAmountOf.t.sol +++ b/tests/integration/fuzz/lockup-linear/withdrawableAmountOf.t.sol @@ -1,7 +1,8 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { Lockup, LockupLinear } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; +import { LockupLinear } from "src/types/LockupLinear.sol"; import { Lockup_Linear_Integration_Fuzz_Test } from "./LockupLinear.t.sol"; @@ -13,7 +14,7 @@ contract WithdrawableAmountOf_Lockup_Linear_Integration_Fuzz_Test is Lockup_Line { timeJump = boundUint40(timeJump, 0, defaults.CLIFF_DURATION() - 1); vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); - uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); + uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(ids.defaultStream); uint128 expectedWithdrawableAmount = 0; assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); } @@ -40,10 +41,8 @@ contract WithdrawableAmountOf_Lockup_Linear_Integration_Fuzz_Test is Lockup_Line // Mint enough tokens to the Sender. deal({ token: address(dai), to: users.sender, give: depositAmount }); - // Create the stream. The broker fee is disabled so that it doesn't interfere with the calculations. - _defaultParams.createWithTimestamps.broker = defaults.brokerNull(); _defaultParams.unlockAmounts = defaults.unlockAmountsZero(); - _defaultParams.createWithTimestamps.totalAmount = depositAmount; + _defaultParams.createWithTimestamps.depositAmount = depositAmount; uint256 streamId = createDefaultStream(); // Simulate the passage of time. @@ -51,7 +50,7 @@ contract WithdrawableAmountOf_Lockup_Linear_Integration_Fuzz_Test is Lockup_Line // Run the test. uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(streamId); - uint128 expectedWithdrawableAmount = calculateLockupLinearStreamedAmount( + uint128 expectedWithdrawableAmount = calculateStreamedAmountLL( defaults.START_TIME(), defaults.CLIFF_TIME(), defaults.END_TIME(), @@ -88,9 +87,9 @@ contract WithdrawableAmountOf_Lockup_Linear_Integration_Fuzz_Test is Lockup_Line // Mint enough tokens to the Sender. deal({ token: address(dai), to: users.sender, give: depositAmount }); - // Create the stream. The broker fee is disabled so that it doesn't interfere with the calculations. - Lockup.CreateWithTimestamps memory params = defaults.createWithTimestampsBrokerNull(); - params.totalAmount = depositAmount; + // Create the stream. + Lockup.CreateWithTimestamps memory params = defaults.createWithTimestamps(); + params.depositAmount = depositAmount; LockupLinear.UnlockAmounts memory unlockAmounts = defaults.unlockAmountsZero(); uint256 streamId = lockup.createWithTimestampsLL(params, unlockAmounts, defaults.CLIFF_TIME()); @@ -100,13 +99,13 @@ contract WithdrawableAmountOf_Lockup_Linear_Integration_Fuzz_Test is Lockup_Line vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); // Bound the withdraw amount. - uint128 streamedAmount = calculateLockupLinearStreamedAmount( + uint128 streamedAmount = calculateStreamedAmountLL( defaults.START_TIME(), defaults.CLIFF_TIME(), defaults.END_TIME(), depositAmount, unlockAmounts ); withdrawAmount = boundUint128(withdrawAmount, 1, streamedAmount); // Make the withdrawal. - lockup.withdraw({ streamId: streamId, to: users.recipient, amount: withdrawAmount }); + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ streamId: streamId, to: users.recipient, amount: withdrawAmount }); // Run the test. uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(streamId); diff --git a/tests/integration/fuzz/lockup-tranched/LockupTranched.t.sol b/tests/integration/fuzz/lockup-tranched/LockupTranched.t.sol index db48a6b83..6eb15d0f7 100644 --- a/tests/integration/fuzz/lockup-tranched/LockupTranched.t.sol +++ b/tests/integration/fuzz/lockup-tranched/LockupTranched.t.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { Lockup } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; import { Integration_Test } from "./../../Integration.t.sol"; -import { Cancel_Integration_Fuzz_Test } from "./../lockup-base/cancel.t.sol"; -import { RefundableAmountOf_Integration_Fuzz_Test } from "./../lockup-base/refundableAmountOf.t.sol"; +import { Cancel_Integration_Fuzz_Test } from "./../lockup/cancel.t.sol"; +import { RefundableAmountOf_Integration_Fuzz_Test } from "./../lockup/refundableAmountOf.t.sol"; abstract contract Lockup_Tranched_Integration_Fuzz_Test is Integration_Test { function setUp() public virtual override { diff --git a/tests/integration/fuzz/lockup-tranched/createWithDurationsLT.t.sol b/tests/integration/fuzz/lockup-tranched/createWithDurationsLT.t.sol index eb7bb771d..9e4a7d710 100644 --- a/tests/integration/fuzz/lockup-tranched/createWithDurationsLT.t.sol +++ b/tests/integration/fuzz/lockup-tranched/createWithDurationsLT.t.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; -import { Lockup, LockupTranched } from "src/types/DataTypes.sol"; +import { ISablierLockupTranched } from "src/interfaces/ISablierLockupTranched.sol"; +import { Lockup } from "src/types/Lockup.sol"; +import { LockupTranched } from "src/types/LockupTranched.sol"; import { Lockup_Tranched_Integration_Fuzz_Test } from "./LockupTranched.t.sol"; @@ -11,21 +12,18 @@ contract CreateWithDurationsLT_Integration_Fuzz_Test is Lockup_Tranched_Integrat uint256 actualNextStreamId; address actualNFTOwner; Lockup.Status actualStatus; - Lockup.CreateAmounts createAmounts; uint256 expectedNextStreamId; address expectedNFTOwner; Lockup.Status expectedStatus; - address funder; bool isCancelable; bool isSettled; LockupTranched.Tranche[] tranchesWithTimestamps; - uint128 totalAmount; + uint128 depositAmount; } function testFuzz_CreateWithDurationsLT(LockupTranched.TrancheWithDuration[] memory tranches) external whenNoDelegateCall - whenTrancheCountNotExceedMaxValue whenTimestampsCalculationNotOverflow { vm.assume(tranches.length != 0); @@ -34,23 +32,16 @@ contract CreateWithDurationsLT_Integration_Fuzz_Test is Lockup_Tranched_Integrat Vars memory vars; fuzzTrancheDurations(tranches); - // Fuzz the tranche amounts and calculate the total and create amounts (deposit and broker fee). - (vars.totalAmount, vars.createAmounts) = fuzzTranchedStreamAmounts(tranches, defaults.BROKER_FEE()); + // Fuzz the tranche amounts and calculate the deposit amount. + vars.depositAmount = fuzzTranchedStreamAmounts(tranches); - // Make the Sender the stream's funder (recall that the Sender is the default caller). - vars.funder = users.sender; uint256 expectedStreamId = lockup.nextStreamId(); - // Mint enough tokens to the fuzzed funder. - deal({ token: address(dai), to: vars.funder, give: vars.totalAmount }); + // Mint enough tokens to the sender. + deal({ token: address(dai), to: users.sender, give: vars.depositAmount }); - // Expect the tokens to be transferred from the funder to {SablierLockup}. - expectCallToTransferFrom({ from: vars.funder, to: address(lockup), value: vars.createAmounts.deposit }); - - // Expect the broker fee to be paid to the broker, if not zero. - if (vars.createAmounts.brokerFee > 0) { - expectCallToTransferFrom({ from: vars.funder, to: users.broker, value: vars.createAmounts.brokerFee }); - } + // Expect the tokens to be transferred from the sender to {SablierLockup}. + expectCallToTransferFrom({ from: users.sender, to: address(lockup), value: vars.depositAmount }); // Create the timestamps struct. vars.tranchesWithTimestamps = getTranchesWithTimestamps(tranches); @@ -61,24 +52,24 @@ contract CreateWithDurationsLT_Integration_Fuzz_Test is Lockup_Tranched_Integrat // Expect the relevant event to be emitted. vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockup.CreateLockupTranchedStream({ + emit ISablierLockupTranched.CreateLockupTranchedStream({ streamId: expectedStreamId, - commonParams: defaults.lockupCreateEvent(vars.createAmounts, timestamps), + commonParams: defaults.lockupCreateEvent(vars.depositAmount, timestamps), tranches: vars.tranchesWithTimestamps }); // Create the stream. - _defaultParams.createWithDurations.totalAmount = vars.totalAmount; + _defaultParams.createWithDurations.depositAmount = vars.depositAmount; _defaultParams.createWithDurations.transferable = true; uint256 streamId = lockup.createWithDurationsLT(_defaultParams.createWithDurations, tranches); - // Check if the stream is settled. It is possible for a Lockup Tranched stream to settle at the time of creation - // because some tranche amounts can be zero. + // Check if the stream is settled. It is possible for a stream to settle at the time of creation because some + // tranche amounts can be zero. vars.isSettled = lockup.refundableAmountOf(streamId) == 0; vars.isCancelable = vars.isSettled ? false : true; // It should create the stream. - assertEq(lockup.getDepositedAmount(streamId), vars.createAmounts.deposit, "depositedAmount"); + assertEq(lockup.getDepositedAmount(streamId), vars.depositAmount, "depositedAmount"); assertEq(lockup.getEndTime(streamId), timestamps.end, "endTime"); assertEq(lockup.isCancelable(streamId), vars.isCancelable, "isCancelable"); assertFalse(lockup.isDepleted(streamId), "isDepleted"); diff --git a/tests/integration/fuzz/lockup-tranched/createWithTimestampsLT.t.sol b/tests/integration/fuzz/lockup-tranched/createWithTimestampsLT.t.sol index c7e6ba3e0..678dfbf2b 100644 --- a/tests/integration/fuzz/lockup-tranched/createWithTimestampsLT.t.sol +++ b/tests/integration/fuzz/lockup-tranched/createWithTimestampsLT.t.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { MAX_UD60x18, ud } from "@prb/math/src/UD60x18.sol"; import { stdError } from "forge-std/src/StdError.sol"; -import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; +import { ISablierLockupTranched } from "src/interfaces/ISablierLockupTranched.sol"; import { Errors } from "src/libraries/Errors.sol"; -import { Broker, Lockup, LockupTranched } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; +import { LockupTranched } from "src/types/LockupTranched.sol"; import { Lockup_Tranched_Integration_Fuzz_Test } from "./LockupTranched.t.sol"; @@ -27,22 +27,6 @@ contract CreateWithTimestampsLT_Integration_Fuzz_Test is Lockup_Tranched_Integra createDefaultStream(); } - function testFuzz_RevertWhen_TrancheCountTooHigh(uint256 trancheCount) - external - whenNoDelegateCall - whenShapeNotExceed32Bytes - whenSenderNotZeroAddress - whenRecipientNotZeroAddress - whenDepositAmountNotZero - whenTrancheCountNotZero - { - uint256 defaultMax = defaults.MAX_COUNT(); - trancheCount = _bound(trancheCount, defaultMax + 1, defaultMax * 10); - LockupTranched.Tranche[] memory tranches = new LockupTranched.Tranche[](trancheCount); - vm.expectRevert(abi.encodeWithSelector(Errors.SablierHelpers_TrancheCountTooHigh.selector, trancheCount)); - lockup.createWithTimestampsLT(_defaultParams.createWithTimestamps, tranches); - } - function testFuzz_RevertWhen_TrancheAmountsSumOverflows( uint128 amount0, uint128 amount1 @@ -54,7 +38,6 @@ contract CreateWithTimestampsLT_Integration_Fuzz_Test is Lockup_Tranched_Integra whenRecipientNotZeroAddress whenDepositAmountNotZero whenTrancheCountNotZero - whenTrancheCountNotExceedMaxValue { amount0 = boundUint128(amount0, MAX_UINT128 / 2 + 1, MAX_UINT128); amount1 = boundUint128(amount0, MAX_UINT128 / 2 + 1, MAX_UINT128); @@ -73,7 +56,6 @@ contract CreateWithTimestampsLT_Integration_Fuzz_Test is Lockup_Tranched_Integra whenRecipientNotZeroAddress whenDepositAmountNotZero whenTrancheCountNotZero - whenTrancheCountNotExceedMaxValue whenTrancheAmountsSumNotOverflow { firstTimestamp = boundUint40(firstTimestamp, 0, defaults.START_TIME()); @@ -101,22 +83,21 @@ contract CreateWithTimestampsLT_Integration_Fuzz_Test is Lockup_Tranched_Integra whenRecipientNotZeroAddress whenDepositAmountNotZero whenTrancheCountNotZero - whenTrancheCountNotExceedMaxValue whenTrancheAmountsSumNotOverflow whenStartTimeLessThanFirstTimestamp whenTrancheTimestampsAreOrdered { - depositDiff = boundUint128(depositDiff, 100, defaults.TOTAL_AMOUNT()); + depositDiff = boundUint128(depositDiff, 100, defaults.DEPOSIT_AMOUNT()); - resetPrank({ msgSender: users.sender }); + setMsgSender(users.sender); // Adjust the default deposit amount. uint128 defaultDepositAmount = defaults.DEPOSIT_AMOUNT(); uint128 depositAmount = defaultDepositAmount + depositDiff; // Prepare the params. - Lockup.CreateWithTimestamps memory params = defaults.createWithTimestampsBrokerNull(); - params.totalAmount = depositAmount; + Lockup.CreateWithTimestamps memory params = defaults.createWithTimestamps(); + params.depositAmount = depositAmount; LockupTranched.Tranche[] memory tranches = defaults.tranches(); // Expect the relevant error to be thrown. @@ -131,52 +112,26 @@ contract CreateWithTimestampsLT_Integration_Fuzz_Test is Lockup_Tranched_Integra lockup.createWithTimestampsLT(params, tranches); } - function testFuzz_RevertWhen_BrokerFeeTooHigh(Broker memory broker) - external - whenNoDelegateCall - whenShapeNotExceed32Bytes - whenSenderNotZeroAddress - whenRecipientNotZeroAddress - whenDepositAmountNotZero - whenTrancheCountNotZero - whenTrancheCountNotExceedMaxValue - whenTrancheAmountsSumNotOverflow - whenStartTimeLessThanFirstTimestamp - whenTrancheTimestampsAreOrdered - whenDepositAmountNotEqualTrancheAmountsSum - { - vm.assume(broker.account != address(0)); - broker.fee = _bound(broker.fee, MAX_BROKER_FEE + ud(1), MAX_UD60x18); - vm.expectRevert( - abi.encodeWithSelector(Errors.SablierHelpers_BrokerFeeTooHigh.selector, broker.fee, MAX_BROKER_FEE) - ); - _defaultParams.createWithTimestamps.broker = broker; - lockup.createWithTimestampsLT(_defaultParams.createWithTimestamps, _defaultParams.tranches); - } - struct Vars { uint256 actualNextStreamId; address actualNFTOwner; Lockup.Status actualStatus; - Lockup.CreateAmounts createAmounts; uint256 expectedNextStreamId; address expectedNFTOwner; Lockup.Status expectedStatus; bool isCancelable; bool isSettled; - uint128 totalAmount; } /// @dev Given enough fuzz runs, all of the following scenarios will be fuzzed: /// - /// - All possible permutations for the funder, sender, recipient, and broker + /// - All possible permutations for the funder, sender and recipient /// - Multiple values for the tranche amounts, exponents, and timestamps /// - Cancelable and not cancelable /// - Start time in the past /// - Start time in the present /// - Start time in the future /// - Start time equal and not equal to the first tranche timestamp - /// - Multiple values for the broker fee, including zero function testFuzz_CreateWithTimestampsLT( address funder, Lockup.CreateWithTimestamps memory params, @@ -190,88 +145,66 @@ contract CreateWithTimestampsLT_Integration_Fuzz_Test is Lockup_Tranched_Integra whenDepositAmountNotZero whenStartTimeNotZero whenTrancheCountNotZero - whenTrancheCountNotExceedMaxValue whenTrancheAmountsSumNotOverflow whenStartTimeLessThanFirstTimestamp whenTrancheTimestampsAreOrdered whenDepositAmountNotEqualTrancheAmountsSum - whenBrokerFeeNotExceedMaxValue whenTokenContract whenTokenERC20 { - vm.assume( - funder != address(0) && params.sender != address(0) && params.recipient != address(0) - && params.broker.account != address(0) - ); + vm.assume(funder != address(0) && params.sender != address(0) && params.recipient != address(0)); vm.assume(tranches.length != 0); - params.broker.fee = _bound(params.broker.fee, 0, MAX_BROKER_FEE); params.token = dai; params.timestamps.start = boundUint40(params.timestamps.start, 1, defaults.START_TIME()); params.transferable = true; - // If shape exceeds 32 bytes, use the default value. + // If the shape exceeds 32 bytes, use the default value. if (bytes(params.shape).length > 32) params.shape = defaults.SHAPE(); + uint256 previousAggregateAmount = lockup.aggregateAmount(dai); + // Fuzz the tranche timestamps. fuzzTrancheTimestamps(tranches, params.timestamps.start); params.timestamps.end = tranches[tranches.length - 1].timestamp; - // Fuzz the tranche amounts and calculate the total and create amounts (deposit and broker fee). - Vars memory vars; - (vars.totalAmount, vars.createAmounts) = - fuzzTranchedStreamAmounts({ upperBound: MAX_UINT128, tranches: tranches, brokerFee: params.broker.fee }); - - params.totalAmount = vars.totalAmount; + params.depositAmount = fuzzTranchedStreamAmounts({ upperBound: MAX_UINT128, tranches: tranches }); // Make the fuzzed funder the caller in the rest of this test. - resetPrank(funder); + setMsgSender(funder); // Mint enough tokens to the fuzzed funder. - deal({ token: address(dai), to: funder, give: vars.totalAmount }); + deal({ token: address(dai), to: funder, give: params.depositAmount }); // Approve {SablierLockup} to transfer the tokens from the fuzzed funder. dai.approve({ spender: address(lockup), value: MAX_UINT256 }); // Expect the tokens to be transferred from the funder to {SablierLockup}. - expectCallToTransferFrom({ from: funder, to: address(lockup), value: vars.createAmounts.deposit }); - - // Expect the broker fee to be paid to the broker, if not zero. - if (vars.createAmounts.brokerFee > 0) { - expectCallToTransferFrom({ from: funder, to: params.broker.account, value: vars.createAmounts.brokerFee }); - } + expectCallToTransferFrom({ from: funder, to: address(lockup), value: params.depositAmount }); uint256 expectedStreamId = lockup.nextStreamId(); // Expect the relevant event to be emitted. vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockup.CreateLockupTranchedStream({ + emit ISablierLockupTranched.CreateLockupTranchedStream({ streamId: expectedStreamId, - commonParams: Lockup.CreateEventCommon({ - funder: funder, - sender: params.sender, - recipient: params.recipient, - amounts: vars.createAmounts, - token: dai, - cancelable: params.cancelable, - transferable: params.transferable, - timestamps: params.timestamps, - shape: params.shape, - broker: params.broker.account - }), + commonParams: defaults.lockupCreateEvent(funder, params, dai), tranches: tranches }); // Create the stream. uint256 streamId = lockup.createWithTimestampsLT(params, tranches); - // Check if the stream is settled. It is possible for a Lockup Tranched stream to settle at the time of creation - // because some tranche amounts can be zero. + // Fuzz the tranche amounts and calculate the deposit amount. + Vars memory vars; + + // Check if the stream is settled. It is possible for a stream to settle at the time of creation because some + // tranche amounts can be zero. vars.isSettled = (lockup.getDepositedAmount(streamId) - lockup.streamedAmountOf(streamId)) == 0; vars.isCancelable = vars.isSettled ? false : params.cancelable; // It should create the stream. - assertEq(lockup.getDepositedAmount(streamId), vars.createAmounts.deposit, "depositedAmount"); + assertEq(lockup.getDepositedAmount(streamId), params.depositAmount, "depositedAmount"); assertEq(lockup.getEndTime(streamId), params.timestamps.end, "endTime"); assertEq(lockup.isCancelable(streamId), vars.isCancelable, "isCancelable"); assertFalse(lockup.isDepleted(streamId), "isDepleted"); @@ -305,5 +238,8 @@ contract CreateWithTimestampsLT_Integration_Fuzz_Test is Lockup_Tranched_Integra vars.actualNFTOwner = lockup.ownerOf({ tokenId: streamId }); vars.expectedNFTOwner = params.recipient; assertEq(vars.actualNFTOwner, vars.expectedNFTOwner, "NFT owner"); + + // Assert that the aggregate amount has been updated. + assertEq(lockup.aggregateAmount(dai), previousAggregateAmount + params.depositAmount); } } diff --git a/tests/integration/fuzz/lockup-tranched/streamedAmountOf.t.sol b/tests/integration/fuzz/lockup-tranched/streamedAmountOf.t.sol index c2834adad..489e0321a 100644 --- a/tests/integration/fuzz/lockup-tranched/streamedAmountOf.t.sol +++ b/tests/integration/fuzz/lockup-tranched/streamedAmountOf.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { ZERO } from "@prb/math/src/UD60x18.sol"; -import { Broker, Lockup, LockupTranched } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; +import { LockupTranched } from "src/types/LockupTranched.sol"; import { Lockup_Tranched_Integration_Fuzz_Test } from "./LockupTranched.t.sol"; @@ -32,8 +32,7 @@ contract StreamedAmountOf_Lockup_Tranched_Integration_Fuzz_Test is Lockup_Tranch fuzzTrancheTimestamps(tranches, defaults.START_TIME()); // Fuzz the tranche amounts. - (uint128 totalAmount,) = - fuzzTranchedStreamAmounts({ upperBound: MAX_UINT128, tranches: tranches, brokerFee: ZERO }); + uint128 depositAmount = fuzzTranchedStreamAmounts({ upperBound: MAX_UINT128, tranches: tranches }); // Bound the time jump. uint40 firstTrancheDuration = tranches[1].timestamp - tranches[0].timestamp; @@ -41,12 +40,11 @@ contract StreamedAmountOf_Lockup_Tranched_Integration_Fuzz_Test is Lockup_Tranch timeJump = boundUint40(timeJump, firstTrancheDuration, totalDuration + 100 seconds); // Mint enough tokens to the Sender. - deal({ token: address(dai), to: users.sender, give: totalAmount }); + deal({ token: address(dai), to: users.sender, give: depositAmount }); // Create the stream with the fuzzed tranches. Lockup.CreateWithTimestamps memory params = defaults.createWithTimestamps(); - params.broker = Broker({ account: address(0), fee: ZERO }); - params.totalAmount = totalAmount; + params.depositAmount = depositAmount; params.timestamps.end = tranches[tranches.length - 1].timestamp; uint256 streamId = lockup.createWithTimestampsLT(params, tranches); @@ -55,7 +53,7 @@ contract StreamedAmountOf_Lockup_Tranched_Integration_Fuzz_Test is Lockup_Tranch // Run the test. uint128 actualStreamedAmount = lockup.streamedAmountOf(streamId); - uint128 expectedStreamedAmount = calculateLockupTranchedStreamedAmount(tranches, totalAmount); + uint128 expectedStreamedAmount = calculateStreamedAmountLT(tranches, depositAmount); assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } @@ -78,8 +76,7 @@ contract StreamedAmountOf_Lockup_Tranched_Integration_Fuzz_Test is Lockup_Tranch fuzzTrancheTimestamps(tranches, defaults.START_TIME()); // Fuzz the tranche amounts. - (uint128 totalAmount,) = - fuzzTranchedStreamAmounts({ upperBound: MAX_UINT128, tranches: tranches, brokerFee: ZERO }); + uint128 depositAmount = fuzzTranchedStreamAmounts({ upperBound: MAX_UINT128, tranches: tranches }); // Bound the time warps. uint40 firstTrancheDuration = tranches[1].timestamp - tranches[0].timestamp; @@ -88,12 +85,12 @@ contract StreamedAmountOf_Lockup_Tranched_Integration_Fuzz_Test is Lockup_Tranch timeWarp1 = boundUint40(timeWarp1, timeWarp0, totalDuration); // Mint enough tokens to the Sender. - deal({ token: address(dai), to: users.sender, give: totalAmount }); + deal({ token: address(dai), to: users.sender, give: depositAmount }); // Create the stream with the fuzzed tranches. Lockup.CreateWithTimestamps memory params = defaults.createWithTimestamps(); - params.broker = Broker({ account: address(0), fee: ZERO }); - params.totalAmount = totalAmount; + + params.depositAmount = depositAmount; params.timestamps.end = tranches[tranches.length - 1].timestamp; uint256 streamId = lockup.createWithTimestampsLT(params, tranches); diff --git a/tests/integration/fuzz/lockup-tranched/withdraw.t.sol b/tests/integration/fuzz/lockup-tranched/withdraw.t.sol index ebd64c010..98f396778 100644 --- a/tests/integration/fuzz/lockup-tranched/withdraw.t.sol +++ b/tests/integration/fuzz/lockup-tranched/withdraw.t.sol @@ -2,11 +2,12 @@ pragma solidity >=0.8.22 <0.9.0; import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; -import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; -import { Lockup, LockupTranched } from "src/types/DataTypes.sol"; +import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; +import { Lockup } from "src/types/Lockup.sol"; +import { LockupTranched } from "src/types/LockupTranched.sol"; import { Integration_Test } from "../../Integration.t.sol"; -import { Withdraw_Integration_Fuzz_Test } from "./../lockup-base/withdraw.t.sol"; +import { Withdraw_Integration_Fuzz_Test } from "./../lockup/withdraw.t.sol"; import { Lockup_Tranched_Integration_Fuzz_Test } from "./LockupTranched.t.sol"; /// @dev This contract complements the tests in {Withdraw_Integration_Fuzz_Test} by testing the withdraw function /// against streams created with fuzzed tranches. @@ -28,14 +29,12 @@ contract Withdraw_Lockup_Tranched_Integration_Fuzz_Test is struct Vars { Lockup.Status actualStatus; uint256 actualWithdrawnAmount; - Lockup.CreateAmounts createAmounts; Lockup.Status expectedStatus; uint256 expectedWithdrawnAmount; bool isDepleted; bool isSettled; - address funder; uint256 streamId; - uint128 totalAmount; + uint128 depositAmount; uint40 totalDuration; uint128 withdrawAmount; uint128 withdrawableAmount; @@ -52,29 +51,27 @@ contract Withdraw_Lockup_Tranched_Integration_Fuzz_Test is vm.assume(params.tranches.length != 0); vm.assume(params.to != address(0)); - // Make the Sender the stream's funder (recall that the Sender is the default caller). Vars memory vars; - vars.funder = users.sender; // Fuzz the tranche timestamps. fuzzTrancheTimestamps(params.tranches, defaults.START_TIME()); // Fuzz the tranche amounts. - (vars.totalAmount, vars.createAmounts) = fuzzTranchedStreamAmounts(params.tranches, defaults.BROKER_FEE()); + vars.depositAmount = fuzzTranchedStreamAmounts(params.tranches); // Bound the time jump. vars.totalDuration = params.tranches[params.tranches.length - 1].timestamp - defaults.START_TIME(); params.timeJump = _bound(params.timeJump, 1 seconds, vars.totalDuration + 100 seconds); - // Mint enough tokens to the funder. - deal({ token: address(dai), to: vars.funder, give: vars.totalAmount }); + // Mint enough tokens to the sender. + deal({ token: address(dai), to: users.sender, give: vars.depositAmount }); // Make the Sender the caller. - resetPrank({ msgSender: users.sender }); + setMsgSender(users.sender); // Create the stream with the fuzzed tranches. Lockup.CreateWithTimestamps memory createParams = defaults.createWithTimestamps(); - createParams.totalAmount = vars.totalAmount; + createParams.depositAmount = vars.depositAmount; createParams.timestamps.end = params.tranches[params.tranches.length - 1].timestamp; vars.streamId = lockup.createWithTimestampsLT(createParams, params.tranches); @@ -93,15 +90,17 @@ contract Withdraw_Lockup_Tranched_Integration_Fuzz_Test is // Bound the withdraw amount. vars.withdrawAmount = boundUint128(vars.withdrawAmount, 1, vars.withdrawableAmount); + uint256 previousAggregateAmount = lockup.aggregateAmount(dai); + // Make the Recipient the caller. - resetPrank({ msgSender: users.recipient }); + setMsgSender(users.recipient); // Expect the tokens to be transferred to the fuzzed `to` address. expectCallToTransfer({ to: params.to, value: vars.withdrawAmount }); // Expect the relevant events to be emitted. vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.WithdrawFromLockupStream({ + emit ISablierLockup.WithdrawFromLockupStream({ streamId: vars.streamId, to: params.to, token: dai, @@ -111,11 +110,15 @@ contract Withdraw_Lockup_Tranched_Integration_Fuzz_Test is emit IERC4906.MetadataUpdate({ _tokenId: vars.streamId }); // Make the withdrawal. - lockup.withdraw({ streamId: vars.streamId, to: params.to, amount: vars.withdrawAmount }); + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: vars.streamId, + to: params.to, + amount: vars.withdrawAmount + }); // Check if the stream is depleted or settled. It is possible for the stream to be just settled // and not depleted because the withdraw amount is fuzzed. - vars.isDepleted = vars.withdrawAmount == vars.createAmounts.deposit; + vars.isDepleted = vars.withdrawAmount == vars.depositAmount; vars.isSettled = lockup.refundableAmountOf(vars.streamId) == 0; // Assert that the stream's status is correct. @@ -133,5 +136,8 @@ contract Withdraw_Lockup_Tranched_Integration_Fuzz_Test is vars.actualWithdrawnAmount = lockup.getWithdrawnAmount(vars.streamId); vars.expectedWithdrawnAmount = vars.withdrawAmount; assertEq(vars.actualWithdrawnAmount, vars.expectedWithdrawnAmount, "withdrawnAmount"); + + // Assert that the aggregate amount has been updated. + assertEq(lockup.aggregateAmount(dai), previousAggregateAmount - vars.actualWithdrawnAmount, "aggregateAmount"); } } diff --git a/tests/integration/fuzz/lockup-tranched/withdrawableAmountOf.t.sol b/tests/integration/fuzz/lockup-tranched/withdrawableAmountOf.t.sol index 4f417b235..577238b14 100644 --- a/tests/integration/fuzz/lockup-tranched/withdrawableAmountOf.t.sol +++ b/tests/integration/fuzz/lockup-tranched/withdrawableAmountOf.t.sol @@ -1,9 +1,6 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { ZERO } from "@prb/math/src/UD60x18.sol"; -import { Broker, Lockup } from "src/types/DataTypes.sol"; - import { Lockup_Tranched_Integration_Fuzz_Test } from "./LockupTranched.t.sol"; contract WithdrawableAmountOf_Lockup_Tranched_Integration_Fuzz_Test is Lockup_Tranched_Integration_Fuzz_Test { @@ -17,12 +14,8 @@ contract WithdrawableAmountOf_Lockup_Tranched_Integration_Fuzz_Test is Lockup_Tr function testFuzz_WithdrawableAmountOf_NoPreviousWithdrawals(uint40 timeJump) external givenStartTimeInPast { timeJump = boundUint40(timeJump, defaults.WARP_26_PERCENT_DURATION(), defaults.TOTAL_DURATION() * 2); - // Create the stream with a custom total amount. The broker fee is disabled so that it doesn't interfere with - // the calculations. - Lockup.CreateWithTimestamps memory params = defaults.createWithTimestamps(); - params.broker = Broker({ account: address(0), fee: ZERO }); - params.totalAmount = defaults.DEPOSIT_AMOUNT(); - uint256 streamId = lockup.createWithTimestampsLT(params, defaults.tranches()); + // Create the stream. + uint256 streamId = createDefaultStream(); // Simulate the passage of time. uint40 blockTimestamp = defaults.START_TIME() + timeJump; @@ -30,8 +23,7 @@ contract WithdrawableAmountOf_Lockup_Tranched_Integration_Fuzz_Test is Lockup_Tr // Run the test. uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(streamId); - uint128 expectedWithdrawableAmount = - calculateLockupTranchedStreamedAmount(defaults.tranches(), defaults.DEPOSIT_AMOUNT()); + uint128 expectedWithdrawableAmount = calculateStreamedAmountLT(defaults.tranches(), defaults.DEPOSIT_AMOUNT()); assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); } @@ -53,12 +45,8 @@ contract WithdrawableAmountOf_Lockup_Tranched_Integration_Fuzz_Test is Lockup_Tr givenStartTimeInPast givenPreviousWithdrawal { - // Create the stream with a custom total amount. The broker fee is disabled so that it doesn't interfere with - // the calculations. - Lockup.CreateWithTimestamps memory params = defaults.createWithTimestamps(); - params.broker = Broker({ account: address(0), fee: ZERO }); - params.totalAmount = defaults.DEPOSIT_AMOUNT(); - uint256 streamId = lockup.createWithTimestampsLT(params, defaults.tranches()); + // Create the stream. + uint256 streamId = createDefaultStream(); timeJump = boundUint40(timeJump, defaults.WARP_26_PERCENT_DURATION(), defaults.TOTAL_DURATION() * 2); @@ -66,11 +54,11 @@ contract WithdrawableAmountOf_Lockup_Tranched_Integration_Fuzz_Test is Lockup_Tr vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); // Bound the withdraw amount. - uint128 streamedAmount = calculateLockupTranchedStreamedAmount(defaults.tranches(), defaults.DEPOSIT_AMOUNT()); + uint128 streamedAmount = calculateStreamedAmountLT(defaults.tranches(), defaults.DEPOSIT_AMOUNT()); withdrawAmount = boundUint128(withdrawAmount, 1, streamedAmount); // Make the withdrawal. - lockup.withdraw({ streamId: streamId, to: users.recipient, amount: withdrawAmount }); + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ streamId: streamId, to: users.recipient, amount: withdrawAmount }); // Run the test. uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(streamId); diff --git a/tests/integration/fuzz/lockup-base/cancel.t.sol b/tests/integration/fuzz/lockup/cancel.t.sol similarity index 59% rename from tests/integration/fuzz/lockup-base/cancel.t.sol rename to tests/integration/fuzz/lockup/cancel.t.sol index be189da14..e5d517cfb 100644 --- a/tests/integration/fuzz/lockup-base/cancel.t.sol +++ b/tests/integration/fuzz/lockup/cancel.t.sol @@ -3,9 +3,9 @@ pragma solidity >=0.8.22 <0.9.0; import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; -import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; +import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; import { ISablierLockupRecipient } from "src/interfaces/ISablierLockupRecipient.sol"; -import { Lockup } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; import { Integration_Test } from "../../Integration.t.sol"; @@ -20,20 +20,25 @@ abstract contract Cancel_Integration_Fuzz_Test is Integration_Test { { timeJump = _bound(timeJump, 1 seconds, 100 weeks); - // Warp to the past. - vm.warp({ newTimestamp: getBlockTimestamp() - timeJump }); + // Rewind time to the past. + rewind(timeJump); + + uint256 previousAggregateAmount = lockup.aggregateAmount(dai); // Cancel the stream. - lockup.cancel(defaultStreamId); + uint128 refundedAmount = lockup.cancel(ids.defaultStream); // Assert that the stream's status is "DEPLETED". - Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); + Lockup.Status actualStatus = lockup.statusOf(ids.defaultStream); Lockup.Status expectedStatus = Lockup.Status.DEPLETED; assertEq(actualStatus, expectedStatus); // Assert that the stream is not cancelable anymore. - bool isCancelable = lockup.isCancelable(defaultStreamId); + bool isCancelable = lockup.isCancelable(ids.defaultStream); assertFalse(isCancelable, "isCancelable"); + + // Assert that the aggregate amount has been updated. + assertEq(lockup.aggregateAmount(dai), previousAggregateAmount - refundedAmount, "aggregateAmount"); } /// @dev Given enough fuzz runs, all of the following scenarios will be fuzzed: @@ -62,50 +67,62 @@ abstract contract Cancel_Integration_Fuzz_Test is Integration_Test { vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); // Bound the withdraw amount. - uint128 streamedAmount = lockup.streamedAmountOf(recipientGoodStreamId); + uint128 streamedAmount = lockup.streamedAmountOf(ids.recipientGoodStream); withdrawAmount = boundUint128(withdrawAmount, 0, streamedAmount - 1); // Make the withdrawal only if the amount is greater than zero. if (withdrawAmount > 0) { - lockup.withdraw({ streamId: recipientGoodStreamId, to: address(recipientGood), amount: withdrawAmount }); + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: ids.recipientGoodStream, + to: address(recipientGood), + amount: withdrawAmount + }); } + uint256 previousAggregateAmount = lockup.aggregateAmount(dai); + // Expect the tokens to be refunded to the Sender. - uint128 senderAmount = lockup.refundableAmountOf(recipientGoodStreamId); + uint128 senderAmount = lockup.refundableAmountOf(ids.recipientGoodStream); expectCallToTransfer({ to: users.sender, value: senderAmount }); // Expect the recipient to be called. - uint128 recipientAmount = lockup.withdrawableAmountOf(recipientGoodStreamId); + uint128 recipientAmount = lockup.withdrawableAmountOf(ids.recipientGoodStream); vm.expectCall( address(recipientGood), abi.encodeCall( ISablierLockupRecipient.onSablierLockupCancel, - (recipientGoodStreamId, users.sender, senderAmount, recipientAmount) + (ids.recipientGoodStream, users.sender, senderAmount, recipientAmount) ) ); // Expect the relevant events to be emitted. vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.CancelLockupStream( - recipientGoodStreamId, users.sender, address(recipientGood), dai, senderAmount, recipientAmount + emit ISablierLockup.CancelLockupStream( + ids.recipientGoodStream, users.sender, address(recipientGood), dai, senderAmount, recipientAmount ); vm.expectEmit({ emitter: address(lockup) }); - emit IERC4906.MetadataUpdate({ _tokenId: recipientGoodStreamId }); + emit IERC4906.MetadataUpdate({ _tokenId: ids.recipientGoodStream }); // Cancel the stream. - lockup.cancel(recipientGoodStreamId); + uint128 refundedAmount = lockup.cancel(ids.recipientGoodStream); + + // Assert that the amount refunded matches the expected value. + assertEq(refundedAmount, senderAmount, "refundedAmount"); // Assert that the stream's status is "CANCELED". - Lockup.Status actualStatus = lockup.statusOf(recipientGoodStreamId); + Lockup.Status actualStatus = lockup.statusOf(ids.recipientGoodStream); Lockup.Status expectedStatus = Lockup.Status.CANCELED; assertEq(actualStatus, expectedStatus); // Assert that the stream is not cancelable anymore. - bool isCancelable = lockup.isCancelable(recipientGoodStreamId); + bool isCancelable = lockup.isCancelable(ids.recipientGoodStream); assertFalse(isCancelable, "isCancelable"); + // Assert that the aggregate amount has been updated. + assertEq(lockup.aggregateAmount(dai), previousAggregateAmount - refundedAmount, "aggregateAmount"); + // Assert that the not burned NFT. - address actualNFTOwner = lockup.ownerOf({ tokenId: recipientGoodStreamId }); + address actualNFTOwner = lockup.ownerOf({ tokenId: ids.recipientGoodStream }); address expectedNFTOwner = address(recipientGood); assertEq(actualNFTOwner, expectedNFTOwner, "NFT owner"); } diff --git a/tests/integration/fuzz/lockup-base/refundableAmountOf.t.sol b/tests/integration/fuzz/lockup/refundableAmountOf.t.sol similarity index 90% rename from tests/integration/fuzz/lockup-base/refundableAmountOf.t.sol rename to tests/integration/fuzz/lockup/refundableAmountOf.t.sol index 3d3eb0797..0067e735c 100644 --- a/tests/integration/fuzz/lockup-base/refundableAmountOf.t.sol +++ b/tests/integration/fuzz/lockup/refundableAmountOf.t.sol @@ -15,10 +15,10 @@ abstract contract RefundableAmountOf_Integration_Fuzz_Test is Integration_Test { vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); // Get the streamed amount. - uint128 streamedAmount = lockup.streamedAmountOf(defaultStreamId); + uint128 streamedAmount = lockup.streamedAmountOf(ids.defaultStream); // Run the test. - uint256 actualRefundableAmount = lockup.refundableAmountOf(defaultStreamId); + uint256 actualRefundableAmount = lockup.refundableAmountOf(ids.defaultStream); uint256 expectedRefundableAmount = defaults.DEPOSIT_AMOUNT() - streamedAmount; assertEq(actualRefundableAmount, expectedRefundableAmount, "refundableAmount"); } diff --git a/tests/integration/fuzz/lockup-base/withdraw.t.sol b/tests/integration/fuzz/lockup/withdraw.t.sol similarity index 72% rename from tests/integration/fuzz/lockup-base/withdraw.t.sol rename to tests/integration/fuzz/lockup/withdraw.t.sol index 6e56752ce..170364141 100644 --- a/tests/integration/fuzz/lockup-base/withdraw.t.sol +++ b/tests/integration/fuzz/lockup/withdraw.t.sol @@ -3,8 +3,8 @@ pragma solidity >=0.8.22 <0.9.0; import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; -import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; -import { Lockup } from "src/types/DataTypes.sol"; +import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; +import { Lockup } from "src/types/Lockup.sol"; import { Integration_Test } from "../../Integration.t.sol"; @@ -23,21 +23,26 @@ abstract contract Withdraw_Integration_Fuzz_Test is Integration_Test { vm.assume(caller != users.sender && caller != users.recipient); // Make the fuzzed address the caller in this test. - resetPrank({ msgSender: caller }); + setMsgSender(caller); + // vm.deal(caller, 1 ether); // Simulate the passage of time. vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); // Make the withdrawal. - lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: defaults.STREAMED_AMOUNT_26_PERCENT() }); + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: ids.defaultStream, + to: users.recipient, + amount: defaults.STREAMED_AMOUNT_26_PERCENT() + }); // Assert that the stream's status is still "STREAMING". - Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); + Lockup.Status actualStatus = lockup.statusOf(ids.defaultStream); Lockup.Status expectedStatus = Lockup.Status.STREAMING; assertEq(actualStatus, expectedStatus); // Assert that the withdrawn amount has been updated. - uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(defaultStreamId); + uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(ids.defaultStream); uint128 expectedWithdrawnAmount = defaults.STREAMED_AMOUNT_26_PERCENT(); assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); } @@ -57,21 +62,25 @@ abstract contract Withdraw_Integration_Fuzz_Test is Integration_Test { vm.assume(to != address(0)); // Make the operator the caller in this test. - resetPrank({ msgSender: users.operator }); + setMsgSender(users.operator); // Simulate the passage of time. vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); // Make the withdrawal. - lockup.withdraw({ streamId: defaultStreamId, to: to, amount: defaults.STREAMED_AMOUNT_26_PERCENT() }); + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: ids.defaultStream, + to: to, + amount: defaults.STREAMED_AMOUNT_26_PERCENT() + }); // Assert that the stream's status is still "STREAMING". - Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); + Lockup.Status actualStatus = lockup.statusOf(ids.defaultStream); Lockup.Status expectedStatus = Lockup.Status.STREAMING; assertEq(actualStatus, expectedStatus); // Assert that the withdrawn amount has been updated. - uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(defaultStreamId); + uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(ids.defaultStream); uint128 expectedWithdrawnAmount = defaults.STREAMED_AMOUNT_26_PERCENT(); assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); } @@ -101,44 +110,49 @@ abstract contract Withdraw_Integration_Fuzz_Test is Integration_Test { vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); // Cancel the stream. - resetPrank({ msgSender: users.sender }); - lockup.cancel({ streamId: defaultStreamId }); - resetPrank({ msgSender: users.recipient }); + setMsgSender(users.sender); + lockup.cancel({ streamId: ids.defaultStream }); + setMsgSender(users.recipient); // Bound the withdraw amount. - uint128 withdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); + uint128 withdrawableAmount = lockup.withdrawableAmountOf(ids.defaultStream); withdrawAmount = boundUint128(withdrawAmount, 1, withdrawableAmount); + uint256 previousAggregateAmount = lockup.aggregateAmount(dai); + // Expect the tokens to be transferred to the fuzzed `to` address. expectCallToTransfer({ to: to, value: withdrawAmount }); // Expect the relevant events to be emitted. vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.WithdrawFromLockupStream(defaultStreamId, to, dai, withdrawAmount); + emit ISablierLockup.WithdrawFromLockupStream(ids.defaultStream, to, dai, withdrawAmount); vm.expectEmit({ emitter: address(lockup) }); - emit IERC4906.MetadataUpdate({ _tokenId: defaultStreamId }); + emit IERC4906.MetadataUpdate({ _tokenId: ids.defaultStream }); // Make the withdrawal. - lockup.withdraw({ streamId: defaultStreamId, to: to, amount: withdrawAmount }); + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ streamId: ids.defaultStream, to: to, amount: withdrawAmount }); // Check if the stream has been depleted. - uint128 refundedAmount = lockup.getRefundedAmount(defaultStreamId); + uint128 refundedAmount = lockup.getRefundedAmount(ids.defaultStream); bool isDepleted = withdrawAmount == defaults.DEPOSIT_AMOUNT() - refundedAmount; // Assert that the stream's status is correct. - Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); + Lockup.Status actualStatus = lockup.statusOf(ids.defaultStream); Lockup.Status expectedStatus = isDepleted ? Lockup.Status.DEPLETED : Lockup.Status.CANCELED; assertEq(actualStatus, expectedStatus); // Assert that the withdrawn amount has been updated. - uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(defaultStreamId); + uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(ids.defaultStream); uint128 expectedWithdrawnAmount = withdrawAmount; assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); // Assert that the not burned NFT. - address actualNFTowner = lockup.ownerOf({ tokenId: defaultStreamId }); + address actualNFTowner = lockup.ownerOf({ tokenId: ids.defaultStream }); address expectedNFTOwner = users.recipient; assertEq(actualNFTowner, expectedNFTOwner, "NFT owner"); + + // It should update the aggregate amount. + assertEq(lockup.aggregateAmount(dai), previousAggregateAmount - withdrawAmount, "aggregateAmount"); } /// @dev Given enough fuzz runs, all of the following scenarios will be fuzzed: @@ -169,28 +183,31 @@ abstract contract Withdraw_Integration_Fuzz_Test is Integration_Test { vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); // Bound the withdraw amount. - uint128 withdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); + uint128 withdrawableAmount = lockup.withdrawableAmountOf(ids.defaultStream); withdrawAmount = boundUint128(withdrawAmount, 1, withdrawableAmount); + uint256 previousAggregateAmount = lockup.aggregateAmount(dai); + uint256 previousLockupETHBalance = address(lockup).balance; + // Expect the tokens to be transferred to the fuzzed `to` address. expectCallToTransfer({ to: to, value: withdrawAmount }); // Expect the relevant events to be emitted. vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.WithdrawFromLockupStream(defaultStreamId, to, dai, withdrawAmount); + emit ISablierLockup.WithdrawFromLockupStream(ids.defaultStream, to, dai, withdrawAmount); vm.expectEmit({ emitter: address(lockup) }); - emit IERC4906.MetadataUpdate({ _tokenId: defaultStreamId }); + emit IERC4906.MetadataUpdate({ _tokenId: ids.defaultStream }); // Make the withdrawal. - lockup.withdraw(defaultStreamId, to, withdrawAmount); + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }(ids.defaultStream, to, withdrawAmount); // Check if the stream is depleted or settled. It is possible for the stream to be just settled // and not depleted because the withdraw amount is fuzzed. bool isDepleted = withdrawAmount == defaults.DEPOSIT_AMOUNT(); - bool isSettled = lockup.refundableAmountOf(defaultStreamId) == 0; + bool isSettled = lockup.refundableAmountOf(ids.defaultStream) == 0; // Assert that the stream's status is correct. - Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); + Lockup.Status actualStatus = lockup.statusOf(ids.defaultStream); Lockup.Status expectedStatus; if (isDepleted) { expectedStatus = Lockup.Status.DEPLETED; @@ -202,13 +219,17 @@ abstract contract Withdraw_Integration_Fuzz_Test is Integration_Test { assertEq(actualStatus, expectedStatus); // Assert that the withdrawn amount has been updated. - uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(defaultStreamId); + uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(ids.defaultStream); uint128 expectedWithdrawnAmount = withdrawAmount; assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); // Assert that the not burned NFT. - address actualNFTowner = lockup.ownerOf({ tokenId: defaultStreamId }); + address actualNFTowner = lockup.ownerOf({ tokenId: ids.defaultStream }); address expectedNFTOwner = users.recipient; assertEq(actualNFTowner, expectedNFTOwner, "NFT owner"); + + // It should update the aggregate amount. + assertEq(lockup.aggregateAmount(dai), previousAggregateAmount - withdrawAmount, "aggregateAmount"); + assertEq(address(lockup).balance, previousLockupETHBalance + LOCKUP_MIN_FEE_WEI, "lockup balance"); } } diff --git a/tests/integration/fuzz/lockup-base/withdrawMax.t.sol b/tests/integration/fuzz/lockup/withdrawMax.t.sol similarity index 75% rename from tests/integration/fuzz/lockup-base/withdrawMax.t.sol rename to tests/integration/fuzz/lockup/withdrawMax.t.sol index 03c36885f..0bf473ae8 100644 --- a/tests/integration/fuzz/lockup-base/withdrawMax.t.sol +++ b/tests/integration/fuzz/lockup/withdrawMax.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; -import { Lockup } from "src/types/DataTypes.sol"; +import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; +import { Lockup } from "src/types/Lockup.sol"; import { Integration_Test } from "../../Integration.t.sol"; @@ -18,32 +18,32 @@ contract WithdrawMax_Integration_Fuzz_Test is Integration_Test { // Expect the relevant event to be emitted. vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.WithdrawFromLockupStream({ - streamId: defaultStreamId, + emit ISablierLockup.WithdrawFromLockupStream({ + streamId: ids.defaultStream, to: users.recipient, token: dai, amount: defaults.DEPOSIT_AMOUNT() }); // Make the max withdrawal. - lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); + lockup.withdrawMax{ value: LOCKUP_MIN_FEE_WEI }({ streamId: ids.defaultStream, to: users.recipient }); // Assert that the withdrawn amount has been updated. - uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(defaultStreamId); + uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(ids.defaultStream); uint128 expectedWithdrawnAmount = defaults.DEPOSIT_AMOUNT(); assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); // Assert that the stream's status is "DEPLETED". - Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); + Lockup.Status actualStatus = lockup.statusOf(ids.defaultStream); Lockup.Status expectedStatus = Lockup.Status.DEPLETED; assertEq(actualStatus, expectedStatus); // Assert that the stream is not cancelable anymore. - bool isCancelable = lockup.isCancelable(defaultStreamId); + bool isCancelable = lockup.isCancelable(ids.defaultStream); assertFalse(isCancelable, "isCancelable"); // Assert that the not burned NFT. - address actualNFTowner = lockup.ownerOf({ tokenId: defaultStreamId }); + address actualNFTowner = lockup.ownerOf({ tokenId: ids.defaultStream }); address expectedNFTOwner = users.recipient; assertEq(actualNFTowner, expectedNFTOwner, "NFT owner"); } @@ -55,25 +55,25 @@ contract WithdrawMax_Integration_Fuzz_Test is Integration_Test { vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); // Get the withdraw amount. - uint128 withdrawAmount = lockup.withdrawableAmountOf(defaultStreamId); + uint128 withdrawAmount = lockup.withdrawableAmountOf(ids.defaultStream); // Expect the tokens to be transferred to the Recipient. expectCallToTransfer({ to: users.recipient, value: withdrawAmount }); // Expect the relevant event to be emitted. vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.WithdrawFromLockupStream({ - streamId: defaultStreamId, + emit ISablierLockup.WithdrawFromLockupStream({ + streamId: ids.defaultStream, to: users.recipient, token: dai, amount: withdrawAmount }); // Make the max withdrawal. - lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); + lockup.withdrawMax{ value: LOCKUP_MIN_FEE_WEI }({ streamId: ids.defaultStream, to: users.recipient }); // Assert that the withdrawn amount has been updated. - uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(defaultStreamId); + uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(ids.defaultStream); uint128 expectedWithdrawnAmount = withdrawAmount; assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); } diff --git a/tests/integration/fuzz/lockup-base/withdrawMaxAndTransfer.t.sol b/tests/integration/fuzz/lockup/withdrawMaxAndTransfer.t.sol similarity index 79% rename from tests/integration/fuzz/lockup-base/withdrawMaxAndTransfer.t.sol rename to tests/integration/fuzz/lockup/withdrawMaxAndTransfer.t.sol index b5bf2adcd..d7bf2bad5 100644 --- a/tests/integration/fuzz/lockup-base/withdrawMaxAndTransfer.t.sol +++ b/tests/integration/fuzz/lockup/withdrawMaxAndTransfer.t.sol @@ -3,7 +3,7 @@ pragma solidity >=0.8.22 <0.9.0; import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; -import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; +import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; import { Integration_Test } from "../../Integration.t.sol"; @@ -30,7 +30,7 @@ contract WithdrawMaxAndTransfer_Integration_Fuzz_Test is Integration_Test { vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); // Get the withdraw amount. - uint128 withdrawAmount = lockup.withdrawableAmountOf(defaultStreamId); + uint128 withdrawAmount = lockup.withdrawableAmountOf(ids.defaultStream); if (withdrawAmount > 0) { // Expect the tokens to be transferred to the fuzzed recipient. @@ -38,8 +38,8 @@ contract WithdrawMaxAndTransfer_Integration_Fuzz_Test is Integration_Test { // Expect the relevant event to be emitted. vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.WithdrawFromLockupStream({ - streamId: defaultStreamId, + emit ISablierLockup.WithdrawFromLockupStream({ + streamId: ids.defaultStream, to: users.recipient, token: dai, amount: withdrawAmount @@ -48,18 +48,21 @@ contract WithdrawMaxAndTransfer_Integration_Fuzz_Test is Integration_Test { // Expect the relevant event to be emitted. vm.expectEmit({ emitter: address(lockup) }); - emit IERC721.Transfer({ from: users.recipient, to: newRecipient, tokenId: defaultStreamId }); + emit IERC721.Transfer({ from: users.recipient, to: newRecipient, tokenId: ids.defaultStream }); // Make the max withdrawal and transfer the NFT. - lockup.withdrawMaxAndTransfer({ streamId: defaultStreamId, newRecipient: newRecipient }); + lockup.withdrawMaxAndTransfer{ value: LOCKUP_MIN_FEE_WEI }({ + streamId: ids.defaultStream, + newRecipient: newRecipient + }); // Assert that the withdrawn amount has been updated. - uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(defaultStreamId); + uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(ids.defaultStream); uint128 expectedWithdrawnAmount = withdrawAmount; assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); // Assert that the fuzzed recipient is the new stream recipient (and NFT owner). - address actualRecipient = lockup.getRecipient(defaultStreamId); + address actualRecipient = lockup.getRecipient(ids.defaultStream); address expectedRecipient = newRecipient; assertEq(actualRecipient, expectedRecipient, "recipient"); } diff --git a/tests/invariant/Invariant.t.sol b/tests/invariant/Invariant.t.sol index fa06cdb6b..621f0e0ed 100644 --- a/tests/invariant/Invariant.t.sol +++ b/tests/invariant/Invariant.t.sol @@ -2,8 +2,12 @@ pragma solidity >=0.8.22 <0.9.0; import { StdInvariant } from "forge-std/src/StdInvariant.sol"; -import { Lockup, LockupDynamic, LockupTranched } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; +import { LockupDynamic } from "src/types/LockupDynamic.sol"; +import { LockupTranched } from "src/types/LockupTranched.sol"; +import { StreamAction } from "tests/utils/Types.sol"; import { Base_Test } from "../Base.t.sol"; +import { LockupComptrollerHandler } from "./handlers/LockupComptrollerHandler.sol"; import { LockupCreateHandler } from "./handlers/LockupCreateHandler.sol"; import { LockupHandler } from "./handlers/LockupHandler.sol"; import { LockupStore } from "./stores/LockupStore.sol"; @@ -14,9 +18,10 @@ contract Invariant_Test is Base_Test, StdInvariant { TEST CONTRACTS //////////////////////////////////////////////////////////////////////////*/ + LockupComptrollerHandler internal comptrollerHandler; + LockupCreateHandler internal createHandler; LockupHandler internal handler; LockupStore internal lockupStore; - LockupCreateHandler internal createHandler; /*////////////////////////////////////////////////////////////////////////// SET-UP FUNCTION @@ -30,6 +35,7 @@ contract Invariant_Test is Base_Test, StdInvariant { vm.label({ account: address(lockupStore), newLabel: "LockupStore" }); // Deploy the Lockup handlers. + comptrollerHandler = new LockupComptrollerHandler({ token_: dai, lockup_: lockup }); createHandler = new LockupCreateHandler({ token_: dai, lockupStore_: lockupStore, lockup_: lockup }); handler = new LockupHandler({ token_: dai, lockupStore_: lockupStore, lockup_: lockup }); @@ -38,6 +44,7 @@ contract Invariant_Test is Base_Test, StdInvariant { vm.label({ account: address(handler), newLabel: "LockupHandler" }); // Target the LockupDynamic handlers for invariant testing. + targetContract(address(comptrollerHandler)); targetContract(address(createHandler)); targetContract(address(handler)); @@ -49,32 +56,44 @@ contract Invariant_Test is Base_Test, StdInvariant { } /*////////////////////////////////////////////////////////////////////////// - COMMON + COMMON INVARIANTS //////////////////////////////////////////////////////////////////////////*/ - // solhint-disable max-line-length - function invariant_ContractTokenBalance() external view { - uint256 contractBalance = dai.balanceOf(address(lockup)); + function invariant_NextStreamId() external view { + uint256 lastStreamId = lockupStore.lastStreamId(); + for (uint256 i = 0; i < lastStreamId; ++i) { + uint256 nextStreamId = lockup.nextStreamId(); + assertEq(nextStreamId, lastStreamId + 1, "Invariant violation: next stream ID not incremented"); + } + } + + function invariant_Balances() external view { + uint256 erc20Balance = dai.balanceOf(address(lockup)); uint256 lastStreamId = lockupStore.lastStreamId(); - uint256 depositedAmountsSum; - uint256 refundedAmountsSum; - uint256 withdrawnAmountsSum; + uint256 totalDeposits; + uint256 totalRefunds; + uint256 totalWithdrawals; for (uint256 i = 0; i < lastStreamId; ++i) { uint256 streamId = lockupStore.streamIds(i); - depositedAmountsSum += uint256(lockup.getDepositedAmount(streamId)); - refundedAmountsSum += uint256(lockup.getRefundedAmount(streamId)); - withdrawnAmountsSum += uint256(lockup.getWithdrawnAmount(streamId)); + totalDeposits += uint256(lockup.getDepositedAmount(streamId)); + totalRefunds += uint256(lockup.getRefundedAmount(streamId)); + totalWithdrawals += uint256(lockup.getWithdrawnAmount(streamId)); } + uint256 totals = totalDeposits - totalRefunds - totalWithdrawals; + assertEq( + lockup.aggregateAmount(dai), + totals, + unicode"Invariant violation: aggregate amount != Σ deposits - Σ refunds - Σ withdrawals" + ); + assertGe( - contractBalance, - depositedAmountsSum - refundedAmountsSum - withdrawnAmountsSum, - unicode"Invariant violation: contract balances < Σ deposited amounts - Σ refunded amounts - Σ withdrawn amounts" + erc20Balance, totals, unicode"Invariant violation: ERC-20 balance < Σ deposits - Σ refunds - Σ withdrawals" ); } - function invariant_DepositedAmountGteStreamedAmount() external view { + function invariant_DepositedGteStreamed() external view { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { uint256 streamId = lockupStore.streamIds(i); @@ -86,7 +105,7 @@ contract Invariant_Test is Base_Test, StdInvariant { } } - function invariant_DepositedAmountGteWithdrawableAmount() external view { + function invariant_DepositedGteWithdrawable() external view { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { uint256 streamId = lockupStore.streamIds(i); @@ -98,7 +117,7 @@ contract Invariant_Test is Base_Test, StdInvariant { } } - function invariant_DepositedAmountGteWithdrawnAmount() external view { + function invariant_DepositedGteWithdrawn() external view { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { uint256 streamId = lockupStore.streamIds(i); @@ -110,12 +129,12 @@ contract Invariant_Test is Base_Test, StdInvariant { } } - function invariant_DepositedAmountNotZero() external view { + function invariant_DepositedNotZero() external view { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { uint256 streamId = lockupStore.streamIds(i); uint128 depositAmount = lockup.getDepositedAmount(streamId); - assertNotEq(depositAmount, 0, "Invariant violated: stream non-null, deposited amount zero"); + assertNotEq(depositAmount, 0, "Invariant violation: stream non-null, deposited amount zero"); } } @@ -131,20 +150,36 @@ contract Invariant_Test is Base_Test, StdInvariant { } } - function invariant_NextStreamId() external view { + function invariant_StartTimeNotZero() external view { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { - uint256 nextStreamId = lockup.nextStreamId(); - assertEq(nextStreamId, lastStreamId + 1, "Invariant violation: next stream ID not incremented"); + uint256 streamId = lockupStore.streamIds(i); + uint40 startTime = lockup.getStartTime(streamId); + assertGt(startTime, 0, "Invariant violation: start time zero"); } } - function invariant_StartTimeNotZero() external view { + function invariant_StreamedGteWithdrawable() external view { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { uint256 streamId = lockupStore.streamIds(i); - uint40 startTime = lockup.getStartTime(streamId); - assertGt(startTime, 0, "Invariant violated: start time zero"); + assertGe( + lockup.streamedAmountOf(streamId), + lockup.withdrawableAmountOf(streamId), + "Invariant violation: streamed amount < withdrawable amount" + ); + } + } + + function invariant_StreamedGteWithdrawn() external view { + uint256 lastStreamId = lockupStore.lastStreamId(); + for (uint256 i = 0; i < lastStreamId; ++i) { + uint256 streamId = lockupStore.streamIds(i); + assertGe( + lockup.streamedAmountOf(streamId), + lockup.getWithdrawnAmount(streamId), + "Invariant violation: streamed amount < withdrawn amount" + ); } } @@ -276,7 +311,7 @@ contract Invariant_Test is Base_Test, StdInvariant { } } - /// @dev See diagram at https://docs.sablier.com/concepts/protocol/statuses#diagram + /// @dev See diagram at https://docs.sablier.com/concepts/lockup/statuses#diagram function invariant_StatusTransitions() external { uint256 lastStreamId = lockupStore.lastStreamId(); if (lastStreamId == 0) { @@ -326,32 +361,36 @@ contract Invariant_Test is Base_Test, StdInvariant { } } - function invariant_StreamedAmountGteWithdrawableAmount() external view { + function invariant_GasUsedCreateGeCancel() external view { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { uint256 streamId = lockupStore.streamIds(i); - assertGe( - lockup.streamedAmountOf(streamId), - lockup.withdrawableAmountOf(streamId), - "Invariant violation: streamed amount < withdrawable amount" - ); + uint256 createGas = lockupStore.gasUsed(streamId, StreamAction.CREATE); + uint256 cancelGas = lockupStore.gasUsed(streamId, StreamAction.CANCEL); + + // If cancel action is called 0 times, skip. + if (cancelGas == 0) return; + + assertGe(createGas, cancelGas, "Invariant violation: cancel gas > create gas"); } } - function invariant_StreamedAmountGteWithdrawnAmount() external view { + function invariant_GasUsedCreateGeWithdraw() external view { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { uint256 streamId = lockupStore.streamIds(i); - assertGe( - lockup.streamedAmountOf(streamId), - lockup.getWithdrawnAmount(streamId), - "Invariant violation: streamed amount < withdrawn amount" - ); + uint256 createGas = lockupStore.gasUsed(streamId, StreamAction.CREATE); + uint256 withdrawGas = lockupStore.gasUsed(streamId, StreamAction.WITHDRAW); + + // If withdraw action is called 0 times, skip. + if (withdrawGas == 0) return; + + assertGe(createGas, withdrawGas, "Invariant violation: withdraw gas > create gas"); } } /*////////////////////////////////////////////////////////////////////////// - LOCKUP DYNAMIC + LD MODEL INVARIANTS //////////////////////////////////////////////////////////////////////////*/ /// @dev Unordered segment timestamps are not allowed. @@ -364,7 +403,7 @@ contract Invariant_Test is Base_Test, StdInvariant { uint40 previousTimestamp = segments[0].timestamp; for (uint256 j = 1; j < segments.length; ++j) { assertGt( - segments[j].timestamp, previousTimestamp, "Invariant violated: segment timestamps not ordered" + segments[j].timestamp, previousTimestamp, "Invariant violation: segment timestamps not ordered" ); previousTimestamp = segments[j].timestamp; } @@ -373,7 +412,7 @@ contract Invariant_Test is Base_Test, StdInvariant { } /*////////////////////////////////////////////////////////////////////////// - LOCKUP LINEAR + LL MODEL INVARIANTS //////////////////////////////////////////////////////////////////////////*/ /// @dev If it is not zero, the cliff time must be strictly greater than the start time. @@ -386,7 +425,7 @@ contract Invariant_Test is Base_Test, StdInvariant { assertGt( lockup.getCliffTime(streamId), lockup.getStartTime(streamId), - "Invariant violated: cliff time <= start time" + "Invariant violation: cliff time <= start time" ); } } @@ -402,14 +441,14 @@ contract Invariant_Test is Base_Test, StdInvariant { assertGt( lockup.getEndTime(streamId), lockup.getCliffTime(streamId), - "Invariant violated: end time <= cliff time" + "Invariant violation: end time <= cliff time" ); } } } /*////////////////////////////////////////////////////////////////////////// - LOCKUP TRANCHED + LT MODEL INVARIANTS //////////////////////////////////////////////////////////////////////////*/ /// @dev Unordered tranche timestamps are not allowed. @@ -422,7 +461,7 @@ contract Invariant_Test is Base_Test, StdInvariant { uint40 previousTimestamp = tranches[0].timestamp; for (uint256 j = 1; j < tranches.length; ++j) { assertGt( - tranches[j].timestamp, previousTimestamp, "Invariant violated: tranche timestamps not ordered" + tranches[j].timestamp, previousTimestamp, "Invariant violation: tranche timestamps not ordered" ); previousTimestamp = tranches[j].timestamp; } diff --git a/tests/invariant/README.md b/tests/invariant/README.md new file mode 100644 index 000000000..4e15e55fd --- /dev/null +++ b/tests/invariant/README.md @@ -0,0 +1,65 @@ +### List of Invariants Implemented in [Invariant.t.sol](./Invariant.t.sol) + +1. Next stream id = Current stream id + 1 + +2. For a token: + - Aggregate amount = (Total deposited - Total refunded - Total withdrawn) + - token.balanceOf(lockup) $`\ge`$ (Total deposited - Total refunded - Total withdrawn) + +3. For a stream: + - Deposited amount $`\ge`$ Streamed amount + - Deposited amount $`\ge`$ Withdrawable amount + - Deposited amount $`\ge`$ Withdrawn amount + - Deposited amount $`\ge`$ 0 + - End time > Start time + - Start time $`\ge`$ 0 + - Streamed amount $`\ge`$ Withdrawable amount + - Streamed amount $`\ge`$ Withdrawn amount + +4. For a canceled stream: + - Refunded amount > 0 + - Stream should not be cancelable anymore + - Refundable amount = 0 + - Withdrawable amount > 0 + +5. For a depleted stream: + - Withdrawn amount = (Deposited amount - Refunded amount) + - Stream should not be cancelable anymore + - Refundable amount = 0 + - Withdrawable amount = 0 + +6. For a pending stream: + - Refunded amount = 0 + - Withdrawn amount = 0 + - Refundable amount = Deposited amount + - Streamed amount = 0 + - Withdrawable amount = 0 + +7. For a settled stream: + - Refunded amount = 0 + - Stream should not be cancelable anymore + - Refundable amount = 0 + - Streamed amount = Deposited amount + +8. For a streaming stream: + - Refunded amount = 0 + - Streamed amount < Deposited amount + +9. State transitions: + - PENDING $`\not\to`$ DEPLETED + - STREAMING $`\not\to`$ PENDING + - SETTLED $`\not\to`$ { PENDING, STREAMING, CANCELED } + - CANCELED $`\not\to`$ { PENDING, STREAMING, SETTLED } + - DEPLETED $`\to`$ DEPLETED + +10. Gas usage: + - Create $`\ge`$ Cancel + - Create $`\ge`$ Withdraw + +11. For a Dynamic stream, segment timestamps should be strictly increasing. + +12. For a Linear stream, + - If Cliff time > 0, $`\implies`$ Cliff time > Start time. + - End time > Cliff time + +13. For a Tranched stream, tranche timestamps should be strictly increasing. diff --git a/tests/invariant/handlers/BaseHandler.sol b/tests/invariant/handlers/BaseHandler.sol index bf267b3c1..48462901c 100644 --- a/tests/invariant/handlers/BaseHandler.sol +++ b/tests/invariant/handlers/BaseHandler.sol @@ -5,11 +5,10 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { StdCheats } from "forge-std/src/StdCheats.sol"; import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; -import { Constants } from "../../utils/Constants.sol"; import { Fuzzers } from "../../utils/Fuzzers.sol"; /// @notice Base contract with common logic needed by {LockupHandler} and {LockupCreateHandler} contracts. -abstract contract BaseHandler is Constants, Fuzzers, StdCheats { +abstract contract BaseHandler is Fuzzers, StdCheats { /*////////////////////////////////////////////////////////////////////////// STATE-VARIABLES //////////////////////////////////////////////////////////////////////////*/ @@ -49,17 +48,17 @@ abstract contract BaseHandler is Constants, Fuzzers, StdCheats { /// @param timeJumpSeed A fuzzed value needed for generating random time warps. modifier adjustTimestamp(uint256 timeJumpSeed) { uint256 timeJump = _bound(timeJumpSeed, 2 minutes, 40 days); - vm.warp(getBlockTimestamp() + timeJump); + skip(timeJump); _; } /// @dev Checks user assumptions. - modifier checkUsers(address sender, address recipient, address broker) { - // Prevent the sender, recipient and broker to be the zero address. - vm.assume(sender != address(0) && recipient != address(0) && broker != address(0)); + modifier checkUsers(address sender, address recipient) { + // Prevent the sender and recipient to be the zero address. + vm.assume(sender != address(0) && recipient != address(0)); // Prevent the contract itself from playing the role of any user. - vm.assume(sender != address(this) && recipient != address(this) && broker != address(this)); + vm.assume(sender != address(this) && recipient != address(this)); _; } @@ -72,7 +71,7 @@ abstract contract BaseHandler is Constants, Fuzzers, StdCheats { /// @dev Makes the provided sender the caller. modifier useNewSender(address sender) { - resetPrank(sender); + setMsgSender(sender); _; } } diff --git a/tests/invariant/handlers/LockupComptrollerHandler.sol b/tests/invariant/handlers/LockupComptrollerHandler.sol new file mode 100644 index 000000000..297d21de5 --- /dev/null +++ b/tests/invariant/handlers/LockupComptrollerHandler.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; +import { BaseHandler } from "./BaseHandler.sol"; + +contract LockupComptrollerHandler is BaseHandler { + /*////////////////////////////////////////////////////////////////////////// + MODIFIERS + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Since all comptroller-related functions are rarely called compared to core lockup functionalities, + /// we limit the number of calls to 10. + modifier limitNumberOfCalls(string memory name) { + vm.assume(calls[name] < 10); + _; + } + + /*////////////////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////////////////*/ + + constructor(IERC20 token_, ISablierLockup lockup_) BaseHandler(token_, lockup_) { } + + /*////////////////////////////////////////////////////////////////////////// + SABLIER-LOCKUP + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Increase the Lockup contract's balance by directly transferring tokens to it. + function randomTransfer(uint256 amount) external { + amount = _bound(amount, 1, 100e18); + + deal({ token: address(token), to: address(lockup), give: token.balanceOf(address(lockup)) + amount }); + } + + function recover() external limitNumberOfCalls("recover") instrument("recover") { + vm.assume(token.balanceOf(address(lockup)) > lockup.aggregateAmount(token)); + + setMsgSender(address(lockup.comptroller())); + + lockup.recover({ token: token, to: address(lockup.comptroller()) }); + } +} diff --git a/tests/invariant/handlers/LockupCreateHandler.sol b/tests/invariant/handlers/LockupCreateHandler.sol index 1eb84fdb8..bb2231e18 100644 --- a/tests/invariant/handlers/LockupCreateHandler.sol +++ b/tests/invariant/handlers/LockupCreateHandler.sol @@ -4,9 +4,13 @@ pragma solidity >=0.8.22 <0.9.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; -import { Lockup, LockupDynamic, LockupLinear, LockupTranched } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; +import { LockupDynamic } from "src/types/LockupDynamic.sol"; +import { LockupLinear } from "src/types/LockupLinear.sol"; +import { LockupTranched } from "src/types/LockupTranched.sol"; import { Calculations } from "tests/utils/Calculations.sol"; +import { StreamAction } from "tests/utils/Types.sol"; import { LockupStore } from "../stores/LockupStore.sol"; import { BaseHandler } from "./BaseHandler.sol"; @@ -38,7 +42,7 @@ contract LockupCreateHandler is BaseHandler, Calculations { public instrument("createWithDurationsLD") adjustTimestamp(timeJumpSeed) - checkUsers(params.sender, params.recipient, params.broker.account) + checkUsers(params.sender, params.recipient) useNewSender(params.sender) { // We don't want to create more than a certain number of streams. @@ -47,26 +51,28 @@ contract LockupCreateHandler is BaseHandler, Calculations { // The protocol doesn't allow empty segment arrays. vm.assume(segments.length != 0); - // Bound the broker fee. - params.broker.fee = _bound(params.broker.fee, 0, MAX_BROKER_FEE); - // Fuzz the durations. fuzzSegmentDurations(segments); // Fuzz the segment amounts and calculate the total amount. - (params.totalAmount,) = - fuzzDynamicStreamAmounts({ upperBound: 1_000_000_000e18, segments: segments, brokerFee: params.broker.fee }); + params.depositAmount = fuzzDynamicStreamAmounts({ upperBound: 1_000_000_000e18, segments: segments }); // Mint enough tokens to the Sender. - deal({ token: address(token), to: params.sender, give: token.balanceOf(params.sender) + params.totalAmount }); + deal({ token: address(token), to: params.sender, give: params.depositAmount }); // Approve {SablierLockup} to spend the tokens. - token.approve({ spender: address(lockup), value: params.totalAmount }); + token.approve({ spender: address(lockup), value: params.depositAmount }); // Create the stream. params.token = token; params.shape = "Dynamic Stream"; + + // Create the stream and record the gas used. + uint256 gasBefore = gasleft(); uint256 streamId = lockup.createWithDurationsLD(params, segments); + uint256 gasAfter = gasleft(); + + lockupStore.recordGasUsage({ streamId: streamId, action: StreamAction.CREATE, gas: gasBefore - gasAfter }); // Store the stream ID. lockupStore.pushStreamId(streamId, params.sender, params.recipient); @@ -81,7 +87,7 @@ contract LockupCreateHandler is BaseHandler, Calculations { public instrument("createWithDurationsLL") adjustTimestamp(timeJumpSeed) - checkUsers(params.sender, params.recipient, params.broker.account) + checkUsers(params.sender, params.recipient) useNewSender(params.sender) { // We don't want to create more than a certain number of streams. @@ -90,15 +96,21 @@ contract LockupCreateHandler is BaseHandler, Calculations { (params, unlockAmounts, durations) = _boundCreateWithDurationsLLParams(params, unlockAmounts, durations); // Mint enough tokens to the Sender. - deal({ token: address(token), to: params.sender, give: token.balanceOf(params.sender) + params.totalAmount }); + deal({ token: address(token), to: params.sender, give: params.depositAmount }); // Approve {SablierLockup} to spend the tokens. - token.approve({ spender: address(lockup), value: params.totalAmount }); + token.approve({ spender: address(lockup), value: params.depositAmount }); // Create the stream. params.token = token; params.shape = "Linear Stream"; + + // Create the stream and record the gas used. + uint256 gasBefore = gasleft(); uint256 streamId = lockup.createWithDurationsLL(params, unlockAmounts, durations); + uint256 gasAfter = gasleft(); + + lockupStore.recordGasUsage({ streamId: streamId, action: StreamAction.CREATE, gas: gasBefore - gasAfter }); // Store the stream ID. lockupStore.pushStreamId(streamId, params.sender, params.recipient); @@ -112,7 +124,7 @@ contract LockupCreateHandler is BaseHandler, Calculations { public instrument("createWithDurationsLT") adjustTimestamp(timeJumpSeed) - checkUsers(params.sender, params.recipient, params.broker.account) + checkUsers(params.sender, params.recipient) useNewSender(params.sender) { // We don't want to create more than a certain number of streams. @@ -121,29 +133,28 @@ contract LockupCreateHandler is BaseHandler, Calculations { // The protocol doesn't allow empty tranche arrays. vm.assume(tranches.length != 0); - // Bound the broker fee. - params.broker.fee = _bound(params.broker.fee, 0, MAX_BROKER_FEE); - // Fuzz the durations. fuzzTrancheDurations(tranches); // Fuzz the tranche amounts and calculate the total amount. - (params.totalAmount,) = fuzzTranchedStreamAmounts({ - upperBound: 1_000_000_000e18, - tranches: tranches, - brokerFee: params.broker.fee - }); + params.depositAmount = fuzzTranchedStreamAmounts({ upperBound: 1_000_000_000e18, tranches: tranches }); // Mint enough tokens to the Sender. - deal({ token: address(token), to: params.sender, give: token.balanceOf(params.sender) + params.totalAmount }); + deal({ token: address(token), to: params.sender, give: params.depositAmount }); // Approve {SablierLockup} to spend the tokens. - token.approve({ spender: address(lockup), value: params.totalAmount }); + token.approve({ spender: address(lockup), value: params.depositAmount }); // Create the stream. params.token = token; params.shape = "Tranched Stream"; + + // Create the stream and record the gas used. + uint256 gasBefore = gasleft(); uint256 streamId = lockup.createWithDurationsLT(params, tranches); + uint256 gasAfter = gasleft(); + + lockupStore.recordGasUsage({ streamId: streamId, action: StreamAction.CREATE, gas: gasBefore - gasAfter }); // Store the stream ID. lockupStore.pushStreamId(streamId, params.sender, params.recipient); @@ -157,7 +168,7 @@ contract LockupCreateHandler is BaseHandler, Calculations { public instrument("createWithTimestampsLD") adjustTimestamp(timeJumpSeed) - checkUsers(params.sender, params.recipient, params.broker.account) + checkUsers(params.sender, params.recipient) useNewSender(params.sender) { // We don't want to create more than a certain number of streams. @@ -166,27 +177,31 @@ contract LockupCreateHandler is BaseHandler, Calculations { // The protocol doesn't allow empty segment arrays. vm.assume(segments.length != 0); - params.broker.fee = _bound(params.broker.fee, 0, MAX_BROKER_FEE); params.timestamps.start = boundUint40(params.timestamps.start, 1, getBlockTimestamp()); // Fuzz the segment timestamps. fuzzSegmentTimestamps(segments, params.timestamps.start); // Fuzz the segment amounts and calculate the total amount. - (params.totalAmount,) = - fuzzDynamicStreamAmounts({ upperBound: 1_000_000_000e18, segments: segments, brokerFee: params.broker.fee }); + params.depositAmount = fuzzDynamicStreamAmounts({ upperBound: 1_000_000_000e18, segments: segments }); // Mint enough tokens to the Sender. - deal({ token: address(token), to: params.sender, give: token.balanceOf(params.sender) + params.totalAmount }); + deal({ token: address(token), to: params.sender, give: params.depositAmount }); // Approve {SablierLockup} to spend the tokens. - token.approve({ spender: address(lockup), value: params.totalAmount }); + token.approve({ spender: address(lockup), value: params.depositAmount }); // Create the stream. params.token = token; params.shape = "Dynamic Stream"; params.timestamps.end = segments[segments.length - 1].timestamp; + + // Create the stream and record the gas used. + uint256 gasBefore = gasleft(); uint256 streamId = lockup.createWithTimestampsLD(params, segments); + uint256 gasAfter = gasleft(); + + lockupStore.recordGasUsage({ streamId: streamId, action: StreamAction.CREATE, gas: gasBefore - gasAfter }); // Store the stream ID. lockupStore.pushStreamId(streamId, params.sender, params.recipient); @@ -201,7 +216,7 @@ contract LockupCreateHandler is BaseHandler, Calculations { public instrument("createWithTimestampsLL") adjustTimestamp(timeJumpSeed) - checkUsers(params.sender, params.recipient, params.broker.account) + checkUsers(params.sender, params.recipient) useNewSender(params.sender) { // We don't want to create more than a certain number of streams. @@ -210,15 +225,21 @@ contract LockupCreateHandler is BaseHandler, Calculations { (params, unlockAmounts, cliffTime) = _boundCreateWithTimestampsLLParams(params, unlockAmounts, cliffTime); // Mint enough tokens to the Sender. - deal({ token: address(token), to: params.sender, give: token.balanceOf(params.sender) + params.totalAmount }); + deal({ token: address(token), to: params.sender, give: params.depositAmount }); // Approve {SablierLockup} to spend the tokens. - token.approve({ spender: address(lockup), value: params.totalAmount }); + token.approve({ spender: address(lockup), value: params.depositAmount }); // Create the stream. params.token = token; params.shape = "Linear Stream"; + + // Create the stream and record the gas used. + uint256 gasBefore = gasleft(); uint256 streamId = lockup.createWithTimestampsLL(params, unlockAmounts, cliffTime); + uint256 gasAfter = gasleft(); + + lockupStore.recordGasUsage({ streamId: streamId, action: StreamAction.CREATE, gas: gasBefore - gasAfter }); // Store the stream ID. lockupStore.pushStreamId(streamId, params.sender, params.recipient); @@ -232,7 +253,7 @@ contract LockupCreateHandler is BaseHandler, Calculations { public instrument("createWithTimestampsLT") adjustTimestamp(timeJumpSeed) - checkUsers(params.sender, params.recipient, params.broker.account) + checkUsers(params.sender, params.recipient) useNewSender(params.sender) { // We don't want to create more than a certain number of streams. @@ -241,30 +262,31 @@ contract LockupCreateHandler is BaseHandler, Calculations { // The protocol doesn't allow empty tranche arrays. vm.assume(tranches.length != 0); - params.broker.fee = _bound(params.broker.fee, 0, MAX_BROKER_FEE); params.timestamps.start = boundUint40(params.timestamps.start, 1, getBlockTimestamp()); // Fuzz the tranche timestamps. fuzzTrancheTimestamps(tranches, params.timestamps.start); // Fuzz the tranche amounts and calculate the total amount. - (params.totalAmount,) = fuzzTranchedStreamAmounts({ - upperBound: 1_000_000_000e18, - tranches: tranches, - brokerFee: params.broker.fee - }); + params.depositAmount = fuzzTranchedStreamAmounts({ upperBound: 1_000_000_000e18, tranches: tranches }); // Mint enough tokens to the Sender. - deal({ token: address(token), to: params.sender, give: token.balanceOf(params.sender) + params.totalAmount }); + deal({ token: address(token), to: params.sender, give: params.depositAmount }); // Approve {SablierLockup} to spend the tokens. - token.approve({ spender: address(lockup), value: params.totalAmount }); + token.approve({ spender: address(lockup), value: params.depositAmount }); // Create the stream. params.token = token; params.shape = "Tranched Stream"; params.timestamps.end = tranches[tranches.length - 1].timestamp; + + // Create the stream and record the gas used. + uint256 gasBefore = gasleft(); uint256 streamId = lockup.createWithTimestampsLT(params, tranches); + uint256 gasAfter = gasleft(); + + lockupStore.recordGasUsage({ streamId: streamId, action: StreamAction.CREATE, gas: gasBefore - gasAfter }); // Store the stream ID. lockupStore.pushStreamId(streamId, params.sender, params.recipient); @@ -287,15 +309,14 @@ contract LockupCreateHandler is BaseHandler, Calculations { returns (Lockup.CreateWithDurations memory, LockupLinear.UnlockAmounts memory, LockupLinear.Durations memory) { // Bound the stream parameters. - params.broker.fee = _bound(params.broker.fee, 0, MAX_BROKER_FEE); + durations.cliff = boundUint40(durations.cliff, 1 seconds, 2500 seconds); durations.total = boundUint40(durations.total, durations.cliff + 1 seconds, MAX_UNIX_TIMESTAMP); - params.totalAmount = boundUint128(params.totalAmount, 1, 1_000_000_000e18); - uint128 depositAmount = calculateDepositAmount(params.totalAmount, params.broker.fee); - unlockAmounts.start = boundUint128(unlockAmounts.start, 0, depositAmount); - unlockAmounts.cliff = depositAmount == unlockAmounts.start + params.depositAmount = boundUint128(params.depositAmount, 1, 1_000_000_000e18); + unlockAmounts.start = boundUint128(unlockAmounts.start, 0, params.depositAmount); + unlockAmounts.cliff = params.depositAmount == unlockAmounts.start ? 0 - : boundUint128(unlockAmounts.cliff, 0, depositAmount - unlockAmounts.start); + : boundUint128(unlockAmounts.cliff, 0, params.depositAmount - unlockAmounts.start); return (params, unlockAmounts, durations); } @@ -313,20 +334,19 @@ contract LockupCreateHandler is BaseHandler, Calculations { returns (Lockup.CreateWithTimestamps memory, LockupLinear.UnlockAmounts memory, uint40) { uint40 blockTimestamp = getBlockTimestamp(); - params.broker.fee = _bound(params.broker.fee, 0, MAX_BROKER_FEE); + params.timestamps.start = boundUint40(params.timestamps.start, 1 seconds, blockTimestamp); - params.totalAmount = boundUint128(params.totalAmount, 1, 1_000_000_000e18); - uint128 depositAmount = calculateDepositAmount(params.totalAmount, params.broker.fee); - unlockAmounts.start = boundUint128(unlockAmounts.start, 0, depositAmount); + params.depositAmount = boundUint128(params.depositAmount, 1, 1_000_000_000e18); + unlockAmounts.start = boundUint128(unlockAmounts.start, 0, params.depositAmount); unlockAmounts.cliff = 0; // The cliff time must be either zero or greater than the start time. if (cliffTime > 0) { cliffTime = boundUint40(cliffTime, params.timestamps.start + 1 seconds, params.timestamps.start + 52 weeks); - unlockAmounts.cliff = depositAmount == unlockAmounts.start + unlockAmounts.cliff = params.depositAmount == unlockAmounts.start ? 0 - : boundUint128(unlockAmounts.cliff, 0, depositAmount - unlockAmounts.start); + : boundUint128(unlockAmounts.cliff, 0, params.depositAmount - unlockAmounts.start); } // Bound the end time so that it is always greater than the start time, and the cliff time. diff --git a/tests/invariant/handlers/LockupHandler.sol b/tests/invariant/handlers/LockupHandler.sol index 20c6bb2fe..021028cf2 100644 --- a/tests/invariant/handlers/LockupHandler.sol +++ b/tests/invariant/handlers/LockupHandler.sol @@ -4,7 +4,8 @@ pragma solidity >=0.8.22 <0.9.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; -import { Lockup } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; +import { StreamAction } from "tests/utils/Types.sol"; import { LockupStore } from "../stores/LockupStore.sol"; import { BaseHandler } from "./BaseHandler.sol"; @@ -48,14 +49,14 @@ contract LockupHandler is BaseHandler { modifier useFuzzedStreamRecipient() { currentRecipient = lockupStore.recipients(currentStreamId); - resetPrank(currentRecipient); + setMsgSender(currentRecipient); vm.deal({ account: currentRecipient, newBalance: 100 ether }); _; } modifier useFuzzedStreamSender() { currentSender = lockupStore.senders(currentStreamId); - resetPrank(currentSender); + setMsgSender(currentSender); _; } @@ -102,8 +103,12 @@ contract LockupHandler is BaseHandler { // Not cancelable streams cannot be canceled. vm.assume(lockup.isCancelable(currentStreamId)); - // Cancel the stream. + // Cancel the stream and record the gas used. + uint256 gasBefore = gasleft(); lockup.cancel(currentStreamId); + uint256 gasAfter = gasleft(); + + lockupStore.recordGasUsage({ streamId: currentStreamId, action: StreamAction.CANCEL, gas: gasBefore - gasAfter }); } function renounce( @@ -130,8 +135,7 @@ contract LockupHandler is BaseHandler { uint256 timeJumpSeed, uint256 streamIndexSeed, address to, - uint128 withdrawAmount, - bool payFee + uint128 withdrawAmount ) external instrument("withdraw") @@ -160,19 +164,22 @@ contract LockupHandler is BaseHandler { to = currentRecipient; } - // Withdraw from the stream. - lockup.withdraw{ value: payFee ? FEE : 0 }({ streamId: currentStreamId, to: to, amount: withdrawAmount }); - } + // Withdraw from the stream and record the gas used. + uint256 gasBefore = gasleft(); + lockup.withdraw{ value: LOCKUP_MIN_FEE_WEI }({ streamId: currentStreamId, to: to, amount: withdrawAmount }); + uint256 gasAfter = gasleft(); - function collectFees() external instrument("collectFees") { - lockup.collectFees(); + lockupStore.recordGasUsage({ + streamId: currentStreamId, + action: StreamAction.WITHDRAW, + gas: gasBefore - gasAfter + }); } function withdrawMax( uint256 timeJumpSeed, uint256 streamIndexSeed, - address to, - bool payFee + address to ) external instrument("withdrawMax") @@ -197,15 +204,22 @@ contract LockupHandler is BaseHandler { to = currentRecipient; } - // Make the max withdrawal. - lockup.withdrawMax{ value: payFee ? FEE : 0 }({ streamId: currentStreamId, to: to }); + // Make the max withdrawal and record the gas used. + uint256 gasBefore = gasleft(); + lockup.withdrawMax{ value: LOCKUP_MIN_FEE_WEI }({ streamId: currentStreamId, to: to }); + uint256 gasAfter = gasleft(); + + lockupStore.recordGasUsage({ + streamId: currentStreamId, + action: StreamAction.WITHDRAW, + gas: gasBefore - gasAfter + }); } function withdrawMaxAndTransfer( uint256 timeJumpSeed, uint256 streamIndexSeed, - address newRecipient, - bool payFee + address newRecipient ) external instrument("withdrawMaxAndTransfer") @@ -230,7 +244,7 @@ contract LockupHandler is BaseHandler { vm.assume(lockup.withdrawableAmountOf(currentStreamId) != 0); // Make the max withdrawal and transfer the NFT. - lockup.withdrawMaxAndTransfer{ value: payFee ? FEE : 0 }({ + lockup.withdrawMaxAndTransfer{ value: LOCKUP_MIN_FEE_WEI }({ streamId: currentStreamId, newRecipient: newRecipient }); diff --git a/tests/invariant/stores/LockupStore.sol b/tests/invariant/stores/LockupStore.sol index 0314ac96a..70828639c 100644 --- a/tests/invariant/stores/LockupStore.sol +++ b/tests/invariant/stores/LockupStore.sol @@ -1,7 +1,8 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { Lockup } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; +import { StreamAction } from "tests/utils/Types.sol"; /// @dev Storage variables needed by all lockup handlers. contract LockupStore { @@ -9,12 +10,14 @@ contract LockupStore { VARIABLES //////////////////////////////////////////////////////////////////////////*/ - mapping(uint256 streamId => bool recorded) public isPreviousStatusRecorded; uint256 public lastStreamId; + uint256[] public streamIds; + + mapping(uint256 streamId => mapping(StreamAction action => uint256 gas)) public gasUsed; + mapping(uint256 streamId => bool recorded) public isPreviousStatusRecorded; mapping(uint256 streamId => Lockup.Status status) public previousStatusOf; mapping(uint256 streamId => address recipient) public recipients; mapping(uint256 streamId => address sender) public senders; - uint256[] public streamIds; /*////////////////////////////////////////////////////////////////////////// HELPERS @@ -30,6 +33,14 @@ contract LockupStore { lastStreamId = streamId; } + /// @dev Records gas used by an action. + function recordGasUsage(uint256 streamId, StreamAction action, uint256 gas) external { + // We want to store the maximum gas used by any action. + if (gas > gasUsed[streamId][action]) { + gasUsed[streamId][action] = gas; + } + } + function updateIsPreviousStatusRecorded(uint256 streamId) external { isPreviousStatusRecorded[streamId] = true; } diff --git a/tests/mocks/AdminableMock.sol b/tests/mocks/AdminableMock.sol deleted file mode 100644 index b262f066d..000000000 --- a/tests/mocks/AdminableMock.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.22; - -import { Adminable } from "src/abstracts/Adminable.sol"; - -contract AdminableMock is Adminable { - constructor(address initialAdmin) Adminable(initialAdmin) { } -} diff --git a/tests/mocks/BatchMock.sol b/tests/mocks/BatchMock.sol deleted file mode 100644 index 84e3bd902..000000000 --- a/tests/mocks/BatchMock.sol +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.22; - -import { Batch } from "src/abstracts/Batch.sol"; - -contract BatchMock is Batch { - error InvalidNumber(uint256); - - uint256 internal _number = 42; - - // A view only function. - function getNumber() public view returns (uint256) { - return _number; - } - - // A view only function that reverts. - function getNumberAndRevert() public pure returns (uint256) { - revert InvalidNumber(1); - } - - // A state changing function with no payable modifier and no return value. - function setNumber(uint256 number) public { - _number = number; - } - - // A state changing function with a payable modifier and no return value. - function setNumberWithPayable(uint256 number) public payable { - _number = number; - } - - // A state changing function with a payable modifier and a return value. - function setNumberWithPayableAndReturn(uint256 number) public payable returns (uint256) { - _number = number; - return _number; - } - - // A state changing function with a payable modifier, which reverts with a custom error. - function setNumberWithPayableAndRevertError(uint256 number) public payable { - _number = number; - revert InvalidNumber(number); - } - - // A state changing function with a payable modifier, which reverts with a reason string. - function setNumberWithPayableAndRevertString(uint256 number) public payable { - _number = number; - revert("You cannot pass"); - } -} diff --git a/tests/mocks/Hooks.sol b/tests/mocks/Hooks.sol index 43ebbd549..a11845cbb 100644 --- a/tests/mocks/Hooks.sol +++ b/tests/mocks/Hooks.sol @@ -3,7 +3,7 @@ pragma solidity >=0.8.22; import { IERC165, ERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; -import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; +import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; import { ISablierLockupRecipient } from "src/interfaces/ISablierLockupRecipient.sol"; contract RecipientGood is ISablierLockupRecipient, ERC165 { @@ -138,7 +138,10 @@ contract RecipientReentrant is ISablierLockupRecipient, ERC165 { senderAmount; recipientAmount; - ISablierLockupBase(msg.sender).withdraw(streamId, address(this), recipientAmount); + uint256 feeUSD = 1e8; + uint256 feeWei = (1e18 * feeUSD) / 3000e8; + + ISablierLockup(msg.sender).withdraw{ value: feeWei }(streamId, address(this), recipientAmount); return ISablierLockupRecipient.onSablierLockupCancel.selector; } @@ -158,7 +161,10 @@ contract RecipientReentrant is ISablierLockupRecipient, ERC165 { to; amount; - ISablierLockupBase(msg.sender).withdraw(streamId, address(this), amount); + uint256 feeUSD = 1e8; + uint256 feeWei = (1e18 * feeUSD) / 3000e8; + + ISablierLockup(msg.sender).withdraw{ value: feeWei }(streamId, address(this), amount); return ISablierLockupRecipient.onSablierLockupWithdraw.selector; } diff --git a/tests/mocks/NFTDescriptorMock.sol b/tests/mocks/NFTDescriptorMock.sol index 47a387d4c..0c52987ea 100644 --- a/tests/mocks/NFTDescriptorMock.sol +++ b/tests/mocks/NFTDescriptorMock.sol @@ -4,7 +4,7 @@ pragma solidity >=0.8.22; import { NFTSVG } from "src/libraries/NFTSVG.sol"; import { SVGElements } from "src/libraries/SVGElements.sol"; import { LockupNFTDescriptor } from "src/LockupNFTDescriptor.sol"; -import { Lockup } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; /// @dev This mock is needed for: /// - Running the tests against optimized contracts compiled with `--via-ir` diff --git a/tests/mocks/Receive.sol b/tests/mocks/Receive.sol deleted file mode 100644 index 31a8fa96b..000000000 --- a/tests/mocks/Receive.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.22 <0.9.0; - -contract ContractWithoutReceive { } - -contract ContractWithReceive { - receive() external payable { } -} diff --git a/tests/mocks/erc20/ERC20Bytes32.sol b/tests/mocks/erc20/ERC20Bytes32.sol deleted file mode 100644 index 50b065903..000000000 --- a/tests/mocks/erc20/ERC20Bytes32.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.22; - -contract ERC20Bytes32 { - function symbol() external pure returns (bytes32) { - return bytes32("ERC20"); - } -} diff --git a/tests/mocks/erc20/ERC20MissingReturn.sol b/tests/mocks/erc20/ERC20MissingReturn.sol deleted file mode 100644 index 0f9d548e7..000000000 --- a/tests/mocks/erc20/ERC20MissingReturn.sol +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.22; - -/// @notice An implementation of ERC-20 that does not return a boolean in {transfer} and {transferFrom}. -/// @dev See https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca/. -contract ERC20MissingReturn { - uint8 public decimals; - string public name; - string public symbol; - uint256 public totalSupply; - - mapping(address owner => mapping(address spender => uint256 allowance)) internal _allowances; - mapping(address account => uint256 balance) internal _balances; - - event Transfer(address indexed from, address indexed to, uint256 amount); - - event Approval(address indexed owner, address indexed spender, uint256 amount); - - constructor(string memory name_, string memory symbol_, uint8 decimals_) { - name = name_; - symbol = symbol_; - decimals = decimals_; - } - - function allowance(address owner, address spender) public view returns (uint256) { - return _allowances[owner][spender]; - } - - function balanceOf(address account) public view returns (uint256) { - return _balances[account]; - } - - function approve(address spender, uint256 value) public returns (bool) { - _approve(msg.sender, spender, value); - return true; - } - - function burn(address holder, uint256 amount) public { - _balances[holder] -= amount; - totalSupply -= amount; - emit Transfer(holder, address(0), amount); - } - - function mint(address beneficiary, uint256 amount) public { - _balances[beneficiary] += amount; - totalSupply += amount; - emit Transfer(address(0), beneficiary, amount); - } - - function _approve(address owner, address spender, uint256 value) internal virtual { - _allowances[owner][spender] = value; - emit Approval(owner, spender, value); - } - - /// @dev This function does not return a value, although the ERC-20 standard mandates that it should. - function transfer(address to, uint256 amount) public { - _transfer(msg.sender, to, amount); - } - - /// @dev This function does not return a value, although the ERC-20 standard mandates that it should. - function transferFrom(address from, address to, uint256 amount) public { - _transfer(from, to, amount); - _approve(from, msg.sender, _allowances[from][msg.sender] - amount); - } - - function _transfer(address from, address to, uint256 amount) internal virtual { - _balances[from] = _balances[from] - amount; - _balances[to] = _balances[to] + amount; - emit Transfer(from, to, amount); - } -} diff --git a/tests/mocks/erc20/ERC20Mock.sol b/tests/mocks/erc20/ERC20Mock.sol deleted file mode 100644 index 8cf2389dd..000000000 --- a/tests/mocks/erc20/ERC20Mock.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.22; - -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - -contract ERC20Mock is ERC20 { - constructor(string memory name, string memory symbol) ERC20(name, symbol) { } -} diff --git a/tests/unit/concrete/adminable/transfer-admin/transferAdmin.t.sol b/tests/unit/concrete/adminable/transfer-admin/transferAdmin.t.sol deleted file mode 100644 index f058a2aac..000000000 --- a/tests/unit/concrete/adminable/transfer-admin/transferAdmin.t.sol +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.22 <0.9.0; - -import { IAdminable } from "src/interfaces/IAdminable.sol"; -import { Errors } from "src/libraries/Errors.sol"; - -import { Adminable_Unit_Shared_Test } from "../../../shared/Adminable.t.sol"; - -contract TransferAdmin_Unit_Concrete_Test is Adminable_Unit_Shared_Test { - function test_RevertWhen_CallerNotAdmin() external { - // Make Eve the caller in this test. - resetPrank(users.eve); - - // Run the test. - vm.expectRevert(abi.encodeWithSelector(Errors.CallerNotAdmin.selector, users.admin, users.eve)); - adminableMock.transferAdmin(users.eve); - } - - function test_WhenNewAdminSameAsCurrentAdmin() external whenCallerAdmin { - // It should emit a {TransferAdmin} event. - vm.expectEmit({ emitter: address(adminableMock) }); - emit IAdminable.TransferAdmin({ oldAdmin: users.admin, newAdmin: users.admin }); - - // Transfer the admin. - adminableMock.transferAdmin(users.admin); - - // It should keep the same admin. - address actualAdmin = adminableMock.admin(); - address expectedAdmin = users.admin; - assertEq(actualAdmin, expectedAdmin, "admin"); - } - - function test_WhenNewAdminZeroAddress() external whenCallerAdmin whenNewAdminNotSameAsCurrentAdmin { - // It should emit a {TransferAdmin}. - vm.expectEmit({ emitter: address(adminableMock) }); - emit IAdminable.TransferAdmin({ oldAdmin: users.admin, newAdmin: address(0) }); - - // Transfer the admin. - adminableMock.transferAdmin(address(0)); - - // It should set the admin to the zero address. - address actualAdmin = adminableMock.admin(); - address expectedAdmin = address(0); - assertEq(actualAdmin, expectedAdmin, "admin"); - } - - function test_WhenNewAdminNotZeroAddress() external whenCallerAdmin whenNewAdminNotSameAsCurrentAdmin { - // It should emit a {TransferAdmin} event. - vm.expectEmit({ emitter: address(adminableMock) }); - emit IAdminable.TransferAdmin({ oldAdmin: users.admin, newAdmin: users.alice }); - - // Transfer the admin. - adminableMock.transferAdmin(users.alice); - - // It should set the new admin. - address actualAdmin = adminableMock.admin(); - address expectedAdmin = users.alice; - assertEq(actualAdmin, expectedAdmin, "admin"); - } -} diff --git a/tests/unit/concrete/adminable/transfer-admin/transferAdmin.tree b/tests/unit/concrete/adminable/transfer-admin/transferAdmin.tree deleted file mode 100644 index 81d14c3f4..000000000 --- a/tests/unit/concrete/adminable/transfer-admin/transferAdmin.tree +++ /dev/null @@ -1,14 +0,0 @@ -TransferAdmin_Unit_Concrete_Test -├── when caller not admin -│ └── it should revert -└── when caller admin - ├── when new admin same as current admin - │ ├── it should keep the same admin - │ └── it should emit a {TransferAdmin} event - └── when new admin not same as current admin - ├── when new admin zero address - │ ├── it should set the admin to the zero address - │ └── it should emit a {TransferAdmin} - └── when new admin not zero address - ├── it should set the new admin - └── it should emit a {TransferAdmin} event diff --git a/tests/unit/concrete/batch/batch.t.sol b/tests/unit/concrete/batch/batch.t.sol deleted file mode 100644 index a32bc5f67..000000000 --- a/tests/unit/concrete/batch/batch.t.sol +++ /dev/null @@ -1,139 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.22; - -import { Base_Test } from "../../../Base.t.sol"; -import { BatchMock } from "../../../mocks/BatchMock.sol"; - -contract Batch_Unit_Concrete_Test is Base_Test { - BatchMock internal batchMock; - bytes[] internal calls; - uint256 internal newNumber = 100; - bytes[] internal results; - - function setUp() public virtual override { - Base_Test.setUp(); - - batchMock = new BatchMock(); - } - - function test_RevertWhen_FunctionDoesNotExist() external { - calls = new bytes[](1); - calls[0] = abi.encodeWithSignature("nonExistentFunction()"); - - // It should revert. - vm.expectRevert(bytes("")); - batchMock.batch(calls); - } - - modifier whenFunctionExists() { - _; - } - - modifier whenNonStateChangingFunction() { - _; - } - - function test_RevertWhen_FunctionReverts() external whenFunctionExists whenNonStateChangingFunction { - calls = new bytes[](1); - calls[0] = abi.encodeCall(batchMock.getNumberAndRevert, ()); - - // It should revert. - vm.expectRevert(abi.encodeWithSelector(BatchMock.InvalidNumber.selector, 1)); - batchMock.batch(calls); - } - - function test_WhenFunctionNotRevert() external whenFunctionExists whenNonStateChangingFunction { - calls = new bytes[](1); - calls[0] = abi.encodeCall(batchMock.getNumber, ()); - results = batchMock.batch(calls); - - // It should return the expected value. - assertEq(results.length, 1, "batch results length"); - assertEq(abi.decode(results[0], (uint256)), 42, "batch results[0]"); - } - - modifier whenStateChangingFunction() { - _; - } - - modifier whenNotPayable() { - _; - } - - function test_RevertWhen_BatchIncludesETHValue() - external - whenFunctionExists - whenStateChangingFunction - whenNotPayable - { - calls = new bytes[](1); - calls[0] = abi.encodeCall(batchMock.setNumber, (newNumber)); - - // It should revert. - vm.expectRevert(bytes("")); - batchMock.batch{ value: 1 wei }(calls); - } - - function test_WhenBatchNotIncludeETHValue() external whenFunctionExists whenStateChangingFunction whenNotPayable { - calls = new bytes[](1); - calls[0] = abi.encodeCall(batchMock.setNumber, (newNumber)); - - results = batchMock.batch(calls); - - // It should return the empty string. - assertEq(results.length, 1, "batch results length"); - assertEq(results[0], "", "batch results[0]"); - } - - modifier whenPayable() { - _; - } - - function test_RevertWhen_FunctionRevertsWithCustomError() - external - whenFunctionExists - whenStateChangingFunction - whenPayable - { - calls = new bytes[](1); - calls[0] = abi.encodeCall(batchMock.setNumberWithPayableAndRevertError, (newNumber)); - - // It should revert. - vm.expectRevert(abi.encodeWithSelector(BatchMock.InvalidNumber.selector, newNumber)); - batchMock.batch{ value: 1 wei }(calls); - } - - function test_RevertWhen_FunctionRevertsWithStringError() - external - whenFunctionExists - whenStateChangingFunction - whenPayable - { - calls = new bytes[](1); - calls[0] = abi.encodeCall(batchMock.setNumberWithPayableAndRevertString, (newNumber)); - - // It should revert. - vm.expectRevert("You cannot pass"); - batchMock.batch{ value: 1 wei }(calls); - } - - function test_WhenFunctionReturnsAValue() external whenFunctionExists whenStateChangingFunction whenPayable { - calls = new bytes[](1); - calls[0] = abi.encodeCall(batchMock.setNumberWithPayableAndReturn, (newNumber)); - results = batchMock.batch{ value: 1 wei }(calls); - - // It should return expected value. - assertEq(results.length, 1, "batch results length"); - assertEq(abi.decode(results[0], (uint256)), newNumber, "batch results[0]"); - } - - function test_WhenFunctionDoesNotReturnAValue() external whenFunctionExists whenStateChangingFunction whenPayable { - calls = new bytes[](1); - calls[0] = abi.encodeCall(batchMock.setNumberWithPayable, (newNumber)); - results = batchMock.batch{ value: 1 wei }(calls); - - // It should return an empty value. - assertEq(results.length, 1, "batch results length"); - assertEq(results[0], "", "batch results[0]"); - } -} diff --git a/tests/unit/concrete/batch/batch.tree b/tests/unit/concrete/batch/batch.tree deleted file mode 100644 index 35501ea18..000000000 --- a/tests/unit/concrete/batch/batch.tree +++ /dev/null @@ -1,24 +0,0 @@ -Batch_Unit_Concrete_Test -├── when function does not exist -│ └── it should revert -└── when function exists - ├── when non state changing function - │ ├── when function reverts - │ │ └── it should revert - │ └── when function not revert - │ └── it should return expected value - └── when state changing function - ├── when not payable - │ ├── when batch includes ETH value - │ │ └── it should revert - │ └── when batch not include ETH value - │ └── it should return empty value - └── when payable - ├── when function reverts with custom error - │ └── it should revert - ├── when function reverts with string error - │ └── it should revert - ├── when function returns a value - │ └── it should return expected value - └── when function does not return a value - └── it should return empty value diff --git a/tests/unit/concrete/nft-descriptor/stringifyStatus.t.sol b/tests/unit/concrete/nft-descriptor/stringifyStatus.t.sol index 4a4fee282..60379df75 100644 --- a/tests/unit/concrete/nft-descriptor/stringifyStatus.t.sol +++ b/tests/unit/concrete/nft-descriptor/stringifyStatus.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { Lockup } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/Lockup.sol"; import { Base_Test } from "tests/Base.t.sol"; diff --git a/tests/unit/fuzz/transferAdmin.t.sol b/tests/unit/fuzz/transferAdmin.t.sol deleted file mode 100644 index 89d82d5ad..000000000 --- a/tests/unit/fuzz/transferAdmin.t.sol +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.22 <0.9.0; - -import { IAdminable } from "src/interfaces/IAdminable.sol"; -import { Errors } from "src/libraries/Errors.sol"; - -import { Adminable_Unit_Shared_Test } from "../shared/Adminable.t.sol"; - -contract TransferAdmin_Unit_Fuzz_Test is Adminable_Unit_Shared_Test { - function testFuzz_RevertWhen_CallerNotAdmin(address eve) external { - vm.assume(eve != address(0) && eve != users.admin); - assumeNotPrecompile(eve); - - // Make Eve the caller in this test. - resetPrank(eve); - - // Run the test. - vm.expectRevert(abi.encodeWithSelector(Errors.CallerNotAdmin.selector, users.admin, eve)); - adminableMock.transferAdmin(eve); - } - - function testFuzz_TransferAdmin(address newAdmin) external whenCallerAdmin { - vm.assume(newAdmin != address(0)); - - // Expect the relevant event to be emitted. - vm.expectEmit({ emitter: address(adminableMock) }); - emit IAdminable.TransferAdmin({ oldAdmin: users.admin, newAdmin: newAdmin }); - - // Transfer the admin. - adminableMock.transferAdmin(newAdmin); - - // Assert that the admin has been transferred. - address actualAdmin = adminableMock.admin(); - address expectedAdmin = newAdmin; - assertEq(actualAdmin, expectedAdmin, "admin"); - } -} diff --git a/tests/unit/shared/Adminable.t.sol b/tests/unit/shared/Adminable.t.sol deleted file mode 100644 index 9a0ffe2b8..000000000 --- a/tests/unit/shared/Adminable.t.sol +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.22 <0.9.0; - -import { Base_Test } from "../../Base.t.sol"; -import { AdminableMock } from "../../mocks/AdminableMock.sol"; - -abstract contract Adminable_Unit_Shared_Test is Base_Test { - AdminableMock internal adminableMock; - - function setUp() public virtual override { - Base_Test.setUp(); - deployConditionally(); - resetPrank({ msgSender: users.admin }); - } - - /// @dev Conditionally deploys {AdminableMock} normally or from a source precompiled with `--via-ir`. - function deployConditionally() internal { - if (!isTestOptimizedProfile()) { - adminableMock = new AdminableMock(users.admin); - } else { - adminableMock = - AdminableMock(deployCode("out-optimized/AdminableMock.sol/AdminableMock.json", abi.encode(users.admin))); - } - vm.label({ account: address(adminableMock), newLabel: "AdminableMock" }); - } -} diff --git a/tests/utils/Assertions.sol b/tests/utils/Assertions.sol index bdf80f1cf..cbe35851b 100644 --- a/tests/utils/Assertions.sol +++ b/tests/utils/Assertions.sol @@ -1,11 +1,15 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// solhint-disable event-name-camelcase +// solhint-disable event-name-capwords pragma solidity >=0.8.22; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { PRBMathAssertions } from "@prb/math/test/utils/Assertions.sol"; -import { Lockup, LockupDynamic, LockupTranched } from "../../src/types/DataTypes.sol"; +import { ISablierLockup } from "../../src/interfaces/ISablierLockup.sol"; +import { Lockup } from "../../src/types/Lockup.sol"; +import { LockupDynamic } from "../../src/types/LockupDynamic.sol"; +import { LockupLinear } from "../../src/types/LockupLinear.sol"; +import { LockupTranched } from "../../src/types/LockupTranched.sol"; abstract contract Assertions is PRBMathAssertions { /*////////////////////////////////////////////////////////////////////////// @@ -27,11 +31,6 @@ abstract contract Assertions is PRBMathAssertions { assertEq(a.withdrawn, b.withdrawn, "amounts.withdrawn"); } - /// @dev Compares two {IERC20} values. - function assertEq(IERC20 a, IERC20 b) internal pure { - assertEq(address(a), address(b)); - } - /// @dev Compares two {IERC20} values. function assertEq(IERC20 a, IERC20 b, string memory err) internal pure { assertEq(address(a), address(b), err); @@ -39,7 +38,7 @@ abstract contract Assertions is PRBMathAssertions { /// @dev Compares two {Lockup.Model} enum values. function assertEq(Lockup.Model a, Lockup.Model b) internal pure { - assertEq(uint8(a), uint8(b), "lockup model"); + assertEq(uint256(a), uint256(b), "Lockup.Model"); } /// @dev Compares two {Lockup.Timestamps} struct entities. @@ -58,6 +57,12 @@ abstract contract Assertions is PRBMathAssertions { } } + /// @dev Compares two {LockupLinear.UnlockAmounts} structs. + function assertEq(LockupLinear.UnlockAmounts memory a, LockupLinear.UnlockAmounts memory b) internal pure { + assertEq(a.start, b.start, "unlockAmounts.start"); + assertEq(a.cliff, b.cliff, "unlockAmounts.cliff"); + } + /// @dev Compares two {LockupTranched.Tranche} arrays. function assertEq(LockupTranched.Tranche[] memory a, LockupTranched.Tranche[] memory b) internal { if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) { @@ -87,4 +92,28 @@ abstract contract Assertions is PRBMathAssertions { function assertNotEq(Lockup.Status a, Lockup.Status b, string memory err) internal pure { assertNotEq(uint256(a), uint256(b), err); } + + /// @dev Compares common states between models with {Lockup.CreateWithTimestamps} parameters for a given stream ID. + function assertEq( + ISablierLockup lockup, + uint256 streamId, + Lockup.CreateWithTimestamps memory expectedLockup + ) + internal + view + { + assertEq(lockup.getDepositedAmount(streamId), expectedLockup.depositAmount, "depositedAmount"); + assertEq(lockup.getEndTime(streamId), expectedLockup.timestamps.end, "endTime"); + assertEq(lockup.getRecipient(streamId), expectedLockup.recipient, "recipient"); + assertEq(lockup.getSender(streamId), expectedLockup.sender, "sender"); + assertEq(lockup.getStartTime(streamId), expectedLockup.timestamps.start, "startTime"); + assertEq(lockup.getUnderlyingToken(streamId), expectedLockup.token, "underlyingToken"); + assertEq(lockup.getWithdrawnAmount(streamId), 0, "withdrawnAmount"); + assertFalse(lockup.isDepleted(streamId), "isDepleted"); + assertTrue(lockup.isStream(streamId), "isStream"); + assertEq(lockup.isTransferable(streamId), expectedLockup.transferable, "isTransferable"); + assertEq(lockup.nextStreamId(), streamId + 1, "post-create nextStreamId"); + assertFalse(lockup.wasCanceled(streamId), "wasCanceled"); + assertEq(lockup.ownerOf(streamId), expectedLockup.recipient, "post-create NFT owner"); + } } diff --git a/tests/utils/BaseScript.t.sol b/tests/utils/BaseScript.t.sol index f553b9f21..572ac31b9 100644 --- a/tests/utils/BaseScript.t.sol +++ b/tests/utils/BaseScript.t.sol @@ -2,25 +2,26 @@ pragma solidity >=0.8.22 <0.9.0; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { BaseScript } from "@sablier/evm-utils/src/tests/BaseScript.sol"; import { StdAssertions } from "forge-std/src/StdAssertions.sol"; -import { BaseScript } from "script/Base.s.sol"; +contract BaseScriptMock is BaseScript { } contract BaseScript_Test is StdAssertions { using Strings for uint256; - BaseScript internal baseScript; + BaseScriptMock internal baseScript; function setUp() public { - baseScript = new BaseScript(); + baseScript = new BaseScriptMock(); } function test_ConstructCreate2Salt() public view { string memory chainId = block.chainid.toString(); - string memory version = "2.0.1"; + string memory version = "3.0.1"; string memory salt = string.concat("ChainID ", chainId, ", Version ", version); - bytes32 actualSalt = baseScript.constructCreate2Salt(); + bytes32 actualSalt = baseScript.SALT(); bytes32 expectedSalt = bytes32(abi.encodePacked(salt)); assertEq(actualSalt, expectedSalt, "CREATE2 salt mismatch"); } diff --git a/tests/utils/BatchLockupBuilder.sol b/tests/utils/BatchLockupBuilder.sol index eba8734a5..fff0233d4 100644 --- a/tests/utils/BatchLockupBuilder.sol +++ b/tests/utils/BatchLockupBuilder.sol @@ -1,7 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity >=0.8.22; -import { BatchLockup, Lockup, LockupDynamic, LockupLinear, LockupTranched } from "../../src/types/DataTypes.sol"; +import { BatchLockup } from "../../src/types/BatchLockup.sol"; +import { Lockup } from "../../src/types/Lockup.sol"; +import { LockupDynamic } from "../../src/types/LockupDynamic.sol"; +import { LockupLinear } from "../../src/types/LockupLinear.sol"; +import { LockupTranched } from "../../src/types/LockupTranched.sol"; library BatchLockupBuilder { /// @notice Generates an array containing `batchSize` copies of `batchSingle`. @@ -33,12 +37,11 @@ library BatchLockupBuilder { BatchLockup.CreateWithDurationsLD memory batchSingle = BatchLockup.CreateWithDurationsLD({ sender: params.sender, recipient: params.recipient, - totalAmount: params.totalAmount, + depositAmount: params.depositAmount, cancelable: params.cancelable, transferable: params.transferable, segmentsWithDuration: segmentsWithDurations, - shape: params.shape, - broker: params.broker + shape: params.shape }); batch = fillBatch(batchSingle, batchSize); } @@ -73,13 +76,12 @@ library BatchLockupBuilder { BatchLockup.CreateWithDurationsLL memory batchSingle = BatchLockup.CreateWithDurationsLL({ sender: params.sender, recipient: params.recipient, - totalAmount: params.totalAmount, + depositAmount: params.depositAmount, cancelable: params.cancelable, transferable: params.transferable, durations: durations, unlockAmounts: unlockAmounts, - shape: params.shape, - broker: params.broker + shape: params.shape }); batch = fillBatch(batchSingle, batchSize); } @@ -113,12 +115,11 @@ library BatchLockupBuilder { BatchLockup.CreateWithDurationsLT memory batchSingle = BatchLockup.CreateWithDurationsLT({ sender: params.sender, recipient: params.recipient, - totalAmount: params.totalAmount, + depositAmount: params.depositAmount, cancelable: params.cancelable, transferable: params.transferable, tranchesWithDuration: tranchesWithDuration, - shape: params.shape, - broker: params.broker + shape: params.shape }); batch = fillBatch(batchSingle, batchSize); } @@ -152,13 +153,12 @@ library BatchLockupBuilder { BatchLockup.CreateWithTimestampsLD memory batchSingle = BatchLockup.CreateWithTimestampsLD({ sender: params.sender, recipient: params.recipient, - totalAmount: params.totalAmount, + depositAmount: params.depositAmount, cancelable: params.cancelable, transferable: params.transferable, startTime: params.timestamps.start, segments: segments, - shape: params.shape, - broker: params.broker + shape: params.shape }); batch = fillBatch(batchSingle, batchSize); } @@ -193,14 +193,13 @@ library BatchLockupBuilder { BatchLockup.CreateWithTimestampsLL memory batchSingle = BatchLockup.CreateWithTimestampsLL({ sender: params.sender, recipient: params.recipient, - totalAmount: params.totalAmount, + depositAmount: params.depositAmount, cancelable: params.cancelable, transferable: params.transferable, timestamps: params.timestamps, cliffTime: cliffTime, unlockAmounts: unlockAmounts, - shape: params.shape, - broker: params.broker + shape: params.shape }); batch = fillBatch(batchSingle, batchSize); } @@ -234,13 +233,12 @@ library BatchLockupBuilder { BatchLockup.CreateWithTimestampsLT memory batchSingle = BatchLockup.CreateWithTimestampsLT({ sender: params.sender, recipient: params.recipient, - totalAmount: params.totalAmount, + depositAmount: params.depositAmount, cancelable: params.cancelable, transferable: params.transferable, startTime: params.timestamps.start, tranches: tranches, - shape: params.shape, - broker: params.broker + shape: params.shape }); batch = fillBatch(batchSingle, batchSize); } diff --git a/tests/utils/Calculations.sol b/tests/utils/Calculations.sol index 618878a10..cf3592faa 100644 --- a/tests/utils/Calculations.sol +++ b/tests/utils/Calculations.sol @@ -5,21 +5,18 @@ import { PRBMathCastingUint128 as CastingUint128 } from "@prb/math/src/casting/U import { PRBMathCastingUint40 as CastingUint40 } from "@prb/math/src/casting/Uint40.sol"; import { SD59x18 } from "@prb/math/src/SD59x18.sol"; import { UD60x18, ud } from "@prb/math/src/UD60x18.sol"; +import { BaseUtils } from "@sablier/evm-utils/src/tests/BaseUtils.sol"; -import { LockupDynamic, LockupLinear, LockupTranched } from "../../src/types/DataTypes.sol"; +import { LockupDynamic } from "../../src/types/LockupDynamic.sol"; +import { LockupLinear } from "../../src/types/LockupLinear.sol"; +import { LockupTranched } from "../../src/types/LockupTranched.sol"; -abstract contract Calculations { +abstract contract Calculations is BaseUtils { using CastingUint128 for uint128; using CastingUint40 for uint40; - /// @dev Calculates the deposit amount by calculating and subtracting the broker fee amount from the total amount. - function calculateDepositAmount(uint128 totalAmount, UD60x18 brokerFee) internal pure returns (uint128) { - uint128 brokerFeeAmount = ud(totalAmount).mul(brokerFee).intoUint128(); - return totalAmount - brokerFeeAmount; - } - - /// @dev Replicates the logic of {VestingMath.calculateLockupDynamicStreamedAmount}. - function calculateLockupDynamicStreamedAmount( + /// @dev Replicates the logic of {LockupMath._calculateStreamedAmountLD}. + function calculateStreamedAmountLD( LockupDynamic.Segment[] memory segments, uint40 startTime, uint128 depositAmount @@ -28,7 +25,12 @@ abstract contract Calculations { view returns (uint128) { - uint40 blockTimestamp = uint40(block.timestamp); + uint40 blockTimestamp = getBlockTimestamp(); + + if (startTime >= blockTimestamp) { + return 0; + } + if (blockTimestamp >= segments[segments.length - 1].timestamp) { return depositAmount; } @@ -64,8 +66,8 @@ abstract contract Calculations { } } - /// @dev Helper function that replicates the logic of {VestingMath.calculateLockupLinearStreamedAmount}. - function calculateLockupLinearStreamedAmount( + /// @dev Replicates the logic of {LockupMath._calculateStreamedAmountLL}. + function calculateStreamedAmountLL( uint40 startTime, uint40 cliffTime, uint40 endTime, @@ -76,17 +78,17 @@ abstract contract Calculations { view returns (uint128) { - uint40 blockTimestamp = uint40(block.timestamp); + uint40 blockTimestamp = getBlockTimestamp(); if (startTime >= blockTimestamp) { return 0; } - if (blockTimestamp >= endTime) { - return depositAmount; - } if (cliffTime > blockTimestamp) { return unlockAmounts.start; } + if (blockTimestamp >= endTime) { + return depositAmount; + } unchecked { UD60x18 unlockAmountsSum = ud(unlockAmounts.start).add(ud(unlockAmounts.cliff)); @@ -105,8 +107,8 @@ abstract contract Calculations { } } - /// @dev Helper function that replicates the logic of {VestingMath.calculateLockupTranchedStreamedAmount}. - function calculateLockupTranchedStreamedAmount( + /// @dev Replicates the logic of {LockupMath._calculateStreamedAmountLT}. + function calculateStreamedAmountLT( LockupTranched.Tranche[] memory tranches, uint128 depositAmount ) @@ -114,7 +116,11 @@ abstract contract Calculations { view returns (uint128) { - uint40 blockTimestamp = uint40(block.timestamp); + uint40 blockTimestamp = getBlockTimestamp(); + + if (tranches[0].timestamp > blockTimestamp) { + return 0; + } if (blockTimestamp >= tranches[tranches.length - 1].timestamp) { return depositAmount; } diff --git a/tests/utils/Constants.sol b/tests/utils/Constants.sol index 1f58a8638..74cf5921d 100644 --- a/tests/utils/Constants.sol +++ b/tests/utils/Constants.sol @@ -1,14 +1,30 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity >=0.8.22; -import { UD60x18 } from "@prb/math/src/UD60x18.sol"; - abstract contract Constants { - uint256 internal constant FEE = 0.001e18; - uint40 internal constant JULY_1_2024 = 1_719_792_000; - UD60x18 internal constant MAX_BROKER_FEE = UD60x18.wrap(0.1e18); // 10% - uint128 internal constant MAX_UINT128 = type(uint128).max; - uint256 internal constant MAX_UINT256 = type(uint256).max; - uint40 internal constant MAX_UINT40 = type(uint40).max; - uint40 internal constant MAX_UNIX_TIMESTAMP = 2_147_483_647; // 2^31 - 1 + /*////////////////////////////////////////////////////////////////////////// + CONSTANTS + //////////////////////////////////////////////////////////////////////////*/ + + uint64 public constant BATCH_SIZE = 10; + uint128 public constant CLIFF_AMOUNT = 2500e18 + 2534; + uint128 public constant CLIFF_AMOUNT_6D = CLIFF_AMOUNT / 1e12; + uint40 public constant CLIFF_DURATION = 2500 seconds; + uint40 public constant CLIFF_TIME = START_TIME + CLIFF_DURATION; + uint128 public constant DEPOSIT_AMOUNT = 10_000e18; + uint128 public constant DEPOSIT_AMOUNT_6D = 10_000e6; + uint40 public constant END_TIME = START_TIME + TOTAL_DURATION; + uint40 public constant FEB_1_2025 = 1_738_368_000; + uint128 public constant REFUND_AMOUNT = DEPOSIT_AMOUNT - WITHDRAW_AMOUNT; + uint256 public constant SEGMENT_COUNT = 2; + string public constant SHAPE = "emits in the event"; + uint128 public constant START_AMOUNT = 0; + uint40 public constant START_TIME = FEB_1_2025 + 2 days; + uint128 public constant STREAMED_AMOUNT_26_PERCENT = 2600e18; + uint40 public constant TOTAL_DURATION = 10_000 seconds; + uint128 public constant TOTAL_TRANSFER_AMOUNT = DEPOSIT_AMOUNT * uint128(BATCH_SIZE); + uint256 public constant TRANCHE_COUNT = 2; + uint40 public constant WARP_26_PERCENT = START_TIME + WARP_26_PERCENT_DURATION; + uint40 public constant WARP_26_PERCENT_DURATION = 2600 seconds; // 26% of the way through the stream + uint128 public constant WITHDRAW_AMOUNT = STREAMED_AMOUNT_26_PERCENT; } diff --git a/tests/utils/Defaults.sol b/tests/utils/Defaults.sol index a85885696..fc39a4cd2 100644 --- a/tests/utils/Defaults.sol +++ b/tests/utils/Defaults.sol @@ -3,10 +3,12 @@ pragma solidity >=0.8.22; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ud2x18 } from "@prb/math/src/UD2x18.sol"; -import { UD60x18, ZERO } from "@prb/math/src/UD60x18.sol"; - -import { BatchLockup, Broker, Lockup, LockupDynamic, LockupLinear, LockupTranched } from "../../src/types/DataTypes.sol"; +import { BatchLockup } from "../../src/types/BatchLockup.sol"; +import { Lockup } from "../../src/types/Lockup.sol"; +import { LockupDynamic } from "../../src/types/LockupDynamic.sol"; +import { LockupLinear } from "../../src/types/LockupLinear.sol"; +import { LockupTranched } from "../../src/types/LockupTranched.sol"; import { ArrayBuilder } from "./ArrayBuilder.sol"; import { BatchLockupBuilder } from "./BatchLockupBuilder.sol"; import { Constants } from "./Constants.sol"; @@ -14,35 +16,6 @@ import { Users } from "./Types.sol"; /// @notice Contract with default values used throughout the tests. contract Defaults is Constants { - /*////////////////////////////////////////////////////////////////////////// - GENERICS - //////////////////////////////////////////////////////////////////////////*/ - - uint64 public constant BATCH_SIZE = 10; - UD60x18 public constant BROKER_FEE = UD60x18.wrap(0.003e18); // 0.3% - uint128 public constant BROKER_FEE_AMOUNT = 30.090270812437311935e18; // 0.3% of total amount - uint128 public constant CLIFF_AMOUNT = 2500e18 + 2534; - uint40 public immutable CLIFF_TIME; - uint40 public constant CLIFF_DURATION = 2500 seconds; - uint128 public constant DEPOSIT_AMOUNT = 10_000e18; - uint40 public immutable END_TIME; - uint256 public constant MAX_COUNT = 10_000; - uint40 public immutable MAX_SEGMENT_DURATION; - uint256 public constant MAX_TRANCHE_COUNT = 10_000; - uint128 public constant REFUND_AMOUNT = DEPOSIT_AMOUNT - WITHDRAW_AMOUNT; - uint256 public constant SEGMENT_COUNT = 2; - string public constant SHAPE = "emits in the event"; - uint40 public immutable START_TIME; - uint128 public constant START_AMOUNT = 0; - uint128 public constant STREAMED_AMOUNT_26_PERCENT = 2600e18; - uint128 public constant TOTAL_AMOUNT = 10_030.090270812437311935e18; // deposit + broker fee - uint40 public constant TOTAL_DURATION = 10_000 seconds; - uint256 public constant TRANCHE_COUNT = 2; - uint128 public constant TOTAL_TRANSFER_AMOUNT = DEPOSIT_AMOUNT * uint128(BATCH_SIZE); - uint128 public constant WITHDRAW_AMOUNT = STREAMED_AMOUNT_26_PERCENT; - uint40 public immutable WARP_26_PERCENT; - uint40 public immutable WARP_26_PERCENT_DURATION = 2600 seconds; // 26% of the way through the stream - /*////////////////////////////////////////////////////////////////////////// VARIABLES //////////////////////////////////////////////////////////////////////////*/ @@ -50,18 +23,6 @@ contract Defaults is Constants { IERC20 private token; Users private users; - /*////////////////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////////////////*/ - - constructor() { - START_TIME = JULY_1_2024 + 2 days; - CLIFF_TIME = START_TIME + CLIFF_DURATION; - END_TIME = START_TIME + TOTAL_DURATION; - MAX_SEGMENT_DURATION = TOTAL_DURATION / uint40(MAX_COUNT); - WARP_26_PERCENT = START_TIME + WARP_26_PERCENT_DURATION; - } - /*////////////////////////////////////////////////////////////////////////// HELPERS //////////////////////////////////////////////////////////////////////////*/ @@ -78,14 +39,6 @@ contract Defaults is Constants { STRUCTS //////////////////////////////////////////////////////////////////////////*/ - function broker() public view returns (Broker memory) { - return Broker({ account: users.broker, fee: BROKER_FEE }); - } - - function brokerNull() public pure returns (Broker memory) { - return Broker({ account: address(0), fee: ZERO }); - } - function durations() public pure returns (LockupLinear.Durations memory) { return LockupLinear.Durations({ cliff: CLIFF_DURATION, total: TOTAL_DURATION }); } @@ -94,12 +47,15 @@ contract Defaults is Constants { return Lockup.Amounts({ deposited: DEPOSIT_AMOUNT, refunded: 0, withdrawn: 0 }); } - function lockupCreateAmounts() public pure returns (Lockup.CreateAmounts memory) { - return Lockup.CreateAmounts({ deposit: DEPOSIT_AMOUNT, brokerFee: BROKER_FEE_AMOUNT }); - } - - function lockupCreateEvent(IERC20 token_) public view returns (Lockup.CreateEventCommon memory) { - return lockupCreateEvent(token_, lockupCreateAmounts(), lockupTimestamps()); + function lockupCreateEvent( + IERC20 token_, + uint128 depositAmount + ) + public + view + returns (Lockup.CreateEventCommon memory) + { + return lockupCreateEvent(depositAmount, token_, lockupTimestamps()); } function lockupCreateEvent(Lockup.Timestamps memory timestamps) @@ -107,48 +63,62 @@ contract Defaults is Constants { view returns (Lockup.CreateEventCommon memory) { - return lockupCreateEvent(token, lockupCreateAmounts(), timestamps); + return lockupCreateEvent(DEPOSIT_AMOUNT, token, timestamps); } function lockupCreateEvent( - Lockup.CreateAmounts memory createAmounts, + uint128 depositAmount, Lockup.Timestamps memory timestamps ) public view returns (Lockup.CreateEventCommon memory) { - return lockupCreateEvent(token, createAmounts, timestamps); + return lockupCreateEvent(depositAmount, token, timestamps); } function lockupCreateEvent( + uint128 depositAmount, IERC20 token_, - Lockup.CreateAmounts memory createAmounts, Lockup.Timestamps memory timestamps ) public view returns (Lockup.CreateEventCommon memory) + { + Lockup.CreateWithTimestamps memory params = createWithTimestamps(); + params.depositAmount = depositAmount; + params.timestamps = timestamps; + return lockupCreateEvent(users.sender, params, token_); + } + + function lockupCreateEvent( + address caller, + Lockup.CreateWithTimestamps memory params, + IERC20 token_ + ) + public + pure + returns (Lockup.CreateEventCommon memory) { return Lockup.CreateEventCommon({ - funder: users.sender, - sender: users.sender, - recipient: users.recipient, - amounts: createAmounts, + funder: caller, + sender: params.sender, + recipient: params.recipient, + depositAmount: params.depositAmount, token: token_, - cancelable: true, - transferable: true, - timestamps: timestamps, - shape: SHAPE, - broker: users.broker + cancelable: params.cancelable, + transferable: params.transferable, + timestamps: params.timestamps, + shape: params.shape }); } - function lockupTimestamps() public view returns (Lockup.Timestamps memory) { + function lockupTimestamps() public pure returns (Lockup.Timestamps memory) { return Lockup.Timestamps({ start: START_TIME, end: END_TIME }); } - function segments() public view returns (LockupDynamic.Segment[] memory segments_) { + function segments() public pure returns (LockupDynamic.Segment[] memory segments_) { segments_ = new LockupDynamic.Segment[](2); segments_[0] = ( LockupDynamic.Segment({ @@ -164,7 +134,7 @@ contract Defaults is Constants { function segmentsWithDurations() public - view + pure returns (LockupDynamic.SegmentWithDuration[] memory segmentsWithDurations_) { LockupDynamic.Segment[] memory segments_ = segments(); @@ -185,7 +155,7 @@ contract Defaults is Constants { ); } - function tranches() public view returns (LockupTranched.Tranche[] memory tranches_) { + function tranches() public pure returns (LockupTranched.Tranche[] memory tranches_) { tranches_ = new LockupTranched.Tranche[](2); tranches_[0] = LockupTranched.Tranche({ amount: 2600e18, timestamp: WARP_26_PERCENT }); tranches_[1] = LockupTranched.Tranche({ amount: 7400e18, timestamp: START_TIME + TOTAL_DURATION }); @@ -217,41 +187,27 @@ contract Defaults is Constants { return Lockup.CreateWithDurations({ sender: users.sender, recipient: users.recipient, - totalAmount: TOTAL_AMOUNT, + depositAmount: DEPOSIT_AMOUNT, token: token, cancelable: true, transferable: true, - shape: SHAPE, - broker: broker() + shape: SHAPE }); } - function createWithDurationsBrokerNull() public view returns (Lockup.CreateWithDurations memory params_) { - params_ = createWithDurations(); - params_.totalAmount = DEPOSIT_AMOUNT; - params_.broker = brokerNull(); - } - function createWithTimestamps() public view returns (Lockup.CreateWithTimestamps memory) { return Lockup.CreateWithTimestamps({ sender: users.sender, recipient: users.recipient, - totalAmount: TOTAL_AMOUNT, + depositAmount: DEPOSIT_AMOUNT, token: token, cancelable: true, transferable: true, timestamps: lockupTimestamps(), - shape: SHAPE, - broker: broker() + shape: SHAPE }); } - function createWithTimestampsBrokerNull() public view returns (Lockup.CreateWithTimestamps memory params_) { - params_ = createWithTimestamps(); - params_.totalAmount = DEPOSIT_AMOUNT; - params_.broker = brokerNull(); - } - /*////////////////////////////////////////////////////////////////////////// BATCH-LOCKUP //////////////////////////////////////////////////////////////////////////*/ @@ -262,17 +218,17 @@ contract Defaults is Constants { /// @dev Returns a default-size batch of {BatchLockup.CreateWithDurationsLD} parameters. function batchCreateWithDurationsLD() public view returns (BatchLockup.CreateWithDurationsLD[] memory batch) { - batch = BatchLockupBuilder.fillBatch(createWithDurationsBrokerNull(), segmentsWithDurations(), BATCH_SIZE); + batch = BatchLockupBuilder.fillBatch(createWithDurations(), segmentsWithDurations(), BATCH_SIZE); } /// @dev Returns a default-size batch of {BatchLockup.CreateWithDurationsLL} parameters. function batchCreateWithDurationsLL() public view returns (BatchLockup.CreateWithDurationsLL[] memory batch) { - batch = BatchLockupBuilder.fillBatch(createWithDurationsBrokerNull(), unlockAmounts(), durations(), BATCH_SIZE); + batch = BatchLockupBuilder.fillBatch(createWithDurations(), unlockAmounts(), durations(), BATCH_SIZE); } /// @dev Returns a default-size batch of {BatchLockup.CreateWithDurationsLT} parameters. function batchCreateWithDurationsLT() public view returns (BatchLockup.CreateWithDurationsLT[] memory batch) { - batch = BatchLockupBuilder.fillBatch(createWithDurationsBrokerNull(), tranchesWithDurations(), BATCH_SIZE); + batch = BatchLockupBuilder.fillBatch(createWithDurations(), tranchesWithDurations(), BATCH_SIZE); } /// @dev Returns a default-size batch of {BatchLockup.CreateWithTimestampsLD} parameters. @@ -286,7 +242,7 @@ contract Defaults is Constants { view returns (BatchLockup.CreateWithTimestampsLD[] memory batch) { - batch = BatchLockupBuilder.fillBatch(createWithTimestampsBrokerNull(), segments(), batchSize); + batch = BatchLockupBuilder.fillBatch(createWithTimestamps(), segments(), batchSize); } /// @dev Returns a default-size batch of {BatchLockup.CreateWithTimestampsLL} parameters. @@ -300,7 +256,7 @@ contract Defaults is Constants { view returns (BatchLockup.CreateWithTimestampsLL[] memory batch) { - batch = BatchLockupBuilder.fillBatch(createWithTimestampsBrokerNull(), unlockAmounts(), CLIFF_TIME, batchSize); + batch = BatchLockupBuilder.fillBatch(createWithTimestamps(), unlockAmounts(), CLIFF_TIME, batchSize); } /// @dev Returns a default-size batch of {BatchLockup.CreateWithTimestampsLT} parameters. @@ -314,6 +270,6 @@ contract Defaults is Constants { view returns (BatchLockup.CreateWithTimestampsLT[] memory batch) { - batch = BatchLockupBuilder.fillBatch(createWithTimestampsBrokerNull(), tranches(), batchSize); + batch = BatchLockupBuilder.fillBatch(createWithTimestamps(), tranches(), batchSize); } } diff --git a/tests/utils/DeployOptimized.t.sol b/tests/utils/DeployOptimized.t.sol index ce5fea65e..5febdc768 100644 --- a/tests/utils/DeployOptimized.t.sol +++ b/tests/utils/DeployOptimized.t.sol @@ -2,15 +2,14 @@ // solhint-disable no-inline-assembly pragma solidity >=0.8.22 <0.9.0; -import { CommonBase } from "forge-std/src/Base.sol"; -import { StdCheats } from "forge-std/src/StdCheats.sol"; +import { BaseTest as CommonBase } from "@sablier/evm-utils/src/tests/BaseTest.sol"; import { stdJson } from "forge-std/src/StdJson.sol"; import { ILockupNFTDescriptor } from "../../src/interfaces/ILockupNFTDescriptor.sol"; import { ISablierBatchLockup } from "../../src/interfaces/ISablierBatchLockup.sol"; import { ISablierLockup } from "../../src/interfaces/ISablierLockup.sol"; -abstract contract DeployOptimized is StdCheats, CommonBase { +abstract contract DeployOptimized is CommonBase { using stdJson for string; /// @dev Deploys {SablierBatchLockup} from an optimized source compiled with `--via-ir`. @@ -18,24 +17,23 @@ abstract contract DeployOptimized is StdCheats, CommonBase { return ISablierBatchLockup(deployCode("out-optimized/SablierBatchLockup.sol/SablierBatchLockup.json")); } - /// @dev Deploys the optimized {Helpers} and {VestingMath} libraries. - function deployOptimizedLibraries() internal returns (address helpers, address vestingMath) { + /// @dev Deploys the optimized {Helpers} and {LockupMath} libraries. + function deployOptimizedLibraries() internal returns (address helpers, address lockupMath) { // Deploy public libraries. helpers = deployCode("out-optimized/Helpers.sol/Helpers.json"); - vestingMath = deployCode("out-optimized/VestingMath.sol/VestingMath.json"); + lockupMath = deployCode("out-optimized/LockupMath.sol/LockupMath.json"); } /// @dev Deploys {SablierLockup} from an optimized source compiled with `--via-ir`. function deployOptimizedLockup( - address initialAdmin, - ILockupNFTDescriptor nftDescriptor_, - uint256 maxCount + address initialComptroller, + ILockupNFTDescriptor nftDescriptor_ ) internal returns (ISablierLockup lockup) { // Deploy the libraries. - (address helpers, address vestingMath) = deployOptimizedLibraries(); + (address helpers, address lockupMath) = deployOptimizedLibraries(); // Get the bytecode from {SablierLockup} artifact. string memory artifactJson = vm.readFile("out-optimized/SablierLockup.sol/SablierLockup.json"); @@ -49,19 +47,19 @@ abstract contract DeployOptimized is StdCheats, CommonBase { }); rawBytecode = vm.replace({ input: rawBytecode, - from: libraryPlaceholder("src/libraries/VestingMath.sol:VestingMath"), - to: vm.replace(vm.toString(vestingMath), "0x", "") + from: libraryPlaceholder("src/libraries/LockupMath.sol:LockupMath"), + to: vm.replace(vm.toString(lockupMath), "0x", "") }); // Generate the creation bytecode with the constructor arguments. bytes memory createBytecode = - bytes.concat(vm.parseBytes(rawBytecode), abi.encode(initialAdmin, nftDescriptor_, maxCount)); + bytes.concat(vm.parseBytes(rawBytecode), abi.encode(initialComptroller, nftDescriptor_)); assembly { // Deploy the Lockup contract. lockup := create(0, add(createBytecode, 0x20), mload(createBytecode)) } - require(address(lockup) != address(0), "Lockup deployment failed."); + require(address(lockup) != address(0), "Lockup deployment failed"); return ISablierLockup(lockup); } @@ -76,15 +74,12 @@ abstract contract DeployOptimized is StdCheats, CommonBase { /// 1. {LockupNFTDescriptor} /// 2. {SablierLockup} /// 3. {SablierBatchLockup} - function deployOptimizedProtocol( - address initialAdmin, - uint256 maxCount - ) + function deployOptimizedProtocol(address initialComptroller) internal returns (ILockupNFTDescriptor nftDescriptor_, ISablierLockup lockup_, ISablierBatchLockup batchLockup_) { nftDescriptor_ = deployOptimizedNFTDescriptor(); - lockup_ = deployOptimizedLockup(initialAdmin, nftDescriptor_, maxCount); + lockup_ = deployOptimizedLockup(initialComptroller, nftDescriptor_); batchLockup_ = deployOptimizedBatchLockup(); } diff --git a/tests/utils/EstimateMaxCount.sol b/tests/utils/EstimateMaxCount.sol new file mode 100644 index 000000000..413dca490 --- /dev/null +++ b/tests/utils/EstimateMaxCount.sol @@ -0,0 +1,151 @@ +// solhint-disable no-console +pragma solidity >=0.8.22 <0.9.0; + +import { ud2x18 } from "@prb/math/src/UD2x18.sol"; +import { ERC20Mock } from "@sablier/evm-utils/src/mocks/erc20/ERC20Mock.sol"; +import { BaseTest as CommonBase } from "@sablier/evm-utils/src/tests/BaseTest.sol"; +import { console } from "forge-std/src/console.sol"; + +import { ISablierLockup } from "../../src/interfaces/ISablierLockup.sol"; +import { LockupNFTDescriptor } from "../../src/LockupNFTDescriptor.sol"; +import { SablierLockup } from "../../src/SablierLockup.sol"; +import { Lockup } from "../../src/types/Lockup.sol"; +import { LockupDynamic } from "../../src/types/LockupDynamic.sol"; +import { Defaults } from "./Defaults.sol"; +import { DeployOptimized } from "./DeployOptimized.t.sol"; +import { Users } from "./Types.sol"; + +/// @notice Structure to group the block gas limit and chain id. +struct ChainInfo { + uint256 blockGasLimit; + uint256 chainId; +} + +/// @notice Estimate the maximum number of segments allowed in a Lockup Dynamic stream. +contract EstimateMaxCount is Defaults, DeployOptimized { + /*////////////////////////////////////////////////////////////////////////// + VARIABLES + //////////////////////////////////////////////////////////////////////////*/ + + uint128 public constant AMOUNT_PER_SEGMENT = 1e18; + + // Buffer gas units to be deducted from the block gas limit so that the segment count never exceeds the block limit. + uint256 public constant BUFFER_GAS = 1_000_000; + + // Initial guess for the maximum number of segments/tranches. + uint128 public constant INITIAL_GUESS = 240; + + /// @dev List of chains with their block gas limit. + ChainInfo[] public chains; + + ISablierLockup public lockup; + Users public users; + + /*////////////////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////////////////*/ + + constructor() { + // Populate the chains array with respective block gas limit for each chain ID. + chains.push(ChainInfo({ blockGasLimit: 32_000_000, chainId: 42_161 })); // Arbitrum + chains.push(ChainInfo({ blockGasLimit: 15_000_000, chainId: 43_114 })); // Avalanche + chains.push(ChainInfo({ blockGasLimit: 60_000_000, chainId: 8453 })); // Base + chains.push(ChainInfo({ blockGasLimit: 30_000_000, chainId: 81_457 })); // Blast + chains.push(ChainInfo({ blockGasLimit: 138_000_000, chainId: 56 })); // BSC + chains.push(ChainInfo({ blockGasLimit: 30_000_000, chainId: 1 })); // Ethereum + chains.push(ChainInfo({ blockGasLimit: 17_000_000, chainId: 100 })); // Gnosis + chains.push(ChainInfo({ blockGasLimit: 30_000_000, chainId: 10 })); // Optimism + chains.push(ChainInfo({ blockGasLimit: 30_000_000, chainId: 137 })); // Polygon + chains.push(ChainInfo({ blockGasLimit: 10_000_000, chainId: 534_352 })); // Scroll + chains.push(ChainInfo({ blockGasLimit: 30_000_000, chainId: 11_155_111 })); // Sepolia + } + + /*////////////////////////////////////////////////////////////////////////// + SET-UP + //////////////////////////////////////////////////////////////////////////*/ + + function setUp() public virtual override { + CommonBase.setUp(); + + // Initialize the variables. + dai = new ERC20Mock("Dai stablecoin", "DAI", 18); + setToken(dai); + users.sender = payable(makeAddr("sender")); + users.recipient = payable(makeAddr("recipient")); + setUsers(users); + + // Deploy the Lockup contract. + if (!isTestOptimizedProfile()) { + lockup = new SablierLockup(address(comptroller), address(new LockupNFTDescriptor())); + } else { + (, lockup,) = deployOptimizedProtocol({ initialComptroller: address(comptroller) }); + } + + // Set up the caller. + setMsgSender(users.sender); + deal({ token: address(dai), to: users.sender, give: type(uint256).max }); + dai.approve(address(lockup), type(uint256).max); + + // Create dummy streams to initialize contract storage. + for (uint128 i = 0; i < 100; ++i) { + lockup.createWithTimestampsLD(createWithTimestamps(), segments()); + } + } + + /*////////////////////////////////////////////////////////////////////////// + ESTIMATE-COUNT-FUNCTION + //////////////////////////////////////////////////////////////////////////*/ + + function test_EstimateSegments() public { + // Estimate the maximum number of segments for each chain. + for (uint256 i = 0; i < chains.length; ++i) { + uint128 count = INITIAL_GUESS; + + // Subtract `BUFFER_GAS` from `blockGasLimit` as an additional precaution to account for the dynamic gas for + // ether transfer on different chains. + uint256 blockGasLimit = chains[i].blockGasLimit - BUFFER_GAS; + + uint256 gasConsumed = 0; + uint256 lastGasConsumed = 0; + while (blockGasLimit > gasConsumed) { + count += 10; + lastGasConsumed = gasConsumed; + + // Estimate the gas consumed by adding 10 segments. + (Lockup.CreateWithDurations memory params, LockupDynamic.SegmentWithDuration[] memory segments) = + _createWithDurationParamsLD({ totalSegments: count + 10 }); + + uint256 beforeGas = gasleft(); + lockup.createWithDurationsLD(params, segments); + + gasConsumed = beforeGas - gasleft(); + } + + console.log("count: %d and gasUsed: %d and chainId: %d", count, lastGasConsumed, chains[i].chainId); + } + } + + // Helper function to return the parameters of `createWithDurationsLD`. + function _createWithDurationParamsLD(uint128 totalSegments) + private + view + returns (Lockup.CreateWithDurations memory params, LockupDynamic.SegmentWithDuration[] memory segments_) + { + segments_ = new LockupDynamic.SegmentWithDuration[](totalSegments); + + // Populate segments. + for (uint256 i = 0; i < totalSegments; ++i) { + segments_[i] = ( + LockupDynamic.SegmentWithDuration({ + amount: AMOUNT_PER_SEGMENT, + exponent: ud2x18(0.5e18), + duration: CLIFF_DURATION + }) + ); + } + + params = createWithDurations(); + params.depositAmount = AMOUNT_PER_SEGMENT * totalSegments; + return (params, segments_); + } +} diff --git a/tests/utils/Fuzzers.sol b/tests/utils/Fuzzers.sol index 05ecf59b2..0438ca6f2 100644 --- a/tests/utils/Fuzzers.sol +++ b/tests/utils/Fuzzers.sol @@ -1,112 +1,77 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity >=0.8.22; -import { PRBMathCastingUint128 as CastingUint128 } from "@prb/math/src/casting/Uint128.sol"; -import { UD60x18, ud, uUNIT } from "@prb/math/src/UD60x18.sol"; - -import { Lockup, LockupDynamic, LockupTranched } from "../../src/types/DataTypes.sol"; - -import { Constants } from "./Constants.sol"; +import { BaseConstants } from "@sablier/evm-utils/src/tests/BaseConstants.sol"; +import { LockupDynamic } from "../../src/types/LockupDynamic.sol"; +import { LockupTranched } from "../../src/types/LockupTranched.sol"; import { Utils } from "./Utils.sol"; -abstract contract Fuzzers is Constants, Utils { - using CastingUint128 for uint128; - +abstract contract Fuzzers is BaseConstants, Utils { /*////////////////////////////////////////////////////////////////////////// LOCKUP-DYNAMIC //////////////////////////////////////////////////////////////////////////*/ /// @dev Just like {fuzzDynamicStreamAmounts} but with defaults. - function fuzzDynamicStreamAmounts( - LockupDynamic.Segment[] memory segments, - UD60x18 brokerFee - ) + function fuzzDynamicStreamAmounts(LockupDynamic.Segment[] memory segments) internal pure - returns (uint128 totalAmount, Lockup.CreateAmounts memory createAmounts) + returns (uint128 depositAmount) { - (totalAmount, createAmounts) = - fuzzDynamicStreamAmounts({ upperBound: MAX_UINT128, segments: segments, brokerFee: brokerFee }); + depositAmount = fuzzDynamicStreamAmounts({ upperBound: MAX_UINT128, segments: segments }); } /// @dev Just like {fuzzDynamicStreamAmounts} but with defaults. - function fuzzDynamicStreamAmounts( - LockupDynamic.SegmentWithDuration[] memory segments, - UD60x18 brokerFee - ) + function fuzzDynamicStreamAmounts(LockupDynamic.SegmentWithDuration[] memory segments) internal view - returns (uint128 totalAmount, Lockup.CreateAmounts memory createAmounts) + returns (uint128 depositAmount) { LockupDynamic.Segment[] memory segmentsWithTimestamps = getSegmentsWithTimestamps(segments); - (totalAmount, createAmounts) = fuzzDynamicStreamAmounts({ - upperBound: MAX_UINT128, - segments: segmentsWithTimestamps, - brokerFee: brokerFee - }); + depositAmount = fuzzDynamicStreamAmounts({ upperBound: MAX_UINT128, segments: segmentsWithTimestamps }); for (uint256 i = 0; i < segmentsWithTimestamps.length; ++i) { segments[i].amount = segmentsWithTimestamps[i].amount; } } - /// @dev Fuzzes the segment amounts and calculate the total and create amounts (deposit and broker fee). + /// @dev Fuzzes the segment amounts and calculate the deposit amount. function fuzzDynamicStreamAmounts( uint128 upperBound, - LockupDynamic.SegmentWithDuration[] memory segments, - UD60x18 brokerFee + LockupDynamic.SegmentWithDuration[] memory segments ) internal view - returns (uint128 totalAmount, Lockup.CreateAmounts memory createAmounts) + returns (uint128 depositAmount) { LockupDynamic.Segment[] memory segmentsWithTimestamps = getSegmentsWithTimestamps(segments); - (totalAmount, createAmounts) = fuzzDynamicStreamAmounts(upperBound, segmentsWithTimestamps, brokerFee); + depositAmount = fuzzDynamicStreamAmounts(upperBound, segmentsWithTimestamps); for (uint256 i = 0; i < segmentsWithTimestamps.length; ++i) { segments[i].amount = segmentsWithTimestamps[i].amount; } } - /// @dev Fuzzes the segment amounts and calculate the total and create amounts (deposit and broker fee). + /// @dev Fuzzes the segment amounts and calculate the deposit amount. function fuzzDynamicStreamAmounts( uint128 upperBound, - LockupDynamic.Segment[] memory segments, - UD60x18 brokerFee + LockupDynamic.Segment[] memory segments ) internal pure - returns (uint128 totalAmount, Lockup.CreateAmounts memory createAmounts) + returns (uint128 depositAmount) { uint256 segmentCount = segments.length; uint128 maxSegmentAmount = upperBound / uint128(segmentCount * 2); // Precompute the first segment amount to prevent zero deposit amounts. segments[0].amount = boundUint128(segments[0].amount, 100, maxSegmentAmount); - uint128 estimatedDepositAmount = segments[0].amount; + depositAmount = segments[0].amount; // Fuzz the other segment amounts by bounding from 0. unchecked { for (uint256 i = 1; i < segmentCount; ++i) { segments[i].amount = boundUint128(segments[i].amount, 0, maxSegmentAmount); - estimatedDepositAmount += segments[i].amount; + depositAmount += segments[i].amount; } } - - // Calculate the total amount from the approximated deposit amount (recall that the sum of all segment amounts - // must equal the deposit amount) using this formula: - // - // $$ - // total = \frac{deposit}{1e18 - brokerFee} - // $$ - totalAmount = ud(estimatedDepositAmount).div(ud(uUNIT - brokerFee.intoUint256())).intoUint128(); - - // Calculate the broker fee amount. - createAmounts.brokerFee = ud(totalAmount).mul(brokerFee).intoUint128(); - - // Here, we account for rounding errors and adjust the estimated deposit amount and the segments. We know - // that the estimated deposit amount is not greater than the adjusted deposit amount below, because the inverse - // of {Helpers.checkAndCalculateBrokerFee} over-expresses the weight of the broker fee. - createAmounts.deposit = totalAmount - createAmounts.brokerFee; - segments[segments.length - 1].amount += (createAmounts.deposit - estimatedDepositAmount); } /// @dev Fuzzes the segment durations. @@ -198,95 +163,65 @@ abstract contract Fuzzers is Constants, Utils { } /// @dev Just like {fuzzTranchedStreamAmounts} but with defaults. - function fuzzTranchedStreamAmounts( - LockupTranched.Tranche[] memory tranches, - UD60x18 brokerFee - ) + function fuzzTranchedStreamAmounts(LockupTranched.Tranche[] memory tranches) internal pure - returns (uint128 totalAmount, Lockup.CreateAmounts memory createAmounts) + returns (uint128 depositAmount) { - (totalAmount, createAmounts) = - fuzzTranchedStreamAmounts({ upperBound: MAX_UINT128, tranches: tranches, brokerFee: brokerFee }); + depositAmount = fuzzTranchedStreamAmounts({ upperBound: MAX_UINT128, tranches: tranches }); } /// @dev Just like {fuzzTranchedStreamAmounts} but with defaults. - function fuzzTranchedStreamAmounts( - LockupTranched.TrancheWithDuration[] memory tranches, - UD60x18 brokerFee - ) + function fuzzTranchedStreamAmounts(LockupTranched.TrancheWithDuration[] memory tranches) internal view - returns (uint128 totalAmount, Lockup.CreateAmounts memory createAmounts) + returns (uint128 depositAmount) { LockupTranched.Tranche[] memory tranchesWithTimestamps = getTranchesWithTimestamps(tranches); - (totalAmount, createAmounts) = fuzzTranchedStreamAmounts({ - upperBound: MAX_UINT128, - tranches: tranchesWithTimestamps, - brokerFee: brokerFee - }); + depositAmount = fuzzTranchedStreamAmounts({ upperBound: MAX_UINT128, tranches: tranchesWithTimestamps }); for (uint256 i = 0; i < tranchesWithTimestamps.length; ++i) { tranches[i].amount = tranchesWithTimestamps[i].amount; } } - /// @dev Fuzzes the tranche amounts and calculates the total and create amounts (deposit and broker fee). + /// @dev Fuzzes the tranche amounts and calculates the deposit amount. function fuzzTranchedStreamAmounts( uint128 upperBound, - LockupTranched.TrancheWithDuration[] memory tranches, - UD60x18 brokerFee + LockupTranched.TrancheWithDuration[] memory tranches ) internal view - returns (uint128 totalAmount, Lockup.CreateAmounts memory createAmounts) + returns (uint128 depositAmount) { LockupTranched.Tranche[] memory tranchesWithTimestamps = getTranchesWithTimestamps(tranches); - (totalAmount, createAmounts) = fuzzTranchedStreamAmounts(upperBound, tranchesWithTimestamps, brokerFee); + depositAmount = fuzzTranchedStreamAmounts(upperBound, tranchesWithTimestamps); for (uint256 i = 0; i < tranchesWithTimestamps.length; ++i) { tranches[i].amount = tranchesWithTimestamps[i].amount; } } - /// @dev Fuzzes the tranche amounts and calculates the total and create amounts (deposit and broker fee). + /// @dev Fuzzes the tranche amounts and calculates the deposit amount. function fuzzTranchedStreamAmounts( uint128 upperBound, - LockupTranched.Tranche[] memory tranches, - UD60x18 brokerFee + LockupTranched.Tranche[] memory tranches ) internal pure - returns (uint128 totalAmount, Lockup.CreateAmounts memory createAmounts) + returns (uint128 depositAmount) { uint256 trancheCount = tranches.length; uint128 maxTrancheAmount = upperBound / uint128(trancheCount * 2); // Precompute the first tranche amount to prevent zero deposit amounts. tranches[0].amount = boundUint128(tranches[0].amount, 100, maxTrancheAmount); - uint128 estimatedDepositAmount = tranches[0].amount; + depositAmount = tranches[0].amount; // Fuzz the other tranche amounts by bounding from 0. unchecked { for (uint256 i = 1; i < trancheCount; ++i) { tranches[i].amount = boundUint128(tranches[i].amount, 0, maxTrancheAmount); - estimatedDepositAmount += tranches[i].amount; + depositAmount += tranches[i].amount; } } - - // Calculate the total amount from the approximated deposit amount (recall that the sum of all tranche amounts - // must equal the deposit amount) using this formula: - // - // $$ - // total = \frac{deposit}{1e18 - brokerFee} - // $$ - totalAmount = ud(estimatedDepositAmount).div(ud(uUNIT - brokerFee.intoUint256())).intoUint128(); - - // Calculate the broker fee amount. - createAmounts.brokerFee = ud(totalAmount).mul(brokerFee).intoUint128(); - - // Here, we account for rounding errors and adjust the estimated deposit amount and the tranches. We know - // that the estimated deposit amount is not greater than the adjusted deposit amount below, because the inverse - // of {Helpers.checkAndCalculateBrokerFee} over-expresses the weight of the broker fee. - createAmounts.deposit = totalAmount - createAmounts.brokerFee; - tranches[tranches.length - 1].amount += (createAmounts.deposit - estimatedDepositAmount); } } diff --git a/tests/utils/Modifiers.sol b/tests/utils/Modifiers.sol index 0be761e40..3c814ac9e 100644 --- a/tests/utils/Modifiers.sol +++ b/tests/utils/Modifiers.sol @@ -1,13 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity >=0.8.22; -import { ISablierLockup } from "../../src/interfaces/ISablierLockup.sol"; +import { BaseTest as EvmUtilsBase } from "@sablier/evm-utils/src/tests/BaseTest.sol"; +import { ISablierLockup } from "../../src/interfaces/ISablierLockup.sol"; import { Defaults } from "./Defaults.sol"; import { Fuzzers } from "./Fuzzers.sol"; import { Users } from "./Types.sol"; -abstract contract Modifiers is Fuzzers { +abstract contract Modifiers is EvmUtilsBase, Fuzzers { /*////////////////////////////////////////////////////////////////////////// VARIABLES //////////////////////////////////////////////////////////////////////////*/ @@ -81,23 +82,22 @@ abstract contract Modifiers is Fuzzers { _; } - modifier whenCallerAdmin() { - // Make the Admin the caller in the rest of this test suite. - resetPrank({ msgSender: users.admin }); + modifier whenCallerAuthorizedForAllStreams() virtual { _; } - modifier whenCallerAuthorizedForAllStreams() virtual { + modifier whenCallerComptroller() { + setMsgSender(address(comptroller)); _; } modifier whenCallerRecipient() { - resetPrank({ msgSender: users.recipient }); + setMsgSender(users.recipient); _; } modifier whenCallerSender() { - resetPrank({ msgSender: users.sender }); + setMsgSender(users.sender); _; } @@ -147,7 +147,7 @@ abstract contract Modifiers is Fuzzers { modifier givenDepletedStream(ISablierLockup lockup, uint256 streamId) { vm.warp({ newTimestamp: defaults.END_TIME() }); - lockup.withdrawMax({ streamId: streamId, to: users.recipient }); + lockup.withdrawMax{ value: LOCKUP_MIN_FEE_WEI }({ streamId: streamId, to: users.recipient }); _; } @@ -195,15 +195,7 @@ abstract contract Modifiers is Fuzzers { CREATE-COMMON //////////////////////////////////////////////////////////////////////////*/ - modifier whenBrokerFeeNotExceedMaxValue() { - _; - } - - modifier whenSegmentCountNotExceedMaxValue() { - _; - } - - modifier whenTrancheCountNotExceedMaxValue() { + modifier whenEndTimeEqualsLastTimestamp() { _; } @@ -275,6 +267,10 @@ abstract contract Modifiers is Fuzzers { _; } + modifier whenTokenNotNativeToken() { + _; + } + modifier whenTrancheAmountsSumNotOverflow() { _; } @@ -311,6 +307,14 @@ abstract contract Modifiers is Fuzzers { _; } + /*////////////////////////////////////////////////////////////////////////// + MAP-SYMBOL + //////////////////////////////////////////////////////////////////////////*/ + + modifier givenKnownNFTContract() { + _; + } + /*////////////////////////////////////////////////////////////////////////// SAFE-TOKEN-SYMBOL //////////////////////////////////////////////////////////////////////////*/ @@ -331,14 +335,6 @@ abstract contract Modifiers is Fuzzers { _; } - /*////////////////////////////////////////////////////////////////////////// - MAP-SYMBOL - //////////////////////////////////////////////////////////////////////////*/ - - modifier givenKnownNFTContract() { - _; - } - /*////////////////////////////////////////////////////////////////////////// STATUS-OF //////////////////////////////////////////////////////////////////////////*/ @@ -383,14 +379,6 @@ abstract contract Modifiers is Fuzzers { _; } - /*////////////////////////////////////////////////////////////////////////// - TRANSFER-ADMIN - //////////////////////////////////////////////////////////////////////////*/ - - modifier whenNewAdminNotSameAsCurrentAdmin() { - _; - } - /*////////////////////////////////////////////////////////////////////////// WITHDRAW //////////////////////////////////////////////////////////////////////////*/ @@ -400,6 +388,10 @@ abstract contract Modifiers is Fuzzers { _; } + modifier whenFeeNotLessThanMinFee() { + _; + } + modifier whenHookReturnsValidSelector() { _; } @@ -420,14 +412,6 @@ abstract contract Modifiers is Fuzzers { _; } - /*////////////////////////////////////////////////////////////////////////// - COLLECT-FEES - //////////////////////////////////////////////////////////////////////////*/ - - modifier givenAdminIsContract() { - _; - } - /*////////////////////////////////////////////////////////////////////////// WITHDRAW-HOOKS //////////////////////////////////////////////////////////////////////////*/ diff --git a/tests/utils/Types.sol b/tests/utils/Types.sol index c638a23f9..6af5b4a7c 100644 --- a/tests/utils/Types.sol +++ b/tests/utils/Types.sol @@ -1,13 +1,39 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity >=0.8.22; +/// @notice Enum to represent actions performed on a stream. +/// @dev If there are multiple functions that can perform the same action, the same enum value should be associated with +/// all of them. For example, a `WITHDRAW` action should be associated with `withdraw` and `withdrawMax` functions. +enum StreamAction { + CANCEL, + CREATE, + WITHDRAW +} + +struct StreamIds { + // Default stream ID. + uint256 defaultStream; + // A stream with a recipient contract that is not allowed to hook. + uint256 notAllowedToHookStream; + // A non-cancelable stream ID. + uint256 notCancelableStream; + // A non-transferable stream ID. + uint256 notTransferableStream; + // A stream ID that does not exist. + uint256 nullStream; + // A stream with a recipient contract that implements {ISablierLockupRecipient}. + uint256 recipientGoodStream; + // A stream with a recipient contract that returns invalid selector bytes on the hook call. + uint256 recipientInvalidSelectorStream; + // A stream with a reentrant contract as the recipient. + uint256 recipientReentrantStream; + // A stream with a reverting contract as the stream's recipient. + uint256 recipientRevertStream; +} + struct Users { - // Default admin. - address payable admin; // Impartial user. address payable alice; - // Default stream broker. - address payable broker; // Malicious user. address payable eve; // Default NFT operator. diff --git a/tests/utils/Utils.sol b/tests/utils/Utils.sol index 578299e1f..744f6318d 100644 --- a/tests/utils/Utils.sol +++ b/tests/utils/Utils.sol @@ -1,28 +1,13 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity >=0.8.22; -import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { PRBMathUtils } from "@prb/math/test/utils/Utils.sol"; -import { CommonBase } from "forge-std/src/Base.sol"; +import { BaseUtils } from "@sablier/evm-utils/src/tests/BaseUtils.sol"; -import { LockupDynamic, LockupTranched } from "../../src/types/DataTypes.sol"; - -abstract contract Utils is CommonBase, PRBMathUtils { - /// @dev Bounds a `uint128` number. - function boundUint128(uint128 x, uint128 min, uint128 max) internal pure returns (uint128) { - return uint128(_bound(uint256(x), uint256(min), uint256(max))); - } - - /// @dev Bounds a `uint40` number. - function boundUint40(uint40 x, uint40 min, uint40 max) internal pure returns (uint40) { - return uint40(_bound(uint256(x), uint256(min), uint256(max))); - } - - /// @dev Retrieves the current block timestamp as an `uint40`. - function getBlockTimestamp() internal view returns (uint40) { - return uint40(block.timestamp); - } +import { LockupDynamic } from "../../src/types/LockupDynamic.sol"; +import { LockupTranched } from "../../src/types/LockupTranched.sol"; +abstract contract Utils is BaseUtils, PRBMathUtils { /// @dev Turns the segments with durations into canonical segments, which have timestamps. function getSegmentsWithTimestamps(LockupDynamic.SegmentWithDuration[] memory segments) internal @@ -67,18 +52,6 @@ abstract contract Utils is CommonBase, PRBMathUtils { } } - /// @dev Checks if the Foundry profile is "benchmark". - function isBenchmarkProfile() internal view returns (bool) { - string memory profile = vm.envOr({ name: "FOUNDRY_PROFILE", defaultValue: string("default") }); - return Strings.equal(profile, "benchmark"); - } - - /// @dev Checks if the Foundry profile is "test-optimized". - function isTestOptimizedProfile() internal view returns (bool) { - string memory profile = vm.envOr({ name: "FOUNDRY_PROFILE", defaultValue: string("default") }); - return Strings.equal(profile, "test-optimized"); - } - /// @dev Returns the largest of the provided `uint40` numbers. function maxOfTwo(uint40 a, uint40 b) internal pure returns (uint40) { uint40 max = a; @@ -87,10 +60,4 @@ abstract contract Utils is CommonBase, PRBMathUtils { } return max; } - - /// @dev Stops the active prank and sets a new one. - function resetPrank(address msgSender) internal { - vm.stopPrank(); - vm.startPrank(msgSender); - } }