mirror of
https://github.com/vacp2p/staking-reward-streamer.git
synced 2026-01-07 22:43:53 -05:00
import foundry template (#1)
This commit is contained in:
11
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
11
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal 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`?
|
||||
18
.github/workflows/add-issue-to-project-board.yml
vendored
Normal file
18
.github/workflows/add-issue-to-project-board.yml
vendored
Normal 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 }}
|
||||
18
.github/workflows/add-pr-to-project-board.yml
vendored
Normal file
18
.github/workflows/add-pr-to-project-board.yml
vendored
Normal 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
171
.github/workflows/ci.yml
vendored
Normal 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
27
.gitignore
vendored
@@ -1,14 +1,19 @@
|
||||
# Compiler files
|
||||
cache/
|
||||
out/
|
||||
# directories
|
||||
cache
|
||||
node_modules
|
||||
out
|
||||
|
||||
# Ignores development broadcast logs
|
||||
!/broadcast
|
||||
/broadcast/*/31337/
|
||||
/broadcast/**/dry-run/
|
||||
# files
|
||||
*.env
|
||||
*.log
|
||||
.DS_Store
|
||||
.pnp.*
|
||||
lcov.info
|
||||
yarn.lock
|
||||
|
||||
# Docs
|
||||
docs/
|
||||
# broadcasts
|
||||
!broadcast
|
||||
broadcast/*
|
||||
broadcast/*/31337/
|
||||
|
||||
# Dotenv file
|
||||
.env
|
||||
.certora_internal
|
||||
|
||||
18
.prettierignore
Normal file
18
.prettierignore
Normal 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
7
.prettierrc.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
bracketSpacing: true
|
||||
printWidth: 120
|
||||
proseWrap: "always"
|
||||
singleQuote: false
|
||||
tabWidth: 2
|
||||
trailingComma: "all"
|
||||
useTabs: false
|
||||
13
.solhint.json
Normal file
13
.solhint.json
Normal 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
0
CHANGELOG.md
Normal file
16
LICENSE.md
Normal file
16
LICENSE.md
Normal 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
15
PROPERTIES.md
Normal 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
125
README.md
@@ -12,19 +12,18 @@ It represents the accumulated rewards per staked token since the beginning of th
|
||||
Here's how it works:
|
||||
|
||||
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.
|
||||
The increase is calculated as:
|
||||
`rewardIndex += (newRewards * SCALE_FACTOR) / totalStaked`
|
||||
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 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 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. This "resets" their reward accumulation for the next period.
|
||||
2. Whenever new rewards are added to the contract (detected in updateRewardIndex()), the rewardIndex increases. The
|
||||
increase is calculated as: `rewardIndex += (newRewards * SCALE_FACTOR) / totalStaked` 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
|
||||
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
|
||||
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.
|
||||
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
|
||||
single global variable (rewardIndex).
|
||||
Instead of updating each user's rewards every time new rewards are added, we only need to update a single global
|
||||
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.
|
||||
|
||||
@@ -38,111 +37,111 @@ User-specific calculations are done only when a user interacts with the contract
|
||||
|
||||
**Initial setup:**
|
||||
|
||||
* rewardIndex: 0
|
||||
* accountedRewards: 0
|
||||
* Rewards in contract: 0
|
||||
- rewardIndex: 0
|
||||
- accountedRewards: 0
|
||||
- Rewards in contract: 0
|
||||
|
||||
**T1: Alice stakes 10 tokens**
|
||||
|
||||
* Alice's userRewardIndex: 0
|
||||
* Alice's staked tokens: 10
|
||||
* totalStaked: 10 tokens
|
||||
- Alice's userRewardIndex: 0
|
||||
- Alice's staked tokens: 10
|
||||
- totalStaked: 10 tokens
|
||||
|
||||
**T2: Bob stakes 30 tokens**
|
||||
|
||||
* Alice's userRewardIndex: 0
|
||||
* Bob's userRewardIndex: 0
|
||||
* Alice's staked tokens: 10
|
||||
* Bob's staked tokens: 30
|
||||
* totalStaked: 40 tokens
|
||||
- Alice's userRewardIndex: 0
|
||||
- Bob's userRewardIndex: 0
|
||||
- Alice's staked tokens: 10
|
||||
- Bob's staked tokens: 30
|
||||
- totalStaked: 40 tokens
|
||||
|
||||
**T3: 1000 Rewards arrive**
|
||||
|
||||
New rewardIndex calculation:
|
||||
|
||||
* newRewards = 1000
|
||||
* rewardIndex increase = 1000 / 40 = 25
|
||||
* rewardIndex = 0 + 25 = 25
|
||||
* accountedRewards: 1000
|
||||
* Rewards in contract: 1000
|
||||
- newRewards = 1000
|
||||
- rewardIndex increase = 1000 / 40 = 25
|
||||
- rewardIndex = 0 + 25 = 25
|
||||
- accountedRewards: 1000
|
||||
- Rewards in contract: 1000
|
||||
|
||||
Potential Rewards for Alice and Bob:
|
||||
|
||||
For Alice:
|
||||
|
||||
* Staked amount: 10 tokens
|
||||
* Potential Rewards: 10 * (25 - 0) = 250
|
||||
- Staked amount: 10 tokens
|
||||
- Potential Rewards: 10 \* (25 - 0) = 250
|
||||
|
||||
For Bob:
|
||||
|
||||
* Staked amount: 30 tokens
|
||||
* Potential Rewards: 30 * (25 - 0) = 750
|
||||
- Staked amount: 30 tokens
|
||||
- Potential Rewards: 30 \* (25 - 0) = 750
|
||||
|
||||
**T4: Alice withdraws her stake and Rewards**
|
||||
|
||||
Alice's withdrawal:
|
||||
|
||||
* tokens returned: 10
|
||||
* Rewards: 250
|
||||
- tokens returned: 10
|
||||
- Rewards: 250
|
||||
|
||||
Update state:
|
||||
|
||||
* totalStaked = 40 - 10 = 30 tokens
|
||||
* Rewards in contract = 1000 - 250 = 750
|
||||
- totalStaked = 40 - 10 = 30 tokens
|
||||
- Rewards in contract = 1000 - 250 = 750
|
||||
|
||||
**T5: Charlie stakes 30 tokens**
|
||||
|
||||
* Charlie's userRewardIndex: 25
|
||||
* totalStaked = 30 + 30 = 60 tokens
|
||||
- Charlie's userRewardIndex: 25
|
||||
- totalStaked = 30 + 30 = 60 tokens
|
||||
|
||||
**T6: Another 1000 Rewards arrive**
|
||||
|
||||
New rewardIndex calculation:
|
||||
|
||||
* newRewards = 1000
|
||||
* rewardIndex increase = 1000 / 60 = 16.67
|
||||
* new rewardIndex = 25 + 16.67 = 41.67
|
||||
* accountedRewards: 1000 + 1000 = 2000
|
||||
* Rewards in contract = 750 + 1000 = 1750
|
||||
- newRewards = 1000
|
||||
- rewardIndex increase = 1000 / 60 = 16.67
|
||||
- new rewardIndex = 25 + 16.67 = 41.67
|
||||
- accountedRewards: 1000 + 1000 = 2000
|
||||
- Rewards in contract = 750 + 1000 = 1750
|
||||
|
||||
Rewards for Bob and Charlie:
|
||||
|
||||
For Bob:
|
||||
|
||||
* Staked amount: 30 tokens
|
||||
* Potential Rewards: 30 * (41.67 - 0) = 1250.1 // rounding error
|
||||
* In bucket 1: 30 * (25 - 0) = 750
|
||||
* In bucket 2: 30 * (16.67 - 0) = 500.1
|
||||
* Total of b1 + b2: 750 + 500.1 = 1250.1
|
||||
* Which is equal to
|
||||
* 30 * ( (25 - 0) + (41.67 - 25) )
|
||||
- Staked amount: 30 tokens
|
||||
- Potential Rewards: 30 \* (41.67 - 0) = 1250.1 // rounding error
|
||||
- In bucket 1: 30 \* (25 - 0) = 750
|
||||
- In bucket 2: 30 \* (16.67 - 0) = 500.1
|
||||
- Total of b1 + b2: 750 + 500.1 = 1250.1
|
||||
- Which is equal to
|
||||
- 30 \* ( (25 - 0) + (41.67 - 25) )
|
||||
|
||||
For Charlie:
|
||||
|
||||
* Staked amount: 30 tokens
|
||||
* Potential Rewards: 30 * (41.67 - 25) = 500.1 // rounding error
|
||||
- Staked amount: 30 tokens
|
||||
- Potential Rewards: 30 \* (41.67 - 25) = 500.1 // rounding error
|
||||
|
||||
If Bob and Charlie were to withdraw now:
|
||||
|
||||
Bob's withdrawal:
|
||||
|
||||
* tokens returned: 30
|
||||
* Rewards: 1250.1
|
||||
* Rewards in contract after Bob's withdrawal: 1750 - 1250.1 = 499.9
|
||||
- tokens returned: 30
|
||||
- Rewards: 1250.1
|
||||
- Rewards in contract after Bob's withdrawal: 1750 - 1250.1 = 499.9
|
||||
|
||||
Charlie's withdrawal:
|
||||
|
||||
* tokens returned: 30
|
||||
* Rewards: 499.9
|
||||
* Rewards in contract after Charlie's withdrawal: 499.9 - 499.9 = 0
|
||||
- tokens returned: 30
|
||||
- Rewards: 499.9
|
||||
- Rewards in contract after Charlie's withdrawal: 499.9 - 499.9 = 0
|
||||
|
||||
**T7: Final state:**
|
||||
|
||||
* Alice received: 10 tokens and 250 Rewards
|
||||
* Bob received: 30 tokens and 1250.1 Rewards
|
||||
* Charlie received: 30 tokens and 499.9 Rewards
|
||||
* Total Rewards distributed: 2000 Rewards
|
||||
* Rewards remaining in contract: 0
|
||||
- Alice received: 10 tokens and 250 Rewards
|
||||
- Bob received: 30 tokens and 1250.1 Rewards
|
||||
- Charlie received: 30 tokens and 499.9 Rewards
|
||||
- Total Rewards distributed: 2000 Rewards
|
||||
- Rewards remaining in contract: 0
|
||||
|
||||
## Rewards Streamer with Multiplier Points
|
||||
|
||||
|
||||
14
certora/certora.conf
Normal file
14
certora/certora.conf
Normal 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"
|
||||
]
|
||||
}
|
||||
|
||||
3
certora/specs/RewardsStreamerMP.spec
Normal file
3
certora/specs/RewardsStreamerMP.spec
Normal file
@@ -0,0 +1,3 @@
|
||||
rule checkIdOutputIsAlwaysEqualToInput {
|
||||
assert true;
|
||||
}
|
||||
28
codecov.yml
Normal file
28
codecov.yml
Normal 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
|
||||
59
foundry.toml
59
foundry.toml
@@ -1,6 +1,55 @@
|
||||
[profile.default]
|
||||
src = "src"
|
||||
out = "out"
|
||||
libs = ["lib"]
|
||||
# Full reference https://github.com/foundry-rs/foundry/tree/master/config
|
||||
|
||||
# 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}"
|
||||
|
||||
21
githooks/pre-commit-adorno
Normal file
21
githooks/pre-commit-adorno
Normal 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
34
package.json
Normal 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
1801
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1 +1,2 @@
|
||||
forge-std/=lib/forge-std/src/
|
||||
@openzeppelin/contracts=./lib/openzeppelin-contracts/contracts
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import {Script, console} from "forge-std/Script.sol";
|
||||
import {RewardsStreamer} from "../src/RewardsStreamer.sol";
|
||||
import { Script, console } from "forge-std/Script.sol";
|
||||
import { RewardsStreamer } from "../src/RewardsStreamer.sol";
|
||||
|
||||
contract RewardsStreamerScript is Script {
|
||||
RewardsStreamer public rewardsStreamer;
|
||||
|
||||
function setUp() public {}
|
||||
function setUp() public { }
|
||||
|
||||
function run() public {
|
||||
vm.startBroadcast();
|
||||
|
||||
8
slither.config.json
Normal file
8
slither.config.json
Normal 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/"
|
||||
]
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||||
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||||
|
||||
contract RewardsStreamer is ReentrancyGuard {
|
||||
error StakingManager__AmountCannotBeZero();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||||
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||||
|
||||
// Rewards Streamer with Multiplier Points
|
||||
contract RewardsStreamerMP is ReentrancyGuard {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import {Test, console} from "forge-std/Test.sol";
|
||||
import {RewardsStreamer} from "../src/RewardsStreamer.sol";
|
||||
import {MockToken} from "./mocks/MockToken.sol";
|
||||
import { Test, console } from "forge-std/Test.sol";
|
||||
import { RewardsStreamer } from "../src/RewardsStreamer.sol";
|
||||
import { MockToken } from "./mocks/MockToken.sol";
|
||||
|
||||
contract RewardsStreamerTest is Test {
|
||||
MockToken rewardToken;
|
||||
@@ -122,8 +122,8 @@ contract RewardsStreamerTest is Test {
|
||||
})
|
||||
);
|
||||
|
||||
checkUser(CheckUserParams({user: alice, rewardBalance: 0, stakedBalance: 10e18, rewardIndex: 0}));
|
||||
checkUser(CheckUserParams({user: bob, rewardBalance: 0, stakedBalance: 30e18, rewardIndex: 0}));
|
||||
checkUser(CheckUserParams({ user: alice, rewardBalance: 0, stakedBalance: 10e18, rewardIndex: 0 }));
|
||||
checkUser(CheckUserParams({ user: bob, rewardBalance: 0, stakedBalance: 30e18, rewardIndex: 0 }));
|
||||
|
||||
// T4
|
||||
vm.prank(alice);
|
||||
@@ -139,8 +139,8 @@ contract RewardsStreamerTest is Test {
|
||||
})
|
||||
);
|
||||
|
||||
checkUser(CheckUserParams({user: alice, rewardBalance: 250e18, stakedBalance: 0e18, rewardIndex: 25e18}));
|
||||
checkUser(CheckUserParams({user: bob, rewardBalance: 0, stakedBalance: 30e18, rewardIndex: 0}));
|
||||
checkUser(CheckUserParams({ user: alice, rewardBalance: 250e18, stakedBalance: 0e18, rewardIndex: 25e18 }));
|
||||
checkUser(CheckUserParams({ user: bob, rewardBalance: 0, stakedBalance: 30e18, rewardIndex: 0 }));
|
||||
|
||||
// T5
|
||||
vm.prank(charlie);
|
||||
@@ -156,9 +156,9 @@ contract RewardsStreamerTest is Test {
|
||||
})
|
||||
);
|
||||
|
||||
checkUser(CheckUserParams({user: alice, rewardBalance: 250e18, stakedBalance: 0e18, rewardIndex: 25e18}));
|
||||
checkUser(CheckUserParams({user: bob, rewardBalance: 0, stakedBalance: 30e18, rewardIndex: 0}));
|
||||
checkUser(CheckUserParams({user: charlie, rewardBalance: 0, stakedBalance: 30e18, 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: charlie, rewardBalance: 0, stakedBalance: 30e18, rewardIndex: 25e18 }));
|
||||
|
||||
// T6
|
||||
vm.prank(admin);
|
||||
@@ -170,14 +170,14 @@ contract RewardsStreamerTest is Test {
|
||||
totalStaked: 60e18,
|
||||
stakingBalance: 60e18,
|
||||
rewardBalance: 1750e18,
|
||||
rewardIndex: 41666666666666666666,
|
||||
rewardIndex: 41_666_666_666_666_666_666,
|
||||
accountedRewards: 1750e18
|
||||
})
|
||||
);
|
||||
|
||||
checkUser(CheckUserParams({user: alice, rewardBalance: 250e18, stakedBalance: 0, rewardIndex: 25e18}));
|
||||
checkUser(CheckUserParams({user: bob, rewardBalance: 0, stakedBalance: 30e18, rewardIndex: 0}));
|
||||
checkUser(CheckUserParams({user: charlie, rewardBalance: 0, stakedBalance: 30e18, 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: charlie, rewardBalance: 0, stakedBalance: 30e18, rewardIndex: 25e18 }));
|
||||
|
||||
//T7
|
||||
vm.prank(bob);
|
||||
@@ -188,18 +188,18 @@ contract RewardsStreamerTest is Test {
|
||||
totalStaked: 30e18,
|
||||
stakingBalance: 30e18,
|
||||
rewardBalance: 500e18 + 20, // 500e18 (with rounding error of 20 wei)
|
||||
rewardIndex: 41666666666666666666,
|
||||
rewardIndex: 41_666_666_666_666_666_666,
|
||||
accountedRewards: 500e18 + 20
|
||||
})
|
||||
);
|
||||
|
||||
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: 1249999999999999999980, // 750e18 + 500e18 (with rounding error)
|
||||
rewardBalance: 1_249_999_999_999_999_999_980, // 750e18 + 500e18 (with rounding error)
|
||||
stakedBalance: 0,
|
||||
rewardIndex: 41666666666666666666
|
||||
rewardIndex: 41_666_666_666_666_666_666
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import {Test, console} from "forge-std/Test.sol";
|
||||
import {RewardsStreamerMP} from "../src/RewardsStreamerMP.sol";
|
||||
import {MockToken} from "./mocks/MockToken.sol";
|
||||
import { Test, console } from "forge-std/Test.sol";
|
||||
import { RewardsStreamerMP } from "../src/RewardsStreamerMP.sol";
|
||||
import { MockToken } from "./mocks/MockToken.sol";
|
||||
import "forge-std/console.sol";
|
||||
|
||||
contract RewardsStreamerMPTest is Test {
|
||||
@@ -222,7 +222,8 @@ contract RewardsStreamerMPTest is Test {
|
||||
CheckStreamerParams({
|
||||
totalStaked: 30e18,
|
||||
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,
|
||||
rewardBalance: 750e18,
|
||||
rewardIndex: 10e18,
|
||||
@@ -313,7 +314,7 @@ contract RewardsStreamerMPTest is Test {
|
||||
potentialMP: 225e18,
|
||||
stakingBalance: 60e18,
|
||||
rewardBalance: 1750e18,
|
||||
rewardIndex: 17407407407407407407,
|
||||
rewardIndex: 17_407_407_407_407_407_407,
|
||||
accountedRewards: 1750e18
|
||||
})
|
||||
);
|
||||
@@ -363,9 +364,9 @@ contract RewardsStreamerMPTest is Test {
|
||||
potentialMP: 120e18,
|
||||
stakingBalance: 30e18,
|
||||
// 1750 - (750 + 555.55) = 444.44
|
||||
rewardBalance: 444444444444444444475,
|
||||
rewardIndex: 17407407407407407407,
|
||||
accountedRewards: 444444444444444444475
|
||||
rewardBalance: 444_444_444_444_444_444_475,
|
||||
rewardIndex: 17_407_407_407_407_407_407,
|
||||
accountedRewards: 444_444_444_444_444_444_475
|
||||
})
|
||||
);
|
||||
|
||||
@@ -390,9 +391,9 @@ contract RewardsStreamerMPTest is Test {
|
||||
// total weight = 135
|
||||
// bobs rewards = 1000 * 75 / 135 = 555.555555555555555555
|
||||
// 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,
|
||||
rewardIndex: 17407407407407407407,
|
||||
rewardIndex: 17_407_407_407_407_407_407,
|
||||
userMP: 0,
|
||||
userPotentialMP: 0
|
||||
})
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
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 {
|
||||
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 {
|
||||
_mint(account, amount);
|
||||
|
||||
Reference in New Issue
Block a user