mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 06:01:11 +00:00
feat(amm): add transferOwnership (UpdateConfig admin transfer)
Expose the authority-only UpdateConfig as a module op so the admin can transfer AMM ownership. The guest change (UpdateConfig restricted to the current admin) is already shipped; this is the module wrapper.
This commit is contained in:
@@ -187,6 +187,20 @@ QVariantMap AmmUiBackend::configAccount()
|
||||
return m_logos->amm_module.configAccount();
|
||||
}
|
||||
|
||||
QVariantMap AmmUiBackend::transferOwnership(QVariantMap request)
|
||||
{
|
||||
// Submit guard — this app's wallet-open state is authoritative even though the shared
|
||||
// wallet may remain open elsewhere (same guard as createPool / the swaps).
|
||||
if (!isWalletOpen())
|
||||
return QVariantMap {
|
||||
{ QStringLiteral("status"), QStringLiteral("error") },
|
||||
{ QStringLiteral("error"), QStringLiteral("wallet_unavailable") },
|
||||
};
|
||||
|
||||
// No balance refresh — transferring admin authority doesn't touch token balances.
|
||||
return m_logos->amm_module.transferOwnership(request);
|
||||
}
|
||||
|
||||
QString AmmUiBackend::swapExactInput(QString defAHex, QString defBHex, QString userInputHoldingHex,
|
||||
QString userOutputHoldingHex, QString amountInDecimal,
|
||||
QString minOutDecimal, QString deadlineDecimal)
|
||||
|
||||
@@ -56,6 +56,7 @@ public slots:
|
||||
// AMM — all forwarded to the amm_module core module.
|
||||
QVariantMap resolvePoolAccount(QString defAHex, QString defBHex) override;
|
||||
QVariantMap configAccount() override;
|
||||
QVariantMap transferOwnership(QVariantMap request) override;
|
||||
QString swapExactInput(QString defAHex, QString defBHex, QString userInputHoldingHex,
|
||||
QString userOutputHoldingHex, QString amountInDecimal,
|
||||
QString minOutDecimal, QString deadlineDecimal) override;
|
||||
|
||||
@@ -53,6 +53,12 @@ class AmmUiBackend
|
||||
// ammProgramId, authority, tokenProgramId, twapOracleProgramId }` (ids base58), or
|
||||
// `{ status:"error", error:"config_missing"|"config_unavailable"|"backend_error" }`.
|
||||
SLOT(QVariantMap configAccount())
|
||||
// Submits an UpdateConfig transferring admin authority. `request` carries
|
||||
// { newAuthorityId } (base58 or hex). Only the current admin can sign, so the
|
||||
// connected wallet must control it. Returns { status:"ok", error:"",
|
||||
// transactionId:<hex> } or { status:"error", error:<code> } (wallet_unavailable,
|
||||
// config_missing, invalid_account_id, wallet_submission_failed, backend_error).
|
||||
SLOT(QVariantMap transferOwnership(QVariantMap request))
|
||||
// Submits a real on-chain SwapExactInput transaction against the pool for
|
||||
// (defAHex, defBHex). amountInDecimal/minOutDecimal are decimal-string
|
||||
// u128 amounts in base units; deadlineDecimal is a decimal-string u64 unix
|
||||
|
||||
@@ -50,6 +50,8 @@ char *amm_remove_liquidity_plan(const char *request_json);
|
||||
|
||||
char *amm_sync_reserves_plan(const char *request_json);
|
||||
|
||||
char *amm_transfer_ownership_plan(const char *request_json);
|
||||
|
||||
char *amm_token_holdings(const char *request_json);
|
||||
|
||||
char *amm_program_id(const char *request_json);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
use amm_core::{compute_config_pda, Instruction};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use super::{config::load_config, TransferOwnershipPlanRequest};
|
||||
use crate::account::{account_id_from_hex, account_id_hex, parse_program_id};
|
||||
|
||||
/// Builds the `UpdateConfig` submission that transfers the AMM's admin authority. The current
|
||||
/// admin — the config's stored `authority`, decoded from `config` — is the sole signer;
|
||||
/// `new_authority_id` (hex) becomes the new admin. The guest enforces that only the current admin
|
||||
/// (signed) may call this and that the immutable program ids can't change.
|
||||
pub(super) fn transfer_ownership_plan(
|
||||
request: TransferOwnershipPlanRequest,
|
||||
) -> Result<Value, String> {
|
||||
let amm_program = parse_program_id(&request.amm_program_id)?;
|
||||
let new_authority = account_id_from_hex(&request.new_authority_id, "new authority id")?;
|
||||
let Ok(config) = load_config(amm_program, &request.config) else {
|
||||
return Err(String::from("config_unavailable"));
|
||||
};
|
||||
|
||||
let instruction = risc0_zkvm::serde::to_vec(&Instruction::UpdateConfig { new_authority })
|
||||
.map_err(|error| format!("instruction serialization failed: {error}"))?;
|
||||
|
||||
// Fixed IDL account order for UpdateConfig: the config account (mut, updated in place, not a
|
||||
// signer) and the current admin authority (signs). `new_authority` is instruction data, not
|
||||
// an account.
|
||||
let account_ids = [compute_config_pda(amm_program), config.authority];
|
||||
let signing_requirements = [false, true];
|
||||
|
||||
Ok(json!({
|
||||
"programId": request.amm_program_id,
|
||||
"accountIds": account_ids.into_iter().map(account_id_hex).collect::<Vec<_>>(),
|
||||
"signingRequirements": signing_requirements,
|
||||
"instruction": instruction,
|
||||
}))
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Transport-independent AMM client operations.
|
||||
|
||||
mod admin;
|
||||
mod config;
|
||||
mod context;
|
||||
mod fee;
|
||||
@@ -22,6 +23,7 @@ pub use request::{
|
||||
ProgramIdRequest, RemoveLiquidityPlanRequest, RemoveLiquidityQuoteRequest, ResolvePoolRequest,
|
||||
ResolveTokensRequest, SwapExactInPlanRequest, SwapExactInQuoteRequest, SwapExactOutPlanRequest,
|
||||
SwapExactOutQuoteRequest, SwapPairRequest, SyncReservesPlanRequest, TokenHoldingsRequest,
|
||||
TransferOwnershipPlanRequest,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -146,6 +148,11 @@ pub fn sync_reserves_plan(request: SyncReservesPlanRequest) -> AmmResult {
|
||||
liquidity::sync_reserves_plan(request).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Builds the `UpdateConfig` submission that transfers the AMM's admin authority.
|
||||
pub fn transfer_ownership_plan(request: TransferOwnershipPlanRequest) -> AmmResult {
|
||||
admin::transfer_ownership_plan(request).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Lists the wallet's fungible token holdings for the account selector.
|
||||
pub fn token_holdings(request: TokenHoldingsRequest) -> AmmResult {
|
||||
token_holdings::token_holdings(request).map_err(Into::into)
|
||||
|
||||
@@ -18,6 +18,17 @@ pub struct ConfigAccountRequest {
|
||||
pub config: AccountRead,
|
||||
}
|
||||
|
||||
/// Builds the `UpdateConfig` submission transferring admin authority. `config` is the read of the
|
||||
/// config PDA (the current admin — the sole signer — is decoded from it); `new_authority_id` is
|
||||
/// hex (the module normalizes base58→hex).
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TransferOwnershipPlanRequest {
|
||||
pub amm_program_id: String,
|
||||
pub config: AccountRead,
|
||||
pub new_authority_id: String,
|
||||
}
|
||||
|
||||
/// Resolves an app-provided set of token ids into selector rows. `token_ids` are hex — the
|
||||
/// module normalizes base58→hex and reads each definition into `token_definitions` (keyed by
|
||||
/// hex id) plus the wallet accounts; the FFI is stateless and reads nothing itself.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use alloy_primitives::U256;
|
||||
use amm_core::{
|
||||
compute_config_pda, compute_liquidity_token_pda, compute_lp_lock_holding_pda, compute_pool_pda,
|
||||
compute_vault_pda, isqrt_product, AmmConfig, PoolDefinition, MINIMUM_LIQUIDITY,
|
||||
compute_vault_pda, isqrt_product, AmmConfig, Instruction, PoolDefinition, MINIMUM_LIQUIDITY,
|
||||
};
|
||||
use clock_core::CLOCK_01_PROGRAM_ACCOUNT_ID;
|
||||
use nssa_core::{
|
||||
@@ -14,6 +14,7 @@ use token_core::{TokenDefinition, TokenHolding};
|
||||
use twap_oracle_core::compute_current_tick_account_pda;
|
||||
|
||||
use super::{
|
||||
admin::transfer_ownership_plan,
|
||||
config::config_account as decode_config_account,
|
||||
context::resolve_tokens,
|
||||
holding::{select_holding, SelectedHolding},
|
||||
@@ -21,7 +22,7 @@ use super::{
|
||||
quote::{div_ceil_u256, minimum_opening_pair, Q64},
|
||||
swap::{swap_exact_in_plan, swap_exact_out_plan},
|
||||
ConfigAccountRequest, PairIdsRequest, ResolveTokensRequest, SwapExactInPlanRequest,
|
||||
SwapExactOutPlanRequest,
|
||||
SwapExactOutPlanRequest, TransferOwnershipPlanRequest,
|
||||
};
|
||||
use crate::{
|
||||
account::{account_id_hex, account_read, decode_account, program_id_base58, program_id_bytes},
|
||||
@@ -240,6 +241,44 @@ fn resolve_tokens_returns_lean_rows_held_first_and_omits_unresolvable() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_ownership_plan_targets_config_and_current_admin() {
|
||||
let config_id = compute_config_pda(AMM_PROGRAM);
|
||||
let new_authority = AccountId::new([5; 32]);
|
||||
let plan = transfer_ownership_plan(TransferOwnershipPlanRequest {
|
||||
amm_program_id: amm_program_id(),
|
||||
config: account_read(config_id, &config_account()),
|
||||
new_authority_id: account_id_hex(new_authority),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// Accounts: [config (not signer), current admin (signs)]. The current admin is [7; 32] (the
|
||||
// config_account fixture's authority); new_authority is instruction data, not an account.
|
||||
assert_eq!(
|
||||
plan["accountIds"],
|
||||
json!([
|
||||
account_id_hex(config_id),
|
||||
account_id_hex(AccountId::new([7; 32]))
|
||||
])
|
||||
);
|
||||
assert_eq!(plan["signingRequirements"], json!([false, true]));
|
||||
|
||||
// The instruction decodes back to UpdateConfig { new_authority }.
|
||||
let words: Vec<u32> = plan["instruction"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|word| word.as_u64().unwrap() as u32)
|
||||
.collect();
|
||||
let Instruction::UpdateConfig {
|
||||
new_authority: decoded,
|
||||
} = risc0_zkvm::serde::from_slice(&words).unwrap()
|
||||
else {
|
||||
panic!("expected UpdateConfig");
|
||||
};
|
||||
assert_eq!(decoded, new_authority);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_account_decodes_authority_and_program_ids() {
|
||||
let config_id = compute_config_pda(AMM_PROGRAM);
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::api::{
|
||||
FeeTiersRequest, PairIdsRequest, PoolIdRequest, ProgramIdRequest, RemoveLiquidityPlanRequest,
|
||||
RemoveLiquidityQuoteRequest, ResolvePoolRequest, ResolveTokensRequest, SwapExactInPlanRequest,
|
||||
SwapExactInQuoteRequest, SwapExactOutPlanRequest, SwapExactOutQuoteRequest, SwapPairRequest,
|
||||
SyncReservesPlanRequest, TokenHoldingsRequest,
|
||||
SyncReservesPlanRequest, TokenHoldingsRequest, TransferOwnershipPlanRequest,
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -170,6 +170,11 @@ pub extern "C" fn amm_sync_reserves_plan(request_json: *const c_char) -> *mut c_
|
||||
call::<SyncReservesPlanRequest>(request_json, api::sync_reserves_plan)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn amm_transfer_ownership_plan(request_json: *const c_char) -> *mut c_char {
|
||||
call::<TransferOwnershipPlanRequest>(request_json, api::transfer_ownership_plan)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn amm_token_holdings(request_json: *const c_char) -> *mut c_char {
|
||||
call::<TokenHoldingsRequest>(request_json, api::token_holdings)
|
||||
|
||||
@@ -8,9 +8,10 @@ pub mod api;
|
||||
pub use api::{
|
||||
config_account, config_id, create_pool_plan, create_pool_quote, fee_tiers, pair_ids, pool_id,
|
||||
program_id, resolve_pool, resolve_tokens, swap_exact_in_plan, swap_exact_in_quote,
|
||||
swap_exact_out_plan, swap_exact_out_quote, swap_pair, AccountRead, AmmApiError, AmmResponse,
|
||||
AmmResult, ConfigAccountRequest, ConfigIdRequest, CreatePoolPlanRequest,
|
||||
CreatePoolQuoteRequest, FeeTiersRequest, PairIdsRequest, PoolIdRequest, ProgramIdRequest,
|
||||
ResolvePoolRequest, ResolveTokensRequest, SwapExactInPlanRequest, SwapExactInQuoteRequest,
|
||||
SwapExactOutPlanRequest, SwapExactOutQuoteRequest, SwapPairRequest, WalletAccount,
|
||||
swap_exact_out_plan, swap_exact_out_quote, swap_pair, transfer_ownership_plan, AccountRead,
|
||||
AmmApiError, AmmResponse, AmmResult, ConfigAccountRequest, ConfigIdRequest,
|
||||
CreatePoolPlanRequest, CreatePoolQuoteRequest, FeeTiersRequest, PairIdsRequest, PoolIdRequest,
|
||||
ProgramIdRequest, ResolvePoolRequest, ResolveTokensRequest, SwapExactInPlanRequest,
|
||||
SwapExactInQuoteRequest, SwapExactOutPlanRequest, SwapExactOutQuoteRequest, SwapPairRequest,
|
||||
TransferOwnershipPlanRequest, WalletAccount,
|
||||
};
|
||||
|
||||
@@ -420,6 +420,52 @@ LogosMap AmmModuleImpl::configAccount() {
|
||||
return result.value;
|
||||
}
|
||||
|
||||
LogosMap AmmModuleImpl::transferOwnership(const LogosMap& request) {
|
||||
auto error = [](const std::string& err) {
|
||||
return LogosMap{{"status", "error"}, {"error", err}};
|
||||
};
|
||||
|
||||
const std::string amm_program_id = ammProgramId();
|
||||
if (amm_program_id.empty())
|
||||
return error("config_missing");
|
||||
|
||||
// The plan needs the config account to decode the CURRENT admin (the sole signer).
|
||||
const json config = readConfig(amm_program_id);
|
||||
if (config.is_null())
|
||||
return error("config_missing");
|
||||
|
||||
const std::string new_authority = normalizeAccountId(jStr(request, "newAuthorityId"));
|
||||
if (new_authority.empty())
|
||||
return error("invalid_account_id");
|
||||
|
||||
const FfiResult planResult = call(amm_transfer_ownership_plan, json{
|
||||
{"ammProgramId", amm_program_id},
|
||||
{"config", config},
|
||||
{"newAuthorityId", new_authority},
|
||||
});
|
||||
if (!planResult.ok)
|
||||
return error(planResult.error.empty() ? "backend_error" : planResult.error);
|
||||
const json plan = planResult.value;
|
||||
|
||||
const std::vector<std::string> accounts = jsonStrVec(plan.value("accountIds", json::array()));
|
||||
const std::vector<bool> signers = jsonBoolVec(plan.value("signingRequirements", json::array()));
|
||||
const std::vector<uint8_t> instruction = jsonWordsToLeBytes(plan.value("instruction", json::array()));
|
||||
const std::string program_id = jStr(plan, "programId");
|
||||
|
||||
AMM_TRACE("transferOwnership: SUBMIT programId=" << program_id
|
||||
<< " accounts=" << accounts.size());
|
||||
|
||||
const std::string reply = modules().logos_execution_zone.send_generic_public_transaction(
|
||||
accounts, signers, instruction, program_id);
|
||||
AMM_TRACE("transferOwnership: tx reply=" << reply);
|
||||
|
||||
const auto obj = json::parse(reply, nullptr, /*allow_exceptions=*/false);
|
||||
if (!obj.is_object() || !obj.value("success", false))
|
||||
return error("wallet_submission_failed");
|
||||
|
||||
return LogosMap{{"status", "ok"}, {"error", ""}, {"transactionId", jStr(obj, "tx_hash")}};
|
||||
}
|
||||
|
||||
LogosMap AmmModuleImpl::swapExactInQuote(const std::string& token_in_hex,
|
||||
const std::string& token_out_hex,
|
||||
const nlohmann::json& amount_in,
|
||||
|
||||
@@ -47,6 +47,13 @@ public:
|
||||
/// `backend_error` when the backend FFI call fails.
|
||||
LogosMap configAccount();
|
||||
|
||||
/// Submits an `UpdateConfig` transferring admin authority to `request.newAuthorityId`
|
||||
/// (base58 or hex). Only the current admin can sign, so the connected wallet must control it.
|
||||
/// On success `{ status:"ok", error:"", transactionId:<hex> }`; on failure:
|
||||
/// `{ status:"error", error:<code> }` — `config_missing`, `invalid_account_id`,
|
||||
/// `wallet_submission_failed`, `backend_error`, or a plan code (e.g. `config_unavailable`).
|
||||
LogosMap transferOwnership(const LogosMap& request);
|
||||
|
||||
/// Prices a `SwapExactInput` for the (token_in_hex, token_out_hex) pair:
|
||||
/// reads the pool and returns `{ status:"ok", error:"", expectedOutRaw,
|
||||
/// minReceivedRaw, priceImpactBps }`, oriented and computed server-side via
|
||||
|
||||
Reference in New Issue
Block a user