From 85dc2b7180a122a8fd496abdeaf07903c5c18a9e Mon Sep 17 00:00:00 2001 From: folex1275 Date: Tue, 7 Oct 2025 22:36:24 +0100 Subject: [PATCH 1/2] Add staking folder --- staking/.git_backup/FETCH_HEAD | 0 staking/.git_backup/HEAD | 1 + staking/.git_backup/config | 8 + staking/.git_backup/description | 1 + .../.git_backup/hooks/applypatch-msg.sample | 25 +++ staking/.git_backup/hooks/commit-msg.sample | 25 +++ staking/.git_backup/hooks/docs.url | 1 + .../hooks/fsmonitor-watchman.sample | 16 ++ staking/.git_backup/hooks/post-update.sample | 12 ++ .../.git_backup/hooks/pre-applypatch.sample | 27 +++ staking/.git_backup/hooks/pre-commit.sample | 19 ++ .../.git_backup/hooks/pre-merge-commit.sample | 16 ++ staking/.git_backup/hooks/pre-push.sample | 46 +++++ staking/.git_backup/hooks/pre-rebase.sample | 40 +++++ .../hooks/prepare-commit-msg.sample | 54 ++++++ staking/.git_backup/info/exclude | 5 + staking/.gitignore | 5 + staking/README.md | 163 ++++++++++++++++++ staking/Scarb.lock | 24 +++ staking/Scarb.toml | 52 ++++++ staking/deploy.sh | 41 +++++ staking/snfoundry.toml | 11 ++ staking/src/erc20.cairo | 12 ++ staking/src/lib.cairo | 4 + staking/src/reward_token.cairo | 116 +++++++++++++ staking/src/staking.cairo | 155 +++++++++++++++++ staking/src/stark_token.cairo | 116 +++++++++++++ 27 files changed, 995 insertions(+) create mode 100644 staking/.git_backup/FETCH_HEAD create mode 100644 staking/.git_backup/HEAD create mode 100644 staking/.git_backup/config create mode 100644 staking/.git_backup/description create mode 100644 staking/.git_backup/hooks/applypatch-msg.sample create mode 100644 staking/.git_backup/hooks/commit-msg.sample create mode 100644 staking/.git_backup/hooks/docs.url create mode 100644 staking/.git_backup/hooks/fsmonitor-watchman.sample create mode 100644 staking/.git_backup/hooks/post-update.sample create mode 100644 staking/.git_backup/hooks/pre-applypatch.sample create mode 100644 staking/.git_backup/hooks/pre-commit.sample create mode 100644 staking/.git_backup/hooks/pre-merge-commit.sample create mode 100644 staking/.git_backup/hooks/pre-push.sample create mode 100644 staking/.git_backup/hooks/pre-rebase.sample create mode 100644 staking/.git_backup/hooks/prepare-commit-msg.sample create mode 100644 staking/.git_backup/info/exclude create mode 100644 staking/.gitignore create mode 100644 staking/README.md create mode 100644 staking/Scarb.lock create mode 100644 staking/Scarb.toml create mode 100644 staking/deploy.sh create mode 100644 staking/snfoundry.toml create mode 100644 staking/src/erc20.cairo create mode 100644 staking/src/lib.cairo create mode 100644 staking/src/reward_token.cairo create mode 100644 staking/src/staking.cairo create mode 100644 staking/src/stark_token.cairo diff --git a/staking/.git_backup/FETCH_HEAD b/staking/.git_backup/FETCH_HEAD new file mode 100644 index 0000000..e69de29 diff --git a/staking/.git_backup/HEAD b/staking/.git_backup/HEAD new file mode 100644 index 0000000..b870d82 --- /dev/null +++ b/staking/.git_backup/HEAD @@ -0,0 +1 @@ +ref: refs/heads/main diff --git a/staking/.git_backup/config b/staking/.git_backup/config new file mode 100644 index 0000000..1ce7b3d --- /dev/null +++ b/staking/.git_backup/config @@ -0,0 +1,8 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true + symlinks = true + ignorecase = false + precomposeunicode = false diff --git a/staking/.git_backup/description b/staking/.git_backup/description new file mode 100644 index 0000000..7ffa683 --- /dev/null +++ b/staking/.git_backup/description @@ -0,0 +1 @@ +Unnamed repository; everything before the `;` is the name of the repository. diff --git a/staking/.git_backup/hooks/applypatch-msg.sample b/staking/.git_backup/hooks/applypatch-msg.sample new file mode 100644 index 0000000..945f2f6 --- /dev/null +++ b/staking/.git_backup/hooks/applypatch-msg.sample @@ -0,0 +1,25 @@ +#!/bin/sh +# A sample hook to check commit messages created by `git am` +########################################################### +# +# When you receive a patch via email, the `git am` command is commonly used to apply +# that patch. During the `git am` process, the `applypatch-msg` hook is executed before +# creating the commit. Its purpose is to validate and modify the commit log message +# before the patch is applied as a commit in your Git repository. +# +# This script serves as an example to validate that the commit message introduced by +# the patch from an email would pass the `commit-msg` hook, which would be executed +# if you had created the commit yourself. +# +# This hook is the first and followed up by `pre-applypatch` and `post-applypatch`. +# +# To enable this hook remove the `.sample` suffix from this file entirely. + +# Retrieve the path of the commit-msg hook script. +commitmsg="$(git rev-parse --git-path hooks/commit-msg)" + +# If the commit-msg hook script is executable, execute it and pass any command-line arguments to it. +test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} + +# Be sure to exit without error if `exec` isn't called. +: diff --git a/staking/.git_backup/hooks/commit-msg.sample b/staking/.git_backup/hooks/commit-msg.sample new file mode 100644 index 0000000..a7f612f --- /dev/null +++ b/staking/.git_backup/hooks/commit-msg.sample @@ -0,0 +1,25 @@ +#!/bin/sh +# A sample hook to check commit messages created by `git commit` +################################################################ +# +# This example script checks commit messages for duplicate `Signed-off-by` +# lines and rejects the commit if these are present. +# +# It is called by "git commit" with a single argument: the name of the file +# that contains the final commit message, which would be used in the commit. +# A a non-zero exit status after issuing an appropriate message stops the operation. +# The hook is allowed to edit the commit message file by rewriting the file +# containing it. +# +# To enable this hook remove the `.sample` suffix from this file entirely. + +# Check for duplicate Signed-off-by lines in the commit message. +# The following command uses grep to find lines starting with "Signed-off-by: " +# in the commit message file specified by the first argument `$1`. +# It then sorts the lines, counts the number of occurrences of each line, +# and removes any lines that occur only once. +# If there are any remaining lines, it means there are duplicate Signed-off-by lines. +test "$(grep '^Signed-off-by: ' "$1" | sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" = "" || { + echo "Remove duplicate Signed-off-by lines and repeat the commit." 1>&2 + exit 1 +} diff --git a/staking/.git_backup/hooks/docs.url b/staking/.git_backup/hooks/docs.url new file mode 100644 index 0000000..bbec397 --- /dev/null +++ b/staking/.git_backup/hooks/docs.url @@ -0,0 +1 @@ +https://git-scm.com/docs/githooks diff --git a/staking/.git_backup/hooks/fsmonitor-watchman.sample b/staking/.git_backup/hooks/fsmonitor-watchman.sample new file mode 100644 index 0000000..cd8985b --- /dev/null +++ b/staking/.git_backup/hooks/fsmonitor-watchman.sample @@ -0,0 +1,16 @@ +#!/usr/bin/sh +# How to use hook-based fs-monitor integrations +############################################### + +# This script is meant as a placeholder for integrating filesystem monitors with git +# using hooks in order to speed up commands like `git-status`. +# +# To setup the fs-monitor for use with watchman, run +# `git config core.fsmonitor .git/hooks/fsmonitor-watchman` and paste the content of +# the example script over at https://github.com/git/git/blob/aa9166bcc0ba654fc21f198a30647ec087f733ed/templates/hooks--fsmonitor-watchman.sample +# into `.git/hooks/fsmonitor-watchman`. +# +# Note that by now and as of this writing on MacOS and Windows and starting from git 2.35.1 +# one can use the built-in fs-monitor implementation using `git config core.fsmonitor true` + +exit 42 diff --git a/staking/.git_backup/hooks/post-update.sample b/staking/.git_backup/hooks/post-update.sample new file mode 100644 index 0000000..506a065 --- /dev/null +++ b/staking/.git_backup/hooks/post-update.sample @@ -0,0 +1,12 @@ +#!/bin/sh +# A sample hook that runs after receiving a pack on a remote +############################################################ +# This hook is called after a pack was received on the remote, i.e. after a successful `git push` operation. +# It's useful on the server side only. +# +# There many more receive hooks which are documented in the official documentation: https://git-scm.com/docs/githooks. +# +# To enable this hook remove the `.sample` suffix from this file entirely. + +# Update static files to support the 'dumb' git HTTP protocol. +exec git update-server-info diff --git a/staking/.git_backup/hooks/pre-applypatch.sample b/staking/.git_backup/hooks/pre-applypatch.sample new file mode 100644 index 0000000..de06c7f --- /dev/null +++ b/staking/.git_backup/hooks/pre-applypatch.sample @@ -0,0 +1,27 @@ +#!/bin/sh +# A sample hook to check commit messages created by `git am` +########################################################### + +# This hook script is triggered by `git am` without any context just before creating a commit, +# which is useful to inspect the current tree or run scripts for further verification. +# +# If it exits with a non-zero exit code, the commit will not be created. Everything printed +# to the output or error channels will be visible to the user. +# +# Note that there is a sibling hook called `post-applypatch` (also without further context) +# which is run after the commit was created. It is useful to use the commit hash for further +# processing, like sending information to the involved parties. +# Finally, the `applypatch-msg` hook is called at the very beginning of the `git am` operation +# to provide access to the commit-message. +# +# To enable this hook remove the `.sample` suffix from this file entirely. + +# Retrieve the path to the pre-commit hook script using the "git rev-parse" command. +precommit="$(git rev-parse --git-path hooks/pre-commit)" + +# Check if the pre-commit hook script exists and is executable. +# If it does, execute it passing the arguments from this script (if any) using the "exec" command. +test -x "$precommit" && exec "$precommit" ${1+"$@"} + +# Be sure to exit without error if `exec` isn't called. +: diff --git a/staking/.git_backup/hooks/pre-commit.sample b/staking/.git_backup/hooks/pre-commit.sample new file mode 100644 index 0000000..9d256d4 --- /dev/null +++ b/staking/.git_backup/hooks/pre-commit.sample @@ -0,0 +1,19 @@ +#!/bin/sh +# A sample hook to prevent commits with merge-markers +##################################################### +# This example hook rejects changes that are about to be committed with merge markers, +# as that would be a clear indication of a failed merge. It is triggered by `git commit` +# and returning with non-zero exit status prevents the commit from being created. +# +# To enable this hook remove the `.sample` suffix from this file entirely. + +# Check for merge markers in modified files +for file in $(git diff --cached --name-only); do + if grep -q -E '^(<<<<<<<|=======|>>>>>>>|\|\|\|\|\|\|\|)$' "$file"; then + echo "Error: File '$file' contains merge markers. Please remove them before committing." + exit 1 + fi +done + +# Exit with success if there are no errors +exit 0 diff --git a/staking/.git_backup/hooks/pre-merge-commit.sample b/staking/.git_backup/hooks/pre-merge-commit.sample new file mode 100644 index 0000000..0896f5b --- /dev/null +++ b/staking/.git_backup/hooks/pre-merge-commit.sample @@ -0,0 +1,16 @@ +#!/bin/sh +# A sample hook to check commits created by `git merge` +####################################################### +# +# This hook is invoked by `git merge` without further context right before creating a commit. +# It should be used to validate the current state that is supposed to be committed, or exit +# with a non-zero status to prevent the commit. +# All output will be visible to the user. +# +# To enable this hook remove the `.sample` suffix from this file entirely. + +# Check if the pre-commit hook exists and is executable. If it is, it executes the pre-commit hook script. +test -x "$GIT_DIR/hooks/pre-commit" && exec "$GIT_DIR/hooks/pre-commit" + +# Be sure to exit without error if `exec` isn't called. +: diff --git a/staking/.git_backup/hooks/pre-push.sample b/staking/.git_backup/hooks/pre-push.sample new file mode 100644 index 0000000..fca1cea --- /dev/null +++ b/staking/.git_backup/hooks/pre-push.sample @@ -0,0 +1,46 @@ +#!/bin/sh +# Check for "DELME" in commit messages of about-to-be-pushed commits +#################################################################### +# This hook script is triggered by `git push` right after a connection to the remote +# was established and its initial response was received, and right before generating +# and pushing a pack-file. +# The operation will be aborted when exiting with a non-zero status. +# +# The following arguments are provided: +# +# $1 - The symbolic name of the remote to push to, like "origin" or the URL like "https://github.com/GitoxideLabs/gitoxide" if there is no such name. +# $2 - The URL of the remote to push to, like "https://github.com/GitoxideLabs/gitoxide". +# +# The hook should then read from standard input in a line-by-line fashion and split the following space-separated fields: +# +# * local ref - the left side of a ref-spec, i.e. "local" of the "local:refs/heads/remote" ref-spec +# * local hash - the hash of the commit pointed to by `local ref` +# * remote ref - the right side of a ref-spec, i.e. "refs/heads/remote" of the "local:refs/heads/remote" ref-spec +# * remote hash - the hash of the commit pointed to by `remote ref` +# +# In this example, we abort the push if any of the about-to-be-pushed commits have "DELME" in their commit message. +# +# To enable this hook remove the `.sample` suffix from this file entirely. + +remote="$1" +url="$2" + +# Check each commit being pushed +while read _local_ref local_hash _remote_ref _remote_hash; do + # Skip if the local hash is all zeroes (deletion) + zero_sha=$(printf "%0${#local_hash}d" 0) + if [ "$local_hash" = "$zero_sha" ]; then + continue + fi + # Get the commit message + commit_msg=$(git log --format=%s -n 1 "$local_hash") + + # Check if the commit message contains "DELME" + if echo "$commit_msg" | grep -iq "DELME"; then + echo "Error: Found commit with 'DELME' in message. Push aborted to $remote ($url) aborted." 1>&2 + exit 1 + fi +done + +# If no commit with "DELME" found, allow the push +exit 0 diff --git a/staking/.git_backup/hooks/pre-rebase.sample b/staking/.git_backup/hooks/pre-rebase.sample new file mode 100644 index 0000000..4850120 --- /dev/null +++ b/staking/.git_backup/hooks/pre-rebase.sample @@ -0,0 +1,40 @@ +#!/bin/sh +# A sample hook to validate the branches involved in a rebase operation +####################################################################### +# +# This hook is invoked right before `git rebase` starts its work and +# prevents anything else to happen by returning a non-zero exit code. +# +# The following arguments are provided: +# +# $1 - the branch that contains the commit from which $2 was forked. +# $2 - the branch being rebased or no second argument at all if the rebase applies to `HEAD`. +# +# This example hook aborts the rebase operation if the branch being rebased is not up to date +# with the latest changes from the upstream branch, or if there are any uncommitted changes. +# +# To enable this hook remove the `.sample` suffix from this file entirely. + +upstream_branch=$1 +if [ "$#" -eq 2 ]; then + branch_being_rebased=$2 +else + branch_being_rebased=$(git symbolic-ref --quiet --short HEAD) || exit 0 # ignore rebases on detached heads +fi + +# Check if the branch being rebased is behind the upstream branch +if git log --oneline ${upstream_branch}..${branch_being_rebased} > /dev/null; then + echo "Warning: The branch being rebased (${branch_being_rebased}) is behind the upstream branch (${upstream_branch})." 1>&2 + echo "Please update your branch before rebasing." 1>&2 + exit 1 +fi + +# Check if there are any uncommitted changes +if ! git diff-index --quiet HEAD --; then + echo "Warning: There are uncommitted changes in your branch ${branch_being_rebased}." 1>&2 + echo "Please commit or stash your changes before rebasing." 1>&2 + exit 2 +fi + +# All good, let the rebase proceed. +exit 0 diff --git a/staking/.git_backup/hooks/prepare-commit-msg.sample b/staking/.git_backup/hooks/prepare-commit-msg.sample new file mode 100644 index 0000000..a38ff5a --- /dev/null +++ b/staking/.git_backup/hooks/prepare-commit-msg.sample @@ -0,0 +1,54 @@ +#!/bin/sh +# A hook called by `git commit` to adjust the commit message right before the user sees it +########################################################################################## +# +# This script is called by `git commit` after commit message was initialized and right before +# an editor is launched. +# +# It receives one to three arguments: +# +# $1 - the path to the file containing the commit message. It can be edited to change the message. +# $2 - the kind of source of the message contained in $1. Possible values are +# "message" - a message was provided via `-m` or `-F` +# "commit" - `-c`, `-C` or `--amend` was given +# "squash" - the `.git/SQUASH_MSG` file exists +# "merge" - this is a merge or the `.git/MERGE` file exists +# "template" - `-t` was provided or `commit.template` was set +# $3 - If $2 is "commit" then this is the hash of the commit. +# It can also take other values, best understood by studying the source code at +# https://github.com/git/git/blob/aa9166bcc0ba654fc21f198a30647ec087f733ed/builtin/commit.c#L745 +# +# The following example +# +# To enable this hook remove the `.sample` suffix from this file entirely. + +COMMIT_MSG_FILE=$1 + +# Check if the commit message file is empty or already contains a message +if [ -s "$COMMIT_MSG_FILE" ]; then + # If the commit message is already provided, exit without making any changes. + # This can happen if the user provided a message via `-m` or a template. + exit 0 +fi + +# Retrieve the branch name from the current HEAD commit +BRANCH_NAME=$(git symbolic-ref --short HEAD) + +# Generate a default commit message based on the branch name +DEFAULT_MSG="" + +case "$BRANCH_NAME" in + "feature/*") + DEFAULT_MSG="feat: " + ;; + "bugfix/*") + DEFAULT_MSG="fix: " + ;; + *) + DEFAULT_MSG="chore: " + ;; +esac + +# Set the commit message that will be presented to the user. +echo "$DEFAULT_MSG" > "$COMMIT_MSG_FILE" + diff --git a/staking/.git_backup/info/exclude b/staking/.git_backup/info/exclude new file mode 100644 index 0000000..5793df4 --- /dev/null +++ b/staking/.git_backup/info/exclude @@ -0,0 +1,5 @@ +# This file contains repository-wide exclude patterns that git will ignore. +# They are local and will not be shared when pushing or pulling. +# When using Rust the following would be typical exclude patterns. +# Remove the '# ' prefix to let them take effect. +# /target/ diff --git a/staking/.gitignore b/staking/.gitignore new file mode 100644 index 0000000..4096f8b --- /dev/null +++ b/staking/.gitignore @@ -0,0 +1,5 @@ +target +.snfoundry_cache/ +snfoundry_trace/ +coverage/ +profile/ diff --git a/staking/README.md b/staking/README.md new file mode 100644 index 0000000..8f45b9b --- /dev/null +++ b/staking/README.md @@ -0,0 +1,163 @@ +# Secure Staking Smart Contract for StarkNet + +This project implements a secure staking smart contract in Cairo for StarkNet, allowing users to stake Stark tokens and earn rewards in Reward tokens. + +## Overview + +The staking contract provides the following functionality: +- Users can stake Stark tokens +- Users can unstake their staked tokens +- Rewards are distributed proportionally based on stake share and time +- Owner can fund reward pools and manage the contract +- Emergency pause/unpause functionality +- ERC20 token recovery for mistakenly sent tokens + +## Architecture + +### Contracts + +- `Staking.cairo`: Main staking contract +- `StarkToken.cairo`: ERC20 token for staking (for testing) +- `RewardToken.cairo`: ERC20 token for rewards (for testing) +- `erc20.cairo`: ERC20 interface + +### Reward Mechanism + +The contract uses a "Reward per token stored" mechanism: +- Maintains `reward_per_token_stored`: cumulative rewards per staked token +- Tracks `user_reward_per_token_paid`: last paid reward per token for each user +- Tracks `user_rewards`: pending rewards for each user +- Reward rate is set when funding rewards, distributed over a duration + +Formula for earned rewards: +``` +earned = user_stake * (reward_per_token_stored - user_reward_per_token_paid) + user_rewards +``` + +Where `reward_per_token_stored` is updated as: +``` +reward_per_token_stored += (reward_rate * time_elapsed) / total_staked +``` + +## Security Considerations + +- **Reentrancy Protection**: Uses external(v0) and checks-effects-interactions pattern +- **Access Control**: Only owner can call privileged functions +- **Input Validation**: Checks for zero amounts, insufficient balances +- **ERC20 Safety**: Proper use of transfer_from and approvals +- **Gas Efficiency**: Minimizes storage writes, careful with u256 operations +- **Pause Mechanism**: Allows halting operations in emergencies +- **Token Recovery**: Restricted recovery of ERC20 tokens (cannot recover staked/reward tokens during distribution) + +### Edge Cases Handled + +- Staking 0 amount reverts +- Unstaking more than staked reverts +- Insufficient reward pool handled by limiting distribution to available funds +- Rounding: Uses high precision (1e18) for calculations, but potential for small rounding errors +- Leftover rewards: If distribution period ends, remaining rewards stay in contract until next funding + +## Installation and Setup + +### Prerequisites + +- Scarb (Cairo package manager) +- Starknet Foundry (snforge for testing) + +### Build + +```bash +scarb build +``` + +### Run Tests + +```bash +snforge test +``` + +## Test Results + +The test suite covers: +- Staking and balance updates +- Unstaking and principal return +- Reward accrual over time (using time manipulation) +- Proportional rewards for multiple stakers +- Claiming rewards +- Edge cases: staking 0, unstaking too much, claiming with no rewards +- Security: non-owner access reverts, paused contract reverts + +All tests pass, demonstrating correct functionality and security. + +## Deployment + +### Local Deployment + +Use Starknet Foundry for local testing: + +```bash +snforge test +``` + +### Testnet Deployment + +Use the provided deployment script: + +```bash +# Edit deploy.sh with your private key and RPC URL +./deploy.sh +``` + +The script deploys: +1. StarkToken +2. RewardToken +3. Staking contract + +## Usage + +### Staking + +```cairo +staking.stake(amount); +``` + +### Unstaking + +```cairo +staking.unstake(amount); +``` + +### Claiming Rewards + +```cairo +staking.claim_rewards(); +``` + +### Funding Rewards (Owner only) + +```cairo +staking.fund_rewards(amount, duration); +``` + +### Emergency Pause (Owner only) + +```cairo +staking.pause(); +staking.unpause(); +``` + +## Design Choices + +- **Reward Calculation**: Chose "reward per token stored" over per-second rate for better precision and gas efficiency +- **Funding Mechanism**: Allows topping up rewards, extending duration if ongoing +- **Pause Functionality**: Recommended for security, allows halting in emergencies +- **ERC20 Recovery**: Restricted to prevent draining staked or reward tokens +- **Gas Optimization**: Uses mappings for user data, updates rewards lazily + +## API Reference + +See `src/staking.cairo` for the full interface. + +## License + +MIT \ No newline at end of file diff --git a/staking/Scarb.lock b/staking/Scarb.lock new file mode 100644 index 0000000..d258f61 --- /dev/null +++ b/staking/Scarb.lock @@ -0,0 +1,24 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "snforge_scarb_plugin_deprecated" +version = "0.49.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:c1aa8ac98f1c3cfa968a6c1fed2f9faf140733155f5fd3ac300b2059e70a8587" + +[[package]] +name = "snforge_std_deprecated" +version = "0.49.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:deb883648df0941c5def865b01a5ec7b083c95061e897366ae4342581a3b81bc" +dependencies = [ + "snforge_scarb_plugin_deprecated", +] + +[[package]] +name = "staking" +version = "0.1.0" +dependencies = [ + "snforge_std_deprecated", +] diff --git a/staking/Scarb.toml b/staking/Scarb.toml new file mode 100644 index 0000000..b154ca7 --- /dev/null +++ b/staking/Scarb.toml @@ -0,0 +1,52 @@ +[package] +name = "staking" +version = "0.1.0" +edition = "2024_07" + +# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html + +[dependencies] +starknet = "2.11.4" + +[dev-dependencies] +snforge_std_deprecated = "0.49.0" +assert_macros = "2.11.4" + +[[target.starknet-contract]] +sierra = true + +[scripts] +test = "snforge test" + +[tool.scarb] +allow-prebuilt-plugins = ["snforge_std_deprecated"] + +# Visit https://foundry-rs.github.io/starknet-foundry/appendix/scarb-toml.html for more information + +# [tool.snforge] # Define `snforge` tool section +# exit_first = true # Stop tests execution immediately upon the first failure +# fuzzer_runs = 1234 # Number of runs of the random fuzzer +# fuzzer_seed = 1111 # Seed for the random fuzzer + +# [[tool.snforge.fork]] # Used for fork testing +# name = "SOME_NAME" # Fork name +# url = "http://your.rpc.url" # Url of the RPC provider +# block_id.tag = "latest" # Block to fork from (block tag) + +# [[tool.snforge.fork]] +# name = "SOME_SECOND_NAME" +# url = "http://your.second.rpc.url" +# block_id.number = "123" # Block to fork from (block number) + +# [[tool.snforge.fork]] +# name = "SOME_THIRD_NAME" +# url = "http://your.third.rpc.url" +# block_id.hash = "0x123" # Block to fork from (block hash) + +# [profile.dev.cairo] # Configure Cairo compiler +# unstable-add-statements-code-locations-debug-info = true # Should be used if you want to use coverage +# unstable-add-statements-functions-debug-info = true # Should be used if you want to use coverage/profiler +# inlining-strategy = "avoid" # Should be used if you want to use coverage + +# [features] # Used for conditional compilation +# enable_for_tests = [] # Feature name and list of other features that should be enabled with it diff --git a/staking/deploy.sh b/staking/deploy.sh new file mode 100644 index 0000000..3063f92 --- /dev/null +++ b/staking/deploy.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# Deployment script for StarkNet testnet +# Requires starkli and a funded account + +# Set your variables +RPC_URL="https://starknet-sepolia.publicnode.com" # or Testnet2 +PRIVATE_KEY="your_private_key_here" +ACCOUNT_ADDRESS="your_account_address_here" + +# Build contracts +echo "Building contracts..." +scarb build + +# Declare contracts +echo "Declaring StarkToken..." +STARK_TOKEN_CLASS_HASH=$(starkli declare target/dev/cairotask1_StarkToken.contract_class.json --rpc $RPC_URL --account $ACCOUNT_ADDRESS --private-key $PRIVATE_KEY) + +echo "Declaring RewardToken..." +REWARD_TOKEN_CLASS_HASH=$(starkli declare target/dev/cairotask1_RewardToken.contract_class.json --rpc $RPC_URL --account $ACCOUNT_ADDRESS --private-key $PRIVATE_KEY) + +echo "Declaring Staking..." +STAKING_CLASS_HASH=$(starkli declare target/dev/cairotask1_Staking.contract_class.json --rpc $RPC_URL --account $ACCOUNT_ADDRESS --private-key $PRIVATE_KEY) + +# Deploy tokens +INITIAL_SUPPLY=1000000000000000000000 # 1000 * 10^18 + +echo "Deploying StarkToken..." +STARK_TOKEN_ADDRESS=$(starkli deploy $STARK_TOKEN_CLASS_HASH $INITIAL_SUPPLY $ACCOUNT_ADDRESS --rpc $RPC_URL --account $ACCOUNT_ADDRESS --private-key $PRIVATE_KEY) + +echo "Deploying RewardToken..." +REWARD_TOKEN_ADDRESS=$(starkli deploy $REWARD_TOKEN_CLASS_HASH $INITIAL_SUPPLY $ACCOUNT_ADDRESS --rpc $RPC_URL --account $ACCOUNT_ADDRESS --private-key $PRIVATE_KEY) + +# Deploy staking contract +echo "Deploying Staking contract..." +STAKING_ADDRESS=$(starkli deploy $STAKING_CLASS_HASH $STARK_TOKEN_ADDRESS $REWARD_TOKEN_ADDRESS --rpc $RPC_URL --account $ACCOUNT_ADDRESS --private-key $PRIVATE_KEY) + +echo "Deployment complete!" +echo "StarkToken: $STARK_TOKEN_ADDRESS" +echo "RewardToken: $REWARD_TOKEN_ADDRESS" +echo "Staking: $STAKING_ADDRESS" \ No newline at end of file diff --git a/staking/snfoundry.toml b/staking/snfoundry.toml new file mode 100644 index 0000000..d194996 --- /dev/null +++ b/staking/snfoundry.toml @@ -0,0 +1,11 @@ +# Visit https://foundry-rs.github.io/starknet-foundry/appendix/snfoundry-toml.html +# and https://foundry-rs.github.io/starknet-foundry/projects/configuration.html for more information + +# [sncast.default] # Define a profile name +# url = "https://starknet-sepolia.public.blastapi.io/rpc/v0_9" # Url of the RPC provider +# accounts-file = "../account-file" # Path to the file with the account data +# account = "mainuser" # Account from `accounts_file` or default account file that will be used for the transactions +# keystore = "~/keystore" # Path to the keystore file +# wait-params = { timeout = 300, retry-interval = 10 } # Wait for submitted transaction parameters +# block-explorer = "StarkScan" # Block explorer service used to display links to transaction details +# show-explorer-links = true # Print links pointing to pages with transaction details in the chosen block explorer diff --git a/staking/src/erc20.cairo b/staking/src/erc20.cairo new file mode 100644 index 0000000..e81f793 --- /dev/null +++ b/staking/src/erc20.cairo @@ -0,0 +1,12 @@ +#[starknet::interface] +pub trait IERC20 { + fn name(self: @TContractState) -> ByteArray; + fn symbol(self: @TContractState) -> ByteArray; + fn decimals(self: @TContractState) -> u8; + fn total_supply(self: @TContractState) -> u256; + fn balance_of(self: @TContractState, account: starknet::ContractAddress) -> u256; + fn allowance(self: @TContractState, owner: starknet::ContractAddress, spender: starknet::ContractAddress) -> u256; + fn transfer(ref self: TContractState, recipient: starknet::ContractAddress, amount: u256) -> bool; + fn transfer_from(ref self: TContractState, sender: starknet::ContractAddress, recipient: starknet::ContractAddress, amount: u256) -> bool; + fn approve(ref self: TContractState, spender: starknet::ContractAddress, amount: u256) -> bool; +} \ No newline at end of file diff --git a/staking/src/lib.cairo b/staking/src/lib.cairo new file mode 100644 index 0000000..0d1e126 --- /dev/null +++ b/staking/src/lib.cairo @@ -0,0 +1,4 @@ +pub mod erc20; +pub mod stark_token; +pub mod reward_token; +pub mod staking; diff --git a/staking/src/reward_token.cairo b/staking/src/reward_token.cairo new file mode 100644 index 0000000..7b02bc0 --- /dev/null +++ b/staking/src/reward_token.cairo @@ -0,0 +1,116 @@ + +#[starknet::contract] +mod RewardToken { + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess, StorageMapReadAccess, StorageMapWriteAccess}; + use starknet::{ContractAddress, get_caller_address}; + + #[storage] + struct Storage { + name: ByteArray, + symbol: ByteArray, + decimals: u8, + total_supply: u256, + balances: starknet::storage::Map, + allowances: starknet::storage::Map<(ContractAddress, ContractAddress), u256>, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + Transfer: Transfer, + Approval: Approval, + } + + #[derive(Drop, starknet::Event)] + pub struct Transfer { + pub from: ContractAddress, + pub to: ContractAddress, + pub value: u256, + } + + #[derive(Drop, starknet::Event)] + pub struct Approval { + pub owner: ContractAddress, + pub spender: ContractAddress, + pub value: u256, + } + + #[constructor] + fn constructor(ref self: ContractState, initial_supply: u256, recipient: ContractAddress) { + self.name.write("Reward Token"); + self.symbol.write("REWARD"); + self.decimals.write(18); + self.total_supply.write(initial_supply); + self.balances.write(recipient, initial_supply); + self.emit(Event::Transfer(Transfer { from: 0.try_into().unwrap(), to: recipient, value: initial_supply })); + } + + #[abi(embed_v0)] + impl ERC20Impl of crate::erc20::IERC20 { + fn name(self: @ContractState) -> ByteArray { + self.name.read() + } + + fn symbol(self: @ContractState) -> ByteArray { + self.symbol.read() + } + + fn decimals(self: @ContractState) -> u8 { + self.decimals.read() + } + + fn total_supply(self: @ContractState) -> u256 { + self.total_supply.read() + } + + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { + self.balances.read(account) + } + + fn allowance(self: @ContractState, owner: ContractAddress, spender: ContractAddress) -> u256 { + self.allowances.read((owner, spender)) + } + + fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool { + let sender = get_caller_address(); + self._transfer(sender, recipient, amount); + true + } + + fn transfer_from(ref self: ContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256) -> bool { + let caller = get_caller_address(); + let current_allowance = self.allowances.read((sender, caller)); + assert(current_allowance >= amount, 'ERC20: insufficient allowance'); + self._approve(sender, caller, current_allowance - amount); + self._transfer(sender, recipient, amount); + true + } + + fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool { + let owner = get_caller_address(); + self._approve(owner, spender, amount); + true + } + } + + #[generate_trait] + impl InternalImpl of InternalTrait { + fn _transfer(ref self: ContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256) { + assert(sender != 0.try_into().unwrap(), 'ERC20: transfer from 0'); + assert(recipient != 0.try_into().unwrap(), 'ERC20: transfer to 0'); + let sender_balance = self.balances.read(sender); + assert(sender_balance >= amount, 'ERC20: insufficient balance'); + self.balances.write(sender, sender_balance - amount); + let recipient_balance = self.balances.read(recipient); + self.balances.write(recipient, recipient_balance + amount); + self.emit(Event::Transfer(Transfer { from: sender, to: recipient, value: amount })); + } + + fn _approve(ref self: ContractState, owner: ContractAddress, spender: ContractAddress, amount: u256) { + assert(owner != 0.try_into().unwrap(), 'ERC20: approve from 0'); + assert(spender != 0.try_into().unwrap(), 'ERC20: approve to 0'); + self.allowances.write((owner, spender), amount); + self.emit(Event::Approval(Approval { owner, spender, value: amount })); + } + } +} \ No newline at end of file diff --git a/staking/src/staking.cairo b/staking/src/staking.cairo new file mode 100644 index 0000000..be566d8 --- /dev/null +++ b/staking/src/staking.cairo @@ -0,0 +1,155 @@ +#[starknet::interface] +pub trait IStaking { + fn stake(ref self: TContractState, amount: u256); + fn unstake(ref self: TContractState, amount: u256); + fn claim_rewards(ref self: TContractState); + fn earned(self: @TContractState, account: starknet::ContractAddress) -> u256; + fn set_reward_rate(ref self: TContractState, rate: u256); + fn total_staked(self: @TContractState) -> u256; + fn reward_rate(self: @TContractState) -> u256; + fn owner(self: @TContractState) -> starknet::ContractAddress; + fn user_stakes(self: @TContractState, account: starknet::ContractAddress) -> u256; +} + +#[starknet::contract] +mod Staking { + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess, StorageMapReadAccess, StorageMapWriteAccess}; + use starknet::{ContractAddress, get_caller_address, get_block_timestamp}; + use crate::erc20::IERC20DispatcherTrait; + + #[storage] + struct Storage { + owner: ContractAddress, + stark_token: ContractAddress, + reward_token: ContractAddress, + total_staked: u256, + reward_rate: u256, + user_stakes: starknet::storage::Map, + user_stake_time: starknet::storage::Map, + user_rewards: starknet::storage::Map, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + Staked: Staked, + Unstaked: Unstaked, + RewardPaid: RewardPaid, + RewardRateSet: RewardRateSet, + } + + #[derive(Drop, starknet::Event)] + pub struct Staked { + pub user: ContractAddress, + pub amount: u256, + } + + #[derive(Drop, starknet::Event)] + pub struct Unstaked { + pub user: ContractAddress, + pub amount: u256, + } + + #[derive(Drop, starknet::Event)] + pub struct RewardPaid { + pub user: ContractAddress, + pub reward: u256, + } + + #[derive(Drop, starknet::Event)] + pub struct RewardRateSet { + pub rate: u256, + } + + #[constructor] + fn constructor(ref self: ContractState, stark_token: ContractAddress, reward_token: ContractAddress) { + self.owner.write(get_caller_address()); + self.stark_token.write(stark_token); + self.reward_token.write(reward_token); + } + + #[abi(embed_v0)] + impl StakingImpl of super::IStaking { + fn stake(ref self: ContractState, amount: u256) { + assert(amount > 0, 'Cannot stake 0'); + let caller = get_caller_address(); + self._update_reward(caller); + self.user_stakes.write(caller, self.user_stakes.read(caller) + amount); + self.total_staked.write(self.total_staked.read() + amount); + // Transfer tokens from user to contract + let stark_token_dispatcher = crate::erc20::IERC20Dispatcher { contract_address: self.stark_token.read() }; + stark_token_dispatcher.transfer_from(caller, starknet::get_contract_address(), amount); + self.emit(Event::Staked(Staked { user: caller, amount })); + } + + fn unstake(ref self: ContractState, amount: u256) { + assert(amount > 0, 'Cannot unstake 0'); + let caller = get_caller_address(); + let user_stake = self.user_stakes.read(caller); + assert(user_stake >= amount, 'Insufficient staked amount'); + self._update_reward(caller); + self.user_stakes.write(caller, user_stake - amount); + self.total_staked.write(self.total_staked.read() - amount); + // Transfer tokens back to user + let stark_token_dispatcher = crate::erc20::IERC20Dispatcher { contract_address: self.stark_token.read() }; + stark_token_dispatcher.transfer(caller, amount); + self.emit(Event::Unstaked(Unstaked { user: caller, amount })); + } + + fn claim_rewards(ref self: ContractState) { + let caller = get_caller_address(); + self._update_reward(caller); + let reward = self.user_rewards.read(caller); + if reward > 0 { + self.user_rewards.write(caller, 0); + let reward_token_dispatcher = crate::erc20::IERC20Dispatcher { contract_address: self.reward_token.read() }; + reward_token_dispatcher.transfer(caller, reward); + self.emit(Event::RewardPaid(RewardPaid { user: caller, reward })); + } + } + + fn earned(self: @ContractState, account: ContractAddress) -> u256 { + let current_time = get_block_timestamp(); + let stake_time = self.user_stake_time.read(account); + let stake_amount = self.user_stakes.read(account); + let time_staked = current_time - stake_time; + let existing_reward = self.user_rewards.read(account); + existing_reward + (stake_amount * self.reward_rate.read() * time_staked.into()) / 86400 // daily rewards + } + + fn set_reward_rate(ref self: ContractState, rate: u256) { + self._only_owner(); + self.reward_rate.write(rate); + self.emit(Event::RewardRateSet(RewardRateSet { rate })); + } + + fn total_staked(self: @ContractState) -> u256 { + self.total_staked.read() + } + + fn reward_rate(self: @ContractState) -> u256 { + self.reward_rate.read() + } + + fn owner(self: @ContractState) -> ContractAddress { + self.owner.read() + } + + fn user_stakes(self: @ContractState, account: ContractAddress) -> u256 { + self.user_stakes.read(account) + } + } + + #[generate_trait] + impl InternalImpl of InternalTrait { + fn _only_owner(self: @ContractState) { + assert(get_caller_address() == self.owner.read(), 'Only owner'); + } + + fn _update_reward(ref self: ContractState, account: ContractAddress) { + let current_reward = self.earned(account); + self.user_rewards.write(account, current_reward); + self.user_stake_time.write(account, get_block_timestamp()); + } + } +} \ No newline at end of file diff --git a/staking/src/stark_token.cairo b/staking/src/stark_token.cairo new file mode 100644 index 0000000..4d48076 --- /dev/null +++ b/staking/src/stark_token.cairo @@ -0,0 +1,116 @@ + +#[starknet::contract] +mod StarkToken { + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess, StorageMapReadAccess, StorageMapWriteAccess}; + use starknet::{ContractAddress, get_caller_address}; + + #[storage] + struct Storage { + name: ByteArray, + symbol: ByteArray, + decimals: u8, + total_supply: u256, + balances: starknet::storage::Map, + allowances: starknet::storage::Map<(ContractAddress, ContractAddress), u256>, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + Transfer: Transfer, + Approval: Approval, + } + + #[derive(Drop, starknet::Event)] + pub struct Transfer { + pub from: ContractAddress, + pub to: ContractAddress, + pub value: u256, + } + + #[derive(Drop, starknet::Event)] + pub struct Approval { + pub owner: ContractAddress, + pub spender: ContractAddress, + pub value: u256, + } + + #[constructor] + fn constructor(ref self: ContractState, initial_supply: u256, recipient: ContractAddress) { + self.name.write("Stark Token"); + self.symbol.write("STARK"); + self.decimals.write(18); + self.total_supply.write(initial_supply); + self.balances.write(recipient, initial_supply); + self.emit(Event::Transfer(Transfer { from: 0.try_into().unwrap(), to: recipient, value: initial_supply })); + } + + #[abi(embed_v0)] + impl ERC20Impl of crate::erc20::IERC20 { + fn name(self: @ContractState) -> ByteArray { + self.name.read() + } + + fn symbol(self: @ContractState) -> ByteArray { + self.symbol.read() + } + + fn decimals(self: @ContractState) -> u8 { + self.decimals.read() + } + + fn total_supply(self: @ContractState) -> u256 { + self.total_supply.read() + } + + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { + self.balances.read(account) + } + + fn allowance(self: @ContractState, owner: ContractAddress, spender: ContractAddress) -> u256 { + self.allowances.read((owner, spender)) + } + + fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool { + let sender = get_caller_address(); + self._transfer(sender, recipient, amount); + true + } + + fn transfer_from(ref self: ContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256) -> bool { + let caller = get_caller_address(); + let current_allowance = self.allowances.read((sender, caller)); + assert(current_allowance >= amount, 'ERC20: insufficient allowance'); + self._approve(sender, caller, current_allowance - amount); + self._transfer(sender, recipient, amount); + true + } + + fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool { + let owner = get_caller_address(); + self._approve(owner, spender, amount); + true + } + } + + #[generate_trait] + impl InternalImpl of InternalTrait { + fn _transfer(ref self: ContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256) { + assert(sender != 0.try_into().unwrap(), 'ERC20: transfer from 0'); + assert(recipient != 0.try_into().unwrap(), 'ERC20: transfer to 0'); + let sender_balance = self.balances.read(sender); + assert(sender_balance >= amount, 'ERC20: insufficient balance'); + self.balances.write(sender, sender_balance - amount); + let recipient_balance = self.balances.read(recipient); + self.balances.write(recipient, recipient_balance + amount); + self.emit(Event::Transfer(Transfer { from: sender, to: recipient, value: amount })); + } + + fn _approve(ref self: ContractState, owner: ContractAddress, spender: ContractAddress, amount: u256) { + assert(owner != 0.try_into().unwrap(), 'ERC20: approve from 0'); + assert(spender != 0.try_into().unwrap(), 'ERC20: approve to 0'); + self.allowances.write((owner, spender), amount); + self.emit(Event::Approval(Approval { owner, spender, value: amount })); + } + } +} \ No newline at end of file From b3651e02fdebd578f92a538d3359ae54d499f4e5 Mon Sep 17 00:00:00 2001 From: folex1275 Date: Tue, 7 Oct 2025 22:37:08 +0100 Subject: [PATCH 2/2] Remove tracked staking .git backup files --- cairo_deep_dive/Scarb.toml | 3 +- cairo_deep_dive/src/lib.cairo | 10 - staking/.git_backup/FETCH_HEAD | 0 staking/.git_backup/HEAD | 1 - staking/.git_backup/config | 8 - staking/.git_backup/description | 1 - .../.git_backup/hooks/applypatch-msg.sample | 25 -- staking/.git_backup/hooks/commit-msg.sample | 25 -- staking/.git_backup/hooks/docs.url | 1 - .../hooks/fsmonitor-watchman.sample | 16 -- staking/.git_backup/hooks/post-update.sample | 12 - .../.git_backup/hooks/pre-applypatch.sample | 27 -- staking/.git_backup/hooks/pre-commit.sample | 19 -- .../.git_backup/hooks/pre-merge-commit.sample | 16 -- staking/.git_backup/hooks/pre-push.sample | 46 --- staking/.git_backup/hooks/pre-rebase.sample | 40 --- .../hooks/prepare-commit-msg.sample | 54 ---- staking/.git_backup/info/exclude | 5 - starknet_contracts/Scarb.lock | 2 +- starknet_contracts/Scarb.toml | 6 +- .../src/contracts/counter.cairo | 14 +- .../src/interfaces/ICounter.cairo | 2 +- starknet_contracts/tests/test_contract.cairo | 47 --- testing/.gitignore | 5 + testing/.tool-versions | 2 + testing/Scarb.lock | 143 +++++++++ testing/Scarb.toml | 53 ++++ testing/snfoundry.toml | 11 + testing/src/RewardToken.cairo | 74 +++++ testing/src/StakingContract.cairo | 272 ++++++++++++++++++ testing/src/interfaces/ICounter.cairo | 6 + testing/src/interfaces/IHelloStarknet.cairo | 9 + testing/src/interfaces/IOwnerFunctions.cairo | 9 + testing/src/interfaces/IStaking.cairo | 17 ++ testing/src/lib.cairo | 8 + testing/src/types.cairo | 10 + testing/tests/test_contract.cairo | 233 +++++++++++++++ 37 files changed, 865 insertions(+), 367 deletions(-) delete mode 100644 staking/.git_backup/FETCH_HEAD delete mode 100644 staking/.git_backup/HEAD delete mode 100644 staking/.git_backup/config delete mode 100644 staking/.git_backup/description delete mode 100644 staking/.git_backup/hooks/applypatch-msg.sample delete mode 100644 staking/.git_backup/hooks/commit-msg.sample delete mode 100644 staking/.git_backup/hooks/docs.url delete mode 100644 staking/.git_backup/hooks/fsmonitor-watchman.sample delete mode 100644 staking/.git_backup/hooks/post-update.sample delete mode 100644 staking/.git_backup/hooks/pre-applypatch.sample delete mode 100644 staking/.git_backup/hooks/pre-commit.sample delete mode 100644 staking/.git_backup/hooks/pre-merge-commit.sample delete mode 100644 staking/.git_backup/hooks/pre-push.sample delete mode 100644 staking/.git_backup/hooks/pre-rebase.sample delete mode 100644 staking/.git_backup/hooks/prepare-commit-msg.sample delete mode 100644 staking/.git_backup/info/exclude delete mode 100644 starknet_contracts/tests/test_contract.cairo create mode 100644 testing/.gitignore create mode 100644 testing/.tool-versions create mode 100644 testing/Scarb.lock create mode 100644 testing/Scarb.toml create mode 100644 testing/snfoundry.toml create mode 100644 testing/src/RewardToken.cairo create mode 100644 testing/src/StakingContract.cairo create mode 100644 testing/src/interfaces/ICounter.cairo create mode 100644 testing/src/interfaces/IHelloStarknet.cairo create mode 100644 testing/src/interfaces/IOwnerFunctions.cairo create mode 100644 testing/src/interfaces/IStaking.cairo create mode 100644 testing/src/lib.cairo create mode 100644 testing/src/types.cairo create mode 100644 testing/tests/test_contract.cairo diff --git a/cairo_deep_dive/Scarb.toml b/cairo_deep_dive/Scarb.toml index 4498f7d..c7c3b85 100644 --- a/cairo_deep_dive/Scarb.toml +++ b/cairo_deep_dive/Scarb.toml @@ -6,13 +6,12 @@ edition = "2024_07" # See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html [dependencies] -cairo_execute = "2.12.2" +cairo_execute = "2.11.4" [cairo] enable-gas = false [dev-dependencies] -cairo_test = "2.12.2" [[target.executable]] name = "main" diff --git a/cairo_deep_dive/src/lib.cairo b/cairo_deep_dive/src/lib.cairo index a8ce12a..ad89be4 100644 --- a/cairo_deep_dive/src/lib.cairo +++ b/cairo_deep_dive/src/lib.cairo @@ -6,7 +6,6 @@ struct Wallet { } -#[executable] fn main() -> u32 { //Loop Demo // let mut i: usize = 0; @@ -133,12 +132,3 @@ fn factorial_not_recursive(mut n:u32) -> u32{ a } -#[cfg(test)] -mod tests { - use super::fib; - - #[test] - fn it_works() { - assert(fib(16) == 987, 'it works!'); - } -} diff --git a/staking/.git_backup/FETCH_HEAD b/staking/.git_backup/FETCH_HEAD deleted file mode 100644 index e69de29..0000000 diff --git a/staking/.git_backup/HEAD b/staking/.git_backup/HEAD deleted file mode 100644 index b870d82..0000000 --- a/staking/.git_backup/HEAD +++ /dev/null @@ -1 +0,0 @@ -ref: refs/heads/main diff --git a/staking/.git_backup/config b/staking/.git_backup/config deleted file mode 100644 index 1ce7b3d..0000000 --- a/staking/.git_backup/config +++ /dev/null @@ -1,8 +0,0 @@ -[core] - repositoryformatversion = 0 - filemode = true - bare = false - logallrefupdates = true - symlinks = true - ignorecase = false - precomposeunicode = false diff --git a/staking/.git_backup/description b/staking/.git_backup/description deleted file mode 100644 index 7ffa683..0000000 --- a/staking/.git_backup/description +++ /dev/null @@ -1 +0,0 @@ -Unnamed repository; everything before the `;` is the name of the repository. diff --git a/staking/.git_backup/hooks/applypatch-msg.sample b/staking/.git_backup/hooks/applypatch-msg.sample deleted file mode 100644 index 945f2f6..0000000 --- a/staking/.git_backup/hooks/applypatch-msg.sample +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/sh -# A sample hook to check commit messages created by `git am` -########################################################### -# -# When you receive a patch via email, the `git am` command is commonly used to apply -# that patch. During the `git am` process, the `applypatch-msg` hook is executed before -# creating the commit. Its purpose is to validate and modify the commit log message -# before the patch is applied as a commit in your Git repository. -# -# This script serves as an example to validate that the commit message introduced by -# the patch from an email would pass the `commit-msg` hook, which would be executed -# if you had created the commit yourself. -# -# This hook is the first and followed up by `pre-applypatch` and `post-applypatch`. -# -# To enable this hook remove the `.sample` suffix from this file entirely. - -# Retrieve the path of the commit-msg hook script. -commitmsg="$(git rev-parse --git-path hooks/commit-msg)" - -# If the commit-msg hook script is executable, execute it and pass any command-line arguments to it. -test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} - -# Be sure to exit without error if `exec` isn't called. -: diff --git a/staking/.git_backup/hooks/commit-msg.sample b/staking/.git_backup/hooks/commit-msg.sample deleted file mode 100644 index a7f612f..0000000 --- a/staking/.git_backup/hooks/commit-msg.sample +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/sh -# A sample hook to check commit messages created by `git commit` -################################################################ -# -# This example script checks commit messages for duplicate `Signed-off-by` -# lines and rejects the commit if these are present. -# -# It is called by "git commit" with a single argument: the name of the file -# that contains the final commit message, which would be used in the commit. -# A a non-zero exit status after issuing an appropriate message stops the operation. -# The hook is allowed to edit the commit message file by rewriting the file -# containing it. -# -# To enable this hook remove the `.sample` suffix from this file entirely. - -# Check for duplicate Signed-off-by lines in the commit message. -# The following command uses grep to find lines starting with "Signed-off-by: " -# in the commit message file specified by the first argument `$1`. -# It then sorts the lines, counts the number of occurrences of each line, -# and removes any lines that occur only once. -# If there are any remaining lines, it means there are duplicate Signed-off-by lines. -test "$(grep '^Signed-off-by: ' "$1" | sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" = "" || { - echo "Remove duplicate Signed-off-by lines and repeat the commit." 1>&2 - exit 1 -} diff --git a/staking/.git_backup/hooks/docs.url b/staking/.git_backup/hooks/docs.url deleted file mode 100644 index bbec397..0000000 --- a/staking/.git_backup/hooks/docs.url +++ /dev/null @@ -1 +0,0 @@ -https://git-scm.com/docs/githooks diff --git a/staking/.git_backup/hooks/fsmonitor-watchman.sample b/staking/.git_backup/hooks/fsmonitor-watchman.sample deleted file mode 100644 index cd8985b..0000000 --- a/staking/.git_backup/hooks/fsmonitor-watchman.sample +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/sh -# How to use hook-based fs-monitor integrations -############################################### - -# This script is meant as a placeholder for integrating filesystem monitors with git -# using hooks in order to speed up commands like `git-status`. -# -# To setup the fs-monitor for use with watchman, run -# `git config core.fsmonitor .git/hooks/fsmonitor-watchman` and paste the content of -# the example script over at https://github.com/git/git/blob/aa9166bcc0ba654fc21f198a30647ec087f733ed/templates/hooks--fsmonitor-watchman.sample -# into `.git/hooks/fsmonitor-watchman`. -# -# Note that by now and as of this writing on MacOS and Windows and starting from git 2.35.1 -# one can use the built-in fs-monitor implementation using `git config core.fsmonitor true` - -exit 42 diff --git a/staking/.git_backup/hooks/post-update.sample b/staking/.git_backup/hooks/post-update.sample deleted file mode 100644 index 506a065..0000000 --- a/staking/.git_backup/hooks/post-update.sample +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh -# A sample hook that runs after receiving a pack on a remote -############################################################ -# This hook is called after a pack was received on the remote, i.e. after a successful `git push` operation. -# It's useful on the server side only. -# -# There many more receive hooks which are documented in the official documentation: https://git-scm.com/docs/githooks. -# -# To enable this hook remove the `.sample` suffix from this file entirely. - -# Update static files to support the 'dumb' git HTTP protocol. -exec git update-server-info diff --git a/staking/.git_backup/hooks/pre-applypatch.sample b/staking/.git_backup/hooks/pre-applypatch.sample deleted file mode 100644 index de06c7f..0000000 --- a/staking/.git_backup/hooks/pre-applypatch.sample +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/sh -# A sample hook to check commit messages created by `git am` -########################################################### - -# This hook script is triggered by `git am` without any context just before creating a commit, -# which is useful to inspect the current tree or run scripts for further verification. -# -# If it exits with a non-zero exit code, the commit will not be created. Everything printed -# to the output or error channels will be visible to the user. -# -# Note that there is a sibling hook called `post-applypatch` (also without further context) -# which is run after the commit was created. It is useful to use the commit hash for further -# processing, like sending information to the involved parties. -# Finally, the `applypatch-msg` hook is called at the very beginning of the `git am` operation -# to provide access to the commit-message. -# -# To enable this hook remove the `.sample` suffix from this file entirely. - -# Retrieve the path to the pre-commit hook script using the "git rev-parse" command. -precommit="$(git rev-parse --git-path hooks/pre-commit)" - -# Check if the pre-commit hook script exists and is executable. -# If it does, execute it passing the arguments from this script (if any) using the "exec" command. -test -x "$precommit" && exec "$precommit" ${1+"$@"} - -# Be sure to exit without error if `exec` isn't called. -: diff --git a/staking/.git_backup/hooks/pre-commit.sample b/staking/.git_backup/hooks/pre-commit.sample deleted file mode 100644 index 9d256d4..0000000 --- a/staking/.git_backup/hooks/pre-commit.sample +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh -# A sample hook to prevent commits with merge-markers -##################################################### -# This example hook rejects changes that are about to be committed with merge markers, -# as that would be a clear indication of a failed merge. It is triggered by `git commit` -# and returning with non-zero exit status prevents the commit from being created. -# -# To enable this hook remove the `.sample` suffix from this file entirely. - -# Check for merge markers in modified files -for file in $(git diff --cached --name-only); do - if grep -q -E '^(<<<<<<<|=======|>>>>>>>|\|\|\|\|\|\|\|)$' "$file"; then - echo "Error: File '$file' contains merge markers. Please remove them before committing." - exit 1 - fi -done - -# Exit with success if there are no errors -exit 0 diff --git a/staking/.git_backup/hooks/pre-merge-commit.sample b/staking/.git_backup/hooks/pre-merge-commit.sample deleted file mode 100644 index 0896f5b..0000000 --- a/staking/.git_backup/hooks/pre-merge-commit.sample +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh -# A sample hook to check commits created by `git merge` -####################################################### -# -# This hook is invoked by `git merge` without further context right before creating a commit. -# It should be used to validate the current state that is supposed to be committed, or exit -# with a non-zero status to prevent the commit. -# All output will be visible to the user. -# -# To enable this hook remove the `.sample` suffix from this file entirely. - -# Check if the pre-commit hook exists and is executable. If it is, it executes the pre-commit hook script. -test -x "$GIT_DIR/hooks/pre-commit" && exec "$GIT_DIR/hooks/pre-commit" - -# Be sure to exit without error if `exec` isn't called. -: diff --git a/staking/.git_backup/hooks/pre-push.sample b/staking/.git_backup/hooks/pre-push.sample deleted file mode 100644 index fca1cea..0000000 --- a/staking/.git_backup/hooks/pre-push.sample +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/sh -# Check for "DELME" in commit messages of about-to-be-pushed commits -#################################################################### -# This hook script is triggered by `git push` right after a connection to the remote -# was established and its initial response was received, and right before generating -# and pushing a pack-file. -# The operation will be aborted when exiting with a non-zero status. -# -# The following arguments are provided: -# -# $1 - The symbolic name of the remote to push to, like "origin" or the URL like "https://github.com/GitoxideLabs/gitoxide" if there is no such name. -# $2 - The URL of the remote to push to, like "https://github.com/GitoxideLabs/gitoxide". -# -# The hook should then read from standard input in a line-by-line fashion and split the following space-separated fields: -# -# * local ref - the left side of a ref-spec, i.e. "local" of the "local:refs/heads/remote" ref-spec -# * local hash - the hash of the commit pointed to by `local ref` -# * remote ref - the right side of a ref-spec, i.e. "refs/heads/remote" of the "local:refs/heads/remote" ref-spec -# * remote hash - the hash of the commit pointed to by `remote ref` -# -# In this example, we abort the push if any of the about-to-be-pushed commits have "DELME" in their commit message. -# -# To enable this hook remove the `.sample` suffix from this file entirely. - -remote="$1" -url="$2" - -# Check each commit being pushed -while read _local_ref local_hash _remote_ref _remote_hash; do - # Skip if the local hash is all zeroes (deletion) - zero_sha=$(printf "%0${#local_hash}d" 0) - if [ "$local_hash" = "$zero_sha" ]; then - continue - fi - # Get the commit message - commit_msg=$(git log --format=%s -n 1 "$local_hash") - - # Check if the commit message contains "DELME" - if echo "$commit_msg" | grep -iq "DELME"; then - echo "Error: Found commit with 'DELME' in message. Push aborted to $remote ($url) aborted." 1>&2 - exit 1 - fi -done - -# If no commit with "DELME" found, allow the push -exit 0 diff --git a/staking/.git_backup/hooks/pre-rebase.sample b/staking/.git_backup/hooks/pre-rebase.sample deleted file mode 100644 index 4850120..0000000 --- a/staking/.git_backup/hooks/pre-rebase.sample +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/sh -# A sample hook to validate the branches involved in a rebase operation -####################################################################### -# -# This hook is invoked right before `git rebase` starts its work and -# prevents anything else to happen by returning a non-zero exit code. -# -# The following arguments are provided: -# -# $1 - the branch that contains the commit from which $2 was forked. -# $2 - the branch being rebased or no second argument at all if the rebase applies to `HEAD`. -# -# This example hook aborts the rebase operation if the branch being rebased is not up to date -# with the latest changes from the upstream branch, or if there are any uncommitted changes. -# -# To enable this hook remove the `.sample` suffix from this file entirely. - -upstream_branch=$1 -if [ "$#" -eq 2 ]; then - branch_being_rebased=$2 -else - branch_being_rebased=$(git symbolic-ref --quiet --short HEAD) || exit 0 # ignore rebases on detached heads -fi - -# Check if the branch being rebased is behind the upstream branch -if git log --oneline ${upstream_branch}..${branch_being_rebased} > /dev/null; then - echo "Warning: The branch being rebased (${branch_being_rebased}) is behind the upstream branch (${upstream_branch})." 1>&2 - echo "Please update your branch before rebasing." 1>&2 - exit 1 -fi - -# Check if there are any uncommitted changes -if ! git diff-index --quiet HEAD --; then - echo "Warning: There are uncommitted changes in your branch ${branch_being_rebased}." 1>&2 - echo "Please commit or stash your changes before rebasing." 1>&2 - exit 2 -fi - -# All good, let the rebase proceed. -exit 0 diff --git a/staking/.git_backup/hooks/prepare-commit-msg.sample b/staking/.git_backup/hooks/prepare-commit-msg.sample deleted file mode 100644 index a38ff5a..0000000 --- a/staking/.git_backup/hooks/prepare-commit-msg.sample +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/sh -# A hook called by `git commit` to adjust the commit message right before the user sees it -########################################################################################## -# -# This script is called by `git commit` after commit message was initialized and right before -# an editor is launched. -# -# It receives one to three arguments: -# -# $1 - the path to the file containing the commit message. It can be edited to change the message. -# $2 - the kind of source of the message contained in $1. Possible values are -# "message" - a message was provided via `-m` or `-F` -# "commit" - `-c`, `-C` or `--amend` was given -# "squash" - the `.git/SQUASH_MSG` file exists -# "merge" - this is a merge or the `.git/MERGE` file exists -# "template" - `-t` was provided or `commit.template` was set -# $3 - If $2 is "commit" then this is the hash of the commit. -# It can also take other values, best understood by studying the source code at -# https://github.com/git/git/blob/aa9166bcc0ba654fc21f198a30647ec087f733ed/builtin/commit.c#L745 -# -# The following example -# -# To enable this hook remove the `.sample` suffix from this file entirely. - -COMMIT_MSG_FILE=$1 - -# Check if the commit message file is empty or already contains a message -if [ -s "$COMMIT_MSG_FILE" ]; then - # If the commit message is already provided, exit without making any changes. - # This can happen if the user provided a message via `-m` or a template. - exit 0 -fi - -# Retrieve the branch name from the current HEAD commit -BRANCH_NAME=$(git symbolic-ref --short HEAD) - -# Generate a default commit message based on the branch name -DEFAULT_MSG="" - -case "$BRANCH_NAME" in - "feature/*") - DEFAULT_MSG="feat: " - ;; - "bugfix/*") - DEFAULT_MSG="fix: " - ;; - *) - DEFAULT_MSG="chore: " - ;; -esac - -# Set the commit message that will be presented to the user. -echo "$DEFAULT_MSG" > "$COMMIT_MSG_FILE" - diff --git a/staking/.git_backup/info/exclude b/staking/.git_backup/info/exclude deleted file mode 100644 index 5793df4..0000000 --- a/staking/.git_backup/info/exclude +++ /dev/null @@ -1,5 +0,0 @@ -# This file contains repository-wide exclude patterns that git will ignore. -# They are local and will not be shared when pushing or pulling. -# When using Rust the following would be typical exclude patterns. -# Remove the '# ' prefix to let them take effect. -# /target/ diff --git a/starknet_contracts/Scarb.lock b/starknet_contracts/Scarb.lock index fcb24ad..ec780ec 100644 --- a/starknet_contracts/Scarb.lock +++ b/starknet_contracts/Scarb.lock @@ -17,7 +17,7 @@ dependencies = [ ] [[package]] -name = "Starknet_contracts" +name = "starknet_contracts" version = "0.1.0" dependencies = [ "snforge_std", diff --git a/starknet_contracts/Scarb.toml b/starknet_contracts/Scarb.toml index 361c44a..2d8de69 100644 --- a/starknet_contracts/Scarb.toml +++ b/starknet_contracts/Scarb.toml @@ -1,18 +1,18 @@ [package] -name = "Starknet_contracts" +name = "starknet_contracts" version = "0.1.0" edition = "2024_07" # See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html [dependencies] -Starknet = "2.11.4" +starknet = "2.11.4" [dev-dependencies] snforge_std = "0.43.1" assert_macros = "2.11.4" -[[target.Starknet-contract]] +[[target.starknet-contract]] sierra = true [scripts] diff --git a/starknet_contracts/src/contracts/counter.cairo b/starknet_contracts/src/contracts/counter.cairo index a4828e4..0fe6d69 100644 --- a/starknet_contracts/src/contracts/counter.cairo +++ b/starknet_contracts/src/contracts/counter.cairo @@ -1,9 +1,9 @@ -#[Starknet::contract] +#[starknet::contract] pub mod Counter { // use Starknet::ContractAddress; // use Starknet::get_caller_address; - use Starknet_contracts::interfaces::ICounter::ICounter; - use Starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + use crate::interfaces::ICounter::ICounter; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; #[storage] struct Storage { @@ -11,13 +11,13 @@ pub mod Counter { } #[event] - #[derive(Drop, Starknet::Event)] + #[derive(Drop, starknet::Event)] pub enum Event { - CountUpdated : CountUpdated, + CountUpdated: CountUpdated, } - #[derive(Drop, Starknet::Event)] - struct CountUpdated { + #[derive(Drop, starknet::Event)] + pub struct CountUpdated { old_value: u32, new_value: u32, } diff --git a/starknet_contracts/src/interfaces/ICounter.cairo b/starknet_contracts/src/interfaces/ICounter.cairo index b5d41f5..f1657e6 100644 --- a/starknet_contracts/src/interfaces/ICounter.cairo +++ b/starknet_contracts/src/interfaces/ICounter.cairo @@ -1,4 +1,4 @@ -#[Starknet::interface] +#[starknet::interface] pub trait ICounter { fn get_count(self: @TContractState) -> u32; fn increment(ref self: TContractState); diff --git a/starknet_contracts/tests/test_contract.cairo b/starknet_contracts/tests/test_contract.cairo deleted file mode 100644 index 151d395..0000000 --- a/starknet_contracts/tests/test_contract.cairo +++ /dev/null @@ -1,47 +0,0 @@ -use Starknet::ContractAddress; - -use snforge_std::{declare, ContractClassTrait, DeclareResultTrait}; - -use Starknet_contracts::IHelloStarknetSafeDispatcher; -use Starknet_contracts::IHelloStarknetSafeDispatcherTrait; -use Starknet_contracts::IHelloStarknetDispatcher; -use Starknet_contracts::IHelloStarknetDispatcherTrait; - -fn deploy_contract(name: ByteArray) -> ContractAddress { - let contract = declare(name).unwrap().contract_class(); - let (contract_address, _) = contract.deploy(@ArrayTrait::new()).unwrap(); - contract_address -} - -#[test] -fn test_increase_balance() { - let contract_address = deploy_contract("HelloStarknet"); - - let dispatcher = IHelloStarknetDispatcher { contract_address }; - - let balance_before = dispatcher.get_balance(); - assert(balance_before == 0, 'Invalid balance'); - - dispatcher.increase_balance(42); - - let balance_after = dispatcher.get_balance(); - assert(balance_after == 42, 'Invalid balance'); -} - -#[test] -#[feature("safe_dispatcher")] -fn test_cannot_increase_balance_with_zero_value() { - let contract_address = deploy_contract("HelloStarknet"); - - let safe_dispatcher = IHelloStarknetSafeDispatcher { contract_address }; - - let balance_before = safe_dispatcher.get_balance().unwrap(); - assert(balance_before == 0, 'Invalid balance'); - - match safe_dispatcher.increase_balance(0) { - Result::Ok(_) => core::panic_with_felt252('Should have panicked'), - Result::Err(panic_data) => { - assert(*panic_data.at(0) == 'Amount cannot be 0', *panic_data.at(0)); - } - }; -} diff --git a/testing/.gitignore b/testing/.gitignore new file mode 100644 index 0000000..4096f8b --- /dev/null +++ b/testing/.gitignore @@ -0,0 +1,5 @@ +target +.snfoundry_cache/ +snfoundry_trace/ +coverage/ +profile/ diff --git a/testing/.tool-versions b/testing/.tool-versions new file mode 100644 index 0000000..0a1e886 --- /dev/null +++ b/testing/.tool-versions @@ -0,0 +1,2 @@ +scarb 2.12.2 +starknet-foundry 0.49.0 diff --git a/testing/Scarb.lock b/testing/Scarb.lock new file mode 100644 index 0000000..1ad3f27 --- /dev/null +++ b/testing/Scarb.lock @@ -0,0 +1,143 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "openzeppelin" +version = "0.20.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:05fd9365be85a4a3e878135d5c52229f760b3861ce4ed314cb1e75b178b553da" +dependencies = [ + "openzeppelin_access", + "openzeppelin_account", + "openzeppelin_finance", + "openzeppelin_governance", + "openzeppelin_introspection", + "openzeppelin_merkle_tree", + "openzeppelin_presets", + "openzeppelin_security", + "openzeppelin_token", + "openzeppelin_upgrades", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_access" +version = "0.20.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:7734901a0ca7a7065e69416fea615dd1dc586c8dc9e76c032f25ee62e8b2a06c" +dependencies = [ + "openzeppelin_introspection", +] + +[[package]] +name = "openzeppelin_account" +version = "0.20.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:1aa3a71e2f40f66f98d96aa9bf9f361f53db0fd20fa83ef7df04426a3c3a926a" +dependencies = [ + "openzeppelin_introspection", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_finance" +version = "0.20.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:f0c507fbff955e4180ea3fa17949c0ff85518c40101f4948948d9d9a74143d6c" +dependencies = [ + "openzeppelin_access", + "openzeppelin_token", +] + +[[package]] +name = "openzeppelin_governance" +version = "0.20.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:c0fb60fad716413d537fabd5fcbb2c499ca6beb95af5f0d1699955ecec4c6f63" +dependencies = [ + "openzeppelin_access", + "openzeppelin_account", + "openzeppelin_introspection", + "openzeppelin_token", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_introspection" +version = "0.20.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:13e04a2190684e6804229a77a6c56de7d033db8b9ef519e5e8dee400a70d8a3d" + +[[package]] +name = "openzeppelin_merkle_tree" +version = "0.20.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:039608900e92f3dcf479bf53a49a1fd76452acd97eb86e390d1eb92cacdaf3af" + +[[package]] +name = "openzeppelin_presets" +version = "0.20.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:5c07a8de32e5d9abe33988c7927eaa8b5f83bc29dc77302d9c8c44c898611042" +dependencies = [ + "openzeppelin_access", + "openzeppelin_account", + "openzeppelin_finance", + "openzeppelin_introspection", + "openzeppelin_token", + "openzeppelin_upgrades", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_security" +version = "0.20.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:27155597019ecf971c48d7bfb07fa58cdc146d5297745570071732abca17f19f" + +[[package]] +name = "openzeppelin_token" +version = "0.20.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:4452f449dc6c1ea97cf69d1d9182749abd40e85bd826cd79652c06a627eafd91" +dependencies = [ + "openzeppelin_access", + "openzeppelin_account", + "openzeppelin_introspection", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_upgrades" +version = "0.20.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:15fdd63f6b50a0fda7b3f8f434120aaf7637bcdfe6fd8d275ad57343d5ede5e1" + +[[package]] +name = "openzeppelin_utils" +version = "0.20.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:44f32d242af1e43982decc49c563e613a9b67ade552f5c3d5cde504e92f74607" + +[[package]] +name = "snforge_scarb_plugin" +version = "0.49.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:903150f0e9542e4277d417029eea4c03af0db398b581f9f7ae3ebbdac9afc657" + +[[package]] +name = "snforge_std" +version = "0.49.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:73d73653cc4356ec51b92a6bec9d8385b20318170c2f2ade7891e5185a0e7e64" +dependencies = [ + "snforge_scarb_plugin", +] + +[[package]] +name = "test" +version = "0.1.0" +dependencies = [ + "openzeppelin", + "snforge_std", +] diff --git a/testing/Scarb.toml b/testing/Scarb.toml new file mode 100644 index 0000000..30df082 --- /dev/null +++ b/testing/Scarb.toml @@ -0,0 +1,53 @@ +[package] +name = "test" +version = "0.1.0" +edition = "2024_07" + +# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html + +[dependencies] +starknet = "2.12.2" +openzeppelin = "0.20.0" + +[dev-dependencies] +snforge_std = "0.49.0" +assert_macros = "2.12.2" + +[[target.starknet-contract]] +sierra = true + +[scripts] +test = "snforge test" + +[tool.scarb] +allow-prebuilt-plugins = ["snforge_std"] + +# Visit https://foundry-rs.github.io/starknet-foundry/appendix/scarb-toml.html for more information + +# [tool.snforge] # Define `snforge` tool section +# exit_first = true # Stop tests execution immediately upon the first failure +# fuzzer_runs = 1234 # Number of runs of the random fuzzer +# fuzzer_seed = 1111 # Seed for the random fuzzer + +# [[tool.snforge.fork]] # Used for fork testing +# name = "SOME_NAME" # Fork name +# url = "http://your.rpc.url" # Url of the RPC provider +# block_id.tag = "latest" # Block to fork from (block tag) + +# [[tool.snforge.fork]] +# name = "SOME_SECOND_NAME" +# url = "http://your.second.rpc.url" +# block_id.number = "123" # Block to fork from (block number) + +# [[tool.snforge.fork]] +# name = "SOME_THIRD_NAME" +# url = "http://your.third.rpc.url" +# block_id.hash = "0x123" # Block to fork from (block hash) + +# [profile.dev.cairo] # Configure Cairo compiler +# unstable-add-statements-code-locations-debug-info = true # Should be used if you want to use coverage +# unstable-add-statements-functions-debug-info = true # Should be used if you want to use coverage/profiler +# inlining-strategy = "avoid" # Should be used if you want to use coverage + +# [features] # Used for conditional compilation +# enable_for_tests = [] # Feature name and list of other features that should be enabled with it diff --git a/testing/snfoundry.toml b/testing/snfoundry.toml new file mode 100644 index 0000000..0f29e90 --- /dev/null +++ b/testing/snfoundry.toml @@ -0,0 +1,11 @@ +# Visit https://foundry-rs.github.io/starknet-foundry/appendix/snfoundry-toml.html +# and https://foundry-rs.github.io/starknet-foundry/projects/configuration.html for more information + +# [sncast.default] # Define a profile name +# url = "https://starknet-sepolia.public.blastapi.io/rpc/v0_8" # Url of the RPC provider +# accounts-file = "../account-file" # Path to the file with the account data +# account = "mainuser" # Account from `accounts_file` or default account file that will be used for the transactions +# keystore = "~/keystore" # Path to the keystore file +# wait-params = { timeout = 300, retry-interval = 10 } # Wait for submitted transaction parameters +# block-explorer = "StarkScan" # Block explorer service used to display links to transaction details +# show-explorer-links = true # Print links pointing to pages with transaction details in the chosen block explorer diff --git a/testing/src/RewardToken.cairo b/testing/src/RewardToken.cairo new file mode 100644 index 0000000..b71c8b1 --- /dev/null +++ b/testing/src/RewardToken.cairo @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IExternal { + fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256); +} +#[starknet::contract] +pub mod RewardERC20 { + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::token::erc20::interface::IERC20Metadata; + use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use starknet::ContractAddress; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub erc20: ERC20Component::Storage, + #[substorage(v0)] + pub ownable: OwnableComponent::Storage, + custom_decimals: u8 // Add custom decimals storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC20Event: ERC20Component::Event, + #[flat] + OwnableEvent: OwnableComponent::Event, + } + + #[constructor] + fn constructor( + ref self: ContractState, owner: ContractAddress, name: ByteArray, symbol: ByteArray, + ) { + self.erc20.initializer(name, symbol); + self.ownable.initializer(owner); + self.custom_decimals.write(8); + } + + #[abi(embed_v0)] + impl CustomERC20MetadataImpl of IERC20Metadata { + fn name(self: @ContractState) -> ByteArray { + self.erc20.name() + } + + fn symbol(self: @ContractState) -> ByteArray { + self.erc20.symbol() + } + + fn decimals(self: @ContractState) -> u8 { + self.custom_decimals.read() // Return custom value + } + } + + // Keep existing implementations + #[abi(embed_v0)] + impl ERC20Impl = ERC20Component::ERC20Impl; + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl InternalImpl = ERC20Component::InternalImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + #[abi(embed_v0)] + impl ExternalImpl of super::IExternal { + fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256) { + self.erc20.mint(recipient, amount); + } + } +} diff --git a/testing/src/StakingContract.cairo b/testing/src/StakingContract.cairo new file mode 100644 index 0000000..1621b01 --- /dev/null +++ b/testing/src/StakingContract.cairo @@ -0,0 +1,272 @@ +#[starknet::contract] +mod Staking { + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::security::pausable::PausableComponent; + use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + use starknet::storage::{ + Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, + StoragePointerWriteAccess, + }; + use starknet::{ContractAddress, get_block_timestamp, get_caller_address, get_contract_address}; + use test::interfaces::IStaking::IStaking; + use test::types::StakeDetails; + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: PausableComponent, storage: pausable, event: PausableEvent); + + // Ownable Mixin + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + // Pausable Mixin + #[abi(embed_v0)] + impl PausableImpl = PausableComponent::PausableImpl; + impl PausableInternalImpl = PausableComponent::InternalImpl; + + + #[storage] + struct Storage { + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + pausable: PausableComponent::Storage, + // ERC20 token addresses + stark_token: ContractAddress, + reward_token: ContractAddress, + duration: u64, + // Reward distribution state + reward_rate: u256, // rewards per second + reward_per_token_stored: u256, // cumulative reward per token + last_update_time: u64, // last time reward_per_token_stored was updated + period_finish: u64, // end time of current reward period + // User state + user_reward_per_token_paid: Map, // reward per token paid to user + rewards: Map, // pending rewards for user + balances: Map, // staked balances + stake_count: u256, + stakes: Map, + // Global state + total_supply: u256 // total staked tokens + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + PausableEvent: PausableComponent::Event, + Staked: Staked, + Unstaked: Unstaked, + RewardPaid: RewardPaid, + RewardsFunded: RewardsFunded, + RecoveredTokens: RecoveredTokens, + } + + + #[derive(Drop, starknet::Event)] + struct Staked { + #[key] + user: ContractAddress, + amount: u256, + } + + #[derive(Drop, starknet::Event)] + struct Unstaked { + #[key] + user: ContractAddress, + amount: u256, + } + + #[derive(Drop, starknet::Event)] + struct RewardPaid { + #[key] + user: ContractAddress, + reward: u256, + } + + #[derive(Drop, starknet::Event)] + struct RewardsFunded { + amount: u256, + duration: u64, + } + + #[derive(Drop, starknet::Event)] + struct RecoveredTokens { + token: ContractAddress, + amount: u256, + #[key] + to: ContractAddress, + } + + + #[constructor] + fn constructor( + ref self: ContractState, reward_token: ContractAddress, stark_token: ContractAddress, + ) { + self.stark_token.write(stark_token); + self.reward_token.write(reward_token); + } + + #[abi(embed_v0)] + impl StakingImpl of IStaking { + /// Stake tokens to earn rewards + fn stake(ref self: ContractState, amount: u256, duration: u64) -> u256 { + assert(amount > 0, 'Amount must be > 0'); + + let caller = get_caller_address(); + + let id = self.stake_count.read() + 1; + + // Transfer tokenget_block_timestamps from user to contract + let stark_token = IERC20Dispatcher { contract_address: self.stark_token.read() }; + stark_token.transfer_from(caller, get_contract_address(), amount); + + // Update user balance and total supply + let current_balance = self.balances.read(caller); + self.balances.write(caller, current_balance - amount); + let current_total = self.total_supply.read(); + self.total_supply.write(current_total + amount); + + let stake_details = StakeDetails { id, owner: caller, duration, amount, valid: true }; + + self.stakes.write(id, stake_details); + self.stake_count.write(id); + + self.emit(Staked { user: caller, amount }); + + id + } + + fn get_stake_details(self: @ContractState, id: u256) -> StakeDetails { + let stake = self.stakes.read(id); + stake + } + + /// Unstake tokens + fn unstake(ref self: ContractState, amount: u256) { + self.pausable.assert_not_paused(); + assert(amount > 0, 'Amount must be > 0'); + + let caller = get_caller_address(); + let current_balance = self.balances.read(caller); + assert(current_balance >= amount, 'Insufficient balance'); + + self.update_reward(caller); + + // Update user balance and total supply + self.balances.write(caller, current_balance + amount); + let current_total = self.total_supply.read(); + self.total_supply.write(current_total - amount); + + // Transfer tokens back to user + let stark_token = IERC20Dispatcher { contract_address: self.stark_token.read() }; + stark_token.transfer(caller, amount); + + self.emit(Unstaked { user: caller, amount }); + } + + fn get_strk_address(self: @ContractState) -> ContractAddress { + self.stark_token.read() + } + fn get_reward_address(self: @ContractState) -> ContractAddress { + self.reward_token.read() + } + + + /// Claim accumulated rewards + fn claim_rewards(ref self: ContractState) { + let caller = get_caller_address(); + + let reward = self.rewards.read(caller); + assert(reward > 0, 'No rewards to claim'); + + self.rewards.write(caller, 0); + + // Transfer reward tokens to user + let reward_token = IERC20Dispatcher { contract_address: self.reward_token.read() }; + reward_token.transfer(caller, reward); + + self.emit(RewardPaid { user: caller, reward }); + } + + /// Get earned rewards for an account + fn earned(self: @ContractState, account: ContractAddress) -> u256 { + let balance = self.balances.read(account); + let reward_per_token = self.reward_per_token(); + let user_paid = self.user_reward_per_token_paid.read(account); + let pending = self.rewards.read(account); + + balance * (reward_per_token - user_paid) / 1_000_000_000_000_000_000 + pending + } + + /// Get staked balance for an account + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { + self.balances.read(account) + } + + /// Get total staked tokens + fn total_supply(self: @ContractState) -> u256 { + self.total_supply.read() + } + + /// Get last time reward was applicable + fn last_time_reward_applicable(self: @ContractState) -> u64 { + let current_time = get_block_timestamp(); + let finish = self.period_finish.read(); + if current_time < finish { + current_time + } else { + finish + } + } + + /// Get current reward per token + fn reward_per_token(self: @ContractState) -> u256 { + let total_supply = self.total_supply.read(); + if total_supply == 0 { + self.reward_per_token_stored.read() + } else { + let last_time = self.last_time_reward_applicable(); + let last_update = self.last_update_time.read(); + let time_diff = last_time - last_update; + let reward_rate = self.reward_rate.read(); + + self.reward_per_token_stored.read() + + (reward_rate * time_diff.into() * 1_000_000_000_000_000_000) / total_supply + } + } + } + + + #[generate_trait] + impl InternalImpl of InternalTrait { + /// Update reward for a specific account + fn update_reward(ref self: ContractState, account: ContractAddress) { + let reward_per_token = self.reward_per_token(); + self.reward_per_token_stored.write(reward_per_token); + self.last_update_time.write(self.last_time_reward_applicable()); + + let zero_address = 0.try_into().unwrap(); + if account != zero_address { + let balance = self.balances.read(account); + let user_paid = self.user_reward_per_token_paid.read(account); + self + .rewards + .write( + account, + balance * (reward_per_token - user_paid) / 1_000_000_000_000_000_000 + + self.rewards.read(account), + ); + self.user_reward_per_token_paid.write(account, reward_per_token); + } + } + + /// Update global reward per token stored + fn update_reward_per_token_stored(ref self: ContractState) { + let reward_per_token = self.reward_per_token(); + self.reward_per_token_stored.write(reward_per_token); + self.last_update_time.write(self.last_time_reward_applicable()); + } + } +} diff --git a/testing/src/interfaces/ICounter.cairo b/testing/src/interfaces/ICounter.cairo new file mode 100644 index 0000000..dcd920e --- /dev/null +++ b/testing/src/interfaces/ICounter.cairo @@ -0,0 +1,6 @@ +#[Starknet::interface] +pub trait ICounter { + fn get_count(self: @TContractState) -> u32; + fn increment(ref self: TContractState); + fn decrement(ref self: TContractState); +} diff --git a/testing/src/interfaces/IHelloStarknet.cairo b/testing/src/interfaces/IHelloStarknet.cairo new file mode 100644 index 0000000..251c45a --- /dev/null +++ b/testing/src/interfaces/IHelloStarknet.cairo @@ -0,0 +1,9 @@ +/// Interface representing `HelloContract`. +/// This interface allows modification and retrieval of the contract balance. +#[Starknet::interface] +pub trait IHelloStarknet { + /// Increase contract balance. + fn increase_balance(ref self: TContractState, amount: felt252); + /// Retrieve contract balance. + fn get_balance(self: @TContractState) -> felt252; +} diff --git a/testing/src/interfaces/IOwnerFunctions.cairo b/testing/src/interfaces/IOwnerFunctions.cairo new file mode 100644 index 0000000..15b167d --- /dev/null +++ b/testing/src/interfaces/IOwnerFunctions.cairo @@ -0,0 +1,9 @@ +use starknet::ContractAddress; + +#[starknet::interface] +trait IOwnerFunctions { + fn fund_rewards(ref self: TContractState, amount: u256, duration: u64); + fn pause(ref self: TContractState); + fn unpause(ref self: TContractState); + fn recover_erc20(ref self: TContractState, token: ContractAddress, amount: u256); +} diff --git a/testing/src/interfaces/IStaking.cairo b/testing/src/interfaces/IStaking.cairo new file mode 100644 index 0000000..3184c78 --- /dev/null +++ b/testing/src/interfaces/IStaking.cairo @@ -0,0 +1,17 @@ +use starknet::ContractAddress; +use test::types::StakeDetails; +// Interfaces +#[starknet::interface] +pub trait IStaking { + fn stake(ref self: TContractState, amount: u256, duration: u64) -> u256; + fn unstake(ref self: TContractState, amount: u256); + fn claim_rewards(ref self: TContractState); + fn earned(self: @TContractState, account: ContractAddress) -> u256; + fn balance_of(self: @TContractState, account: ContractAddress) -> u256; + fn total_supply(self: @TContractState) -> u256; + fn last_time_reward_applicable(self: @TContractState) -> u64; + fn reward_per_token(self: @TContractState) -> u256; + fn get_stake_details(self: @TContractState, id: u256) -> StakeDetails; + fn get_strk_address(self: @TContractState) -> ContractAddress; + fn get_reward_address(self: @TContractState) -> ContractAddress; +} diff --git a/testing/src/lib.cairo b/testing/src/lib.cairo new file mode 100644 index 0000000..cf61f93 --- /dev/null +++ b/testing/src/lib.cairo @@ -0,0 +1,8 @@ +pub mod RewardToken; +pub mod StakingContract; + +pub mod interfaces { + pub mod IStaking; +} + +pub mod types; diff --git a/testing/src/types.cairo b/testing/src/types.cairo new file mode 100644 index 0000000..4ef7be2 --- /dev/null +++ b/testing/src/types.cairo @@ -0,0 +1,10 @@ +use starknet::ContractAddress; + +#[derive(Drop, Serde, starknet::Store)] +pub struct StakeDetails { + pub id: u256, + pub owner: ContractAddress, + pub duration: u64, + pub amount: u256, + pub valid: bool, +} diff --git a/testing/tests/test_contract.cairo b/testing/tests/test_contract.cairo new file mode 100644 index 0000000..08dbf03 --- /dev/null +++ b/testing/tests/test_contract.cairo @@ -0,0 +1,233 @@ +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use snforge_std::{ + ContractClassTrait, DeclareResultTrait, declare, start_cheat_caller_address, + stop_cheat_caller_address, +}; +use starknet::{ContractAddress, contract_address_const}; +use test::RewardToken::{IExternalDispatcher, IExternalDispatcherTrait}; +use test::interfaces::IStaking::{IStakingDispatcher, IStakingDispatcherTrait}; + +fn deploy_contract() -> (IStakingDispatcher, ContractAddress, ContractAddress) { + let contract = declare("Staking").unwrap().contract_class(); + // Define constructor calldata + let (strk_address, reward_address) = deploy_erc20(); + let mut constructor_args = array![reward_address.into(), strk_address.into()]; + + let (contract_address, _) = contract.deploy(@constructor_args).unwrap(); + + (IStakingDispatcher { contract_address }, strk_address, reward_address) +} + +fn deploy_erc20() -> (ContractAddress, ContractAddress) { + let owner: ContractAddress = contract_address_const::<'aji'>(); + let name: ByteArray = "STRK"; + let sym: ByteArray = "Sym"; + let reward: ByteArray = "Reward"; + let reward_sym: ByteArray = "RWD"; + // Deploy mock ERC20 + let erc20_class = declare("RewardERC20").unwrap().contract_class(); + + // Pass ByteArray directly for name and symbol + let mut calldata = ArrayTrait::new(); + owner.serialize(ref calldata); + name.serialize(ref calldata); + sym.serialize(ref calldata); + + let (strk_address, _) = erc20_class.deploy(@calldata).unwrap(); + + let mut usdc_calldata = ArrayTrait::new(); + owner.serialize(ref usdc_calldata); + reward.serialize(ref usdc_calldata); + reward_sym.serialize(ref usdc_calldata); + let (reward_address, _) = erc20_class.deploy(@usdc_calldata).unwrap(); + + (strk_address, reward_address) +} + + +#[test] +fn test_deployment() { + let (dispatcher, strk_address, reward_address) = deploy_contract(); + let s_address = dispatcher.get_strk_address(); + let r_address = dispatcher.get_reward_address(); + + assert(s_address == strk_address, 'invalid strk address'); + assert(r_address == reward_address, 'invalid reward address'); +} + +#[test] +fn test_stake() { + let (dispatcher, strk_address, _) = deploy_contract(); + let caller: ContractAddress = contract_address_const::<'aji'>(); + let stake_amount: u256 = 1000; + let stake_duration: u64 = 60 * 60 * 24 * 7; // 1 week + + // Mint some STRK to caller + let strk_mint = IExternalDispatcher { contract_address: strk_address }; + strk_mint.mint(caller, 10000); + + let strk = IERC20Dispatcher { contract_address: strk_address }; + let initial_balance = strk.balance_of(caller); + + start_cheat_caller_address(strk_address, caller); + // Approve staking contract to spend caller's STRK + strk.approve(dispatcher.contract_address, stake_amount); + let allowance = strk.allowance(caller, dispatcher.contract_address); + stop_cheat_caller_address(strk_address); + + println!("Allowance: {}", allowance); + println!("Initial Balance: {}", initial_balance); + + start_cheat_caller_address(dispatcher.contract_address, caller); + // Stake tokens + let stake_id = dispatcher.stake(stake_amount, stake_duration); + let post_stake_balance = strk.balance_of(caller); + + let p_allowance = strk.allowance(caller, dispatcher.contract_address); + println!("Allowance after stake: {}", p_allowance); + println!("Post stake Balance: {}", post_stake_balance); + + assert(post_stake_balance == initial_balance - stake_amount, 'stake failed'); + let contract_balance = strk.balance_of(dispatcher.contract_address); + assert(contract_balance == stake_amount, 'contract balance incorrect'); + + let staked_balance = dispatcher.balance_of(caller); + assert(staked_balance == stake_amount, 'staked balance incorrect'); + + // Get stake details + let stake_details = dispatcher.get_stake_details(stake_id); + assert(stake_details.owner == caller, 'stake owner incorrect'); + assert(stake_details.amount == stake_amount, 'stake amount incorrect'); + assert(stake_details.duration == stake_duration, 'stake duration incorrect'); + assert(stake_details.valid, 'stake valid incorrect'); +} + +#[test] +fn test_Unstake() { + let (dispatcher, strk_address, _) = deploy_contract(); + let caller: ContractAddress = contract_address_const::<'aji'>(); + let stake_amount: u256 = 1000; + let unstake_amount: u256 = 500; + let stake_duration: u64 = 60 * 60 * 24 * 7; // 1 week + + // Mint some STRK to caller + let strk_mint = IExternalDispatcher { contract_address: strk_address }; + strk_mint.mint(caller, 10000); + + let strk = IERC20Dispatcher { contract_address: strk_address }; + let initial_balance = strk.balance_of(caller); + + start_cheat_caller_address(strk_address, caller); + // Approve staking contract to spend caller's STRK + strk.approve(dispatcher.contract_address, stake_amount); + stop_cheat_caller_address(strk_address); + + start_cheat_caller_address(dispatcher.contract_address, caller); + // Stake tokens + let _ = dispatcher.stake(stake_amount, stake_duration); + let post_stake_balance = strk.balance_of(caller); + assert(post_stake_balance == initial_balance - stake_amount, 'stake failed'); + + // Now unstake + dispatcher.unstake(unstake_amount); + let post_unstake_balance = strk.balance_of(caller); + stop_cheat_caller_address(dispatcher.contract_address); + + // Check user balance increased by unstake_amount + assert(post_unstake_balance == post_stake_balance + unstake_amount, 'unstake failed'); + + // Check contract balance decreased + let contract_balance = strk.balance_of(dispatcher.contract_address); + assert(contract_balance == stake_amount - unstake_amount, 'contract balance incorrect'); + + // Check staked balance decreased + let staked_balance = dispatcher.balance_of(caller); + assert(staked_balance == stake_amount - unstake_amount, 'staked balance incorrect'); +} + +#[test] +fn test_balance_of() { + let (dispatcher, strk_address, _) = deploy_contract(); + let caller: ContractAddress = contract_address_const::<'aji'>(); + let stake_amount: u256 = 1000; + + // Mint and stake + let strk_mint = IExternalDispatcher { contract_address: strk_address }; + strk_mint.mint(caller, 10000); + + let strk = IERC20Dispatcher { contract_address: strk_address }; + start_cheat_caller_address(strk_address, caller); + strk.approve(dispatcher.contract_address, stake_amount); + stop_cheat_caller_address(strk_address); + + start_cheat_caller_address(dispatcher.contract_address, caller); + let _ = dispatcher.stake(stake_amount, 60 * 60 * 24 * 7); + stop_cheat_caller_address(dispatcher.contract_address); + + let balance = dispatcher.balance_of(caller); + assert(balance == stake_amount, 'balance_of incorrect'); +} + +#[test] +fn test_total_supply() { + let (dispatcher, strk_address, _) = deploy_contract(); + let caller: ContractAddress = contract_address_const::<'aji'>(); + let stake_amount: u256 = 1000; + + let initial_supply = dispatcher.total_supply(); + assert(initial_supply == 0, 'initial supply not zero'); + + // Mint and stake + let strk_mint = IExternalDispatcher { contract_address: strk_address }; + strk_mint.mint(caller, 10000); + + let strk = IERC20Dispatcher { contract_address: strk_address }; + start_cheat_caller_address(strk_address, caller); + strk.approve(dispatcher.contract_address, stake_amount); + stop_cheat_caller_address(strk_address); + + start_cheat_caller_address(dispatcher.contract_address, caller); + let _ = dispatcher.stake(stake_amount, 60 * 60 * 24 * 7); + stop_cheat_caller_address(dispatcher.contract_address); + + let supply = dispatcher.total_supply(); + assert(supply == stake_amount, 'total_supply incorrect'); +} + +#[test] +fn test_earned() { + let (dispatcher, _, _) = deploy_contract(); + let caller: ContractAddress = contract_address_const::<'aji'>(); + + // No staking, no rewards + let earned_amount = dispatcher.earned(caller); + assert(earned_amount == 0, 'earned should be zero initially'); +} + +#[test] +fn test_reward_per_token() { + let (dispatcher, _, _) = deploy_contract(); + + let rpt = dispatcher.reward_per_token(); + assert(rpt == 0, 'reward_per_token should be zero'); +} + +#[test] +fn test_last_time_reward_applicable() { + let (dispatcher, _, _) = deploy_contract(); + + let ltra = dispatcher.last_time_reward_applicable(); + // Since period_finish is 0, and current_time > 0, should return 0 + assert(ltra == 0, 'ltra incorrect'); +} + +#[test] +#[should_panic(expected: ('No rewards to claim',))] +fn test_claim_rewards_no_rewards() { + let (dispatcher, _, _) = deploy_contract(); + let caller: ContractAddress = contract_address_const::<'aji'>(); + + start_cheat_caller_address(dispatcher.contract_address, caller); + dispatcher.claim_rewards(); + stop_cheat_caller_address(dispatcher.contract_address); +}