Skip to main content

Advanced Smart Contract Patterns and DeFi Protocol Development

Building Production-Grade Decentralized Finance Protocols​

Prerequisites Assumed​

  • Completion of Tutorial 1 (Blockchain Engineering Fundamentals)
  • Solid understanding of Solidity and smart contract development
  • Experience with testing frameworks and deployment tools
  • Knowledge of DeFi concepts (lending, AMMs, yield farming)

1. Advanced Smart Contract Architecture Patterns​

Factory Pattern for Protocol Deployment​

The Factory pattern enables scalable deployment of similar contracts with consistent interfaces.

// contracts/LendingPoolFactory.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "./LendingPool.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

/**
* @title LendingPoolFactory
* @dev Factory contract for deploying and managing lending pools
*/
contract LendingPoolFactory is Ownable, ReentrancyGuard {
struct PoolInfo {
address poolAddress;
address asset;
string name;
uint256 createdAt;
bool isActive;
}

mapping(address => PoolInfo) public pools;
mapping(address => address[]) public assetToPools;
address[] public allPools;

event PoolCreated(
address indexed poolAddress,
address indexed asset,
string name,
address indexed creator
);

event PoolStatusChanged(address indexed pool, bool isActive);

/**
* @dev Creates a new lending pool for a specific asset
* @param asset The ERC20 token address for the pool
* @param name Human-readable name for the pool
* @param initialParams Pool configuration parameters
*/
function createPool(
address asset,
string memory name,
LendingPool.PoolParams memory initialParams
) external nonReentrant returns (address) {
require(asset != address(0), "Invalid asset address");
require(bytes(name).length > 0, "Pool name required");

// Deploy new lending pool
LendingPool newPool = new LendingPool(
asset,
name,
initialParams,
msg.sender
);

address poolAddress = address(newPool);

// Store pool information
pools[poolAddress] = PoolInfo({
poolAddress: poolAddress,
asset: asset,
name: name,
createdAt: block.timestamp,
isActive: true
});

assetToPools[asset].push(poolAddress);
allPools.push(poolAddress);

emit PoolCreated(poolAddress, asset, name, msg.sender);

return poolAddress;
}

/**
* @dev Gets all pools for a specific asset
*/
function getPoolsForAsset(address asset) external view returns (address[] memory) {
return assetToPools[asset];
}

/**
* @dev Gets total number of pools
*/
function getTotalPools() external view returns (uint256) {
return allPools.length;
}

/**
* @dev Toggles pool active status (emergency control)
*/
function setPoolStatus(address pool, bool isActive) external onlyOwner {
require(pools[pool].poolAddress != address(0), "Pool does not exist");
pools[pool].isActive = isActive;
emit PoolStatusChanged(pool, isActive);
}
}

Upgradeable Smart Contract Pattern​

Using OpenZeppelin's proxy pattern for upgradeable contracts:

// contracts/LendingPoolV1.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

/**
* @title LendingPoolV1
* @dev Upgradeable lending pool implementation
*/
contract LendingPoolV1 is
Initializable,
OwnableUpgradeable,
PausableUpgradeable,
ReentrancyGuardUpgradeable
{
struct UserInfo {
uint256 deposited;
uint256 borrowed;
uint256 collateral;
uint256 lastUpdateTime;
uint256 rewardDebt;
}

struct PoolParams {
uint256 baseBorrowRate; // Base borrow rate (18 decimals)
uint256 multiplier; // Multiplier for utilization rate
uint256 jumpMultiplier; // Jump multiplier after kink
uint256 kink; // Utilization rate at which jump multiplier kicks in
uint256 collateralFactor; // Maximum LTV ratio (18 decimals)
uint256 liquidationThreshold; // Liquidation threshold (18 decimals)
uint256 liquidationBonus; // Liquidation bonus (18 decimals)
uint256 reserveFactor; // Reserve factor (18 decimals)
}

IERC20Upgradeable public asset;
string public poolName;
PoolParams public params;

uint256 public totalDeposits;
uint256 public totalBorrows;
uint256 public totalReserves;
uint256 public lastAccrualTime;
uint256 public borrowIndex;

mapping(address => UserInfo) public users;
address[] public userList;

event Deposit(address indexed user, uint256 amount, uint256 timestamp);
event Withdraw(address indexed user, uint256 amount, uint256 timestamp);
event Borrow(address indexed user, uint256 amount, uint256 timestamp);
event Repay(address indexed user, uint256 amount, uint256 timestamp);
event Liquidation(
address indexed liquidator,
address indexed borrower,
uint256 debtAmount,
uint256 collateralSeized
);

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

/**
* @dev Initializer function (replaces constructor for upgradeable contracts)
*/
function initialize(
address _asset,
string memory _name,
PoolParams memory _params,
address _owner
) public initializer {
__Ownable_init();
__Pausable_init();
__ReentrancyGuard_init();

asset = IERC20Upgradeable(_asset);
poolName = _name;
params = _params;
borrowIndex = 1e18;
lastAccrualTime = block.timestamp;

_transferOwnership(_owner);
}

/**
* @dev Deposits assets into the lending pool
* @param amount Amount to deposit
*/
function deposit(uint256 amount) external nonReentrant whenNotPaused {
require(amount > 0, "Amount must be greater than 0");

accrueInterest();

UserInfo storage user = users[msg.sender];
if (user.deposited == 0) {
userList.push(msg.sender);
}

user.deposited += amount;
user.lastUpdateTime = block.timestamp;
totalDeposits += amount;

require(
asset.transferFrom(msg.sender, address(this), amount),
"Transfer failed"
);

emit Deposit(msg.sender, amount, block.timestamp);
}

/**
* @dev Calculates current utilization rate
*/
function getUtilizationRate() public view returns (uint256) {
if (totalDeposits == 0) return 0;
return (totalBorrows * 1e18) / totalDeposits;
}

/**
* @dev Calculates current borrow rate based on utilization
*/
function getBorrowRate() public view returns (uint256) {
uint256 util = getUtilizationRate();

if (util <= params.kink) {
return params.baseBorrowRate + (util * params.multiplier) / 1e18;
} else {
uint256 normalRate = params.baseBorrowRate +
(params.kink * params.multiplier) / 1e18;
uint256 excessUtil = util - params.kink;
return normalRate + (excessUtil * params.jumpMultiplier) / 1e18;
}
}

/**
* @dev Accrues interest to borrows and reserves
*/
function accrueInterest() public {
uint256 currentTime = block.timestamp;
if (currentTime == lastAccrualTime) return;

if (totalBorrows == 0) {
lastAccrualTime = currentTime;
return;
}

uint256 timeDelta = currentTime - lastAccrualTime;
uint256 borrowRate = getBorrowRate();
uint256 interestFactor = (borrowRate * timeDelta) / (365 days);

uint256 interestAccumulated = (totalBorrows * interestFactor) / 1e18;
uint256 reserveContribution = (interestAccumulated * params.reserveFactor) / 1e18;

totalBorrows += interestAccumulated;
totalReserves += reserveContribution;
borrowIndex += (borrowIndex * interestFactor) / 1e18;
lastAccrualTime = currentTime;
}

/**
* @dev Borrows assets from the pool
* @param amount Amount to borrow
*/
function borrow(uint256 amount) external nonReentrant whenNotPaused {
require(amount > 0, "Amount must be greater than 0");
require(amount <= getAvailableToBorrow(), "Insufficient liquidity");

accrueInterest();

UserInfo storage user = users[msg.sender];

// Calculate new borrow amount with interest
uint256 newBorrowAmount = user.borrowed + amount;

// Check collateral requirements
require(
isValidBorrow(msg.sender, newBorrowAmount),
"Insufficient collateral"
);

user.borrowed = newBorrowAmount;
user.lastUpdateTime = block.timestamp;
totalBorrows += amount;

require(asset.transfer(msg.sender, amount), "Transfer failed");

emit Borrow(msg.sender, amount, block.timestamp);
}

/**
* @dev Checks if a borrow amount is valid for a user
*/
function isValidBorrow(address user, uint256 borrowAmount) public view returns (bool) {
UserInfo memory userInfo = users[user];
if (userInfo.collateral == 0) return borrowAmount == 0;

uint256 maxBorrow = (userInfo.collateral * params.collateralFactor) / 1e18;
return borrowAmount <= maxBorrow;
}

/**
* @dev Gets available liquidity for borrowing
*/
function getAvailableToBorrow() public view returns (uint256) {
return totalDeposits - totalBorrows;
}

/**
* @dev Emergency pause function
*/
function pause() external onlyOwner {
_pause();
}

/**
* @dev Unpause function
*/
function unpause() external onlyOwner {
_unpause();
}

/**
* @dev Get contract version for upgrade tracking
*/
function version() external pure returns (string memory) {
return "1.0.0";
}
}

Practical Exercise 1: Proxy Deployment​

// scripts/deployUpgradeable.js
const { ethers, upgrades } = require("hardhat");

async function main() {
console.log("Deploying upgradeable LendingPool...");

const [deployer] = await ethers.getSigners();
console.log("Deploying with account:", deployer.address);

// Deploy implementation contract
const LendingPoolV1 = await ethers.getContractFactory("LendingPoolV1");

// Configuration parameters
const poolParams = {
baseBorrowRate: ethers.utils.parseEther("0.02"), // 2% base rate
multiplier: ethers.utils.parseEther("0.15"), // 15% multiplier
jumpMultiplier: ethers.utils.parseEther("3.0"), // 300% jump multiplier
kink: ethers.utils.parseEther("0.8"), // 80% kink
collateralFactor: ethers.utils.parseEther("0.75"), // 75% LTV
liquidationThreshold: ethers.utils.parseEther("0.83"), // 83% liquidation
liquidationBonus: ethers.utils.parseEther("0.05"), // 5% bonus
reserveFactor: ethers.utils.parseEther("0.1") // 10% reserve
};

// Deploy proxy
const lendingPool = await upgrades.deployProxy(
LendingPoolV1,
[
process.env.ASSET_ADDRESS, // Asset token address
"ETH Lending Pool", // Pool name
poolParams, // Pool parameters
deployer.address // Owner
],
{ initializer: 'initialize' }
);

await lendingPool.deployed();

console.log("LendingPool deployed to:", lendingPool.address);

// Verify the deployment
const version = await lendingPool.version();
console.log("Contract version:", version);

// Save deployment addresses
const deploymentInfo = {
proxy: lendingPool.address,
implementation: await upgrades.erc1967.getImplementationAddress(lendingPool.address),
admin: await upgrades.erc1967.getAdminAddress(lendingPool.address)
};

console.log("Deployment info:", deploymentInfo);
}

main()
.then(() => process.exit(0))
.catch(error => {
console.error(error);
process.exit(1);
});

2. Automated Market Maker (AMM) Implementation​

Constant Product AMM with Advanced Features​

// contracts/AdvancedAMM.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";

/**
* @title AdvancedAMM
* @dev Automated Market Maker with dynamic fees and concentrated liquidity
*/
contract AdvancedAMM is ERC20, ReentrancyGuard, Ownable {
using Math for uint256;

IERC20 public immutable token0;
IERC20 public immutable token1;

uint256 public reserve0;
uint256 public reserve1;
uint256 public kLast; // reserve0 * reserve1 at last liquidity event

// Fee structure
uint256 public constant FEE_DENOMINATOR = 10000;
uint256 public baseFee = 30; // 0.3% base fee
uint256 public maxFee = 100; // 1% max fee
uint256 public protocolFee = 500; // 5% of trading fees go to protocol

// Liquidity mining
uint256 public rewardRate = 1e18; // Rewards per second
uint256 public lastUpdateTime;
uint256 public rewardPerTokenStored;
mapping(address => uint256) public userRewardPerTokenPaid;
mapping(address => uint256) public rewards;

// Price oracle
uint256 public price0CumulativeLast;
uint256 public price1CumulativeLast;
uint32 public blockTimestampLast;

// Events
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(uint256 reserve0, uint256 reserve1);
event RewardPaid(address indexed user, uint256 reward);

modifier updateReward(address account) {
rewardPerTokenStored = rewardPerToken();
lastUpdateTime = block.timestamp;

if (account != address(0)) {
rewards[account] = earned(account);
userRewardPerTokenPaid[account] = rewardPerTokenStored;
}
_;
}

constructor(
address _token0,
address _token1,
string memory _name,
string memory _symbol
) ERC20(_name, _symbol) {
require(_token0 != _token1, "Identical tokens");

(address tokenA, address tokenB) = _token0 < _token1 ?
(_token0, _token1) : (_token1, _token0);

token0 = IERC20(tokenA);
token1 = IERC20(tokenB);

lastUpdateTime = block.timestamp;
}

/**
* @dev Adds liquidity to the pool
* @param amount0Desired Desired amount of token0
* @param amount1Desired Desired amount of token1
* @param amount0Min Minimum amount of token0
* @param amount1Min Minimum amount of token1
* @param to Address to mint LP tokens to
*/
function addLiquidity(
uint256 amount0Desired,
uint256 amount1Desired,
uint256 amount0Min,
uint256 amount1Min,
address to
) external nonReentrant updateReward(to) returns (uint256 liquidity) {
(uint256 amount0, uint256 amount1) = _addLiquidity(
amount0Desired,
amount1Desired,
amount0Min,
amount1Min
);

liquidity = mint(to, amount0, amount1);
}

/**
* @dev Internal function to calculate optimal amounts for adding liquidity
*/
function _addLiquidity(
uint256 amount0Desired,
uint256 amount1Desired,
uint256 amount0Min,
uint256 amount1Min
) internal view returns (uint256 amount0, uint256 amount1) {
if (reserve0 == 0 && reserve1 == 0) {
(amount0, amount1) = (amount0Desired, amount1Desired);
} else {
uint256 amount1Optimal = (amount0Desired * reserve1) / reserve0;
if (amount1Optimal <= amount1Desired) {
require(amount1Optimal >= amount1Min, "Insufficient token1 amount");
(amount0, amount1) = (amount0Desired, amount1Optimal);
} else {
uint256 amount0Optimal = (amount1Desired * reserve0) / reserve1;
require(amount0Optimal <= amount0Desired);
require(amount0Optimal >= amount0Min, "Insufficient token0 amount");
(amount0, amount1) = (amount0Optimal, amount1Desired);
}
}
}

/**
* @dev Mints liquidity tokens
*/
function mint(address to, uint256 amount0, uint256 amount1) internal returns (uint256 liquidity) {
uint256 balance0 = token0.balanceOf(address(this));
uint256 balance1 = token1.balanceOf(address(this));

require(
token0.transferFrom(msg.sender, address(this), amount0) &&
token1.transferFrom(msg.sender, address(this), amount1),
"Transfer failed"
);

uint256 _totalSupply = totalSupply();
if (_totalSupply == 0) {
liquidity = Math.sqrt(amount0 * amount1) - 1000; // Minimum liquidity lock
_mint(address(0), 1000); // Lock minimum liquidity
} else {
liquidity = Math.min(
(amount0 * _totalSupply) / reserve0,
(amount1 * _totalSupply) / reserve1
);
}

require(liquidity > 0, "Insufficient liquidity minted");
_mint(to, liquidity);

_update(balance0, balance1);

if (kLast != 0) kLast = reserve0 * reserve1;

emit Mint(msg.sender, amount0, amount1);
}

/**
* @dev Swaps tokens with dynamic fee calculation
* @param amount0Out Amount of token0 to receive
* @param amount1Out Amount of token1 to receive
* @param to Address to send tokens to
*/
function swap(
uint256 amount0Out,
uint256 amount1Out,
address to,
bytes calldata data
) external nonReentrant {
require(amount0Out > 0 || amount1Out > 0, "Insufficient output amount");
require(amount0Out < reserve0 && amount1Out < reserve1, "Insufficient liquidity");

uint256 balance0;
uint256 balance1;

{ // Scope to avoid stack too deep
require(to != address(token0) && to != address(token1), "Invalid to address");

if (amount0Out > 0) token0.transfer(to, amount0Out);
if (amount1Out > 0) token1.transfer(to, amount1Out);

if (data.length > 0) {
// Flash swap callback
IFlashSwapCallback(to).flashSwapCall(
msg.sender,
amount0Out,
amount1Out,
data
);
}

balance0 = token0.balanceOf(address(this));
balance1 = 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");

{ // Scope for dynamic fee calculation
uint256 currentFee = calculateDynamicFee(amount0In + amount1In);
uint256 balance0Adjusted = balance0 * 1000 - amount0In * currentFee;
uint256 balance1Adjusted = balance1 * 1000 - amount1In * currentFee;

require(
balance0Adjusted * balance1Adjusted >=
uint256(reserve0) * reserve1 * (1000**2),
"K invariant violated"
);
}

_update(balance0, balance1);

emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}

/**
* @dev Calculates dynamic fee based on trade size and market conditions
*/
function calculateDynamicFee(uint256 tradeSize) public view returns (uint256) {
uint256 poolSize = reserve0 + reserve1;
if (poolSize == 0) return baseFee;

// Increase fee for larger trades relative to pool size
uint256 tradeSizeRatio = (tradeSize * 1e18) / poolSize;
uint256 additionalFee = (tradeSizeRatio * (maxFee - baseFee)) / 1e18;

return Math.min(baseFee + additionalFee, maxFee);
}

/**
* @dev Updates reserves and price accumulators
*/
function _update(uint256 balance0, uint256 balance1) private {
uint32 blockTimestamp = uint32(block.timestamp % 2**32);
uint32 timeElapsed = blockTimestamp - blockTimestampLast;

if (timeElapsed > 0 && reserve0 != 0 && reserve1 != 0) {
price0CumulativeLast += uint256((reserve1 * 1e18) / reserve0) * timeElapsed;
price1CumulativeLast += uint256((reserve0 * 1e18) / reserve1) * timeElapsed;
}

reserve0 = balance0;
reserve1 = balance1;
blockTimestampLast = blockTimestamp;

emit Sync(reserve0, reserve1);
}

// Liquidity Mining Functions

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) *
(rewardPerToken() - userRewardPerTokenPaid[account])) / 1e18 + rewards[account];
}

function claimReward() external nonReentrant updateReward(msg.sender) {
uint256 reward = rewards[msg.sender];
if (reward > 0) {
rewards[msg.sender] = 0;
// Transfer reward token (implementation depends on reward token)
emit RewardPaid(msg.sender, reward);
}
}
}

interface IFlashSwapCallback {
function flashSwapCall(
address sender,
uint256 amount0,
uint256 amount1,
bytes calldata data
) external;
}

Practical Exercise 2: AMM Testing Suite​

// test/AdvancedAMM.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");

describe("AdvancedAMM", function() {
async function deployAMMFixture() {
const [owner, alice, bob, carol] = await ethers.getSigners();

// Deploy mock tokens
const MockToken = await ethers.getContractFactory("MockERC20");
const tokenA = await MockToken.deploy("Token A", "TKA", ethers.utils.parseEther("1000000"));
const tokenB = await MockToken.deploy("Token B", "TKB", ethers.utils.parseEther("1000000"));

// Deploy AMM
const AdvancedAMM = await ethers.getContractFactory("AdvancedAMM");
const amm = await AdvancedAMM.deploy(
tokenA.address,
tokenB.address,
"LP Token",
"LP"
);

// Distribute tokens
await tokenA.transfer(alice.address, ethers.utils.parseEther("10000"));
await tokenA.transfer(bob.address, ethers.utils.parseEther("10000"));
await tokenB.transfer(alice.address, ethers.utils.parseEther("10000"));
await tokenB.transfer(bob.address, ethers.utils.parseEther("10000"));

return { amm, tokenA, tokenB, owner, alice, bob, carol };
}

describe("Liquidity Management", function() {
it("Should add initial liquidity correctly", async function() {
const { amm, tokenA, tokenB, alice } = await loadFixture(deployAMMFixture);

const amount0 = ethers.utils.parseEther("100");
const amount1 = ethers.utils.parseEther("200");

await tokenA.connect(alice).approve(amm.address, amount0);
await tokenB.connect(alice).approve(amm.address, amount1);

await expect(
amm.connect(alice).addLiquidity(
amount0,
amount1,
amount0,
amount1,
alice.address
)
).to.emit(amm, "Mint");

const lpBalance = await amm.balanceOf(alice.address);
expect(lpBalance).to.be.gt(0);

const reserves = await amm.reserve0();
expect(reserves).to.equal(amount0);
});

it("Should calculate optimal amounts correctly", async function() {
const { amm, tokenA, tokenB, alice, bob } = await loadFixture(deployAMMFixture);

// Add initial liquidity 1:2 ratio
await tokenA.connect(alice).approve(amm.address, ethers.utils.parseEther("100"));
await tokenB.connect(alice).approve(amm.address, ethers.utils.parseEther("200"));

await amm.connect(alice).addLiquidity(
ethers.utils.parseEther("100"),
ethers.utils.parseEther("200"),
0, 0,
alice.address
);

// Bob tries to add liquidity with different ratio
await tokenA.connect(bob).approve(amm.address, ethers.utils.parseEther("50"));
await tokenB.connect(bob).approve(amm.address, ethers.utils.parseEther("150"));

// Should adjust to maintain 1:2 ratio
await amm.connect(bob).addLiquidity(
ethers.utils.parseEther("50"),
ethers.utils.parseEther("150"),
0, 0,
bob.address
);

const reserve0 = await amm.reserve0();
const reserve1 = await amm.reserve1();

// Check ratio is maintained (approximately)
const ratio = reserve1.mul(1000).div(reserve0);
expect(ratio).to.be.closeTo(2000, 1); // 2.0 with tolerance
});
});

describe("Trading", function() {
it("Should perform swaps with dynamic fees", async function() {
const { amm, tokenA, tokenB, alice, bob } = await loadFixture(deployAMMFixture);

// Add liquidity
await tokenA.connect(alice).approve(amm.address, ethers.utils.parseEther("1000"));
await tokenB.connect(alice).approve(amm.address, ethers.utils.parseEther("2000"));

await amm.connect(alice).addLiquidity(
ethers.utils.parseEther("1000"),
ethers.utils.parseEther("2000"),
0, 0,
alice.address
);

// Bob performs a small trade
const smallTradeAmount = ethers.utils.parseEther("10");
await tokenA.connect(bob).transfer(amm.address, smallTradeAmount);

const smallFee = await amm.calculateDynamicFee(smallTradeAmount);
expect(smallFee).to.equal(30); // Base fee

// Bob performs a large trade
const largeTradeAmount = ethers.utils.parseEther("100");
const largeFee = await amm.calculateDynamicFee(largeTradeAmount);
expect(largeFee).to.be.gt(30); // Higher than base fee
});

it("Should maintain K invariant after swaps", async function() {
const { amm, tokenA, tokenB, alice, bob } = await loadFixture(deployAMMFixture);

// Setup liquidity
await tokenA.connect(alice).approve(amm.address, ethers.utils.parseEther("1000"));
await tokenB.connect(alice).approve(amm.address, ethers.utils.parseEther("1000"));

await amm.connect(alice).addLiquidity(
ethers.utils.parseEther("1000"),
ethers.utils.parseEther("1000"),
0, 0,
alice.address
);

const initialK = (await amm.reserve0()).mul(await amm.reserve1());

// Perform swap
await tokenA.connect(bob).approve(amm.address, ethers.utils.parseEther("100"));
await tokenA.connect(bob).transfer(amm.address, ethers.utils.parseEther("100"));

await amm.connect(bob).swap(0, ethers.utils.parseEther("90"), bob.address, "0x");

const finalK = (await amm.reserve0()).mul(await amm.reserve1());
expect(finalK).to.be.gte(initialK); // K should not decrease
});
});

describe("Liquidity Mining", function() {
it("Should accrue rewards for liquidity providers", async function() {
const { amm, tokenA, tokenB, alice } = await loadFixture(deployAMMFixture);

// Add liquidity
await tokenA.connect(alice).approve(amm.address, ethers.utils.parseEther("100"));
await tokenB.connect(alice).approve(amm.address, ethers.utils.parseEther("100"));

await amm.connect(alice).addLiquidity(
ethers.utils.parseEther("100"),
ethers.utils.parseEther("100"),
0, 0,
alice.address
);

// Fast forward time
await ethers.provider.send("evm_increaseTime", [86400]); // 1 day
await ethers.provider.send("evm_mine");

const earned = await amm.earned(alice.address);
expect(earned).to.be.gt(0);
});
});

describe("Price Oracle", function() {
it("Should update price accumulators correctly", async function() {
const { amm, tokenA, tokenB, alice, bob } = await loadFixture(deployAMMFixture);

// Add liquidity
await tokenA.connect(alice).approve(amm.address, ethers.utils.parseEther("1000"));
await tokenB.connect(alice).approve(amm.address, ethers.utils.parseEther("2000"));

await amm.connect(alice).addLiquidity(
ethers.utils.parseEther("1000"),
ethers.utils.parseEther("2000"),
0, 0,
alice.address
);

const initialPrice0Cumulative = await amm.price0CumulativeLast();

// Fast forward time and perform trade
await ethers.provider.send("evm_increaseTime", [3600]); // 1 hour

await tokenA.connect(bob).approve(amm.address, ethers.utils.parseEther("10"));
await tokenA.connect(bob).transfer(amm.address, ethers.utils.parseEther("10"));
await amm.connect(bob).swap(0, ethers.utils.parseEther("19"), bob.address, "0x");

const finalPrice0Cumulative = await amm.price0CumulativeLast();
expect(finalPrice0Cumulative).to.be.gt(initialPrice0Cumulative);
});
});
});

3. Governance and DAO Implementation​

Comprehensive Governance System​

// contracts/GovernanceToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

/**
* @title GovernanceToken
* @dev ERC20 token with voting capabilities for governance
*/
contract GovernanceToken is ERC20, ERC20Permit, ERC20Votes, Ownable {
constructor(
string memory name,
string memory symbol,
uint256 initialSupply
) ERC20(name, symbol) ERC20Permit(name) {
_mint(msg.sender, initialSupply);
}

function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
}

// Override required by Solidity
function _afterTokenTransfer(
address from,
address to,
uint256 amount
) internal override(ERC20, ERC20Votes) {
super._afterTokenTransfer(from, to, amount);
}

function _mint(address to, uint256 amount) internal override(ERC20, ERC20Votes) {
super._mint(to, amount);
}

function _burn(address account, uint256 amount) internal override(ERC20, ERC20Votes) {
super._burn(account, amount);
}
}
// contracts/GovernanceDAO.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/governance/Governor.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorSettings.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol";

/**
* @title GovernanceDAO
* @dev Complete governance system with timelock controls
*/
contract GovernanceDAO is
Governor,
GovernorSettings,
GovernorCountingSimple,
GovernorVotes,
GovernorVotesQuorumFraction,
GovernorTimelockControl
{
constructor(
IVotes _token,
TimelockController _timelock,
uint256 _quorumPercentage,
uint256 _votingDelay,
uint256 _votingPeriod
)
Governor("GovernanceDAO")
GovernorSettings(_votingDelay, _votingPeriod, 0)
GovernorVotes(_token)
GovernorVotesQuorumFraction(_quorumPercentage)
GovernorTimelockControl(_timelock)
{}

// Override required functions
function votingDelay() public view override(IGovernor, GovernorSettings) returns (uint256) {
return super.votingDelay();
}

function votingPeriod() public view override(IGovernor, GovernorSettings) returns (uint256) {
return super.votingPeriod();
}

function quorum(uint256 blockNumber)
public
view
override(IGovernor, GovernorVotesQuorumFraction)
returns (uint256)
{
return super.quorum(blockNumber);
}

function state(uint256 proposalId)
public
view
override(Governor, GovernorTimelockControl)
returns (ProposalState)
{
return super.state(proposalId);
}

function propose(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
string memory description
) public override(Governor, IGovernor) returns (uint256) {
return super.propose(targets, values, calldatas, description);
}

function proposalThreshold() public view override(Governor, GovernorSettings) returns (uint256) {
return super.proposalThreshold();
}

function _execute(
uint256 proposalId,
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) internal override(Governor, GovernorTimelockControl) {
super._execute(proposalId, targets, values, calldatas, descriptionHash);
}

function _cancel(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) internal override(Governor, GovernorTimelockControl) returns (uint256) {
return super._cancel(targets, values, calldatas, descriptionHash);
}

function _executor() internal view override(Governor, GovernorTimelockControl) returns (address) {
return super._executor();
}

function supportsInterface(bytes4 interfaceId)
public
view
override(Governor, GovernorTimelockControl)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}

4. Flash Loan Implementation​

Secure Flash Loan Provider​

// contracts/FlashLoanProvider.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

interface IFlashLoanReceiver {
function executeOperation(
address asset,
uint256 amount,
uint256 fee,
bytes calldata params
) external returns (bool);
}

/**
* @title FlashLoanProvider
* @dev Provides flash loans with configurable fees
*/
contract FlashLoanProvider is ReentrancyGuard, Ownable {
mapping(address => bool) public supportedAssets;
mapping(address => uint256) public assetFees; // Fee in basis points (10000 = 100%)

uint256 public constant MAX_FEE = 500; // 5% maximum fee
uint256 public defaultFee = 9; // 0.09% default fee

event FlashLoan(
address indexed receiver,
address indexed asset,
uint256 amount,
uint256 fee
);

event AssetAdded(address indexed asset, uint256 fee);
event FeeUpdated(address indexed asset, uint256 oldFee, uint256 newFee);

modifier onlySupportedAsset(address asset) {
require(supportedAssets[asset], "Asset not supported");
_;
}

/**
* @dev Adds support for a new asset
* @param asset The ERC20 token address
* @param fee Fee in basis points
*/
function addAsset(address asset, uint256 fee) external onlyOwner {
require(asset != address(0), "Invalid asset address");
require(fee <= MAX_FEE, "Fee too high");

supportedAssets[asset] = true;
assetFees[asset] = fee;

emit AssetAdded(asset, fee);
}

/**
* @dev Updates fee for an asset
* @param asset The ERC20 token address
* @param newFee New fee in basis points
*/
function updateFee(address asset, uint256 newFee) external onlyOwner onlySupportedAsset(asset) {
require(newFee <= MAX_FEE, "Fee too high");

uint256 oldFee = assetFees[asset];
assetFees[asset] = newFee;

emit FeeUpdated(asset, oldFee, newFee);
}

/**
* @dev Executes a flash loan
* @param receiver The contract that will receive the flash loan
* @param asset The asset to borrow
* @param amount The amount to borrow
* @param params Additional parameters for the receiver
*/
function flashLoan(
address receiver,
address asset,
uint256 amount,
bytes calldata params
) external nonReentrant onlySupportedAsset(asset) {
require(receiver != address(0), "Invalid receiver");
require(amount > 0, "Amount must be greater than 0");

IERC20 token = IERC20(asset);
uint256 availableBalance = token.balanceOf(address(this));
require(amount <= availableBalance, "Insufficient liquidity");

uint256 fee = (amount * getFee(asset)) / 10000;
uint256 balanceBeforeLoan = availableBalance;

// Transfer tokens to receiver
require(token.transfer(receiver, amount), "Transfer failed");

// Execute the receiver's logic
require(
IFlashLoanReceiver(receiver).executeOperation(asset, amount, fee, params),
"Flash loan execution failed"
);

// Verify repayment
uint256 balanceAfterLoan = token.balanceOf(address(this));
require(
balanceAfterLoan >= balanceBeforeLoan + fee,
"Flash loan not repaid with fee"
);

emit FlashLoan(receiver, asset, amount, fee);
}

/**
* @dev Gets the fee for an asset
* @param asset The asset address
* @return The fee in basis points
*/
function getFee(address asset) public view returns (uint256) {
return supportedAssets[asset] ? assetFees[asset] : defaultFee;
}

/**
* @dev Gets available liquidity for an asset
* @param asset The asset address
* @return Available balance
*/
function getAvailableLiquidity(address asset) external view returns (uint256) {
return IERC20(asset).balanceOf(address(this));
}

/**
* @dev Emergency withdrawal function
* @param asset Asset to withdraw
* @param amount Amount to withdraw
* @param to Recipient address
*/
function emergencyWithdraw(
address asset,
uint256 amount,
address to
) external onlyOwner {
require(to != address(0), "Invalid recipient");
require(IERC20(asset).transfer(to, amount), "Transfer failed");
}
}

5. Practical Assignment: Build a Complete DeFi Protocol​

Project: Multi-Asset Lending Protocol with Governance​

Build a comprehensive DeFi lending protocol that includes:

Core Components Required:​

  1. Multi-Asset Lending Pool

    • Support for multiple ERC20 tokens
    • Dynamic interest rates based on utilization
    • Collateralization and liquidation mechanics
    • Flash loan integration
  2. Automated Market Maker

    • Constant product formula with dynamic fees
    • Liquidity mining rewards
    • Price oracle integration
    • Impermanent loss protection
  3. Governance System

    • ERC20 governance token with voting power
    • Proposal creation and voting mechanisms
    • Timelock controller for security
    • Parameter adjustment capabilities
  4. Advanced Features

    • Cross-chain compatibility preparation
    • Yield optimization strategies
    • Insurance fund mechanisms
    • Analytics and monitoring tools

Technical Specifications:​

// Example: Protocol Registry Pattern
contract ProtocolRegistry is Ownable {
struct ProtocolInfo {
address implementation;
uint256 version;
bool isActive;
string name;
}

mapping(string => ProtocolInfo) public protocols;
mapping(address => bool) public authorizedUpdaters;

event ProtocolRegistered(string name, address implementation, uint256 version);
event ProtocolUpdated(string name, address oldImpl, address newImpl, uint256 version);

function registerProtocol(
string memory name,
address implementation,
uint256 version
) external onlyOwner {
protocols[name] = ProtocolInfo({
implementation: implementation,
version: version,
isActive: true,
name: name
});

emit ProtocolRegistered(name, implementation, version);
}
}

Deliverables Required:​

  1. Smart Contract Suite (40 points)

    • All contracts with comprehensive documentation
    • Gas-optimized implementations
    • Full security considerations
    • Upgradeability mechanisms
  2. Testing Framework (25 points)

    • 95% code coverage

    • Integration tests
    • Stress testing scenarios
    • Gas usage optimization reports
  3. Frontend Application (20 points)

    • React-based dApp
    • Web3 integration
    • Real-time data updates
    • Responsive design
  4. Documentation & Analysis (15 points)

    • Technical documentation
    • Security audit report
    • Economic model analysis
    • Deployment guides

Evaluation Criteria:​

  • Innovation (25%): Novel features and improvements
  • Security (25%): Robust security practices and audit results
  • Code Quality (20%): Clean, maintainable, well-documented code
  • Functionality (15%): All features work as specified
  • Performance (15%): Gas optimization and efficient algorithms

Timeline: 4 weeks​

  • Week 1: Core lending functionality
  • Week 2: AMM and trading features
  • Week 3: Governance and advanced features
  • Week 4: Testing, documentation, and optimization

Resources and Professional Development​

Industry-Standard Tools​

  • Foundry: Advanced testing framework
  • Slither: Static analysis for security
  • Mythril: Security analysis tool
  • Tenderly: Simulation and monitoring platform
  • Defender: Automated operations and security

Advanced Learning Resources​

  • DeFi Protocol Analysis: Study Uniswap V3, Compound, Aave codebases
  • Security Best Practices: Trail of Bits security guidelines
  • Gas Optimization: EVM deep dive and optimization techniques
  • MEV Protection: Flashbots and MEV-resistant design patterns

Professional Certification​

  • ConsenSys Academy: Blockchain Developer Certification
  • ChainSafe: Advanced Solidity Development
  • OpenZeppelin: Smart Contract Security Auditing
  • DeFi Alliance: DeFi Protocol Development

Next Tutorial: Cross-Chain Protocols and Layer 2 Scaling Solutions