on Somnia, inspired by Uniswap V2's core mechanics. We'll implement the essential components: Liquidity Pools, Automated Market Maker (AMM) logic, and Token Swapping functionality.
{% hint style="success" %}
Somnia Mainnet is LIVE. To deploy on Somnia Mainnet, you will need SOMI Tokens. Please refer to the [guide](/get-started/getting-started-for-mainnet) on Moving from Testnet to Mainnet.
{% endhint %}
## Prerequisites
1. This guide is not an introduction to Solidity Programming; you are expected to understand Basic Solidity Programming.
2. You can deploy the Smart Contracts using our [Hardhat](/developer/development-frameworks/deploy-with-hardhat) or [Foundry](/developer/development-frameworks/deploy-with-foundry) guides.
## Core Concepts
### Automated Market Maker (AMM)
At the core of an AMM is a Liquidity Pool, a Smart Contract that holds reserves of two (or more) tokens (e.g., STT and USDC). Users trade directly against this pool instead of with other users.
Most AMMs (like Uniswap v2) use the constant product formula:
x⋅y=kx \cdot y = kx⋅y=k
* `x` = amount of Token A in the pool
* `y` = amount of Token B in the pool
* `k` = constant (must remain unchanged)
This ensures price adjustment based on supply and demand.
If a user wants to buy Token A with Token B:
* They send Token B into the Pool
* The Smart Contract calculates how much Token A to send out to maintain `x * y = k`
* As more Token A is withdrawn, its price increases (slippage)
Anyone can deposit an equal value of both tokens into the pool to become a Liquidity Provider (LP) and earn a share of the trading fees (e.g., 0.3%).
> **AMMs are fully decentralized with no need for counterparties and are Open and permissionless to use and contribute liquidity.**
### Liquidity Pools
Pairs of tokens locked in Smart Contracts that facilitate trading without traditional order books.
## Smart Contract Architecture
We'll build three main contracts:
1. `SomniaFactory`: Creates and manages pair contracts
2. `SomniaPair`: Individual liquidity pool for token pairs
3. `SomniaRouter`: User-facing contract for swaps and liquidity management
## Implementation
### ERC-20 Interface
First, let's define the ERC-20 interface we'll use.
<details>
<summary>SomniaPair.sol</summary>
```solidity
// IERC20.sol
pragma solidity ^0.8.0;
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
```
</details>
### SomniaPair Contract
The pair contract manages individual Liquidity Pools.
<details>
<summary>SomniaPair.sol</summary>
```solidity
// SomniaPair.sol
pragma solidity ^0.8.0;
import "./IERC20.sol";
contract SomniaPair is IERC20 {
uint256 public constant MINIMUM_LIQUIDITY = 10**3;
address public factory;
address public token0;
address public token1;
uint112 private reserve0;
uint112 private reserve1;
uint32 private blockTimestampLast;
uint256 public kLast;
uint256 private unlocked = 1;
modifier lock() {
require(unlocked == 1, 'LOCKED');
unlocked = 0;
_;
unlocked = 1;
}
// ERC-20 Implementation
string public constant name = "Somnia LP Token";
string public constant symbol = "SLP";
uint8 public constant decimals = 18;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
event Mint(address indexed sender, uint256 amount0, uint256 amount1);
event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to);
event Swap(
address indexed sender,
uint256 amount0In,
uint256 amount1In,
uint256 amount0Out,
uint256 amount1Out,
address indexed to
);
event Sync(uint112 reserve0, uint112 reserve1);
constructor() {
factory = msg.sender;
}
function initialize(address _token0, address _token1) external {
require(msg.sender == factory, 'FORBIDDEN');
token0 = _token0;
token1 = _token1;
}
function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
_reserve0 = reserve0;
_reserve1 = reserve1;
_blockTimestampLast = blockTimestampLast;
}
function _safeTransfer(address token, address to, uint256 value) private {
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transfer.selector, to, value));
require(success && (data.length == 0 || abi.decode(data, (bool))), 'TRANSFER_FAILED');
}
function _update(uint256 balance0, uint256 balance1, uint112 _reserve0, uint112 _reserve1) private {
require(balance0 <= type(uint112).max && balance1 <= type(uint112).max, 'OVERFLOW');
uint32 blockTimestamp = uint32(block.timestamp % 2**32);
reserve0 = uint112(balance0);
reserve1 = uint112(balance1);
blockTimestampLast = blockTimestamp;
emit Sync(reserve0, reserve1);
}
function mint(address to) external lock returns (uint256 liquidity) {
(uint112 _reserve0, uint112 _reserve1,) = getReserves();
uint256 balance0 = IERC20(token0).balanceOf(address(this));
uint256 balance1 = IERC20(token1).balanceOf(address(this));
uint256 amount0 = balance0 - _reserve0;
uint256 amount1 = balance1 - _reserve1;
uint256 _totalSupply = totalSupply;
if (_totalSupply == 0) {
liquidity = sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY;
_mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
} else {
liquidity = min(amount0 * _totalSupply / _reserve0, amount1 * _totalSupply / _reserve1);
}
require(liquidity > 0, 'INSUFFICIENT_LIQUIDITY_MINTED');
_mint(to, liquidity);
_update(balance0, balance1, _reserve0, _reserve1);
kLast = uint256(reserve0) * reserve1;
emit Mint(msg.sender, amount0, amount1);
}
function burn(address to) external lock returns (uint256 amount0, uint256 amount1) {
(uint112 _reserve0, uint112 _reserve1,) = getReserves();
address _token0 = token0;
address _token1 = token1;
uint256 balance0 = IERC20(_token0).balanceOf(address(this));
uint256 balance1 = IERC20(_token1).balanceOf(address(this));
uint256 liquidity = balanceOf[address(this)];
uint256 _totalSupply = totalSupply;
amount0 = liquidity * balance0 / _totalSupply;
amount1 = liquidity * balance1 / _totalSupply;
require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED');
_burn(address(this), liquidity);
_safeTransfer(_token0, to, amount0);
_safeTransfer(_token1, to, amount1);
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
_update(balance0, balance1, _reserve0, _reserve1);
kLast = uint256(reserve0) * reserve1;
emit Burn(msg.sender, amount0, amount1, to);
}
function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external lock {
require(amount0Out > 0 || amount1Out > 0, 'INSUFFICIENT_OUTPUT_AMOUNT');
(uint112 _reserve0, uint112 _reserve1,) = getReserves();
require(amount0Out < _reserve0 && amount1Out < _reserve1, 'INSUFFICIENT_LIQUIDITY');
uint256 balance0;
uint256 balance1;
{
address _token0 = token0;
address _token1 = token1;
require(to != _token0 && to != _token1, 'INVALID_TO');
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out);
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out);
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
}
uint256 amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint256 amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
require(amount0In > 0 || amount1In > 0, 'INSUFFICIENT_INPUT_AMOUNT');
{
uint256 balance0Adjusted = balance0 * 1000 - amount0In * 3;
uint256 balance1Adjusted = balance1 * 1000 - amount1In * 3;
require(balance0Adjusted * balance1Adjusted >= uint256(_reserve0) * _reserve1 * 1000**2, 'K');
}
_update(balance0, balance1, _reserve0, _reserve1);
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}
// Helper functions
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 x, uint256 y) internal pure returns (uint256 z) {
z = x < y ? x : y;
}
// ERC-20 functions
function _mint(address to, uint256 value) internal {
totalSupply += value;
balanceOf[to] += value;
emit Transfer(address(0), to, value);
}
function _burn(address from, uint256 value) internal {
balanceOf[from] -= value;
totalSupply -= value;
emit Transfer(from, address(0), value);
}
function approve(address spender, uint256 value) external returns (bool) {
allowance[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
return true;
}
function transfer(address to, uint256 value) external returns (bool) {
balanceOf[msg.sender] -= value;
balanceOf[to] += value;
emit Transfer(msg.sender, to, value);
return true;
}
function transferFrom(address from, address to, uint256 value) external returns (bool) {
if (allowance[from][msg.sender] != type(uint256).max) {
allowance[from][msg.sender] -= value;
}
balanceOf[from] -= value;
balanceOf[to] += value;
emit Transfer(from, to, value);
return true;
}
}
```
</details>
### SomniaFactory Contract
The factory creates and tracks all pair contracts.
<details>
<summary>SomniaFactory.sol</summary>
```solidity
// SomniaFactory.sol
pragma solidity ^0.8.0;
import "./SomniaPair.sol";
contract SomniaFactory {
mapping(address => mapping(address => address)) public getPair;
address[] public allPairs;
event PairCreated(address indexed token0, address indexed token1, address pair, uint256);
function allPairsLength() external view returns (uint256) {
return allPairs.length;
}
function createPair(address tokenA, address tokenB) external returns (address pair) {
require(tokenA != tokenB, 'IDENTICAL_ADDRESSES');
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), 'ZERO_ADDRESS');
require(getPair[token0][token1] == address(0), 'PAIR_EXISTS');
bytes memory bytecode = type(SomniaPair).creationCode;
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
SomniaPair(pair).initialize(token0, token1);
getPair[token0][token1] = pair;
getPair[token1][token0] = pair;
allPairs.push(pair);
emit PairCreated(token0, token1, pair, allPairs.length);
}
}
```
</details>
### SomniaRouter Contract
The router provides user-friendly functions for swapping and liquidity management.
<details>
<summary>SomniaRouter.sol</summary>
```solidity
// SomniaRouter.sol
pragma solidity ^0.8.0;
import "./IERC20.sol";
import "./SomniaPair.sol";
import "./SomniaFactory.sol";
contract SomniaRouter {
address public immutable factory;
modifier ensure(uint256 deadline) {
require(deadline >= block.timestamp, 'EXPIRED');
_;
}
constructor(address _factory) {
factory = _factory;
}
// Add liquidity
function addLiquidity(
address tokenA,
address tokenB,
uint256 amountADesired,
uint256 amountBDesired,
uint256 amountAMin,
uint256 amountBMin,
address to,
uint256 deadline
) external ensure(deadline) returns (uint256 amountA, uint256 amountB, uint256 liquidity) {
(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
address pair = pairFor(tokenA, tokenB);
_safeTransferFrom(tokenA, msg.sender, pair, amountA);
_safeTransferFrom(tokenB, msg.sender, pair, amountB);
liquidity = SomniaPair(pair).mint(to);
}
function _addLiquidity(
address tokenA,
address tokenB,
uint256 amountADesired,
uint256 amountBDesired,
uint256 amountAMin,
uint256 amountBMin
) internal returns (uint256 amountA, uint256 amountB) {
if (SomniaFactory(factory).getPair(tokenA, tokenB) == address(0)) {
SomniaFactory(factory).createPair(tokenA, tokenB);
}
(uint256 reserveA, uint256 reserveB) = getReserves(tokenA, tokenB);
if (reserveA == 0 && reserveB == 0) {
(amountA, amountB) = (amountADesired, amountBDesired);
} else {
uint256 amountBOptimal = quote(amountADesired, reserveA, reserveB);
if (amountBOptimal <= amountBDesired) {
require(amountBOptimal >= amountBMin, 'INSUFFICIENT_B_AMOUNT');
(amountA, amountB) = (amountADesired, amountBOptimal);
} else {
uint256 amountAOptimal = quote(amountBDesired, reserveB, reserveA);
assert(amountAOptimal <= amountADesired);
require(amountAOptimal >= amountAMin, 'INSUFFICIENT_A_AMOUNT');
(amountA, amountB) = (amountAOptimal, amountBDesired);
}
}
}
// Remove liquidity
function removeLiquidity(
address tokenA,
address tokenB,
uint256 liquidity,
uint256 amountAMin,
uint256 amountBMin,
address to,
uint256 deadline
) public ensure(deadline) returns (uint256 amountA, uint256 amountB) {
address pair = pairFor(tokenA, tokenB);
SomniaPair(pair).transferFrom(msg.sender, pair, liquidity);
(uint256 amount0, uint256 amount1) = SomniaPair(pair).burn(to);
(address token0,) = sortTokens(tokenA, tokenB);
(amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
require(amountA >= amountAMin, 'INSUFFICIENT_A_AMOUNT');
require(amountB >= amountBMin, 'INSUFFICIENT_B_AMOUNT');
}
// Swap functions
function swapExactTokensForTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external ensure(deadline) returns (uint256[] memory amounts) {
amounts = getAmountsOut(amountIn, path);
require(amounts[amounts.length - 1] >= amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT');
_safeTransferFrom(
path[0], msg.sender, pairFor(path[0], path[1]), amounts[0]
);
_swap(amounts, path, to);
}
function swapTokensForExactTokens(
uint256 amountOut,
uint256 amountInMax,
address[] calldata path,
address to,
uint256 deadline
) external ensure(deadline) returns (uint256[] memory amounts) {
amounts = getAmountsIn(amountOut, path);
require(amounts[0] <= amountInMax, 'EXCESSIVE_INPUT_AMOUNT');
_safeTransferFrom(
path[0], msg.sender, pairFor(path[0], path[1]), amounts[0]
);
_swap(amounts, path, to);
}
// Internal functions
function _swap(uint256[] memory amounts, address[] memory path, address _to) internal {
for (uint256 i; i < path.length - 1; i++) {
(address input, address output) = (path[i], path[i + 1]);
(address token0,) = sortTokens(input, output);
uint256 amountOut = amounts[i + 1];
(uint256 amount0Out, uint256 amount1Out) = input == token0 ? (uint256(0), amountOut) : (amountOut, uint256(0));
address to = i < path.length - 2 ? pairFor(output, path[i + 2]) : _to;
SomniaPair(pairFor(input, output)).swap(
amount0Out, amount1Out, to, new bytes(0)
);
}
}
// Library functions
function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
require(tokenA != tokenB, 'IDENTICAL_ADDRESSES');
(token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), 'ZERO_ADDRESS');
}
function pairFor(address tokenA, address tokenB) internal view returns (address pair) {
(address token0, address token1) = sortTokens(tokenA, tokenB);
pair = SomniaFactory(factory).getPair(token0, token1);
require(pair != address(0), 'PAIR_DOES_NOT_EXIST');
}
function getReserves(address tokenA, address tokenB) internal view returns (uint256 reserveA, uint256 reserveB) {
(address token0,) = sortTokens(tokenA, tokenB);
(uint256 reserve0, uint256 reserve1,) = SomniaPair(pairFor(tokenA, tokenB)).getReserves();
(reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
}
function quote(uint256 amountA, uint256 reserveA, uint256 reserveB) internal pure returns (uint256 amountB) {
require(amountA > 0, 'INSUFFICIENT_AMOUNT');
require(reserveA > 0 && reserveB > 0, 'INSUFFICIENT_LIQUIDITY');
amountB = amountA * reserveB / reserveA;
}
function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) internal pure returns (uint256 amountOut) {
require(amountIn > 0, 'INSUFFICIENT_INPUT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'INSUFFICIENT_LIQUIDITY');
uint256 amountInWithFee = amountIn * 997;
uint256 numerator = amountInWithFee * reserveOut;
uint256 denominator = reserveIn * 1000 + amountInWithFee;
amountOut = numerator / denominator;
}
function getAmountIn(uint256 amountOut, uint256 reserveIn, uint256 reserveOut) internal pure returns (uint256 amountIn) {
require(amountOut > 0, 'INSUFFICIENT_OUTPUT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'INSUFFICIENT_LIQUIDITY');
uint256 numerator = reserveIn * amountOut * 1000;
uint256 denominator = (reserveOut - amountOut) * 997;
amountIn = (numerator / denominator) + 1;
}
function getAmountsOut(uint256 amountIn, address[] memory path) public view returns (uint256[] memory amounts) {
require(path.length >= 2, 'INVALID_PATH');
amounts = new uint256[](path.length);
amounts[0] = amountIn;
for (uint256 i; i < path.length - 1; i++) {
(uint256 reserveIn, uint256 reserveOut) = getReserves(path[i], path[i + 1]);
amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
}
}
function getAmountsIn(uint256 amountOut, address[] memory path) public view returns (uint256[] memory amounts) {
require(path.length >= 2, 'INVALID_PATH');
amounts = new uint256[](path.length);
amounts[amounts.length - 1] = amountOut;
for (uint256 i = path.length - 1; i > 0; i--) {
(uint256 reserveIn, uint256 reserveOut) = getReserves(path[i - 1], path[i]);
amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);
}
}
function _safeTransferFrom(address token, address from, address to, uint256 value) private {
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, value));
require(success && (data.length == 0 || abi.decode(data, (bool))), 'TRANSFER_FROM_FAILED');
}
}
```
</details>
You can deploy the Smart Contracts in the order they have been created above. Create `Token A` and `Token B` or, for example, use `wSTT` and `USDC` Token Pairs.\
\
The next step will be to create a Pool Pair for `Token A` and `Token B`. Deploy the Pair and Add Liquidity. Then users will be able to create `SWAP` using the `Router` Smart Contract.
### Usage Example
<details>
<summary>Add Liquidity</summary>
```solidity
// Approve router to spend tokens
tokenA.approve(router.address, amountA);
tokenB.approve(router.address, amountB);
// Add liquidity
router.addLiquidity(
tokenA.address,
tokenB.address,
amountA,
amountB,
minAmountA,
minAmountB,
userAddress,
deadline
);
```
</details>
<details>
<summary>Swap Tokens</summary>
```solidity
// Approve router to spend input token
tokenA.approve(router.address, amountIn);
// Swap exact tokens for tokens
address[] memory path = new address[](2);
path[0] = tokenA.address;
path[1] = tokenB.address;
router.swapExactTokensForTokens(
amountIn,
minAmountOut,
path,
userAddress,
deadline
);
```
</details>
<details>
<summary>Remove Liquidity</summary>
```solidity
// Approve router to spend LP tokens
pair.approve(router.address, lpTokenAmount);
// Remove liquidity
router.removeLiquidity(
tokenA.address,
tokenB.address,
lpTokenAmount,
minAmountA,
minAmountB,
userAddress,
deadline
);
```
</details>
### Test Your DEX
Create a test script to verify functionality:
<details>
<summary>TestDEX.sol</summary>
```solidity
pragma solidity ^0.8.0;
imp프로젝트로 돌아가기