Skip to content
Closed
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
363 changes: 54 additions & 309 deletions .github/workflows/anchor.yml

Large diffs are not rendered by default.

28 changes: 21 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
# CI Pipeline - Runs on every Pull Request
# Performs linting, type checking, unit tests, and build checks

name: CI

on:
Expand All @@ -15,13 +12,14 @@ concurrency:
env:
PYTHON_VERSION: '3.11'
NODE_VERSION: '20'
RUST_VERSION: '1.85'
RUST_VERSION: '1.79'

jobs:
# ==================== BACKEND (Python/FastAPI) ====================
backend-lint:
name: Backend Lint (Ruff)
runs-on: ubuntu-latest
if: hashFiles('backend/requirements.txt') != ''
steps:
- name: Checkout
uses: actions/checkout@v4
Expand All @@ -45,6 +43,7 @@ jobs:
backend-tests:
name: Backend Tests (pytest)
runs-on: ubuntu-latest
if: hashFiles('backend/requirements.txt') != ''
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down Expand Up @@ -208,15 +207,30 @@ jobs:
- name: Install Solana CLI
if: steps.check.outputs.has_anchor == 'true'
run: |
mkdir -p $HOME/.local/bin
sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"
echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH

- name: Cache Anchor
uses: actions/cache@v4
with:
path: |
~/.avm
~/.cargo/bin/anchor
key: anchor-0.30.1-${{ runner.os }}

- name: Install Anchor
if: steps.check.outputs.has_anchor == 'true'
run: |
cargo install --git https://github.com/coral-xyz/anchor avm --locked --force
avm install 0.30.1
avm use 0.30.1
mkdir -p $HOME/.local/bin
npm install -g @coral-xyz/anchor-cli@0.30.1 --prefix=$HOME/.local
echo "$HOME/.local/bin" >> $GITHUB_PATH

- name: Verify Tools
if: steps.check.outputs.has_anchor == 'true'
run: |
solana --version
anchor --version

- name: Build Anchor Program
if: steps.check.outputs.has_anchor == 'true'
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ downloads/
eggs/
.eggs/
lib/
!frontend/src/lib/
lib64/
parts/
sdist/
Expand Down
18 changes: 9 additions & 9 deletions contracts/Anchor.toml
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
[toolchain]
anchor_version = "0.30.0"
anchor_version = "0.30.1"

[features]
resolution = true
skip-lint = false

[programs.localnet]
escrow = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"
reputation = "RPTNzHXtmDSy7y7UoU3X3r56QQW3bRGSJhWKFpkXHmk"
treasury = "TRSRy3jy5nK6tMxpPjzKpWkMN1AXyTWPz1yXD1yGxbj"
staking = "5yBCCECfnEPBQTaKxhwABHNeAmb5j8rNHLLuvJfUCkZ"
escrow = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"
bounty_registry = "DwCJkFvRD7NJqzUnPo1njptVScDJsMS6ezZPNXxRrQxe"
treasury = "TRSRy3jy5nK6tMxpPjzKpWkMN1AXyTWPz1yXD1yGxbj"
fndry_staking = "Wkvaa5DdWWN1GWAa4UX26CJzGuU5otXF7obLL27TFET"

[programs.devnet]
escrow = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"
reputation = "RPTNzHXtmDSy7y7UoU3X3r56QQW3bRGSJhWKFpkXHmk"
treasury = "TRSRy3jy5nK6tMxpPjzKpWkMN1AXyTWPz1yXD1yGxbj"
staking = "5yBCCECfnEPBQTaKxhwABHNeAmb5j8rNHLLuvJfUCkZ"
escrow = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"
bounty_registry = "DwCJkFvRD7NJqzUnPo1njptVScDJsMS6ezZPNXxRrQxe"
treasury = "TRSRy3jy5nK6tMxpPjzKpWkMN1AXyTWPz1yXD1yGxbj"
fndry_staking = "Wkvaa5DdWWN1GWAa4UX26CJzGuU5otXF7obLL27TFET"

[registry]
url = "https://api.apr.dev"
Expand Down
3 changes: 2 additions & 1 deletion contracts/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ members = ["programs/*"]
resolver = "2"

[workspace.dependencies]
anchor-lang = "0.30.0"
anchor-lang = "0.30.1"


[profile.release]
overflow-checks = true
Expand Down
10 changes: 3 additions & 7 deletions contracts/bounty-registry/Anchor.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[toolchain]
anchor_version = "0.30.1"

[features]
seeds = false
skip-lint = false
Expand All @@ -18,13 +21,6 @@ wallet = "~/.config/solana/id.json"
[scripts]
test = "npx ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

# ---------------------------------------------------------------------------
# Local validator configuration for `anchor test`
# ---------------------------------------------------------------------------
# anchor test automatically starts a local validator using these settings.
# Run `scripts/local-validator.sh` to start manually; use
# `anchor test --skip-local-validator` to connect to an already-running one.

[test]
startup_wait = 5000

Expand Down
6 changes: 6 additions & 0 deletions contracts/bounty-registry/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
members = ["programs/bounty-registry"]
resolver = "2"

[workspace.dependencies]
anchor-lang = "0.30.1"

[patch.crates-io]
proc-macro2 = "1.0.86"

[profile.release]
overflow-checks = true
lto = "fat"
Expand Down
5 changes: 2 additions & 3 deletions contracts/bounty-registry/programs/bounty-registry/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@ crate-type = ["cdylib", "lib"]
name = "bounty_registry"

[features]
idl-build = ["anchor-lang/idl-build"]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
cpi = ["no-entrypoint"]
default = []

[dependencies]
anchor-lang = "0.30.1"
anchor-lang = { workspace = true }
2 changes: 1 addition & 1 deletion contracts/rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[toolchain]
channel = "1.76"
channel = "stable"
components = ["rustfmt", "clippy"]
14 changes: 14 additions & 0 deletions contracts/staking-program/Anchor.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[toolchain]
anchor_version = "0.30.1"

[features]
seeds = false
skip-lint = false
Expand All @@ -17,3 +20,14 @@ wallet = "~/.config/solana/id.json"

[scripts]
test = "npx ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

[test]
startup_wait = 5000

[test.validator]
bind_address = "0.0.0.0"
url = "http://127.0.0.1:8899"
ledger = ".ledger"

[test.validator.slots_per_epoch]
slots_per_epoch = 32
6 changes: 6 additions & 0 deletions contracts/staking-program/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
members = ["programs/fndry-staking"]
resolver = "2"

[workspace.dependencies]
anchor-lang = "0.30.1"

[patch.crates-io]
proc-macro2 = "1.0.86"

[profile.release]
overflow-checks = true
lto = "fat"
Expand Down
14 changes: 6 additions & 8 deletions contracts/staking-program/programs/fndry-staking/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,15 @@ crate-type = ["cdylib", "lib"]
name = "fndry_staking"

[features]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
cpi = ["no-entrypoint"]
default = []
idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]
no-entrypoint = []
cpi = ["no-entrypoint"]
default = []

[dependencies]
anchor-lang = { version = "0.30.1", features = ["init-if-needed"] }
anchor-lang = { workspace = true, features = ["init-if-needed"] }
anchor-spl = { version = "0.30.1", features = ["token"] }

[dev-dependencies]
# Pull in the library build (no program entrypoint) for unit-test compilation.
# The actual on-chain integration tests use the Anchor test validator.
anchor-lang = { version = "0.30.1", features = ["init-if-needed"] }
anchor-lang = { workspace = true, features = ["init-if-needed"] }
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ pub struct ClaimRewards<'info> {
seeds = [b"config"],
bump = config.config_bump,
)]
pub config: Account<'info, StakingConfig>,
pub config: Box<Account<'info, StakingConfig>>,

/// The user's stake account PDA.
#[account(
Expand All @@ -34,14 +34,14 @@ pub struct ClaimRewards<'info> {
bump = stake_account.bump,
constraint = stake_account.owner == user.key() @ StakingError::Unauthorized,
)]
pub stake_account: Account<'info, StakeAccount>,
pub stake_account: Box<Account<'info, StakeAccount>>,

/// The reward pool token account (source of reward tokens).
#[account(
mut,
constraint = reward_pool_vault.key() == config.reward_pool_vault @ StakingError::Unauthorized,
)]
pub reward_pool_vault: Account<'info, TokenAccount>,
pub reward_pool_vault: Box<Account<'info, TokenAccount>>,

/// The user's token account to receive reward tokens.
#[account(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub struct Initialize<'info> {
seeds = [b"config"],
bump,
)]
pub config: Account<'info, StakingConfig>,
pub config: Box<Account<'info, StakingConfig>>,

/// The $FNDRY token mint.
pub token_mint: Account<'info, Mint>,
Expand All @@ -50,7 +50,7 @@ pub struct Initialize<'info> {
seeds = [b"reward_pool"],
bump,
)]
pub reward_pool_vault: Account<'info, TokenAccount>,
pub reward_pool_vault: Box<Account<'info, TokenAccount>>,

/// Solana system program.
pub system_program: Program<'info, System>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub struct Stake<'info> {
seeds = [b"config"],
bump = config.config_bump,
)]
pub config: Account<'info, StakingConfig>,
pub config: Box<Account<'info, StakingConfig>>,

/// Per-user stake account PDA.
/// Seeds: `["stake", user_pubkey]`.
Expand All @@ -36,7 +36,7 @@ pub struct Stake<'info> {
seeds = [b"stake", user.key().as_ref()],
bump,
)]
pub stake_account: Account<'info, StakeAccount>,
pub stake_account: Box<Account<'info, StakeAccount>>,

/// The $FNDRY token mint.
#[account(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub struct UnstakeComplete<'info> {
seeds = [b"config"],
bump = config.config_bump,
)]
pub config: Account<'info, StakingConfig>,
pub config: Box<Account<'info, StakingConfig>>,

/// The user's stake account PDA.
#[account(
Expand All @@ -32,15 +32,15 @@ pub struct UnstakeComplete<'info> {
bump = stake_account.bump,
constraint = stake_account.owner == user.key() @ StakingError::Unauthorized,
)]
pub stake_account: Account<'info, StakeAccount>,
pub stake_account: Box<Account<'info, StakeAccount>>,

/// PDA-owned stake vault holding the user's staked tokens.
#[account(
mut,
seeds = [b"stake_vault", user.key().as_ref()],
bump,
)]
pub stake_vault: Account<'info, TokenAccount>,
pub stake_vault: Box<Account<'info, TokenAccount>>,

/// The user's token account to receive unstaked tokens.
#[account(
Expand Down Expand Up @@ -112,27 +112,14 @@ pub fn handler(ctx: Context<UnstakeComplete>) -> Result<()> {
);
token::transfer(transfer_ctx, unstake_amount)?;

// Update stake account.
stake_account.amount = stake_account
.amount
.checked_sub(unstake_amount)
.ok_or(StakingError::MathOverflow)?;
// Update stake account state.
// NOTE: stake_account.amount was already decremented in unstake_initiate
// to prevent rewards double-dipping.
stake_account.cooldown_active = false;
stake_account.cooldown_start = 0;
stake_account.cooldown_amount = 0;
stake_account.last_claim = current_timestamp;

// Update global stats.
let config = &mut ctx.accounts.config;
config.total_staked = config
.total_staked
.checked_sub(unstake_amount)
.ok_or(StakingError::MathOverflow)?;

if stake_account.amount == 0 {
config.active_stakers = config.active_stakers.saturating_sub(1);
}

emit!(UnstakeCompletedEvent {
user: ctx.accounts.user.key(),
amount: unstake_amount,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ pub struct UnstakeInitiate<'info> {

/// Global staking configuration PDA.
#[account(
mut,
seeds = [b"config"],
bump = config.config_bump,
)]
pub config: Account<'info, StakingConfig>,
pub config: Box<Account<'info, StakingConfig>>,

/// The user's stake account PDA.
#[account(
Expand All @@ -31,7 +32,7 @@ pub struct UnstakeInitiate<'info> {
bump = stake_account.bump,
constraint = stake_account.owner == user.key() @ StakingError::Unauthorized,
)]
pub stake_account: Account<'info, StakeAccount>,
pub stake_account: Box<Account<'info, StakeAccount>>,
}

/// Initiates the unstake cooldown for the specified amount.
Expand Down Expand Up @@ -85,8 +86,27 @@ pub fn handler(ctx: Context<UnstakeInitiate>, amount: u64) -> Result<()> {
stake_account.cooldown_start = current_timestamp;
stake_account.cooldown_amount = amount;

// IMPORTANT: Subtract from active stake IMMEDIATELY to prevent "double-dipping" rewards
// during the 7-day cooldown period. The tokens remain in the vault but are no longer
// considered "staked" for reward calculations.
stake_account.amount = stake_account
.amount
.checked_sub(amount)
.ok_or(StakingError::MathOverflow)?;

// Update global stats immediately.
let config_mut = &mut ctx.accounts.config;
config_mut.total_staked = config_mut
.total_staked
.checked_sub(amount)
.ok_or(StakingError::MathOverflow)?;

if stake_account.amount == 0 {
config_mut.active_stakers = config_mut.active_stakers.saturating_sub(1);
}

let cooldown_end = current_timestamp
.checked_add(config.cooldown_seconds)
.checked_add(config_mut.cooldown_seconds)
.ok_or(StakingError::MathOverflow)?;

emit!(UnstakeInitiatedEvent {
Expand Down
Loading
Loading