mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 06:01:11 +00:00
fix(amm): avoid redundant output holding signatures
This commit is contained in:
@@ -108,6 +108,9 @@ pub struct SwapExactInPlanRequest {
|
||||
pub config: AccountRead,
|
||||
pub user_input_holding_id: String,
|
||||
pub user_output_holding_id: String,
|
||||
/// Current read of the output holding. The plan uses its owner to avoid
|
||||
/// requesting a signature for an already initialized Token holding.
|
||||
pub user_output_holding: AccountRead,
|
||||
pub amount_in: String,
|
||||
pub min_out: String,
|
||||
pub deadline_ms: String,
|
||||
@@ -126,6 +129,9 @@ pub struct SwapExactOutPlanRequest {
|
||||
pub config: AccountRead,
|
||||
pub user_input_holding_id: String,
|
||||
pub user_output_holding_id: String,
|
||||
/// Current read of the output holding. The plan uses its owner to avoid
|
||||
/// requesting a signature for an already initialized Token holding.
|
||||
pub user_output_holding: AccountRead,
|
||||
pub amount_out: String,
|
||||
pub max_in: String,
|
||||
pub deadline_ms: String,
|
||||
|
||||
@@ -7,7 +7,7 @@ use amm_core::{
|
||||
compute_pool_pda, mul_div_ceil, mul_div_floor, price_impact_bps, swap_exact_in_amounts,
|
||||
swap_exact_out_amounts, PoolDefinition, FEE_BPS_DENOMINATOR,
|
||||
};
|
||||
use nssa_core::account::AccountId;
|
||||
use nssa_core::{account::AccountId, program::ProgramId};
|
||||
use risc0_binfmt::ProgramBinary;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
@@ -18,8 +18,20 @@ use super::{
|
||||
};
|
||||
use crate::account::{
|
||||
account_id_from_hex, account_id_hex, decode_account, parse_program_id, program_id_bytes,
|
||||
AccountRead,
|
||||
};
|
||||
|
||||
fn output_holding_requires_signature(
|
||||
read: &AccountRead,
|
||||
expected_id: AccountId,
|
||||
token_program_id: ProgramId,
|
||||
) -> bool {
|
||||
let Ok((account_id, account)) = decode_account(read) else {
|
||||
return true;
|
||||
};
|
||||
account_id != expected_id || account.program_owner != token_program_id
|
||||
}
|
||||
|
||||
/// Orders `(token_in, token_out)` into the pool's canonical `(token_a, token_b)`
|
||||
/// so derived vault PDAs line up with the pool's stored `vault_a`/`vault_b`.
|
||||
fn canonical_pair(token_in: AccountId, token_out: AccountId) -> (AccountId, AccountId) {
|
||||
@@ -330,7 +342,21 @@ pub(super) fn swap_exact_in_plan(request: SwapExactInPlanRequest) -> Result<Valu
|
||||
pair.current_tick,
|
||||
pair.clock,
|
||||
];
|
||||
let signing_requirements = [false, false, false, false, true, true, false, false];
|
||||
let output_requires_signature = output_holding_requires_signature(
|
||||
&request.user_output_holding,
|
||||
user_output_holding,
|
||||
pair.token_program,
|
||||
);
|
||||
let signing_requirements = [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
output_requires_signature,
|
||||
false,
|
||||
false,
|
||||
];
|
||||
|
||||
Ok(json!({
|
||||
"programId": request.amm_program_id,
|
||||
@@ -397,7 +423,21 @@ pub(super) fn swap_exact_out_plan(request: SwapExactOutPlanRequest) -> Result<Va
|
||||
pair.current_tick,
|
||||
pair.clock,
|
||||
];
|
||||
let signing_requirements = [false, false, false, false, true, true, false, false];
|
||||
let output_requires_signature = output_holding_requires_signature(
|
||||
&request.user_output_holding,
|
||||
user_output_holding,
|
||||
pair.token_program,
|
||||
);
|
||||
let signing_requirements = [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
output_requires_signature,
|
||||
false,
|
||||
false,
|
||||
];
|
||||
|
||||
Ok(json!({
|
||||
"programId": request.amm_program_id,
|
||||
@@ -441,6 +481,54 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_holding_signature_requires_fresh_or_unreadable_account() {
|
||||
let output_id = AccountId::new([0xAB; 32]);
|
||||
let token_program = [22; 8];
|
||||
let initialized = AccountRead {
|
||||
id: account_id_hex(output_id),
|
||||
status: String::from("ok"),
|
||||
account: Some(WalletAccount {
|
||||
program_owner: crate::account::program_id_hex(token_program),
|
||||
balance: "00".repeat(16),
|
||||
nonce: "00".repeat(16),
|
||||
data: String::new(),
|
||||
}),
|
||||
};
|
||||
assert!(!output_holding_requires_signature(
|
||||
&initialized,
|
||||
output_id,
|
||||
token_program,
|
||||
));
|
||||
|
||||
let fresh = AccountRead {
|
||||
id: account_id_hex(output_id),
|
||||
status: String::from("ok"),
|
||||
account: Some(WalletAccount {
|
||||
program_owner: "00".repeat(32),
|
||||
balance: "00".repeat(16),
|
||||
nonce: "00".repeat(16),
|
||||
data: String::new(),
|
||||
}),
|
||||
};
|
||||
assert!(output_holding_requires_signature(
|
||||
&fresh,
|
||||
output_id,
|
||||
token_program,
|
||||
));
|
||||
|
||||
let unreadable = AccountRead {
|
||||
id: account_id_hex(output_id),
|
||||
status: String::from("read_failed"),
|
||||
account: None,
|
||||
};
|
||||
assert!(output_holding_requires_signature(
|
||||
&unreadable,
|
||||
output_id,
|
||||
token_program,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_pool_reports_canonical_token_ids() {
|
||||
let def_a = AccountId::new([0xAA; 32]);
|
||||
@@ -515,6 +603,11 @@ mod tests {
|
||||
config: dummy_config,
|
||||
user_input_holding_id: String::new(),
|
||||
user_output_holding_id: String::new(),
|
||||
user_output_holding: AccountRead {
|
||||
id: String::new(),
|
||||
status: String::from("read_failed"),
|
||||
account: None,
|
||||
},
|
||||
amount_in: String::new(),
|
||||
min_out: String::new(),
|
||||
deadline_ms: String::new(),
|
||||
@@ -540,6 +633,11 @@ mod tests {
|
||||
},
|
||||
user_input_holding_id: String::new(),
|
||||
user_output_holding_id: String::new(),
|
||||
user_output_holding: AccountRead {
|
||||
id: String::new(),
|
||||
status: String::from("read_failed"),
|
||||
account: None,
|
||||
},
|
||||
amount_in: String::new(),
|
||||
min_out: String::new(),
|
||||
deadline_ms: String::new(),
|
||||
@@ -566,6 +664,11 @@ mod tests {
|
||||
config: dummy_config,
|
||||
user_input_holding_id: String::new(),
|
||||
user_output_holding_id: String::new(),
|
||||
user_output_holding: AccountRead {
|
||||
id: String::new(),
|
||||
status: String::from("read_failed"),
|
||||
account: None,
|
||||
},
|
||||
amount_out: String::new(),
|
||||
max_in: String::new(),
|
||||
deadline_ms: String::new(),
|
||||
|
||||
@@ -859,6 +859,7 @@ fn swap_plan_uses_the_pool_stored_vaults_not_canonical_order() {
|
||||
config: account_read(compute_config_pda(AMM_PROGRAM), &config_account()),
|
||||
user_input_holding_id: account_id_hex(holding),
|
||||
user_output_holding_id: account_id_hex(holding),
|
||||
user_output_holding: default_read(holding),
|
||||
amount_in: String::from("100"),
|
||||
min_out: String::from("0"),
|
||||
deadline_ms: String::from("0"),
|
||||
@@ -896,6 +897,7 @@ fn swap_exact_in_plan_missing_pool_fails_closed_with_err() {
|
||||
config: account_read(compute_config_pda(AMM_PROGRAM), &config_account()),
|
||||
user_input_holding_id: account_id_hex(holding),
|
||||
user_output_holding_id: account_id_hex(holding),
|
||||
user_output_holding: default_read(holding),
|
||||
amount_in: String::from("100"),
|
||||
min_out: String::from("0"),
|
||||
deadline_ms: String::from("0"),
|
||||
@@ -934,6 +936,7 @@ fn swap_exact_out_plan_uses_the_pool_stored_vaults_not_canonical_order() {
|
||||
config: account_read(compute_config_pda(AMM_PROGRAM), &config_account()),
|
||||
user_input_holding_id: account_id_hex(holding),
|
||||
user_output_holding_id: account_id_hex(holding),
|
||||
user_output_holding: default_read(holding),
|
||||
amount_out: String::from("100"),
|
||||
max_in: String::from("1000"),
|
||||
deadline_ms: String::from("0"),
|
||||
|
||||
@@ -622,6 +622,7 @@ std::string AmmModuleImpl::swapExactInput(const std::string& def_a_hex,
|
||||
}
|
||||
const json pool = readPublicAccount(jStr(poolId.value, "poolId"));
|
||||
const std::string pool_data = jStr(pool.value("account", json::object()), "data");
|
||||
const json user_output_holding = readPublicAccount(user_output_holding_hex);
|
||||
|
||||
// amm_swap_exact_in_plan resolves the pool accounts, encodes SwapExactInput,
|
||||
// and returns a ready-to-submit plan.
|
||||
@@ -633,6 +634,7 @@ std::string AmmModuleImpl::swapExactInput(const std::string& def_a_hex,
|
||||
{"poolData", pool_data},
|
||||
{"userInputHoldingId", user_input_holding_hex},
|
||||
{"userOutputHoldingId", user_output_holding_hex},
|
||||
{"userOutputHolding", user_output_holding},
|
||||
{"amountIn", amount_in_decimal},
|
||||
{"minOut", min_out_decimal},
|
||||
{"deadlineMs", deadline_decimal},
|
||||
@@ -705,6 +707,7 @@ std::string AmmModuleImpl::swapExactOutput(const std::string& def_a_hex,
|
||||
}
|
||||
const json pool = readPublicAccount(jStr(poolId.value, "poolId"));
|
||||
const std::string pool_data = jStr(pool.value("account", json::object()), "data");
|
||||
const json user_output_holding = readPublicAccount(user_output_holding_hex);
|
||||
|
||||
// amm_swap_exact_out_plan resolves the pool accounts, encodes SwapExactOutput,
|
||||
// and returns a ready-to-submit plan.
|
||||
@@ -716,6 +719,7 @@ std::string AmmModuleImpl::swapExactOutput(const std::string& def_a_hex,
|
||||
{"poolData", pool_data},
|
||||
{"userInputHoldingId", user_input_holding_hex},
|
||||
{"userOutputHoldingId", user_output_holding_hex},
|
||||
{"userOutputHolding", user_output_holding},
|
||||
{"amountOut", amount_out_decimal},
|
||||
{"maxIn", max_in_decimal},
|
||||
{"deadlineMs", deadline_decimal},
|
||||
|
||||
@@ -203,12 +203,12 @@ pub fn swap_exact_input(
|
||||
assert_user_holding_owner_or_fresh(
|
||||
&user_holding_a,
|
||||
token_program_id,
|
||||
"User Token A holding must be owned by the configured Token Program",
|
||||
"User Token A holding must be owned by the configured Token Program or be a fresh authorized account",
|
||||
);
|
||||
assert_user_holding_owner_or_fresh(
|
||||
&user_holding_b,
|
||||
token_program_id,
|
||||
"User Token B holding must be owned by the configured Token Program",
|
||||
"User Token B holding must be owned by the configured Token Program or be a fresh authorized account",
|
||||
);
|
||||
// The current tick is refreshed by a chained call to the oracle; validate its PDA and the
|
||||
// clock here so the swap is rejected early with an AMM-level error.
|
||||
|
||||
Reference in New Issue
Block a user