Skip to content

Commit 7912326

Browse files
authored
Merge pull request #220 from sablierhq/prb/fees
Segment[] array, comptroller, protocol and operator fees, structified parameters, via IR, PRBMath types, major tests refactor
2 parents 5774a54 + dbc88bf commit 7912326

File tree

216 files changed

+7896
-6357
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

216 files changed

+7896
-6357
lines changed

.github/workflows/ci.yml

+58-19
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
name: "CI"
22

3+
concurrency:
4+
group: ${{github.workflow}}-${{github.ref}}
5+
cancel-in-progress: true
6+
37
env:
48
ETH_RPC_URL: "${{ secrets.ETH_RPC_URL }}"
5-
FOUNDRY_PROFILE: "ci"
69

710
on:
811
pull_request:
@@ -13,7 +16,30 @@ on:
1316
- "main"
1417

1518
jobs:
16-
ci:
19+
lint:
20+
runs-on: "ubuntu-latest"
21+
steps:
22+
- name: "Check out the repo"
23+
uses: "actions/checkout@v3"
24+
25+
- name: "Install Node.js"
26+
uses: "actions/setup-node@v3"
27+
with:
28+
cache: "yarn"
29+
node-version: "lts/*"
30+
31+
- name: "Install the Node.js dependencies"
32+
run: "yarn install --immutable"
33+
34+
- name: "Lint the contracts"
35+
run: "yarn lint"
36+
37+
- name: "Add lint summary"
38+
run: |
39+
echo "## Lint results" >> $GITHUB_STEP_SUMMARY
40+
echo "✅ Passed" >> $GITHUB_STEP_SUMMARY
41+
42+
test:
1743
runs-on: "ubuntu-latest"
1844
steps:
1945
- name: "Check out the repo"
@@ -36,30 +62,43 @@ jobs:
3662
restore-keys: |
3763
"${{ runner.os }}-foundry-chain-fork-"
3864
39-
- name: "Install Node.js"
40-
uses: "actions/setup-node@v3"
41-
with:
42-
cache: "yarn"
43-
node-version: "lts/*"
65+
- name: "Show the Foundry config"
66+
run: "forge config"
4467

45-
- name: "Install the Node.js dependencies"
46-
run: "yarn install --immutable"
68+
- name: "Build the production code with --via-ir"
69+
run: "FOUNDRY_PROFILE=optimized forge build"
4770

48-
- name: "Lint the contracts"
49-
run: "yarn lint"
71+
- name: "Test the optimized code"
72+
run: "FOUNDRY_PROFILE=test-optimized forge test"
5073

51-
- name: "Add lint summary"
74+
- name: "Add test summary"
5275
run: |
53-
echo "## Lint results" >> $GITHUB_STEP_SUMMARY
76+
echo "## Test results" >> $GITHUB_STEP_SUMMARY
5477
echo "✅ Passed" >> $GITHUB_STEP_SUMMARY
5578
56-
- name: "Show the Foundry config"
57-
run: "forge config"
79+
coverage:
80+
runs-on: "ubuntu-latest"
81+
steps:
82+
- name: "Check out the repo"
83+
uses: "actions/checkout@v3"
84+
with:
85+
submodules: "recursive"
5886

59-
- name: "Run the tests"
60-
run: "forge test"
87+
- name: "Install Foundry"
88+
uses: "foundry-rs/foundry-toolchain@v1"
89+
with:
90+
version: "nightly"
6191

62-
- name: "Add test summary"
92+
- name: "Generate the coverage report"
93+
run: "forge coverage --report lcov"
94+
95+
- name: "Upload coverage report to Coveralls"
96+
uses: "coverallsapp/github-action@master"
97+
with:
98+
github-token: "${{ secrets.GITHUB_TOKEN }}"
99+
path-to-lcov: "./lcov.info"
100+
101+
- name: "Add coverage summary"
63102
run: |
64-
echo "## Test results" >> $GITHUB_STEP_SUMMARY
103+
echo "## Coverage results" >> $GITHUB_STEP_SUMMARY
65104
echo "✅ Passed" >> $GITHUB_STEP_SUMMARY

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@
77
!.yarn/versions
88
**/cache
99
**/node_modules
10+
**/optimized-out
1011
**/out
1112

1213
# files
1314
*.env
15+
*.html
1416
*.log
1517
.DS_Store
1618
.pnp.*
19+
lcov.info
1720
npm-debug.log*
1821
yarn-debug.log*
1922
yarn-error.log*

.gitmodules

+8-4
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,14 @@
1111
path = "lib/prb-contracts"
1212
url = "https://github.com/paulrberg/prb-contracts"
1313
[submodule "lib/prb-math"]
14-
branch = "v3"
14+
branch = "main"
1515
path = "lib/prb-math"
1616
url = "https://github.com/paulrberg/prb-math"
1717
[submodule "lib/prb-test"]
18-
branch = "0.3.1"
19-
path = "lib/prb-test"
20-
url = "https://github.com/paulrberg/prb-test"
18+
branch = "main"
19+
path = "lib/prb-test"
20+
url = "https://github.com/paulrberg/prb-test"
21+
[submodule "lib/solarray"]
22+
branch = "master"
23+
path = "lib/solarray"
24+
url = "https://github.com/paulrberg/solarray"

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ You will need the following VSCode extensions:
7979
Tests are organized in two categories:
8080

8181
1. Unit - simple tests that check the behavior of a single function on a local development EMV.
82-
2. Integration - complex tests that run against a fork of Ethereum Mainnet.
82+
2. Integration - complex tests that run against a fork of Ethereum Mainnet to check that Sablier V2 works with deployed
83+
ERC-20 tokens.
8384

8485
You can run all the tests by using this command:
8586

foundry.toml

+14-6
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,27 @@
22

33
[profile.default]
44
auto_detect_solc = false
5-
fuzz = { runs = 256 }
5+
fs_permissions = [
6+
{ access = "read", path = "./optimized-out" },
7+
]
8+
fuzz = { max_test_rejects = 100_000, runs = 1_000 }
69
libs = ["lib"]
710
gas_reports = ["*"]
811
optimizer = true
912
optimizer_runs = 10_000
1013
out = "out"
11-
sender = "0x9aF2E2B7e57c1CD7C68C5C3796d8ea67e0018dB7"
1214
solc = "0.8.17"
1315
src = "src"
1416
test = "test/unit"
15-
tx_origin = "0x9aF2E2B7e57c1CD7C68C5C3796d8ea67e0018dB7"
1617

1718
[profile.ci]
18-
fuzz = { runs = 1_000 }
19-
test = "test"
20-
verbosity = 4
19+
fuzz = { runs = 5_000 }
20+
21+
[profile.optimized]
22+
out = "optimized-out"
23+
test = "src" # compile only the production code with IR
24+
via_ir = true
25+
26+
[profile.test-optimized]
27+
fuzz = { runs = 5_000 }
28+
src = "test/unit" # do not re-compile the production code

lib/prb-contracts

lib/prb-test

lib/solarray

Submodule solarray added at f433f93

package.json

+3-4
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
"ethereum",
2525
"foundry",
2626
"money-streaming",
27-
"real-time-finance",
2827
"sablier",
2928
"smart-contracts",
3029
"solidity"
@@ -34,11 +33,11 @@
3433
"private": true,
3534
"scripts": {
3635
"clean": "rimraf cache out",
37-
"lint": "yarn solhint && yarn prettier:check",
38-
"lint:check": "yarn prettier:check && yarn solhint:check",
36+
"lint": "yarn prettier:check && yarn solhint \"{src,test}/**/*.sol\"",
3937
"postinstall": "husky install",
4038
"prettier:write": "prettier --write \"**/*.{json,md,sol,yml}\"",
4139
"prettier:check": "prettier --check \"**/*.{json,md,sol,yml}\"",
42-
"solhint": "solhint --config ./.solhint.json \"{src,test}/**/*.sol\""
40+
"test": "forge test",
41+
"test:optimized": "FOUNDRY_PROFILE=optimized forge build && FOUNDRY_PROFILE=test-optimized forge test"
4342
}
4443
}

remappings.txt

+2
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@
33
@prb/math/=lib/prb-math/src/
44
@prb/test/=lib/prb-test/src/
55
forge-std/=lib/forge-std/src/
6+
solarray=lib/solarray/src
67
src/=src/
8+
test/=test/

src/SablierV2.sol

+77-9
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,64 @@
11
// SPDX-License-Identifier: LGPL-3.0
22
pragma solidity >=0.8.13;
33

4+
import { IERC20 } from "@prb/contracts/token/erc20/IERC20.sol";
5+
import { Ownable } from "@prb/contracts/access/Ownable.sol";
6+
import { SafeERC20 } from "@prb/contracts/token/erc20/SafeERC20.sol";
7+
import { UD60x18 } from "@prb/math/UD60x18.sol";
8+
49
import { Errors } from "./libraries/Errors.sol";
10+
import { Events } from "./libraries/Events.sol";
511

612
import { ISablierV2 } from "./interfaces/ISablierV2.sol";
13+
import { ISablierV2Comptroller } from "./interfaces/ISablierV2Comptroller.sol";
714

815
/// @title SablierV2
916
/// @dev Abstract contract implementing common logic. Implements the ISablierV2 interface.
10-
abstract contract SablierV2 is ISablierV2 {
17+
abstract contract SablierV2 is
18+
Ownable, // one dependency
19+
ISablierV2 // three dependencies
20+
{
21+
using SafeERC20 for IERC20;
22+
23+
/*//////////////////////////////////////////////////////////////////////////
24+
CONSTANTS
25+
//////////////////////////////////////////////////////////////////////////*/
26+
27+
/// @inheritdoc ISablierV2
28+
UD60x18 public immutable override MAX_FEE;
29+
1130
/*//////////////////////////////////////////////////////////////////////////
1231
PUBLIC STORAGE
1332
//////////////////////////////////////////////////////////////////////////*/
1433

34+
/// @inheritdoc ISablierV2
35+
ISablierV2Comptroller public override comptroller;
36+
1537
/// @inheritdoc ISablierV2
1638
uint256 public override nextStreamId;
1739

40+
/*//////////////////////////////////////////////////////////////////////////
41+
INTERNAL STORAGE
42+
//////////////////////////////////////////////////////////////////////////*/
43+
44+
/// @dev Protocol revenues mapped by token addresses.
45+
mapping(IERC20 => uint128) internal _protocolRevenues;
46+
1847
/*//////////////////////////////////////////////////////////////////////////
1948
MODIFIERS
2049
//////////////////////////////////////////////////////////////////////////*/
2150

22-
/// @notice Checks that `msg.sender` is the sender of the stream, an approved operator, or the owner of the
23-
/// NFT (also known as the recipient of the stream).
51+
/// @notice Checks that `msg.sender` is the sender of the stream, the recipient of the stream (also known as
52+
/// the owner of the NFT), or an approved operator.
2453
modifier isAuthorizedForStream(uint256 streamId) {
2554
if (!_isCallerStreamSender(streamId) && !_isApprovedOrOwner(streamId, msg.sender)) {
2655
revert Errors.SablierV2__Unauthorized(streamId, msg.sender);
2756
}
2857
_;
2958
}
3059

31-
/// @notice Checks that `msg.sender` is either the sender of the stream or the owner of the NFT (also known as
32-
/// the recipient of the stream).
60+
/// @notice Checks that `msg.sender` is either the sender of the stream or the recipient of the stream (also known
61+
/// as the owner of the NFT).
3362
modifier onlySenderOrRecipient(uint256 streamId) {
3463
if (!_isCallerStreamSender(streamId) && msg.sender != getRecipient(streamId)) {
3564
revert Errors.SablierV2__Unauthorized(streamId, msg.sender);
@@ -49,14 +78,21 @@ abstract contract SablierV2 is ISablierV2 {
4978
CONSTRUCTOR
5079
//////////////////////////////////////////////////////////////////////////*/
5180

52-
constructor() {
81+
constructor(ISablierV2Comptroller initialComptroller, UD60x18 maxFee) {
82+
comptroller = initialComptroller;
83+
MAX_FEE = maxFee;
5384
nextStreamId = 1;
5485
}
5586

5687
/*//////////////////////////////////////////////////////////////////////////
5788
PUBLIC CONSTANT FUNCTIONS
5889
//////////////////////////////////////////////////////////////////////////*/
5990

91+
/// @inheritdoc ISablierV2
92+
function getProtocolRevenues(IERC20 token) external view override returns (uint128 protocolRevenues) {
93+
protocolRevenues = _protocolRevenues[token];
94+
}
95+
6096
/// @inheritdoc ISablierV2
6197
function getRecipient(uint256 streamId) public view virtual override returns (address recipient);
6298

@@ -99,7 +135,7 @@ abstract contract SablierV2 is ISablierV2 {
99135
}
100136

101137
/// @inheritdoc ISablierV2
102-
function cancelAll(uint256[] calldata streamIds) external override {
138+
function cancelMultiple(uint256[] calldata streamIds) external override {
103139
// Iterate over the provided array of stream ids and cancel each stream that exists and is cancelable.
104140
uint256 count = streamIds.length;
105141
uint256 streamId;
@@ -118,6 +154,24 @@ abstract contract SablierV2 is ISablierV2 {
118154
}
119155
}
120156

157+
/// @inheritdoc ISablierV2
158+
function claimProtocolRevenues(IERC20 token) external override onlyOwner {
159+
// Checks: the protocol revenues are not zero.
160+
uint128 protocolRevenues = _protocolRevenues[token];
161+
if (protocolRevenues == 0) {
162+
revert Errors.SablierV2__ClaimZeroProtocolRevenues(token);
163+
}
164+
165+
// Effects: set the protocol revenues to zero.
166+
_protocolRevenues[token] = 0;
167+
168+
// Interactions: perform the ERC-20 transfer to pay the protocol revenues.
169+
token.safeTransfer(msg.sender, protocolRevenues);
170+
171+
// Emit an event.
172+
emit Events.ClaimProtocolRevenues(msg.sender, token, protocolRevenues);
173+
}
174+
121175
/// @inheritdoc ISablierV2
122176
function renounce(uint256 streamId) external override streamExists(streamId) {
123177
// Checks: the `msg.sender` is the sender of the stream.
@@ -133,6 +187,20 @@ abstract contract SablierV2 is ISablierV2 {
133187
_renounce(streamId);
134188
}
135189

190+
/// @inheritdoc ISablierV2
191+
function setComptroller(ISablierV2Comptroller newComptroller) external override onlyOwner {
192+
// Effects: set the comptroller.
193+
ISablierV2Comptroller oldComptroller = comptroller;
194+
comptroller = newComptroller;
195+
196+
// Emit an event.
197+
emit Events.SetComptroller({
198+
owner: msg.sender,
199+
oldComptroller: oldComptroller,
200+
newComptroller: newComptroller
201+
});
202+
}
203+
136204
/// @inheritdoc ISablierV2
137205
function withdraw(
138206
uint256 streamId,
@@ -154,7 +222,7 @@ abstract contract SablierV2 is ISablierV2 {
154222
}
155223

156224
/// @inheritdoc ISablierV2
157-
function withdrawAll(uint256[] calldata streamIds, address to, uint128[] calldata amounts) external override {
225+
function withdrawMultiple(uint256[] calldata streamIds, address to, uint128[] calldata amounts) external override {
158226
// Checks: the provided address to withdraw to is not zero.
159227
if (to == address(0)) {
160228
revert Errors.SablierV2__WithdrawToZeroAddress();
@@ -164,7 +232,7 @@ abstract contract SablierV2 is ISablierV2 {
164232
uint256 streamIdsCount = streamIds.length;
165233
uint256 amountsCount = amounts.length;
166234
if (streamIdsCount != amountsCount) {
167-
revert Errors.SablierV2__WithdrawAllArraysNotEqual(streamIdsCount, amountsCount);
235+
revert Errors.SablierV2__WithdrawArraysNotEqual(streamIdsCount, amountsCount);
168236
}
169237

170238
// Iterate over the provided array of stream ids and withdraw from each stream.

0 commit comments

Comments
 (0)