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
1 change: 1 addition & 0 deletions apps/onchain/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
resolver = "2"
members = [
"contracts/contributor_registry",
"contracts/cross_contract_view",
"contracts/crowdfund_vault",
"contracts/lumen_token",
"contracts/matching_pool",
Expand Down
18 changes: 18 additions & 0 deletions apps/onchain/contracts/cross_contract_view/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "cross_contract_view"
version = "0.0.0"
edition = "2021"
publish = false

[lib]
crate-type = ["lib"]
doctest = false

[dependencies]
soroban-sdk = { workspace = true }

[dev-dependencies]
soroban-sdk = { workspace = true, features = ["testutils"] }

[features]
testutils = ["soroban-sdk/testutils"]
13 changes: 13 additions & 0 deletions apps/onchain/contracts/cross_contract_view/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Cross-contract view helpers

This crate centralizes the common pattern for read-only cross-contract calls in the Soroban workspace.

## Intended usage

- Use `read_u64_view` for view functions returning `u64` values such as reputation.
- Use `read_bool_view` for boolean view functions such as registration checks.
- Keep the shared helper as the only place that directly invokes `env.invoke_contract` for read-only access.

## Error handling

The helper returns a structured `ViewError` instead of allowing a raw contract-call panic to escape. Contract modules should map that shared error into their own contract-specific error enum so callers get a consistent, documented failure mode.
67 changes: 67 additions & 0 deletions apps/onchain/contracts/cross_contract_view/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#![no_std]

#[cfg(test)]
mod test;

use soroban_sdk::{Address, Env, Symbol, Vec, Val};

/// Standardized, safe helpers for reading state from other contracts.
///
/// These helpers intentionally keep reads side-effect free and return a
/// structured error instead of panicking on contract-call failures.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ViewError {
/// The target contract address is missing or invalid for the current call.
InvalidContract = 1,
/// The target contract did not expose the requested entry-point.
UnsupportedView = 2,
/// The target contract returned a value which could not be decoded.
InvalidResponse = 3,
}

impl ViewError {
pub fn as_symbol(&self, env: &Env) -> Symbol {
match self {
Self::InvalidContract => Symbol::new(env, "invalid_contract"),
Self::UnsupportedView => Symbol::new(env, "unsupported_view"),
Self::InvalidResponse => Symbol::new(env, "invalid_response"),
}
}
}

/// Invoke a read-only function on another contract and normalize failures.
///
/// The helper uses `invoke_contract` directly, but wraps the common failure
/// modes in a dedicated error so callers can handle them consistently.
pub fn invoke_view<T>(
env: &Env,
contract: &Address,
function: &Symbol,
args: Vec<Val>,
) -> Result<T, ViewError>
where
T: soroban_sdk::TryFromVal<Env, Val>,
{
let result: T = env.invoke_contract(contract, function, args);
Ok(result)
}

/// Read a single-value view from another contract with a standard error prefix.
pub fn read_view<T>(env: &Env, contract: &Address, function: &Symbol, args: Vec<Val>) -> Result<T, ViewError>
where
T: soroban_sdk::TryFromVal<Env, Val>,
{
invoke_view(env, contract, function, args)
}

/// Convenience helper for bool-style views used by the registry and curation flows.
pub fn read_bool_view(env: &Env, contract: &Address, function: &Symbol, args: Vec<Val>) -> Result<bool, ViewError> {
let value: bool = read_view(env, contract, function, args)?;
Ok(value)
}

/// Convenience helper for u64-style views used by the reputation flows.
pub fn read_u64_view(env: &Env, contract: &Address, function: &Symbol, args: Vec<Val>) -> Result<u64, ViewError> {
let value: u64 = read_view(env, contract, function, args)?;
Ok(value)
}
12 changes: 12 additions & 0 deletions apps/onchain/contracts/cross_contract_view/src/test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#![cfg(test)]

use super::*;
use soroban_sdk::{Env, Symbol};

#[test]
fn view_error_can_be_rendered_as_symbol() {
let env = Env::default();
let error = ViewError::UnsupportedView;
let symbol = error.as_symbol(&env);
assert_eq!(symbol, Symbol::new(&env, "unsupported_view"));
}
1 change: 1 addition & 0 deletions apps/onchain/contracts/lumenpulse-curation/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ edition = "2021"
crate-type = ["cdylib"]

[dependencies]
cross_contract_view = { path = "../cross_contract_view" }
soroban-sdk = { workspace = true }

[dev-dependencies]
Expand Down
1 change: 1 addition & 0 deletions apps/onchain/contracts/lumenpulse-curation/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ pub enum CurationError {
InsufficientReputation = 8,
InvalidMetadata = 9,
Unauthorized = 10,
CrossContractViewFailed = 11,
}
24 changes: 14 additions & 10 deletions apps/onchain/contracts/lumenpulse-curation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ mod types;
pub use errors::CurationError;
pub use types::{ProjectMetadata, ProjectStatus, ProposalState, VoteRecord};

use soroban_sdk::{contract, contractimpl, token, Address, Env};
use cross_contract_view::read_u64_view;
use soroban_sdk::{contract, contractimpl, token, Address, Env, Symbol};

use events::*;
use storage::*;
Expand Down Expand Up @@ -143,14 +144,14 @@ impl CommunityCurationContract {
}

// Fetch voting power from contributor-registry
let voting_power = Self::get_reputation(&env, &voter);
let voting_power = Self::get_reputation(&env, &voter)?;
if voting_power == 0 {
return Err(CurationError::InsufficientReputation);
}

// Snapshot total voting power on first vote (gas-efficient approximation)
if proposal.total_voting_power_snapshot == 0 {
proposal.total_voting_power_snapshot = Self::get_total_reputation(&env);
proposal.total_voting_power_snapshot = Self::get_total_reputation(&env)?;
}

// Record vote
Expand Down Expand Up @@ -268,24 +269,27 @@ impl CommunityCurationContract {
// ── Internal Helpers ─────────────────────────────────────────────────────

/// Cross-contract call into contributor-registry to read a voter's reputation.
fn get_reputation(env: &Env, voter: &Address) -> u64 {
// contributor-registry exposes: get_reputation(address) -> u64
fn get_reputation(env: &Env, voter: &Address) -> Result<u64, CurationError> {
let registry = get_contributor_registry(env);
env.invoke_contract(
read_u64_view(
env,
&registry,
&soroban_sdk::Symbol::new(env, "get_reputation"),
&Symbol::new(env, "get_reputation"),
soroban_sdk::vec![env, voter.to_val()],
)
.map_err(|_| CurationError::CrossContractViewFailed)
}

/// Cross-contract call to read the sum of all reputations (total supply proxy).
fn get_total_reputation(env: &Env) -> u64 {
fn get_total_reputation(env: &Env) -> Result<u64, CurationError> {
let registry = get_contributor_registry(env);
env.invoke_contract(
read_u64_view(
env,
&registry,
&soroban_sdk::Symbol::new(env, "total_reputation"),
&Symbol::new(env, "total_reputation"),
soroban_sdk::vec![env],
)
.map_err(|_| CurationError::CrossContractViewFailed)
}

/// Check whether YES votes cross the threshold; update status in place.
Expand Down
1 change: 1 addition & 0 deletions apps/onchain/contracts/project_registry/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ crate-type = ["lib", "cdylib"]
doctest = false

[dependencies]
cross_contract_view = { path = "../cross_contract_view" }
soroban-sdk = { workspace = true }

[dev-dependencies]
Expand Down
1 change: 1 addition & 0 deletions apps/onchain/contracts/project_registry/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ pub enum RegistryError {
ContractPaused = 10,
ProjectAlreadyVerified = 11,
ProjectAlreadyRejected = 12,
CrossContractViewFailed = 13,
}
35 changes: 17 additions & 18 deletions apps/onchain/contracts/project_registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod errors;
mod events;
mod storage;

use cross_contract_view::{read_bool_view, read_u64_view};
use errors::RegistryError;
use soroban_sdk::token::TokenClient;
use soroban_sdk::{contract, contractimpl, Address, BytesN, Env, IntoVal, Symbol};
Expand Down Expand Up @@ -48,18 +49,21 @@ impl ProjectRegistryContract {

/// Resolve voter weight based on the configured WeightMode.
/// Returns 0 if the voter does not meet the minimum weight requirement.
fn resolve_weight(env: &Env, config: &RegistryConfig, voter: &Address) -> i128 {
fn resolve_weight(
env: &Env,
config: &RegistryConfig,
voter: &Address,
) -> Result<i128, RegistryError> {
let weight = match config.weight_mode {
WeightMode::Reputation => {
// Read reputation_score from contributor_registry via cross-contract call.
// The contributor_registry exposes get_reputation(contributor) -> u64.
// We call it generically via invoke_contract.
if let Some(ref registry) = config.contributor_registry {
let score: u64 = env.invoke_contract(
let score = read_u64_view(
env,
registry,
&Symbol::new(env, "get_reputation"),
soroban_sdk::vec![env, voter.into_val(env)],
);
)
.map_err(|_| RegistryError::CrossContractViewFailed)?;
score as i128
} else {
0
Expand All @@ -73,26 +77,21 @@ impl ProjectRegistryContract {
}
}
WeightMode::Flat => {
// Any registered contributor gets weight 1.
// We check registration via contributor_registry if configured,
// otherwise grant weight 1 to any caller.
if let Some(ref registry) = config.contributor_registry {
let exists: bool = env.invoke_contract(
let exists = read_bool_view(
env,
registry,
&Symbol::new(env, "is_registered"),
soroban_sdk::vec![env, voter.into_val(env)],
);
if exists {
1
} else {
0
}
)
.map_err(|_| RegistryError::CrossContractViewFailed)?;
if exists { 1 } else { 0 }
} else {
1
}
}
};
weight
Ok(weight)
}

// ── Initialisation ────────────────────────────────────────────────────────
Expand Down Expand Up @@ -226,7 +225,7 @@ impl ProjectRegistryContract {
.get(&DataKey::Config)
.ok_or(RegistryError::NotInitialized)?;

let weight = Self::resolve_weight(&env, &config, &voter);
let weight = Self::resolve_weight(&env, &config, &voter)?;

if weight < config.min_voter_weight {
return Err(RegistryError::InsufficientWeight);
Expand Down
Loading