A Simple Auction Contract: Creating, Testing and Deploying using Foundry

Exploring the Basics of Auction Functionality and TestingRecently, I decided to dive deep into the world of smart contract testing on my journey through development. I even posted my first tested contract in this article .In the spirit of learning, I e…


This content originally appeared on Level Up Coding - Medium and was authored by Lara Taiwo

Exploring the Basics of Auction Functionality and Testing

Recently, I decided to dive deep into the world of smart contract testing on my journey through development. I even posted my first tested contract in this article .

In the spirit of learning, I embarked on a new project, and I’m glad I did.

Sure, I might have one pesky failing test function out of ten


Ran 10 tests for test/TestAuction.t.sol:TestAuction
[PASS] test_FallbackFunction() (gas: 110575)
[PASS] test_auctionEndedModifier() (gas: 14344)
[PASS] test_deployment() (gas: 615474)
[PASS] test_onlyOwnerModifier() (gas: 11310)
[PASS] test_pickWinner() (gas: 151496)
[PASS] test_placeBid() (gas: 166304)
[PASS] test_recieveFunction() (gas: 109977)
[PASS] test_refund() (gas: 180477)
[PASS] test_remainingTime() (gas: 10460)
[FAIL: revert: WithdrawFailed] test_withdraw() (gas: 119648)
Suite result: FAILED. 9 passed; 1 failed; 0 skipped; finished in 39.03ms (32.84ms CPU time)

(thanks for keeping me humble, code!), but I’ve learned so much along the way.

The best part? I’m on a mission to fix that little hiccup and will update the article soon! Let’s dive into the ups and downs of smart contract testing as a beginner together!

1. src file(Auction.sol)

The auction system includes a function for users to place their bid, a function for the contract owner to pick a winner and withdrawn Ether after the auction ends.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Auction {
error Auction__InvalidWithdrawAmount();
error Auction__OnlyOwnerCanCall();
error Auction__MinimumNotMet();
error Auction__BiddingNotOver();
error Auction__InvalidAccount();
error Auction__BidTooLow();
error Auction__Ended();


uint256 private constant MINIMUM_BID = 5 ether;
address private immutable i_owner;
uint256 private immutable i_bidEnded;
uint256 private highestBid;
uint256 private totalBid;
address private highestBidder;

mapping(address => uint256) private bidder;

event PaymentFunction(string message);
event HighestBid(address user, uint256 amount, string message);
event Refunded(address user, uint256 amount);
event WinnerPicked(string message, uint256 bid, address winner);

modifier onlyOwner() {
if (msg.sender != i_owner) {
revert Auction__OnlyOwnerCanCall();
}
_;
}

modifier auctionEnded() {
require(block.timestamp >= i_bidEnded, Auction__BiddingNotOver());
_;
}

constructor(uint256 _bidEnded) {
i_owner = msg.sender;
i_bidEnded = block.timestamp + _bidEnded;
}

receive() external payable {
placeBid();
emit PaymentFunction("Receive Function called");
}

fallback() external payable {
if (msg.sender != i_owner) {
placeBid();
emit PaymentFunction("Fallback Function called");
}
}


function placeBid() public payable {
require(msg.value > MINIMUM_BID, Auction__MinimumNotMet());
require(msg.value > highestBid, Auction__BidTooLow());
require(msg.sender != address(0), Auction__InvalidAccount());
require(block.timestamp <= i_bidEnded, Auction__Ended());

uint256 previousBid = bidder[msg.sender];
bidder[msg.sender] = msg.value;
totalBid += msg.value;

if (previousBid > 0) {
(bool refundSuccess,) = msg.sender.call{value: previousBid}("");
require(refundSuccess, "Refund Failed");
emit Refunded(msg.sender, previousBid);
}

highestBidder = msg.sender;
highestBid = msg.value;

emit HighestBid(msg.sender, msg.value, "New highestBid added");
}


function pickWinner() public onlyOwner auctionEnded {
require(highestBid > 0 && highestBidder != address(0), "No bids placed");
emit WinnerPicked("Winner selected!", highestBid, highestBidder);
}


function refund() public auctionEnded {
uint256 biddedAmount = bidder[msg.sender];
require(msg.sender != address(0) || msg.sender != highestBidder, "Cannot Refund");
require(biddedAmount < highestBid, "Higghest bidder cannot be Refunded");

bidder[msg.sender] = 0; // Set their balance to 0
totalBid -= biddedAmount; // Decrease the total donation amount

// Send back the amount
(bool success,) = msg.sender.call{value: biddedAmount}("");
require(success, "RefundFailed");

emit Refunded(msg.sender, biddedAmount);
}


function withdraw() external onlyOwner auctionEnded {
uint256 contractBalance = address(this).balance;

require(contractBalance > 0, "Invalid withdraw amount");

(bool success,) = i_owner.call{value: contractBalance}("");
require(success, "WithdrawFailed");

emit PaymentFunction("Withdrawal completed!");
}

/////Getter functions/////

function getOwner() public view returns (address) {
return i_owner;
}

function getHighestBid() public view returns (uint256) {
return highestBid;
}

function getHighestBidder() public view returns (address) {
return highestBidder;
}

function getContractBalance() public view returns (uint256) {
return address(this).balance;
}

function getTimeLeft() public view returns (uint256) {
if (block.timestamp >= i_bidEnded) {
return 0;
} else {
return (i_bidEnded - block.timestamp);
}
}
}

Breaking down the code:

Constructor

The constructor initializes the owner and sets the end time for the auction.

constructor(uint256 _bidEnded) {
i_owner = payable(msg.sender);
i_bidEnded = block.timestamp + _bidEnded;
}

The owner is the account that deploys the contract, and _bidEnded defines how long the auction will last.

The constructor will be used while writing the deployment script to initialize _bidEnded to the specified time we want.
Deployment script is added after the Voting.sol contract explanation.

Modifiers

Modifiers are used to restrict function execution or to add preconditions before a function is run.

onlyOwner Modifier

modifier onlyOwner() {
if (msg.sender != i_owner) {
revert Auction__OnlyOwnerCanCall();
}
_;
}

This modifier ensures that only the contract owner can call specific functions, like picking a winner or withdrawing funds.

auctionEnded Modifier

modifier auctionEnded() {
require(block.timestamp >= i_bidEnded, Auction__BiddingNotOver());
_;
}

This modifier ensures that functions like withdrawing funds or picking the winner can only be called after the auction has ended.

Fallback and Receive Functions

Solidity provides two special functions to handle incoming Ether: fallback and receive. These functions ensure that any Ether sent to the contract during the auction is considered as a bid.

receive() external payable {
placeBid();
emit PaymentFunction("Receive Function called");
}

fallback() external payable {
if (msg.sender != i_owner) {
placeBid();
emit PaymentFunction("Fallback Function called");
}
}

The receive() function is called when Ether is sent directly to the contract. The fallback() function serves as a backup if receive() is not available.

The implemetation and interaction will be shown in the Test file below.

Core Functions: Placing Bids, Picking the Winner, and Withdrawing Funds

Here’s a breakdown of the most important functions:

placeBid()

function placeBid() public payable {
require(msg.value > MINIMUM_BID, Auction__MinimumNotMet());
require(msg.value > highestBid, Auction__BidTooLow());
require(msg.sender != address(0), Auction__InvalidAccount());
require(block.timestamp <= i_bidEnded, Auction__Ended());

uint256 previousBid = bidder[msg.sender];
bidder[msg.sender] = msg.value;

if (previousBid > 0) {
(bool refundSuccess,) = msg.sender.call{value: previousBid}("");
require(refundSuccess, "Refund Failed");
emit Refunded(msg.sender, previousBid);
}

highestBidder = msg.sender;
highestBid = msg.value;

emit HighestBid(msg.sender, msg.value, "New highestBid added");
}

This function allows users to place bids. It checks that the bid is higher than the minimum bid and the current highest bid.

If the bidder has placed a bid before, it refunds the previous amount. The new highest bid and bidder are updated.

pickWinner()

function pickWinner() public onlyOwner auctionEnded {
require(highestBid > 0 && highestBidder != address(0), "No bids placed");
emit WinnerPicked("Winner selected!", highestBid, highestBidder);
}

Once the auction ends, the owner can call this function to declare the highest bidder as the winner.

withdraw()

function withdraw() external onlyOwner auctionEnded {
uint256 contractBalance = address(this).balance;
require(contractBalance > 0, "No balance to withdraw");

(bool success,) = i_owner.call{value: contractBalance}("");
require(success, Auction__WithdrawFailed());

emit PaymentFunction("Withdrawal completed!");
}

This function allows the owner to withdraw the funds after the auction has ended. The contract balance is transferred to the owner’s address.

2. script file(DeployAuction.s.sol)

In addition to writing smart contracts, you need to deploy them. Below is a simple Foundry script that allows you to deploy your auction contract.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Script} from "forge-std/Script.sol";
import {Auction} from "../src/Auction.sol";

contract Deploy is Script {
uint256 public constant AUCTION_DAYS = 5 days;

function run() external returns (Auction) {
vm.startBroadcast();
Auction auction = new Auction(AUCTION_DAYS);
vm.stopBroadcast();
return auction;
}
}

Explanation of the Deployment Script:

  • Foundry Setup: The script uses Foundry’s forge-std library to manage the deployment.
  • AUCTION_DAYS: This sets the duration of the auction to 5 days.
  • run() Function: This function executes the deployment. vm.startBroadcast() broadcasts the transaction to deploy the Auction contract, and vm.stopBroadcast() stops the broadcast.

Once the deployment is completed, an instance of the Auction contract is returned.

Running the Script

To deploy the contract using this script, run the following command in your terminal:

forge script path/to/Deploy.sol --broadcast --rpc-url <your_rpc_url>

This command broadcasts the transaction to deploy your contract to the specified network (e.g.,Sepolia testnet, or your local blockchain).

3. test file(TestAuction.t.sol)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Test, console} from "forge-std/Test.sol";
import {Auction} from "../src/Auction.sol";
import {Deploy} from "../script/DeployAuction.s.sol";

contract TestAuction is Test {
Auction public auction;
Deploy public deploy;

uint256 public constant BIDDING_PERIOD = 5 days;
address public constant BOB = address(1);
address public constant ALICE = address(2);

function setUp() public {
auction = new Auction(BIDDING_PERIOD);
deploy = new Deploy();

vm.deal(BOB, 50 ether);
vm.deal(ALICE, 50 ether);
}

function test_onlyOwnerModifier() public {
vm.warp(block.timestamp + 6 days);
vm.prank(BOB);
vm.expectRevert(Auction.Auction__OnlyOwnerCanCall.selector);
auction.withdraw();
}

function test_auctionEndedModifier() public {
vm.warp(block.timestamp + 3 days);
vm.expectRevert(Auction.Auction__BiddingNotOver.selector);
auction.withdraw();
vm.expectRevert(Auction.Auction__BiddingNotOver.selector);
auction.pickWinner();
}

function test_placeBid() public {
vm.prank(BOB);
auction.placeBid{value: 10 ether}();

vm.prank(ALICE);
auction.placeBid{value: 15 ether}();

vm.prank(BOB);
auction.placeBid{value: 20 ether}();

assertEq(auction.getHighestBid(), 20 ether);
assertEq(auction.getContractBalance(), 35 ether);
assertEq(auction.getHighestBidder(), BOB);
auction.getContractBalance();
}

function test_refund() public {
vm.warp(block.timestamp + BIDDING_PERIOD);

vm.prank(BOB);
auction.placeBid{value: 10 ether}();
vm.prank(ALICE);
auction.placeBid{value: 15 ether}();
assertEq(auction.getContractBalance(), 25 ether);

vm.prank(BOB);
auction.placeBid{value: 20 ether}();
vm.prank(ALICE);
auction.placeBid{value: 25 ether}();
assertEq(auction.getContractBalance(), 45 ether);

vm.prank(BOB);
auction.refund();
assertEq(auction.getContractBalance(), 25 ether);
auction.getHighestBid();
auction.getHighestBidder();
}

function test_remainingTime() public {
uint256 time = 5 days - block.timestamp + 1;
assertEq(auction.getTimeLeft(), time);

vm.warp(block.timestamp + 7 days);
assertEq(auction.getTimeLeft(), 0);
}

function test_recieveFunction() public {
vm.prank(BOB);
(bool success,) = address(auction).call{value: 6 ether}("");
require(success);

assertEq(auction.getHighestBid(), 6 ether);
assertEq(auction.getHighestBidder(), BOB);
}

function test_FallbackFunction() public {
vm.prank(BOB);
(bool success,) = address(auction).call{value: 10 ether}(abi.encodeWithSignature("invalidFunction()"));
require(success);
assertEq(address(auction).balance, 10 ether);
}

function test_pickWinner() public {
vm.prank(BOB);
auction.placeBid{value: 20 ether}();

vm.prank(ALICE);
(bool success,) = address(auction).call{value: 30 ether}(abi.encodeWithSignature("invalidFunction()"));
require(success);

vm.warp(block.timestamp + 5 days); // Move time past the auction end
auction.pickWinner();

assertEq(auction.getHighestBid(), 30 ether);
assertEq(auction.getHighestBidder(), ALICE);
auction.getContractBalance();
auction.getHighestBidder();
}

function test_withdraw() public {
vm.prank(BOB);
auction.placeBid{value: 20 ether}();

auction.getHighestBid();
auction.getContractBalance();

vm.warp(block.timestamp + 5 days);
vm.startPrank(auction.getOwner());
auction.withdraw();
assertEq(address(auction).balance, 0);
vm.stopPrank();
}

function test_deployment() public {
auction = deploy.run();
assertTrue(address(auction) != address(0), "Auction invalid address");

uint256 auctionPeriod = BIDDING_PERIOD;
uint256 currentTime = block.timestamp;
uint256 expectedAuctionEnd = currentTime + 5 days;

assertTrue(
auctionPeriod >= expectedAuctionEnd - 2 && auctionPeriod <= expectedAuctionEnd + 2,
"Auction duration should be approximately 5 days"
);
}
}

Let’s break down the test file for the Auction contract step by step, explaining each function in detail:

Contract Setup

contract TestAuction is Test {
Auction public auction;
Deploy public deploy;

uint256 public constant BIDDING_PERIOD = 5 days;
address public constant BOB = address(1);
address public constant ALICE = address(2);
  • The TestAuction contract inherits from the Test contract provided by Foundry, allowing us to use testing features.
  • auction is an instance of the Auction contract being tested, while deploy is used to deploy instances of the Auction.
  • BIDDING_PERIOD sets a constant bidding duration of 5 days.
  • BOB and ALICE are predefined addresses representing two bidders.

setUp Function

function setUp() public {
auction = new Auction(BIDDING_PERIOD);
deploy = new Deploy();

vm.deal(BOB, 50 ether); // set BOB account to 50 ether
vm.deal(ALICE, 50 ether); // set ALICE account to 50 ether
}
  • This function initializes the test environment. It is automatically called before each test function.
  • A new Auction contract instance is created with the defined bidding period.
  • The vm.deal() function assigns 50 ether to both BOB and ALICE, simulating an environment where these accounts have sufficient funds to participate in the auction.

Test Functions

1. test_onlyOwnerModifier

function test_onlyOwnerModifier() public {
vm.warp(block.timestamp + 6 days);
vm.prank(BOB);
vm.expectRevert(Auction.Auction__OnlyOwnerCanCall.selector);
auction.withdraw();
}
  • Purpose: Tests that only the owner of the auction can call the withdraw function after the auction has ended.
  • vm.warp() simulates the passage of time, moving the current block timestamp forward by 6 days.
  • vm.prank(BOB) sets the context so that any calls made afterward will be treated as if they are coming from BOB.
  • vm.expectRevert() anticipates a revert with the specified selector when auction.withdraw() is called, confirming that the modifier works correctly.

2. test_auctionEndedModifier

function test_auctionEndedModifier() public {
vm.warp(block.timestamp + 3 days);
vm.expectRevert(Auction.Auction__BiddingNotOver.selector);
auction.withdraw();
vm.expectRevert(Auction.Auction__BiddingNotOver.selector);
auction.pickWinner();
}
  • Purpose: Tests the behavior when trying to withdraw funds or pick a winner before the auction has ended.
  • Similar to the previous test, it uses vm.warp() to simulate time progression, but here it sets it to 3 days.
  • It checks for reverts for both the withdraw and pickWinner functions, confirming they cannot be called until the auction period is complete.

3. test_placeBid

function test_placeBid() public {
vm.prank(BOB);
auction.placeBid{value: 10 ether}();

vm.prank(ALICE);
auction.placeBid{value: 15 ether}();

vm.prank(BOB);
auction.placeBid{value: 20 ether}();

assertEq(auction.getHighestBid(), 20 ether);
assertEq(auction.getContractBalance(), 35 ether);
assertEq(auction.getHighestBidder(), BOB);
}
  • Purpose: Tests placing bids by multiple users and verifies that the highest bid is correctly tracked.
  • Each user (BOB and ALICE) places bids with different values.
  • Assertions at the end ensure the highest bid, total contract balance, and highest bidder are accurately updated.

4. test_refund

function test_refund() public {
vm.warp(block.timestamp + BIDDING_PERIOD);

vm.prank(BOB);
auction.placeBid{value: 10 ether}();
vm.prank(ALICE);
auction.placeBid{value: 15 ether}();

vm.prank(BOB);
auction.placeBid{value: 20 ether}();
vm.prank(ALICE);
auction.placeBid{value: 25 ether}();

vm.prank(BOB);
auction.refund();
assertEq(auction.getContractBalance(), 25 ether);
}
  • Purpose: Tests the refund mechanism after the auction ends.
  • After warping time to the end of the bidding period, both bidders place their bids.
  • The test checks that BOB can call refund and verifies the contract balance afterwards.

5. test_remainingTime

function test_remainingTime() public {
uint256 time = 5 days - block.timestamp + 1;
assertEq(auction.getTimeLeft(), time);

vm.warp(block.timestamp + 7 days);
assertEq(auction.getTimeLeft(), 0);
}
  • Purpose: Tests the function that returns the time left in the auction.
  • It checks the remaining time immediately after deployment and confirms it is accurate.
  • After warping the time past the auction end, it asserts that the time left is zero.

6. test_recieveFunction

function test_recieveFunction() public {
vm.prank(BOB);
(bool success,) = address(auction).call{value: 6 ether}("");
require(success);

assertEq(auction.getHighestBid(), 6 ether);
assertEq(auction.getHighestBidder(), BOB);
}
  • Purpose: Tests the fallback function when ether is sent directly to the contract without the sender calling any function.
  • It uses a low-level call to send ether, simulating a bid.
  • Assertions verify that the auction records this as the highest bid.

7. test_FallbackFunction

function test_FallbackFunction() public {
vm.prank(BOB);
(bool success,) = address(auction).call{value: 10 ether}(abi.encodeWithSignature("invalidFunction()"));
require(success);
assertEq(address(auction).balance, 10 ether);
}
  • Purpose: Tests the fallback function’s behavior when an invalid function is called.
  • It confirms that ether can still be sent to the contract even if the placeBid function is not called and that the contract balance updates accordingly.

8. test_pickWinner

function test_pickWinner() public {
vm.prank(BOB);
auction.placeBid{value: 20 ether}();

vm.prank(ALICE);
(bool success,) = address(auction).call{value: 30 ether}(abi.encodeWithSignature("invalidFunction()"));
require(success);

vm.warp(block.timestamp + 5 days);
auction.pickWinner();

assertEq(auction.getHighestBid(), 30 ether);
assertEq(auction.getHighestBidder(), ALICE);
}
  • Purpose: Tests the winner selection after the auction has ended.
  • After placing bids, it simulates the auction ending and calls pickWinner.
  • Assertions confirm that the highest bid and bidder are correctly identified.

9. test_withdraw(the failed test)

function test_withdraw() public {
vm.prank(BOB);
auction.placeBid{value: 20 ether}();

vm.warp(block.timestamp + 5 days);
vm.startPrank(auction.getOwner());
auction.withdraw();
assertEq(address(auction).balance, 0);
vm.stopPrank();
}
  • Purpose: Tests the withdraw function, ensuring the auction owner can withdraw funds after the auction ends.
The withdraw function throws an error when trying to call it. The article would be updated once it’s fixed.
If you have a suggestion, please drop a comment. Thanks!

10. test_deployment

function test_deployment() public {
auction = deploy.run();
assertTrue(address(auction) != address(0), "Auction invalid address");

uint256 auctionPeriod = BIDDING_PERIOD;
uint256 currentTime = block.timestamp;
uint256 expectedAuctionEnd = currentTime + 5 days;

assertTrue(
auctionPeriod >= expectedAuctionEnd - 2 && auctionPeriod <= expectedAuctionEnd + 2,
"Auction duration should be approximately 5 days"
);
}
  • Purpose: Tests the deployment process of the auction.
  • It checks that the auction contract was successfully deployed and verifies the auction period is within expected bounds.

Conclusion

Each function in this test file plays a crucial role in validating the functionality of the Auction contract. By simulating user interactions and checking expected outcomes, the tests help ensure the contract behaves as intended and is secure against misuse.

Additional Resources


A Simple Auction Contract: Creating, Testing and Deploying using Foundry was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.


This content originally appeared on Level Up Coding - Medium and was authored by Lara Taiwo


Print Share Comment Cite Upload Translate Updates
APA

Lara Taiwo | Sciencx (2024-09-29T16:54:18+00:00) A Simple Auction Contract: Creating, Testing and Deploying using Foundry. Retrieved from https://www.scien.cx/2024/09/29/a-simple-auction-contract-creating-testing-and-deploying-using-foundry/

MLA
" » A Simple Auction Contract: Creating, Testing and Deploying using Foundry." Lara Taiwo | Sciencx - Sunday September 29, 2024, https://www.scien.cx/2024/09/29/a-simple-auction-contract-creating-testing-and-deploying-using-foundry/
HARVARD
Lara Taiwo | Sciencx Sunday September 29, 2024 » A Simple Auction Contract: Creating, Testing and Deploying using Foundry., viewed ,<https://www.scien.cx/2024/09/29/a-simple-auction-contract-creating-testing-and-deploying-using-foundry/>
VANCOUVER
Lara Taiwo | Sciencx - » A Simple Auction Contract: Creating, Testing and Deploying using Foundry. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2024/09/29/a-simple-auction-contract-creating-testing-and-deploying-using-foundry/
CHICAGO
" » A Simple Auction Contract: Creating, Testing and Deploying using Foundry." Lara Taiwo | Sciencx - Accessed . https://www.scien.cx/2024/09/29/a-simple-auction-contract-creating-testing-and-deploying-using-foundry/
IEEE
" » A Simple Auction Contract: Creating, Testing and Deploying using Foundry." Lara Taiwo | Sciencx [Online]. Available: https://www.scien.cx/2024/09/29/a-simple-auction-contract-creating-testing-and-deploying-using-foundry/. [Accessed: ]
rf:citation
» A Simple Auction Contract: Creating, Testing and Deploying using Foundry | Lara Taiwo | Sciencx | https://www.scien.cx/2024/09/29/a-simple-auction-contract-creating-testing-and-deploying-using-foundry/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.