Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions starknet_contracts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
target
.snfoundry_cache/
snfoundry_trace/
coverage/
profile/
24 changes: 24 additions & 0 deletions starknet_contracts/Scarb.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Code generated by scarb DO NOT EDIT.
version = 1

[[package]]
name = "snforge_scarb_plugin"
version = "0.43.1"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:178e1e2081003ae5e40b5a8574654bed15acbd31cce651d4e74fe2f009bc0122"

[[package]]
name = "snforge_std"
version = "0.43.1"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:17bc65b0abfb9b174784835df173f9c81c9ad39523dba760f30589ef53cf8bb5"
dependencies = [
"snforge_scarb_plugin",
]

[[package]]
name = "starknet_contracts"
version = "0.1.0"
dependencies = [
"snforge_std",
]
52 changes: 52 additions & 0 deletions starknet_contracts/Scarb.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
[package]
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"

[dev-dependencies]
snforge_std = "0.43.1"
assert_macros = "2.11.4"

[[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
11 changes: 11 additions & 0 deletions starknet_contracts/snfoundry.toml
Original file line number Diff line number Diff line change
@@ -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
52 changes: 52 additions & 0 deletions starknet_contracts/src/contracts/HelloStarknet.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/// Simple contract for managing balance.
#[starknet::contract]
pub mod HelloStarknet {

use starknet_contracts::interfaces::IHelloStarknet::IHelloStarknet;
// use starknet::storage::{StoragePointerReadAccess, StoragePathEntry, StoragePointerWriteAccess, Map };
use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, StoragePointerWriteAccess };
use starknet::{ContractAddress, get_caller_address};

#[storage]
struct Storage {
balance: felt252,
balances: Map<ContractAddress, felt252>,
}

#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
Balance : BalanceIncreased,
}

#[derive(Drop, starknet::Event)]
pub struct BalanceIncreased {
pub caller: ContractAddress,
pub amount: felt252,
}

#[abi(embed_v0)]
impl HelloStarknetImpl of IHelloStarknet<ContractState> {
fn increase_balance(ref self: ContractState, amount: felt252) {
assert(amount != 0, 'Amount cannot be 0');
let caller = get_caller_address();

let updated_amount = self.balance.read() + amount;
self.balance.write(updated_amount);

// let unique_balance = self.balances.entry(caller).read();

let unique_balance = self.balances.read(caller);
// self.balances.entry(caller).write(unique_balance + amount);
self.balances.write(caller, unique_balance + amount);

// self.balance.write(self.balance.read() + amount);

self.emit(BalanceIncreased{caller, amount});
}

fn get_balance(self: @ContractState) -> felt252 {
self.balance.read()
}
}
}
51 changes: 51 additions & 0 deletions starknet_contracts/src/contracts/counter.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#[starknet::contract]
pub mod Counter {
// use starknet::ContractAddress;
// use starknet::get_caller_address;
use starknet_contracts::interfaces::ICounter::ICounter;
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};

#[storage]
struct Storage {
count: u32,
}

#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
CountUpdated : CountUpdated,
}

#[derive(Drop, starknet::Event)]
struct CountUpdated {
old_value: u32,
new_value: u32,
}

#[constructor]
fn constructor(ref self: ContractState) {
self.count.write(0);
}

#[abi(embed_v0)]
impl CounterImpl of ICounter<ContractState> {
fn get_count(self: @ContractState) -> u32 {
self.count.read()
}

fn increment(ref self: ContractState) {
let old_value = self.count.read();
let new_value = old_value + 1;
self.count.write(new_value);
self.emit(CountUpdated { old_value, new_value });
}

fn decrement(ref self: ContractState) {
let old_value = self.count.read();
assert(old_value > 0, 'Count cannot be negative');
let new_value = old_value - 1;
self.count.write(new_value);
self.emit(CountUpdated { old_value, new_value });
}
}
}
6 changes: 6 additions & 0 deletions starknet_contracts/src/interfaces/ICounter.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#[starknet::interface]
pub trait ICounter<TContractState> {
fn get_count(self: @TContractState) -> u32;
fn increment(ref self: TContractState);
fn decrement(ref self: TContractState);
}
9 changes: 9 additions & 0 deletions starknet_contracts/src/interfaces/IHelloStarknet.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/// Interface representing `HelloContract`.
/// This interface allows modification and retrieval of the contract balance.
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
/// Increase contract balance.
fn increase_balance(ref self: TContractState, amount: felt252);
/// Retrieve contract balance.
fn get_balance(self: @TContractState) -> felt252;
}
9 changes: 9 additions & 0 deletions starknet_contracts/src/lib.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
pub mod interfaces{
pub mod IHelloStarknet;
pub mod ICounter;
}

pub mod contracts{
pub mod HelloStarknet;
pub mod counter;
}
91 changes: 91 additions & 0 deletions starknet_contracts/task.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# StarkNet Staking Contract Assignment

**Title:** Staking Stark Token on StarkNet — Earn ERC20 Rewards

**Summary**
Create a secure staking smart contract in **Cairo** for StarkNet where users stake the `Stark` token (an ERC20 on StarkNet) and receive rewards in another ERC20 token (e.g., `RewardToken`).
---

## Objectives

* Implement a gas‑efficient and secure staking contract.
* Support staking, unstaking, and reward distribution based on stake share and time.
* Write comprehensive unit tests and a deployment script using **Starknet Foundry (snforge)**.
* Provide clear documentation and a README explaining design choices and test results.

---


## Deliverables

1. `staking.cairo` — Cairo contract implementing staking functionality.
2. `stark_token.cairo` and `reward_token.cairo` — simple ERC20 tokens for local testing.
3. (OPTIONAL) Deployment script to a StarkNet testnet (Sepolia or Testnet2).
4. `README.md` with instructions to run tests, deploy locally, and explanation of reward formula and security considerations.
5. Optional: small frontend demo (bonus).

---

## Functional Requirements

* Users can `stake(amount: u256)` Stark tokens by transferring them to the staking contract (use ERC20 `transfer_from`).
* Users can `unstake(amount: u256)` and receive their principal back.
* Rewards are paid in a separate ERC20 `RewardToken` and accrue over time.
* Reward calculation must be fair: rewards based on stake share and time. Implement one of the following approaches:

* **Per-second reward rate**: fixed reward rate distributed proportionally to staked balance over time using `get_block_timestamp()`.
* **Reward per token stored**: maintain cumulative reward per staked token and track each user’s paid rewards.
* Users can `claim_rewards()` to withdraw accumulated reward tokens.
* Owner/manager functions:

* `fund_rewards(amount: u256, duration: u64)` to top up reward pool and set distribution duration.
* `pause()` / `unpause()` (recommended) to halt staking in emergencies.
* `recover_erc20(token: ContractAddress, amount: u256)` to rescue mistakenly sent tokens (with restrictions: cannot rescue staked or reward tokens while active distribution).
* Emit events: `Staked`, `Unstaked`, `RewardPaid`, `RewardsFunded`, `Paused`, `Unpaused`, `RecoveredTokens`.

---

## Non-functional & Security Requirements

* Protect against reentrancy (use `#[external(v0)]` best practices and checks‑effects‑interactions pattern).
* Correct use of ERC20 `transfer_from` & token approvals.
* Gas efficiency: minimize storage writes; use felt252/u256 carefully.
* Provide reasoning in README about edge cases (e.g., insufficient reward pool, rounding behavior, leftover rewards after distribution period).

---

## Test Cases (minimum)

1. **Staking and balances**

* User stakes N tokens: contract balance increases, user stake recorded, total_staked updated.
2. **Unstaking and principal return**

* User unstakes N tokens: receives N tokens back, balances updated.
3. **Reward accrual over time**

* Fund rewards for a duration; advance block timestamp; verify `earned()` matches expected formula for single staker and multiple stakers.
4. **Multiple stakers, proportional rewards**

* Two stakers stake different amounts at different times; simulate time progression and verify correct reward share.
5. **Claiming rewards**

* After accrual, user calls `claim_rewards()` and receives correct `RewardToken` amount.
6. **Edge cases**

* Staking 0 should revert.
* Unstaking more than staked should revert.
* Claiming with no rewards should not revert and pay zero.
7. **Security behaviors**

* Attempted reentrancy should fail.
* Owner-only functions revert for non-owners.

---

## Submission Instructions

* Create a Pull request on this repository.
* Include all source, tests, and README.

**Good luck building on StarkNet!**
47 changes: 47 additions & 0 deletions starknet_contracts/tests/test_contract.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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));
}
};
}