UpHeld
$100.00
Contracts

Smart contracts and faucet

UpHeld uses two contracts: MockUSDC, a 6-decimal testnet USDC with a public faucet, and UpHealthSLA, which handles subscriptions, reserves, outage records and payouts. Compiled with solc 0.8.24.

Deployments & faucet

Contract addresses and a testnet USDC faucet for the Judge Sandbox wallet.

Network & addresses

Base Sepolia endpoints and the addresses the app uses.

Network
Base Sepolia · chainId 84532 (0x14a34)
RPC
https://sepolia.base.org
Explorer
sepolia.basescan.org
Circle USDC (Sepolia)
0x036CbD53…3dCF7e
UpHealthSLA · sandbox mirror
0x8d098bE9…2cC445
MockUSDC · sandbox mirror
0xa8b284aD…857361
Judge Sandbox wallet
0x87c76143…5bc226
Base mainnet USDC
0x833589fC…A02913
Sandbox mirror addresses are fixed labels for the in-browser contract mirror. No private key exists for them. Circle USDC on BaseScan.
Sandbox faucet

Mint MockUSDC to the Judge Sandbox wallet.

Judge wallet balance
$100.00
0x87c76143…5bc226
Sandbox balance. No wallet required.

Deployment proof

Verified contracts and transactions on BaseScan, the block explorer for Base.

On BaseScan

Links open on BaseScan in a new tab.

Set by NEXT_PUBLIC_UPHELD_SLA_ADDRESS, NEXT_PUBLIC_MOCK_USDC_ADDRESS, NEXT_PUBLIC_PROOF_OUTAGE_TX and NEXT_PUBLIC_PROOF_CLAIM_TX. Malformed values are ignored. If BaseScan shows no source, run npm run verify:input.

Escrow reserves

USDC held by UpHealthSLA, split between provider reserves and claimable compensation.

Provider reserves
$17,400.00
Locked by providers to back their SLA.
Claimable compensation
$0.00
Owed to subscribers after outages.
USDC held by UpHealthSLA
$17,400.00
Reserves + claimable = holdings
Reserves by service

Mirror of the on-chain services(id) struct for each registered plan.

services(id)reserveBalanceUsdcdowntimeMinutescurrentUptimeBpsprovider
#1 Streamly2,400,000,000010000treasury
#2 CloudBox5,000,000,000010000treasury
#3 HyperNode RPC10,000,000,000010000treasury

Trust model

Who can record outages, the rules the contract enforces, and planned production changes.

Who records outages

An attestation is a signed record of how many minutes a service was down, identified by a unique hash.

  • Only approved attestors can record an outage. Providers and subscribers cannot.

    recordOutageisAttestor
  • The wallet that deploys the contract is the first attestor. The owner can add or remove others.

    setAttestor
  • Transferring ownership moves the attestor role to the new owner and removes it from the old one.

    transferOwnership
  • In the Judge Sandbox, a simulated monitor address records outages in your browser.

    0x85fD…8400
Contract rules

Enforced in Solidity. Transactions that break a rule revert.

  • Each subscriber receives the refund for the highest tier reached. A tier is paid once, and subscribers who join during an outage start at the current tier.

    recordOutage
  • Subscribing locks the maximum refund for that seat from the provider’s reserve. When the reserve cannot cover another seat, the plan shows “Sold out”.

    subscribeavailableReserveseatCoverRequired
  • Zero-minute outages and reused attestation hashes are rejected.

    recordOutage
  • Subscribers withdraw refunds themselves. The balance is set to zero before the transfer.

    claimCompensationclaimableCompensation
  • Live mode approves the exact USDC amount each step needs.

    approve
Production roadmap

Planned work. This testnet build uses trusted attestors.

  • Independent probes in several regions, with a quorum of attestors required to record an outage.

    setAttestor
  • A rolling 30-day uptime window. Downtime currently accumulates for the life of a deployment.

    downtimeMinutes
  • A notice period on reserve withdrawals, so providers cannot remove cover shortly before an outage.

    fundReserve
  • An external security audit before mainnet. Seat cover released when a plan ends.

    UpHealthSLA.sol

Live on Base Sepolia

Deploy both contracts from your wallet and call each function.

Source & ABI

Solidity source compiled with solc 0.8.24, and the functions and events in each ABI.

Contract source

contracts/UpHealthSLA.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IERC20 {
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
    function transfer(address recipient, uint256 amount) external returns (bool);
}

/// @title UpHealthSLA: performance-backed USDC subscriptions with automatic SLA compensation
/// @notice Core escrow for UpHeld ("Uptime, Upheld on Base"), targeting Base Sepolia (84532).
///         Units: USDC in 6-decimal minor units, uptime/SLA/refunds in basis points (10000 = 100%).
/// @dev    Trust model: outages are recorded only by approved attestors (the deployer by default;
///         in production, an independent monitor network). Providers and subscribers cannot attest,
///         so neither side can grade its own homework. Each subscriber is credited at most the
///         refund for the worst tier reached; a deeper breach only pays the difference.
///         Every seat is fully backed: subscribing locks that subscriber's worst-case refund out of
///         the provider's reserve, so a provider can't sell coverage it can't pay, and refunds are paid
///         from each seat's own locked amount (no first-come race for a shared pool).
contract UpHealthSLA {
    IERC20 public immutable usdc;
    uint256 public constant BILLING_WINDOW_MINUTES = 43200; // 30 days

    struct Service {
        uint256 id;
        address provider;
        string name;
        uint256 monthlyPriceUsdc; // 6 decimals
        uint16 slaTargetBps;      // e.g. 9995 = 99.95%
        uint16 currentUptimeBps;  // starts at 10000 = 100.00%
        uint256 downtimeMinutes;
        uint256 reserveBalanceUsdc;
        uint16 minorRefundBps;    // e.g. 1000 = 10%
        uint16 moderateRefundBps; // e.g. 2500 = 25%
        uint16 severeRefundBps;   // e.g. 5000 = 50%
        bool active;
    }

    uint256 public nextServiceId = 1;
    mapping(uint256 => Service) public services;
    mapping(uint256 => address[]) public serviceSubscribers;
    mapping(address => mapping(uint256 => bool)) public isSubscribed;
    mapping(address => mapping(uint256 => uint256)) public claimableCompensation;
    /// @notice Refund already credited to a subscriber for the current breach, in USDC minor units.
    ///         A new attestation only pays the gap up to the current tier, never the same tier twice.
    mapping(address => mapping(uint256 => uint256)) public creditedCompensation;

    /// @notice Reserve locked for existing seats, per service. reserveBalanceUsdc >= committedReserve always.
    mapping(uint256 => uint256) public committedReserve;
    /// @notice Worst-case refund still locked for one subscriber's seat.
    mapping(address => mapping(uint256 => uint256)) public seatCover;

    address public owner;
    mapping(address => bool) public isAttestor;
    mapping(bytes32 => bool) public attestationUsed;

    event ServiceRegistered(uint256 indexed serviceId, address indexed provider, string name, uint256 priceUsdc, uint16 slaBps);
    event ReserveFunded(uint256 indexed serviceId, uint256 amountUsdc, uint256 newReserveBalance);
    event Subscribed(uint256 indexed serviceId, address indexed subscriber, uint256 priceUsdc);
    event OutageRecorded(uint256 indexed serviceId, uint256 downtimeMinutes, uint16 newUptimeBps, uint16 refundBps, bytes32 attestationHash);
    event CompensationClaimed(uint256 indexed serviceId, address indexed subscriber, uint256 amountUsdc);
    event AttestorUpdated(address indexed attestor, bool allowed);
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    constructor(address _usdc) {
        require(_usdc != address(0), "USDC address required");
        usdc = IERC20(_usdc);
        owner = msg.sender;
        isAttestor[msg.sender] = true;
        emit OwnershipTransferred(address(0), msg.sender);
        emit AttestorUpdated(msg.sender, true);
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Only owner");
        _;
    }

    function setAttestor(address attestor, bool allowed) external onlyOwner {
        require(attestor != address(0), "Zero address");
        isAttestor[attestor] = allowed;
        emit AttestorUpdated(attestor, allowed);
    }

    function transferOwnership(address newOwner) external onlyOwner {
        require(newOwner != address(0), "Zero address");
        // the outgoing owner stops being an attestor; the new owner becomes one
        isAttestor[owner] = false;
        emit AttestorUpdated(owner, false);
        emit OwnershipTransferred(owner, newOwner);
        owner = newOwner;
        isAttestor[newOwner] = true;
        emit AttestorUpdated(newOwner, true);
    }

    /// @notice Refund tier (bps of the monthly price) for an uptime reading. Shared by subscribe and recordOutage.
    function _refundBpsFor(Service storage svc, uint16 uptimeBps) internal view returns (uint16) {
        if (uptimeBps >= svc.slaTargetBps) return 0;
        if (uptimeBps < 9800) return svc.severeRefundBps;
        if (uptimeBps < 9900) return svc.moderateRefundBps;
        return svc.minorRefundBps;
    }

    function registerService(
        string calldata name,
        uint256 monthlyPriceUsdc,
        uint16 slaTargetBps,
        uint16 minorRefundBps,
        uint16 moderateRefundBps,
        uint16 severeRefundBps
    ) external returns (uint256 serviceId) {
        require(bytes(name).length > 0, "Name required");
        require(monthlyPriceUsdc > 0, "Price must be > 0");
        require(slaTargetBps > 0 && slaTargetBps <= 10000, "SLA target out of range");
        require(minorRefundBps <= moderateRefundBps && moderateRefundBps <= severeRefundBps && severeRefundBps <= 10000, "Tiers must ascend, max 100%");
        serviceId = nextServiceId++;
        services[serviceId] = Service({
            id: serviceId,
            provider: msg.sender,
            name: name,
            monthlyPriceUsdc: monthlyPriceUsdc,
            slaTargetBps: slaTargetBps,
            currentUptimeBps: 10000,
            downtimeMinutes: 0,
            reserveBalanceUsdc: 0,
            minorRefundBps: minorRefundBps,
            moderateRefundBps: moderateRefundBps,
            severeRefundBps: severeRefundBps,
            active: true
        });
        emit ServiceRegistered(serviceId, msg.sender, name, monthlyPriceUsdc, slaTargetBps);
    }

    function fundReserve(uint256 serviceId, uint256 amountUsdc) external {
        require(services[serviceId].active, "Service inactive");
        require(amountUsdc > 0, "Amount must be > 0");
        require(usdc.transferFrom(msg.sender, address(this), amountUsdc), "Reserve transfer failed");
        services[serviceId].reserveBalanceUsdc += amountUsdc;
        emit ReserveFunded(serviceId, amountUsdc, services[serviceId].reserveBalanceUsdc);
    }

    /// @notice Reserve not yet locked to a seat, available to back the next subscriber.
    function availableReserve(uint256 serviceId) public view returns (uint256) {
        return services[serviceId].reserveBalanceUsdc - committedReserve[serviceId];
    }

    /// @notice Reserve a new seat must lock: the severe-tier refund minus the credit it starts with.
    function seatCoverRequired(uint256 serviceId) public view returns (uint256) {
        Service storage svc = services[serviceId];
        uint256 worst = (svc.monthlyPriceUsdc * svc.severeRefundBps) / 10000;
        uint256 start = (svc.monthlyPriceUsdc * _refundBpsFor(svc, svc.currentUptimeBps)) / 10000;
        return worst - start;
    }

    /// @notice Tops every subscriber up to the refund for the service's current tier, paid from that
    ///         subscriber's own locked seat cover. Never pays the same tier twice.
    function _settle(uint256 serviceId) internal returns (uint16 refundBps) {
        Service storage svc = services[serviceId];
        refundBps = _refundBpsFor(svc, svc.currentUptimeBps);
        if (refundBps == 0) return 0;
        uint256 tierRefund = (svc.monthlyPriceUsdc * refundBps) / 10000;
        address[] storage subs = serviceSubscribers[serviceId];
        for (uint256 i = 0; i < subs.length; i++) {
            address sub = subs[i];
            uint256 already = creditedCompensation[sub][serviceId];
            if (tierRefund <= already) continue; // this tier (or a worse one) was already paid
            uint256 owed = tierRefund - already;
            // owed <= seatCover by construction (tiers ascend); the check keeps it true defensively
            if (seatCover[sub][serviceId] >= owed) {
                seatCover[sub][serviceId] -= owed;
                committedReserve[serviceId] -= owed;
                svc.reserveBalanceUsdc -= owed;
                creditedCompensation[sub][serviceId] = tierRefund;
                claimableCompensation[sub][serviceId] += owed;
            }
        }
    }

    function subscribe(uint256 serviceId) external {
        Service storage svc = services[serviceId];
        require(svc.active, "Service inactive");
        require(!isSubscribed[msg.sender][serviceId], "Already subscribed");
        // lock this seat's worst-case refund before taking payment: no backing, no sale
        uint256 cover = seatCoverRequired(serviceId);
        require(availableReserve(serviceId) >= cover, "Reserve can't back another seat");
        committedReserve[serviceId] += cover;
        seatCover[msg.sender][serviceId] = cover;
        isSubscribed[msg.sender][serviceId] = true;
        serviceSubscribers[serviceId].push(msg.sender);
        // Joining mid-breach is not compensated for downtime that happened before you subscribed:
        // start the subscriber's credit at the current tier so only further degradation pays.
        creditedCompensation[msg.sender][serviceId] = (svc.monthlyPriceUsdc * _refundBpsFor(svc, svc.currentUptimeBps)) / 10000;
        require(usdc.transferFrom(msg.sender, svc.provider, svc.monthlyPriceUsdc), "Subscription payment failed");
        emit Subscribed(serviceId, msg.sender, svc.monthlyPriceUsdc);
    }

    function recordOutage(uint256 serviceId, uint256 additionalDowntimeMinutes, bytes32 attestationHash) external {
        require(isAttestor[msg.sender], "Not an attestor");
        Service storage svc = services[serviceId];
        require(svc.active, "Service inactive");
        require(additionalDowntimeMinutes > 0, "No downtime");
        require(attestationHash != bytes32(0) && !attestationUsed[attestationHash], "Attestation reused");
        attestationUsed[attestationHash] = true;

        svc.downtimeMinutes += additionalDowntimeMinutes;
        if (svc.downtimeMinutes >= BILLING_WINDOW_MINUTES) {
            svc.currentUptimeBps = 0;
        } else {
            svc.currentUptimeBps = uint16(((BILLING_WINDOW_MINUTES - svc.downtimeMinutes) * 10000) / BILLING_WINDOW_MINUTES);
        }

        uint16 refundBps = _settle(serviceId);

        emit OutageRecorded(serviceId, svc.downtimeMinutes, svc.currentUptimeBps, refundBps, attestationHash);
    }

    function claimCompensation(uint256 serviceId) external {
        uint256 owed = claimableCompensation[msg.sender][serviceId];
        require(owed > 0, "No compensation available");
        claimableCompensation[msg.sender][serviceId] = 0;
        require(usdc.transfer(msg.sender, owed), "Compensation transfer failed");
        emit CompensationClaimed(serviceId, msg.sender, owed);
    }
}
ABI explorer

22 functions · 7 events

Functions · 22
  • BILLING_WINDOW_MINUTES()view
    → uint256
  • attestationUsed(bytes32)view
    → bool
  • availableReserve(uint256 serviceId)view
    → uint256
  • claimCompensation(uint256 serviceId)nonpayable
  • claimableCompensation(address, uint256)view
    → uint256
  • committedReserve(uint256)view
    → uint256
  • creditedCompensation(address, uint256)view
    → uint256
  • fundReserve(uint256 serviceId, uint256 amountUsdc)nonpayable
  • isAttestor(address)view
    → bool
  • isSubscribed(address, uint256)view
    → bool
  • nextServiceId()view
    → uint256
  • owner()view
    → address
  • recordOutage(uint256 serviceId, uint256 additionalDowntimeMinutes, bytes32 attestationHash)nonpayable
  • registerService(string name, uint256 monthlyPriceUsdc, uint16 slaTargetBps, uint16 minorRefundBps, uint16 moderateRefundBps, uint16 severeRefundBps)nonpayable
    → uint256
  • seatCover(address, uint256)view
    → uint256
  • seatCoverRequired(uint256 serviceId)view
    → uint256
  • serviceSubscribers(uint256, uint256)view
    → address
  • services(uint256)view
    → uint256, address, string, uint256, uint16, uint16, uint256, uint256, uint16, uint16, uint16, bool
  • setAttestor(address attestor, bool allowed)nonpayable
  • subscribe(uint256 serviceId)nonpayable
  • transferOwnership(address newOwner)nonpayable
  • usdc()view
    → address
Events · 7
  • AttestorUpdated(address indexed attestor, bool allowed)
  • CompensationClaimed(uint256 indexed serviceId, address indexed subscriber, uint256 amountUsdc)
  • OutageRecorded(uint256 indexed serviceId, uint256 downtimeMinutes, uint16 newUptimeBps, uint16 refundBps, bytes32 attestationHash)
  • OwnershipTransferred(address indexed previousOwner, address indexed newOwner)
  • ReserveFunded(uint256 indexed serviceId, uint256 amountUsdc, uint256 newReserveBalance)
  • ServiceRegistered(uint256 indexed serviceId, address indexed provider, string name, uint256 priceUsdc, uint16 slaBps)
  • Subscribed(uint256 indexed serviceId, address indexed subscriber, uint256 priceUsdc)