DeFi Patterns
** Factory & Pool Patterns**
Factory contracts are fundamental to scalable DeFi architectures. They solve a critical problem: how to efficiently deploy and manage multiple instances of similar contracts (like lending pools or AMM pairs) without duplicating deployment logic.
Solution: Use a factory contract to deploy clones.
Example: Lending Pool Factory
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface ILendingPool {
function initialize(address _token) external;
}
contract LendingPool {
address public token;
bool public initialized;
// Using initialize instead of constructor for proxy compatibility
function initialize(address _token) external {
require(!initialized, "Already initialized");
token = _token;
initialized = true;
}
}
contract PoolFactory {
event PoolCreated(address indexed pool, address indexed token);
// Track all pools by token address
mapping(address => address) public getPool;
address[] public allPools;
function createPool(address token) external returns (address pool) {
require(getPool[token] == address(0), "Pool exists");
// Create new pool
pool = address(new LendingPool());
ILendingPool(pool).initialize(token);
// Update registry
getPool[token] = pool;
allPools.push(pool);
emit PoolCreated(pool, token);
}
function getAllPools() external view returns (address[] memory) {
return allPools;
}
}
Use Case: Uniswap’s PairFactory, Aave’s LendingPoolConfigurator.
** Upgradeable Contracts (Proxy Pattern)**
Problem: Smart contracts are immutable, but upgrades may be needed.
Solution: Separate logic (implementation) from storage (proxy).
Minimal Proxy Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// Implementation Contract (Upgradeable Logic)
contract LogicV1 {
uint public value;
address public admin;
function initialize(address _admin) external {
require(admin == address(0), "Already initialized");
admin = _admin;
}
function setValue(uint _value) external {
require(msg.sender == admin, "Unauthorized");
value = _value;
}
}
// Proxy Contract
contract Proxy {
address public implementation;
address public admin;
constructor(address _impl) {
implementation = _impl;
admin = msg.sender;
}
modifier onlyAdmin {
require(msg.sender == admin, "Not admin");
_;
}
function upgradeTo(address newImpl) external onlyAdmin {
implementation = newImpl;
}
fallback() external payable {
(bool success, ) = implementation.delegatecall(msg.data);
require(success, "Delegatecall failed");
}
receive() external payable {}
}
Use Case: OpenZeppelin’s TransparentProxy, Aave’s Configurator.
** Pull-over-Push Payments**
Problem: Direct transfers (transfer) can fail if the recipient is a contract.
Solution: Let users withdraw funds themselves (pull payments).
Example: Secure Withdrawals
mapping(address => uint) public balances;
function withdraw() external {
uint bal = balances[msg.sender];
balances[msg.sender] = 0;
payable(msg.sender).transfer(bal);
}
Use Case: Used in staking contracts (e.g., Synthetix, Lido).
** Reentrancy Protection**
Problem: Malicious contracts can re-enter functions during transfer.
Solution: Use nonReentrant modifier or checks-effects-interactions.
OpenZeppelin’s ReentrancyGuard
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SecureVault is ReentrancyGuard {
mapping(address => uint256) public balances;
function deposit(uint256 amount) external {
balances[msg.sender] += amount;
}
function withdraw() external nonReentrant {
uint256 bal = balances[msg.sender];
balances[msg.sender] = 0;
payable(msg.sender).transfer(bal); // safe transfer
}
}
Use Case: All major DeFi protocols (Aave, Compound).
** Circuit Breaker (Pause Mechanism)**
Problem: Exploits require emergency shutdown.
Solution: Add a pause function for critical operations.
Example: Pausable Contract
import "@openzeppelin/contracts/security/Pausable.sol";
contract Pool is Pausable {
function deposit() external whenNotPaused { /* ... */ }
function emergencyPause() external onlyOwner { _pause(); }
}
Use Case: MakerDAO, Compound.
** Oracle Security (Pull vs. Push)**
. Oracles bridge off-chain data to on-chain contracts, creating a critical attack vector:
. Manipulation risks (flash loan attacks, stale data)
. Centralization risks (single data source failure)
Problem: On-chain price feeds can be manipulated.
Solution: Use pull-based oracles (e.g., Chainlink).
Example: Safe Price Feed
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract PullOracle {
AggregatorV3Interface internal priceFeed;
uint256 public lastUpdated;
uint256 public priceStaleThreshold = 1 hours;
constructor(address _aggregator) {
priceFeed = AggregatorV3Interface(_aggregator);
}
function getLatestPrice() public returns (uint256) {
// Pull fresh data from Chainlink
(, int256 price,, uint256 updatedAt,) = priceFeed.latestRoundData();
require(updatedAt > lastUpdated, "Stale price");
require(price > 0, "Invalid price");
lastUpdated = block.timestamp;
return uint256(price);
}
function isPriceFresh() public view returns (bool) {
return block.timestamp - lastUpdated <= priceStaleThreshold;
}
}
Use Case: Chainlink, Uniswap TWAP.
** Timelock for Admin Actions**
Problem: Admins can change parameters instantly (risky!).
Solution: Enforce a delay before execution.
Example: Governance Timelock
mapping(bytes32 => uint) public queuedTx;
function queueTx(bytes32 txHash, uint delay) external onlyOwner {
queuedTx[txHash] = block.timestamp + delay;
}
function executeTx(bytes32 txHash) external {
require(block.timestamp >= queuedTx[txHash], "Too early");
delete queuedTx[txHash];
// Execute tx...
}
Use Case: Compound, Aave governance.
** Flashloan Attack Mitigation**
Problem: Flashloans enable single-tx exploits.
Solution: Limit actions per block.
Example: Block-Level Protection
mapping(address => uint) public lastActionBlock;
function borrow(uint amount) external {
require(lastActionBlock[msg.sender] < block.number, "Flashloan blocked");
lastActionBlock[msg.sender] = block.number;
// Borrow logic...
}
Use Case: Aave v3, dYdX.
** Liquidation & Close Factor**
Problem: Liquidators can seize too much collateral.
Solution: Limit repay amount (closeFactor).
Example: Safe Liquidation
uint public closeFactor = 0.5e18; // Max 50% repay
uint public liquidationBonus = 1.08e18; // 8% bonus
function liquidate(address user, uint repay) external {
uint maxRepay = borrows[user] * closeFactor / 1e18;
require(repay <= maxRepay, "Exceeds closeFactor");
// Liquidate...
}
Use Case: Compound, Aave.
** Multi-Collateral & LTV Ratios**
Problem: Users deposit multiple assets as collateral.
Solution: Track collateral per asset and enforce Loan-to-Value (LTV).
Example: Collateral Tracking
mapping(address => mapping(address => uint)) public collateral;
function depositCollateral(address token, uint amount) external {
IERC20(token).transferFrom(msg.sender, address(this), amount);
collateral[msg.sender][token] += amount;
}
Use Case: MakerDAO, Aave.
** Slippage Protection (AMMs)**
Problem: Swaps can fail due to price impact.
Solution: Enforce a minimum output amount.
Example: Uniswap-Style Swap
function swap(uint amountIn, uint minOut) external {
uint amountOut = getAmountOut(amountIn);
require(amountOut >= minOut, "Slippage too high");
// Execute swap...
}
Use Case: Uniswap, SushiSwap.
** Interest Rate Index (Compound Model)**
Problem: Accruing interest for all users is gas-heavy.
Solution: Use a global borrow index.
Example: Interest Accrual
uint public borrowIndex = 1e18;
function accrueInterest(uint rate) public {
borrowIndex += borrowIndex * rate / 1e18;
}
Use Case: Compound, Aave.
* Emergency Token Rescue**
Problem: Tokens accidentally sent to contracts get stuck.
Solution: Allow admin to rescue non-core tokens.
Example: Admin Recovery
function rescueToken(IERC20 token, address to) external onlyOwner {
require(token != coreToken, "Cannot withdraw core asset");
token.transfer(to, token.balanceOf(address(this)));
}
Use Case: Used in most DeFi protocols.
** Access Control / Role management**
Hack Prevented: Unauthorized contract changes or admin actions.
Pattern : Use Ownable or role-based control.
import "@openzeppelin/contracts/access/Ownable.sol";
contract AdminControl is Ownable {
function pausePool() external onlyOwner {
// emergency pause
}
}