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"
);
}
}