Skip to content

Commit c8bb631

Browse files
Refactor StakeSablierNFT (#41)
* refactor: change "tokenId" to "streamId" docs: improve wording in comments refactor: named params in function calls refactor: rename error * refactor: use stream to refer to Lockup NFT test: move tests to dedicated tests folder style: add prettier and solhint configs ci: add github ci build: include build and test script in package file * tests: comply with bulloak check * ci: rename RPC_URL_SEPOLIA to secrets.SEPOLIA_RPC_URL --------- Co-authored-by: smol-ninja <[email protected]>
1 parent 2bfb594 commit c8bb631

File tree

15 files changed

+223
-118
lines changed

15 files changed

+223
-118
lines changed

.github/workflows/ci.yml

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
name: "CI"
2+
3+
env:
4+
SEPOLIA_RPC_URL: ${{ secrets.SEPOLIA_RPC_URL }}
5+
FOUNDRY_PROFILE: "ci"
6+
7+
on:
8+
workflow_dispatch:
9+
pull_request:
10+
push:
11+
branches:
12+
- "main"
13+
14+
jobs:
15+
lint:
16+
runs-on: "ubuntu-latest"
17+
steps:
18+
- name: "Check out the repo"
19+
uses: "actions/checkout@v4"
20+
21+
- name: "Install Foundry"
22+
uses: "foundry-rs/foundry-toolchain@v1"
23+
24+
- name: "Install Bun"
25+
uses: "oven-sh/setup-bun@v1"
26+
27+
- name: "Install the Node.js dependencies"
28+
run: "bun install"
29+
30+
- name: "Lint the code"
31+
run: "bun run lint"
32+
33+
- name: "Add lint summary"
34+
run: |
35+
echo "## Lint result" >> $GITHUB_STEP_SUMMARY
36+
echo "✅ Passed" >> $GITHUB_STEP_SUMMARY
37+
38+
build:
39+
runs-on: "ubuntu-latest"
40+
steps:
41+
- name: "Check out the repo"
42+
uses: "actions/checkout@v4"
43+
44+
- name: "Install Foundry"
45+
uses: "foundry-rs/foundry-toolchain@v1"
46+
47+
- name: "Install Bun"
48+
uses: "oven-sh/setup-bun@v1"
49+
50+
- name: "Install the Node.js dependencies"
51+
run: "bun install"
52+
53+
- name: "Build the contracts and print their size"
54+
run: "forge build --sizes"
55+
56+
- name: "Add build summary"
57+
run: |
58+
echo "## Build result" >> $GITHUB_STEP_SUMMARY
59+
echo "✅ Passed" >> $GITHUB_STEP_SUMMARY
60+
61+
test:
62+
needs: ["lint", "build"]
63+
runs-on: "ubuntu-latest"
64+
steps:
65+
- name: "Check out the repo"
66+
uses: "actions/checkout@v4"
67+
68+
- name: "Install Foundry"
69+
uses: "foundry-rs/foundry-toolchain@v1"
70+
71+
- name: "Install Bun"
72+
uses: "oven-sh/setup-bun@v1"
73+
74+
- name: "Install the Node.js dependencies"
75+
run: "bun install"
76+
77+
- name: "Show the Foundry config"
78+
run: "forge config"
79+
80+
- name: "Run the tests"
81+
run: "forge test"
82+
83+
- name: "Add test summary"
84+
run: |
85+
echo "## Tests result" >> $GITHUB_STEP_SUMMARY
86+
echo "✅ Passed" >> $GITHUB_STEP_SUMMARY

.prettierignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# directories
2+
broadcast
3+
cache
4+
node_modules
5+
out
6+
7+
# files
8+
*.env
9+
*.log
10+
.DS_Store
11+
.pnp.*
12+
lcov.info
13+
bun.lockb
14+
package-lock.json
15+
pnpm-lock.yaml
16+
yarn.lock

.solhint.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"extends": "solhint:recommended",
3+
"rules": {
4+
"code-complexity": ["error", 8],
5+
"compiler-version": ["error", ">=0.8.13"],
6+
"contract-name-camelcase": "off",
7+
"func-name-mixedcase": "off",
8+
"func-visibility": ["error", { "ignoreConstructors": true }],
9+
"max-line-length": ["error", 124],
10+
"named-parameters-mapping": "warn",
11+
"no-console": "off",
12+
"not-rely-on-time": "off"
13+
}
14+
}

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,12 @@
4040
"private": true,
4141
"repository": "github.com:sablier-labs/examples",
4242
"scripts": {
43+
"build": "forge build",
4344
"clean": "rm -rf cache out",
4445
"lint": "bun run lint:sol && bun run prettier:check",
4546
"lint:sol": "forge fmt --check && bun solhint \"{script,src,test}/**/*.sol\"",
4647
"prettier:check": "prettier --check \"**/*.{json,md,yml}\"",
47-
"prettier:write": "prettier --write \"**/*.{json,md,yml}\""
48+
"prettier:write": "prettier --write \"**/*.{json,md,yml}\"",
49+
"test": "forge test"
4850
}
4951
}

v2/core/StakeSablierNFT.sol

Lines changed: 55 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ contract StakeSablierNFT is Adminable, ERC721Holder, ISablierLockupRecipient {
3131
ERRORS
3232
//////////////////////////////////////////////////////////////////////////*/
3333

34-
error AlreadyStaking(address account, uint256 tokenId);
35-
error DifferentStreamingAsset(uint256 tokenId, IERC20 rewardToken);
34+
error AlreadyStaking(address account, uint256 streamId);
35+
error DifferentStreamingToken(uint256 streamId, IERC20 rewardToken);
3636
error ProvidedRewardTooHigh();
3737
error StakingAlreadyActive();
38-
error UnauthorizedCaller(address account, uint256 tokenId);
38+
error UnauthorizedCaller(address account, uint256 streamId);
3939
error ZeroAddress(address account);
4040
error ZeroAmount();
4141
error ZeroDuration();
@@ -47,8 +47,8 @@ contract StakeSablierNFT is Adminable, ERC721Holder, ISablierLockupRecipient {
4747
event RewardAdded(uint256 reward);
4848
event RewardDurationUpdated(uint256 newDuration);
4949
event RewardPaid(address indexed user, uint256 reward);
50-
event Staked(address indexed user, uint256 tokenId);
51-
event Unstaked(address indexed user, uint256 tokenId);
50+
event Staked(address indexed user, uint256 streamId);
51+
event Unstaked(address indexed user, uint256 streamId);
5252

5353
/*//////////////////////////////////////////////////////////////////////////
5454
USER-FACING STATE
@@ -57,7 +57,7 @@ contract StakeSablierNFT is Adminable, ERC721Holder, ISablierLockupRecipient {
5757
/// @dev The last time when rewards were updated.
5858
uint256 public lastUpdateTime;
5959

60-
/// @dev This should be your own ERC20 token in which the staking rewards will be distributed.
60+
/// @dev This should be your own ERC-20 token in which the staking rewards will be distributed.
6161
IERC20 public rewardERC20Token;
6262

6363
/// @dev Total rewards to be distributed per second.
@@ -74,23 +74,23 @@ contract StakeSablierNFT is Adminable, ERC721Holder, ISablierLockupRecipient {
7474
/// - If you used Lockup Dynamic, you should use the LockupDynamic contract address.
7575
ISablierV2Lockup public sablierLockup;
7676

77-
/// @dev The owner of the streams mapped by tokenId.
78-
mapping(uint256 tokenId => address account) public stakedAssets;
77+
/// @dev The owners of the streams mapped by stream IDs.
78+
mapping(uint256 streamId => address account) public stakedUsers;
7979

80-
/// @dev The staked token ID mapped by each account.
81-
mapping(address account => uint256 tokenId) public stakedTokenId;
80+
/// @dev The staked stream IDs mapped by user addresses.
81+
mapping(address account => uint256 streamId) public stakedStreams;
8282

8383
/// @dev The timestamp when the staking ends.
8484
uint256 public stakingEndTime;
8585

86-
/// @dev The total amount of ERC20 tokens staked through Sablier NFTs.
86+
/// @dev The total amount of ERC-20 tokens staked through Sablier NFTs.
8787
uint256 public totalERC20StakedSupply;
8888

8989
/// @dev Keeps track of the total rewards distributed divided by total staked supply.
9090
uint256 public totalRewardPaidPerERC20Token;
9191

92-
/// @dev The rewards paid to each account per ERC20 token mapped by the account.
93-
mapping(address account => uint256 paidAmount) public userRewardPerERC20Token;
92+
/// @dev The rewards paid to each account per ERC-20 token mapped by the account.
93+
mapping(address account => uint256 reward) public userRewardPerERC20Token;
9494

9595
/*//////////////////////////////////////////////////////////////////////////
9696
MODIFIERS
@@ -111,8 +111,8 @@ contract StakeSablierNFT is Adminable, ERC721Holder, ISablierLockupRecipient {
111111
//////////////////////////////////////////////////////////////////////////*/
112112

113113
/// @param initialAdmin The address of the initial contract admin.
114-
/// @param rewardERC20Token_ The address of the ERC20 token used for rewards.
115-
/// @param sablierLockup_ The address of the ERC721 Contract.
114+
/// @param rewardERC20Token_ The address of the ERC-20 token used for rewards.
115+
/// @param sablierLockup_ The address of the ERC-721 Contract.
116116
constructor(address initialAdmin, IERC20 rewardERC20Token_, ISablierV2Lockup sablierLockup_) {
117117
admin = initialAdmin;
118118
rewardERC20Token = rewardERC20Token_;
@@ -127,13 +127,12 @@ contract StakeSablierNFT is Adminable, ERC721Holder, ISablierLockupRecipient {
127127
/// @param account The address of the account to calculate available rewards for.
128128
/// @return earned The amount available as rewards for the account.
129129
function calculateUserRewards(address account) public view returns (uint256 earned) {
130-
if (stakedTokenId[account] == 0) {
130+
if (stakedStreams[account] == 0) {
131131
return rewards[account];
132132
}
133133

134-
uint256 amountInStream = _getAmountInStream(stakedTokenId[account]);
134+
uint256 amountInStream = _getAmountInStream(stakedStreams[account]);
135135
uint256 userRewardPerERC20Token_ = userRewardPerERC20Token[account];
136-
137136
uint256 rewardsSinceLastTime = (amountInStream * (rewardPaidPerERC20Token() - userRewardPerERC20Token_)) / 1e18;
138137

139138
return rewardsSinceLastTime + rewards[account];
@@ -144,10 +143,10 @@ contract StakeSablierNFT is Adminable, ERC721Holder, ISablierLockupRecipient {
144143
return block.timestamp < stakingEndTime ? block.timestamp : stakingEndTime;
145144
}
146145

147-
/// @notice Calculates the total rewards distributed per ERC20 token.
148-
/// @dev This is called by `updateReward` which also update the value of `totalRewardPaidPerERC20Token`.
146+
/// @notice Calculates the total rewards distributed per ERC-20 token.
147+
/// @dev This is called by `updateReward`, which also updates the value of `totalRewardPaidPerERC20Token`.
149148
function rewardPaidPerERC20Token() public view returns (uint256) {
150-
// If the total staked supply is zero or staking has ended, return the stored value of reward per ERC20.
149+
// If the total staked supply is zero or staking has ended, return the stored value of reward per ERC-20.
151150
if (totalERC20StakedSupply == 0 || block.timestamp >= stakingEndTime) {
152151
return totalRewardPaidPerERC20Token;
153152
}
@@ -190,10 +189,10 @@ contract StakeSablierNFT is Adminable, ERC721Holder, ISablierLockupRecipient {
190189
uint128 /* recipientAmount */
191190
)
192191
external
193-
updateReward(stakedAssets[streamId])
192+
updateReward(stakedUsers[streamId])
194193
returns (bytes4 selector)
195194
{
196-
// Check: the caller is the lockup contract.
195+
// Check: the caller is the Lockup contract.
197196
if (msg.sender != address(sablierLockup)) {
198197
revert UnauthorizedCaller(msg.sender, streamId);
199198
}
@@ -214,15 +213,15 @@ contract StakeSablierNFT is Adminable, ERC721Holder, ISablierLockupRecipient {
214213
uint128 amount
215214
)
216215
external
217-
updateReward(stakedAssets[streamId])
216+
updateReward(stakedUsers[streamId])
218217
returns (bytes4 selector)
219218
{
220-
// Check: the caller is the lockup contract
219+
// Check: the caller is the Lockup contract
221220
if (msg.sender != address(sablierLockup)) {
222221
revert UnauthorizedCaller(msg.sender, streamId);
223222
}
224223

225-
address staker = stakedAssets[streamId];
224+
address staker = stakedUsers[streamId];
226225

227226
// Check: the staker is not the zero address.
228227
if (staker == address(0)) {
@@ -240,46 +239,46 @@ contract StakeSablierNFT is Adminable, ERC721Holder, ISablierLockupRecipient {
240239

241240
/// @notice Stake a Sablier NFT with specified base asset.
242241
/// @dev The `msg.sender` must approve the staking contract to spend the Sablier NFT before calling this function.
243-
/// One user can only stake one NFT at a time.
244-
/// @param tokenId The tokenId of the Sablier NFT to be staked.
245-
function stake(uint256 tokenId) external updateReward(msg.sender) {
242+
/// One user can only stake one NFT at a time.
243+
/// @param streamId The stream ID of the Sablier NFT to be staked.
244+
function stake(uint256 streamId) external updateReward(msg.sender) {
246245
// Check: the Sablier NFT is streaming the staking asset.
247-
if (sablierLockup.getAsset(tokenId) != rewardERC20Token) {
248-
revert DifferentStreamingAsset(tokenId, rewardERC20Token);
246+
if (sablierLockup.getAsset(streamId) != rewardERC20Token) {
247+
revert DifferentStreamingToken(streamId, rewardERC20Token);
249248
}
250249

251250
// Check: the user is not already staking.
252-
if (stakedTokenId[msg.sender] != 0) {
253-
revert AlreadyStaking(msg.sender, stakedTokenId[msg.sender]);
251+
if (stakedStreams[msg.sender] != 0) {
252+
revert AlreadyStaking(msg.sender, stakedStreams[msg.sender]);
254253
}
255254

256255
// Effect: store the owner of the Sablier NFT.
257-
stakedAssets[tokenId] = msg.sender;
256+
stakedUsers[streamId] = msg.sender;
258257

259-
// Effect: Store the new tokenId against the user address.
260-
stakedTokenId[msg.sender] = tokenId;
258+
// Effect: store the stream ID.
259+
stakedStreams[msg.sender] = streamId;
261260

262261
// Effect: update the total staked amount.
263-
totalERC20StakedSupply += _getAmountInStream(tokenId);
262+
totalERC20StakedSupply += _getAmountInStream(streamId);
264263

265264
// Interaction: transfer NFT to the staking contract.
266-
sablierLockup.safeTransferFrom({ from: msg.sender, to: address(this), tokenId: tokenId });
265+
sablierLockup.safeTransferFrom({ from: msg.sender, to: address(this), tokenId: streamId });
267266

268-
emit Staked(msg.sender, tokenId);
267+
emit Staked(msg.sender, streamId);
269268
}
270269

271270
/// @notice Unstaking a Sablier NFT will transfer the NFT back to the `msg.sender`.
272-
/// @param tokenId The tokenId of the Sablier NFT to be unstaked.
273-
function unstake(uint256 tokenId) public updateReward(msg.sender) {
271+
/// @param streamId The stream ID of the Sablier NFT to be unstaked.
272+
function unstake(uint256 streamId) public updateReward(msg.sender) {
274273
// Check: the caller is the stored owner of the NFT.
275-
if (stakedAssets[tokenId] != msg.sender) {
276-
revert UnauthorizedCaller(msg.sender, tokenId);
274+
if (stakedUsers[streamId] != msg.sender) {
275+
revert UnauthorizedCaller(msg.sender, streamId);
277276
}
278277

279278
// Effect: update the total staked amount.
280-
totalERC20StakedSupply -= _getAmountInStream(tokenId);
279+
totalERC20StakedSupply -= _getAmountInStream(streamId);
281280

282-
_unstake(tokenId, msg.sender);
281+
_unstake({ streamId: streamId, account: msg.sender });
283282
}
284283

285284
/*//////////////////////////////////////////////////////////////////////////
@@ -288,35 +287,35 @@ contract StakeSablierNFT is Adminable, ERC721Holder, ISablierLockupRecipient {
288287

289288
/// @notice Determine the amount available in the stream.
290289
/// @dev The following function determines the amounts of tokens in a stream irrespective of its cancelable status.
291-
function _getAmountInStream(uint256 tokenId) private view returns (uint256 amount) {
290+
function _getAmountInStream(uint256 streamId) private view returns (uint256 amount) {
292291
// The tokens in the stream = amount deposited - amount withdrawn - amount refunded.
293-
return sablierLockup.getDepositedAmount(tokenId) - sablierLockup.getWithdrawnAmount(tokenId)
294-
- sablierLockup.getRefundedAmount(tokenId);
292+
return sablierLockup.getDepositedAmount(streamId) - sablierLockup.getWithdrawnAmount(streamId)
293+
- sablierLockup.getRefundedAmount(streamId);
295294
}
296295

297-
function _unstake(uint256 tokenId, address account) private {
296+
function _unstake(uint256 streamId, address account) private {
298297
// Check: account is not zero.
299298
if (account == address(0)) {
300299
revert ZeroAddress(account);
301300
}
302301

303-
// Effect: delete the owner of the staked token from the storage.
304-
delete stakedAssets[tokenId];
302+
// Effect: delete the owner of the staked stream.
303+
delete stakedUsers[streamId];
305304

306-
// Effect: delete the `tokenId` from the user storage.
307-
delete stakedTokenId[account];
305+
// Effect: delete the Sablier NFT.
306+
delete stakedStreams[account];
308307

309308
// Interaction: transfer stream back to user.
310-
sablierLockup.safeTransferFrom(address(this), account, tokenId);
309+
sablierLockup.safeTransferFrom({ from: address(this), to: account, tokenId: streamId });
311310

312-
emit Unstaked(account, tokenId);
311+
emit Unstaked(account, streamId);
313312
}
314313

315314
/*//////////////////////////////////////////////////////////////////////////
316315
ADMIN FUNCTIONS
317316
//////////////////////////////////////////////////////////////////////////*/
318317

319-
/// @notice Start a Staking period and set the amount of ERC20 tokens to be distributed as rewards in said period.
318+
/// @notice Start a Staking period and set the amount of ERC-20 tokens to be distributed as rewards in said period.
320319
/// @dev The Staking Contract have to already own enough Rewards Tokens to distribute all the rewards, so make sure
321320
/// to send all the tokens to the contract before calling this function.
322321
/// @param rewardAmount The amount of Reward Tokens to be distributed.

v2/core/stake-sablier-nft-test/claim-rewards/claimRewards.tree

Lines changed: 0 additions & 5 deletions
This file was deleted.

0 commit comments

Comments
 (0)