Skip to content

Commit 0033534

Browse files
committed
feat: reject timestamp=0 in submit_score and submit_scores_batch
- Add InvalidTimestamp = 25 to errors.rs - Guard submit_score: return InvalidTimestamp when timestamp == 0 - Guard submit_scores_batch: skip entries with timestamp == 0 - Update submit_score rustdoc to document non-zero timestamp requirement - Add test_submit_score_zero_timestamp_rejected - Add test_batch_skips_zero_timestamp_entries - Add test_submit_score_nonzero_timestamp_accepted - Fix test_set_service_rotates_authorised_account (timestamp 0 → 1) - Add Error Codes table to README.md
1 parent 3e055bd commit 0033534

4 files changed

Lines changed: 107 additions & 2 deletions

File tree

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,36 @@ A wallet scoring 60-70 on three pairs individually might not breach the per-pair
174174

175175
`get_aggregate_score` iterates the wallet's full pair list, so its cost is O(N) in the number of distinct pairs the wallet has scores for. The contract is designed around a practical maximum of `MAX_WALLET_PAIRS` (20) pairs per wallet; this is documented as a constant but not enforced on-chain.
176176

177+
## Error Codes
178+
179+
| Code | Name | When returned |
180+
|------|------|---------------|
181+
| 1 | `AlreadyInitialized` | `initialize` called more than once |
182+
| 2 | `NotInitialized` | Any state-mutating call before `initialize` |
183+
| 3 | `Unauthorized` | Caller is not the authorised service or admin |
184+
| 4 | `InvalidScore` | `score` outside 0-100 |
185+
| 5 | `InvalidConfidence` | `confidence` outside 0-100 |
186+
| 6 | `ScoreNotFound` | `get_score` / `get_aggregate_score` for an unknown pair |
187+
| 7 | `ContractPaused` | Submission attempted while admin circuit-breaker is active |
188+
| 8 | `NoPendingAdminTransfer` | `accept_admin` / `cancel_admin_transfer` with no transfer in flight |
189+
| 9 | `EmptyBatch` | `submit_scores_batch` called with zero entries |
190+
| 10 | `BatchTooLarge` | Batch exceeds `MAX_BATCH_SIZE` (20) |
191+
| 11 | `ArithmeticOverflow` | Weighted aggregate computation overflows |
192+
| 12 | `UpgradeAlreadyPending` | `propose_upgrade` while a proposal is already pending |
193+
| 13 | `NoPendingUpgrade` | `execute_upgrade` / `veto_upgrade` / `get_pending_upgrade` with no proposal |
194+
| 14 | `InsufficientSigners` | Fewer than threshold signers supplied to `submit_score` |
195+
| 15 | `UnauthorizedSigner` | A supplied signer is not in the service set |
196+
| 16 | `InvalidThreshold` | `set_service_threshold` given `0` or a value > set size |
197+
| 17 | `ServiceSetFull` | `add_service_signer` when set already has `MAX_SERVICE_SIGNERS` members |
198+
| 18 | `SignerAlreadyInSet` | `add_service_signer` with an address already present |
199+
| 19 | `SignerNotInSet` | `remove_service_signer` with an address not in the set |
200+
| 20 | `UpgradeNotReady` | `execute_upgrade` before the time-lock has elapsed |
201+
| 21 | `InvalidUpgradeDelay` | `set_upgrade_delay` value outside `[MIN, MAX]` bounds |
202+
| 22 | `InvalidStalenessWindow` | `set_staleness_window` called with `0` |
203+
| 23 | `RateLimitExceeded` | Submission before the per-pair cooldown has elapsed |
204+
| 24 | `InvalidCooldown` | `set_cooldown` value outside `[MIN_COOLDOWN_SECS, MAX_COOLDOWN_SECS]` |
205+
| 25 | `InvalidTimestamp` | `submit_score` called with `timestamp = 0` |
206+
177207
## Upgrade Governance
178208

179209
Soroban contracts can be upgraded by the admin via `update_current_contract_wasm`, which replaces the **entire** contract logic in a single transaction. Without governance, one admin key — or a compromised one — could silently install a backdoor or disable a security check with no warning. LedgerLens gates every upgrade behind an on-chain **time-lock** so the community always gets a mandatory window to inspect and react.

contracts/ledgerlens-score/src/errors.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,6 @@ pub enum Error {
5959
/// Returned when `set_cooldown` is given a value below
6060
/// `MIN_COOLDOWN_SECS` or above `MAX_COOLDOWN_SECS`.
6161
InvalidCooldown = 24,
62+
/// Returned when the submitted timestamp is zero.
63+
InvalidTimestamp = 25,
6264
}

contracts/ledgerlens-score/src/lib.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ impl LedgerLensScoreContract {
108108
/// elapsed since the last accepted one, returning `RateLimitExceeded`.
109109
/// See the README's Rate Limiting section.
110110
///
111+
/// `timestamp` must be non-zero; `0` is rejected with `InvalidTimestamp`.
112+
///
111113
/// # Examples
112114
///
113115
/// ```
@@ -176,6 +178,9 @@ impl LedgerLensScoreContract {
176178
if confidence > 100 {
177179
return Err(Error::InvalidConfidence);
178180
}
181+
if timestamp == 0 {
182+
return Err(Error::InvalidTimestamp);
183+
}
179184

180185
let last_submit = storage::get_last_submit_time(&env, &wallet, &asset_pair);
181186
let cooldown = storage::get_cooldown_secs(&env);
@@ -263,7 +268,7 @@ impl LedgerLensScoreContract {
263268
for i in 0..submissions.len() {
264269
let sub = submissions.get(i).unwrap();
265270

266-
if sub.score > 100 || sub.confidence > 100 {
271+
if sub.score > 100 || sub.confidence > 100 || sub.timestamp == 0 {
267272
continue;
268273
}
269274

contracts/ledgerlens-score/src/test.rs

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ fn test_set_service_rotates_authorised_account() {
181181

182182
let wallet = Address::generate(&env);
183183
let asset_pair = symbol_short!("XLM_USDC");
184-
client.submit_score(&Vec::new(&env), &wallet, &asset_pair, &10, &false, &false, &0, &10, &1);
184+
client.submit_score(&Vec::new(&env), &wallet, &asset_pair, &10, &false, &false, &1, &10, &1);
185185
}
186186

187187
// ── Pause circuit breaker ─────────────────────────────────────────────────────
@@ -1215,3 +1215,71 @@ fn test_set_staleness_window_updates_stale_check() {
12151215
env.ledger().with_mut(|l| l.timestamp = ts + 11);
12161216
assert!(client.is_score_stale(&wallet, &pair));
12171217
}
1218+
1219+
// ── Timestamp validation ──────────────────────────────────────────────────────
1220+
1221+
#[test]
1222+
fn test_submit_score_zero_timestamp_rejected() {
1223+
let (env, client, _admin, _service) = initialized();
1224+
let wallet = Address::generate(&env);
1225+
let asset_pair = symbol_short!("XLM_USDC");
1226+
let result = client.try_submit_score(
1227+
&Vec::new(&env),
1228+
&wallet,
1229+
&asset_pair,
1230+
&50,
1231+
&false,
1232+
&false,
1233+
&0,
1234+
&80,
1235+
&1,
1236+
);
1237+
assert_eq!(result, Err(Ok(Error::InvalidTimestamp)));
1238+
}
1239+
1240+
#[test]
1241+
fn test_batch_skips_zero_timestamp_entries() {
1242+
let (env, client, _admin, _service) = initialized();
1243+
let wallet_zero = Address::generate(&env);
1244+
let wallet_ok = Address::generate(&env);
1245+
let asset_pair = symbol_short!("XLM_USDC");
1246+
1247+
let mut batch: Vec<ScoreSubmission> = Vec::new(&env);
1248+
batch.push_back(ScoreSubmission {
1249+
wallet: wallet_zero.clone(),
1250+
asset_pair: asset_pair.clone(),
1251+
score: 50,
1252+
benford_flag: false,
1253+
ml_flag: false,
1254+
timestamp: 0,
1255+
confidence: 80,
1256+
model_version: 1,
1257+
});
1258+
batch.push_back(ScoreSubmission {
1259+
wallet: wallet_ok.clone(),
1260+
asset_pair: asset_pair.clone(),
1261+
score: 60,
1262+
benford_flag: false,
1263+
ml_flag: false,
1264+
timestamp: 1,
1265+
confidence: 75,
1266+
model_version: 1,
1267+
});
1268+
1269+
let accepted = client.submit_scores_batch(&batch);
1270+
assert_eq!(accepted, 1);
1271+
assert_eq!(client.get_score(&wallet_ok, &asset_pair).score, 60);
1272+
assert_eq!(
1273+
client.try_get_score(&wallet_zero, &asset_pair),
1274+
Err(Ok(Error::ScoreNotFound))
1275+
);
1276+
}
1277+
1278+
#[test]
1279+
fn test_submit_score_nonzero_timestamp_accepted() {
1280+
let (env, client, _admin, _service) = initialized();
1281+
let wallet = Address::generate(&env);
1282+
let asset_pair = symbol_short!("XLM_USDC");
1283+
client.submit_score(&Vec::new(&env), &wallet, &asset_pair, &50, &false, &false, &1, &80, &1);
1284+
assert_eq!(client.get_score(&wallet, &asset_pair).score, 50);
1285+
}

0 commit comments

Comments
 (0)