Skip to content
Merged
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
3 changes: 2 additions & 1 deletion OVERVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ Elected via Appointed Proof of Stake (APoS). They validate transactions and prod
Operators scheduled in groups of `operators_per_epoch` (default 21 operators in 3 groups of 7); one group is on duty per epoch, and the schedule slides forward every epoch. They run the `batch_operator_plugin` and are responsible for:
- Reading each outpost's outbound envelope and delivering it to `sysio.msgch`
- Relaying the depot's outbound envelopes to each outpost
- Cranking the depot: `sysio.msgch::chkcons` (epoch advance), `sysio.chalg::chkdispute`, `sysio.swap::tickyield`, `sysio.liq::queueyield`
- Cranking the depot: `sysio.msgch::chkcons` (epoch advance), `sysio.chalg::chkdispute`, `sysio.swap::tickyield`, `sysio.liq::queueyield`, `sysio.kicker::kick`

Batch operators post depot-native collateral on `sysio.opreg`; a non-canonical delivery is slashed and missed deliveries lead to termination.

Expand All @@ -110,6 +110,7 @@ Holders of a node-owner NFT who commit it on the Ethereum outpost (`NODE_OWNER_R
| **sysio.bond** | Generic bonded underwriting of provable statements |
| **sysio.liq** | Shadow liq tokens (`LIQETH`, `LIQSOL`) minted against outpost custody, with a WIRE yield index |
| **sysio.swap** | Depot-local constant-product AMM; every pair's second leg is WIRE |
| **sysio.kicker** | Budgeted simple-accrual T5 WIRE gifts to LIQ holders, priced by swap TWAP with bounded accrual and daily payouts |
| **sysio.dclaim** | Funded WIRE launch import, account linking and claims |
| **sysio.andon** | Emergency stop: one cord freezing depot fund egress |
| **sysio.roa** | Resources and node-owner registry |
Expand Down
1 change: 1 addition & 0 deletions contracts/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ add_subdirectory(sysio.token)
add_subdirectory(sysio.wrap)
add_subdirectory(sysio.swap)
add_subdirectory(sysio.liq)
add_subdirectory(sysio.kicker)
add_subdirectory(sysio.bond)
add_subdirectory(sysio.synd)
add_subdirectory(test_contracts)
30 changes: 30 additions & 0 deletions contracts/sysio.kicker/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Register committed artifacts in host builds and compile with CDT in contract builds.
set(contract_name sysio.kicker)
bootstrap_contract(${contract_name})
if(BUILD_SYSTEM_CONTRACTS)
find_cdt_magic_enum()
set(SOURCES src/sysio.kicker.cpp)
add_contract(${contract_name} ${contract_name} ${SOURCES})
set(targets ${contract_name})
if("native-module" IN_LIST SYSIO_WASM_RUNTIMES)
list(APPEND targets ${contract_name}_native)
add_native_contract(
TARGET ${contract_name}_native
SOURCES ${SOURCES}
INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/include
CONTRACT_CLASS "sysio::kicker"
HEADERS "sysio.kicker/sysio.kicker.hpp"
ABI_FILE ${CMAKE_BINARY_DIR}/contracts/${contract_name}/${contract_name}.abi)
endif()
foreach(target ${targets})
target_include_directories(${target} PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_BINARY_DIR}/../../libraries/opp/generated-cdt
${CMAKE_CURRENT_SOURCE_DIR}/../sysio.opp.common/include
${CMAKE_CURRENT_SOURCE_DIR}/../sysio.andon/include
${CMAKE_CURRENT_SOURCE_DIR}/../sysio.liq/include
${CMAKE_CURRENT_SOURCE_DIR}/../sysio.token/include
${CMAKE_CURRENT_SOURCE_DIR}/../sysio.system/include)
target_link_libraries(${target} INTERFACE magic_enum::magic_enum)
endforeach()
endif()
85 changes: 85 additions & 0 deletions contracts/sysio.kicker/include/sysio.kicker/kicker_math.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#pragma once
/**
* @file kicker_math.hpp
* @brief Integer-only simple accrual, floored twice in favour of the treasury.
*/
#include <sysio.opp.common/twap.hpp>
#include <cstdint>
#include <optional>

namespace sysio::kicker_math {
using u128 = unsigned __int128;
/// Governance rates are basis points; one Julian year is exactly 365.25 days.
inline constexpr uint64_t bps_scale = 10'000;
inline constexpr uint64_t year_sec = 31'557'600;
inline constexpr uint64_t micros_per_sec = 1'000'000;
inline constexpr uint64_t accrual_divisor = bps_scale * year_sec * micros_per_sec;
/// Asset amounts are bounded by the protocol, even when intermediate values are wider.
inline constexpr uint64_t max_amount = (uint64_t{1} << 62) - 1;
/// Default minimum unpaid accrual interval: one hour.
inline constexpr uint32_t default_min_interval_sec = 3600;
/// Default per-token annual simple rate: 2%.
inline constexpr uint16_t default_rate_bps = 200;
/// Default payment granularity: one nine-decimal WIRE.
inline constexpr uint64_t default_min_gift = 1'000'000'000;
/// UTC-sized day used for the mandatory per-pool spend ceiling.
inline constexpr uint64_t day_sec = 86'400;
/// Governance observation window defaults to one day and cannot be shorter than one minute.
inline constexpr uint32_t default_twap_window_sec = day_sec;
inline constexpr uint32_t min_twap_window_sec = 60;
/// Bound catch-up to 72 hours per payment, independently of the observation window.
inline constexpr uint64_t max_catchup_us = 3 * day_sec * micros_per_sec;
/// Governance may lower, but cannot exceed, 1,000 WIRE per pool per UTC day.
inline constexpr uint64_t max_daily_gift = 1'000'000'000'000;
/// Default spot-versus-TWAP guard: a kick pays only while spot is within 50% of the window TWAP.
inline constexpr uint16_t default_max_spot_deviation_bps = 5'000;

/** Compute floor(S * bps * elapsed_us / (10000 * Julian_year_us)).
* Dividing S*bps first as quotient and remainder keeps both products below
* 2^128 for every asset, accepted rate and uint64 elapsed time. The caller
* applies the catch-up policy before this arithmetic helper.
*/
inline u128 interest(uint64_t supply, uint16_t rate_bps, uint64_t elapsed_us) {
const u128 annual = static_cast<u128>(supply) * rate_bps;
return (annual / accrual_divisor) * elapsed_us +
(annual % accrual_divisor) * elapsed_us / accrual_divisor;
}

/** Price an interest amount at a supplied Q64.64 TWAP.
* Two 128-bit limbs hold the full product, including intervals so long that
* interest itself exceeds uint64. A result outside the asset range is a
* non-payable gift, never a wrapped or clipped transfer.
*/
inline std::optional<uint64_t> gift_at_price(uint64_t supply, uint16_t rate_bps, uint64_t elapsed_us,
u128 price) {
const u128 amount = interest(supply, rate_bps, elapsed_us);
opp::twap::cumulative_price product{}, high{};
opp::twap::accumulate(product, price, static_cast<uint64_t>(amount));
opp::twap::accumulate(high, price, static_cast<uint64_t>(amount >> opp::twap::PRICE_FRACTION_BITS));
const u128 shifted = high.lo << opp::twap::PRICE_FRACTION_BITS;
const u128 old_lo = product.lo;
product.lo += shifted;
product.hi += (high.hi << opp::twap::PRICE_FRACTION_BITS) +
(high.lo >> opp::twap::PRICE_FRACTION_BITS) + (product.lo < old_lo ? 1 : 0);
const u128 priced = product.lo >> opp::twap::PRICE_FRACTION_BITS;
if (product.hi != 0 || priced > max_amount) return std::nullopt;
return static_cast<uint64_t>(priced);
}
/** True iff spot strays from the window TWAP by more than `max_deviation_bps` of the TWAP:
* |spot - twap| * 10000 > twap * max_deviation_bps, both sides exact in 256 bits (a Q64.64 price times a
* basis-point count can exceed 128 bits). At the threshold itself the spot does not deviate. A zero TWAP
* deviates from any nonzero spot.
*/
inline bool spot_deviates(u128 spot, u128 twap, uint16_t max_deviation_bps) {
const u128 deviation = spot > twap ? spot - twap : twap - spot;
opp::twap::cumulative_price measured{}, allowed{};
opp::twap::accumulate(measured, deviation, bps_scale);
opp::twap::accumulate(allowed, twap, max_deviation_bps);
return measured.hi != allowed.hi ? measured.hi > allowed.hi : measured.lo > allowed.lo;
}
/// Ratio-priced entry point for host arithmetic tests.
inline std::optional<uint64_t> gift(uint64_t supply, uint16_t rate_bps, uint64_t elapsed_us,
uint64_t pool_wire, uint64_t pool_shadow) {
return gift_at_price(supply, rate_bps, elapsed_us, opp::twap::price_fp(pool_wire, pool_shadow));
}
} // namespace sysio::kicker_math
112 changes: 112 additions & 0 deletions contracts/sysio.kicker/include/sysio.kicker/sysio.kicker.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#pragma once
/**
* @file sysio.kicker.hpp
* @brief Governance-budgeted T5 gifts to LIQ holders, triggered by any keeper.
*
* Deploy privileged with roa::setsyscode. Budget is an earmark outside emissions;
* no draw alters sysio.system::t5state.total_distributed. A configurable TWAP values at most
* 72 hours of unpaid accrual per kick, on the smaller of the LIQ supply at the last checkpoint and the
* current supply: a gift never exceeds the honest gift of the supply present at the last checkpoint, and
* LIQ minted after a checkpoint is sized from the next full payment or checkpointing reset.
*/
#include <sysio/sysio.hpp>
#include <sysio/symbol.hpp>
#include <sysio/kv_global.hpp>
#include <sysio/kv_table.hpp>
#include <sysio/time.hpp>
#include <sysio.kicker/kicker_math.hpp>

namespace sysio {
/// Privileged treasury-to-shadow-yield contract; no donation-triggered payment path.
class [[sysio::contract("sysio.kicker")]] kicker : public contract {
public:
using contract::contract;
/// Governance authority and treasury account.
static constexpr name system_account = "sysio"_n;
/// Depot shadow token contract.
static constexpr name liq_account = "sysio.liq"_n;
/// Native WIRE token contract.
static constexpr name token_account = "sysio.token"_n;

/// Governance budget in WIRE subunits, payment spacing, minimum TWAP observation span and the spot guard.
struct [[sysio::table("kickcfg")]] kick_config {
uint64_t budget_remaining = 0; ///< Absolute remaining earmark; setconfig replaces it.
/// Minimum unpaid accrual interval before an attempt.
uint32_t min_interval_sec = kicker_math::default_min_interval_sec;
/// Minimum observation span; the next kick uses this value even for an open observation.
uint32_t twap_window_sec = kicker_math::default_twap_window_sec;
/// Most spot may stray from the window TWAP, in basis points of the TWAP, for a kick to pay; 1..10000.
uint16_t max_spot_deviation_bps = kicker_math::default_max_spot_deviation_bps;
SYSLIB_SERIALIZE(kick_config, (budget_remaining)(min_interval_sec)(twap_window_sec)(max_spot_deviation_bps))
};
/// Symbol-code primary key for independently configured LIQ tokens.
struct pool_key {
uint64_t symbol_code;
SYSLIB_SERIALIZE(pool_key, (symbol_code))
};
/// Per-token accrual and spend state; shortfalls are latest deficits, not duplicated debt.
struct [[sysio::table("kickpools")]] kick_pool {
symbol_code sym;
uint16_t rate_bps = kicker_math::default_rate_bps;
uint64_t min_gift = kicker_math::default_min_gift;
time_point last_kick{};
uint128_t gifted_total = 0; ///< Lifetime paid WIRE, outside emissions accounting.
uint64_t shortfall_amount = 0; ///< Latest gift minus paid amount; cleared by full payment or zero supply.
time_point shortfall_time{}; ///< Last unpaid shortfall, for governance visibility.
bool gift_overflow = false; ///< Gift exceeded asset range; clock remains open.
/// Spot strayed past max_spot_deviation_bps from the TWAP at the latest priced attempt; clock and observation
/// held.
bool spot_deviation = false;
uint64_t max_gift_per_day = kicker_math::max_daily_gift; ///< Positive ceiling, at most 1,000 WIRE per UTC day.
uint64_t day = 0; ///< UTC day number of the most recent payment.
uint128_t spent_today = 0;
/// LIQ supply, in subunits, at the latest checkpoint: addpool, a full payment, a zero-supply or zero-snapshot
/// restart, or a forfeiting reset. A partial payment lowers it to the supply it priced. Gifts are sized on the
/// smaller of this and the current supply, so LIQ minted after a checkpoint earns nothing until the next full
/// payment or reset; a pool whose payments are all partial keeps its snapshot until one lands.
int64_t supply_snapshot = 0;
opp::twap::cumulative_price price_snapshot; ///< Swap price1 extended to price_time.
time_point price_time{}; ///< Observation starts on addpool or the last payment, full or partial.
SYSLIB_SERIALIZE(kick_pool, (sym)(rate_bps)(min_gift)(last_kick)(gifted_total)
(shortfall_amount)(shortfall_time)(gift_overflow)(spot_deviation)
(max_gift_per_day)(day)(spent_today)(supply_snapshot)(price_snapshot)(price_time))
};
/// Governance-owned singleton and per-symbol payment clocks.
using config_table = kv::global<"kickcfg"_n, kick_config>;
using pools_table = kv::table<"kickpools"_n, pool_key, kick_pool>;

/// Replace budget, payment interval, TWAP window and spot guard. Requires sysio; never settles open intervals.
/// The next kick applies the window to the existing price_time/snapshot without restarting observation.
[[sysio::action]] void setconfig(kick_config cfg);
/// Start accrual now for a registered LIQ/WIRE yield pair. Requires sysio; defaults to 200 bps and 1 WIRE.
[[sysio::action]] void addpool(symbol_code sym, uint16_t rate_bps = kicker_math::default_rate_bps,
uint64_t min_gift = kicker_math::default_min_gift,
uint64_t max_gift_per_day = kicker_math::max_daily_gift);
/// Change rate/minimum/daily ceiling without resetting spending or observation; prices bounded accrual.
/// Requires sysio.
[[sysio::action]] void setpool(symbol_code sym, uint16_t rate_bps, uint64_t min_gift, uint64_t max_gift_per_day);
/// Stop accrual and erase the row; re-adding starts a new interval. Requires sysio.
[[sysio::action]] void rmpool(symbol_code sym);
/// Permissionless TWAP gift after the configured observation window; payment forfeits accrual older than 72 hours.
/// The gift is sized on min(supply_snapshot, current LIQ supply): it never exceeds the honest gift of the supply
/// present at the last checkpoint, and LIQ minted after a checkpoint is sized from the next full payment or
/// checkpointing reset (a pool whose payments are all partial keeps its snapshot until one lands). New LIQ still
/// takes its pro-rata share of that correctly sized gift through addyield, which divides by the current supply.
/// Sizing sees the supply at the two ends of the interval, not inside it: LIQ burned after a checkpoint and
/// re-minted before the next payment or checkpointing reset is sized as if held throughout; a kick that neither
/// pays nor checkpoints leaves that gap open. The bound above and the daily cap still hold.
/// A zero snapshot with nonzero supply only checkpoints. A gift below min_gift while the supply has grown past
/// the snapshot and at least 72 hours are unpaid forfeits the interval and checkpoints at the current supply, but
/// only if the same interval sized on the current supply at the same TWAP reaches min_gift (or overflows), i.e. the
/// snapshot is the binding constraint; a price dip that sinks both gifts below the minimum holds instead, while a
/// dip that leaves only the snapshot's gift below the minimum still forfeits.
/// Pays nothing while spot strays from the window TWAP by more than max_spot_deviation_bps, holding the clock.
/// Partial payments advance a floored pro-rata clock within the bounded 72-hour accrual interval; every payment
/// restarts the observation, so a partial payment's remainder waits for, and is priced by, a fresh window.
/// Skips retain state.
[[sysio::action]] void kick(symbol_code sym);
private:
/// Validate governance settings at both pool configuration boundaries.
static void validate_pool(uint16_t rate_bps, uint64_t min_gift, uint64_t max_gift_per_day);
};
} // namespace sysio
Loading
Loading