• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stacks-network / stacks-core / 26250451051-1

21 May 2026 08:11PM UTC coverage: 85.585% (-0.1%) from 85.712%
26250451051-1

Pull #7215

github

ec9d4c
web-flow
Merge 9487bf852 into af1280aac
Pull Request #7215: Chore: fix flake in non_blocking_minority_configured_to_favour_...

188844 of 220651 relevant lines covered (85.58%)

18975267.44 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.13
/pox-locking/src/pox_4.rs
1
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
2
// Copyright (C) 2020-2026 Stacks Open Internet Foundation
3
//
4
// This program is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// This program is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

17
use clarity::boot_util::boot_code_id;
18
use clarity::vm::contexts::{ExecutionState, GlobalContext};
19
use clarity::vm::costs::cost_functions::ClarityCostFunction;
20
use clarity::vm::costs::runtime_cost;
21
use clarity::vm::database::{ClarityDatabase, STXBalance};
22
use clarity::vm::errors::{RuntimeError, VmExecutionError, VmInternalError};
23
use clarity::vm::events::{STXEventType, STXLockEventData, StacksTransactionEvent};
24
use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier};
25
use clarity::vm::Value;
26
use stacks_common::{debug, error};
27

28
use crate::events::synthesize_pox_event_info;
29
// Note: PoX-4 uses the same contract-call result parsing routines as PoX-2
30
use crate::pox_2::{parse_pox_extend_result, parse_pox_increase, parse_pox_stacking_result};
31
use crate::{LockingError, POX_4_NAME};
32

33
/////////////////////// PoX-4 /////////////////////////////////
34

35
/// Lock up STX for PoX for a time.  Does NOT touch the account nonce.
36
pub fn pox_lock_v4(
90,061✔
37
    db: &mut ClarityDatabase,
90,061✔
38
    principal: &PrincipalData,
90,061✔
39
    lock_amount: u128,
90,061✔
40
    unlock_burn_height: u64,
90,061✔
41
) -> Result<(), LockingError> {
90,061✔
42
    assert!(unlock_burn_height > 0);
90,061✔
43
    assert!(lock_amount > 0);
90,061✔
44

45
    let mut snapshot = db.get_stx_balance_snapshot(principal)?;
90,061✔
46

47
    if snapshot.has_locked_tokens()? {
90,061✔
48
        return Err(LockingError::PoxAlreadyLocked);
1✔
49
    }
90,060✔
50
    if !snapshot.can_transfer(lock_amount)? {
90,060✔
51
        return Err(LockingError::PoxInsufficientBalance);
×
52
    }
90,060✔
53
    snapshot.lock_tokens_v4(lock_amount, unlock_burn_height)?;
90,060✔
54

55
    debug!(
90,060✔
56
        "PoX v4 lock applied";
57
        "pox_locked_ustx" => snapshot.balance().amount_locked(),
×
58
        "available_ustx" => snapshot.balance().amount_unlocked(),
×
59
        "unlock_burn_height" => unlock_burn_height,
×
60
        "account" => %principal,
61
    );
62

63
    snapshot.save()?;
90,060✔
64
    Ok(())
90,060✔
65
}
90,061✔
66

67
/// Extend a STX lock up for PoX for a time.  Does NOT touch the account nonce.
68
/// Returns Ok(lock_amount) when successful
69
///
70
/// # Errors
71
/// - Returns Error::PoxExtendNotLocked if this function was called on an account
72
///   which isn't locked. This *should* have been checked by the PoX v4 contract,
73
///   so this should surface in a panic.
74
pub fn pox_lock_extend_v4(
340✔
75
    db: &mut ClarityDatabase,
340✔
76
    principal: &PrincipalData,
340✔
77
    unlock_burn_height: u64,
340✔
78
) -> Result<u128, LockingError> {
340✔
79
    assert!(unlock_burn_height > 0);
340✔
80

81
    let mut snapshot = db.get_stx_balance_snapshot(principal)?;
340✔
82

83
    if !snapshot.has_locked_tokens()? {
340✔
84
        return Err(LockingError::PoxExtendNotLocked);
×
85
    }
340✔
86

87
    snapshot.extend_lock_v4(unlock_burn_height)?;
340✔
88

89
    let amount_locked = snapshot.balance().amount_locked();
340✔
90

91
    debug!(
340✔
92
        "PoX v4 lock applied";
93
        "pox_locked_ustx" => amount_locked,
×
94
        "available_ustx" => snapshot.balance().amount_unlocked(),
×
95
        "unlock_burn_height" => unlock_burn_height,
×
96
        "account" => %principal,
97
    );
98

99
    snapshot.save()?;
340✔
100
    Ok(amount_locked)
340✔
101
}
340✔
102

103
/// Increase a STX lock up for PoX-4.  Does NOT touch the account nonce.
104
/// Returns Ok( account snapshot ) when successful
105
///
106
/// # Errors
107
/// - Returns Error::PoxExtendNotLocked if this function was called on an account
108
///   which isn't locked. This *should* have been checked by the PoX v4 contract,
109
///   so this should surface in a panic.
110
pub fn pox_lock_increase_v4(
180✔
111
    db: &mut ClarityDatabase,
180✔
112
    principal: &PrincipalData,
180✔
113
    new_total_locked: u128,
180✔
114
) -> Result<STXBalance, LockingError> {
180✔
115
    assert!(new_total_locked > 0);
180✔
116

117
    let mut snapshot = db.get_stx_balance_snapshot(principal)?;
180✔
118

119
    if !snapshot.has_locked_tokens()? {
180✔
120
        return Err(LockingError::PoxExtendNotLocked);
×
121
    }
180✔
122

123
    let bal = snapshot.canonical_balance_repr()?;
180✔
124
    let total_amount = bal
180✔
125
        .amount_unlocked()
180✔
126
        .checked_add(bal.amount_locked())
180✔
127
        .expect("STX balance overflowed u128");
180✔
128
    if total_amount < new_total_locked {
180✔
129
        return Err(LockingError::PoxInsufficientBalance);
×
130
    }
180✔
131

132
    if bal.amount_locked() > new_total_locked {
180✔
133
        return Err(LockingError::PoxInvalidIncrease);
×
134
    }
180✔
135

136
    snapshot.increase_lock_v4(new_total_locked)?;
180✔
137

138
    let out_balance = snapshot.canonical_balance_repr()?;
180✔
139

140
    debug!(
180✔
141
        "PoX v4 lock increased";
142
        "pox_locked_ustx" => out_balance.amount_locked(),
×
143
        "available_ustx" => out_balance.amount_unlocked(),
×
144
        "unlock_burn_height" => out_balance.unlock_height(),
×
145
        "account" => %principal,
146
    );
147

148
    snapshot.save()?;
180✔
149
    Ok(out_balance)
180✔
150
}
180✔
151

152
/// Handle responses from stack-stx and delegate-stack-stx in pox-4 -- functions that *lock up* STX
153
fn handle_stack_lockup_pox_v4(
135,101✔
154
    global_context: &mut GlobalContext,
135,101✔
155
    function_name: &str,
135,101✔
156
    value: &Value,
135,101✔
157
) -> Result<Option<StacksTransactionEvent>, VmExecutionError> {
135,101✔
158
    debug!(
135,101✔
159
        "Handle special-case contract-call to {:?} {function_name} (which returned {value:?})",
160
        boot_code_id(POX_4_NAME, global_context.mainnet)
×
161
    );
162
    // applying a pox lock at this point is equivalent to evaluating a transfer
163
    runtime_cost(
135,101✔
164
        ClarityCostFunction::StxTransfer,
135,101✔
165
        &mut global_context.cost_track,
135,101✔
166
        1,
167
    )?;
×
168

169
    let (stacker, locked_amount, unlock_height) = match parse_pox_stacking_result(value) {
135,101✔
170
        Ok(x) => x,
90,061✔
171
        Err(_) => {
172
            // nothing to do -- the function failed
173
            return Ok(None);
45,040✔
174
        }
175
    };
176

177
    match pox_lock_v4(
90,061✔
178
        &mut global_context.database,
90,061✔
179
        &stacker,
90,061✔
180
        locked_amount,
90,061✔
181
        unlock_height,
90,061✔
182
    ) {
183
        Ok(_) => {
184
            // For direct stacking, we log the locked amount in the asset map.
185
            if function_name == "stack-stx" {
90,060✔
186
                global_context.log_stacking(&stacker, locked_amount)?;
89,340✔
187
            }
720✔
188

189
            let event =
90,060✔
190
                StacksTransactionEvent::STXEvent(STXEventType::STXLockEvent(STXLockEventData {
90,060✔
191
                    locked_amount,
90,060✔
192
                    unlock_height,
90,060✔
193
                    locked_address: stacker,
90,060✔
194
                    contract_identifier: boot_code_id(POX_4_NAME, global_context.mainnet),
90,060✔
195
                }));
90,060✔
196
            Ok(Some(event))
90,060✔
197
        }
198
        Err(LockingError::DefunctPoxContract) => Err(VmExecutionError::Runtime(
×
199
            RuntimeError::DefunctPoxContract,
×
200
            None,
×
201
        )),
×
202
        Err(LockingError::PoxAlreadyLocked) => {
203
            // the caller tried to lock tokens into multiple pox contracts
204
            Err(VmExecutionError::Runtime(
1✔
205
                RuntimeError::PoxAlreadyLocked,
1✔
206
                None,
1✔
207
            ))
1✔
208
        }
209
        Err(e) => {
×
210
            panic!(
×
211
                "FATAL: failed to lock {locked_amount} from {stacker} until {unlock_height}: '{e:?}'"
212
            );
213
        }
214
    }
215
}
135,101✔
216

217
/// Handle responses from stack-extend and delegate-stack-extend in pox-4 -- functions that *extend
218
/// already-locked* STX.
219
fn handle_stack_lockup_extension_pox_v4(
530✔
220
    global_context: &mut GlobalContext,
530✔
221
    function_name: &str,
530✔
222
    value: &Value,
530✔
223
) -> Result<Option<StacksTransactionEvent>, VmExecutionError> {
530✔
224
    // in this branch case, the PoX-4 contract has stored the extension information
225
    //  and performed the extension checks. Now, the VM needs to update the account locks
226
    //  (because the locks cannot be applied directly from the Clarity code itself)
227
    // applying a pox lock at this point is equivalent to evaluating a transfer
228
    debug!(
530✔
229
        "Handle special-case contract-call to {:?} {function_name} (which returned {value:?})",
230
        boot_code_id("pox-4", global_context.mainnet),
×
231
    );
232

233
    runtime_cost(
530✔
234
        ClarityCostFunction::StxTransfer,
530✔
235
        &mut global_context.cost_track,
530✔
236
        1,
237
    )?;
×
238

239
    let (stacker, unlock_height) = match parse_pox_extend_result(value) {
530✔
240
        Ok(x) => x,
340✔
241
        Err(_) => {
242
            // The stack-extend function returned an error: we do not need to apply a lock
243
            //  in this case, and can just return and let the normal VM codepath surface the
244
            //  error response type.
245
            return Ok(None);
190✔
246
        }
247
    };
248

249
    match pox_lock_extend_v4(&mut global_context.database, &stacker, unlock_height) {
340✔
250
        Ok(locked_amount) => {
340✔
251
            // For direct stacking, we log the locked amount in the asset map.
252
            if function_name == "stack-extend" {
340✔
253
                global_context.log_stacking(&stacker, locked_amount)?;
300✔
254
            }
40✔
255

256
            let event =
340✔
257
                StacksTransactionEvent::STXEvent(STXEventType::STXLockEvent(STXLockEventData {
340✔
258
                    locked_amount,
340✔
259
                    unlock_height,
340✔
260
                    locked_address: stacker,
340✔
261
                    contract_identifier: boot_code_id(POX_4_NAME, global_context.mainnet),
340✔
262
                }));
340✔
263
            Ok(Some(event))
340✔
264
        }
265
        Err(LockingError::DefunctPoxContract) => Err(VmExecutionError::Runtime(
×
266
            RuntimeError::DefunctPoxContract,
×
267
            None,
×
268
        )),
×
269
        Err(e) => {
×
270
            // Error results *other* than a DefunctPoxContract panic, because
271
            //  those errors should have been caught by the PoX contract before
272
            //  getting to this code path.
273
            panic!("FATAL: failed to extend lock from {stacker} until {unlock_height}: '{e:?}'");
×
274
        }
275
    }
276
}
530✔
277

278
/// Handle responses from stack-increase and delegate-stack-increase in PoX-4 -- functions
279
/// that *increase already-locked* STX amounts.
280
fn handle_stack_lockup_increase_pox_v4(
590✔
281
    global_context: &mut GlobalContext,
590✔
282
    function_name: &str,
590✔
283
    value: &Value,
590✔
284
) -> Result<Option<StacksTransactionEvent>, VmExecutionError> {
590✔
285
    // in this branch case, the PoX-4 contract has stored the increase information
286
    //  and performed the increase checks. Now, the VM needs to update the account locks
287
    //  (because the locks cannot be applied directly from the Clarity code itself)
288
    // applying a pox lock at this point is equivalent to evaluating a transfer
289
    debug!(
590✔
290
        "Handle special-case contract-call";
291
        "contract" => ?boot_code_id("pox-4", global_context.mainnet),
×
292
        "function" => function_name,
×
293
        "return-value" => %value,
294
    );
295

296
    runtime_cost(
590✔
297
        ClarityCostFunction::StxTransfer,
590✔
298
        &mut global_context.cost_track,
590✔
299
        1,
300
    )?;
×
301

302
    let (stacker, total_locked) = match parse_pox_increase(value) {
590✔
303
        Ok(x) => x,
180✔
304
        Err(_) => {
305
            // nothing to do -- function failed
306
            return Ok(None);
410✔
307
        }
308
    };
309
    match pox_lock_increase_v4(&mut global_context.database, &stacker, total_locked) {
180✔
310
        Ok(new_balance) => {
180✔
311
            // For direct stacking, we log the locked amount in the asset map.
312
            if function_name == "stack-increase" {
180✔
313
                global_context.log_stacking(&stacker, new_balance.amount_locked())?;
120✔
314
            }
60✔
315

316
            let event =
180✔
317
                StacksTransactionEvent::STXEvent(STXEventType::STXLockEvent(STXLockEventData {
180✔
318
                    locked_amount: new_balance.amount_locked(),
180✔
319
                    unlock_height: new_balance.unlock_height(),
180✔
320
                    locked_address: stacker,
180✔
321
                    contract_identifier: boot_code_id(POX_4_NAME, global_context.mainnet),
180✔
322
                }));
180✔
323

324
            Ok(Some(event))
180✔
325
        }
326
        Err(LockingError::DefunctPoxContract) => Err(VmExecutionError::Runtime(
×
327
            RuntimeError::DefunctPoxContract,
×
328
            None,
×
329
        )),
×
330
        Err(e) => {
×
331
            // Error results *other* than a DefunctPoxContract panic, because
332
            //  those errors should have been caught by the PoX contract before
333
            //  getting to this code path.
334
            panic!("FATAL: failed to increase lock from {stacker}: '{e:?}'");
×
335
        }
336
    }
337
}
590✔
338

339
/// Handle special cases when calling into the PoX-4 API contract
340
pub fn handle_contract_call(
446,061✔
341
    global_context: &mut GlobalContext,
446,061✔
342
    sender_opt: Option<&PrincipalData>,
446,061✔
343
    contract_id: &QualifiedContractIdentifier,
446,061✔
344
    function_name: &str,
446,061✔
345
    args: &[Value],
446,061✔
346
    value: &Value,
446,061✔
347
) -> Result<(), VmExecutionError> {
446,061✔
348
    // Generate a synthetic print event for all functions that alter stacking state
349
    let print_event_opt = if let Value::Response(response) = value {
446,061✔
350
        if response.committed {
182,361✔
351
            // method succeeded.  Synthesize event info, but default to no event report if we fail
352
            // for some reason.
353
            // Failure to synthesize an event due to a bug is *NOT* an excuse to crash the whole
354
            // network!  Event capture is not consensus-critical.
355
            let event_info_opt = match synthesize_pox_event_info(
135,951✔
356
                global_context,
135,951✔
357
                contract_id,
135,951✔
358
                sender_opt,
135,951✔
359
                function_name,
135,951✔
360
                args,
135,951✔
361
                response,
135,951✔
362
            ) {
363
                Ok(Some(event_info)) => Some(event_info),
92,370✔
364
                Ok(None) => None,
43,580✔
365
                Err(e) => {
1✔
366
                    error!("Failed to synthesize PoX-4 event info: {e:?}");
1✔
367
                    None
1✔
368
                }
369
            };
370
            if let Some(event_info) = event_info_opt {
135,951✔
371
                let event_response =
92,370✔
372
                    Value::okay(event_info).expect("FATAL: failed to construct (ok event-info)");
92,370✔
373
                let tx_event = ExecutionState::construct_print_transaction_event(
92,370✔
374
                    contract_id.clone(),
92,370✔
375
                    event_response,
92,370✔
376
                );
377
                Some(tx_event)
92,370✔
378
            } else {
379
                None
43,581✔
380
            }
381
        } else {
382
            None
46,410✔
383
        }
384
    } else {
385
        None
263,700✔
386
    };
387

388
    // Execute function specific logic to complete the lock-up
389
    let lock_event_opt = if function_name == "stack-stx" || function_name == "delegate-stack-stx" {
446,061✔
390
        handle_stack_lockup_pox_v4(global_context, function_name, value)?
135,101✔
391
    } else if function_name == "stack-extend" || function_name == "delegate-stack-extend" {
310,960✔
392
        handle_stack_lockup_extension_pox_v4(global_context, function_name, value)?
530✔
393
    } else if function_name == "stack-increase" || function_name == "delegate-stack-increase" {
310,430✔
394
        handle_stack_lockup_increase_pox_v4(global_context, function_name, value)?
590✔
395
    } else {
396
        None
309,840✔
397
    };
398

399
    if function_name == "delegate-stx" {
446,060✔
400
        // Update the asset map to reflect the delegation
401
        match (sender_opt, args.first()) {
1,320✔
402
            (Some(sender), Some(Value::UInt(amount))) => {
1,320✔
403
                global_context.log_stacking(sender, *amount)?;
1,320✔
404
            }
405
            _ => {
406
                let msg = "Unreachable: failed to log STX delegation in PoX-4 delegate-stx call";
×
407
                // This should be unreachable!
408
                error!(
×
409
                    "{msg}";
410
                    "sender" => ?sender_opt,
411
                    "arg0" => ?args.first(),
×
412
                );
413
                return Err(VmExecutionError::Internal(VmInternalError::Expect(
×
414
                    msg.into(),
×
415
                )));
×
416
            }
417
        }
418
    }
444,740✔
419

420
    // append the lockup event, so it looks as if the print event happened before the lock-up
421
    if let Some((batch, _)) = global_context.event_batches.last_mut() {
446,060✔
422
        if let Some(print_event) = print_event_opt {
446,060✔
423
            batch.events.push(print_event);
92,370✔
424
        }
353,780✔
425
        if let Some(lock_event) = lock_event_opt {
446,060✔
426
            batch.events.push(lock_event);
90,580✔
427
        }
355,480✔
428
    }
×
429

430
    Ok(())
446,060✔
431
}
446,061✔
432

433
#[cfg(test)]
434
mod tests {
435
    use clarity::boot_util::boot_code_id;
436
    use clarity::consts::CHAIN_ID_TESTNET;
437
    use clarity::types::StacksEpochId;
438
    use clarity::vm::contexts::GlobalContext;
439
    use clarity::vm::costs::LimitedCostTracker;
440
    use clarity::vm::database::MemoryBackingStore;
441
    use clarity::vm::errors::{RuntimeError, VmExecutionError};
442
    use clarity::vm::types::{StandardPrincipalData, TupleData};
443
    use clarity::vm::{ClarityName, Value};
444

445
    use crate::pox_4::{handle_contract_call, POX_4_NAME};
446

447
    #[test]
448
    fn pox_already_locked_error_when_locking_across_pox_versions() {
1✔
449
        // Setup in-memory database
450
        let mut store = MemoryBackingStore::new();
1✔
451
        let db = store.as_clarity_db();
1✔
452
        let mut global_context = GlobalContext::new(
1✔
453
            false,
454
            CHAIN_ID_TESTNET,
455
            db,
1✔
456
            LimitedCostTracker::new_free(),
1✔
457
            StacksEpochId::Epoch33,
1✔
458
        );
459

460
        let total_amount = 1_000_000_000_000;
1✔
461
        let locked_amount = 500_000_000;
1✔
462
        // Account that will try to lock
463
        let stacker = StandardPrincipalData::transient().into();
1✔
464
        let pox4_contract = boot_code_id(POX_4_NAME, false);
1✔
465

466
        global_context.begin();
1✔
467
        // Simulate the account already having locked tokens in PoX-3
468
        {
1✔
469
            let mut snapshot = global_context
1✔
470
                .database
1✔
471
                .get_stx_balance_snapshot(&stacker)
1✔
472
                .unwrap();
1✔
473
            // Give the account plenty of unlocked STX
1✔
474
            snapshot
1✔
475
                .credit(total_amount)
1✔
476
                .expect("Failed to credit account");
1✔
477
            // Manually lock 500 STX until some future burn height (simulating PoX-3 lock)
1✔
478
            snapshot
1✔
479
                .lock_tokens_v3(locked_amount, 10_000)
1✔
480
                .expect("Failed to pre-lock");
1✔
481
            snapshot.save().expect("Failed to save pre-locked balance");
1✔
482
        }
1✔
483

484
        // Verify it really is locked
485
        let balance = global_context
1✔
486
            .database
1✔
487
            .get_account_stx_balance(&stacker)
1✔
488
            .expect("Failed to get balance");
1✔
489
        assert_eq!(balance.amount_locked(), locked_amount);
1✔
490

491
        // Simulate a successful response from pox-4.stack-stx
492
        // (stacker, lock-amount, unlock-height) tuple
493
        let stack_stx_response = Value::okay(Value::Tuple(
1✔
494
            TupleData::from_data(vec![
1✔
495
                (
1✔
496
                    ClarityName::from_literal("stacker"),
1✔
497
                    Value::Principal(stacker.clone()),
1✔
498
                ),
1✔
499
                (
1✔
500
                    ClarityName::from_literal("lock-amount"),
1✔
501
                    Value::UInt(100_000_000),
1✔
502
                ), // trying to lock 100 more STX
1✔
503
                (
1✔
504
                    ClarityName::from_literal("unlock-burn-height"),
1✔
505
                    Value::UInt(15_000),
1✔
506
                ),
1✔
507
            ])
1✔
508
            .unwrap(),
1✔
509
        ))
1✔
510
        .unwrap();
1✔
511

512
        // Call into the special handler via handle_contract_call
513
        let result = handle_contract_call(
1✔
514
            &mut global_context,
1✔
515
            Some(&stacker), // sender
1✔
516
            &pox4_contract,
1✔
517
            "stack-stx", // function name
1✔
518
            &[
1✔
519
                Value::Bool(false),
1✔
520
                Value::Bool(false),
1✔
521
                Value::Bool(false),
1✔
522
                Value::Bool(false),
1✔
523
            ], // We don't care about the actual args for this test. Just that we have 4.
1✔
524
            &stack_stx_response,
1✔
525
        );
526

527
        assert!(
1✔
528
            matches!(
×
529
                result,
1✔
530
                Err(VmExecutionError::Runtime(RuntimeError::PoxAlreadyLocked, _))
531
            ),
532
            "Expected PoxAlreadyLocked. Got: {result:?}"
533
        );
534
        // Verify no lock was applied (balance unchanged)
535
        let final_balance = global_context
1✔
536
            .database
1✔
537
            .get_account_stx_balance(&stacker)
1✔
538
            .expect("Failed to get final balance");
1✔
539
        assert_eq!(final_balance.amount_locked(), locked_amount); // still the original lock
1✔
540
        assert_eq!(
1✔
541
            final_balance.amount_unlocked(),
1✔
542
            total_amount - locked_amount
1✔
543
        );
544
    }
1✔
545
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc