Skip to content

Commit 979d423

Browse files
authored
Merge pull request #195 from nanaf6203-bit/feat/resolve-issues-159-160-161-162
feat: add timeout/dispute errors, upgrade errors, error propagation, and oracle module
2 parents 3cffa19 + 3d1b0f5 commit 979d423

3 files changed

Lines changed: 198 additions & 1 deletion

File tree

crates/contracts/core/src/error_codes.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,29 @@ pub enum FinancialError {
6767
/// Arithmetic overflow detected.
6868
Overflow = 404,
6969
}
70+
71+
/// Errors for timeout and dispute operations (issue #159).
72+
#[contracterror]
73+
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
74+
#[repr(u32)]
75+
pub enum TimeoutDisputeError {
76+
/// Cannot auto-refund yet — dispute window has not elapsed.
77+
DisputeWindowNotElapsed = 500,
78+
/// Dispute already exists for this session.
79+
DisputeAlreadyOpen = 501,
80+
/// No open dispute to resolve on this session.
81+
DisputeNotOpen = 502,
82+
/// Session is not eligible for resolution.
83+
ResolutionNotAllowed = 503,
84+
}
85+
86+
/// Errors for contract upgrade failures (issue #160).
87+
#[contracterror]
88+
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
89+
#[repr(u32)]
90+
pub enum UpgradeError {
91+
/// Provided WASM hash is zero or invalid.
92+
InvalidWasmHash = 600,
93+
/// Low-level upgrade failure.
94+
UpgradeFailed = 601,
95+
}

crates/contracts/core/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
pub mod error_codes;
44

5-
pub use error_codes::{AuthError, FinancialError, InitError, SessionError};
5+
pub use error_codes::{AuthError, FinancialError, InitError, SessionError, TimeoutDisputeError, UpgradeError};
66
pub mod errors;
77
pub mod events;
8+
pub mod oracle;
89

910
pub use errors::ContractError;
1011
pub use events::{ContractUpgraded, DisputeResolved, TreasuryUpdated};
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
use soroban_sdk::{contracttype, Address, Bytes, Env};
2+
3+
use crate::errors::ContractError;
4+
5+
/// Price data returned by the oracle.
6+
#[contracttype]
7+
#[derive(Clone, Debug)]
8+
pub struct PriceData {
9+
/// Price in the smallest unit (e.g. stroops).
10+
pub price: i128,
11+
/// Ledger timestamp when this price was recorded.
12+
pub timestamp: u64,
13+
}
14+
15+
/// Storage keys for oracle state.
16+
#[contracttype]
17+
#[derive(Clone)]
18+
pub enum OracleKey {
19+
/// The oracle contract address set by admin.
20+
OracleAddress,
21+
/// Admin-provided fallback price for a given asset.
22+
FallbackPrice(Bytes),
23+
/// Freshness threshold in seconds (max age of a valid price).
24+
FreshnessThreshold,
25+
}
26+
27+
/// Default freshness threshold: 5 minutes.
28+
pub const DEFAULT_FRESHNESS_THRESHOLD: u64 = 300;
29+
30+
/// Set the oracle contract address. Admin-only.
31+
pub fn set_oracle(env: &Env, admin: &Address, oracle_id: Address) {
32+
admin.require_auth();
33+
env.storage()
34+
.instance()
35+
.set(&OracleKey::OracleAddress, &oracle_id);
36+
}
37+
38+
/// Get the configured oracle address, if any.
39+
pub fn get_oracle(env: &Env) -> Option<Address> {
40+
env.storage()
41+
.instance()
42+
.get(&OracleKey::OracleAddress)
43+
}
44+
45+
/// Set the freshness threshold (seconds). Admin-only.
46+
pub fn set_freshness_threshold(env: &Env, admin: &Address, threshold_secs: u64) {
47+
admin.require_auth();
48+
env.storage()
49+
.instance()
50+
.set(&OracleKey::FreshnessThreshold, &threshold_secs);
51+
}
52+
53+
/// Get the freshness threshold, falling back to the default.
54+
pub fn get_freshness_threshold(env: &Env) -> u64 {
55+
env.storage()
56+
.instance()
57+
.get(&OracleKey::FreshnessThreshold)
58+
.unwrap_or(DEFAULT_FRESHNESS_THRESHOLD)
59+
}
60+
61+
/// Set an admin-provided fallback price for an asset.
62+
pub fn set_fallback_price(env: &Env, admin: &Address, asset: Bytes, price: i128) {
63+
admin.require_auth();
64+
let data = PriceData {
65+
price,
66+
timestamp: env.ledger().timestamp(),
67+
};
68+
env.storage()
69+
.persistent()
70+
.set(&OracleKey::FallbackPrice(asset), &data);
71+
}
72+
73+
/// Retrieve the latest price for `asset`.
74+
///
75+
/// Strategy:
76+
/// 1. Try the on-chain oracle (if configured) and validate freshness.
77+
/// 2. Fall back to the admin-provided price if the oracle is unavailable or stale.
78+
/// 3. Return `ContractError::InternalError` if neither source is available.
79+
pub fn get_price(env: &Env, asset: Bytes) -> Result<i128, ContractError> {
80+
let now = env.ledger().timestamp();
81+
let threshold = get_freshness_threshold(env);
82+
83+
// Attempt oracle lookup first.
84+
if let Some(_oracle_addr) = get_oracle(env) {
85+
// In a real integration the oracle contract would be called here via
86+
// a cross-contract call. For now we fall through to the fallback so
87+
// the module compiles and the interface is stable.
88+
}
89+
90+
// Fallback: admin-provided price.
91+
if let Some(data) = env
92+
.storage()
93+
.persistent()
94+
.get::<_, PriceData>(&OracleKey::FallbackPrice(asset))
95+
{
96+
validate_price_freshness(now, data.timestamp, threshold)?;
97+
return Ok(data.price);
98+
}
99+
100+
Err(ContractError::InternalError)
101+
}
102+
103+
/// Validate that a price timestamp is within the freshness threshold.
104+
pub fn validate_price_freshness(
105+
now: u64,
106+
price_timestamp: u64,
107+
threshold_secs: u64,
108+
) -> Result<(), ContractError> {
109+
if now.saturating_sub(price_timestamp) > threshold_secs {
110+
return Err(ContractError::InternalError); // stale price
111+
}
112+
Ok(())
113+
}
114+
115+
#[cfg(test)]
116+
mod tests {
117+
use super::*;
118+
use soroban_sdk::{testutils::Ledger, Env};
119+
120+
#[test]
121+
fn test_freshness_valid() {
122+
let env = Env::default();
123+
env.ledger().set_timestamp(1000);
124+
assert!(validate_price_freshness(1000, 800, 300).is_ok());
125+
}
126+
127+
#[test]
128+
fn test_freshness_stale() {
129+
let env = Env::default();
130+
env.ledger().set_timestamp(1000);
131+
assert!(validate_price_freshness(1000, 100, 300).is_err());
132+
}
133+
134+
#[test]
135+
fn test_set_and_get_oracle() {
136+
let env = Env::default();
137+
env.mock_all_auths();
138+
let admin = Address::generate(&env);
139+
let oracle = Address::generate(&env);
140+
set_oracle(&env, &admin, oracle.clone());
141+
assert_eq!(get_oracle(&env), Some(oracle));
142+
}
143+
144+
#[test]
145+
fn test_fallback_price() {
146+
let env = Env::default();
147+
env.mock_all_auths();
148+
env.ledger().set_timestamp(1000);
149+
let admin = Address::generate(&env);
150+
let asset = Bytes::from_slice(&env, b"USDC");
151+
set_fallback_price(&env, &admin, asset.clone(), 5_000_000);
152+
let price = get_price(&env, asset).unwrap();
153+
assert_eq!(price, 5_000_000);
154+
}
155+
156+
#[test]
157+
fn test_fallback_price_stale() {
158+
let env = Env::default();
159+
env.mock_all_auths();
160+
env.ledger().set_timestamp(1000);
161+
let admin = Address::generate(&env);
162+
let asset = Bytes::from_slice(&env, b"USDC");
163+
set_fallback_price(&env, &admin, asset.clone(), 5_000_000);
164+
// Advance time past threshold
165+
env.ledger().set_timestamp(2000);
166+
set_freshness_threshold(&env, &admin, 300);
167+
let result = get_price(&env, asset);
168+
assert!(result.is_err());
169+
}
170+
}

0 commit comments

Comments
 (0)