-
Notifications
You must be signed in to change notification settings - Fork 1
Lab 6 ‐ Smart Contracts on Ethereum [WIP]
You may find the starter project in the lab-6
branch of the lab repository.
✏️ Switch to the lab-6
branch.
The project's current structure is as follows:
-
.vscode
: Contains the VSCode configuration, recommended extensions and settings. -
contract
s: Contains some prewritten smart contracts. -
frontend
: Contains a React app that acts as dApp for the smart contracts written in this lab. -
ignition
: Contains the Ignition modules for the deployment of the smart contracts. -
scripts
: Contains Hardhat Scripts for automating useful tasks. -
test
: Contains the tests for this project. -
.mocharc.json
: Configuration file for Mocha (for testing) -
.solhint.json
: Configuration file for solhint (for static analysis) -
eslint.config.mjs
: Configuration file for ESLint (for JS/TS linting) -
prettier.config.cjs
: Configuration file for Prettier (linting) -
hardhat.config.ts
: Configuration file for Hardhat.
✏️ Install the project's dependencies with npm install
. Move inside the frontend
folder and install these dependencies as well with npm install
.
✏️ Install the required VS Code dependencies from .vscode/extensions.json
. Make sure not to install the extensions in unwantedRecommendations
as they are not compatible with the rest of them 😞
Important
Restart VS Code afterwards.
✏️ Install the Brave browser. Later on, we will use Brave's integrated wallet for this lab. You are free to use any other wallet you want, but the instructions will cover Brave Wallet.
Installing Brave on Fedora
sudo dnf install dnf-plugins-core
sudo dnf config-manager addrepo --from-repofile=https://brave-browser-rpm-release.s3.brave.com/brave-browser.repo
sudo dnf install brave-browser
In the first part of this lab, we are going to develop a simple smart contract that can manage an open auction. The general idea of the contract is that everyone can send their bids during a bidding period. The bids include sending compensation (Ether) in order to bind the bidders to their bid. If the highest bid is raised, the previous highest bidder gets their Ether back. After the end of the bidding period, the contract has to be called manually for the beneficiary to receive their Ether.
The constructor of the auction contract takes two arguments: _biddingEnd
indicates when the bidding period is supposed to end and _beneficiary
is the address of the account that will receive the winning bid of the auction.
✏️ Finish the constructor so that it stores its parameters in the biddingEnd
and beneficiary
state variables.
Tip
We already created the beneficiary
state varible for you, create a state variable for the bidding time similarly!
In order to bid in the auction, users have to call the bid
function and send their bid together with the transaction.
The bid
function has no arguments, all information (who makes the bid and with what amount) is already part of the transaction. The keyword payable
is required for the function to be able to receive Ether.
🧟 Function Declarations, Msg.sender, Payable
✏️ Finish the bid
function so that it updates the highest bidder (highestBidder
) and the highest bid (highestBid
) if the value received exceeds the highest bid. If the bid is too low, then the function should revert.
Tip
The contract already contains the highestBidder
and highestBid
state variables, use these!
Tip
You can access the account who sent the transaction with msg.sender
and the value sent together with it with msg.value
.
Tip
Use the revert
statement to revert the transaction. The revert statement will revert all changes in the function execution including it having received the Ether.
Whenever the auction ends, we transfer the winning bid to the beneficiary address. This is done by the auctionEnd
function.
✏️ Finish the auctionEnd
function so that it transfers the winning bid to the beneficiary address! Ensure that this can only happen once.
Tip
The winning bid is contained in the highestBid
state variable.
Tip
You can ensure that the transfer can only happen once by using the ended
state variable that contains whether this transfer has happened yet.
Tip
You can transfer amount
Ether to address
by calling address.transfer(amount)
.
Events provide an easy way to notify the applications interacting with our contract about state changes.
🧟 Events
✏️ Declare the following events and emit them when necessary:
-
HighestBidIncreased(address bidder, uint amount)
: emitted when the highest bid is increased -
AuctionEnded(address winner, uint amount)
: emitted when the auction ends
Tip
You can declare events with the event
keyword and emit them with the emit
keyword. For example, event A()
and emit A()
.
Errors in Solidity provide a convenient and gas-efficient way to explain to the user why an operation failed. They have to be used together with the revert
statement or the require
function.
✏️ Declare the BidNotHighEnough(uint highestBid)
error and modify the bid
function to revert with this error whenever the amount sent with the transaction is not higher than the highest bid.
✏️ Declare the AuctionEndAlreadyCalled()
error and modify the auctionEnd
function to revert with this error if the function was already called before.
Tip
You can declare errors with the error
keyword and use them when reverting with the revert
keyword. For example, error Err()
and revert Err()
.
The auction is created with a timestamp marking the end of the bidding period that is set in the constructor. We only want to allow bids during this period and the winner can only be determined once the bidding period is over.
✏️ Declare the TooLate(uint time)
error and revert the bid
function with this error if it's called after the end of the bidding period. Indicate using the time
parameter what the time limit for calling this function was!
Tip
You can access the timestamp of the block containing the transaction through block.timestamp
.
Tip
You can declare errors with the error
keyword and use them when reverting with the revert
keyword. For example, error Err()
and revert Err()
.
✏️ Declare the TooEarly(uint time)
error and revert the auctionEnd
function with this error if it's called before the end of the bidding period. Indicate using the time
parameter when the call should be retried!
We would like to return bids that were overbid to whoever sent them. However, sending back the Ether in the bid
function by simply using highestBidder.transfer(highestBid)
is a security risk because it could execute an untrusted contract. It is always safer to let the recipients withdraw their Ether themselves, which is what we are going to implement.
✏️ Declare a mapping called pendingReturns
that maps addresses to their allowed withdrawals and store bids that were overbid in this mapping!
Tip
You can create a mapping that maps keys of the address
type to values of the uint
type like this: mapping(address => uint)
.
Tip
You can index a mapping the following way: myMapping[key]
.
✏️ Update bid
to add the previous highest bid to the previous highest bidder's pendingReturns
.
✏️ Finish the withdraw
function so that users can withdraw their corresponding amounts stored in pendingReturns
. Ensure that the allowed amounts can only be withdrawn once.
Tip
msg.sender
is not of type address payable
and must be explicitly converted using payable(msg.sender)
in order use the member function send()
.
Modifiers can be used to change the behavior of functions in a declarative way. For example, you can use a modifier to automatically check a condition prior to executing the function.
✏️ Declare a modifier called onlyBefore(uint time)
that reverts with the error TooLate(time)
if called after time
!
✏️ Update the bid
function to use the onlyBefore(uint time)
modifier!
✏️ Similarly to above, declare a modifier called onlyAfter(uint time)
that reverts with the error TooEarly(time)
if called before time
and modify the auctionEnd
function to use this modifier!
Right now, our auction smart contract is able to collect bids, payout the winning bid to the beneficiary and let the others withdraw their losing bids. The only thing missing is the subject of the auction: a (possibly valuable) NFT.
🧟 ERC721 Standard, Multiple Inheritance
In this exercise we are going to use our own FTSRG NFT ERC721 token. You can find the implementation of this token in FtsrgNft.sol.
✏️ Take a moment to familiarize yourself with the FtsrgNft.sol
contract. Pay attention to the balanceOf
, mintTo
, transferFrom
and safeTransferFrom
functions, what do these functions do?
Tip
The contract allows only a single token to be minted. This is ensured in the mintTo
function through the check require(nextTokenIdToMint == 0, "!TooMany")
Tip
Some functions expect a tokenId
argument. This parameter can be used to identify a specific token. As the contract only allows a single token to be minted, we are only going to use the id 0
.
The subject of the auction will be an NFT, which is uniquely identified by its contract's address (tokenAddress
) and a unique id (tokenId
). The process of the auction will have multiple stages:
flowchart LR
anchor1[ ]:::empty
WaitingForFunding["Waiting for funding"]
Bidding["Bidding"]
WaitingForEnding["Waiting for ending"]
Ended["Ended"]
classDef empty width:0px,height:0px;
anchor1 --> WaitingForFunding
WaitingForFunding -- Transfer NFT --> Bidding
Bidding -- block.timestamp > biddingEnd --> WaitingForEnding
WaitingForEnding -- auctionEnd() --> Ended
- Waiting for funding: Once a beneficiary creates an auction, they have to transfer the NFT to the auction contract so that it can be transferred to the winning bidder.
- Bidding: Once the NFT is transferred to this contract, the public can place bids on it.
- Waiting for ending: After the bidding time ends, someone has to call the
auctionEnd
function that transfers the NFT to the winner, and the bid to the beneficiary. - Ended: The auction is over, the owners of the losing bids can withdraw their funds.
First, let us store the necessary information about the NFT.
✏️ Add two new public
state variables: tokenAddress
(address
) and tokenId
(uint
).
✏️ Add two additional parameters to the constructor: _tokenAddress
(address
) and _tokenId
(uint
) that set the previously added state variables.
Then, restrict certain operations to only allow them once the auction has been funded.
✏️ Add a new public view function called funded
that returns true if and only if the auction has been funded.
Tip
The auction is funded, if owner of the NFT is the auction contract. Since we know the address of the NFT contract and the token ID, we can check the balance IERC721(tokenAddress).ownerOf(tokenId) == address(this)
✏️ Add a new modifier onlyFunded
that only executes the function body, if the auction has been funded. Otherwise, it should raise a new custom error NotYetFunded()
.
✏️ Add the new modifier to the bid
function.
Finally, we need to transfer the auction to the winner of the auction.
✏️ Modify the auctionEnd
function to transfer the NFT to the winner.
Tip
Since we know both the winner and the NFT contract adress and token ID, we can transfer the NFT with: IERC721(tokenAddress).transferFrom(address(this), highestBidder, tokenId)
Right now the contract accepts any ERC721 token transfers, but we only want to accept transfers of the token specified through the tokenAddress
and tokenId
constructor parameters.
There is a special function at the end: onERC721Received
. Whenever an ERC721 token is transferred to this contract via the safeTransferFrom
method (docs), this function is called. This function returns a special value (its Solidity selector) to confirm the token transfer. If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted. The selector can be obtained in Solidity with IERC721Receiver.onERC721Received.selector
. The rationale behind the existence of this method is that this way a contract can reject NFTs that it cannot handle for some reason (e.g., they are not the subject of the auction), preventing the tokens from being stuck in the contract.
✏️ Modify the onERC721Received
method, so that it only allows the transfer of the NFT that is the target of the auction.
Tip
This function is called by our NFT contract during safeTransferFrom
, so msg.sender
should equal tokenAddress
.
Tip
Either create custom errors, or use the require
statement to revert on errors.
The Factory pattern is a design pattern used to create and manage multiple instances of a specific smart contract. A factory contract deploys new contracts on the blockchain and keeps track of the deployed instances, making it easier to manage and interact with them. Typically, a factory contract includes a function that deploys new instances and stores the addresses of the created contracts in an array or mapping.
We have created the skeleton of a factory contract in AuctionFactory.sol
.
🧟 Working with Structs And Arrays
✏️ Finish the body of the createSimpleAuction
function such that it creates a new SimpleAuction
instance with the received parameters and stores its address in the corresponding array. Return the address of the created contract.
Tip
You can use the new
keyword to create a new contract instance: new ContractName(...)
.
Tip
You can access the address of a contract instance with address(...)
.
✏️ Compile the smart contract and run the tests provided by us using npx hardhat compile
and npx hardhat test test/SimpleAuction.ts
📋 CHECK: Do all tests succeed? If not, then check your implementation.
We have created a working system of smart contracts in the previous part. In this part, we will work on deploying them. Moreover, we will discuss Wallets that are a crucial part of interacting with a block chain as an end user, and showcase a DApp (decentralized app) that provides a frontend for our system.
Blockchain testnets are specialized networks that serve as testing environments for developers to build, test, and deploy blockchain applications safely before launching on the mainnet (main network). These networks replicate the mainnet’s architecture, features, and protocols but operate in a controlled, sandboxed environment, allowing experimentation without financial risk or impacting live assets.
Testnets are crucial in the development lifecycle, especially for decentralized applications and smart contracts, as they help developers detect and fix issues early on. They also support testing different scenarios, including performance under heavy loads, smart contract bugs, and transaction processing. Typically, testnets use a different form of currency (often free and readily available through faucets), which has no real-world value, so users can conduct transactions and test without incurring real costs. Popular testnets include Holešky or Sepolia.
Apart from publicly available testnets, we can start a local network for ourselves as well. It is useful for debugging purposes, or running tests. Hardhat also has an integrated test network that it runs automatically for tests. We can start it in a standalone fassion as well to try out our smart contracts and applications locally.
During this lab, we will use the local Hardhat network.
Tip
While it is true that publicly available testnets have faucets available with which the developers can request free funds, there are always limitations. To combat malicious actors accumulating funds rendering the network unusable, faucets usually require authentication (e.g. Google account) or require users to mine the network to receive the requested funds. Moreover, the requestable funds are usually limited.
✏️ Start the local Hardhat network with npx hardhat node
in a dedicated terminal. This will be a long running task. Make note, that the Hardhat Network has funded 20 accounts with 10.000 ETH to aid in testing.
We have a working test network. Now, let us deploy our smart contracts on the network. Hardhat Ignition is a declarative system for deploying smart contracts on Ethereum blockchains. The declarative nature of Ignition enables it to execute steps in parallel, manage and recover from errors, resume interrupted or partial deployments, and even adapt to modifications in your modules.
First, let us recall the architecture of the smart contracts:
classDiagram
direction LR
class AuctionFactory
class SimpleAuction
class IERC721["«interface»\nIERC721"]
class FtsrgNft
IERC721 <|.. FtsrgNft
AuctionFactory ..> SimpleAuction: «create»
SimpleAuction ..> IERC721: «use»
We provided a custom NFT implementation with FtsrgNft
that we can use for testing purposes. The idea behind this NFT, that this contract only mints one token (tokenId
: 0
), so it must be valuable 😃
Out of our three contracts, we need to deploy AuctionFactory
and FtsrgNft
. We do not need to deploy SimpleAuction
as AuctionFactory
was created for this exact purpose.
Now, let us deploy the FtsrgNft
contract.
🔍 Check out the documentation of Hardhat Ignition to see what you need to do.
✏️ Create FtsrgNft.ts
under ignition/modules
. Fill it with the definition of an Ignition module that deploys FtsrgNft
.
✏️ Deploy FtsrgNft
- Run
npx hardhat ignition deploy ignition/modules/FtsrgNft.ts --network localhost
- Make note of the deployed address!
- Run
npx hardhat ignition deploy ignition/modules/FtsrgNft.ts --network localhost
again.
📋 CHECK: Did it deploy anything now? Did the address change?
We have successfully deployed our FtsrgNft
, but we are yet to mint it!
✏️ Modify FtsrgNft.ts
under ignition/modules
to also mint the NFT.
Tip
Use the mintTo
method of FtsrgNft
Tip
You can use m.getAccount(0)
to get the address of the deploying account.
💡 A generic rule in Hardhat, that unless specified otherwise, the first account (index 0) always the account that deploys your code.
✏️ Deploy FtsrgNft
again
- Run
npx hardhat ignition deploy ignition/modules/FtsrgNft.ts --network localhost
📋 CHECK: Did it deploy anything now? Did the address change?
We have learned how to deploy a contract with Hardhat Ignition, and how to change the deployment as long as the changes are compatible and doable incrementally. Let us deploy AuctionFactory
in a similar fashion.
✏️ Create AuctionFactory.ts
under ignition/modules
. Fill it with the definition of an Ignition module that deploys AuctionFactory
.
✏️ Deploy AuctionFactory
- Run
npx hardhat ignition deploy ignition/modules/AuctionFactory.ts --network localhost
- Make note of the address!
📋 CHECK: Why did we get a warning?
We can deploy every contract by hand one-by-one, or we can make use of Ignition's declarative nature and write modules that depend on each other. Since in this case none of the contracts depend on the other, let us create a system module that contains both our smart contracts.
✏️ Create System.ts
under ignition/modules
. Fill it with the definition of an Ignition module that deploys both AuctionFactory
and FtsrgNft
.
✏️ Deploy the system
- Run
npx hardhat ignition deploy ignition/modules/System.ts --network localhost
📋 CHECK: Did anything happen?
Incremental and declarative deployment is useful for making sure that we deploy one consistent system. However, during development, we might want to redeploy the whole application. Hardhat Ignition enables us to discard the previous deployment information and start deployment from scratch using the --reset
argument.
✏️ Deploy the system
- Run
npx hardhat ignition deploy ignition/modules/System.ts --network localhost --reset
📋 CHECK: Did anything happen? Did the addresses change?
A blockchain wallet is a digital tool that allows users to interact with blockchain networks, enabling them to store, send, and receive cryptocurrencies and digital assets securely, or interact with smart contracts and DApps. Unlike traditional wallets that hold physical money, blockchain wallets manage cryptographic keys—unique pairs of public and private keys—used to verify ownership and authorize transactions on the blockchain.
There are numerous wallets available that each tailor to various needs. There are wallets that integrate with a browser (Brave Wallet, MetaMask), and there are always offline hardware backed wallets as well (Ledger). During this lab, we will use Brave Wallet that integrates with the Brave Browser and develop an web-based frontend for our blind auction contract that integrates with it.
Warning
This is NOT a product endorsement. You should always consider questions about data security, privacy and trust before you choose a wallet you trust with your funds, as if it becomes compromised, you will lost your assets. We did not consider any of these aspects when choosing the wallet.
We chose Brave Wallet for the following reason alone. Blockchains are (usually) permanent, and wallets make use of that fact. When using local development networks, like the Hardhat Network, the wallet needs to be able to "reset" itself because the development networks are often restarted, wiping out the history. Brave Wallet can be "reset" quite easily and reliably.
Caution
DO NOT use the wallet you store your assets in for development. Development usually means that you have to "reset" the network, thus your wallet, meaning you are in constant danger of losing access to your assets without proper backup.
Now, let us configure the Brave Wallet.
✏️ Open up the Brave Browser. Then:
- Navigate to
brave://wallet
or click the wallet icon in the top-right corner- Create a new wallet (Need a new wallet?)
- On the Supported networks page click Deselect all. Two nets will remain, the Ethereum Mainnet and Solana Mainnet.
- Create a password and set the auto-lock option for the highest possible value to prevent giving your password constantly (convenience of testing over security)
- In the Save your recovery phrase do not click on Skip. Save it to disable a warning afterwards. It will ask you for this phrase on the next screen.
- You should see your Portfolio
- After you got to the Portfolio, navigate to
brave://settings/web3
in a new page (or access Web3 from Settings)- Add a new Wallet Network with the following settings:
-
Search network: Search for
31337
then select `GoChain Testnet - After that, start overriding the settigns with the following values
-
The name of chain:
Hardhat
-
Chain's currency name:
Ethereum
-
Chain's currency symbol:
ETH
-
Chain's currency decimals:
18
-
RPC URLs:
http://127.0.0.1:8545
(make sure that the radio button is selected as well) - Remove the Block exporer URLs
-
The name of chain:
-
Search network: Search for
- Make note of the Clear wallet transaction and nonce information option. You will need it later to reset the wallet.
- Add a new Wallet Network with the following settings:
- Navigate to Accounts in Brave Wallet
-
Import an Ethereum account.
-
Import type:
Private key
-
Private key:
0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
-
Name this account:
Hardhat 0
-
Import type:
-
Import another Ethereum account.
-
Import type:
Private key
-
Private key:
0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
-
Name this account:
Hardhat 1
-
Import type:
-
Import an Ethereum account.
Important
Each time you restart your local development network, you need to navigate to brave://settings/web3
and click on Clear wallet transaction and nonce information.
Tip
If you remember, npx hardhat node
emitted 20 account information. We got these private keys from the first two accounts. You can import the others as well if you wish to.
Tip
In Accounts, under Hardhat 0 and Hardhat 1 hide the assets apart from ETH on Hardhat to declutter the UI. Do the same in Portfolio.
✏️ Now that you have setup your wallet, try out the different features it provides.
- Use Send to send 1.000 ETH from Hardhat 0 to Hardhat 1.
- Check out the Activity tab afterwards.
- Check out the Transactions of both accounts!
- Import out NFT to Hardhat 0:
-
Select network:
Hardhat
- Contract address: From the last deployment
-
Token ID:
0
-
Token name:
FTSRG NFT
-
Token symbol:
FTSRG
-
Select network:
- Send the NFT to Harhat 1
- Check out the Activity tab afterwards.
- Check out the Transactions of both accounts!
💡 Unfortunately, Brave Wallet will not be able to display nice information about our NFT, as it can only be found on our local network.
Up until this point, we have only interacted with the blockchain using custom scripts. To make Web3 applications more available, we will connect a frontend to our smart contracts.
This frontend was created using with React, a popular TypeScript library for building user interfaces, especially well-suited for single-page applications. React’s component-based architecture enables us to create a modular, responsive UI that interacts seamlessly with the blockchain backend.
✏️ Navigate into the frontend
folder and run the app using npm run dev
.
- Open the website on the URL written to the console.
- Try changing the network and the account in the wallet. You might need to give permission for your site to access your wallet.
- Simulate an auction
- Create a new simple auction with Hardhat 0. Let them be the beneficiary, while the target NFT should be the FtsrgNft. You can recover the address from the deployment logs. (The token ID is
0
.) - Transfer the NFT to the auction contract. You can use the GUI to do so. Make sure that the correct account is selected!
- Place some bids with Hardhat 0 and Hardhat 1. Make sure, that the latter is the winner.
- Forward the clock to simulate the end of the bidding period. You can use a custom helper script for this purpose:
npm run debug-forward-clock-one-week
- End the auction, and withdraw the funds with Hardhat 0 and Hardhat 1.
📋 CHECK: Check in the wallet who owns the NFT, and what are the balances. You should see that the NFT changed hands, and the funds were allocated accordingly.
In this task we are going to extend the previous open auction to a blind auction.
During the bidding period, a bidder does not actually send their bid, but only a hashed version of it. Since it is currently considered practically impossible to find two (sufficiently long) values whose hash values are equal, the bidder commits to the bid by that. After the end of the bidding period, the bidders have to reveal their bids during the reveal period: They send their values unencrypted, and the contract checks that the hash value is the same as the one provided during the bidding period. This pattern is known as the Commit-Reveal Scheme.
flowchart LR
anchor1[ ]:::empty
WaitingForFunding["Waiting for funding"]
Bidding["Bidding"]
Bidding["Revealing"]
WaitingForEnding["Waiting for ending"]
Ended["Ended"]
classDef empty width:0px,height:0px;
anchor1 --> WaitingForFunding
WaitingForFunding -- Transfer NFT --> Bidding
Bidding -- block.timestamp > biddingEnd --> Revealing
Revealing -- block.timestamp > revealEnd --> WaitingForEnding
WaitingForEnding -- auctionEnd() --> Ended
Another challenge is how to make the auction binding and blind at the same time: The only way to prevent the bidder from just not sending the Ether after they won the auction is to make them send it together with the bid. Since value transfers cannot be blinded in Ethereum, anyone can see the value.
We are going to solve this problem by accepting any value that is larger than the highest bid. Since this can of course only be checked during the reveal phase, some bids might be invalid, and this is on purpose (users can even use an explicit flag to place invalid bids with high-value transfers): Bidders can confuse competition by placing several high or low invalid bids.
The commit-reveal scheme is a type of commitment scheme that can be used to store values in a smart contract and keep them secret until you choose to reveal them later.
💡 The commit-reveal scheme uses a two-step process:
- Commit:
The user computes and sends
hash(data, secret)
, wheredata
is the data itself andsecret
is a secret phrase chosen by the user. The smart contract stores the hash.bytes32 hash = keccak256(abi.encodePacked("data", "secret"));
- Reveal:
The user sends the same
data
andsecret
values, the contract computes the hash and compares it to the stored hash value.bytes32 newHash = keccak256(abi.encodePacked(newData, newSecret)); require(hash == newHash);
💡 During the bidding phase, users can commit to bids that they are going to reveal in the reveal phase.
💡 The keccak256
hash function is a cryptographic hash algorithm used widely in Ethereum to securely encode data into a fixed 256-bit output.
The new blind auction contract is going to have a significantly different interface than our previous simple auction. In order to make this transition as easy as possible, we created the BlindAuction
contract for you that will serve as starting point for the next exercise. This contract contains most of the code that we wrote during the previous exercise, but there are some changes:
- The constructor received one further argument:
uint _revealEnd
indicates the end of the reveal period. - The
bid
function was transformed into the internalplaceBid
function that we are going to call whenever a bid is revealed. The functionality remained the same, but the function takes thebidder
andvalue
as arguments instead of extracting these from the transaction usingmsg.sender
andmsg.value
. - A new
bid
function was added that takes a 32 byte hash value as input. - A new struct called
Bid
was added that stores the hash values received by thebid
function along with the amount of Ether sent with them.Thestruct Bid { bytes32 blindedBid; uint deposit; }
bids
mapping stores an array of bids for each address:mapping(address => Bid[]) public bids;
- A function was added for revealing bids (
reveal
).
During the bidding period, users can submit the hashes of their blinded bids. The hash is computed as keccak256(abi.encodePacked(value, fake, secret))
, where value
is the inteded bid, fake
is a Boolean flag indicating whether the bid is real or fake and secret
is a secret phrase chosen by the user.
✏️ Finish the bid
function so that it stores the blinded bid received as the bytes32 blindedBid
parameter in the bids
mapping. Extract the sender and the Ether received along with the call from the transaction. Emit the BidReceived(address sender, bytes32 blindedBid, uint deposit)
event based on the received bid!
Tip
You can create a new Bid
struct instance the following way: Bid({ blindedBid: blindedBid, deposit: deposit })
.
Tip
You can use the push
function to add a new value to an array.
Tip
You can use msg.sender
and msg.value
to access the sender and the amount of Ether sent.
During the reveal phase, the users have to submit the same values unencrypted and we check whether their hash value is the same as the one provided during the bidding period. This is done by the reveal
function, which the contract already includes the skeleton of:
function reveal(
uint[] calldata values,
bool[] calldata fakes,
bytes32[] calldata secrets
) external onlyAfter(biddingEnd) onlyBefore(revealEnd) {
uint length = bids[msg.sender].length;
require(values.length == length);
require(fakes.length == length);
require(secrets.length == length);
uint refund;
for (uint i = 0; i < length; i++) {
// TODO process bids
}
payable(msg.sender).transfer(refund);
}
We first check if the arrays received as input have the appropriate size (the arrays must have the same size as the number of blinded bids we received). We then iterate over the stored bids, while collecting the amount of Ether that has to be refunded to the user.
🧟 Handling Multiple Return Values
✏️ Write the body of the loop such that it checks if the received bids are revealed correctly. If a bid is revealed correctly, then emit the RevealedBid(address sender, bytes32 blindedBid, bool fake, uint value)
event, else proceed to the next bid!
Tip
You can use keccak256(abi.encodePacked(value, fake, secret))
to calculate the hash of the values value
, fake
and secret
.
A bid is considered invalid if either the fake
flag is set to true, or the deposited amount is lower than the value
that was hashed. In these cases, the entire deposit has to be refunded. Valid bids are compared with the current highest bid, which they have to exceed in order to be registered as the new highest bid. If the highest bid is not exceeded, then the entire deposit is refunded, else only the surplus above value
(i.e., deposit - value
).
✏️ Modify the loop body such that bids that were properly revealed are processed using the placeBid
function and invalid bids are refunded (add them to the refund
variable). Also refund bids that do not exceed the current highest bid. For valid bids that are registered as the new highest bid, only refund the surplus deposit.
Tip
The placeBid
function returns a Boolean value indicating whether the processed bid exceeded the highest bid.
Right now the contract allows users to reveal their bids multiple times and get refunds each time. The easiest way to counter this is to clear the stored hash from the contract after a bid was revealed.
✏️ Modify the reveal
function such that the hashes of correctly revealed bids are cleared from the Bid
struct instance that stored them.
Tip
You can use bytes32(0)
to create a 32 bytes long sequence of zeroes.
🧟 Storage vs. Memory (Data Location)
Tip
By using Bid storage bidToCheck = bids[msg.sender][i]
to store the bid that we check, we saved a reference to the Bid
instance from the mapping. We can modify the original instance through this reference.
We would like to ensure that the auction can only start after the auction has been funded (meaning the contract received the NFT).
✏️ Finish the body of the funded
function such that is returns true if the auction contract currently owns the NFT.
✏️ Create an onlyFunded
modifier that reverts with a NotYetFunded()
error if the auction isn't funded. Use the funded
function to check if the auction is funded. Annotate the bid
function with this modifier.
✏️ Modify the onERC721Received
method, so that it only allows the transfer of the NFT that is the target of the auction.
✏️ Modify the auctionEnd
function such that it also transfers the NFT to the winner of the auction.
Tip
These exercises should be fundamentally the same as they were for the simple auction.
✏️ Finish the body of the createBlindAuction
function such that it creates a new BlindAuction
instance with the received parameters and stores its address in the corresponding array. Return the address of the created contract.
✏️ Compile the smart contract and run the tests provided by us using npx hardhat compile
and npx hardhat test test/BlindAuction.ts
📋 CHECK: Do all tests succeed? If not, then check your implementation.
✏️ Deploy the system
- Run
npx hardhat ignition deploy ignition/modules/System.ts --network localhost --reset
✏️ Refresh your browser, try creating a blind auction in the GUI, and perform a blind auction.
Reentrancy refers to a situation where a smart contract calls an external contract, and that external contract then calls back into the original contract before the first function has finished executing. This can lead to unexpected and unsafe changes in the contract’s state, especially if important state updates are performed after making an external call. A famous example of a reentrancy vulnerability was the DAO hack in 2016, where attackers repeatedly withdrew funds by recursively calling the withdrawal function before the contract could update the user’s balance.
🔍 Reentrancy Attacks and The DAO Hack
To prevent reentrancy attacks, developers use techniques such as the checks-effects-interactions pattern (updating state variables before making external calls) and using reentrancy guards to block repeated entries into sensitive functions during execution. Understanding reentrancy is critical for writing secure Ethereum smart contracts.
🔍 Robust Smart Contracts: Checks-Effects-Interactions Pattern for Secure dApps
🔍 OpenZeppelin Reentrancy Guard: A Quickstart Guide
✏️ Check your withdraw
implementation (in either SimpleAuction
or BlindAuction
) if it is vulnerable to reentrancy attacks. If it is, fix it. If it is not, explain why not!
📋 CHECK: Document your findings and reasoning.
- Lab 1 - TypeScript introduction
- Lab 2 - Langium LSP Introduction
- Lab 3 - Code Generation
- Lab 4 - Large Language Models
- Lab 5 - Testing
- Lab 6 - Smart Contracts on Ethereum
ASE Lectures (fall semester)
- Practice 2a ‐ Simple Gradle CI CD
- Practice 2b ‐ Advanced Gradle CI CD
- Practice 3 ‐ Graph Modeling
- Practice 4 ‐ Textual editors
- Practice 5 ‐ LLMs
- Practice 6 ‐ Code Generation
- Practice 8 ‐ Benchmarking
- Practice 9 ‐ Data analysis
- Practice 10 ‐ Static analysis
- Practice 11 ‐ Code Coverage
- Homework part 1 ‐ Graph Modeling
- Homework part 2 ‐ Textual Modeling
- Homework part 3 ‐ Code Generation
- Homework part 4 ‐ Data Analysis
- IMSc Extra Homework Assignment
Old exams are available here.