# DeFi Development Guide — Build Your First Protocol
Decentralized Finance (DeFi) has revolutionized the financial industry by removing intermediaries and enabling anyone to access financial services through smart contracts. In this comprehensive guide, we'll explore the core DeFi primitives and build a simple Automated Market Maker (AMM) protocol from scratch.
What is DeFi?
DeFi refers to financial applications built on blockchain technology that operate without traditional intermediaries like banks or brokerages. These applications use smart contracts to automate financial services such as:
- Trading (Decentralized Exchanges)
- Lending and Borrowing
- Staking and Yield Farming
- Derivatives and Synthetic Assets
- Insurance protocols
The total value locked (TVL) in DeFi protocols exceeded $150 billion in 2026, demonstrating the massive adoption of these technologies.
Core DeFi Primitives
1. Decentralized Exchanges (DEX) and Automated Market Makers (AMM)
Traditional exchanges use order books to match buyers and sellers. AMMs replace this with liquidity pools and mathematical formulas. The most famous formula is the constant product formula:
x * y = k
Where:
x= reserve of token A
y= reserve of token B
k= constant product
This formula ensures that as one reserve increases, the other decreases proportionally, creating a price curve.
2. Lending Protocols
Platforms like Aave and Compound allow users to:
- Deposit assets and earn interest
- Borrow assets by providing collateral
- Liquidate under-collateralized positions
Interest rates are typically determined algorithmically based on supply and demand (utilization rate).
3. Staking and Yield Farming
Users can lock their tokens to:
- Secure a network (Proof of Stake)
- Provide liquidity to AMMs
- Earn rewards in native tokens or trading fees
Yield farming involves moving assets between protocols to maximize returns.
Building a Simple AMM
Let's implement a basic constant product AMM supporting two tokens. This simplified version demonstrates the core mechanics of protocols like Uniswap V2.
Step 1: Contract Setup
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract SimpleAMM is ERC20, ReentrancyGuard {
IERC20 public immutable token0;
IERC20 public immutable token1;
uint256 public reserve0;
uint256 public reserve1;
event LiquidityAdded(address indexed provider, uint256 amount0, uint256 amount1, uint256 liquidity);
event LiquidityRemoved(address indexed provider, uint256 amount0, uint256 amount1, uint256 liquidity);
event Swap(address indexed user, address indexed tokenIn, uint256 amountIn, uint256 amountOut);
constructor(address _token0, address _token1) ERC20("LP Token", "LP") {
token0 = IERC20(_token0);
token1 = IERC20(_token1);
}
function _update(uint256 _reserve0, uint256 _reserve1) private {
reserve0 = _reserve0;
reserve1 = _reserve1;
}
}
Step 2: Add Liquidity
function addLiquidity(uint256 amount0, uint256 amount1)
external
nonReentrant
returns (uint256 liquidity)
{
require(amount0 > 0 && amount1 > 0, "Invalid amounts");
// Transfer tokens from user
token0.transferFrom(msg.sender, address(this), amount0);
token1.transferFrom(msg.sender, address(this), amount1);
uint256 _totalSupply = totalSupply();
if (_totalSupply == 0) {
// First liquidity provider
liquidity = sqrt(amount0 * amount1);
} else {
// Subsequent providers - maintain ratio
liquidity = min(
(amount0 * _totalSupply) / reserve0,
(amount1 * _totalSupply) / reserve1
);
}
require(liquidity > 0, "Insufficient liquidity minted");
// Mint LP tokens
_mint(msg.sender, liquidity);
// Update reserves
_update(reserve0 + amount0, reserve1 + amount1);
emit LiquidityAdded(msg.sender, amount0, amount1, liquidity);
}
function sqrt(uint256 y) internal pure returns (uint256 z) {
if (y > 3) {
z = y;
uint256 x = y / 2 + 1;
while (x < z) {
z = x;
x = (y / x + x) / 2;
}
} else if (y != 0) {
z = 1;
}
}
function min(uint256 a, uint256 b) internal pure returns (uint256) {
return a < b ? a : b;
}
Step 3: Swap Function
function swap(address tokenIn, uint256 amountIn)
external
nonReentrant
returns (uint256 amountOut)
{
require(tokenIn == address(token0) || tokenIn == address(token1), "Invalid token");
require(amountIn > 0, "Invalid amount");
bool isToken0 = tokenIn == address(token0);
(IERC20 _tokenIn, IERC20 _tokenOut, uint256 reserveIn, uint256 reserveOut) = isToken0
? (token0, token1, reserve0, reserve1)
: (token1, token0, reserve1, reserve0);
// Transfer token in
_tokenIn.transferFrom(msg.sender, address(this), amountIn);
// Calculate amount out using constant product formula
// amountOut = (amountIn * reserveOut) / (reserveIn + amountIn)
// With 0.3% fee: amountIn * 0.997
uint256 amountInWithFee = amountIn * 997;
amountOut = (amountInWithFee * reserveOut) / (reserveIn * 1000 + amountInWithFee);
require(amountOut > 0, "Insufficient output amount");
// Transfer token out
_tokenOut.transfer(msg.sender, amountOut);
// Update reserves
uint256 balance0 = token0.balanceOf(address(this));
uint256 balance1 = token1.balanceOf(address(this));
_update(balance0, balance1);
emit Swap(msg.sender, tokenIn, amountIn, amountOut);
}
Step 4: Remove Liquidity
function removeLiquidity(uint256 liquidity)
external
nonReentrant
returns (uint256 amount0, uint256 amount1)
{
require(liquidity > 0, "Invalid liquidity");
uint256 _totalSupply = totalSupply();
// Calculate token amounts proportional to LP tokens burned
amount0 = (liquidity * reserve0) / _totalSupply;
amount1 = (liquidity * reserve1) / _totalSupply;
require(amount0 > 0 && amount1 > 0, "Insufficient liquidity burned");
// Burn LP tokens
_burn(msg.sender, liquidity);
// Transfer tokens to user
token0.transfer(msg.sender, amount0);
token1.transfer(msg.sender, amount1);
// Update reserves
uint256 balance0 = token0.balanceOf(address(this));
uint256 balance1 = token1.balanceOf(address(this));
_update(balance0, balance1);
emit LiquidityRemoved(msg.sender, amount0, amount1, liquidity);
}
Flash Loans Explained
Flash loans are uncollateralized loans that must be borrowed and repaid within the same transaction. They're a unique DeFi primitive enabled by blockchain atomicity.
How They Work
contract FlashLoanProvider {
function flashLoan(uint256 amount, address borrower) external {
uint256 balanceBefore = token.balanceOf(address(this));
// 1. Send tokens to borrower
token.transfer(borrower, amount);
// 2. Call borrower's callback
IFlashLoanReceiver(borrower).executeOperation(amount);
// 3. Ensure tokens + fee were returned
uint256 balanceAfter = token.balanceOf(address(this));
require(balanceAfter >= balanceBefore + fee, "Flash loan not repaid");
}
}
Use cases include:
- Arbitrage across DEXes
- Collateral swaps in lending protocols
- Liquidations without upfront capital
Composability — The DeFi Superpower
DeFi protocols are like Lego blocks that can be combined. Example:
This composability creates powerful strategies but also systemic risks.
Risks in DeFi
1. Impermanent Loss
When you provide liquidity to an AMM, price divergence between tokens causes losses compared to just holding.
Example: You deposit 1 ETH ($2000) + 2000 USDC when ETH = $2000.
If ETH rises to $4000:
- Holding: 1 ETH + 2000 USDC = $6000
- LP position: ~0.707 ETH + ~2828 USDC = $5656
- Impermanent loss: $344 (5.7%)
Formula:
IL = 2 * sqrt(price_ratio) / (1 + price_ratio) - 1
2. Smart Contract Risk
Bugs can lead to:
- Funds drainage (reentrancy, logic errors)
- Locked funds (upgradability issues)
- Oracle manipulation (flash loan attacks on price feeds)
Always audit code before deploying real value.
3. Systemic Risk
DeFi protocols depend on each other. A failure in one can cascade:
- Aave depends on Chainlink oracles
- Curve pools use various tokens
- Leverage protocols amplify losses
Testing DeFi Protocols
Use Hardhat or Foundry for comprehensive testing:
describe("SimpleAMM", function() {
it("should handle swap correctly", async function() {
const [owner, user] = await ethers.getSigners();
// Add liquidity
await amm.connect(owner).addLiquidity(ethers.parseEther("100"), ethers.parseEther("100"));
// User swaps 10 token0 for token1
const amountIn = ethers.parseEther("10");
await token0.connect(user).approve(amm.target, amountIn);
const amountOut = await amm.connect(user).swap.staticCall(token0.target, amountIn);
await amm.connect(user).swap(token0.target, amountIn);
// Verify constant product (with fee)
const newReserve0 = await amm.reserve0();
const newReserve1 = await amm.reserve1();
// k should increase (due to fees)
expect(newReserve0 * newReserve1).to.be.gt(ethers.parseEther("100") * ethers.parseEther("100"));
});
});
Test edge cases:
- Zero amounts
- Very large swaps (price impact)
- Reentrancy attacks
- Integer overflow/underflow
Resources to Go Further
Conclusion
DeFi development requires understanding both financial concepts and smart contract engineering. Start with simple AMMs, then explore lending, derivatives, and advanced yield strategies.
Key takeaways:
- AMMs use mathematical formulas (x*y=k) instead of order books
- Flash loans enable capital-free arbitrage and liquidations
- Composability is powerful but introduces systemic risks
- Always test extensively and audit before deploying real value
- Impermanent loss affects liquidity providers in volatile markets
Ready to build? Solingo's DeFi Development track takes you from basics to building production protocols like Uniswap, Aave, and Compound. Start learning today.