AMM
deposit_liquidity代码详细解析
向流动性池(Liquidity Pool)存入代币(deposit liquidity) 的逻辑,核心功能是:
- 检查存款者的余额,防止存入超出余额的代币。
- 确保存款的代币对(Token Pair)比例匹配现有流动性池的比例,避免破坏池子的流动性。
- 计算流动性代币(LP Token)的数量,用来衡量存入资金的贡献。
- 执行代币转账,将存款者的代币存入池子。
- 铸造 LP 代币(Liquidity Token),作为流动性提供者的凭证。
1. 防止存款者存入超出自己账户的代币
let mut amount_a = if amount_a > ctx.accounts.depositor_account_a.amount {
ctx.accounts.depositor_account_a.amount
} else {
amount_a
};
let mut amount_b = if amount_b > ctx.accounts.depositor_account_b.amount {
ctx.accounts.depositor_account_b.amount
} else {
amount_b
};
- 这里的
amount_a
和amount_b
是用户尝试存入的代币数量(A/B)。 ctx.accounts.depositor_account_a.amount
和ctx.accounts.depositor_account_b.amount
是用户在钱包中的代币余额。- 如果用户输入的存款金额大于钱包余额,则限制存款金额不超过余额,防止提交无效交易。
2. 确保存款的比例与池子匹配
let pool_a = &ctx.accounts.pool_account_a;
let pool_b = &ctx.accounts.pool_account_b;
// 判断是否是该池子的第一次流动性存入(即池子为空)
let pool_creation = pool_a.amount == 0 && pool_b.amount == 0;
(amount_a, amount_b) = if pool_creation {
// 如果池子是新创建的,则不需要比例匹配,直接存入
(amount_a, amount_b)
} else {
// 计算池子当前的 A/B 代币比例
let ratio = I64F64::from_num(pool_a.amount)
.checked_mul(I64F64::from_num(pool_b.amount))
.unwrap();
// 确保存入的代币按照当前池子的比例存入
if pool_a.amount > pool_b.amount {
(
I64F64::from_num(amount_b)
.checked_mul(ratio)
.unwrap()
.to_num::<u64>(),
amount_b,
)
} else {
(
amount_a,
I64F64::from_num(amount_a)
.checked_div(ratio)
.unwrap()
.to_num::<u64>(),
)
}
};
- 如果流动性池是新建的(即
pool_a.amount == 0 && pool_b.amount == 0
),那么存款者可以自由决定代币 A/B 的比例。 - 如果池子已经存在,则需要按照池子当前的 A/B 比例存入:
ratio = pool_a.amount * pool_b.amount
,用来计算比例。- 依据 哪个代币池子数量更多 来调整存款比例,确保不会破坏流动性池的价格稳定性。
3. 计算 LP 代币的数量
let mut liquidity = I64F64::from_num(amount_a)
.checked_mul(I64F64::from_num(amount_b))
.unwrap()
.sqrt()
.to_num::<u64>();
- LP 代币的数量采用
sqrt(amount_a * amount_b)
计算,这个公式来源于 Uniswap v2 的流动性计算方法,目的是确保两种资产的数量成比例增加时,LP 代币的增长是合理的。
4. 处理最小流动性锁定
if pool_creation {
if liquidity < MINIMUM_LIQUIDITY {
return err!(TutorialError::DepositTooSmall);
}
liquidity -= MINIMUM_LIQUIDITY;
}
- Solana 上的流动性池需要最小 LP 代币锁定(
MINIMUM_LIQUIDITY
),防止流动性池因存款极小而造成计算误差。 - 如果存入的流动性低于最小值,直接返回错误
DepositTooSmall
。 - 如果满足最小流动性要求,则需要 扣除最小流动性,通常这部分 LP 代币会被销毁,以避免套利问题。
这个机制在创建新池时永久锁定一小部分流动性代币,这些代币将永远无法赎回。
5. 转移代币到池子
token::transfer(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.depositor_account_a.to_account_info(),
to: ctx.accounts.pool_account_a.to_account_info(),
authority: ctx.accounts.depositor.to_account_info(),
},
),
amount_a,
)?;
token::transfer(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.depositor_account_b.to_account_info(),
to: ctx.accounts.pool_account_b.to_account_info(),
authority: ctx.accounts.depositor.to_account_info(),
},
),
amount_b,
)?;
- 这里使用 Solana 的 CPI(Cross-Program Invocation)调用
**token::transfer**
,完成存款者代币的转移。 - 将
**amount_a**
和**amount_b**
从用户钱包转移到流动性池中。
6. 计算签名种子,铸造 LP 代币
let authority_bump = ctx.bumps.pool_authority;
let authority_seeds = &[
&ctx.accounts.pool.amm.to_bytes(),
&ctx.accounts.mint_a.key().to_bytes(),
&ctx.accounts.mint_b.key().to_bytes(),
AUTHORITY_SEED,
&[authority_bump],
];
let signer_seeds = &[&authority_seeds[..]];
- 这里构造了
pool_authority
账户的 PDA(程序派生地址)。 bump
值用于确保 PDA 地址正确。signer_seeds
用于后续的 LP 代币铸造。
7. 铸造 LP 代币
token::mint_to(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
MintTo {
mint: ctx.accounts.mint_liquidity.to_account_info(),
to: ctx.accounts.depositor_account_liquidity.to_account_info(),
authority: ctx.accounts.pool_authority.to_account_info(),
},
signer_seeds,
),
liquidity,
)?;
mint_to
铸造 LP 代币(liquidity
)。- 目标地址是
depositor_account_liquidity
(存款者的 LP 代币账户)。 - 使用
signer_seeds
确保**pool_authority**
账户能够正确签署铸造交易。
总结
- 防止存款者超额存款(限制金额不超过钱包余额)。
- 确保存款的比例符合流动性池当前的比例,防止破坏池子平衡。
- 计算 LP 代币的数量,用于衡量存款者提供的流动性份额。
- 执行代币转移,把用户的
Token A
和Token B
存入流动性池。 - 铸造 LP 代币,给存款者发放 LP 代币作为流动性提供者的权益证明。
这样,存款者就完成了一次 向流动性池提供流动性 的操作! 🚀
在流动性池(Liquidity Pool)中,最小流动性(Minimum Liquidity) 的概念是为了防止 流动性池创建者在初始流动性提供后立即提取所有流动性并套利。
swapexacttokensfortokens 代码解析
这个 swap_exact_tokens_for_tokens
函数实现了 恒定乘积做市商(Constant Product Market Maker, CPMM) 逻辑,允许交易者在池子中交换两种代币,同时确保流动性池的乘积(恒定乘积公式)不会降低。
1. 确保用户不会转入超出自己账户余额的资产
let input = if swap_a && input_amount > ctx.accounts.trader_account_a.amount {
ctx.accounts.trader_account_a.amount
} else if !swap_a && input_amount > ctx.accounts.trader_account_b.amount {
ctx.accounts.trader_account_b.amount
} else {
input_amount
};
- 交易者不能提供 超出自己余额的代币。
swap_a == true
,表示trader_account_a
是输入代币(Token A),否则trader_account_b
是输入代币(Token B)。- 如果用户输入
input_amount
超过账户余额,则只使用余额。
2. 计算交易费用
let amm = &ctx.accounts.amm;
let taxed_input = input - input * amm.fee as u64 / 10000;
amm.fee
表示交易费率,假设fee = 30
(0.3%)。- 交易费用计算公式:taxedinput=input−(input×fee/10000)taxedinput=input−(input×fee/10000)
- 例如:
- 如果
input = 1000
,fee = 30
,则taxed_input = 997
(扣除 0.3% 手续费)。
- 如果
3. 计算输出代币数量
let pool_a = &ctx.accounts.pool_account_a;
let pool_b = &ctx.accounts.pool_account_b;
let output = if swap_a {
I64F64::from_num(taxed_input)
.checked_mul(I64F64::from_num(pool_b.amount))
.unwrap()
.checked_div(
I64F64::from_num(pool_a.amount)
.checked_add(I64F64::from_num(taxed_input))
.unwrap(),
)
.unwrap()
} else {
I64F64::from_num(taxed_input)
.checked_mul(I64F64::from_num(pool_a.amount))
.unwrap()
.checked_div(
I64F64::from_num(pool_b.amount)
.checked_add(I64F64::from_num(taxed_input))
.unwrap(),
)
.unwrap()
}
.to_num::<u64>();
- 这里使用 恒定乘积公式 x×y=k 计算输出: output=taxedinput×poolBpoolA+taxedinputoutput=poolA+taxedinputtaxedinput×poolB其中:
pool_A
和pool_B
分别是池子中的两种代币数量。taxed_input
是扣除手续费后的输入代币数量。
4. 保护用户避免滑点影响
if output < min_output_amount {
return err!(TutorialError::OutputTooSmall);
}
- 这里用于 滑点保护,如果
output
小于min_output_amount
,则交易失败,避免用户亏损。
用户可以设置滑点容忍度:通过设置minoutputamount,用户实际上是在设置自己能接受的最大滑点
这为什么是滑点保护:
滑点是指交易执行价格与预期价格之间的差异。在AMM中,每次交易都会改变池中的代币比例,从而改变价格。这段代码通过比较实际输出与用户设定的最小接受输出来保护用户。
有无此机制的区别:
有滑点保护时:
- 用户可以设定接受的最低输出金额
- 如果市场波动导致实际输出低于预期,交易会自动失败
- 防止前置交易攻击(frontrunning)或三明治攻击(sandwich attack)
- 用户不会在价格剧烈波动时遭受大额损失
没有滑点保护时:
- 用户可能收到远低于预期的代币数量
- 交易会在任何条件下执行,即使价格极度不利
- 交易者容易遭受MEV(最大可提取价值)攻击
- 在高波动市场中交易风险显著增加
5. 计算交易前的乘积不变量
let invariant = pool_a.amount * pool_b.amount;
- 记录 交易前 流动性池的乘积
k = x * y
,用于后续验证不变量是否被破坏。
6. 执行代币交换
let authority_bump = ctx.bumps.pool_authority;
let authority_seeds = &[
&ctx.accounts.pool.amm.to_bytes(),
&ctx.accounts.mint_a.key().to_bytes(),
&ctx.accounts.mint_b.key().to_bytes(),
AUTHORITY_SEED,
&[authority_bump],
];
let signer_seeds = &[&authority_seeds[..]];
signer_seeds
生成 PDA(程序派生地址)签名,让pool_authority
作为授权方进行代币转移。
(1) 交易 **A -> B**
if swap_a {
token::transfer(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.trader_account_a.to_account_info(),
to: ctx.accounts.pool_account_a.to_account_info(),
authority: ctx.accounts.trader.to_account_info(),
},
), input,
)?;
token::transfer(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.pool_account_b.to_account_info(),
to: ctx.accounts.trader_account_b.to_account_info(),
authority: ctx.accounts.pool_authority.to_account_info(),
},
signer_seeds,
),
output,
)?;
}
- 第一步:用户将
input
代币 A 发送到池子pool_account_a
。 - 第二步:池子将
output
代币 B 发送到用户trader_account_b
,PDA 需要signer_seeds
进行签名授权。
(2) 交易 **B -> A**
else {
token::transfer(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.pool_account_a.to_account_info(),
to: ctx.accounts.trader_account_a.to_account_info(),
authority: ctx.accounts.pool_authority.to_account_info(),
},
signer_seeds,
),
input,
)?;
token::transfer(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.trader_account_b.to_account_info(),
to: ctx.accounts.pool_account_b.to_account_info(),
authority: ctx.accounts.trader.to_account_info(),
},
),
output,
)?;
}
- 第一步:池子将
output
代币 A 发送到用户trader_account_a
,PDA 需要signer_seeds
进行授权。 - 第二步:用户将
input
代币 B 发送到池子pool_account_b
。
7. 交易日志
msg!(
"Traded {} tokens ({} after fees) for {}",
input,
taxed_input,
output
);
- 记录交易细节,便于调试和分析。
8. 确保交易后 **k = x * y**
依然成立
ctx.accounts.pool_account_a.reload()?;
ctx.accounts.pool_account_b.reload()?;
if invariant > ctx.accounts.pool_account_a.amount * ctx.accounts.pool_account_a.amount {
return err!(TutorialError::InvariantViolated);
}
- 重新加载
pool_account_a
和pool_account_b
账户数据,获取最新代币余额。 - 确保新的
k'
不会低于 交易前的k
,否则会报错solidity return err!(TutorialError::InvariantViolated);
- k:交易 发生之前 的恒定乘积 k=x初始×y初始k=x初始×y初始
- k′:交易 发生之后 的乘积 k′=x最终×y最终k′=x最终×y最终
按理论来说,理想情况下(没有手续费或攻击),应有:
k′=kk′=k
但在实际交易中,Uniswap V2 会收取 0.3% 手续费,导致流动性池的资产 增加,从而:
k′>kk′>k
为什么要防止 k' < k?
如果在交易后出现:
k′<kk′< k
这意味着:
- 资产被多转走了(黑客攻击)
- 计算公式出现错误
- 手续费没有正确扣除
- 流动性被偷偷提取
这是一种直接的漏洞,攻击者可以通过操纵池子使自己获得超额利润。
总结
- 检查用户余额,确保不会转入超过自己拥有的资产。
- 计算交易费用,并扣除手续费。
- 基于恒定乘积公式计算输出代币数量。
- 检查滑点保护,避免用户因价格变化损失过多。
- 记录交易前的乘积不变量,用于后续验证。
- 执行代币交换:
- 交易
A -> B
:用户发送 A,池子返回 B。 - 交易
B -> A
:用户发送 B,池子返回 A。
- 交易
- 记录交易日志,用于调试分析。
- 验证
**k = x * y**
不变量仍然成立,防止代币池出现漏洞。
这个实现符合 Uniswap V2 恒定乘积模型,但没有 流动性提供(LP Token) 相关逻辑,如果你想深入优化,可以考虑 支持动态费率、TWAP 价格预言机等特性 🚀。
withdraw_liquidity 代码解析
这个 withdraw_liquidity
方法的作用是从流动性池(Pool)中提取代币,同时销毁(burn)对应的流动性代币(LP 代币)。下面是详细的解析:
1. 计算 PDA 签名
let authority_bump = ctx.bumps.pool_authority;
let authority_seeds = &[
&ctx.accounts.pool.amm.to_bytes(),
&ctx.accounts.pool.mint_a.key().to_bytes(),
&ctx.accounts.pool.mint_b.key().to_bytes(),
AUTHORITY_SEED,
&[authority_bump],
];
let signer_seeds = &[&authority_seeds[..]];
- 这里构造了
pool_authority
的 PDA(程序派生地址) 作为流动性池的管理者。 signer_seeds
用于后续的token::transfer
CPI 调用,确保池子授权这些转账操作。
2. 计算用户可以提取的 Token A 和 Token B
let amount_a = I64F64::from_num(amount)
.checked_mul(I64F64::from_num(ctx.accounts.pool_account_a.amount))
.unwrap()
.checked_div(I64F64::from_num(ctx.accounts.mint_liquidity.supply + MINIMUM_LIQUIDITY))
.unwrap()
.floor()
.to_num::<u64>();
- 计算
amount_a
,即用户按流动性份额应得的 Token A 数量:amount
:用户想要提取的 LP 代币数量ctx.accounts.pool_account_a.amount
:池子中 Token A 的总量ctx.accounts.mint_liquidity.supply + MINIMUM_LIQUIDITY
:总 LP 代币的供应量(包含最小流动性)
这段代码的数学表达式如下:
amounta=⌊amount×poolaccounta.amountmintliquidity.supply+MINIMUMLIQUIDITY⌋amounta=⌊mintliquidity.supply+MINIMUMLIQUIDITYamount×poolaccounta.amount⌋
类似地,计算 Token B:
let amount_b = I64F64::from_num(amount)
.checked_mul(I64F64::from_num(ctx.accounts.pool_account_b.amount))
.unwrap()
.checked_div(I64F64::from_num(ctx.accounts.mint_liquidity.supply + MINIMUM_LIQUIDITY))
.unwrap()
.floor()
.to_num::<u64>();
3. 从池子转移 Token A 和 Token B
token::transfer(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.pool_account_a.to_account_info(),
to: ctx.accounts.depositor_account_a.to_account_info(),
authority: ctx.accounts.pool_authority.to_account_info(),
},
signer_seeds,
),
amount_a,
)?;
token::transfer
将amount_a
的 Token A 从流动性池转给用户。signer_seeds
确保 PDA 作为授权者。
同样地,Token B 也会被转移:
token::transfer(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.pool_account_b.to_account_info(),
to: ctx.accounts.depositor_account_b.to_account_info(),
authority: ctx.accounts.pool_authority.to_account_info(),
},
signer_seeds,
),
amount_b,
)?;
4. 销毁 LP 代币
token::burn(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Burn {
mint: ctx.accounts.mint_liquidity.to_account_info(),
from: ctx.accounts.depositor_account_liquidity.to_account_info(),
authority: ctx.accounts.depositor.to_account_info(),
},
),
amount,
)?;
token::burn
直接销毁amount
数量的 LP 代币(相当于赎回流动性)。- 注意: 这里的
authority
不是 PDA,而是depositor
(用户自己),所以需要用户持有 LP 代币的授权。
总结
- 计算 PDA 作为池子的管理者,用于授权代币转移。
- 计算用户应得的 Token A 和 Token B。
- 从池子里转移 Token A 和 Token B 给用户。
- 销毁用户提供的 LP 代币,减少流动性份额。
这样,用户就成功地从流动性池中取回了自己提供的资金。
你有具体的疑问吗?
流动性 deposit_liquidity
1. 为什么要有最小流动性?
如果没有最小流动性,攻击者可以执行以下操作:
- 创建流动性池:假设某人创建一个新的流动性池,并向其中存入少量代币 A 和代币 B。
- 获取 100% LP 代币:由于池子里只有他的资产,他将收到 100% 的 LP 代币(代表流动性提供者的权益)。
- 立即提取流动性:然后,他可以立即赎回 LP 代币,取回 所有的 A 和 B 代币,而不会留下任何流动性。
- 导致池子崩溃:这样,其他人无法交易,整个流动性池就形同虚设。
解决方案:销毁最小流动性
为了防止这种行为,通常会设定 最小流动性,例如 1000 个 LP 代币(Uniswap V2 的默认值),并且在 流动性池创建时销毁 这部分 LP 代币,使其无法被取回。
2. 代码解释
在你的代码里,这一部分实现了 最小流动性限制:
// 计算要铸造的 LP 代币数量(流动性份额)
let mut liquidity = I64F64::from_num(amount_a)
.checked_mul(I64F64::from_num(amount_b))
.unwrap()
.sqrt()
.to_num::<u64>();
// 如果是第一次存入流动性(池子为空),要锁定最小流动性
if pool_creation {
if liquidity < MINIMUM_LIQUIDITY {
return err!(TutorialError::DepositTooSmall); // 存入的资产太少,不符合最小要求
}
liquidity -= MINIMUM_LIQUIDITY; // 扣除最小流动性
}
逻辑解读
- 计算流动性(LP 代币数量):
liquidity = sqrt(amount_a * amount_b)
采用 *xy=k** 公式(Uniswap V2)。
- 判断是否是池子的第一次流动性提供:
- 如果
pool_creation == true
,说明池子里还没有流动性。
- 如果
- 检查是否满足最小流动性要求:
- 如果
liquidity < MINIMUM_LIQUIDITY
,直接报错,避免创建一个小额池子。
- 如果
- 扣除最小流动性:
liquidity -= MINIMUM_LIQUIDITY
,减少流动性份额,防止池子被清空。
3. 实际案例
无最小流动性时的攻击(漏洞示例)
如果没有 最小流动性,攻击者可以这样做:
- 创建一个池子,存入 10 SOL 和 10 USDC。
- 获得 100% 的 LP 代币(假设没有最小流动性)。
- 立即赎回 LP 代币,把 10 SOL 和 10 USDC 全部取回。
- 池子彻底崩溃,其他人无法使用。
有最小流动性时
- 初始流动性池子创建时,会销毁 1000 个 LP 代币,即使攻击者想清空流动性池,也必须留下 至少 1000 个 LP 代币的价值,从而增加套利成本。
4. 总结
- 最小流动性 机制用于 防止流动性池的创建者直接清空池子套利。
- 通常销毁 1000 LP 代币(或其他设定值),让创建者无法获取全部资产。
- Uniswap、Sushiswap、Raydium 等 AMM(自动做市商)都采用类似机制。
- 代码中
**liquidity -= MINIMUM_LIQUIDITY;**
就是实现这个机制的关键部分。
MINIMUMLIQUIDITY 最小流动性 withdrawliquidity
^744da1
MINIMUM_LIQUIDITY
是为了防止流动性池被完全抽干,通常用于避免早期流动性提供者占据不公平的优势。它的存在确保了流动性池始终留有一定数量的 LP 代币,即使所有流动性提供者都撤资,也不会让池子彻底归零。
为什么要加上 MINIMUM_LIQUIDITY
?
在流动性池的设计中:
- 第一个提供流动性的人(LP)往往会创建一个初始的池子。
- 如果没有
MINIMUM_LIQUIDITY
,第一个 LP 提供流动性后可以马上撤回所有的流动性,并销毁所有的 LP 代币,从而获得比原始存入金额更多的资产(由于计算精度问题或市场滑点)。这会带来套利漏洞。 MINIMUM_LIQUIDITY
设定一个最低的 LP 代币供应量,确保即使所有流动性提供者都撤出,仍然有一部分 LP 代币锁定,防止攻击者利用流动性池完全抽干的机会套利。
代码层面的作用
let amount_a = I64F64::from_num(amount)
.checked_mul(I64F64::from_num(ctx.accounts.pool_account_a.amount))
.unwrap()
.checked_div(I64F64::from_num(
ctx.accounts.mint_liquidity.supply + MINIMUM_LIQUIDITY, // 这里加了 MINIMUM_LIQUIDITY
))
.unwrap()
.floor()
.to_num::<u64>();
如果不加 **MINIMUM_LIQUIDITY**
:
假设:
- LP 代币总供应量
supply = 100
- 用户想要提取
amount = 100
- 池子中的
pool_account_a.amount = 1000
那么:
amount_a = (100 / 100) * 1000 = 1000
用户会把池子里的所有资产提取走,导致池子归零,这样新用户就无法加入,系统崩溃。
加上 **MINIMUM_LIQUIDITY**
后:
假设 MINIMUM_LIQUIDITY = 10
,那么:
amount_a = (100 / (100 + 10)) * 1000 = (100 / 110) * 1000 ≈ 909
这就避免了所有资产被完全取走,保证池子不会崩溃。
总结
MINIMUM_LIQUIDITY
确保池子不会被完全清空。- 保护了流动性池,防止早期流动性提供者套利。
- 维持池子的稳定性,使未来的交易仍然可以进行。
如果 MINIMUM_LIQUIDITY
让你疑惑,你可以理解为“一个安全阀”,它确保流动性池一直有一些剩余,不会完全归零。
滑点 swapexacttokensfortokens
滑点(Slippage)是指交易执行时,实际成交价格与预期价格之间的差异。在 AMM(自动做市商)交易中,由于流动性池的机制,滑点主要由以下两个因素影响:
- 流动性池的深度:如果池子里流动性较低,大额交易会导致价格较大波动,从而产生较高的滑点。
- 市场波动性:在剧烈波动的市场中,交易确认时的价格可能与用户提交交易时的价格不同,导致滑点。
在 **swap_exact_tokens_for_tokens**
代码中的滑点保护机制
在 AMM 交易中,滑点保护通常通过 用户设置最小可接受的输出数量(min_output_amount
)来实现。
代码片段
// 4. Slip point protection
if output < min_output_amount {
return err!(TutorialError::OutputTooSmall);
}
这部分代码的作用是:
- 计算用户的预期
output
(基于 x * y = k 恒定乘积公式)。 - 检查实际获得的
**output**
是否小于**min_output_amount**
。 - 如果
output < min_output_amount
,说明滑点过大,用户可能遭受较大损失,交易失败并返回OutputTooSmall
错误。
滑点保护的作用
- 保护交易者:避免因为池子深度问题或价格剧烈波动而导致的糟糕成交价格。
- 防止 MEV(最大可提取价值)攻击:如果滑点过大,可能会被套利者利用进行三明治攻击(Sandwich Attack)。
- 提高交易确定性:用户可以通过
min_output_amount
控制自己愿意接受的最差成交价格。
如何在前端设置滑点?
在 DEX(如 Uniswap)前端,通常会让用户手动设置可接受的滑点范围,例如:
- 低滑点(0.1%-0.5%):适用于流动性深的池子,如 ETH/USDC。
- 中等滑点(0.5%-1%):适用于波动较大的交易对。
- 高滑点(1%-3% 甚至更高):适用于流动性较低的池子,例如某些新代币交易对。
前端通常计算:
minOutputAmount = expectedOutput * (1 - slippage / 100)
然后将 minOutputAmount
作为参数传递到 swap_exact_tokens_for_tokens
交易中,以防止交易因滑点过大而遭受损失。
如何添加流动性
总结
滑点保护通过 min_output_amount
限制用户可以接受的最小收益:
- 如果滑点导致实际输出低于
**min_output_amount**
,交易会失败。 - 这可以防止因价格冲击或流动性不足导致的糟糕成交价格。
- 在前端,用户可以设置滑点容忍度,以提高交易的成功率和可预测性。
滑点控制是 DeFi 交易中必不可少的机制,确保交易者不会因市场变化而意外损失过多资金。
无常损失
如何减少无常损失?
无常损失(Impermanent Loss) 是指当流动性提供者将资产存入 AMM 流动性池时,如果池中资产的价格发生变化,流动性提供者的资产会面临价值损失。
- 交易对的选择:无常损失在价格波动较大的交易对中更为显著。如果两种资产的价格波动较小,那么无常损失的影响就会较小。
- 稳定币池:选择如 USDC/USDT 这类稳定币对,它们的价格通常比较稳定,导致无常损失较小。
- 使用其他 AMM:某些 AMM,如 Curve Finance,采用了更复杂的算法,可以有效减少无常损失,尤其是在稳定币对之间进行交易时。
1.集中流动性 (Uniswap V3 风格)
有无区别:
- 有: 流动性提供者可以选择特定价格范围提供流动性,大幅提高资本效率,特定范围内的无常损失减少
- 无: 所有流动性分布在整个价格范围(0,∞),资本效率低,无常损失较大
2.动态费率系统
有无区别:
- 有: 在高波动期间提高费率,增加LP收益以抵消无常损失;低波动时降低费率提高交易吸引力
- 无: 固定费率无法适应不同市场状况,高波动期间LP承受更大无常损失风险
3.混合曲线 (特别适合稳定币对)
有无区别:
- 有: 使用非线性曲线(如x³y+xy³=k)在价格接近1:1时提供更多流动性,大幅减少稳定币交易的无常损失
- 无: 恒定乘积曲线对所有价格点平等对待,稳定币交易效率低
4.无常损失保险机制
有无区别:
- 有: 设立一个保险基金来部分或完全补偿LP的无常损失,增加LP信心
- 无: LP完全承担无常损失风险,减少持续提供流动性的意愿
总结
- 从简单措施开始:
- 首先实现动态费率系统,这是相对简单但有效的方法
- 根据池类型选择曲线:
- 对稳定币对使用混合曲线
- 对普通代币对考虑实现集中流动性
- 综合策略:
- 最有效的方法是结合多种策略
- 例如集中流动性+动态费率+保险机制的组合可以显著减少无常损失的影响
1. 无常损失(Impermanent Loss)
无常损失是 AMM 中的一个常见问题,它是指流动性提供者(LP)由于池内资产价格波动,导致其从池子中退出时获得的价值低于如果直接持有资产的价值。
- 常见问题:
- 无常损失的定义是什么?如何计算?
- 为什么无常损失是 AMM 的一个问题?
- 如何设计策略来减轻无常损失的影响?例如使用对称的池子(如 ETH/USDT)或使用稳定币池。
- 可以通过哪些方式减少无常损失?(如引入稳定币池、做合成资产池等)
- 有没有解决方案或补偿机制可以使流动性提供者不受无常损失的影响?
相关知识点:
- 流动性提供者如何承担风险。
- 如何通过对称交易对(例如:ETH/USDT)减少无常损失。
2. 滑点(Slippage)
滑点是指由于市场波动或交易规模过大,导致预期交易价格与实际成交价格之间的差异。
- 常见问题:
- 滑点是如何计算的?为什么在 AMM 中会发生滑点?
- 如何减少滑点的影响?
- AMM 的
x * y = k
原则如何影响滑点?如何通过流动性来减少滑点? - 在高波动市场中,如何通过设计机制避免过多的滑点?
- 如何防止恶意攻击导致滑点过大(如前置交易攻击等)?
- 滑点和交易量有什么关系?
相关知识点:
- 滑点的计算公式。
- 流动性池的深度和滑点的关系。
- 前置交易和滑点保护。
3. 流动性提供与激励机制
流动性提供者通过提供流动性来赚取交易费用。AMM 协议设计如何吸引流动性提供者,以及如何确保公平的激励机制。
- 常见问题:
- 如何设计一个激励机制,让流动性提供者愿意参与?
- 为什么不一样的 AMM 协议有不同的费用结构?如何设计交易费用?
- 如何确保 LP 能够获得足够的回报以覆盖无常损失?
- 如何设计奖励机制(例如,如何分配 LP 收益)?
- 动态调整交易费用会对 AMM 和流动性提供者产生什么影响?
相关知识点:
- 交易费用模型(例如,Uniswap 的 0.3% 收费)。
- 激励机制的设计,如何确保流动性提供者的利益。
4. 流动性池的设计
AMM 协议使用流动性池来交换资产,设计这些池子是 AMM 关键的一部分。面试官可能会询问池子如何管理流动性,如何优化交易和费用的设计。
- 常见问题:
- 如何设计一个多资产流动性池(例如,Balancer)?
- 在多种资产池中,如何确保价格始终平衡?
- 如何设计一个低滑点的池子?
- AMM 如何处理池子中的资产不对称情况?
- 如何处理代币价格波动对池子内流动性的影响?
相关知识点:
- 多资产池的设计(例如,Balancer)。
- 动态调整流动性池内的代币比例。 ### 5. AMM 的性能优化
AMM 协议往往需要高效的计算和状态更新,以确保交易的顺畅进行。
- 常见问题:
- 如何优化 AMM 协议的 gas 费用?如何减少交易成本?
- 如何通过算法优化 AMM 协议中的交易计算?
- 如何处理 AMM 系统的交易拥堵问题?在高并发时如何保证性能?
- 如何进行 AMM 协议的安全性审计?
相关知识点:
- Gas 费用优化(例如,避免不必要的状态变化,减少复杂度)。
- 高并发情况下的性能问题与优化策略。
6. 自由市场与 AMM 的对比
传统的订单簿交易和 AMM 的交易机制不同,面试官可能会问到这两者的优缺点以及如何决定何时使用 AMM。
- 常见问题:
- AMM 和传统的订单簿交易所(如 Coinbase)有什么区别?
- 为什么 AMM 在去中心化交易所中很受欢迎?
- AMM 相比于订单簿交易所有哪些优势和劣势?
- AMM 如何解决传统市场中的流动性问题?
相关知识点:
- AMM 的优势和劣势(去中心化、流动性提供者激励、滑点等)。
- 订单簿交易所与 AMM 的比较。
7. 安全性和攻击防范
AMM 协议是去中心化的,攻击者可能尝试利用协议的漏洞进行攻击(例如,前置交易、重入攻击等)。面试官可能会询问如何增强 AMM 协议的安全性。
- 常见问题:
- AMM 协议如何防止重入攻击?
- 如何防止恶意交易者通过前置交易影响 AMM?
- AMM 如何避免闪电贷攻击?
- 如何保证交易的公平性和安全性?
相关知识点:
- 重入攻击、前置交易攻击、闪电贷攻击等安全问题。
- 安全性优化策略和审计过程。
8. 其他高级问题
在一些高级面试中,可能会涉及一些技术实现和优化问题,包括算法、数据结构、去中心化治理等。
- 常见问题:
- 如何为 AMM 实现自适应定价机制?
- 如何设计一个具有跨链能力的 AMM 协议?
- 如何在 AMM 协议中实现动态资产配置?
- 去中心化治理在 AMM 协议中的作用?
相关知识点:
- 自适应定价、跨链 AMM、去中心化治理。