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: "