Skip to main content

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
}
}