Building an Auction for ERC20 Tokens. Scripts are compiling. 2/3 tests pass. [FAIL. Reason: EvmError: Revert] for 1/3. It may be an issue with the OpenZeppelin import, it may be out of date, wrong import path, or something else. I am unable to resolve.
Auction.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "forge-std/console.sol";
contract Auction {
address public owner;
IERC20 public token;
uint256 public auctionEndTime;
uint256 public totalTokens;
bool public auctionEnded;
mapping(address => uint256) public bids;
mapping(address => bool) public bidders; // Track bidders
address[] public bidderList; // List of bidders
event AuctionStarted(uint256 duration, uint256 totalTokens);
event BidPlaced(address indexed bidder, uint256 amount);
event AuctionEnded(address indexed winner, uint256 amount);
modifier onlyOwner() {
require(msg.sender == owner, "Not the contract owner");
_;
}
modifier auctionActive() {
require(block.timestamp < auctionEndTime, "Auction has ended");
require(!auctionEnded, "Auction already ended");
_;
}
modifier auctionEndedOnly() {
require(
block.timestamp >= auctionEndTime || auctionEnded,
"Auction still active"
);
_;
}
constructor() {
owner = msg.sender; // Set contract deployer as owner
}
function startAuction(
address _token,
uint256 _duration,
uint256 _totalTokens
) external onlyOwner {
require(
auctionEndTime == 0 || block.timestamp >= auctionEndTime,
"Previous auction not ended"
);
token = IERC20(_token);
require(
token.balanceOf(address(this)) >= _totalTokens,
"Not enough tokens for auction"
);
auctionEndTime = block.timestamp + _duration;
totalTokens = _totalTokens;
auctionEnded = false;
emit AuctionStarted(_duration, _totalTokens);
}
function placeBid() external payable auctionActive {
require(msg.sender != owner, "Owner cannot place bids");
require(msg.value > 0, "Bid must be greater than 0");
bids[msg.sender] += msg.value;
bidders[msg.sender] = true; // Register the bidder
bool isNewBidder = true;
for (uint256 i = 0; i < bidderList.length; i++) {
if (bidderList[i] == msg.sender) {
isNewBidder = false;
break;
}
}
if (isNewBidder) {
bidderList.push(msg.sender);
}
emit BidPlaced(msg.sender, msg.value);
}
function endAuction() external onlyOwner auctionEndedOnly {
require(!auctionEnded, "Auction already ended");
console.log("Auction not ended, proceeding");
auctionEnded = true;
uint256 totalBidAmount = address(this).balance;
require(totalBidAmount > 0, "No bids placed");
console.log("Total bid amount:", totalBidAmount);
uint256 tokenDistributed = 0;
console.log("Number of bidders:", bidderList.length);
for (uint256 i = 0; i < bidderList.length; i++) {
address bidder = bidderList[i];
uint256 tokenAmount = (bids[bidder] * totalTokens) / totalBidAmount;
console.log("Bidder", i, ":", bidder);
console.log("Bid amount:", bids[bidder]);
console.log("Token amount to distribute:", tokenAmount);
if (tokenAmount > 0) {
require(
token.transfer(bidder, tokenAmount),
"Token transfer failed"
);
console.log("Tokens transferred to bidder");
emit AuctionEnded(bidder, tokenAmount);
tokenDistributed += tokenAmount;
}
bids[bidder] = 0; // Clear the bid amount
}
require(
tokenDistributed <= totalTokens,
"Distributed more tokens than available"
);
console.log("Total tokens distributed:", tokenDistributed);
uint256 etherToTransfer = address(this).balance;
console.log("Ether to transfer to owner:", etherToTransfer);
payable(owner).transfer(etherToTransfer);
console.log("Ether transferred to owner");
}
function withdrawRemainingTokens() external onlyOwner auctionEndedOnly {
uint256 remainingTokens = token.balanceOf(address(this));
require(
token.transfer(owner, remainingTokens),
"Token transfer failed"
);
}
}
Auction.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "forge-std/console.sol";
import "../src/Auction.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockERC20 is ERC20 {
constructor() ERC20("Mock Token", "MKT") {
_mint(msg.sender, 1000 * 10 ** 18);
}
}
contract AuctionTest is Test {
Auction public auction;
MockERC20 public token;
address public owner;
address public bidder1;
address public bidder2;
function setUp() public {
owner = address(this); // Test contract is the owner
bidder1 = address(0x1);
bidder2 = address(0x2);
auction = new Auction();
token = new MockERC20();
// Transfer tokens to the auction contract
require(
token.transfer(address(auction), 100 * 10 ** 18),
"Token transfer to auction failed"
);
// Approve the auction contract to spend tokens
require(
token.approve(address(auction), type(uint256).max),
"Token approval failed"
);
// Verify the transfer and approval
assertEq(
token.balanceOf(address(auction)),
100 * 10 ** 18,
"Auction didn't receive tokens"
);
assertEq(
token.allowance(address(this), address(auction)),
type(uint256).max,
"Approval failed"
);
}
function testStartAuction() public {
vm.prank(owner);
auction.startAuction(address(token), 1000, 100 * 10 ** 18);
assertEq(
auction.totalTokens(),
100 * 10 ** 18,
"Total tokens mismatch"
);
assertEq(
address(auction.token()),
address(token),
"Token address mismatch"
);
}
function testPlaceBid() public {
vm.prank(owner);
auction.startAuction(address(token), 1000, 100 * 10 ** 18);
vm.prank(bidder1);
vm.deal(bidder1, 1 ether); // Ensure bidder1 has enough ETH
auction.placeBid{value: 1 ether}();
assertEq(auction.bids(bidder1), 1 ether, "Bid amount mismatch");
}
function testEndAuction() public {
console.log("Starting testEndAuction");
vm.prank(owner);
auction.startAuction(address(token), 1000, 100 * 10 ** 18);
console.log("Auction started");
vm.prank(bidder1);
vm.deal(bidder1, 2 ether);
auction.placeBid{value: 2 ether}();
console.log("Bidder1 placed bid");
vm.prank(bidder2);
vm.deal(bidder2, 3 ether);
auction.placeBid{value: 3 ether}();
console.log("Bidder2 placed bid");
vm.warp(block.timestamp + 1001);
console.log("Time warped to end of auction");
uint256 initialOwnerBalance = address(owner).balance;
uint256 initialAuctionBalance = address(auction).balance;
uint256 initialTokenBalance = token.balanceOf(address(auction));
console.log("Initial owner ETH balance:", initialOwnerBalance);
console.log("Initial auction ETH balance:", initialAuctionBalance);
console.log("Initial auction token balance:", initialTokenBalance);
vm.prank(owner);
auction.endAuction();
console.log("Auction ended");
uint256 bidder1TokenBalance = token.balanceOf(bidder1);
uint256 bidder2TokenBalance = token.balanceOf(bidder2);
uint256 finalOwnerBalance = address(owner).balance;
uint256 finalAuctionBalance = address(auction).balance;
uint256 finalTokenBalance = token.balanceOf(address(auction));
console.log("Bidder1 token balance:", bidder1TokenBalance);
console.log("Bidder2 token balance:", bidder2TokenBalance);
console.log("Final owner ETH balance:", finalOwnerBalance);
console.log("Final auction ETH balance:", finalAuctionBalance);
console.log("Final auction token balance:", finalTokenBalance);
assertEq(
bidder1TokenBalance,
40 * 10 ** 18,
"Bidder1 token balance mismatch"
);
assertEq(
bidder2TokenBalance,
60 * 10 ** 18,
"Bidder2 token balance mismatch"
);
assertEq(
finalOwnerBalance,
initialOwnerBalance + 5 ether,
"Owner didn't receive ETH"
);
assertEq(
finalAuctionBalance,
0,
"Auction contract should have 0 ETH balance"
);
assertEq(
finalTokenBalance,
0,
"Auction contract should have 0 token balance"
);
}
}
Output
Ran 1 test suite in 814.65ms (66.59ms CPU time): 2 tests passed, 1 failed, 0 skipped (3 total tests)
Failing tests: Encountered 1 failing test in test/Auction.t.sol:AuctionTest [FAIL. Reason: EvmError: Revert] testEndAuction() (gas: 380426)
Please advise.
Update:
Trace:
├─ [0] console::log("Ether to transfer to owner:", 5000000000000000000 [5e18]) [staticcall] │ └─ ← [Stop] ├─ [45] AuctionTest::fallback{value: 5000000000000000000}() │ └─ ← [Revert] EvmError: Revert └─ ← [Revert] EvmError: Revert