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
Empty file.
117 changes: 117 additions & 0 deletions staking_contract/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Staking Contract

A secure staking smart contract in Cairo for Starknet, allowing users to stake ERC20 tokens and earn rewards proportionally based on stake share and time.

## Features

- **Staking**: Users can stake Stark tokens (ERC20) by calling `stake(amount)`.
- **Unstaking**: Users can unstake their tokens with `unstake(amount)`.
- **Rewards**: Rewards are accrued in a separate RewardToken ERC20, distributed proportionally to staked balances over time.
- **Reward Distribution**: Uses per-second reward rate, updated when funding rewards.
- **Security**: Implements reentrancy guard, checks-effects-interactions pattern, and access controls.
- **Owner Functions**: Fund rewards, pause/unpause, recover ERC20 tokens (with restrictions).

## Design Choices

### Reward Calculation

The contract uses a "reward per token stored" mechanism with per-second reward rate:

- `reward_rate`: Total rewards distributed per second.
- `reward_per_token_stored`: Cumulative rewards per staked token.
- `user_reward_per_token_paid`: Tracks rewards already paid to user.
- `rewards`: Pending rewards for user.

When rewards are funded with `fund_rewards(amount, duration)`, the `reward_rate` is set to `amount / duration`.

On each action (stake, unstake, claim), rewards are updated:

```
reward_per_token += (time_elapsed * reward_rate) / total_staked
user_rewards += balance * (reward_per_token - user_reward_per_token_paid)
```

This ensures fair distribution proportional to stake amount and time.

### Security Measures

- **Reentrancy Protection**: Uses OpenZeppelin's ReentrancyGuard component.
- **Checks-Effects-Interactions**: All checks first, then state changes, then external calls.
- **ERC20 Approvals**: Proper use of `transfer_from` for staking and reward funding.
- **Access Control**: Owner-only functions use Ownable component.
- **Pausable**: Emergency pause functionality.
- **Gas Efficiency**: Minimizes storage writes, uses efficient data structures.

### Edge Cases

- **Zero Staking**: Reverts with "Amount must be > 0".
- **Insufficient Balance**: Unstaking more than staked reverts.
- **No Rewards**: Claiming with no accrued rewards succeeds but pays zero.
- **Rounding**: Integer division may cause small rounding errors; rewards are floored.
- **Leftover Rewards**: After distribution period, any remaining rewards stay in contract until recovered by owner.
- **Token Recovery**: Cannot recover staked or reward tokens during active distribution.

## Installation

```bash
scarb build
```

## Testing

Run tests with Starknet Foundry:

```bash
snforge test
```

### Test Cases

1. **Staking Basics**: Verifies balance updates and events.
2. **Unstaking**: Checks principal return and balance updates.
3. **Reward Accrual**: Funds rewards, advances time, verifies earned amounts.
4. **Multiple Stakers**: Tests proportional rewards for different stake amounts and times.
5. **Claiming**: Ensures correct reward transfer and events.
6. **Edge Cases**: Zero amounts, insufficient balance, no rewards.
7. **Security**: Reentrancy prevention, owner-only access.

Test Results: All tests pass, covering the minimum requirements and additional security checks.

## Deployment

Use the provided script:

```bash
./scripts/deploy.sh
```

Update the script with actual token addresses and account details.

## Usage

### Staking

```cairo
staking_contract.stake(amount);
```

### Claiming Rewards

```cairo
staking_contract.claim_rewards();
```

### Funding Rewards (Owner)

```cairo
staking_contract.fund_rewards(amount, duration);
```

## Dependencies

- Starknet 2.8.4
- OpenZeppelin Cairo Contracts 0.16.0

## License

MIT
143 changes: 143 additions & 0 deletions staking_contract/Scarb.lock
Original file line number Diff line number Diff line change
@@ -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.50.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:8c29e5519362d22f2c802e4e348da846de3898cbaeac19b58aded6a009bf188e"

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

[[package]]
name = "staking_contract"
version = "0.1.0"
dependencies = [
"openzeppelin",
"snforge_std",
]
31 changes: 31 additions & 0 deletions staking_contract/Scarb.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[package]
name = "staking_contract"
version = "0.1.0"
edition = "2023_01"

[dependencies]
starknet = "2.8.4"
openzeppelin = "0.20.0"

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

[[target.starknet-contract]]
sierra = true
casm = true

[tool.snforgery]
exit-first = true

[scripts]
test = "snforge test"

[tool.scarb]
allow-prebuilt-plugins = ["snforge_std"]

[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

27 changes: 27 additions & 0 deletions staking_contract/scripts/deploy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/bash

# Deployment script for Staking Contract
# Assumes sncast is configured with account and network

# Build the project
scarb build

# Declare the contract
echo "Declaring StakingContract..."
DECLARE_OUTPUT=$(sncast --account mainuser --wait declare --contract-name StakingContract)
CLASS_HASH=$(echo "$DECLARE_OUTPUT" | grep "class_hash:" | awk '{print $2}')

echo "Class hash: $CLASS_HASH"

# Deploy the contract
# Constructor args: owner, staking_token, reward_token
# Replace with actual addresses
OWNER="0x123..." # Replace with actual owner address
STAKING_TOKEN="0x456..." # Replace with Stark token address
REWARD_TOKEN="0x789..." # Replace with RewardToken address

echo "Deploying StakingContract..."
DEPLOY_OUTPUT=$(sncast --account mainuser --wait deploy --class-hash $CLASS_HASH --constructor-args $OWNER $STAKING_TOKEN $REWARD_TOKEN)
CONTRACT_ADDRESS=$(echo "$DEPLOY_OUTPUT" | grep "contract_address:" | awk '{print $2}')

echo "Contract deployed at: $CONTRACT_ADDRESS"
5 changes: 5 additions & 0 deletions staking_contract/snfoundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# 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

[snforge]
exit-first = true
57 changes: 57 additions & 0 deletions staking_contract/src/contracts/rewardToken.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts for Cairo 3.0.0-alpha.2

#[starknet::contract]
pub mod RewardToken {
use openzeppelin::access::ownable::OwnableComponent;
use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl};
use starknet::ContractAddress;

component!(path: ERC20Component, storage: erc20, event: ERC20Event);
component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);

// External
#[abi(embed_v0)]
impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl<ContractState>;
#[abi(embed_v0)]
impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl<ContractState>;

// Internal
impl ERC20InternalImpl = ERC20Component::InternalImpl<ContractState>;
impl OwnableInternalImpl = OwnableComponent::InternalImpl<ContractState>;

#[storage]
struct Storage {
#[substorage(v0)]
erc20: ERC20Component::Storage,
#[substorage(v0)]
ownable: OwnableComponent::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.erc20.mint(owner, 1000000000000_u256);
}

#[generate_trait]
#[abi(per_item)]
impl ExternalImpl of ExternalTrait {
#[external(v0)]
fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256) {
self.ownable.assert_only_owner();
self.erc20.mint(recipient, amount);
}
}
}
Loading