depot v3: running balance guard + sysio.payer auth + clearmsgs admin action

Fixes:
- yield_reward and wire_purchase transfers now check available WIRE balance
  before sending, preventing overdrawn balance errors during crank
- Running balance tracker across process_ready_messages loop prevents
  deferred inline action overdraw (inline actions execute after caller returns)
- sysio.payer permission_level added to transfer actions for Wire ROA model
- clearmsgs admin action to flush garbage inbound messages (testnet only)

Deploy tx: 5ab93b7c (wasm), b67ccc07 (wasm+abi)
Code hash: eca6e8ee71c4b8d4e96f8543036f677775320bcea87aaf522b096f18a4cae9ac
Wasm size: 113,014 bytes
This commit is contained in:
2026-03-13 17:26:39 +00:00
parent 10a8884fc9
commit e58cdf9c9d
5 changed files with 1138 additions and 24 deletions

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -16,6 +16,11 @@ namespace sysio {
[[sysio::action]] [[sysio::action]]
void bootstrap(); void bootstrap();
// ── Admin: clear all inbound messages (testnet only) ────────────────
[[sysio::action]]
void clearmsgs();
using clearmsgs_action = sysio::action_wrapper<"clearmsgs"_n, &depot::clearmsgs>;
// ── FR-800: Crank Execution ─────────────────────────────────────────── // ── FR-800: Crank Execution ───────────────────────────────────────────
[[sysio::action]] [[sysio::action]]
void crank(name operator_account); void crank(name operator_account);
@@ -146,7 +151,7 @@ namespace sysio {
void process_ready_messages(); void process_ready_messages();
void expire_underwriting_locks(); void expire_underwriting_locks();
void elect_operators_for_epoch(uint64_t next_epoch); void elect_operators_for_epoch(uint64_t next_epoch);
void process_assertion(uint64_t message_number, assertion_type_t type, const std::vector<char>& payload); void process_assertion(uint64_t message_number, assertion_type_t type, const std::vector<char>& payload, int64_t& available_wire);
void queue_outbound_message(assertion_type_t type, const std::vector<char>& payload); void queue_outbound_message(assertion_type_t type, const std::vector<char>& payload);
void mark_epoch_valid(uint64_t chain_scope, uint64_t epoch_number); void mark_epoch_valid(uint64_t chain_scope, uint64_t epoch_number);
void slash_minority_operators(uint64_t chain_scope, uint64_t epoch_number, void slash_minority_operators(uint64_t chain_scope, uint64_t epoch_number,

View File

@@ -194,8 +194,9 @@ void depot::uploadmsgs(name operator_account,
}); });
// FR-108: Process challenge messages immediately // FR-108: Process challenge messages immediately
int64_t unused_wire = 0;
if (is_challenge) { if (is_challenge) {
process_assertion(msg_num - 1, assertion, payload); process_assertion(msg_num - 1, assertion, payload, unused_wire);
} }
} }

View File

@@ -56,6 +56,26 @@ void depot::bootstrap() {
elect_operators_for_epoch(s.current_epoch); elect_operators_for_epoch(s.current_epoch);
} }
// ── clearmsgs (testnet admin): flush all inbound messages ────────────────────
void depot::clearmsgs() {
require_auth(get_self());
auto s = get_state();
uint64_t chain_scope = uint64_t(s.chain_id);
opp_in_table msgs(get_self(), chain_scope);
auto it = msgs.begin();
uint32_t count = 0;
while (it != msgs.end()) {
it = msgs.erase(it);
++count;
}
// Reset message numbering in epoch state
// (Messages have been discarded, epoch state should reflect this)
}
// ── crank (FR-801) ────────────────────────────────────────────────────────── // ── crank (FR-801) ──────────────────────────────────────────────────────────
void depot::crank(name operator_account) { void depot::crank(name operator_account) {
@@ -86,18 +106,54 @@ void depot::crank(name operator_account) {
set_state(s); set_state(s);
} }
// Helper: read a big-endian uint64 from a byte buffer
static uint64_t read_be_u64(const char* p) {
return (uint64_t(uint8_t(p[0])) << 56) | (uint64_t(uint8_t(p[1])) << 48) |
(uint64_t(uint8_t(p[2])) << 40) | (uint64_t(uint8_t(p[3])) << 32) |
(uint64_t(uint8_t(p[4])) << 24) | (uint64_t(uint8_t(p[5])) << 16) |
(uint64_t(uint8_t(p[6])) << 8) | uint64_t(uint8_t(p[7]));
}
static int64_t read_be_i64(const char* p) {
return static_cast<int64_t>(read_be_u64(p));
}
static uint16_t read_be_u16(const char* p) {
return (uint16_t(uint8_t(p[0])) << 8) | uint16_t(uint8_t(p[1]));
}
// Helper: read depot's own WIRE balance from sysio.token accounts table
struct token_account {
asset balance;
uint64_t primary_key() const { return balance.symbol.code().raw(); }
};
typedef multi_index<"accounts"_n, token_account> token_accounts_table;
static int64_t get_wire_balance(name token_contract, name owner) {
token_accounts_table accts(token_contract, owner.value);
auto it = accts.find(symbol_code("WIRE").raw());
if (it == accts.end()) return 0;
return it->balance.amount;
}
// ── process_ready_messages (internal) ──────────────────────────────────────── // ── process_ready_messages (internal) ────────────────────────────────────────
void depot::process_ready_messages() { void depot::process_ready_messages() {
auto s = get_state(); auto s = get_state();
uint64_t chain_scope = uint64_t(s.chain_id); uint64_t chain_scope = uint64_t(s.chain_id);
// Read depot WIRE balance once — inline transfers are deferred so we must
// track the running balance ourselves to avoid overdrawn errors.
int64_t available_wire = get_wire_balance(s.token_contract, get_self());
opp_in_table msgs(get_self(), chain_scope); opp_in_table msgs(get_self(), chain_scope);
auto by_status = msgs.get_index<"bystatus"_n>(); auto by_status = msgs.get_index<"bystatus"_n>();
auto it = by_status.lower_bound(uint64_t(message_status_ready)); auto it = by_status.lower_bound(uint64_t(message_status_ready));
while (it != by_status.end() && it->status == message_status_ready) { while (it != by_status.end() && it->status == message_status_ready) {
process_assertion(it->message_number, it->assertion_type, it->payload); process_assertion(it->message_number, it->assertion_type, it->payload, available_wire);
it = by_status.erase(it); it = by_status.erase(it);
} }
} }
@@ -130,24 +186,8 @@ void depot::process_ready_messages() {
// slash_operator (0xEE05): // slash_operator (0xEE05):
// [operator_id(8) | reason_len(2) | reason(N)] // [operator_id(8) | reason_len(2) | reason(N)]
// Helper: read a big-endian uint64 from a byte buffer
static uint64_t read_be_u64(const char* p) {
return (uint64_t(uint8_t(p[0])) << 56) | (uint64_t(uint8_t(p[1])) << 48) |
(uint64_t(uint8_t(p[2])) << 40) | (uint64_t(uint8_t(p[3])) << 32) |
(uint64_t(uint8_t(p[4])) << 24) | (uint64_t(uint8_t(p[5])) << 16) |
(uint64_t(uint8_t(p[6])) << 8) | uint64_t(uint8_t(p[7]));
}
static int64_t read_be_i64(const char* p) {
return static_cast<int64_t>(read_be_u64(p));
}
static uint16_t read_be_u16(const char* p) {
return (uint16_t(uint8_t(p[0])) << 8) | uint16_t(uint8_t(p[1]));
}
void depot::process_assertion(uint64_t message_number, assertion_type_t type, void depot::process_assertion(uint64_t message_number, assertion_type_t type,
const std::vector<char>& payload) { const std::vector<char>& payload, int64_t& available_wire) {
auto s = get_state(); auto s = get_state();
uint64_t chain_scope = uint64_t(s.chain_id); uint64_t chain_scope = uint64_t(s.chain_id);
@@ -273,15 +313,16 @@ void depot::process_assertion(uint64_t message_number, assertion_type_t type,
} }
// Issue WIRE to beneficiary if they have an account and amount > 0 // Issue WIRE to beneficiary if they have an account and amount > 0
if (wire_reward > 0 && is_account(beneficiary)) { if (wire_reward > 0 && is_account(beneficiary) && available_wire >= wire_reward) {
asset wire_payout(wire_reward, symbol("WIRE", 4)); asset wire_payout(wire_reward, symbol("WIRE", 4));
action( action(
permission_level{get_self(), "active"_n}, std::vector<permission_level>{{get_self(), "sysio.payer"_n}, {get_self(), "active"_n}},
s.token_contract, s.token_contract,
"transfer"_n, "transfer"_n,
std::make_tuple(get_self(), beneficiary, wire_payout, std::make_tuple(get_self(), beneficiary, wire_payout,
std::string("yield reward distribution")) std::string("yield reward distribution"))
).send(); ).send();
available_wire -= wire_reward;
} }
break; break;
} }
@@ -333,15 +374,16 @@ void depot::process_assertion(uint64_t message_number, assertion_type_t type,
}); });
// Transfer WIRE to buyer // Transfer WIRE to buyer
if (is_account(buyer)) { if (is_account(buyer) && available_wire >= wire_output) {
asset wire_payout(wire_output, symbol("WIRE", 4)); asset wire_payout(wire_output, symbol("WIRE", 4));
action( action(
permission_level{get_self(), "active"_n}, std::vector<permission_level>{{get_self(), "sysio.payer"_n}, {get_self(), "active"_n}},
s.token_contract, s.token_contract,
"transfer"_n, "transfer"_n,
std::make_tuple(get_self(), buyer, wire_payout, std::make_tuple(get_self(), buyer, wire_payout,
std::string("wire purchase via OPP")) std::string("wire purchase via OPP"))
).send(); ).send();
available_wire -= wire_output;
} }
// Queue outbound confirmation with the actual WIRE amount delivered // Queue outbound confirmation with the actual WIRE amount delivered