AMM и формула x*y=k, пулы ликвидности, стейкинг-контракты, оракулы цен, flash loans
DeFi перестроил финансовую систему на открытом коде. Понимание AMM и пулов ликвидности — основа для любого Web3-разработчика
Традиционная биржа использует книгу ордеров (order book): покупатели и продавцы выставляют заявки, матчинг-движок их сводит. Это эффективно, но требует централизованного оператора.
DEX (Decentralized Exchange) — биржа на смарт-контрактах. AMM (Automated Market Maker) — алгоритмический маркет-мейкер без книги ордеров. Цена определяется математической формулой на основе соотношения активов в пуле ликвидности.
Основа Uniswap v1/v2 и множества других AMM. Принцип: произведение резервов двух токенов остаётся константным.
Если пул содержит x единиц токена A и y единиц токена B, то после любого свопа x' * y' = k = x * y.
Покупая dA токенов A, вы добавляете dB токенов B. Формула определяет сколько именно dA вы получите:
x * y = k
(x - dA) * (y + dB) = k
dA = x - k / (y + dB)
Эта математика автоматически регулирует цену: чем больше покупаешь A, тем дороже становится A (скользящая цена — price impact).
Ликвидность обеспечивают LP (Liquidity Providers). Они вносят оба токена в равных долях по текущей рыночной цене и получают LP-токены, подтверждающие долю в пуле.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
// Упрощённый AMM — только иллюстрация механизма
contract SimpleAMM {
IERC20 public tokenA;
IERC20 public tokenB;
uint256 public reserveA;
uint256 public reserveB;
// LP shares
mapping(address => uint256) public shares;
uint256 public totalShares;
constructor(address _tokenA, address _tokenB) {
tokenA = IERC20(_tokenA);
tokenB = IERC20(_tokenB);
}
// Добавление ликвидности
function addLiquidity(uint256 amountA, uint256 amountB) external returns (uint256 mintedShares) {
tokenA.transferFrom(msg.sender, address(this), amountA);
tokenB.transferFrom(msg.sender, address(this), amountB);
if (totalShares == 0) {
// Первое добавление: shares = sqrt(amountA * amountB)
mintedShares = sqrt(amountA * amountB);
} else {
// Последующие: пропорционально существующим резервам
mintedShares = min(
amountA * totalShares / reserveA,
amountB * totalShares / reserveB
);
}
shares[msg.sender] += mintedShares;
totalShares += mintedShares;
reserveA += amountA;
reserveB += amountB;
}
// Своп tokenA -> tokenB
function swapAtoB(uint256 amountAIn) external returns (uint256 amountBOut) {
require(amountAIn > 0, "Zero input");
// Формула: (reserveA + amountAIn) * amountBOut = reserveA * reserveB
// С комиссией 0.3%: амортизируем 99.7% от input
uint256 amountAWithFee = amountAIn * 997;
amountBOut = (amountAWithFee * reserveB) / (reserveA * 1000 + amountAWithFee);
require(amountBOut > 0, "Insufficient output");
tokenA.transferFrom(msg.sender, address(this), amountAIn);
tokenB.transfer(msg.sender, amountBOut);
reserveA += amountAIn;
reserveB -= amountBOut;
}
function sqrt(uint256 x) internal pure returns (uint256) {
if (x == 0) return 0;
uint256 z = (x + 1) / 2;
uint256 y = x;
while (z < y) { y = z; z = (x / z + z) / 2; }
return y;
}
function min(uint256 a, uint256 b) internal pure returns (uint256) {
return a < b ? a : b;
}
}LP провайдеры несут риск непостоянных потерь. Когда цена токена резко меняется по сравнению с моментом внесения ликвидности, стоимость активов LP оказывается ниже, чем если бы они просто держали токены (HODL).
Пример: вы вносите 1 ETH + 1000 USDC (цена ETH = $1000). Цена ETH вырастает до $4000. AMM балансирует пул: теперь там 0.5 ETH + 2000 USDC. Ваши активы стоят $4000, но если бы держали — 1 ETH + 1000 USDC = $5000. Потеря $1000 — это impermanent loss. "Непостоянная" — потому что если цена вернётся к $1000, потеря исчезнет.
Базовый стейкинг: пользователи блокируют токены и получают вознаграждение пропорционально времени и сумме.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract Staking is ReentrancyGuard {
IERC20 public immutable stakingToken;
IERC20 public immutable rewardToken;
uint256 public rewardRate; // токены награды в секунду
uint256 public lastUpdateTime;
uint256 public rewardPerTokenStored;
mapping(address => uint256) public userRewardPerTokenPaid;
mapping(address => uint256) public rewards;
mapping(address => uint256) public balances;
uint256 public totalSupply;
constructor(address _staking, address _reward, uint256 _rewardRate) {
stakingToken = IERC20(_staking);
rewardToken = IERC20(_reward);
rewardRate = _rewardRate;
}
// Accumulated reward per staked token
function rewardPerToken() public view returns (uint256) {
if (totalSupply == 0) return rewardPerTokenStored;
return rewardPerTokenStored +
(rewardRate * (block.timestamp - lastUpdateTime) * 1e18) / totalSupply;
}
// Сколько наград накопил пользователь
function earned(address account) public view returns (uint256) {
return (balances[account] * (rewardPerToken() - userRewardPerTokenPaid[account])) / 1e18
+ rewards[account];
}
modifier updateReward(address account) {
rewardPerTokenStored = rewardPerToken();
lastUpdateTime = block.timestamp;
if (account != address(0)) {
rewards[account] = earned(account);
userRewardPerTokenPaid[account] = rewardPerTokenStored;
}
_;
}
function stake(uint256 amount) external nonReentrant updateReward(msg.sender) {
totalSupply += amount;
balances[msg.sender] += amount;
stakingToken.transferFrom(msg.sender, address(this), amount);
}
function withdraw(uint256 amount) external nonReentrant updateReward(msg.sender) {
totalSupply -= amount;
balances[msg.sender] -= amount;
stakingToken.transfer(msg.sender, amount);
}
function getReward() external nonReentrant updateReward(msg.sender) {
uint256 reward = rewards[msg.sender];
if (reward > 0) {
rewards[msg.sender] = 0;
rewardToken.transfer(msg.sender, reward);
}
}
}Смарт-контракт не может сам получить цену токена — нужен оракул.
Spot price из AMM (простая цена из пула) — уязвима к flash loan манипуляциям. Рекомендуется использовать Uniswap v3 TWAP (Time-Weighted Average Price) или Chainlink Price Feed.
Chainlink — декентрализованная сеть оракулов, которые агрегируют цены с бирж и публикуют их on-chain. Самый надёжный вариант для production. Для чтения цены достаточно одного view-вызова к AggregatorV3Interface.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.