import foundry template (#1)

This commit is contained in:
Andrea Franz
2024-09-25 09:39:14 +02:00
committed by GitHub
parent 197262ba79
commit 4ef75621a3
27 changed files with 2367 additions and 116 deletions

11
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,11 @@
## Description
Describe the changes made in your pull request here.
## Checklist
Ensure you completed **all of the steps** below before submitting your pull request:
- [ ] Added natspec comments?
- [ ] Ran `pnpm adorno`?
- [ ] Ran `pnpm verify`?

View File

@@ -0,0 +1,18 @@
name: Add issue to task board
on:
issues:
types:
- opened
jobs:
add-to-project:
name: Add to task board
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@v0.5.0
with:
# You can target a project in a different organization
# to the issue
project-url: https://github.com/orgs/vacp2p/projects/10
github-token: ${{ secrets.ADD_TO_VAC_BOARD_PAT }}

View File

@@ -0,0 +1,18 @@
name: Add PR task board
on:
pull_request:
types:
- opened
jobs:
add-to-project:
name: Add to task board
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@v0.5.0
with:
# You can target a project in a different organization
# to the issue
project-url: https://github.com/orgs/vacp2p/projects/10
github-token: ${{ secrets.ADD_TO_VAC_BOARD_PAT }}

171
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,171 @@
name: "CI"
env:
API_KEY_ALCHEMY: ${{ secrets.API_KEY_ALCHEMY }}
FOUNDRY_PROFILE: "ci"
on:
workflow_dispatch:
pull_request:
push:
branches:
- "main"
concurrency:
cancel-in-progress: true
group: ${{github.workflow}}-${{github.ref}}
jobs:
lint:
runs-on: "ubuntu-latest"
steps:
- name: "Check out the repo"
uses: "actions/checkout@v4"
with:
submodules: "recursive"
- name: "Install Foundry"
uses: "foundry-rs/foundry-toolchain@v1"
- name: "Install Pnpm"
uses: "pnpm/action-setup@v2"
with:
version: "8"
- name: "Install Node.js"
uses: "actions/setup-node@v3"
with:
cache: "pnpm"
node-version: "lts/*"
- name: "Install the Node.js dependencies"
run: "pnpm install"
- name: "Lint the contracts"
run: "pnpm lint"
- name: "Add lint summary"
run: |
echo "## Lint result" >> $GITHUB_STEP_SUMMARY
echo "✅ Passed" >> $GITHUB_STEP_SUMMARY
build:
runs-on: "ubuntu-latest"
steps:
- name: "Check out the repo"
uses: "actions/checkout@v4"
with:
submodules: "recursive"
- name: "Install Foundry"
uses: "foundry-rs/foundry-toolchain@v1"
- name: "Build the contracts and print their size"
run: "forge build --sizes"
- name: "Add build summary"
run: |
echo "## Build result" >> $GITHUB_STEP_SUMMARY
echo "✅ Passed" >> $GITHUB_STEP_SUMMARY
test:
needs: ["lint", "build"]
runs-on: "ubuntu-latest"
steps:
- name: "Check out the repo"
uses: "actions/checkout@v4"
with:
submodules: "recursive"
- name: "Install Foundry"
uses: "foundry-rs/foundry-toolchain@v1"
- name: "Show the Foundry config"
run: "forge config"
- name: "Generate a fuzz seed that changes weekly to avoid burning through RPC allowance"
run: >
echo "FOUNDRY_FUZZ_SEED=$(
echo $(($EPOCHSECONDS - $EPOCHSECONDS % 604800))
)" >> $GITHUB_ENV
- name: "Run the tests"
run: "forge test"
- name: "Add test summary"
run: |
echo "## Tests result" >> $GITHUB_STEP_SUMMARY
echo "✅ Passed" >> $GITHUB_STEP_SUMMARY
coverage:
needs: ["lint", "build"]
runs-on: "ubuntu-latest"
steps:
- name: "Check out the repo"
uses: "actions/checkout@v4"
with:
submodules: "recursive"
- name: "Install Foundry"
uses: "foundry-rs/foundry-toolchain@v1"
- name: "Generate the coverage report using the unit and the integration tests"
run: 'forge coverage --match-path "test/**/*.sol" --report lcov'
- name: "Upload coverage report to Codecov"
uses: "codecov/codecov-action@v3"
with:
files: "./lcov.info"
- name: "Add coverage summary"
run: |
echo "## Coverage result" >> $GITHUB_STEP_SUMMARY
echo "✅ Uploaded to Codecov" >> $GITHUB_STEP_SUMMARY
verify:
needs: ["lint", "build"]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Python
uses: actions/setup-python@v2
with: { python-version: 3.9 }
- name: Install Java
uses: actions/setup-java@v1
with: { java-version: "11", java-package: jre }
- name: Install Certora CLI
run: pip3 install certora-cli==5.0.5
- name: Install Solidity
run: |
wget https://github.com/ethereum/solidity/releases/download/v0.8.26/solc-static-linux
chmod +x solc-static-linux
sudo mv solc-static-linux /usr/local/bin/solc
- name: "Install Pnpm"
uses: "pnpm/action-setup@v2"
with:
version: "8"
- name: "Install Node.js"
uses: "actions/setup-node@v3"
with:
cache: "pnpm"
node-version: "lts/*"
- name: "Install the Node.js dependencies"
run: "pnpm install"
- name: Verify rules
run: "pnpm verify"
env:
CERTORAKEY: ${{ secrets.CERTORAKEY }}
strategy:
fail-fast: false
max-parallel: 16

27
.gitignore vendored
View File

@@ -1,14 +1,19 @@
# Compiler files # directories
cache/ cache
out/ node_modules
out
# Ignores development broadcast logs # files
!/broadcast *.env
/broadcast/*/31337/ *.log
/broadcast/**/dry-run/ .DS_Store
.pnp.*
lcov.info
yarn.lock
# Docs # broadcasts
docs/ !broadcast
broadcast/*
broadcast/*/31337/
# Dotenv file .certora_internal
.env

18
.prettierignore Normal file
View File

@@ -0,0 +1,18 @@
# directories
broadcast
cache
lib
node_modules
out
# files
*.env
*.log
.DS_Store
.pnp.*
lcov.info
package-lock.json
pnpm-lock.yaml
yarn.lock
slither.config.json

7
.prettierrc.yml Normal file
View File

@@ -0,0 +1,7 @@
bracketSpacing: true
printWidth: 120
proseWrap: "always"
singleQuote: false
tabWidth: 2
trailingComma: "all"
useTabs: false

13
.solhint.json Normal file
View File

@@ -0,0 +1,13 @@
{
"extends": "solhint:recommended",
"rules": {
"code-complexity": ["error", 8],
"compiler-version": ["error", ">=0.8.26"],
"func-name-mixedcase": "off",
"func-visibility": ["error", { "ignoreConstructors": true }],
"max-line-length": ["error", 120],
"named-parameters-mapping": "warn",
"no-console": "off",
"not-rely-on-time": "off"
}
}

0
CHANGELOG.md Normal file
View File

16
LICENSE.md Normal file
View File

@@ -0,0 +1,16 @@
MIT License
Copyright (c) 2024 Institute of Free Technology
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

15
PROPERTIES.md Normal file
View File

@@ -0,0 +1,15 @@
## Protocol properties and invariants
Below is a list of all documented properties and invariants of this project that must hold true.
- **Property** - Describes the property of the project / protocol that should ultimately be tested and formaly verified.
- **Type** - Properties are split into 5 main types: **Valid State**, **State Transition**, **Variable Transition**,
**High-Level Property**, **Unit Test**
- **Risk** - One of **High**, **Medium** and **Low**, depending on the property's risk factor
- **Tested** - Whether this property has been (fuzz) tested
| **Property** | **Type** | **Risk** | **Tested** |
| ------------ | -------- | -------- | ---------- |
| | | | |
| | | | |
| | | | |

125
README.md
View File

@@ -12,19 +12,18 @@ It represents the accumulated rewards per staked token since the beginning of th
Here's how it works: Here's how it works:
1. Initial state: When the contract starts, rewardIndex is 0. 1. Initial state: When the contract starts, rewardIndex is 0.
2. Whenever new rewards are added to the contract (detected in updateRewardIndex()), the rewardIndex increases. 2. Whenever new rewards are added to the contract (detected in updateRewardIndex()), the rewardIndex increases. The
The increase is calculated as: increase is calculated as: `rewardIndex += (newRewards * SCALE_FACTOR) / totalStaked` This calculation distributes
`rewardIndex += (newRewards * SCALE_FACTOR) / totalStaked` the new rewards evenly across all staked tokens.
This calculation distributes the new rewards evenly across all staked tokens. 3. Each user has their own userRewardIndex, which represents the global rewardIndex at the time of their last
3. Each user has their own userRewardIndex, which represents the global rewardIndex at the time interaction (stake, unstake, or reward claim).
of their last interaction (stake, unstake, or reward claim). 4. When a user wants to claim rewards, we calculate the difference between the current rewardIndex and the user's
4. When a user wants to claim rewards, we calculate the difference between the current rewardIndex userRewardIndex, multiply it by their staked balance, and divide by SCALE_FACTOR
and the user's userRewardIndex, multiply it by their staked balance, and divide by SCALE_FACTOR 5. After a user stakes, unstakes, or claims rewards, their userRewardIndex is updated to the current global rewardIndex.
5. After a user stakes, unstakes, or claims rewards, their userRewardIndex is updated to the current This "resets" their reward accumulation for the next period.
global rewardIndex. This "resets" their reward accumulation for the next period.
Instead of updating each user's rewards every time new rewards are added, we only need to update a Instead of updating each user's rewards every time new rewards are added, we only need to update a single global
single global variable (rewardIndex). variable (rewardIndex).
We don't need to assign Rewards to epochs, so we don't need to finalize Rewords for each epoch and each user. We don't need to assign Rewards to epochs, so we don't need to finalize Rewords for each epoch and each user.
@@ -38,111 +37,111 @@ User-specific calculations are done only when a user interacts with the contract
**Initial setup:** **Initial setup:**
* rewardIndex: 0 - rewardIndex: 0
* accountedRewards: 0 - accountedRewards: 0
* Rewards in contract: 0 - Rewards in contract: 0
**T1: Alice stakes 10 tokens** **T1: Alice stakes 10 tokens**
* Alice's userRewardIndex: 0 - Alice's userRewardIndex: 0
* Alice's staked tokens: 10 - Alice's staked tokens: 10
* totalStaked: 10 tokens - totalStaked: 10 tokens
**T2: Bob stakes 30 tokens** **T2: Bob stakes 30 tokens**
* Alice's userRewardIndex: 0 - Alice's userRewardIndex: 0
* Bob's userRewardIndex: 0 - Bob's userRewardIndex: 0
* Alice's staked tokens: 10 - Alice's staked tokens: 10
* Bob's staked tokens: 30 - Bob's staked tokens: 30
* totalStaked: 40 tokens - totalStaked: 40 tokens
**T3: 1000 Rewards arrive** **T3: 1000 Rewards arrive**
New rewardIndex calculation: New rewardIndex calculation:
* newRewards = 1000 - newRewards = 1000
* rewardIndex increase = 1000 / 40 = 25 - rewardIndex increase = 1000 / 40 = 25
* rewardIndex = 0 + 25 = 25 - rewardIndex = 0 + 25 = 25
* accountedRewards: 1000 - accountedRewards: 1000
* Rewards in contract: 1000 - Rewards in contract: 1000
Potential Rewards for Alice and Bob: Potential Rewards for Alice and Bob:
For Alice: For Alice:
* Staked amount: 10 tokens - Staked amount: 10 tokens
* Potential Rewards: 10 * (25 - 0) = 250 - Potential Rewards: 10 \* (25 - 0) = 250
For Bob: For Bob:
* Staked amount: 30 tokens - Staked amount: 30 tokens
* Potential Rewards: 30 * (25 - 0) = 750 - Potential Rewards: 30 \* (25 - 0) = 750
**T4: Alice withdraws her stake and Rewards** **T4: Alice withdraws her stake and Rewards**
Alice's withdrawal: Alice's withdrawal:
* tokens returned: 10 - tokens returned: 10
* Rewards: 250 - Rewards: 250
Update state: Update state:
* totalStaked = 40 - 10 = 30 tokens - totalStaked = 40 - 10 = 30 tokens
* Rewards in contract = 1000 - 250 = 750 - Rewards in contract = 1000 - 250 = 750
**T5: Charlie stakes 30 tokens** **T5: Charlie stakes 30 tokens**
* Charlie's userRewardIndex: 25 - Charlie's userRewardIndex: 25
* totalStaked = 30 + 30 = 60 tokens - totalStaked = 30 + 30 = 60 tokens
**T6: Another 1000 Rewards arrive** **T6: Another 1000 Rewards arrive**
New rewardIndex calculation: New rewardIndex calculation:
* newRewards = 1000 - newRewards = 1000
* rewardIndex increase = 1000 / 60 = 16.67 - rewardIndex increase = 1000 / 60 = 16.67
* new rewardIndex = 25 + 16.67 = 41.67 - new rewardIndex = 25 + 16.67 = 41.67
* accountedRewards: 1000 + 1000 = 2000 - accountedRewards: 1000 + 1000 = 2000
* Rewards in contract = 750 + 1000 = 1750 - Rewards in contract = 750 + 1000 = 1750
Rewards for Bob and Charlie: Rewards for Bob and Charlie:
For Bob: For Bob:
* Staked amount: 30 tokens - Staked amount: 30 tokens
* Potential Rewards: 30 * (41.67 - 0) = 1250.1 // rounding error - Potential Rewards: 30 \* (41.67 - 0) = 1250.1 // rounding error
* In bucket 1: 30 * (25 - 0) = 750 - In bucket 1: 30 \* (25 - 0) = 750
* In bucket 2: 30 * (16.67 - 0) = 500.1 - In bucket 2: 30 \* (16.67 - 0) = 500.1
* Total of b1 + b2: 750 + 500.1 = 1250.1 - Total of b1 + b2: 750 + 500.1 = 1250.1
* Which is equal to - Which is equal to
* 30 * ( (25 - 0) + (41.67 - 25) ) - 30 \* ( (25 - 0) + (41.67 - 25) )
For Charlie: For Charlie:
* Staked amount: 30 tokens - Staked amount: 30 tokens
* Potential Rewards: 30 * (41.67 - 25) = 500.1 // rounding error - Potential Rewards: 30 \* (41.67 - 25) = 500.1 // rounding error
If Bob and Charlie were to withdraw now: If Bob and Charlie were to withdraw now:
Bob's withdrawal: Bob's withdrawal:
* tokens returned: 30 - tokens returned: 30
* Rewards: 1250.1 - Rewards: 1250.1
* Rewards in contract after Bob's withdrawal: 1750 - 1250.1 = 499.9 - Rewards in contract after Bob's withdrawal: 1750 - 1250.1 = 499.9
Charlie's withdrawal: Charlie's withdrawal:
* tokens returned: 30 - tokens returned: 30
* Rewards: 499.9 - Rewards: 499.9
* Rewards in contract after Charlie's withdrawal: 499.9 - 499.9 = 0 - Rewards in contract after Charlie's withdrawal: 499.9 - 499.9 = 0
**T7: Final state:** **T7: Final state:**
* Alice received: 10 tokens and 250 Rewards - Alice received: 10 tokens and 250 Rewards
* Bob received: 30 tokens and 1250.1 Rewards - Bob received: 30 tokens and 1250.1 Rewards
* Charlie received: 30 tokens and 499.9 Rewards - Charlie received: 30 tokens and 499.9 Rewards
* Total Rewards distributed: 2000 Rewards - Total Rewards distributed: 2000 Rewards
* Rewards remaining in contract: 0 - Rewards remaining in contract: 0
## Rewards Streamer with Multiplier Points ## Rewards Streamer with Multiplier Points

14
certora/certora.conf Normal file
View File

@@ -0,0 +1,14 @@
{
"files": ["src/RewardsStreamerMP.sol"],
"msg": "Verifying RewardsStreamerMP.sol",
"rule_sanity": "basic",
"verify": "RewardsStreamerMP:certora/specs/RewardsStreamerMP.spec",
"wait_for_results": "all",
"optimistic_loop": true,
"loop_iter": "3",
"packages": [
"forge-std=lib/forge-std/src",
"@openzeppelin=lib/openzeppelin-contracts"
]
}

View File

@@ -0,0 +1,3 @@
rule checkIdOutputIsAlwaysEqualToInput {
assert true;
}

28
codecov.yml Normal file
View File

@@ -0,0 +1,28 @@
codecov:
require_ci_to_pass: false
comment: false
ignore:
- "script"
- "test"
coverage:
status:
project:
default:
# advanced settings
# Prevents PR from being blocked with a reduction in coverage.
# Note, if we want to re-enable this, a `threshold` value can be used
# allow coverage to drop by x% while still posting a success status.
# `informational`: https://docs.codecov.com/docs/commit-status#informational
# `threshold`: https://docs.codecov.com/docs/commit-status#threshold
informational: true
patch:
default:
# advanced settings
# Prevents PR from being blocked with a reduction in coverage.
# Note, if we want to re-enable this, a `threshold` value can be used
# allow coverage to drop by x% while still posting a success status.
# `informational`: https://docs.codecov.com/docs/commit-status#informational
# `threshold`: https://docs.codecov.com/docs/commit-status#threshold
informational: true

View File

@@ -1,6 +1,55 @@
[profile.default] # Full reference https://github.com/foundry-rs/foundry/tree/master/config
src = "src"
out = "out"
libs = ["lib"]
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options [profile.default]
auto_detect_solc = false
block_timestamp = 1_680_220_800 # March 31, 2023 at 00:00 GMT
bytecode_hash = "none"
cbor_metadata = false
evm_version = "paris"
fuzz = { runs = 1_000 }
gas_reports = ["*"]
libs = ["lib"]
optimizer = true
optimizer_runs = 10_000
out = "out"
script = "script"
solc = "0.8.26"
src = "src"
test = "test"
[profile.ci]
fuzz = { runs = 10_000 }
verbosity = 4
[etherscan]
arbitrum = { key = "${API_KEY_ARBISCAN}" }
avalanche = { key = "${API_KEY_SNOWTRACE}" }
bnb_smart_chain = { key = "${API_KEY_BSCSCAN}" }
gnosis_chain = { key = "${API_KEY_GNOSISSCAN}" }
goerli = { key = "${API_KEY_ETHERSCAN}" }
mainnet = { key = "${API_KEY_ETHERSCAN}" }
optimism = { key = "${API_KEY_OPTIMISTIC_ETHERSCAN}" }
polygon = { key = "${API_KEY_POLYGONSCAN}" }
sepolia = { key = "${API_KEY_ETHERSCAN}" }
[fmt]
bracket_spacing = true
int_types = "long"
line_length = 120
multiline_func_header = "all"
number_underscore = "thousands"
quote_style = "double"
tab_width = 4
wrap_comments = true
[rpc_endpoints]
arbitrum = "https://arbitrum-mainnet.infura.io/v3/${API_KEY_INFURA}"
avalanche = "https://avalanche-mainnet.infura.io/v3/${API_KEY_INFURA}"
bnb_smart_chain = "https://bsc-dataseed.binance.org"
gnosis_chain = "https://rpc.gnosischain.com"
goerli = "https://goerli.infura.io/v3/${API_KEY_INFURA}"
localhost = "http://localhost:8545"
mainnet = "https://eth-mainnet.g.alchemy.com/v2/${API_KEY_ALCHEMY}"
optimism = "https://optimism-mainnet.infura.io/v3/${API_KEY_INFURA}"
polygon = "https://polygon-mainnet.infura.io/v3/${API_KEY_INFURA}"
sepolia = "https://sepolia.infura.io/v3/${API_KEY_INFURA}"

View File

@@ -0,0 +1,21 @@
#!/bin/bash
foundryup
if [ $? -ne 0 ]; then
echo "foundryup failed."
exit 1
fi
pnpm run adorno
if [ $? -ne 0 ]; then
echo "pnpm run adorno failed."
exit 1
fi
git add .
echo "Successfully ran pnpm run adorno and added modified files."
exit 0

34
package.json Normal file
View File

@@ -0,0 +1,34 @@
{
"name": "@vacp2p/staking-rewards-streamer",
"description": "",
"version": "0.1.0",
"author": {
"url": "https://github.com/vacp2p"
},
"devDependencies": {
"prettier": "^3.0.0",
"solhint-community": "^3.6.0",
"commit-and-tag-version": "^12.2.0"
},
"keywords": [
"blockchain",
"ethereum",
"forge",
"foundry",
"smart-contracts",
"solidity",
"template"
],
"private": true,
"scripts": {
"clean": "rm -rf cache out",
"lint": "pnpm lint:sol && pnpm prettier:check",
"verify": "certoraRun certora/certora.conf",
"lint:sol": "forge fmt --check && pnpm solhint {script,src,test,certora}/**/*.sol",
"prettier:check": "prettier --check **/*.{json,md,yml} --ignore-path=.prettierignore",
"prettier:write": "prettier --write **/*.{json,md,yml} --ignore-path=.prettierignore",
"gas-report": "forge snapshot --gas-report 2>&1 | (tee /dev/tty | awk '/Suite result:/ {found=1; buffer=\"\"; next} found && !/Ran/ {buffer=buffer $0 ORS} /Ran/ {found=0} END {printf \"%s\", buffer}' > .gas-report)",
"release": "commit-and-tag-version",
"adorno": "pnpm prettier:write && forge fmt && pnpm gas-report"
}
}

1801
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1,2 @@
forge-std/=lib/forge-std/src/
@openzeppelin/contracts=./lib/openzeppelin-contracts/contracts @openzeppelin/contracts=./lib/openzeppelin-contracts/contracts

View File

@@ -1,13 +1,13 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pragma solidity ^0.8.26; pragma solidity ^0.8.26;
import {Script, console} from "forge-std/Script.sol"; import { Script, console } from "forge-std/Script.sol";
import {RewardsStreamer} from "../src/RewardsStreamer.sol"; import { RewardsStreamer } from "../src/RewardsStreamer.sol";
contract RewardsStreamerScript is Script { contract RewardsStreamerScript is Script {
RewardsStreamer public rewardsStreamer; RewardsStreamer public rewardsStreamer;
function setUp() public {} function setUp() public { }
function run() public { function run() public {
vm.startBroadcast(); vm.startBroadcast();

8
slither.config.json Normal file
View File

@@ -0,0 +1,8 @@
{
"detectors_to_exclude": "naming-convention,reentrancy-events,solc-version,timestamp",
"filter_paths": "(lib|test)",
"solc_remaps": [
"@openzeppelin/contracts=lib/openzeppelin-contracts/contracts/",
"forge-std/=lib/forge-std/src/"
]
}

View File

@@ -1,8 +1,8 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pragma solidity ^0.8.26; pragma solidity ^0.8.26;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract RewardsStreamer is ReentrancyGuard { contract RewardsStreamer is ReentrancyGuard {
error StakingManager__AmountCannotBeZero(); error StakingManager__AmountCannotBeZero();

View File

@@ -1,8 +1,8 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pragma solidity ^0.8.26; pragma solidity ^0.8.26;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
// Rewards Streamer with Multiplier Points // Rewards Streamer with Multiplier Points
contract RewardsStreamerMP is ReentrancyGuard { contract RewardsStreamerMP is ReentrancyGuard {

View File

@@ -1,9 +1,9 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pragma solidity ^0.8.26; pragma solidity ^0.8.26;
import {Test, console} from "forge-std/Test.sol"; import { Test, console } from "forge-std/Test.sol";
import {RewardsStreamer} from "../src/RewardsStreamer.sol"; import { RewardsStreamer } from "../src/RewardsStreamer.sol";
import {MockToken} from "./mocks/MockToken.sol"; import { MockToken } from "./mocks/MockToken.sol";
contract RewardsStreamerTest is Test { contract RewardsStreamerTest is Test {
MockToken rewardToken; MockToken rewardToken;
@@ -122,8 +122,8 @@ contract RewardsStreamerTest is Test {
}) })
); );
checkUser(CheckUserParams({user: alice, rewardBalance: 0, stakedBalance: 10e18, rewardIndex: 0})); checkUser(CheckUserParams({ user: alice, rewardBalance: 0, stakedBalance: 10e18, rewardIndex: 0 }));
checkUser(CheckUserParams({user: bob, rewardBalance: 0, stakedBalance: 30e18, rewardIndex: 0})); checkUser(CheckUserParams({ user: bob, rewardBalance: 0, stakedBalance: 30e18, rewardIndex: 0 }));
// T4 // T4
vm.prank(alice); vm.prank(alice);
@@ -139,8 +139,8 @@ contract RewardsStreamerTest is Test {
}) })
); );
checkUser(CheckUserParams({user: alice, rewardBalance: 250e18, stakedBalance: 0e18, rewardIndex: 25e18})); checkUser(CheckUserParams({ user: alice, rewardBalance: 250e18, stakedBalance: 0e18, rewardIndex: 25e18 }));
checkUser(CheckUserParams({user: bob, rewardBalance: 0, stakedBalance: 30e18, rewardIndex: 0})); checkUser(CheckUserParams({ user: bob, rewardBalance: 0, stakedBalance: 30e18, rewardIndex: 0 }));
// T5 // T5
vm.prank(charlie); vm.prank(charlie);
@@ -156,9 +156,9 @@ contract RewardsStreamerTest is Test {
}) })
); );
checkUser(CheckUserParams({user: alice, rewardBalance: 250e18, stakedBalance: 0e18, rewardIndex: 25e18})); checkUser(CheckUserParams({ user: alice, rewardBalance: 250e18, stakedBalance: 0e18, rewardIndex: 25e18 }));
checkUser(CheckUserParams({user: bob, rewardBalance: 0, stakedBalance: 30e18, rewardIndex: 0})); checkUser(CheckUserParams({ user: bob, rewardBalance: 0, stakedBalance: 30e18, rewardIndex: 0 }));
checkUser(CheckUserParams({user: charlie, rewardBalance: 0, stakedBalance: 30e18, rewardIndex: 25e18})); checkUser(CheckUserParams({ user: charlie, rewardBalance: 0, stakedBalance: 30e18, rewardIndex: 25e18 }));
// T6 // T6
vm.prank(admin); vm.prank(admin);
@@ -170,14 +170,14 @@ contract RewardsStreamerTest is Test {
totalStaked: 60e18, totalStaked: 60e18,
stakingBalance: 60e18, stakingBalance: 60e18,
rewardBalance: 1750e18, rewardBalance: 1750e18,
rewardIndex: 41666666666666666666, rewardIndex: 41_666_666_666_666_666_666,
accountedRewards: 1750e18 accountedRewards: 1750e18
}) })
); );
checkUser(CheckUserParams({user: alice, rewardBalance: 250e18, stakedBalance: 0, rewardIndex: 25e18})); checkUser(CheckUserParams({ user: alice, rewardBalance: 250e18, stakedBalance: 0, rewardIndex: 25e18 }));
checkUser(CheckUserParams({user: bob, rewardBalance: 0, stakedBalance: 30e18, rewardIndex: 0})); checkUser(CheckUserParams({ user: bob, rewardBalance: 0, stakedBalance: 30e18, rewardIndex: 0 }));
checkUser(CheckUserParams({user: charlie, rewardBalance: 0, stakedBalance: 30e18, rewardIndex: 25e18})); checkUser(CheckUserParams({ user: charlie, rewardBalance: 0, stakedBalance: 30e18, rewardIndex: 25e18 }));
//T7 //T7
vm.prank(bob); vm.prank(bob);
@@ -188,18 +188,18 @@ contract RewardsStreamerTest is Test {
totalStaked: 30e18, totalStaked: 30e18,
stakingBalance: 30e18, stakingBalance: 30e18,
rewardBalance: 500e18 + 20, // 500e18 (with rounding error of 20 wei) rewardBalance: 500e18 + 20, // 500e18 (with rounding error of 20 wei)
rewardIndex: 41666666666666666666, rewardIndex: 41_666_666_666_666_666_666,
accountedRewards: 500e18 + 20 accountedRewards: 500e18 + 20
}) })
); );
checkUser(CheckUserParams({user: alice, rewardBalance: 250e18, stakedBalance: 0, rewardIndex: 25e18})); checkUser(CheckUserParams({ user: alice, rewardBalance: 250e18, stakedBalance: 0, rewardIndex: 25e18 }));
checkUser( checkUser(
CheckUserParams({ CheckUserParams({
user: bob, user: bob,
rewardBalance: 1249999999999999999980, // 750e18 + 500e18 (with rounding error) rewardBalance: 1_249_999_999_999_999_999_980, // 750e18 + 500e18 (with rounding error)
stakedBalance: 0, stakedBalance: 0,
rewardIndex: 41666666666666666666 rewardIndex: 41_666_666_666_666_666_666
}) })
); );
} }

View File

@@ -1,9 +1,9 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pragma solidity ^0.8.26; pragma solidity ^0.8.26;
import {Test, console} from "forge-std/Test.sol"; import { Test, console } from "forge-std/Test.sol";
import {RewardsStreamerMP} from "../src/RewardsStreamerMP.sol"; import { RewardsStreamerMP } from "../src/RewardsStreamerMP.sol";
import {MockToken} from "./mocks/MockToken.sol"; import { MockToken } from "./mocks/MockToken.sol";
import "forge-std/console.sol"; import "forge-std/console.sol";
contract RewardsStreamerMPTest is Test { contract RewardsStreamerMPTest is Test {
@@ -222,7 +222,8 @@ contract RewardsStreamerMPTest is Test {
CheckStreamerParams({ CheckStreamerParams({
totalStaked: 30e18, totalStaked: 30e18,
totalMP: 45e18, // 60 - 15 from Alice (10 + 6 months = 5) totalMP: 45e18, // 60 - 15 from Alice (10 + 6 months = 5)
potentialMP: 105e18, // Alice's initial potential MP: 40. 5 already accrued in 6 months. new potentialMP = 140 - 35 = 105 potentialMP: 105e18, // Alice's initial potential MP: 40. 5 already accrued in 6 months. new potentialMP
// = 140 - 35 = 105
stakingBalance: 30e18, stakingBalance: 30e18,
rewardBalance: 750e18, rewardBalance: 750e18,
rewardIndex: 10e18, rewardIndex: 10e18,
@@ -313,7 +314,7 @@ contract RewardsStreamerMPTest is Test {
potentialMP: 225e18, potentialMP: 225e18,
stakingBalance: 60e18, stakingBalance: 60e18,
rewardBalance: 1750e18, rewardBalance: 1750e18,
rewardIndex: 17407407407407407407, rewardIndex: 17_407_407_407_407_407_407,
accountedRewards: 1750e18 accountedRewards: 1750e18
}) })
); );
@@ -363,9 +364,9 @@ contract RewardsStreamerMPTest is Test {
potentialMP: 120e18, potentialMP: 120e18,
stakingBalance: 30e18, stakingBalance: 30e18,
// 1750 - (750 + 555.55) = 444.44 // 1750 - (750 + 555.55) = 444.44
rewardBalance: 444444444444444444475, rewardBalance: 444_444_444_444_444_444_475,
rewardIndex: 17407407407407407407, rewardIndex: 17_407_407_407_407_407_407,
accountedRewards: 444444444444444444475 accountedRewards: 444_444_444_444_444_444_475
}) })
); );
@@ -390,9 +391,9 @@ contract RewardsStreamerMPTest is Test {
// total weight = 135 // total weight = 135
// bobs rewards = 1000 * 75 / 135 = 555.555555555555555555 // bobs rewards = 1000 * 75 / 135 = 555.555555555555555555
// bobs total rewards = 555.55 + 750 of the first bucket = 1305.55 // bobs total rewards = 555.55 + 750 of the first bucket = 1305.55
rewardBalance: 1305555555555555555525, rewardBalance: 1_305_555_555_555_555_555_525,
stakedBalance: 0e18, stakedBalance: 0e18,
rewardIndex: 17407407407407407407, rewardIndex: 17_407_407_407_407_407_407,
userMP: 0, userMP: 0,
userPotentialMP: 0 userPotentialMP: 0
}) })

View File

@@ -1,10 +1,10 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pragma solidity ^0.8.26; pragma solidity ^0.8.26;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockToken is ERC20 { contract MockToken is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {} constructor(string memory name, string memory symbol) ERC20(name, symbol) { }
function mint(address account, uint256 amount) external { function mint(address account, uint256 amount) external {
_mint(account, amount); _mint(account, amount);