Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
d583323
swap: pristine import of EOSArgentina/evolutiondex as contracts/sysio…
qhool Sep 10, 2026
a33f587
swap: port evolutiondex to the sysio CDT and bring its test suite ont…
qhool Sep 10, 2026
da66d45
swap: bound changefee, and add fee-bounds, rounding-table, and random…
qhool Sep 10, 2026
0bb6899
swap: retire the exact-output (negative-amount) exchange mode
qhool Sep 11, 2026
d8742f1
swap: quote exact-input swaps through amm_math::out_given_in
qhool Sep 11, 2026
9d2b95c
swap: take the pair fee with amm_math::split_wire_fee
qhool Sep 11, 2026
a058257
swap: collect at least one unit of fee on every fee-bearing quote
qhool Sep 11, 2026
21b68c7
swap: cumulative-price accumulators for time-weighted average prices
qhool Sep 11, 2026
ccd9ea2
swap: move every table to kv::table / kv::scoped_table
qhool Sep 11, 2026
7a3db3e
swap: replace the wevotethefee integration with a configurable fee au…
qhool Sep 11, 2026
27b4eed
swap: seed pairs with an integer geometric mean and a creator-chosen …
qhool Sep 11, 2026
c0c0608
swap: drop the indexpair migration action
qhool Sep 11, 2026
961dba9
swap: replace safe.hpp with overflow-checked builtins in the memo parser
qhool Sep 11, 2026
16792ad
swap: optional yield leg, one yield pool per shadow symbol, setyield
qhool Sep 13, 2026
1355e60
swap: settle a yield pool's owed WIRE into the pool, minting nothing
qhool Sep 13, 2026
bd59899
swap: fundyield queues shadow in a yield pool's reservoir
qhool Sep 13, 2026
ab99604
swap: tickyield sells the reservoir through the pool over the horizon
qhool Sep 13, 2026
69c32ed
swap: one system token, second leg of every pair; the transfer handle…
qhool Sep 14, 2026
e74e672
swap docs: both seeds are on deposit, only the first leg's needs the …
qhool Sep 14, 2026
96d2c15
amm_math: the proportional pool-share slice, with compute as its wrapper
qhool Sep 14, 2026
88f5244
tests: commit the badtoken and shadowtoken artifacts so CI can load them
qhool Sep 14, 2026
f30e8d7
swap: a clip floor, and a tick below it sells nothing and banks the time
qhool Sep 15, 2026
483f593
swap: trim the tick comments to what the code does
qhool Sep 15, 2026
062097d
swap: settle owed yield before a swap is priced
qhool Sep 15, 2026
fb057fc
swap: measure the depth cap against depth an attacker cannot inflate
qhool Sep 15, 2026
ae2f7fe
swap: bill a funding announcement to its funder, and let them cancel it
qhool Sep 15, 2026
8b28169
swap: drop the undefined string_to_asset declaration
qhool Sep 15, 2026
7b2838c
swap: decline a clip whose output would not bear the pair's fee
qhool Sep 16, 2026
4dc496f
contracts: widen the shadow yield index to uint128
heifner Sep 21, 2026
3b050f7
contracts: the LIQ yield flow on the depot
heifner Sep 22, 2026
06370dd
plugins: cranks and the relay shape for the LIQ yield flow
heifner Sep 22, 2026
6602ec8
contracts/tests: cover selling shadow into the yield pool
heifner Sep 22, 2026
b88f40a
Merge remote-tracking branch 'origin/master' into feature/liq-yield-c…
heifner Sep 22, 2026
298728e
Merge remote-tracking branch 'origin/master' into feature/liq-yield-c…
heifner Sep 22, 2026
df50556
authex: sweep parked liq on createlink; swap tests use the real sysio…
heifner Sep 22, 2026
f043096
liq: request the kicker only on the swap's intake
heifner Sep 23, 2026
16457e7
liq: reserve pending yield against the asset range
heifner Sep 23, 2026
8803bfa
msgch: drop a SYNDICATE_LIQ whose user is not the outpost's family
heifner Sep 23, 2026
fea8449
outpost_ethereum_client: crank the syndication pool's realizeYield
heifner Sep 23, 2026
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ tmp
.run/
/.data
*.a
*.sw*
*.sw?
*.dylib
*.ll
*.bc
Expand Down
2 changes: 2 additions & 0 deletions contracts/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,6 @@ add_subdirectory(sysio.dclaim)

add_subdirectory(sysio.token)
add_subdirectory(sysio.wrap)
add_subdirectory(sysio.swap)
add_subdirectory(sysio.liq)
add_subdirectory(test_contracts)
51 changes: 51 additions & 0 deletions contracts/sysio.authex/include/sysio.authex/sysio.authex.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#pragma once

#include <stdlib.h>
#include <algorithm>
#include <optional>
#include <string>
#include <vector>

Expand Down Expand Up @@ -189,6 +191,55 @@ namespace sysio {
}
}

/// The key width each chain family carries in a `ChainAddress.address`.
inline constexpr size_t EVM_PUBKEY_BYTES = 33; // compressed secp256k1
inline constexpr size_t SVM_PUBKEY_BYTES = 32; // Ed25519

/**
* @brief Build the `sysio::public_key` variant a `links` row stores from a chain
* family and the raw key bytes an outpost carries in a `ChainAddress.address`.
* Inverse of `pubkey_to_bytes`, shared by every contract that resolves an inbound
* pubkey through the `links` `bypubkey` index.
*
* Only the two families authex links carry are representable: EM (33 bytes) for
* EVM and ED (32 bytes) for SVM. Anything else -- another chain kind, or bytes of
* the wrong width -- yields nullopt, and the caller treats that as "no link".
* That is the whole never-throw contract: `pubkey_to_checksum256` aborts on any
* other variant, so a resolver must never hash a key this function did not build.
*
* @param chain The chain family the bytes belong to.
* @param bytes The raw key: 33 bytes for EM, 32 for ED.
* @return The variant, or nullopt when no link could hold these bytes.
*/
inline std::optional<sysio::public_key> public_key_from_op_address(opp::types::ChainKind chain,
const std::vector<char>& bytes) {
sysio::public_key pk;
switch (chain) {
case opp::types::ChainKind::CHAIN_KIND_EVM: { // EM — variant index 3
if (bytes.size() != EVM_PUBKEY_BYTES) return std::nullopt;
sysio::ecc_public_key arr;
std::copy(bytes.begin(), bytes.end(), arr.begin());
pk.emplace<3>(arr);
return pk;
}
case opp::types::ChainKind::CHAIN_KIND_SVM: { // ED — variant index 4
if (bytes.size() != SVM_PUBKEY_BYTES) return std::nullopt;
sysio::ed_public_key arr;
std::copy(bytes.begin(), bytes.end(), reinterpret_cast<char*>(arr.data()));
pk.emplace<4>(arr);
return pk;
}
default:
return std::nullopt;
}
}

/// True iff `bytes` has the width of a key `chain` links can carry — the exact set
/// `public_key_from_op_address` accepts.
inline bool pubkey_fits(opp::types::ChainKind chain, const std::vector<char>& bytes) {
return public_key_from_op_address(chain, bytes).has_value();
}

class [[sysio::contract("sysio.authex")]] authex : public contract {
public:
using contract::contract;
Expand Down
23 changes: 23 additions & 0 deletions contracts/sysio.authex/src/sysio.authex.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ using namespace sysio;
// sysio funds the RAM for every link row (system-paid) -- createlink and recordlink alike.
constexpr name link_row_payer = "sysio"_n;
constexpr name dclaim_account = "sysio.dclaim"_n;
constexpr name liq_account = "sysio.liq"_n;
constexpr name linkswept_action = "linkswept"_n;
constexpr auto dclaim_not_ready_message =
"sysio.dclaim must be deployed and privileged before creating a link";
Expand Down Expand Up @@ -66,6 +67,27 @@ void try_send_linked_rewards_sweep(const name self, const name account,
if (dclaim_ready()) send_linked_rewards_sweep(self, account, chain_kind, native_address);
}

/** Return whether the shadow-liq ledger can receive a system-paid link sweep. */
[[nodiscard]] bool liq_ready() {
return is_account(liq_account) && is_privileged(liq_account);
}

/**
* Best-effort sweep of the shadow liq parked against a user-created link's key.
*
* sysio.liq parks by the key bytes an outpost reports (the 33-byte EM key or the 32-byte ED
* key), so it receives the verified key, not the derived address the DClaim sweep takes. The
* sweep is optional by design: nothing is parked before sysio.liq exists, and a link created
* while it was absent is served by the permissionless `sysio.liq::sweep`.
*/
void try_send_parked_liq_sweep(const name self, const name account,
const opp::types::ChainKind chain_kind,
const std::vector<char>& pubkey) {
if (!liq_ready()) return;
action(permission_level{self, "active"_n}, liq_account, linkswept_action,
std::make_tuple(account, chain_kind, pubkey)).send();
}

} // anonymous namespace


Expand Down Expand Up @@ -188,6 +210,7 @@ namespace sysio {
});

send_linked_rewards_sweep(get_self(), account, chain_kind, native_address);
try_send_parked_liq_sweep(get_self(), account, chain_kind, pubkey_to_bytes(verified_pub_key));

// The verified key is recorded in the links table only; it is NOT added to the
// account's `active` (or any) permission, so the link grants no Wire signing
Expand Down
Binary file modified contracts/sysio.authex/sysio.authex.wasm
Binary file not shown.
2 changes: 1 addition & 1 deletion contracts/sysio.dclaim/src/sysio.dclaim.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ void dclaim::onreward(uint64_t chain_code,
permission_level{ get_self(), "active"_n },
SYSTEM_ACCOUNT,
"fundclaim"_n,
std::make_tuple(static_cast<int64_t>(reward_amount))
std::make_tuple(get_self(), static_cast<int64_t>(reward_amount))
).send();
}

Expand Down
Binary file modified contracts/sysio.dclaim/sysio.dclaim.wasm
Binary file not shown.
48 changes: 48 additions & 0 deletions contracts/sysio.liq/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
set(contract_name sysio.liq)
bootstrap_contract(${contract_name})

if(BUILD_SYSTEM_CONTRACTS)
find_cdt_magic_enum()
file(GLOB_RECURSE SOURCES src/*.cpp)
file(GLOB_RECURSE HEADERS include/*.hpp)
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::liq"
HEADERS ${HEADERS}
ABI_FILE ${CMAKE_BINARY_DIR}/contracts/${contract_name}/${contract_name}.abi
)
endif()

foreach(target ${targets})
if(NOT TARGET ${target})
message(WARNING "Target ${target} not found, skipping include directory setup")
continue()
endif()

target_include_directories(${target}
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/../../libraries/opp/generated-cdt>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../../libraries/libfc-lite/include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../sysio.authex/include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../sysio.chains/include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../sysio.epoch/include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../sysio.msgch/include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../sysio.token/include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../sysio.tokens/include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../sysio.opp.common/include>
)

target_link_libraries(${target}
INTERFACE
magic_enum::magic_enum
)
endforeach()
endif()
99 changes: 99 additions & 0 deletions contracts/sysio.liq/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# sysio.liq

The depot's shadow token for syndicated liq. One shadow symbol per outpost liq
token (`LIQETH`, `LIQSOL`, precision 9), minted 1:1 against liq the outpost holds
in its syndicated pool and burned when a holder de-syndicates. Holders earn WIRE
yield through the cumulative index of `sysio.opp.common/shadow_yield.hpp`: every
balance move settles the row first, and `claim` pays what `shadow::owed` says.

The contract is privileged (`sysio.roa::setsyscode`): holder rows bill the `sysio`
RAM pool, and every inline action carries the authority it needs, so no
`sysio.code` grant exists anywhere.

## Flows

**Syndication (SYNDICATE_LIQ, inbound).** `sysio.msgch` resolves the user's pubkey
through `sysio.authex` and calls `mintsynd` (linked) or `park` (not linked). A
parked row accrues like any holder; `linkswept` (inline from `createlink`) or the
permissionless `sweep` delivers it, accrued WIRE included, to the account the
pubkey later links. The inbound actions never throw: a replayed `sequence`, an
unknown token, a token of another chain or an out-of-range amount is dropped with
a diagnostic.

**Yield (LIQ_YIELD, inbound).** `mintyield` holds the reported yield in the
symbol's pending balance, outside supply but reserved against the asset range
beside it: every supply-growing path measures its headroom net of what is pending,
a report past that headroom is dropped before its sequence is consumed, and
queueing always fits. The permissionless `queueyield` mints it
to this contract, announces it to `sysio.swap` with `fundyield` and transfers it,
in one transaction, so it lands in the pool's reservoir and never accrues to this
contract. The swap sells it in clips through the pool and pays the proceeds in
through `addyield`, which advances the index by `quantity / supply`, carries the
remainder and pulls the WIRE by inline transfer.

**The kicker.** On the swap's intake, the one that is yield, `addyield` requests
`kicker_bps` (default 200) of the intake from T5 through
`sysio.system::fundclaim(sysio.liq, amount)`, then folds what actually landed into
the same index with `addkicker`, measured against the balance the pull left. A
donation from any other account distributes only itself: the treasury never tops
up what is not yield, or a near-sole holder could donate, claim it back with the
kicker on top, and repeat. A short T5 reduces the kicker only. `setkicker` (auth
`sysio`, the account council proposals execute as) changes the next intake.

**De-syndication (DESYNDICATE_LIQ, outbound).** `desyndicate` requires the holder
to be AuthX-linked for the token's chain, settles and burns the shadow, and queues
`DesyndicateLIQ{chain_code, user = linked pubkey, amount, request_id}` through
`sysio.msgch::queueout`. Request ids start at 1. The burn is final: an outpost
refusal is reconciled from its log by governance through `recredit`.

**Launch ingestion (epoch-0 bootstrap window, privileged caller).** `regliqpool`
mints the LCO liq to `sysio`, deposits it with the T5 dex earmark WIRE into
`sysio.swap`, creates the pair (`sysio` fee authority, the shadow as yield leg)
and sets the tick parameters. `importsynd` replays pre-launch positions in
batches (the LCO yield already folded into each amount); `importdone` closes the
import.

## Tables

| Table | Scope / key | Row |
|---|---|---|
| `stat` | symbol code | `supply`, `chain_code`, `token_code`, `pair_symbol` (empty until `regliqpool`); index `bytoken` |
| `accounts` | holder / symbol code | `balance`, `index_checkpoint` (uint128), `owed_wire` |
| `yieldidx` | symbol code | `index` (uint128), `pot`, `carry` |
| `parked` | symbol code, chain kind, pubkey | `chain_kind`, `pubkey`, `holding` (an account row) |
| `liqpending` | symbol code | `quantity` minted by LIQ_YIELD and not yet queued; counts against the asset range beside supply |
| `liqcursors` | chain code | `last_sequence`, `last_epoch` |
| `liqconfig` | singleton | `kicker_bps`, `import_complete` |
| `liqcounters` | singleton | `next_request_id` |

## Actions

| Action | Auth | Purpose |
|---|---|---|
| `create(sym, chain_code, token_code)` | self | Register a shadow for an active `TOKEN_KIND_LIQ` token bound to an active outpost |
| `setkicker(bps)` | `sysio` | Kicker for the intakes from now on |
| `recredit(holder, quantity)` | self | Mint back after an outpost refused a de-syndication |
| `mintsynd(chain_code, sequence, account, token_code, amount)` | `sysio.msgch` | SYNDICATE_LIQ, linked user |
| `park(chain_code, sequence, chain_kind, pubkey, token_code, amount)` | `sysio.msgch` | SYNDICATE_LIQ, unlinked user |
| `mintyield(chain_code, sequence, epoch, token_code, amount)` | `sysio.msgch` | LIQ_YIELD into the pending balance |
| `queueyield(sym)` | none | Pending yield into the swap's reservoir |
| `sweep(account, chain_kind)` | none | Deliver parked rows for an existing link |
| `transfer`, `open`, `close`, `claim` | holder | `sysio.token`'s shape; `close` refuses while yield is owed |
| `addyield(from, quantity, target)` | `from` | Distribute WIRE to `target`'s holders |
| `addkicker(sym, base_balance, requested)` | self | Inline from `addyield` |
| `linkswept(account, chain_kind, pubkey)` | `sysio.authex` | Deliver parked rows on link |
| `desyndicate(holder, quantity)` | holder | Burn and queue DESYNDICATE_LIQ |
| `regliqpool(...)`, `importsynd(...)`, `importdone()` | privileged caller, epoch 0 | Launch ingestion |

## Deployment

In order: `sysio.roa::setsyscode` for `sysio.liq`; the chain and its liq token
registered and active in `sysio.chains` / `sysio.tokens`; `create` per shadow
symbol; `setkicker` if the default is not wanted; `regliqpool` per pool;
`importsynd` batches; `importdone`. `sysio.swap` must be configured
(`setconfig`) before `regliqpool`, and the batch-operator crank pushes
`queueyield` per symbol and `sysio.swap::tickyield` per pair.

The Solana relay must carry the `DESYNDICATE_LIQ` effect shape before this
contract is deployed: a delivered `DesyndicateLIQ` without its accounts aborts
the outpost's handler and wedges the epoch.
Loading
Loading