Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
17 changes: 15 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,23 @@ jobs:
anchor-version: "0.31.1"
rust-version: "nightly"
solana-version: "2.1.21"

- name: "Cache Anchor build artifacts"
id: "cache-build"
uses: "sablier-labs/gha-utils/.github/actions/solana-cache@main"
with:
cache-path: target

- name: "Cache Trident CLI"
id: "cache-trident"
uses: "actions/cache@v4"
with:
path: "~/.cargo/bin/trident"
key: "trident-cli-${{ runner.os }}-0.12.0"

- name: "Install Trident CLI"
if: steps.cache-trident.outputs.cache-hit != 'true' && steps.cache-build.outputs.cache-status != 'primary'
run: "cargo install trident-cli --version 0.12.0 --locked"

- name: "Build the programs"
if: steps.cache-build.outputs.cache-status != 'primary'
run: "just build"
Expand All @@ -45,6 +55,9 @@ jobs:
if: steps.cache-build.outputs.cache-status != 'primary'
run: "just rust-check"

- name: "Run the Trident code checks"
run: "just rust-check-trident"

- name: "Run the other code checks"
run: | # shell
just prettier-check
Expand All @@ -53,7 +66,7 @@ jobs:

- name: "Run the tests"
if: steps.cache-build.outputs.cache-status != 'primary'
run: "just test-lite"
run: "just test"

- name: "Add summary"
run: | # shell
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ repomix
*.env
*.local.json
.DS_Store
trident-tests/.fuzz-artifacts
*.zip
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"rust-analyzer.inlayHints.chainingHints.enable": true,
"rust-analyzer.inlayHints.parameterHints.enable": true,
"rust-analyzer.inlayHints.typeHints.enable": true,
"rust-analyzer.linkedProjects": ["./Cargo.toml", "./trident-tests/Cargo.toml"],
"rust-analyzer.rustfmt.extraArgs": ["+nightly"],
"[javascript][json][jsonc][typescript]": {
"editor.defaultFormatter": "biomejs.biome"
Expand Down
4 changes: 2 additions & 2 deletions Anchor.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
skip-lint = false

[programs.localnet]
sablier_lockup = "4EauRKrNErKfsR4XetEZJNmvACGHbHnHV4R5dvJuqupC"
sablier_merkle_instant = "7XrxoQejBoGouW4V3aozTSwub7xSDjYqB4Go7YLjF9rV"
sablier_lockup = "5rRqnrYc5v1HTWub3nViVX1LwrrRrQTshvGiAAJvK3Er"
sablier_merkle_instant = "7gHPcvMam4Fs6eLV792xJui8bfXZwruq7RZCknrwECP1"

[provider]
cluster = "localnet"
Expand Down
12 changes: 12 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ handing off. If it fails, run `just full-write`, then re-run `just full-check` t
Skip this step when changes are limited to non-code files (e.g., documentation-only conversations, config files not
covered by the checker).

## Fuzz Test Validation

When modifying files under `trident-tests/` or `programs/lockup/src/`, run `just ttlk` to verify the Trident lockup fuzz
tests pass. All instructions must show 0 failures and 0 panics.

## Fuzz Test Conventions

- Assert messages use "must" (not "should")
- Variable names use `_pk` suffix (not `_pubkey`)
- Transaction files have section comments: `// Prepare instruction accounts`, `// Execute the ix`,
`// Assert post-execution state`

## Binary Test Pattern

All `describe` blocks in test files must follow a binary pair structure. Every failure-condition `describe` must have a
Expand Down
13 changes: 11 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = ["programs/*"]
members = ["programs/*", "crates/*"]
resolver = "2"

# Force older versions to avoid edition2024 requirements
Expand Down
13 changes: 13 additions & 0 deletions crates/sablier-common/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "sablier-common"
version = "0.1.0"
description = "Shared utilities for Sablier programs"
edition = "2021"

[lib]
name = "sablier_common"

[dependencies]
anchor-lang = { version = "0.31.1" }
anchor-spl = "0.31.1"
chainlink_solana = { git = "https://github.com/smartcontractkit/chainlink-solana", branch = "solana-2.1" }
7 changes: 7 additions & 0 deletions crates/sablier-common/src/constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
pub const ANCHOR_DISCRIMINATOR_SIZE: usize = 8;
pub const LAMPORTS_PER_SOL: u64 = 1e9 as u64; // 1 billion lamports in 1 SOL

// Account Seeds
pub mod seeds {
pub const TREASURY: &[u8] = b"treasury";
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
use anchor_lang::prelude::*;
use chainlink_solana as chainlink;

use crate::utils::{constants::LAMPORTS_PER_SOL, time::get_current_time};
use crate::{constants::LAMPORTS_PER_SOL, time::get_current_time};

// TODO: export this into a crate that'd be imported by both the lockup and merkle_instant programs.
/// Converts the fee amount from USD to lamports.
/// The price is considered to be 0 if:
/// 1. The USD fee is 0.
Expand Down
47 changes: 47 additions & 0 deletions crates/sablier-common/src/fee_collection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use anchor_lang::{
prelude::*,
solana_program::{program::invoke, system_instruction::transfer},
};

use crate::convert_usd_fee_to_lamports;

/// Charges a fee in lamports by converting a USD amount via Chainlink oracle and transferring SOL to the treasury.
pub fn charge_fee<'info>(
fee_usd: u64,
chainlink_program: AccountInfo<'info>,
chainlink_sol_usd_feed: AccountInfo<'info>,
tx_signer: AccountInfo<'info>,
treasury: AccountInfo<'info>,
) -> Result<u64> {
let fee_in_lamports: u64 = convert_usd_fee_to_lamports(fee_usd, chainlink_program, chainlink_sol_usd_feed);

if fee_in_lamports > 0 {
let fee_charging_ix = transfer(&tx_signer.key(), &treasury.key(), fee_in_lamports);
invoke(&fee_charging_ix, &[tx_signer, treasury])?;
}

Ok(fee_in_lamports)
}

/// Helper function to calculate the collectable amount from an account. As a precaution, we add a buffer to the rent
/// exemption, ensuring that the account balance will not fall below the rent-exempt minimum. This could otherwise
/// make the program unusable.
pub fn safe_collectible_amount(account: &AccountInfo) -> Result<u64> {
// Retrieve the current balance of the account.
let current_balance = account.lamports();

// Determine the size of the account's data.
let data_len = account.data_len();

// Retrieve the rent sysvar.
let rent = Rent::get()?;

// Calculate the minimum balance needed for rent exemption.
let rent_exempt_minimum = rent.minimum_balance(data_len);

const SAFE_RENT_BUFFER_LAMPORTS: u64 = 1_000_000; // 0.001 SOL
let safe_minimum = rent_exempt_minimum.checked_add(SAFE_RENT_BUFFER_LAMPORTS).unwrap();

// Return the collectable amount
Ok(current_balance.saturating_sub(safe_minimum))
}
11 changes: 11 additions & 0 deletions crates/sablier-common/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
pub mod constants;
pub mod fee_calculation;
pub mod fee_collection;
pub mod time;
pub mod token_transfer;

pub use constants::*;
pub use fee_calculation::*;
pub use fee_collection::*;
pub use time::*;
pub use token_transfer::*;
File renamed without changes.
78 changes: 56 additions & 22 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import "./node_modules/@sablier/devkit/just/base.just"
anchor := require("anchor")
# Solana: https://solana.com/docs/intro/installation#install-the-solana-cli
solana := require("solana")
# Trident: https://ackee.xyz/trident/docs/latest/basics/installation/
trident := require("trident")

# ---------------------------------------------------------------------------- #
# ENVIRONMENT #
Expand Down Expand Up @@ -84,12 +86,14 @@ full-check:
just _run-with-status biome-check
just _run-with-status tsc-check
just _run-with-status rust-check
just _run-with-status rust-check-trident

# Run all code fixes
full-write:
just _run-with-status prettier-write
just _run-with-status biome-write
just _run-with-status rust-write
just _run-with-status rust-write-trident

# Run Rust checks
# TODO: Remove `--allow deprecated` once mpl-core crate deprecation warnings are resolved upstream.
Expand All @@ -105,51 +109,81 @@ rust-write:
cargo clippy --fix --allow-dirty -- --allow deprecated
alias rw := rust-write

# Run Rust checks on Trident fuzz tests
rust-check-trident:
cargo fmt --check --manifest-path trident-tests/Cargo.toml
cargo clippy --manifest-path trident-tests/Cargo.toml
alias rct := rust-check-trident

# Format Trident fuzz test code
rust-write-trident:
cargo fmt --manifest-path trident-tests/Cargo.toml
cargo clippy --fix --allow-dirty --manifest-path trident-tests/Cargo.toml
alias rwt := rust-write-trident

# ---------------------------------------------------------------------------- #
# TESTING #
# ---------------------------------------------------------------------------- #

# Run all tests
# To debug the Solana logs, run this as `RUST_LOG=debug just test`
# Run all tests (Anchor + Trident)
[group("test")]
test *args: build _setup-fixtures
na vitest run --hideSkippedTests {{ args }}
test: test-anchor test-trident
alias t := test

# Run all tests without building
# ---------------------------------------------------------------------------- #
# ANCHOR TESTS #
# ---------------------------------------------------------------------------- #

# Run all Anchor tests (lockup + merkle instant)
# To debug the Solana logs, run this as `RUST_LOG=debug just test-anchor`
[group("test")]
test-lite *args: _setup-fixtures
test-anchor *args: build _setup-fixtures
na vitest run --hideSkippedTests {{ args }}
alias tl := test-lite
alias ta := test-anchor

# Run tests with UI
# Run Anchor tests with UI
[group("test")]
test-ui *args: build _setup-fixtures
test-anchor-ui *args: build _setup-fixtures
na vitest --hideSkippedTests --ui {{ args }}
alias tui := test-ui
alias taui := test-anchor-ui

# Run Anchor Lockup tests only
[group("test")]
test-anchor-lockup *args="tests/lockup": build _setup-fixtures
na vitest run --hideSkippedTests {{ args }}
alias talk := test-anchor-lockup

# Run Anchor Merkle Instant tests only
[group("test")]
test-anchor-merkle-instant *args="tests/merkle-instant": build _setup-fixtures
na vitest run --hideSkippedTests {{ args }}
alias tami := test-anchor-merkle-instant

# ---------------------------------------------------------------------------- #
# TRIDENT TESTS #
# ---------------------------------------------------------------------------- #

# Run Lockup tests only
# Run all Trident fuzz tests
[group("test")]
test-lockup *args="tests/lockup":
just test {{ args }}
alias tlk := test-lockup
test-trident: test-trident-lockup
alias tt := test-trident

# Run Merkle Instant tests only
# Run Trident Lockup fuzz tests
[group("test")]
test-merkle-instant *args="tests/merkle-instant":
just test {{ args }}
alias tmi := test-merkle-instant
test-trident-lockup: build _setup-fixtures
just --justfile trident-tests/justfile test-lockup
alias ttlk := test-trident-lockup

# Download external program fixtures for testing
_setup-fixtures:
#!/usr/bin/env sh
FIXTURES_DIR="tests/fixtures"
mkdir -p "$FIXTURES_DIR"

# Token Metadata Program
if [ ! -f "$FIXTURES_DIR/token_metadata_program.so" ]; then
echo "📥 Downloading Token Metadata program..."
solana program dump -u m metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s "$FIXTURES_DIR/token_metadata_program.so"
# MPL Core Program
if [ ! -f "$FIXTURES_DIR/mpl_core_program.so" ]; then
echo "📥 Downloading MPL Core program..."
solana program dump -u m CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d "$FIXTURES_DIR/mpl_core_program.so"
fi

# Chainlink Program
Expand Down
14 changes: 14 additions & 0 deletions lib/chainlink_sol_usd_feed_mock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"pubkey": "99B2bTijsU6f1GCT73HmdR7HCFFjGMBcPZY6jZ96ynrR",
"account": {
"lamports": 2616962,
"data": [
"YLNFQoCBSXUCAWQUUYa2ANnUYYrbqZcNlEBTsnm1vMbAD/Shxwnxadi/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAD9IzGmE6swmiVX6E+mpeINgNJ4h4AyDGib6NC7wlNPTCAvIFVTRAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAr+bMBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD7PsAXAAAAAOkWjmgAAAAAoTdp0wMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"base64"
],
"owner": "HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny",
"executable": false,
"rentEpoch": 18446744073709551615,
"space": 248
}
}
2 changes: 1 addition & 1 deletion programs/lockup/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@
[dependencies]
anchor-lang = { version = "0.31.1", features = ["init-if-needed"] }
anchor-spl = "0.31.1"
chainlink_solana = { git = "https://github.com/smartcontractkit/chainlink-solana", branch = "solana-2.1" }
mpl-core = { version = "=0.11.1-beta.2", features = ["anchor"] }
sablier-common = { path = "../../crates/sablier-common" }
Loading
Loading