본문으로 건너뛰기

DeFi Security I: Smart Contract Vulnerabilities

The $3.8 Billion Problem

On August 10, 2021, Poly Network—a cross-chain bridge protocol—was hacked. The attacker exploited a single smart contract vulnerability and drained $611 million across three blockchains. It was the largest DeFi hack in history at that time.

The vulnerability? A simple access control flaw: the attacker called a privileged function that should have been restricted to administrators. Within hours, the entire treasury was gone.

But here's the shocking part: The hacker returned the funds. They later claimed it was a "white hat" operation to expose vulnerabilities. The crypto world breathed a sigh of relief, but the incident exposed an uncomfortable truth:

DeFi is a hacker's paradise.

The numbers tell the story:

DeFi losses by year:

2020: $162 million stolen (8 major hacks)
2021: $1.8 billion stolen (23 major hacks)
2022: $3.1 billion stolen (163 exploits)
2023: $1.8 billion stolen (231 exploits)
2024: $1.2 billion+ stolen (YTD)

Total (2020-2024): $8+ billion
Average hack: $15 million
Median hack: $2.5 million
Largest single hack: $611M (Poly Network)

Why DeFi is uniquely vulnerable:

1. Code is law (no exceptions):

Traditional finance:
Bug detected → Transaction reversed
Hack occurs → Funds frozen
Mistake made → Bank reverses it

DeFi:
Bug detected → Already exploited
Hack occurs → Funds gone forever
Mistake made → Loss is permanent

Smart contracts execute exactly as written
No "customer service" to call
No "undo" button

2. Money + code = maximum incentive:

Traditional software bugs:
Impact: Website crash, data loss, inconvenience
Attacker gain: Reputation damage, data theft

DeFi bugs:
Impact: Immediate financial loss
Attacker gain: Direct monetary profit
Incentive: Maximum (take millions instantly)

Result: Every line of code under attack
Billions at stake
Professional hackers targeting

3. Transparent targets:

All code is open source (verifiable)
All TVL is visible (know exactly how much to steal)
All transactions are public (can test attacks on-chain)

Attackers can:
- Read entire codebase
- Simulate attacks off-chain
- See exact vulnerability
- Calculate exact profit
- Execute atomically (all or nothing)

Result: Attackers have massive advantage

4. Composability = complexity:

DeFi protocols interact with each other
Bug in one protocol affects all that depend on it
Cascading failures possible
Attack surface grows exponentially

Example:
Protocol A uses Protocol B for oracle
Protocol B has bug
Attacker exploits B to manipulate oracle
All protocols using B's oracle compromised

5. Immutability = no patches:

Traditional software: Deploy patch, update all instances
Smart contracts: Immutable once deployed

If bug found:
- Cannot be patched (code is permanent)
- Must deploy new contract (lose TVL)
- Or use upgrade proxy (adds complexity, centralization)
- Meanwhile: Attackers can exploit anytime

This lesson explores the most common and costly smart contract vulnerabilities—the mistakes that have cost billions and continue to plague DeFi:

What we'll cover:

  • Reentrancy: The DAO hack and how recursive calls steal funds
  • Integer overflow/underflow: When math breaks
  • Access control: Who can call what (and why it matters)
  • Logic errors: Flawed assumptions and edge cases
  • Flash loan attacks: Borrowing millions to manipulate markets
  • Oracle manipulation: Exploiting price feeds
  • Front-running and MEV exploitation
  • Best practices and security patterns

Real hacks we'll analyze:

  • The DAO (2016): $60M stolen via reentrancy
  • Parity Multisig (2017): $150M frozen forever
  • bZx (2020): $1M stolen via flash loan manipulation
  • Harvest Finance (2020): $34M stolen via flash loan + oracle
  • Cream Finance (2021): $130M stolen via reentrancy
  • Poly Network (2021): $611M stolen via access control

Understanding these vulnerabilities isn't just academic—it's essential survival knowledge for anyone building or using DeFi. Every protocol is under constant attack. Every bug will be found. And the cost of mistakes is measured in millions.

Let's explore how attackers exploit smart contracts—and how defenders can protect against them.

Reentrancy: The $60 Million Recursive Call

The DAO Hack (June 2016)

Background:

The DAO (Decentralized Autonomous Organization) was the first major DeFi project:

Launched: April 2016
Raised: $150M in ETH (14% of all ETH)
Purpose: Decentralized venture capital fund
Mechanism: Token holders vote on investments
Market cap: $250M at peak

Largest crowdfunding in history
Highest expectations
Biggest disaster

The vulnerability:

Simple withdraw function:

contract DAO {
mapping(address => uint256) public balances;

// VULNERABLE CODE
function withdraw() public {
uint256 amount = balances[msg.sender];

// Send ETH BEFORE updating balance
(bool success, ) = msg.sender.call{value: amount}("");
require(success);

// Update balance AFTER sending
balances[msg.sender] = 0;
}
}

Why this is vulnerable:

Normal execution:
1. Check balance: 10 ETH
2. Send 10 ETH to user
3. Set balance to 0
✓ No problem

Malicious execution:
1. Check balance: 10 ETH
2. Send 10 ETH to attacker contract
→ Attacker's fallback function called!
→ fallback() calls withdraw() again
→ Balance still 10 ETH (not updated yet)
3. Check balance: 10 ETH (still!)
4. Send 10 ETH again
5. Loop continues...
6. Finally updates balance to 0 (when recursion ends)

Result: Drained contract

The attack in detail:

contract Attacker {
DAO public dao;

constructor(address _dao) {
dao = DAO(_dao);
}

// Step 1: Deposit some ETH
function deposit() public payable {
dao.deposit{value: msg.value}();
}

// Step 2: Start the attack
function attack() public {
dao.withdraw();
}

// Step 3: Fallback function (called when receiving ETH)
fallback() external payable {
// Reenter if DAO still has balance
if (address(dao).balance >= 1 ether) {
dao.withdraw();
}
}
}

Execution trace:

Attacker deposits: 1 ETH
DAO state: {Attacker: 1 ETH, Contract: 1 ETH}

Attacker calls attack():
1. withdraw() called
- amount = 1 ETH
- Send 1 ETH to Attacker
→ Attacker.fallback() triggered
→ withdraw() called AGAIN (reentry!)
- amount = 1 ETH (still!)
- Send 1 ETH to Attacker
→ Attacker.fallback() triggered
→ withdraw() called AGAIN
- amount = 1 ETH (still!)
- Send 1 ETH to Attacker
... (continues until DAO empty)
- balances[Attacker] = 0 (finally)

Result:
Attacker withdrew: 1 ETH x N times
Where N = DAO balance / 1 ETH

The actual hack:

June 17, 2016, 3:34 AM UTC:
Attacker begins draining DAO

Timeline:
00:00 - First recursive withdrawal
01:00 - 3.6M ETH drained (~$60M)
03:00 - Community notices attack
06:00 - Panic spreads
12:00 - 30% of DAO funds stolen
24:00 - Attack paused (DAO has holding period)

Total stolen: $60 million (in 2016 values)
Ethereum response: Hard fork to return funds
Controversy: Immutability vs. justice
Result: Ethereum Classic split off (opposed to fork)

How Reentrancy Works

The pattern:

1. External call made (send ETH, call function)
2. Control transferred to attacker
3. Attacker calls back into vulnerable contract
4. State not yet updated
5. Exploit occurs

Call stack visualization:

Contract A (Victim)            Contract B (Attacker)
─────────────────────────────────────────────────────
withdraw()
├─ Check balance: 10 ETH
├─ Send 10 ETH ────────────► receive()
│ ├─ Call withdraw()
│ │ ├─ Check balance: 10 ETH
│ │ ├─ Send 10 ETH ────────► receive()
│ │ │ ├─ Call withdraw()
│ │ │ │ ├─ Check: 10 ETH
│ │ │ │ ├─ Send...
│ │ │ │ └─ Update: 0
│ │ │ └─ (return)
│ │ └─ Update balance: 0
│ └─ (return)
└─ Update balance: 0

Problem: Balance checked multiple times before update

Types of reentrancy:

1. Single-function reentrancy (simplest):

function withdraw() public {
uint256 amount = balances[msg.sender];
msg.sender.call{value: amount}(""); // Reenter here
balances[msg.sender] = 0;
}

Attack: Call withdraw() → reenter withdraw()

2. Cross-function reentrancy:

function withdraw() public {
uint256 amount = balances[msg.sender];
msg.sender.call{value: amount}("");
balances[msg.sender] = 0;
}

function transfer(address to, uint256 amount) public {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
balances[to] += amount;
}

Attack:
1. Call withdraw()
2. During callback, call transfer()
3. Balance not yet updated, transfer succeeds
4. Double spend

3. Cross-contract reentrancy:

// Contract A
function withdraw() public {
uint256 amount = balances[msg.sender];
msg.sender.call{value: amount}("");
balances[msg.sender] = 0;
contractB.updateRewards(msg.sender); // Update after withdrawal
}

// Contract B
function updateRewards(address user) public {
// Uses contractA.balances[user]
// But balance not yet updated!
}

Attack:
1. Withdraw from Contract A
2. During callback, interact with Contract B
3. Contract B sees stale balance
4. Double rewards or other exploit

Prevention: Checks-Effects-Interactions

The golden rule:

1. Checks: Validate inputs, check conditions
2. Effects: Update state variables
3. Interactions: External calls, send ETH

NEVER make external calls before updating state

Correct implementation:

function withdraw() public {
// 1. CHECKS
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");

// 2. EFFECTS (update state FIRST)
balances[msg.sender] = 0;

// 3. INTERACTIONS (external call LAST)
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}

Why this is safe:

Attack attempt:
1. withdraw() called, balance = 10 ETH
2. Balance set to 0 (BEFORE external call)
3. Send 10 ETH to attacker
→ Attacker.fallback() triggered
→ Calls withdraw() again (reentry)
4. Check balance: 0 ETH (already updated!)
5. Revert (no balance to withdraw)

Attack fails ✓

Reentrancy guard pattern:

contract ReentrancyGuard {
uint256 private _status;
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;

constructor() {
_status = _NOT_ENTERED;
}

modifier nonReentrant() {
require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
_status = _ENTERED;
_;
_status = _NOT_ENTERED;
}
}

contract Safe is ReentrancyGuard {
mapping(address => uint256) public balances;

function withdraw() public nonReentrant { // Protected!
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0;

(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}
}

How the guard works:

First call to withdraw():
1. Check _status: _NOT_ENTERED ✓
2. Set _status: _ENTERED
3. Execute function body
4. Set _status: _NOT_ENTERED

Reentrant call to withdraw():
1. Check _status: _ENTERED ✗
2. Revert immediately

Attack prevented ✓

Real-World Reentrancy Attacks

Cream Finance (August 2021): $25M

// Vulnerable: flash loan + reentrancy
function flashLoan(uint256 amount) external {
uint256 balanceBefore = token.balanceOf(address(this));
token.transfer(msg.sender, amount);

// External call (reentrancy risk)
IFlashLoanReceiver(msg.sender).executeOperation(amount);

uint256 balanceAfter = token.balanceOf(address(this));
require(balanceAfter >= balanceBefore + fee);
}

Attack:
1. Take flash loan
2. During executeOperation callback:
- Call borrow() on Cream market
- Use flash loaned tokens as collateral
- Borrow before flash loan repaid
3. Repay flash loan
4. Keep borrowed funds

Loss: $25M

Uniswap V1 (theoretical, never exploited):

// Uniswap V1 had potential reentrancy
function tokenToEthSwap(uint256 tokens_sold) public {
uint256 eth_out = getOutputPrice(tokens_sold);

// Transfer tokens first (could reenter via token contract)
token.transferFrom(msg.sender, address(this), tokens_sold);

// Send ETH (could reenter)
msg.sender.transfer(eth_out);
}

Mitigation: Checks-effects-interactions
Uniswap V2+: Fixed ordering, reentrancy guards

Lendf.me (April 2020): $25M

// ERC777 token reentrancy
function supply(address token, uint256 amount) public {
// External call via ERC777 hooks
IERC777(token).transferFrom(msg.sender, address(this), amount);

// State update AFTER transfer
balances[msg.sender] += amount;
}

Attack:
1. Supply ERC777 token
2. ERC777 calls sender's hook (tokensReceived)
3. In hook, call supply() again
4. Reenters before balance updated
5. Double credit

Loss: $25M (all funds recovered)

Integer Overflow and Underflow

The Mathematical Vulnerability

Solidity < 0.8.0: No automatic overflow checks

// Solidity 0.7.x and below
uint8 public value = 255;

function overflow() public {
value = value + 1; // What happens?
}

Expected: Error (255 + 1 too large for uint8)
Actual: value = 0 (wraps around!)

Why this happens:

uint8 range: 0 to 255 (2^8 - 1)
Binary representation (8 bits):

255 in binary: 11111111
Add 1: +00000001
─────────────
Result: 100000000 (9 bits)

But uint8 is only 8 bits:
Overflow bit discarded: 00000000 = 0

Result: 255 + 1 = 0 (wrap around)

Underflow (opposite problem):

uint8 public value = 0;

function underflow() public {
value = value - 1; // What happens?
}

Expected: Error (0 - 1 is negative)
Actual: value = 255 (wraps to maximum!)

Why:
0 in binary: 00000000
Subtract 1: -00000001
─────────────
Result (2's complement): 11111111 = 255

Result: 0 - 1 = 255 (wrap to max)

Real Attack: BeautyChain (April 2018)

The vulnerability:

contract BeautyToken {
mapping(address => uint256) public balances;

function batchTransfer(address[] recipients, uint256 value) public {
// VULNERABLE: No overflow check
uint256 amount = recipients.length * value;
require(balances[msg.sender] >= amount);

balances[msg.sender] -= amount;

for (uint256 i = 0; i < recipients.length; i++) {
balances[recipients[i]] += value;
}
}
}

The exploit:

Attacker's balance: 0 tokens

Attack:
recipients = [addr1, addr2] (2 addresses)
value = 2^255 (very large number)

Calculation:
amount = 2 * 2^255 = 2^256

But uint256 max = 2^256 - 1
So: 2^256 = 0 (overflow!)

Check: balances[attacker] >= 0 ✓ (passes!)
Update: balances[attacker] -= 0 (no change)

Then:
balances[addr1] += 2^255 (massive amount!)
balances[addr2] += 2^255 (massive amount!)

Result: Created tokens from nothing
Loss: All token value destroyed (hyperinflation)

Impact:

Date: April 22, 2018
Tokens created: Trillions (essentially infinite)
Market cap before: $4.5 billion
Market cap after: ~$0
Exchange trading: Halted
Blockchain: Permanently damaged

Integer Overflow Exploit Patterns

Pattern 1: Balance overflow

// Vulnerable
function transfer(address to, uint256 amount) public {
balances[msg.sender] -= amount; // Could underflow!
balances[to] += amount; // Could overflow!
}

Attack:
balance[attacker] = 100
transfer(victim, 200)

balances[attacker] -= 200
100 - 200 = underflow
100 - 200 = 2^256 - 100 (wrap around!)
→ Attacker now has ~infinite balance

Pattern 2: Multiplication overflow

function calculateReward(uint256 amount, uint256 multiplier)
public
pure
returns (uint256)
{
return amount * multiplier; // Could overflow!
}

Attack:
amount = 2^128
multiplier = 2^128
result = 2^256 = 0 (overflow)

Expected: Huge reward
Actual: 0 reward

Pattern 3: Time-based overflow

function withdraw() public {
require(block.timestamp >= lastWithdraw + 1 days); // Could overflow!
lastWithdraw = block.timestamp;
// ... withdraw logic
}

Attack (theoretical):
Set lastWithdraw = 2^256 - 1 (near max)
Wait for block.timestamp to overflow
Condition passes immediately

Prevention: SafeMath and Solidity 0.8+

SafeMath library (pre-0.8):

library SafeMath {
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a, "SafeMath: addition overflow");
return c;
}

function sub(uint256 a, uint256 b) internal pure returns (uint256) {
require(b <= a, "SafeMath: subtraction underflow");
uint256 c = a - b;
return c;
}

function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) return 0;
uint256 c = a * b;
require(c / a == b, "SafeMath: multiplication overflow");
return c;
}
}

// Usage
using SafeMath for uint256;

function transfer(address to, uint256 amount) public {
balances[msg.sender] = balances[msg.sender].sub(amount); // Safe!
balances[to] = balances[to].add(amount); // Safe!
}

Solidity 0.8+ (automatic checks):

// Solidity 0.8.0 and above
uint8 public value = 255;

function overflow() public {
value = value + 1; // Automatically reverts!
}

// No SafeMath needed, built-in protection
function transfer(address to, uint256 amount) public {
balances[msg.sender] -= amount; // Reverts on underflow
balances[to] += amount; // Reverts on overflow
}

When overflow is intentional (rare):

// Solidity 0.8.0+
function intentionalOverflow() public {
uint8 value = 255;

// Use unchecked block to skip checks
unchecked {
value = value + 1; // Allows overflow, value = 0
}
}

// Use case: Gas optimization when overflow is impossible
function optimizedLoop(uint256[] memory data) public {
for (uint256 i = 0; i < data.length; ) {
// Process data[i]
unchecked { i++; } // Save gas (i can't overflow in practice)
}
}

Access Control Vulnerabilities

The Parity Multisig Disaster (July 2017)

Background:

Parity: Popular Ethereum wallet
Multisig: Wallet requiring multiple signatures
Used by: ICOs, DAOs, exchanges
Total at risk: $300+ million

The vulnerability:

contract WalletLibrary {
address public owner;

// VULNERABLE: Public initialization function
function initWallet(address[] owners, uint256 required) public {
require(owner == 0); // Only init if not initialized
owner = owners[0];
// ... more initialization
}
}

contract Wallet {
address public library = 0x...; // WalletLibrary address

// Delegate all calls to library
fallback() external payable {
library.delegatecall(msg.data);
}
}

The exploit:

Step 1: Attacker notices initWallet() is public

Step 2: Attacker calls Wallet.initWallet()
→ Delegatecall to WalletLibrary.initWallet()
→ Sets owner to attacker's address

Step 3: Attacker now owns the wallet
→ Can call any owner-only functions
→ Can withdraw all funds

July 19, 2017:
Attacker steals $32M from multiple wallets

But it gets worse... (November 2017):

GitHub user "devops199" (likely accidentally):

Step 1: Calls WalletLibrary.initWallet() directly
(Sets self as owner of LIBRARY contract)

Step 2: Calls selfdestruct() on library
(Owner privilege, destroys library code)

Result:
- Library code deleted from blockchain
- All wallets using library become inoperable
- $150M+ frozen FOREVER
- No way to recover (code gone, wallets bricked)

Affected: 587 wallets, including major funds
Status: Funds still frozen (2024)
Lesson: Access control + upgradability = danger

Common Access Control Mistakes

1. Missing access control:

// VULNERABLE: Anyone can call
function mint(address to, uint256 amount) public {
balances[to] += amount;
totalSupply += amount;
}

// Should be:
function mint(address to, uint256 amount) public onlyOwner {
balances[to] += amount;
totalSupply += amount;
}

2. Incorrect modifier:

// VULNERABLE: Empty modifier
modifier onlyOwner() {
// Forgot to add check!
_;
}

// Should be:
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}

3. Wrong visibility:

// VULNERABLE: Public when should be internal
function _burn(address account, uint256 amount) public {
balances[account] -= amount;
totalSupply -= amount;
}

// Convention: _ prefix = internal/private
// Should be:
function _burn(address account, uint256 amount) internal {
balances[account] -= amount;
totalSupply -= amount;
}

4. tx.origin authentication:

// VULNERABLE: Uses tx.origin instead of msg.sender
function withdraw() public {
require(tx.origin == owner, "Not owner");
payable(owner).transfer(address(this).balance);
}

// Attack via malicious contract:
contract Phishing {
VulnerableContract target;

function attack() public {
// Trick owner into calling this
target.withdraw();
// tx.origin = owner (the caller)
// msg.sender = Phishing contract
// Check passes, funds stolen!
}
}

// Should use msg.sender:
function withdraw() public {
require(msg.sender == owner, "Not owner");
payable(owner).transfer(address(this).balance);
}

5. Delegatecall with untrusted target:

// VULNERABLE: Delegatecall to user-controlled address
function execute(address target, bytes memory data) public {
target.delegatecall(data); // Dangerous!
}

// Attack:
contract Evil {
function attack() public {
// This runs in context of caller contract
// Can modify caller's storage
// Can selfdestruct caller
}
}

// Attacker calls: execute(Evil, "attack()")
// Evil code runs with caller's storage access
// Complete contract compromise

Poly Network Hack (August 2021): $611M

The vulnerability:

contract EthCrossChainManager {
// Whitelist of contracts that can call privileged functions
mapping(bytes => address) public contractAddresses;

// VULNERABLE: Allows changing critical addresses
function crossChain(
uint64 toChainId,
bytes memory method,
bytes memory args
) public {
// Verify signature (implementation omitted)
bytes memory signature = verifySignature(toChainId, method, args);

// Call target contract
address targetContract = contractAddresses[method];
(bool success, ) = targetContract.call(args);
}
}

The exploit:

The attacker noticed:
1. crossChain() can call ANY whitelisted contract
2. One whitelisted contract: EthCrossChainData
3. EthCrossChainData has putCurEpochConPubKeyBytes()
4. This function changes validator public keys
5. No check that crossChain() can't call this function

Attack:
1. Call crossChain() targeting putCurEpochConPubKeyBytes()
2. Replace validator public keys with attacker's keys
3. Now attacker can sign transactions
4. Transfer all funds to attacker

Result: $611M stolen across 3 chains

Why it worked:

Design flaw:
- Function whitelisting, but not function-level permissions
- crossChain() could call ANY function on whitelisted contracts
- Including functions that should be admin-only

Should have been:
- Function-level whitelisting
- Explicit permission for each method
- Principle of least privilege

Best Practices for Access Control

1. OpenZeppelin's Ownable pattern:

import "@openzeppelin/contracts/access/Ownable.sol";

contract MyToken is Ownable {
// Owner automatically set to deployer

function mint(address to, uint256 amount) public onlyOwner {
// Only owner can mint
_mint(to, amount);
}

function transferOwnership(address newOwner) public override onlyOwner {
// Transfer ownership safely
super.transferOwnership(newOwner);
}
}

2. Role-based access control (RBAC):

import "@openzeppelin/contracts/access/AccessControl.sol";

contract MyContract is AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");

constructor() {
// Grant admin role to deployer
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
}

function mint(address to, uint256 amount)
public
onlyRole(MINTER_ROLE)
{
_mint(to, amount);
}

function burn(address from, uint256 amount)
public
onlyRole(BURNER_ROLE)
{
_burn(from, amount);
}

// Admin can grant/revoke roles
function grantMinter(address account) public onlyRole(DEFAULT_ADMIN_ROLE) {
grantRole(MINTER_ROLE, account);
}
}

3. Timelocks for critical operations:

contract TimelockController {
uint256 public constant DELAY = 2 days;

struct QueuedTransaction {
address target;
bytes data;
uint256 executeTime;
}

mapping(bytes32 => QueuedTransaction) public queuedTransactions;

function queueTransaction(address target, bytes memory data)
public
onlyOwner
returns (bytes32)
{
bytes32 txHash = keccak256(abi.encode(target, data));
queuedTransactions[txHash] = QueuedTransaction({
target: target,
data: data,
executeTime: block.timestamp + DELAY
});
return txHash;
}

function executeTransaction(bytes32 txHash) public {
QueuedTransaction memory txn = queuedTransactions[txHash];
require(block.timestamp >= txn.executeTime, "Too early");
require(txn.executeTime != 0, "Not queued");

delete queuedTransactions[txHash];
(bool success, ) = txn.target.call(txn.data);
require(success);
}
}

// Gives users time to exit if malicious upgrade queued

4. Multi-signature requirements:

contract MultiSig {
address[] public owners;
uint256 public required; // Number of signatures needed

mapping(uint256 => Transaction) public transactions;
mapping(uint256 => mapping(address => bool)) public confirmations;

struct Transaction {
address to;
uint256 value;
bytes data;
bool executed;
}

function submitTransaction(address to, uint256 value, bytes memory data)
public
onlyOwner
returns (uint256)
{
uint256 txId = transactions.length;
transactions[txId] = Transaction({
to: to,
value: value,
data: data,
executed: false
});
confirmTransaction(txId);
return txId;
}

function confirmTransaction(uint256 txId) public onlyOwner {
confirmations[txId][msg.sender] = true;
if (isConfirmed(txId)) {
executeTransaction(txId);
}
}

function isConfirmed(uint256 txId) public view returns (bool) {
uint256 count = 0;
for (uint256 i = 0; i < owners.length; i++) {
if (confirmations[txId][owners[i]]) {
count++;
}
}
return count >= required;
}

function executeTransaction(uint256 txId) internal {
Transaction storage txn = transactions[txId];
require(!txn.executed, "Already executed");
require(isConfirmed(txId), "Not confirmed");

txn.executed = true;
(bool success, ) = txn.to.call{value: txn.value}(txn.data);
require(success, "Execution failed");
}
}

Logic Errors and Edge Cases

bZx Flash Loan Attack (February 2020): $1M

The protocol:

bZx: Margin trading and lending protocol
Feature: Flash loans for leveraged trading
Bug: Flawed price oracle logic

The vulnerability:

contract bZx {
function marginTrade(
address loanToken,
address collateralToken,
uint256 loanAmount,
uint256 leverage
) public {
// 1. Take flash loan
uint256 totalAmount = loanAmount * leverage;

// 2. Get price from Uniswap
uint256 price = uniswap.getPrice(collateralToken, loanToken);

// 3. Use borrowed funds to buy collateral
uniswap.swap(loanToken, collateralToken, totalAmount);

// 4. Value collateral at current price
uint256 collateralValue = collateral * price;

// FLAW: Price oracle (Uniswap) was manipulated in step 3!
}
}

The attack:

Step 1: Borrow 10,000 ETH via flash loan (from dYdX)

Step 2: Use 5,100 ETH to open leveraged position on bZx:
- Borrow 1,300 ETH from bZx
- Use to buy sUSD on Uniswap (manipulation begins)
- Massive buy pushes sUSD price up

Step 3: Sell remaining 5,000 ETH for sUSD on Kyber:
- Get sUSD at manipulated high price
- Profit from the price difference

Step 4: Repay flash loan

Result:
- bZx's oracle (Uniswap) showed inflated sUSD price
- Attacker borrowed against overvalued collateral
- bZx left with bad debt
- Attacker profit: $350k

Second attack (same day):
- Similar mechanism, different tokens
- Profit: $633k

Total loss: ~$1M

Why it worked:

Logic error: Using manipulable price oracle

bZx assumed:
- Uniswap price is accurate
- Single trade won't move price significantly
- Oracle reads reflect external market

Reality:
- Attacker moved Uniswap price within same transaction
- Oracle read manipulated price
- All atomic (flash loan enables large capital)

Harvest Finance Flash Loan Attack (October 2020): $34M

The vulnerability:

contract HarvestVault {
function deposit(uint256 amount) public {
// Deposit stablecoin
stablecoin.transferFrom(msg.sender, address(this), amount);

// Calculate share value based on Curve pool price
uint256 sharePrice = curvePool.get_virtual_price();
uint256 shares = amount / sharePrice;

_mint(msg.sender, shares);
}

function withdraw(uint256 shares) public {
// Calculate withdrawal amount based on current price
uint256 sharePrice = curvePool.get_virtual_price();
uint256 amount = shares * sharePrice;

_burn(msg.sender, shares);
stablecoin.transfer(msg.sender, amount);
}
}

The exploit:

Attack used imbalanced Curve pool swaps to manipulate price:

Step 1: Flash loan $50M USDT (from Uniswap)

Step 2: Swap USDT → USDC on Curve (large amount)
→ Imbalances Curve pool
→ virtual_price increases

Step 3: Deposit to Harvest at inflated price
→ Get more shares than should

Step 4: Reverse Curve swap: USDC → USDT
→ Rebalances pool
→ virtual_price decreases

Step 5: Withdraw from Harvest at new (lower) price
→ Shares worth more than deposited

Step 6: Repay flash loan, keep profit

Repeated 7 times in succession:
Total profit: $34M

Why it worked:

Logic flaw: Price oracle manipulation

Harvest's calculation:
deposit_value = curve.get_virtual_price() * amount

But get_virtual_price() changes with pool imbalance
Attacker could manipulate within single transaction

Should have used:
- Time-weighted average price (TWAP)
- Multiple independent oracles
- Manipulation resistance checks

Common Logic Errors

1. Rounding errors:

// VULNERABLE: Always rounds down
function calculateReward(uint256 amount) public pure returns (uint256) {
return amount * REWARD_RATE / PRECISION; // Integer division
}

// Example:
amount = 100
REWARD_RATE = 5
PRECISION = 100
reward = 100 * 5 / 100 = 500 / 100 = 5

But if amount = 50:
reward = 50 * 5 / 100 = 250 / 100 = 2 (not 2.5)
Lost: 0.5 per calculation

Over millions of users: Significant loss

2. Timestamp dependence:

// VULNERABLE: Miners can manipulate timestamp
function claimReward() public {
require(block.timestamp >= lastClaim + 1 days, "Too early");
lastClaim = block.timestamp;
// ...
}

// Miner can:
// - Set timestamp 15 seconds in future (allowed)
// - Allow early claims (small advantage)
// - Grief users (delay timestamp)

// Better: Use block numbers
function claimReward() public {
require(block.number >= lastClaimBlock + 6400, "Too early");
// 6400 blocks ≈ 1 day
lastClaimBlock = block.number;
}

3. Unchecked external call return values:

// VULNERABLE: Ignores return value
function withdraw(uint256 amount) public {
balances[msg.sender] -= amount;
token.transfer(msg.sender, amount); // What if this returns false?
}

// Some ERC20s return false instead of reverting
// If transfer fails, user loses balance anyway

// Should be:
function withdraw(uint256 amount) public {
balances[msg.sender] -= amount;
require(token.transfer(msg.sender, amount), "Transfer failed");
}

// Or use SafeERC20:
using SafeERC20 for IERC20;

function withdraw(uint256 amount) public {
balances[msg.sender] -= amount;
token.safeTransfer(msg.sender, amount); // Reverts on failure
}

4. Frontrunning vulnerabilities:

// VULNERABLE: Publicly visible before execution
function buyNFT(uint256 tokenId, uint256 maxPrice) public {
uint256 price = nft.getPrice(tokenId);
require(price <= maxPrice, "Too expensive");
nft.buy{value: price}(tokenId);
}

// Attack:
// 1. Attacker sees transaction in mempool
// 2. Attacker frontruns with higher gas:
// - Buys NFT first
// 3. Seller's transaction fails or pays higher price
// 4. Attacker profits

// Mitigation: Commit-reveal scheme
mapping(bytes32 => Purchase) public commits;

function commitPurchase(bytes32 hash) public {
commits[hash] = Purchase(msg.sender, block.number);
}

function revealPurchase(uint256 tokenId, uint256 salt) public {
bytes32 hash = keccak256(abi.encode(tokenId, salt, msg.sender));
require(commits[hash].buyer == msg.sender, "Not committed");
require(block.number > commits[hash].block + 1, "Too early");

// Now execute purchase
nft.buy(tokenId);
}

5. Denial of service via gas limits:

// VULNERABLE: Unbounded loop
address[] public users;

function distributeRewards() public {
for (uint256 i = 0; i < users.length; i++) {
rewards[users[i]] += calculateReward(users[i]);
}
}

// Problem: As users.length grows, gas cost increases
// Eventually: Exceeds block gas limit
// Result: Function becomes uncallable

// Solution: Batch processing or pull pattern
function claimReward() public {
uint256 reward = calculateReward(msg.sender);
require(reward > 0, "No reward");
rewards[msg.sender] = 0;
payable(msg.sender).transfer(reward);
}

Flash Loan Attacks

What Are Flash Loans?

Definition:

Flash loan: Uncollateralized loan that must be repaid in same transaction

Properties:
- Borrow any amount (up to pool liquidity)
- No collateral required
- Must repay within same transaction
- If not repaid, entire transaction reverts

Use cases (legitimate):
- Arbitrage
- Collateral swaps
- Liquidations
- Refinancing

Use cases (attacks):
- Price oracle manipulation
- Market manipulation
- Exploit amplification

How they work:

interface IFlashLoanReceiver {
function executeOperation(
address[] memory assets,
uint256[] memory amounts,
uint256[] memory premiums,
address initiator,
bytes memory params
) external returns (bool);
}

contract FlashLoanProvider {
function flashLoan(
address receiver,
address[] memory assets,
uint256[] memory amounts,
bytes memory params
) external {
uint256[] memory balancesBefore = _getBalances(assets);

// Transfer tokens to receiver
for (uint i = 0; i < assets.length; i++) {
IERC20(assets[i]).transfer(receiver, amounts[i]);
}

// Call receiver's callback
require(
IFlashLoanReceiver(receiver).executeOperation(
assets, amounts, premiums, msg.sender, params
),
"Flash loan execution failed"
);

// Verify repayment
uint256[] memory balancesAfter = _getBalances(assets);
for (uint i = 0; i < assets.length; i++) {
require(
balancesAfter[i] >= balancesBefore[i] + premiums[i],
"Flash loan not repaid"
);
}
}
}

Atomic execution:

Single transaction:
┌────────────────────────────────────────┐
│ 1. Borrow 10,000 ETH (flash loan) │
│ 2. Use ETH for attack │
│ 3. Generate profit │
│ 4. Repay 10,000 ETH + 0.09% fee │
│ 5. Keep profit │
└────────────────────────────────────────┘

If step 4 fails → Entire transaction reverts
No risk to attacker (except gas fees)

Alpha Finance Flash Loan Attack (February 2021): $37M

The vulnerability:

contract AlphaHomora {
function leverage(
address token,
uint256 amount,
uint256 borrowAmount
) public {
// Get user's collateral
require(token.transferFrom(msg.sender, address(this), amount));

// Borrow funds from Iron Bank
uint256 borrowed = ironBank.borrow(borrowAmount);

// Supply to Cream (for yield)
cream.supply(token, amount + borrowed);

// Calculate position value
uint256 positionValue = cream.getAccountValue(address(this));

// Update user's position
userPositions[msg.sender] = positionValue;
}
}

The exploit:

Attack used double-counting vulnerability:

Step 1: Flash loan 1,000 ETH from dYdX

Step 2: Leverage position on Alpha Homora:
- Deposit 1,000 ETH
- Borrow 10,000 ETH from Iron Bank
- Supply all 11,000 ETH to Cream

Step 3: Exploit double-counting bug:
- Alpha counted deposited ETH
- Alpha also counted borrowed ETH
- Same ETH counted twice!
- Position shows 22,000 ETH value

Step 4: Borrow against inflated position:
- Position worth "22,000 ETH"
- Can borrow maximum amount
- Actually only deposited 11,000 ETH
- Borrow more than collateral worth

Step 5: Repay flash loan, keep profit

Result: $37M stolen

Flash Loan Attack Pattern

Generic attack structure:

contract FlashLoanAttack is IFlashLoanReceiver {
function attack() external {
// 1. Initiate flash loan
flashLoanProvider.flashLoan(
address(this),
[ETH_ADDRESS],
[10000 ether],
""
);
}

function executeOperation(
address[] memory assets,
uint256[] memory amounts,
uint256[] memory premiums,
address initiator,
bytes memory params
) external override returns (bool) {
// 2. Received flash loan, now execute attack

// 2a. Manipulate price oracle
_manipulateOracle(amounts[0]);

// 2b. Exploit victim contract at manipulated price
_exploitVictim();

// 2c. Reverse manipulation (if needed)
_reverseManipulation();

// 3. Repay flash loan
uint256 repayAmount = amounts[0] + premiums[0];
IERC20(assets[0]).transfer(msg.sender, repayAmount);

// 4. Profit!
return true;
}
}

Why flash loans enable attacks:

Without flash loans:
- Need capital to exploit
- Capital = risk
- Large attacks need large capital
- Attacker must have wealth

With flash loans:
- No capital needed
- No risk (if attack fails, transaction reverts)
- Can borrow millions instantly
- Anyone can attack (even with $0)

Result: Democratized attacks
More attempts (no barrier to entry)
Larger scale (borrow unlimited)

Defenses Against Flash Loan Attacks

1. Check for flash loans:

// Simple but breakable
modifier noFlashLoan() {
require(msg.sender == tx.origin, "No contracts");
_;
}

// Better: Check for balance changes
uint256 private _balanceSnapshot;

modifier flashLoanGuard() {
uint256 balanceBefore = address(this).balance;
_;
require(
address(this).balance >= balanceBefore,
"Flash loan detected"
);
}

2. Use manipulation-resistant oracles:

// BAD: Single source, instant price
uint256 price = uniswap.getPrice(token);

// BETTER: Time-weighted average price (TWAP)
uint256 price = uniswap.getTWAP(token, 30 minutes);

// BEST: Multiple oracles + Chainlink
uint256 priceA = uniswap.getTWAP(token, 30 minutes);
uint256 priceB = chainlink.getPrice(token);
uint256 priceC = otherOracle.getPrice(token);

// Use median
uint256[] memory prices = [priceA, priceB, priceC];
uint256 medianPrice = _median(prices);

3. Rate limiting:

mapping(address => uint256) public lastAction;

function criticalOperation() public {
require(
block.timestamp >= lastAction[msg.sender] + 1 hours,
"Rate limited"
);
lastAction[msg.sender] = block.timestamp;

// Execute operation
}

// Prevents same-transaction exploits

4. Slippage protection:

function swap(
uint256 amountIn,
uint256 minAmountOut // User-specified minimum
) public {
uint256 amountOut = _calculateSwapAmount(amountIn);
require(amountOut >= minAmountOut, "Slippage too high");

// Execute swap
}

// Forces attacker to accept slippage limits
// Makes manipulation unprofitable

DeFi Security I: Smart Contract Vulnerabilities (Continued)

Oracle Manipulation Attacks

The Oracle Problem in DeFi

Why oracles are critical:

DeFi protocols need external data:
- Price feeds (for lending, liquidations, trading)
- Interest rates (for borrowing costs)
- Exchange rates (for stablecoins)
- External events (for derivatives, insurance)

But blockchains can't access external data directly
→ Need oracles to bring data on-chain
→ Oracles become critical dependency
→ Oracle compromise = protocol compromise

Types of oracle vulnerabilities:

1. On-chain oracle manipulation (DEX prices)
2. Off-chain oracle manipulation (centralized feeds)
3. Oracle latency exploitation (stale prices)
4. Oracle front-running (seeing price updates first)
5. No oracle (direct contract state reading)

Harvest Finance Deep Dive (October 2020): $34M

Protocol design:

contract HarvestVault {
ICurvePool public curvePool; // fUSDT pool

function deposit(uint256 amount) public {
// User deposits fUSDT
fUSDT.transferFrom(msg.sender, address(this), amount);

// Get "virtual price" from Curve (ORACLE)
uint256 virtualPrice = curvePool.get_virtual_price();

// Calculate shares to mint
uint256 shares = (amount * 1e18) / virtualPrice;

// Mint shares to user
_mint(msg.sender, shares);
}

function withdraw(uint256 shares) public {
// Get current virtual price (ORACLE)
uint256 virtualPrice = curvePool.get_virtual_price();

// Calculate withdrawal amount
uint256 amount = (shares * virtualPrice) / 1e18;

// Burn shares and return funds
_burn(msg.sender, shares);
fUSDT.transfer(msg.sender, amount);
}
}

What is Curve's virtual_price?

// Simplified Curve logic
contract CurvePool {
uint256[2] public balances; // [USDC, USDT]

function get_virtual_price() public view returns (uint256) {
// Price based on pool balances and invariant
uint256 D = _calculateInvariant(balances);
uint256 totalSupply = lpToken.totalSupply();

return (D * 1e18) / totalSupply;

// When pool is balanced: virtual_price stable
// When pool imbalanced: virtual_price changes
}

function exchange(
int128 i, // From token
int128 j, // To token
uint256 dx, // Amount in
uint256 min_dy // Minimum out
) public returns (uint256) {
// Swap changes balances
balances[i] += dx;
balances[j] -= dy;

// This changes virtual_price!
// Large swaps = large price impact
}
}

The attack execution:

Transaction breakdown:

┌─ Flash loan 17.2M USDT + 50M USDC (Uniswap V2) ──┐
│ │
│ Step 1: Imbalance Curve pool (manipulate up) │
│ ├─ Swap 11.4M USDT → USDC on Curve │
│ └─ virtual_price increases (pool imbalanced) │
│ │
│ Step 2: Deposit to Harvest at inflated price │
│ ├─ Deposit 60.6M fUSDT to Harvest │
│ ├─ Get shares at inflated virtual_price │
│ └─ Receive more shares than fair value │
│ │
│ Step 3: Rebalance Curve pool (manipulate down) │
│ ├─ Reverse swap: USDC → USDT on Curve │
│ └─ virtual_price decreases (pool rebalanced) │
│ │
│ Step 4: Withdraw from Harvest at normal price │
│ ├─ Redeem shares at lower virtual_price │
│ ├─ Receive more fUSDT than deposited │
│ └─ Profit extracted │
│ │
│ Step 5: Repay flash loan + profit │
│ └─ Net profit: $2.5M per transaction │
│ │
└────────────────────────────────────────────────────┘

Repeated 7 times within 4 minutes
Total profit: $34M
Gas cost: ~$50k
Net profit: $33.95M

Mathematical analysis:

Simplified calculation:

Step 1: virtual_price = 1.00 (balanced)
Swap 10M → price goes to 1.05 (+5%)

Step 2: Deposit 50M at price 1.05
Shares received = 50M / 1.05 = 47.62M shares

Step 3: Reverse swap → price returns to 1.00

Step 4: Withdraw 47.62M shares at price 1.00
Amount received = 47.62M × 1.00 = 47.62M

Wait, that's a loss?

Actually:
Step 2: Deposit 50M, get 47.62M shares (less shares)
Step 4: 47.62M shares worth 47.62M (loss $2.38M)

But the attacker did something else...

Real attack:
Step 1: Price 1.00 → 1.05 (up 5%)
Step 2: Deposit 50M, get 47.62M shares
Step 3: Price 1.05 → 0.95 (down 10% from peak!)
Step 4: Redeem 47.62M shares × 0.95 = 45.24M

Still a loss! What am I missing?

The key: Multiple pools and fee extraction

Actual mechanism:
1. Manipulate Curve price UP
2. Deposit gets poor exchange rate (pay high price)
3. Manipulate price DOWN (below starting point)
4. Withdraw at low price (receive more)
5. Profit comes from:
- Other users' deposits (trapped at wrong price)
- Protocol fees collected at wrong prices
- Slippage extracted from imbalanced pools

Why it worked:

Vulnerability: Using manipulable on-chain oracle

Curve's virtual_price is NOT an oracle:
- Reflects current pool state
- Changes with every trade
- Can be manipulated in single transaction
- No time-weighting or averaging

Harvest treated it as oracle:
- Assumed price is "fair"
- Assumed manipulation is expensive
- Didn't account for flash loans

Result:
- Attacker could manipulate price
- Extract value within single transaction
- Harvest users and protocol absorbed loss

Mango Markets Manipulation (October 2022): $110M

Different type: Off-chain oracle + market manipulation

Mango Markets: DEX with off-chain oracles

Oracle design:
- Pyth Network price feeds (off-chain aggregation)
- ~400ms latency
- Multiple data sources
- Generally reliable

The exploit:
Not oracle manipulation per se
But exploiting oracle ALONGSIDE market manipulation

Step 1: Attacker accumulates large MANGO token position
- Uses $5M to buy MANGO
- Price relatively stable

Step 2: Attacker deposits MANGO as collateral
- Opens account on Mango Markets
- Deposits MANGO tokens

Step 3: Market manipulation begins
- Uses funds to pump MANGO price on spot markets
- Price goes from $0.03 to $0.90 (30x!)
- Multiple exchanges affected

Step 4: Oracle updates to inflated price
- Pyth Network sees spot price increase
- Updates feed to ~$0.60 (average of exchanges)
- Mango Markets reads inflated collateral value

Step 5: Borrow against inflated collateral
- Collateral now worth 20x more
- Can borrow maximum against it
- Borrows $110M in various assets
- Withdraw borrowed funds

Step 6: Let MANGO price collapse
- Stop buying, price crashes
- Oracle updates to real price
- Mango Markets left with bad debt

Result: $110M stolen

Was this an oracle attack?

Debate:

Oracle manipulation?
- Oracle reported actual market prices
- Attacker didn't hack oracle
- Didn't exploit oracle logic

Market manipulation?
- Artificially inflated spot prices
- Oracle correctly reported inflated prices
- Protocol accepted inflated collateral

Both!
- Attack combined market manipulation + oracle exploitation
- Oracle was "correct" but manipulated market made it wrong
- Protocol should have:
* Limits on single-asset collateral
* Volatility checks
* Suspicious activity detection

Oracle Manipulation Patterns

Pattern 1: DEX spot price manipulation

// VULNERABLE: Using DEX reserves directly
contract Vulnerable {
IUniswapV2Pair public pair;

function getPrice() public view returns (uint256) {
(uint112 reserve0, uint112 reserve1,) = pair.getReserves();
return (reserve1 * 1e18) / reserve0;

// Can be manipulated in same transaction!
}
}

// Attack:
// 1. Flash loan
// 2. Swap massive amount on DEX → moves price
// 3. Call victim contract → reads manipulated price
// 4. Exploit
// 5. Reverse swap
// 6. Repay flash loan

Pattern 2: Time-Weighted Average Price (TWAP) bypass

// SAFER: Using Uniswap V2 TWAP
contract SaferOracle {
IUniswapV2Pair public pair;
uint256 public constant PERIOD = 30 minutes;

function getPrice() public view returns (uint256) {
uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = PERIOD;
secondsAgos[1] = 0;

(int56[] memory tickCumulatives,) =
IUniswapV3Pool(pair).observe(secondsAgos);

int56 tickCumulativeDelta =
tickCumulatives[1] - tickCumulatives[0];

int24 avgTick = int24(tickCumulativeDelta / int56(PERIOD));

return _getQuoteAtTick(avgTick);
}
}

// Much harder to manipulate:
// - Need to maintain manipulated price for 30 minutes
// - Expensive (capital tied up, fees paid)
// - Other arbitrageurs will trade against you

Pattern 3: Single oracle dependency

// VULNERABLE: Single price source
contract SingleOracle {
IChainlinkAggregator public oracle;

function getPrice() public view returns (uint256) {
(, int256 price,,,) = oracle.latestRoundData();
return uint256(price);
}
}

// Better: Multiple oracles with sanity checks
contract MultiOracle {
IChainlinkAggregator public chainlink;
IUniswapV3Pool public uniswap;

function getPrice() public view returns (uint256) {
uint256 chainlinkPrice = _getChainlinkPrice();
uint256 uniswapPrice = _getUniswapTWAP(30 minutes);

// Sanity check: Prices shouldn't deviate >10%
uint256 diff = chainlinkPrice > uniswapPrice
? chainlinkPrice - uniswapPrice
: uniswapPrice - chainlinkPrice;

require(
diff * 100 / chainlinkPrice < 10,
"Price deviation too high"
);

// Return median (or average)
return (chainlinkPrice + uniswapPrice) / 2;
}
}

Oracle Attack Defense Strategies

1. Use Chainlink or other decentralized oracle networks:

import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

contract SecureOracle {
AggregatorV3Interface internal priceFeed;
uint256 public constant MAX_DELAY = 1 hours;

constructor(address _priceFeed) {
priceFeed = AggregatorV3Interface(_priceFeed);
}

function getPrice() public view returns (uint256) {
(
uint80 roundId,
int256 price,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
) = priceFeed.latestRoundData();

// Validation checks
require(price > 0, "Invalid price");
require(updatedAt > 0, "Round not complete");
require(answeredInRound >= roundId, "Stale round");
require(
block.timestamp - updatedAt < MAX_DELAY,
"Price too stale"
);

return uint256(price);
}
}

2. Time-weighted average for DEX prices:

contract TWAPOracle {
struct Observation {
uint256 timestamp;
uint256 priceCumulative;
}

Observation[] public observations;
uint256 public constant MIN_PERIOD = 30 minutes;

function update() external {
uint256 priceCumulative = _getCurrentPriceCumulative();
observations.push(Observation({
timestamp: block.timestamp,
priceCumulative: priceCumulative
}));
}

function getTWAP() public view returns (uint256) {
require(observations.length >= 2, "Need more observations");

Observation memory latest = observations[observations.length - 1];
Observation memory earliest = observations[0];

require(
latest.timestamp - earliest.timestamp >= MIN_PERIOD,
"Period too short"
);

uint256 priceDelta = latest.priceCumulative -
earliest.priceCumulative;
uint256 timeDelta = latest.timestamp - earliest.timestamp;

return priceDelta / timeDelta;
}
}

3. Circuit breakers for price volatility:

contract CircuitBreaker {
uint256 public lastPrice;
uint256 public constant MAX_DEVIATION = 10; // 10%

function getPrice() public returns (uint256) {
uint256 newPrice = oracle.getPrice();

if (lastPrice != 0) {
uint256 deviation = newPrice > lastPrice
? (newPrice - lastPrice) * 100 / lastPrice
: (lastPrice - newPrice) * 100 / lastPrice;

require(deviation <= MAX_DEVIATION, "Circuit breaker triggered");
}

lastPrice = newPrice;
return newPrice;
}
}

4. Collateral diversity requirements:

contract DiversifiedCollateral {
mapping(address => mapping(address => uint256)) public userCollateral;
uint256 public constant MAX_SINGLE_ASSET = 50; // 50% max

function deposit(address asset, uint256 amount) public {
userCollateral[msg.sender][asset] += amount;

// Check diversification
uint256 totalValue = _getTotalCollateralValue(msg.sender);
uint256 assetValue = _getAssetValue(asset,
userCollateral[msg.sender][asset]);

require(
assetValue * 100 / totalValue <= MAX_SINGLE_ASSET,
"Too concentrated in single asset"
);
}
}

Front-Running and MEV Exploits

Understanding Front-Running

Definition:

Front-running: Observing pending transaction and executing before it

Traditional finance: Illegal (insider trading)
Blockchain: Common and difficult to prevent

How it works:
1. Victim submits transaction (enters mempool)
2. Attacker sees transaction (mempool is public)
3. Attacker submits similar transaction with higher gas
4. Attacker's transaction executes first
5. Victim's transaction executes at worse price

Example: DEX front-running

Victim wants to buy 100 ETH on Uniswap:
- Slippage tolerance: 1%
- Expected price: $2,000/ETH
- Max price willing to pay: $2,020/ETH

Attacker sees transaction in mempool:
- Knows large buy is coming
- Will push price up

Attacker's strategy (sandwich attack):
Transaction 1 (frontrun):
- Buy 50 ETH before victim
- Pushes price to $2,010/ETH
- Costs attacker: $100,250

Victim's transaction:
- Buys 100 ETH at $2,010/ETH
- Pushes price to $2,020/ETH
- Costs victim: $201,500 (worse than expected)

Transaction 2 (backrun):
- Sell 50 ETH after victim
- Gets $2,020/ETH
- Receives: $101,000

Attacker profit:
$101,000 - $100,250 = $750
(Plus gas costs of ~$50)
Net: ~$700 profit extracted from victim

MEV (Maximal Extractable Value)

What is MEV:

MEV = Profit extractable by reordering/including/excluding transactions

Sources of MEV:
1. Front-running (see above)
2. Back-running (execute after target transaction)
3. Sandwich attacks (front + back run)
4. Liquidations (first to liquidate gets bonus)
5. Arbitrage (first to exploit price differences)

Annual MEV extracted: $500M+ (2023)

Generalized front-running:

// Attacker's bot monitors mempool
contract MEVBot {
function frontrunArbitrage(
address victim,
bytes memory victimCalldata
) external {
// 1. Decode victim's transaction
(address dex, uint256 amount) = _decode(victimCalldata);

// 2. Calculate profitability
uint256 profit = _simulateArbitrage(dex, amount);

if (profit > minProfit) {
// 3. Copy victim's transaction
// 4. Execute with higher gas price
_executeArbitrage(dex, amount);
}
}
}

Real example: Front-running profitable trades

Scenario: User finds arbitrage opportunity
- Buy ETH on Uniswap: $2,000
- Sell ETH on SushiSwap: $2,050
- Profit: $50/ETH

User submits transaction:
Gas price: 50 gwei

Bot sees transaction:
1. Simulates user's transaction
2. Calculates profit: $5,000 (100 ETH trade)
3. Copies strategy
4. Submits with 51 gwei gas (higher priority)

Result:
- Bot's transaction executes first
- Captures arbitrage
- User's transaction fails or gets nothing
- Bot profit: $5,000 minus gas (~$200)

Flash Loan + MEV Attacks

Combining flash loans with MEV for maximum extraction:

Example: Liquidation with flash loan front-running

contract MEVLiquidation {
function frontrunLiquidation(
address victim,
address collateralAsset,
address debtAsset,
uint256 debtToCover
) external {
// 1. Detect imminent liquidation transaction
// 2. Take flash loan for debt amount
uint256 flashLoanAmount = debtToCover;

IFlashLoan(flashLoanProvider).flashLoan(
address(this),
debtAsset,
flashLoanAmount,
abi.encode(victim, collateralAsset, debtToCover)
);
}

function executeOperation(
address asset,
uint256 amount,
uint256 premium,
address initiator,
bytes calldata params
) external override returns (bool) {
(address victim, address collateral, uint256 debt) =
abi.decode(params, (address, address, uint256));

// 3. Liquidate position (before other liquidator)
ILendingPool(lendingPool).liquidate(
victim,
asset,
collateral,
debt,
false
);

// 4. Sell seized collateral
uint256 collateralSeized = IERC20(collateral).balanceOf(address(this));
uint256 proceeds = _sellCollateral(collateral, collateralSeized);

// 5. Repay flash loan
uint256 repayAmount = amount + premium;
IERC20(asset).transfer(msg.sender, repayAmount);

// 6. Profit = liquidation bonus + collateral value - debt - fees
return true;
}
}

Impact of MEV on users:

User experience degradation:

Average user trading $10k on DEX:
- Expected slippage: 0.3% ($30)
- Actual with sandwich attack: 0.8% ($80)
- Extra cost from MEV: $50

Annual impact:
- Total DEX volume: ~$1T
- MEV extraction: ~$500M
- Average MEV tax: 0.05% of volume

This is the "invisible tax" users pay

Flashbots and MEV-Boost

Private transaction submission to avoid front-running:

Traditional flow:
User → Mempool (public) → Miner → Block

Attacker sees and frontruns

Flashbots flow:
User → Flashbots Relay (private) → Block builder → Block

Attacker CANNOT see

How to use Flashbots:

// Flashbots example (ethers.js)
const { FlashbotsBundleProvider } = require('@flashbots/ethers-provider-bundle');

// 1. Create Flashbots provider
const flashbotsProvider = await FlashbotsBundleProvider.create(
provider,
authSigner,
'https://relay.flashbots.net'
);

// 2. Create transaction bundle
const signedTransactions = await flashbotsProvider.signBundle([
{
signer: wallet,
transaction: {
to: targetContract,
data: calldata,
gasLimit: 500000,
maxFeePerGas: parseUnits('100', 'gwei'),
maxPriorityFeePerGas: parseUnits('5', 'gwei')
}
}
]);

// 3. Submit to Flashbots
const targetBlock = await provider.getBlockNumber() + 1;
const bundleResponse = await flashbotsProvider.sendBundle(
signedTransactions,
targetBlock
);

// 4. Wait for inclusion
const resolution = await bundleResponse.wait();
if (resolution === FlashbotsBundleResolution.BundleIncluded) {
console.log('Bundle included!');
} else {
console.log('Bundle not included');
}

Benefits of Flashbots:

For users:
+ Transactions not visible in public mempool
+ No front-running risk
+ No failed transactions (bundle atomicity)
+ Can specify exact block for execution

For searchers (MEV extractors):
+ Compete on value, not gas price
+ No failed transactions burning gas
+ More sophisticated strategies possible
+ Fair ordering (highest bid wins)

For miners/validators:
+ Earn MEV revenue
+ More efficient block building
+ No congestion from failed MEV attempts

Defense Against Front-Running

1. Commit-reveal schemes:

contract CommitReveal {
mapping(address => bytes32) public commits;
mapping(address => uint256) public commitBlocks;

// Step 1: Commit to action without revealing
function commit(bytes32 hash) external {
commits[msg.sender] = hash;
commitBlocks[msg.sender] = block.number;
}

// Step 2: Reveal after some blocks
function reveal(
uint256 amount,
uint256 price,
bytes32 salt
) external {
// Verify commitment
bytes32 hash = keccak256(abi.encode(amount, price, salt));
require(commits[msg.sender] == hash, "Invalid reveal");

// Must wait at least 1 block
require(
block.number > commitBlocks[msg.sender],
"Must wait one block"
);

// Execute trade at committed parameters
_executeTrade(msg.sender, amount, price);

// Clear commitment
delete commits[msg.sender];
}
}

2. Submarine sends (gasless):

// Send transaction that looks like it failed
// But actually succeeds if specific condition met

contract SubmarineSend {
function conditionalTransfer(
address to,
uint256 amount,
bytes32 witnessHash
) external payable {
// Looks like it requires impossible condition
require(keccak256(msg.data) == witnessHash, "Witness required");

// Actually, sender knows the witness
payable(to).transfer(amount);
}
}

// Attacker sees transaction but can't decode intent
// Transaction appears to revert
// But sender knows secret witness that makes it succeed

3. Minimum block delay:

contract DelayedExecution {
struct PendingAction {
address user;
bytes data;
uint256 executeBlock;
}

PendingAction[] public pending;
uint256 public constant MIN_DELAY = 2; // blocks

function submitAction(bytes calldata data) external returns (uint256) {
uint256 id = pending.length;
pending.push(PendingAction({
user: msg.sender,
data: data,
executeBlock: block.number + MIN_DELAY
}));
return id;
}

function executeAction(uint256 id) external {
PendingAction memory action = pending[id];
require(block.number >= action.executeBlock, "Too early");

// Execute action
(bool success,) = address(this).call(action.data);
require(success);

// Clear
delete pending[id];
}
}

// Forces delay between submission and execution
// Reduces but doesn't eliminate front-running

4. Fair sequencing:

// Chainlink FSS (Fair Sequencing Service) concept
contract FairSequencing {
IChainlinkFSS public fss;

function trade(uint256 amount, uint256 minOut) external {
// Submit to FSS instead of directly to chain
bytes memory data = abi.encode(msg.sender, amount, minOut);

fss.submitTransaction(
address(this),
abi.encodeCall(this._executeTrade, (data))
);

// FSS orders transactions fairly (by arrival time, not gas)
// Prevents front-running
}

function _executeTrade(bytes calldata data) external {
require(msg.sender == address(fss), "Only FSS");

(address user, uint256 amount, uint256 minOut) =
abi.decode(data, (address, uint256, uint256));

// Execute trade
_swap(user, amount, minOut);
}
}

Upgradability Risks

Proxy Pattern Vulnerabilities

Why upgradability?

Smart contracts are immutable by default
But sometimes you need to:
- Fix bugs
- Add features
- Adjust parameters
- Respond to exploits

Solution: Proxy pattern
- Proxy contract (never changes) holds funds and state
- Logic contract (upgradable) contains business logic
- Proxy delegates calls to logic contract

Common proxy pattern (Transparent Proxy):

contract TransparentProxy {
address public implementation;
address public admin;

constructor(address _implementation) {
implementation = _implementation;
admin = msg.sender;
}

// Admin can upgrade
function upgradeTo(address newImplementation) external {
require(msg.sender == admin, "Not admin");
implementation = newImplementation;
}

// All other calls delegated to implementation
fallback() external payable {
require(msg.sender != admin, "Admin cannot fallback");

address impl = implementation;
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())

switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}

Storage collision vulnerability:

// Implementation V1
contract ImplementationV1 {
uint256 public value; // Slot 0

function setValue(uint256 newValue) external {
value = newValue;
}
}

// Implementation V2 (DANGEROUS!)
contract ImplementationV2 {
address public admin; // Slot 0 - COLLISION!
uint256 public value; // Slot 1

function setValue(uint256 newValue) external {
require(msg.sender == admin);
value = newValue;
}
}

// After upgrade to V2:
// - Old 'value' data in slot 0
// - V2 reads it as 'admin' address
// - Attacker sets value to their address
// - Attacker becomes admin!

The Parity wallet library disaster (revisited):

// WalletLibrary (logic contract)
contract WalletLibrary {
address public owner; // Slot 0

// VULNERABILITY: Public init function
function initWallet(address[] owners) public {
require(owner == address(0));
owner = owners[0];
}

function kill() public {
require(msg.sender == owner);
selfdestruct(payable(owner));
}
}

// Proxy (wallet)
contract Wallet {
address public library; // Slot 0

constructor(address _library) {
library = _library;
}

fallback() external payable {
address lib = library;
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), lib, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())

switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}

// Attack on library ITSELF (not proxy):
// 1. Call WalletLibrary.initWallet() directly
// 2. Set attacker as owner of LIBRARY
// 3. Call WalletLibrary.kill()
// 4. Library code deleted forever
// 5. All proxies relying on it are bricked

// Result: $150M+ frozen forever

UUPS vs Transparent Proxy

Transparent Proxy (older):

// Upgrade logic in PROXY
contract TransparentProxy {
address public implementation;
address public admin;

modifier ifAdmin() {
if (msg.sender == admin) {
_;
} else {
_fallback();
}
}

function upgradeTo(address newImpl) external ifAdmin {
implementation = newImpl;
}

function _fallback() internal {
// Delegate to implementation
}

fallback() external payable {
require(msg.sender != admin);
_fallback();
}
}

// Problem: Admin calls never reach implementation
// Limits functionality for admin

UUPS (Universal Upgradeable Proxy Standard):

// Upgrade logic in IMPLEMENTATION
contract UUPSProxy {
address public implementation;

constructor(address _implementation) {
implementation = _implementation;
}

fallback() external payable {
address impl = implementation;
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())

switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}

// Implementation with upgrade logic
contract UUPSImplementation {
address public implementation; // Must match proxy storage
address public admin;

function upgradeTo(address newImpl) external {
require(msg.sender == admin);
implementation = newImpl;
}
}

// Benefits:
// + Simpler proxy (less gas)
// + Upgrade logic in implementation (more flexible)
// + Admin can call implementation functions

// Risk:
// - If implementation forgets upgrade function, proxy is locked
// - More complex to implement correctly

Malicious Upgrades

Rug pull via upgrade:

// Original implementation (looks safe)
contract TokenV1 {
mapping(address => uint256) public balances;
address public owner;

function transfer(address to, uint256 amount) external {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
balances[to] += amount;
}
}

// Malicious upgrade (rug pull)
contract TokenV2 {
mapping(address => uint256) public balances;
address public owner;

function transfer(address to, uint256 amount) external {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
balances[to] += amount;
}

// NEW: Owner can steal funds
function emergencyWithdraw(address token, uint256 amount) external {
require(msg.sender == owner);
IERC20(token).transfer(owner, amount);
}
}

// After upgrade:
// Owner calls emergencyWithdraw()
// Drains all tokens
// Users have no recourse

Defense: Timelock + Governance:

contract GovernedUpgrade {
address public implementation;
address public governance;

struct UpgradeProposal {
address newImplementation;
uint256 executeTime;
bool executed;
}

mapping(uint256 => UpgradeProposal) public proposals;
uint256 public proposalCount;

uint256 public constant TIMELOCK = 2 days;

// Step 1: Propose upgrade
function proposeUpgrade(address newImpl) external {
require(msg.sender == governance);

uint256 id = proposalCount++;
proposals[id] = UpgradeProposal({
newImplementation: newImpl,
executeTime: block.timestamp + TIMELOCK,
executed: false
});

emit UpgradeProposed(id, newImpl);
}

// Step 2: Execute after timelock
function executeUpgrade(uint256 proposalId) external {
UpgradeProposal storage proposal = proposals[proposalId];

require(!proposal.executed, "Already executed");
require(
block.timestamp >= proposal.executeTime,
"Timelock not expired"
);

proposal.executed = true;
implementation = proposal.newImplementation;

emit UpgradeExecuted(proposalId, proposal.newImplementation);
}
}

// Users have 2 days warning
// Can exit if upgrade is malicious

Initializer Vulnerabilities

Problem with constructors in proxies:

// WRONG: Constructor won't work with proxy
contract Implementation {
address public owner;

constructor() {
owner = msg.sender; // Runs when IMPLEMENTATION deployed
// NOT when proxy created
// Owner is implementation deployer, not proxy!
}
}

// CORRECT: Use initializer
contract Implementation {
address public owner;
bool private initialized;

function initialize() external {
require(!initialized, "Already initialized");
initialized = true;
owner = msg.sender;
}
}

Initializer frontrun vulnerability:

contract Proxy {
address public implementation;

constructor(address _impl) {
implementation = _impl;
// Forgot to call initialize()!
}

fallback() external payable {
// Delegate to implementation
}
}

contract Implementation {
address public owner;
bool private initialized;

function initialize() external {
require(!initialized, "Already initialized");
initialized = true;
owner = msg.sender; // Whoever calls becomes owner!
}
}

// Attack:
// 1. Proxy deployed without calling initialize()
// 2. Attacker sees proxy address
// 3. Attacker calls proxy.initialize() (via fallback → delegatecall)
// 4. Attacker becomes owner
// 5. Attacker controls contract

OpenZeppelin's Initializable:

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract MyContract is Initializable {
address public owner;

function initialize() external initializer {
owner = msg.sender;
}

// Can only be called once
// Protected by initializer modifier
}

Testing and Security Best Practices

Testing Strategies

1. Unit tests (test individual functions):

// Hardhat test example
describe("Token", function () {
it("Should transfer tokens", async function () {
const [owner, addr1] = await ethers.getSigners();
const Token = await ethers.getContractFactory("MyToken");
const token = await Token.deploy(1000000);

// Test transfer
await token.transfer(addr1.address, 1000);
expect(await token.balanceOf(addr1.address)).to.equal(1000);
});

it("Should reject transfer with insufficient balance", async function () {
const [owner, addr1] = await ethers.getSigners();
const Token = await ethers.getContractFactory("MyToken");
const token = await Token.deploy(1000);

// Test rejection
await expect(
token.connect(addr1).transfer(owner.address, 2000)
).to.be.revertedWith("Insufficient balance");
});
});

2. Integration tests (test interactions):

describe("Lending Protocol", function () {
it("Should handle deposit -> borrow -> repay flow", async function () {
// Deploy all contracts
const oracle = await deployOracle();
const pool = await deployLendingPool(oracle.address);
const token = await deployToken();

// Test full flow
await token.approve(pool.address, 1000);
await pool.deposit(token.address, 1000);

await pool.borrow(token.address, 500);
expect(await token.balanceOf(user.address)).to.equal(500);

await pool.repay(token.address, 500);
expect(await pool.getBorrowBalance(user.address)).to.equal(0);
});
});

3. Fuzz testing (random inputs):

// Echidna example (Solidity-based fuzzer)
contract TestToken {
MyToken token;

constructor() {
token = new MyToken(type(uint256).max);
}

// Invariant: Total supply never increases
function echidna_total_supply() public view returns (bool) {
return token.totalSupply() <= type(uint256).max;
}

// Invariant: Balance never exceeds total supply
function echidna_balance_less_than_supply(address user)
public
view
returns (bool)
{
return token.balanceOf(user) <= token.totalSupply();
}
}

// Echidna generates random transactions
// Checks invariants after each
// Finds edge cases humans miss

4. Formal verification (mathematical proofs):

// Certora spec (CVL - Certora Verification Language)
rule transferPreservesTotalSupply(address from, address to, uint256 amount) {
uint256 totalBefore = totalSupply();

env e;
transfer(e, from, to, amount);

uint256 totalAfter = totalSupply();

assert totalBefore == totalAfter;
}

rule transferCannotIncreaseBalance(address from, address to, uint256 amount) {
uint256 balanceFromBefore = balanceOf(from);
uint256 balanceToBefore = balanceOf(to);

env e;
require e.msg.sender == from;
transfer(e, to, amount);

uint256 balanceFromAfter = balanceOf(from);
uint256 balanceToAfter = balanceOf(to);

assert balanceFromAfter <= balanceFromBefore;
assert balanceToAfter >= balanceToBefore;
}

Security Checklist

Before deployment:

[ ] All functions have appropriate access control
[ ] No function can be called by arbitrary addresses when it shouldn't
[ ] All external calls are protected (reentrancy guards)
[ ] Integer overflow/underflow protected (Solidity 0.8+ or SafeMath)
[ ] All user inputs are validated
[ ] No unchecked external call return values
[ ] Oracle manipulation resistance (TWAP, multiple oracles)
[ ] Flash loan attack vectors considered
[ ] Front-running risks assessed and mitigated
[ ] Upgrade mechanism is secure (timelock + governance if applicable)
[ ] Initializer protected from front-running
[ ] Storage layout compatible across upgrades
[ ] All invariants tested (fuzzing + formal verification)
[ ] Gas optimization doesn't compromise security
[ ] Error messages are clear and don't leak sensitive info
[ ] Events emitted for all state changes
[ ] Documentation complete and accurate
[ ] Multiple audits completed (3+ for high value)
[ ] Bug bounty program in place
[ ] Emergency pause mechanism (if appropriate)
[ ] Test coverage >90% (branches, not just lines)
[ ] Integration tests with mainnet forks

Common Anti-Patterns to Avoid

1. Assuming tx.origin is safe:

// NEVER USE tx.origin FOR AUTHORIZATION
function withdraw() external {
require(tx.origin == owner); // WRONG!
// Can be phished via malicious contract
}

// Always use msg.sender
function withdraw() external {
require(msg.sender == owner); // CORRECT
}

2. Using block.timestamp for critical logic:

// RISKY: Miners can manipulate ±15 seconds
function claimReward() external {
require(block.timestamp >= claimTime);
// Miner can adjust timestamp slightly
}

// BETTER: Use block numbers
function claimReward() external {
require(block.number >= claimBlock);
}

3. Sending ether without error handling:

// WRONG: transfer() can fail on gas limit
function sendReward(address user) external {
payable(user).transfer(reward); // Can run out of gas
}

// CORRECT: call() with error handling
function sendReward(address user) external {
(bool success, ) = payable(user).call{value: reward}("");
require(success, "Transfer failed");
}

4. Not handling token return values:

// WRONG: Some tokens return false instead of reverting
function deposit(uint256 amount) external {
token.transferFrom(msg.sender, address(this), amount);
// If this returns false, balance still credited!
balances[msg.sender] += amount;
}

// CORRECT: Check return value or use SafeERC20
function deposit(uint256 amount) external {
require(
token.transferFrom(msg.sender, address(this), amount),
"Transfer failed"
);
balances[msg.sender] += amount;
}

Conclusion: Defense in Depth

Key principles:

1. Assume attackers are sophisticated
- Professional hackers with millions in capital
- Can use flash loans
- Will find any vulnerability

2. Defense in depth
- Multiple layers of protection
- No single point of failure
- Fail safe, not fail deadly

3. Simplicity is security
- Complex code = more bugs
- "Clever" optimizations = vulnerabilities
- Clear code > efficient code

4. Test everything
- Unit tests (basic functionality)
- Integration tests (interactions)
- Fuzz tests (edge cases)
- Formal verification (mathematical guarantees)

5. External review
- Multiple audits
- Bug bounties
- Public code review
- Time-tested patterns

6. Fail gracefully
- Pause mechanisms
- Circuit breakers
- Upgrade paths
- Recovery procedures

The reality of DeFi security:

Perfect security is impossible
- Code will have bugs
- Economics will have edge cases
- Humans will make mistakes

Best practices:
- Minimize attack surface
- Limit blast radius
- Monitor and respond quickly
- Learn from others' mistakes
- Never deploy without multiple audits

For users:

Red flags:
- No audit
- Anonymous team
- Upgradeable without timelock
- Copied code (unaudited changes)
- Unrealistic yields
- Complex mechanisms (more bugs)

Green flags:
- Multiple audits (3+)
- Known team
- Open source
- Bug bounty
- Timelock on upgrades
- Battle-tested code
- Simple, clear logic

DeFi security is hard. The stakes are high, attackers are sophisticated, and mistakes are permanent. But with careful design, thorough testing, and defense in depth, it's possible to build relatively secure systems.

The $8B+ lost to hacks proves we're not there yet. But each failure teaches the ecosystem lessons that make future protocols more secure.


Key takeaways recap:

Vulnerability types:
- Reentrancy: $500M+ stolen
- Integer overflow: $100M+ impact
- Access control: $800M+ stolen
- Logic errors: $2B+ lost
- Flash loans: Attack amplifier
- Oracle manipulation: $500M+ stolen
- Front-running: $500M+ MEV extracted

Prevention:
- Checks-Effects-Interactions pattern
- Reentrancy guards
- Solidity 0.8+ (overflow protection)
- Multiple audits
- Time-tested patterns (OpenZeppelin)
- Formal verification
- Defense in depth

Security is not optional in DeFi. It's existential. Learn from these billion-dollar mistakes so you don't repeat them.