用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/oyi77/1ai-skills --skill defi-protocols命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
正在显示 SKILL.md
基于 SOC 职业分类
| name | defi-protocols |
| description | Skill: defi-protocols. See SKILL.md body for details. Use when this domain is relevant. |
| domain | trading |
| author | oyi77 |
| license | Apache-2.0 |
| subdomain | trading |
| tags | ["algorithms","defi","markets","protocols","trading"] |
| version | 1.0.0 |
DeFi protocol patterns for Automated Market Makers (AMMs), lending pools, flash loans, yield farming, order-book DEXs, cross-chain bridges, and oracle integrations. Covers the mathematical foundations, Solidity implementations, Hardhat fork-testing patterns, and Web3.py/Ethers.js interaction code for each primitive. Security invariants (reentrancy, price manipulation, MEV) are treated as first-class design constraints, not afterthoughts.
Each section provides the economic invariant, the Solidity implementation pattern, a concrete test or interaction script, and the attack surface specific to that primitive.
Trigger phrases:
Matching signal: The task involves implementing, auditing, extending, or interacting with any of the core DeFi primitives below. The skill covers both the math and the production Solidity with tests.
The Uniswap V2 invariant: reserve0 * reserve1 = k. A trade that adds dx of token0 and receives dy of token1 must satisfy:
(reserve0 + dx * 0.997) * (reserve1 - dy) = reserve0 * reserve1
The 0.3% fee is extracted before the trade reaches the curve. The output formula:
dy = (dx * 997 * reserve1) / (reserve0 * 1000 + dx * 997)
// Uniswap V2-style pair swap
function swap(uint256 amountIn, address tokenIn, address tokenOut, uint256 minOut) external {
(uint256 rIn, uint256 rOut) = getReserves(tokenIn, tokenOut);
// amountOut = amountIn * 997 * rOut / (rIn * 1000 + amountIn * 997)
uint256 amountOut = (amountIn * 997 * rOut) / (rIn * 1000 + amountIn * 997);
require(amountOut >= minOut, "slippage");
require(amountOut < rOut, "insufficient liquidity");
// transfer in
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
// transfer out
IERC20(tokenOut).safeTransfer(msg.sender, amountOut);
// update reserves
_update(afterBalance(token0), afterBalance(token1));
}
function getReserves(address t0, address t1) internal view returns (uint256, uint256) {
return t0 < t1
? (reserve0, reserve1)
: (reserve1, reserve0);
}
Spot price from reserves: P = reserve1 / reserve0 (price of token0 in terms of token1). Executing a trade shifts the price proportionally to trade size. After selling dx, the new spot price:
P_new = reserve1 / (reserve0 + dx * 0.997) = reserve1 / (newReserve0)
async function getPoolPrice(pairAddress, provider) {
const pair = new ethers.Contract(pairAddress, PAIR_ABI, provider);
const [r0, r1] = await pair.getReserves();
const token0 = await pair.token0();
const price = token0.toLowerCase() < someToken.toLowerCase()
? Number(r1) / Number(r0)
: Number(r0) / Number(r1);
return price;
}
from web3 import Web3
def simulate_swap(amount_in_wei: int, reserve_in: int, reserve_out: int) -> int:
"""Calculate output for a Uniswap V2 swap (0.3% fee)."""
amount_in_with_fee = amount_in_wei * 997
numerator = amount_in_with_fee * reserve_out
denominator = reserve_in * 1000 + amount_in_with_fee
return numerator // denominator
# Example: swap 1000 USDC (6 decimals) into ETH
usdc_in = 1000 * 10**6
r_usdc = 5_000_000 * 10**6 # 5M USDC in pool
r_eth = 2_500 * 10**18 # 2500 ETH in pool
eth_out = simulate_swap(usdc_in, r_usdc, r_eth)
print(f"Output: {eth_out / 10**18:.6f} ETH")
Balancer generalizes constant product to n tokens with weights w_i:
prod(reserve_i ^ w_i) = k
For a two-token pool with weights 80/20:
r0 ^ 0.8 * r1 ^ 0.2 = k
Spot price with weights: P = (r0 * w1) / (r1 * w0)
Trading one token for another changes the price differently than a 50/50 pool. Weighted pools allow LPs to concentrate risk exposure while still earning fees.
Curve combines constant product and constant sum for correlated assets (USDC/USDT):
x * y * (x^2 + y^2) = k (simplified two-peg)
More generally: A * n^n * sum(x_i) + D = A * D * n^n + D^(n+1) / (n^n * prod(x_i))
Where A is the amplification coefficient — higher A means flatter curve near peg, less slippage for stable pairs. The D invariant converges to total liquidity when all tokens are at peg, and to constant product when far from peg.
LPs provide liquidity within discrete price ranges [p_a, p_b]. L = sqrt(k). The virtual reserves adjust so liquidity only depletes when price exits the range.
// V3 tick math
function sqrtPriceX96ToPrice(uint160 sqrtPriceX96, uint256 decimals0, uint256 decimals1)
internal pure returns (uint256)
{
// sqrt(P) is stored as Q64.96: sqrtPriceX96 = sqrt(P) * 2^96
uint256 sqrtPrice = uint256(sqrtPriceX96);
uint256 priceX192 = sqrtPrice * sqrtPrice;
return priceX192 * 10**decimals0 / (2**192 * 10**decimals1);
}
Position value within a range:
amount0 = L * (1 / sqrt(p_low) - 1 / sqrt(p_high))
amount1 = L * (sqrt(p_high) - sqrt(p_low))
Impermanent loss is concentrated: if price exits the range, the position becomes fully denomimated in the cheaper token.
IL occurs when LP return diverges from HODL return. For a price change factor r = P_new / P_old in a constant-product pool:
IL(r) = (2 * sqrt(r)) / (1 + r) - 1
| Price Change | IL |
|---|---|
| 1.25x (25% up) | -0.6% |
| 1.5x (50% up) | -2.0% |
| 2x (100% up) | -5.7% |
| 3x (200% up) | -13.4% |
| 4x (300% up) | -20.0% |
def impermanent_loss(price_ratio: float) -> float:
"""IL as fraction of HODL value. r = new_price / old_price."""
return (2 * (price_ratio ** 0.5)) / (1 + price_ratio) - 1
IL is symmetric: a 50% drop (-50%) has the same IL as a 100% gain (+100%). Fee income offsets IL over time; the breakeven period depends on pool volume and fee tier.
// Given pool reserves and trade size, compute price impact
function priceImpact(uint256 amountIn, uint256 reserveIn, uint256 reserveOut)
internal pure returns (uint256 bp)
{
// returns basis points of price impact
uint256 kPrev = reserveIn * reserveOut;
uint256 newReserveIn = reserveIn + amountIn;
uint256 newReserveOut = (kPrev * 1000) / (newReserveIn * 997); // approximate
uint256 realizedPrice = (amountIn * 1e18) / (reserveOut - newReserveOut);
uint256 spotPrice = (reserveIn * 1e18) / reserveOut;
bp = (realizedPrice * 10000) / spotPrice - 10000;
}
Accumulate price * elapsed at the first transaction of each block:
// In _update()
(uint112 r0, uint112 r1, ) = getReserves();
blockTimestampLast = blockTimestamp;
price0CumulativeLast += r1 * elapsed / r0; // price of token0 in token1
price1CumulativeLast += r0 * elapsed / r1;
// Consumer computes TWAP over a window
function consult(address token, uint256 amountIn, uint256 window)
external view returns (uint256 amountOut)
{
uint256 elapsed = blockTimestamp - observationTimestamp;
uint256 priceCumulative = price0CumulativeLast - price0Cumulative;
uint256 twap = priceCumulative / elapsed;
amountOut = amountIn * twap / 1e18; // assuming 18-decimal tokens
}
V3 stores an array of observations (tick, timestamp) per pool. Each observation is a tickCumulative accumulator. Oracle subgraph queries fetch two observations and compute average tick:
// ethers.js — get V3 TWAP
async function getV3Twap(pool: string, windowSeconds: number, provider: Provider) {
const contract = new ethers.Contract(pool, V3_POOL_ABI, provider);
const obs = await contract.observations(await contract.slot0());
// simplified: observe() returns (tickCumulatives, secondsPerLiquidity)
const [tickCumulatives] = await contract.observe([0, windowSeconds]);
const avgTick = Number(tickCumulatives[0] - tickCumulatives[1]) / windowSeconds;
return 1.0001 ** avgTick;
}
Never use spot price from a single pool as an oracle. Always use TWAP over >= 30 minutes.
Borrowers deposit collateral (e.g., ETH), then borrow up to a loan-to-value (LTV) ratio. The core state:
// Simplified lending pool
contract LendingPool {
mapping(address => mapping(address => uint256)) public deposits; // user => asset => amount
mapping(address => mapping(address => uint256)) public borrows; // user => asset => amount
mapping(address => uint256) public totalLiquidity; // asset => total supplied
mapping(address => uint256) public totalBorrows; // asset => total borrowed
uint256 constant LTV = 7500; // 75% in basis points
uint256 constant LIQUIDATION_THRESHOLD = 8250; // 82.5%
uint256 constant LIQUIDATION_BONUS = 500; // 5%
function deposit(address asset, uint256 amount) external {
IERC20(asset).safeTransferFrom(msg.sender, address(this), amount);
deposits[msg.sender][asset] += amount;
totalLiquidity[asset] += amount;
}
function borrow(address asset, uint256 amount, address collateral) external {
uint256 collateralValue = getOraclePrice(collateral) * deposits[msg.sender][collateral];
uint256 borrowValue = getOraclePrice(asset) * (borrows[msg.sender][asset] + amount);
require(borrowValue * 10000 <= collateralValue * LTV, "over LTV");
borrows[msg.sender][asset] += amount;
totalBorrows[asset] += amount;
IERC20(asset).safeTransfer(msg.sender, amount);
}
}
healthFactor = (collateralValue * liquidationThreshold) / (borrowValue * 10000)
When healthFactor < 1.0, the position is liquidatable.
function getHealthFactor(address user) public view returns (uint256) {
(uint256 totalCollateralETH, uint256 totalDebtETH) = getUserAccountData(user);
if (totalDebtETH == 0) return type(uint256).max;
return (totalCollateralETH * LIQUIDATION_THRESHOLD) / (totalDebtETH * 10000);
}
function liquidationCall(
address collateral, address debt, address user, uint256 debtToCover, bool receiveAToken
) external {
uint256 healthFactor = getHealthFactor(user);
require(healthFactor < 1e18, "healthy");
// Liquidator repays debt, receives collateral at a discount
uint256 collateralAmount = debtToCover * getOraclePrice(debt) * (10000 + LIQUIDATION_BONUS)
/ (getOraclePrice(collateral) * 10000);
IERC20(debt).safeTransferFrom(msg.sender, address(this), debtToCover);
IERC20(collateral).safeTransfer(msg.sender, collateralAmount);
// update state
deposits[user][collateral] -= collateralAmount;
borrows[user][debt] -= debtToCover;
}
Linear model: borrowRate = baseRate + slope * utilization
Jump rate (Aave V2): Below the optimal utilization U_opt, the slope is moderate. Above U_opt, the slope spikes to incentivize new deposits and penalize further borrowing.
// Jump rate model
function calculateBorrowRate(uint256 utilization) public pure returns (uint256) {
uint256 U_OPTIMAL = 8000; // 80%
uint256 BASE_RATE = 0; // 0%
uint256 SLOPE1 = 400; // 4% APR
uint256 SLOPE2 = 30000 * 1e9; // 300% APR at U_max
if (utilization <= U_OPTIMAL) {
return BASE_RATE + (SLOPE1 * utilization) / U_OPTIMAL;
} else {
uint256 excess = utilization - U_OPTIMAL;
return BASE_RATE + SLOPE1
+ (SLOPE2 * excess) / (1e4 - U_OPTIMAL);
}
}
function utilizationRate(uint256 totalBorrows, uint256 totalLiquidity)
internal pure returns (uint256)
{
if (totalLiquidity == 0) return 0;
return totalBorrows * 10_000 / totalLiquidity;
}
Flash loans allow uncollateralized borrowing within a single transaction. The borrowed amount must be returned (plus fee) before the transaction ends, or it reverts.
// Receiver contract
contract FlashLoanReceiver is IFlashLoanSimpleReceiver {
address constant POOL = 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2; // Aave V3 on Mainnet
uint256 public constant FLASHLOAN_FEE = 5; // 0.05% in basis points
function executeOperation(
address asset, uint256 amount, uint256 premium,
address initiator, bytes calldata /* params */
) external override returns (bool) {
require(msg.sender == POOL, "unauthorized");
require(initiator == address(this), "wrong initiator");
// --- arbitrage logic here ---
// Approve repayment with premium
IERC20(asset).approve(POOL, amount + premium);
return true;
}
function initiateFlashLoan(
address tokenBorrow, uint256 amount, bytes calldata params
) external {
IPool(POOL).flashLoanSimple(
address(this), tokenBorrow, amount, params, 0
);
}
}
Uniswap V3 allows any swap to borrow tokens as long as the callbacks deposit the equivalent amount. No separate flash loan contract needed.
// Uniswap V3 flash swap — borrow token1, repay with token0
function uniswapV3SwapCallback(
int256 amount0Delta, int256 amount1Delta, bytes calldata /* data */
) external override {
// amount0Delta > 0 means we owe token0; amount1Delta > 0 means we received token1
require(amount0Delta > 0, "only token0 flash");
// use borrowed token1 here...
// repay token0
IERC20(token0).safeTransfer(pool, uint256(amount0Delta));
}
function initiateFlashSwap(address pool, uint256 amountIn, uint256 amountOutMin) external {
// amountSpecified < 0 means exactOutput (borrow output)
ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
tokenIn: token0,
tokenOut: token1,
fee: 3000,
recipient: address(this),
deadline: block.timestamp,
amountIn: amountIn,
amountOutMinimum: amountOutMin,
sqrtPriceLimitX96: 0
});
ISwapRouter(ROUTER).exactInputSingle(params);
}
// dYdX SoloMargin flash loan (simplified)
function callFunction(
address sender, Account.Info memory account,
bytes memory data
) external {
// borrowed funds available here
// repay happens implicitly — SoloMargin checks balance difference
}
A staking contract accepts LP tokens and distributes rewards proportionally.
contract StakingRewards {
IERC20 public stakingToken;
IERC20 public rewardsToken;
uint256 public rewardRate; // reward tokens per second
uint256 public lastUpdateTime;
uint256 public rewardPerTokenStored;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => uint256) public userRewardPerTokenPaid;
mapping(address => uint256) public rewards;
modifier updateReward(address account) {
rewardPerTokenStored = rewardPerToken();
lastUpdateTime = block.timestamp;
if (account != address(0)) {
rewards[account] = earned(account);
userRewardPerTokenPaid[account] = rewardPerTokenStored;
}
_;
}
function rewardPerToken() public view returns (uint256) {
if (totalSupply == 0) return rewardPerTokenStored;
return rewardPerTokenStored
+ (block.timestamp - lastUpdateTime) * rewardRate * 1e18 / totalSupply;
}
function earned(address account) public view returns (uint256) {
return balanceOf[account] * (rewardPerTokenStored - userRewardPerTokenPaid[account]) / 1e18
+ rewards[account];
}
function stake(uint256 amount) external updateReward(msg.sender) {
totalSupply += amount;
balanceOf[msg.sender] += amount;
stakingToken.safeTransferFrom(msg.sender, address(this), amount);
}
function withdraw(uint256 amount) external updateReward(msg.sender) {
totalSupply -= amount;
balanceOf[msg.sender] -= amount;
stakingToken.safeTransfer(msg.sender, amount);
}
function getReward() external updateReward(msg.sender) {
uint256 reward = rewards[msg.sender];
if (reward > 0) {
rewards[msg.sender] = 0;
rewardsToken.safeTransfer(msg.sender, reward);
}
}
}
Distribute rewards linearly over a duration:
contract Vesting {
struct Schedule {
uint256 totalAmount;
uint256 startTime;
uint256 duration;
uint256 claimed;
}
mapping(address => Schedule) public schedules;
function claimable(address user) public view returns (uint256) {
Schedule storage s = schedules[user];
if (block.timestamp < s.startTime) return 0;
uint256 elapsed = block.timestamp - s.startTime;
uint256 vested = s.totalAmount * Math.min(elapsed, s.duration) / s.duration;
return vested - s.claimed;
}
function claim() external {
uint256 amount = claimable(msg.sender);
require(amount > 0, "nothing to claim");
schedules[msg.sender].claimed += amount;
token.safeTransfer(msg.sender, amount);
}
}
Orders are signed off-chain and submitted on-chain by a taker. The protocol matches orders via a settlement contract.
// 0x limit order (simplified)
struct Order {
address makerToken;
address takerToken;
uint256 makerAmount;
uint256 takerAmount;
address maker;
uint256 expiry;
uint256 salt;
}
function fillOrder(Order calldata order, bytes calldata signature) external {
require(block.timestamp <= order.expiry, "expired");
require(
ECDSA.recover(keccak256(abi.encode(order)), signature) == order.maker,
"bad sig"
);
IERC20(order.takerToken).safeTransferFrom(msg.sender, order.maker, order.takerAmount);
IERC20(order.makerToken).safeTransferFrom(order.maker, msg.sender, order.makerAmount);
}
Used by 0x API, Matcha, 1inch — market makers quote prices via off-chain API, then the taker submits the quote on-chain. The quote includes a signature that binds the maker to the price.
// Chain A — lock tokens, emit event for relayer
contract BridgeLock {
mapping(address => uint256) public totalLocked;
event Locked(address indexed token, address indexed sender, uint256 amount, bytes32 indexed destChain);
function lock(address token, uint256 amount, bytes32 destChain, bytes calldata recipient) external {
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
totalLocked[token] += amount;
emit Locked(token, msg.sender, amount, destChain);
}
function unlock(address token, uint256 amount, bytes calldata proof) external {
require(verifyBurnProof(token, amount, proof), "invalid proof");
IERC20(token).safeTransfer(msg.sender, amount);
totalLocked[token] -= amount;
}
}
// Chain B — mint tokens when locked on Chain A
contract BridgeMint {
mapping(address => address) public wrappedTokens; // original token => wrapped
event Minted(address indexed wrapped, address indexed recipient, uint256 amount);
function mint(bytes calldata proof) external {
(address originalToken, address recipient, uint256 amount) = parseLockProof(proof);
address wrapped = wrappedTokens[originalToken];
require(wrapped != address(0), "unsupported token");
_mint(wrapped, recipient, amount);
emit Minted(wrapped, recipient, amount);
}
// burn/redeem
function burn(address wrapped, uint256 amount, bytes32 destChain, bytes calldata recipient) external {
_burn(wrapped, msg.sender, amount);
emit Burned(wrapped, msg.sender, amount, destChain, recipient);
}
}
Two pools hold the same asset on both chains. Users deposit on chain A and receive from the pool on chain B. The operator rebalances periodically. No wrapped tokens needed, but liquidity is fragmented.
Advanced bridges (LayerZero, IBC) validate headers from chain A on chain B using light clients. The Solidity contract stores block headers and verifies state proofs:
// Simplified header relay
contract LightClientBridge {
struct BlockHeader {
uint256 number;
bytes32 stateRoot;
bytes32 txRoot;
uint256 timestamp;
}
mapping(uint256 => BlockHeader) public headers;
function submitHeader(BlockHeader calldata header, bytes calldata attestation) external {
require(verifyAttestation(header, attestation), "invalid attestation");
headers[header.number] = header;
}
function verifyProof(bytes32 leaf, bytes32[] calldata proof, uint256 blockNumber)
external view returns (bool)
{
bytes32 computedRoot = computeMerkleRoot(leaf, proof);
return computedRoot == headers[blockNumber].stateRoot;
}
}
contract PriceConsumer {
AggregatorV3Interface internal priceFeed;
constructor(address feed) {
priceFeed = AggregatorV3Interface(feed);
}
function getLatestPrice() public view returns (int256) {
(, int256 price, , uint256 updatedAt, ) = priceFeed.latestRoundData();
require(block.timestamp - updatedAt <= 1 hours, "stale price");
require(price > 0, "invalid price");
return price;
}
}
// ETH/USD feed on Mainnet: 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419
// BTC/USD feed on Mainnet: 0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c
// ethers.js — read Chainlink price
async function getChainlinkPrice(feedAddress, provider) {
const aggregator = new ethers.Contract(feedAddress, [
"function latestRoundData() view returns (uint80,int256,uint256,uint256,uint80)",
"function decimals() view returns (uint8)"
], provider);
const [, price, , updatedAt] = await aggregator.latestRoundData();
const decimals = await aggregator.decimals();
return { price: Number(price) / 10 ** decimals, updatedAt: Number(updatedAt) };
}
Already covered above — accumulate price * elapsed at each update, compute over a window. Deploy as a separate oracle contract that reads a pair's cumulative price.
Maker's Medianizer aggregates prices from multiple feeds, publishes the median, and includes a delay before use. The DSValue and DSMedianizer contracts are legacy.
// Multi-source validation
function validatePrice(uint256 priceCL, uint256 twap) internal pure returns (uint256) {
uint256 deviation = priceCL > twap
? (priceCL - twap) * 10000 / twap
: (twap - priceCL) * 10000 / priceCL;
require(deviation < 500, "oracle divergence"); // max 5% divergence
// return weighted average or min
return priceCL < twap ? priceCL : twap;
}
The Checks-Effects-Interactions pattern plus OpenZeppelin's ReentrancyGuard:
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SecureLending is ReentrancyGuard {
function withdraw(uint256 amount) external nonReentrant {
require(balances[msg.sender] >= amount, "insufficient");
balances[msg.sender] -= amount; // effect
IERC20(token).safeTransfer(msg.sender, amount); // interaction
}
}
The example of failure: Aave V2 EURS depeg incident — attacker manipulated a low-liquidity Uniswap pair to get a false spot price, then liquidated positions in Aave.
// VULNERABLE — spot price from a single AMM
function getPriceLiquid(address pair) external view returns (uint256) {
(uint256 r0, uint256 r1,) = IUniswapV2Pair(pair).getReserves();
return r1 * 1e18 / r0; // manipulatable with a single swap
}
// SAFER — TWAP from multiple sources
function getPriceSafe(address pair) external view returns (uint256) {
uint256 twap = IUniswapV2TWAP(pair).consult(someToken, 1e18, 30 minutes);
uint256 chainlink = aggregator.latestAnswer();
// Take the lower of two (or the average, or revert on divergence > 5%)
return (twap + chainlink) / 2;
}
MEV bots front-run and back-run user swaps:
Mitigation:
// Slippage sandwich protection — require exact output
function swapExactTokensForTokens(
uint256 amountIn, uint256 amountOutMin,
address[] calldata path, address to
) external returns (uint256[] memory amounts) {
amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
require(amounts[amounts.length - 1] >= amountOutMin, "slippage");
// ...transfer and swap
}
// Deadline protection — prevents long-pending transactions
function swapExactTokensForTokens(
// ...
uint256 deadline
) external {
require(block.timestamp <= deadline, "expired");
// ...
}
import { ethers } from "hardhat";
describe("AMM Integration", function () {
it("should execute a flash loan arbitrage on mainnet fork", async function () {
// Fork mainnet at a specific block
await network.provider.request({
method: "hardhat_reset",
params: [{
forking: {
jsonRpcUrl: `https://eth-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`,
blockNumber: 18000000,
}
}]
});
// Impersonate a whale
const whale = "0x...";
await network.provider.request({
method: "hardhat_impersonateAccount",
params: [whale],
});
const signer = await ethers.getSigner(whale);
// Execute the arbitrage
const arb = await (await ethers.getContractFactory("FlashArb")).deploy();
const tx = await arb.connect(signer).execute(amount, { gasLimit: 5000000 });
const receipt = await tx.wait();
expect(receipt.status).to.equal(1);
const profit = await arb.profit();
expect(profit).to.be.gt(0);
});
});
amountOut >= minAmountOut or amountIn <= maxAmountInrequire(block.timestamp <= deadline)Pausable — halt all state-changing functions during incidentsInitializable for upgradeable contractsAccessControl for role-based admin functionsamountIn == received// Fee-on-transfer safe pattern
function deposit(address token, uint256 amount) external {
uint256 before = IERC20(token).balanceOf(address(this));
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
uint256 received = IERC20(token).balanceOf(address(this)) - before;
shares[msg.sender] += received; // use received, not amount
}
safeTransfer — non-standard tokens (USDT) cause transfers to silently fail.approve race condition — Uniswap V2 pattern uses safeIncreaseAllowance instead.k-value check after V2 swap (Uniswap enforces k * 1000 >= kLast for fee harvesting).After completing DeFi protocol implementation, confirm:
x * y = k with correct fee deduction (0.3% taken before curve)minAmountOut or maxAmountInsafeTransfer and safeTransferFrom used for all token transfersx*y = k, healthFactor >= 1.0). All other code enforces this invariant.| Rationalization | Reality |
|---|---|
| "TWAP adds complexity I don't need; spot price works fine for my use case" | Spot price from a single pool is manipulatable for ~$100k on most pairs. TWAP over 30 min costs nothing in gas (one extra read) and prevents oracle attacks. |
| "I can skip the deadline parameter to simplify the interface" | Without a deadline, a user's transaction can be held by a validator and executed hours later at manipulated prices. |
| "AMMs replace order books entirely" | AMMs excel at passive liquidity but have worse price discovery for large orders and suffer from MEV. Order books with RFQ matching are superior for institutional flows. |
| "Flash loans are free capital — no risk" | Flash loan callbacks can re-enter the lending protocol to drain it. The Aave V2 codebase requires receiver contracts to be non-reentrant relative to the pool. |
| "I can use the same interest rate model for all pools" | Stable assets (USDC/DAI) can tolerate higher utilization before spike. Volatile assets need jump-rate models with a lower U_opt (~60%) to keep liquidity available. |
| "Concentrated liquidity is always better than V2" | Concentrated positions need active rebalancing. Passive LPs in V3 often earn less than V2 LPs after gas costs for position management. IL is concentrated, not eliminated. |
| "Bridges just lock and mint — simple" | Bridge security depends on the validator/threshold set, not the lock/mint logic. Wormhole's $320M hack was a signature verification bug, not a lock/mint flaw. Light client bridges are more secure but significantly more expensive to verify on-chain. |
| "Chainlink feeds are always reliable" | Chainlink feeds can go stale during extreme volatility (LUNA collapse). Always add a staleness check (heartbeat) and a fallback TWAP source with divergence checks. |
| "I'll add emergency pause after the audit" | Without a pause mechanism deployed from the start, a live exploit forces the team to coordinate a migration before the protocol can be stopped. Code pause + proxy admin = minutes to stop an exploit. |