From ecb7ec31a58c3308a59b9472cb14dc335d6f2537 Mon Sep 17 00:00:00 2001 From: jmc Date: Fri, 15 Sep 2023 17:07:37 +0100 Subject: [PATCH 01/11] add auction tutorial Signed-off-by: jmc --- docs/src/SUMMARY.md | 4 + .../example_contracts/auction_contract.md | 86 ++++++ .../src/user-guide/example_contracts/index.md | 1 + docs/src/user-guide/tutorials/auction.md | 258 ++++++++++++++++++ docs/src/user-guide/tutorials/index.md | 9 + 5 files changed, 358 insertions(+) create mode 100644 docs/src/user-guide/example_contracts/auction_contract.md create mode 100644 docs/src/user-guide/example_contracts/index.md create mode 100644 docs/src/user-guide/tutorials/auction.md create mode 100644 docs/src/user-guide/tutorials/index.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 78d42117a..906d92378 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -7,6 +7,10 @@ * [Using Fe](user-guide/index.md) * [Installation](user-guide/installation.md) * [Using projects](user-guide/projects.md) + * [Tutorials](user-guide/tutorials/index.md) + * [Open auction](user-guide/tutorials/auction.md) + * [Example Contracts](user-guide/example_contracts/index.md) + * [Open auction](user-guide/example_contracts/auction_contract.md) * [Development](development/index.md) * [Build & Test](development/build.md) * [Release](development/release.md) diff --git a/docs/src/user-guide/example_contracts/auction_contract.md b/docs/src/user-guide/example_contracts/auction_contract.md new file mode 100644 index 000000000..6030e698a --- /dev/null +++ b/docs/src/user-guide/example_contracts/auction_contract.md @@ -0,0 +1,86 @@ +```fe +// errors +struct AuctionAlreadyEnded { +} + +struct AuctionNotYetEnded { +} + +struct AuctionEndAlreadyCalled {} + +struct BidNotHighEnough { + pub highest_bid: u256 +} + +// events +struct HighestBidIncreased { + #indexed + pub bidder: address + pub amount: u256 +} + +struct AuctionEnded { + #indexed + pub winner: address + pub amount: u256 +} + +contract SimpleOpenAuction { + // states + auction_end_time: u256 + beneficiary: address + + highest_bidder: address + highest_bid: u256 + + pending_returns: Map + + ended: bool + + // constructor + pub fn __init__(mut self, ctx: Context, bidding_time: u256, beneficiary_addr: address) { + self.beneficiary = beneficiary_addr + self.auction_end_time = ctx.block_timestamp() + bidding_time + } + + //method + pub fn bid(mut self, mut ctx: Context) { + if ctx.block_timestamp() > self.auction_end_time { + revert AuctionAlreadyEnded() + } + if ctx.msg_value() <= self.highest_bid { + revert BidNotHighEnough(highest_bid: self.highest_bid) + } + if self.highest_bid != 0 { + self.pending_returns[self.highest_bidder] += self.highest_bid + } + self.highest_bidder = ctx.msg_sender() + self.highest_bid = ctx.msg_value() + + ctx.emit(HighestBidIncreased(bidder: ctx.msg_sender(), amount: ctx.msg_value())) + } + + pub fn withdraw(mut self, mut ctx: Context) -> bool { + let amount: u256 = self.pending_returns[ctx.msg_sender()] + + if amount > 0 { + self.pending_returns[ctx.msg_sender()] = 0 + ctx.send_value(to: ctx.msg_sender(), wei: amount) + } + return true + } + + pub fn action_end(mut self, mut ctx: Context) { + if ctx.block_timestamp() <= self.auction_end_time { + revert AuctionNotYetEnded() + } + if self.ended { + revert AuctionEndAlreadyCalled() + } + self.ended = true + ctx.emit(AuctionEnded(winner: self.highest_bidder, amount: self.highest_bid)) + + ctx.send_value(to: self.beneficiary, wei: self.highest_bid) + } +} +``` \ No newline at end of file diff --git a/docs/src/user-guide/example_contracts/index.md b/docs/src/user-guide/example_contracts/index.md new file mode 100644 index 000000000..2f14ff89c --- /dev/null +++ b/docs/src/user-guide/example_contracts/index.md @@ -0,0 +1 @@ +# Example Contracts diff --git a/docs/src/user-guide/tutorials/auction.md b/docs/src/user-guide/tutorials/auction.md new file mode 100644 index 000000000..aa63fbcc6 --- /dev/null +++ b/docs/src/user-guide/tutorials/auction.md @@ -0,0 +1,258 @@ +# Auction contract + +This tutorial aims to implement a simple auction contract in Fe. Along the way you will learn some foundational Fe concepts. + +An open auction is one where prices are determined in real time by live bidding. The winner is the participant who has made the highest bid at the time the auction ends. + +## The auction rules + +To run an open auction, you need an item for sale, a seller, a pool of buyers and a deadlien after which no more bids will be recognized. In this tutorial we will not have an item per se, the buyers are simply bidding to win! The highest bidder is provably crowned the winner, and the value of their bid is passed to the beneficiary. Bidders can also withdraw their bids at any time. + + +## Get Started + +To follow this guide you should have Fe installed on your computer. If you haven't installed Fe yet, follow the instructions on the [Installation](../installation.md) page. + +With Fe installed, you can create a project folder, `auction` that will act as your project root. In that folder, create an empty file called `auction.fe`. + +Now you are ready to start coding in Fe! + +You will also need [Foundry](https://getfoundry.sh) installed to follow the deployment instructions in this guide - you can use your alternative tooling for this if you prefer. + +## Writing the Contract + +You can see the entire contract [here](../example_contracts/auction_contract.md). You can refer back to this any time to check implementation details. + +### Defining the `Contract` and initializing variables + +A contract is Fe is defined using the `contract` keyword. A contract requires a constructor function to initialize any state variables used by the contract. If no constructor is defined, Fe will add a default with no state variables. The skeleton of the contract can look as follows: + +```rust +contract Auction { + pub fn __init__() {} +} +``` + +To run the auction you will need several state variables, some of which can be initialized at the time the contract is instantiated. +You will need to track the address of the beneficiary so you know who to pay out to. You will also need to keep track of the highest bidder, and the amount they have bid. You will also need to keep track of how much each specific address has sent into the contract, so you can refund them the right amount if they decide to withdraw. You will also need a flag that tracks whether or not the auction has ended. The following list of variables will suffice: + +```fe +auction_end_time: u256 +beneficiary: address +highest_bidder: address +highest_bid: u256 +pending_returns: Map +ended bool +``` + +Notice that variables are named using camel case (lower case, underscore separated, `like_this`). +[Addresses](../../spec/type_system/types/address.md) have their own type in Fe - it represents 20 hex-encoded bytes as per the Ethereum specification. + +The variables that expect numbers are given the `u256` type. This is an unsigned integer of length 256 bits. There are other [choices for integers](../../spec/type_system/types/numeric.md) too, with both signed and unsigned integers between 8 and 256 bits in length. + +The `ended` variable will be used to check whether the auction is live or not. If it has finished `ended` will be set to `true`. There are only two possible states for this, so it makes sense to declare it as a `bool` - i.e. true/false. + +The `pending returns` variable is a mapping between N keys and N values, with user addresses as the keys and their bids as values. For this, a `Map` type is used. In Fe, you define the types for the key and value in the Map definition - in this case it is `Map`. Keys can be any `numeric` type, `address`, `boolean` or `unit`. + +Now you should decide which of these variable will have values that are known at the time the contract is instantiated. It makes sense to set the `beneficiary` right away, so you can add that to the constructor arguments. + +The other thing to consider here is *how* the contract will keep track of time. On its own, the contract has no concept of time. However, the contract does have access to the current block timestamp which is measured in seconds since the Unix epoch (Jan 1st 1970). This can be used to measure the time elapsed in a smart contract. In this contract you can use this concept to set a deadline on the auction. By passing a length of time in seconds to the constructor, you can then add that value to the current block timestamp and create a deadline for bidding to end. Therefore, you should add a `bidding_time` argument to the constructor. Its type can be `u256`. + +When you have implemented all this, your contract should look like this: + +```fe +contract Auction { + // states + auction_end_time: u256 + beneficiary: address + highest_bidder: address + highest_bid: u256 + pending_returns: Map + ended: bool + + // constructor + pub fn __init__(mut self, ctx: Context, bidding_time: u256, beneficiary_addr: address) { + self.beneficiary = beneficiary_addr + self.auction_end_time = ctx.block_timestamp() + bidding_time + } +} +``` + +Notice that the constructor receive values for `bidding_time` and `beneficiary_addr` and uses the to initialize the contract's `auction_end_time` and `beneficiary` variables. + +The other thing to notice about the constructor is that there are two additional arguments passed to the constructor: `must self` and `ctx: Context`. + +#### self + +`self` is used to represent the specific instance of a Contract. It is used to access variables that are owned by that specific instance. This works the same way for Fe contracts as for, e.g. 'self' in the context of classes in Python, or `this` in Javascript. + +Here, you are not only using `self` but you are prepending it with `mut`. `mut` is a keyword inherited from Rust that indicates that the value can be overwritten - i.e. it is "mutable". Variables are not mutable by default - this is a safety feature that helps protect developers from unintended changes during runtime. If you do not make `self` mutable, then you will not be able to update the values it contains. + + +#### Context + +Context is used to track deadlines for requests. It gives the contract the ability to cancel functions that have taken too long to run, and they provide a structure for storing values that are scoped to a particular request so that they can be accessed anywhere in the call chain. This will be familiar to Go and Rust developers, as Context is often used in the same way in those languages and is frequently employed when working with external API calls. It is conventional to name the context object `ctx`. + +Read more on [Context in Go](https://www.makeuseof.com/go-contexts/) +Read more on [Context in Rust](https://docs.rs/ctx/latest/ctx/) + +In Fe contracts `ctx` will also be used to track transaction and blockchain data including `msg.sender`, `msg.value`, `block.timestamp` etc. + + +### Bidding + +Now that you have your contract constructor and state variables, you can implement some logic for receiving bids. To do this, you will create a method called `bid`. To handle a bid, you will first need to determine whether the auction is still open. If it has closed then the bid should revert. If the auction is open you need to record the address of the bidder and the amount and determine whether their bid was the highest. If their bid is highest, then their address should be assigned to the `highest_bidder` variable and the amount they sent recorded in the `highest_bid` variable. + +This logic can be implemented as follows: + +```fe +pub fn bid(mut self, mut ctx: Context) { + if ctx.block_timestamp() > self.auction_end_time { + revert AuctionAlreadyEnded() + } + if ctx.msg_value() <= self.highest_bid { + revert BidNotHighEnough(highest_bid: self.highest_bid) + } + if self.highest_bid != 0 { + self.pending_returns[self.highest_bidder] += self.highest_bid + } + self.highest_bidder = ctx.msg_sender() + self.highest_bid = ctx.msg_value() + + ctx.emit(HighestBidIncreased(bidder: ctx.msg_sender(), amount: ctx.msg_value())) +} +``` + +The method first checks that the current block timestamp is not later than the contract's `aution_end_time` variable. If it *is* later, then the contract reverts. This is triggered using the []`revert`](../spec/statements/revert.md) keyword. The `revert` can accept a struct that becomes encoded as [revert data](https://github.com/ethereum/EIPs/issues/838). Here you can just revert without any arguments. Add the following definition somehwere in `Auction.fe` outside the main contract definition: + +```fe +struct AuctionAlreadyEnded { +} +``` + +The next check is whether the incoming bid exceeds the current highest bid. If not, the bid has failed and it may as well revert. We can repeat the same logic as for `AuctionAlreadyEnded`. We can also report the current highest bid in the revert message to help the user to reprice if they want to. Add the following to `auction.fe`: + +``` +struct BidNotHighEnough { + pub highest_bid: u256 +} +``` + +> Notice that the value being checked is `msg.value` which is included in the `ctx` object. `ctx` is where you can access incoming transaction data. + +Next, if the incoming transaction *is* the highest bid, you need to track how much the sender shoudl receive as a payout if their bid ends up being exceeded by another user (i.e. if they get outbid, they get their ETH back). To do this, you add a key-value pair to the `pending_returns` mapping, with the user address as the key and the transaction amount as the value. Both of these come from `ctx` in the form of `msg.sender` and `msg.value`. + +Finally, if the incoming bid *is* the highest, you can emit an event. Events are useful because they provide a cheap way to return data from a contract as they use logs instead of contract storage. Unlike other smart contract languages there is no `emit` keyword or `Event` type. Instead, you trigger an event by calling the `emit` method on the `ctx` object. You can pass this method a struct that defines the emitted message. You can add the following struct for this event: + +```fe +struct HighestBidIncreased { + #indexed + pub bidder: address + pub amount: u256 +} +``` + +You have now implemented all the logic to handle a bid! + +### Withdrawing + +A previous high-bidder will want to retrieve their ETH from the contract so they can either walk-away for bid again. You therefore need to create a `withdraw` methodn that the user can call. The function will lookup the user address in `pending_returns`. if there is a non-zero value associated with the user's address, the contract should send that amount back to the sender's address. It is important to first update the value in `pending_returns` and *then* send the ETH to the user, otherwise you are exposing a [re-entrancy](https://www.certik.com/resources/blog/3K7ZUAKpOr1GW75J2i0VHh-what-is-a-reentracy-attack) vulnerability (where a user can repeatedlyt call the contract and receibve the ETH multiple times). + +Add the following to the contract to implement the `withdraw` method: + +```fe +pub fn withdraw(mut self, mut ctx: Context) -> bool { + let amount: u256 = self.pending_returns[ctx.msg_sender()] + + if amount > 0 { + self.pending_returns[ctx.msg_sender()] = 0 + ctx.send_value(to: ctx.msg_sender(), wei: amount) + } + return true +} +``` + +### End the auction + +Fianlly, you need to add a way to end the auction. This will check whether the biddign period is over, and if it is, automaticaly trigger the payment to the beneficiary and emit the address of the winner in an event. + +First, check the auction is not still live - if the auction is live you cannot end it early. If an attempt to end the auction early is made, it shoudl revert using a `AuctionNotYetEnded` struct, which can look as follows: + +```fe +struct AuctionNotYetEnded { +} +``` + +You should also check whether the auction was *already* ended by a previous valid call to this method. In this case, revert with a `AuctionEndAlreadyCalled` struct: + +```fe +struct AuctionEndAlreadyCalled {} +``` + +If the auction is still live, you can end it. First set `self.ended` to `true` to update the contract state. Then emit the event using `ctx.emit()`. Then, send the ETH to the beneficiary. Again, the order is important - you should always send value last to protect against re-entrancy. +Your method can look as follows: + +```fe +pub fn action_end(mut self, mut ctx: Context) { + if ctx.block_timestamp() <= self.auction_end_time { + revert AuctionNotYetEnded() + } + if self.ended { + revert AuctionEndAlreadyCalled() + } + self.ended = true + ctx.emit(AuctionEnded(winner: self.highest_bidder, amount: self.highest_bid)) + + ctx.send_value(to: self.beneficiary, wei: self.highest_bid) +} +``` + +Congratulations! You just write an open auction contract in Fe! + + +## Build and deploy the contract + +Your contract is now ready to use! Compile it using + +```fe +fe build auction.fe +``` + +You will find the contract ABI and bytecode in the newly created `outputs` directory. + +Deploy it to a local blockchain using: + +```sh +anvil +``` + +and + +```sh +cast send --rpc-url localhost:8545 --private-key --create $(cat output/Auction/Auction.bin) --constructor-args +``` + +Now you can interact with your contract using + +```sh +cast call "method_name(args)" --rpc-url localhost:8545 | cast to_ascii +``` + +For a reminder how to use Foundry to deploy and interact with contracts, revisit the [quickstart guide](../../quickstart/deploy_contract.md). + +## Summary + +Congratulations! You wrote an open auction contract in Fe and deployed it to a local blockchain! + +If you are using a local Anvil blockchain, you can use the ten ephemeral addresses created when the network started to simul;ate a bidding war! + +By following this tutorial, you learned: + +- basic Fe types, such as `bool`, `address`, `map` and `u256` +- basic Fe styles, such as snake case for variable names +- how to create a `contract` with a constructor +- how to `revert` +- how to handle state variables +- how to avoid reentrancy +- how to use `ctx` to handle transaction data +- how to emit events usign `ctx.emit` \ No newline at end of file diff --git a/docs/src/user-guide/tutorials/index.md b/docs/src/user-guide/tutorials/index.md new file mode 100644 index 000000000..2466b48b7 --- /dev/null +++ b/docs/src/user-guide/tutorials/index.md @@ -0,0 +1,9 @@ +# Tutorials + +Welcome to the Tutorials section. We will be adding walkthrough guides for example Fe projects here! + +For now, you can get started with: + +- [Open auction](auction.md) + +Watch this space for more tutorials coming soon! \ No newline at end of file From 37c61349e59ba47bac592ca7c5c45010f2a19ead Mon Sep 17 00:00:00 2001 From: jmc <33655003+jmcook1186@users.noreply.github.com> Date: Tue, 19 Sep 2023 09:45:54 +0100 Subject: [PATCH 02/11] add deploy details to tutorial Signed-off-by: jmc <33655003+jmcook1186@users.noreply.github.com> --- .../example_contracts/auction_contract.md | 16 ++- docs/src/user-guide/tutorials/auction.md | 107 ++++++++++++++++-- 2 files changed, 113 insertions(+), 10 deletions(-) diff --git a/docs/src/user-guide/example_contracts/auction_contract.md b/docs/src/user-guide/example_contracts/auction_contract.md index 6030e698a..8dc04adcc 100644 --- a/docs/src/user-guide/example_contracts/auction_contract.md +++ b/docs/src/user-guide/example_contracts/auction_contract.md @@ -25,7 +25,7 @@ struct AuctionEnded { pub amount: u256 } -contract SimpleOpenAuction { +contract Auction { // states auction_end_time: u256 beneficiary: address @@ -70,7 +70,7 @@ contract SimpleOpenAuction { return true } - pub fn action_end(mut self, mut ctx: Context) { + pub fn auction_end(mut self, mut ctx: Context) { if ctx.block_timestamp() <= self.auction_end_time { revert AuctionNotYetEnded() } @@ -82,5 +82,17 @@ contract SimpleOpenAuction { ctx.send_value(to: self.beneficiary, wei: self.highest_bid) } + + pub fn check_highest_bidder(mut self, ctx: Context) -> address { + return self.highest_bidder; + } + + pub fn check_highest_bid(mut self, ctx: Context) -> u256 { + return self.highest_bid; + } + + pub fn check_ended(mut self, ctx: Context) -> bool { + return self.ended; + } } ``` \ No newline at end of file diff --git a/docs/src/user-guide/tutorials/auction.md b/docs/src/user-guide/tutorials/auction.md index aa63fbcc6..dba9b4e33 100644 --- a/docs/src/user-guide/tutorials/auction.md +++ b/docs/src/user-guide/tutorials/auction.md @@ -210,6 +210,26 @@ pub fn action_end(mut self, mut ctx: Context) { Congratulations! You just write an open auction contract in Fe! +## View functions + +To help us test the copntract without having to decode transaction logs, we can add some simple functions to the contract that simply report the current values for some key state variables (specifically, `highest_bidder`, `highest_bid` and `ended`). This will allow a user to use `eth_call` to query these values in the contract. `eth_call` is used for functions that do not update the state of the blockchain and costs no gas because the queries can be performed on local data. + +You can add the following functions to the contract: + +```fe +pub fn check_highest_bidder(mut self, ctx: Context) -> address { + return self.highest_bidder; +} + +pub fn check_highest_bid(mut self, ctx: Context) -> u256 { + return self.highest_bid; +} + +pub fn check_ended(mut self, ctx: Context) -> bool { + return self.ended; +} +``` + ## Build and deploy the contract Your contract is now ready to use! Compile it using @@ -220,31 +240,100 @@ fe build auction.fe You will find the contract ABI and bytecode in the newly created `outputs` directory. -Deploy it to a local blockchain using: +Start a local blockchain to deploy your contract to: ```sh anvil ``` -and +There are constructor arguments (`bidding_time: u256`, `beneficiary_addr: address`) that have to be added to the contract bytecode so that the contract is instantiated with your desired values. To add constructor arguments you can encode them intoi bytecode and append them to the contract bytecode. + +First, hex encode the value you want to pass to `bidding_time`. In this case, we will use a value of 10: + +```sh +cast --to_hex(10) + +>> 0xa // this is 10 in hex +``` + +Ethereum addresses are already hex, so there is no further encoding required. The following command will take the constructor function and the hex-encoded arguments and concatenate them into a contiguous hex string, and save it to `constructor_args.txt`. + +```sh +cast abi-encode "__init__(uint256,address)" "0xa" "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720" > constructor_args.txt +``` + +Now append these constructor argument bytes to the bytecode generated by `fe build` (not that the `cut -c3` command removes the leading `0x` from the bytes being appended) + +```sh +cat constructor_args.txt | cut -c3- >> output/Auction/Auction.bin +``` + +Now deploy the contract to Anvil + +```sh +cast send --rpc-url localhost:8545 --private-key --create $(cat output/SimpleOpenAuction/SimpleOpenAuction.bin) +``` + +You will see the contract address reported in your terminal. + +Now you can interact with your contract. Start by sending an initial bid, let's say 100 ETH. For contract address `0x700b6A60ce7EaaEA56F065753d8dcB9653dbAD35`: + +```sh +cast send 0x700b6A60ce7EaaEA56F065753d8dcB9653dbAD35 "bid()" --value "100ether" --private-key --from 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720 +``` + +You can check whether this was successful by calling the `check_highest_bidder()` function: + +```sh +cast call 0x700b6A60ce7EaaEA56F065753d8dcB9653dbAD35 "check_highest_bidder()" +``` + +You will see a response looking similar to: + +```sh +0x000000000000000000000000a0Ee7A142d267C1f36714E4a8F75612F20a79720 +``` + +The characters after the leading zeros are the address for the highest bidder (notice they match the characters after the 0x in the bidding address). + +You can do the same to check the highest bid: + +```sh +cast call 0x700b6A60ce7EaaEA56F065753d8dcB9653dbAD35 "check_highest_bid()" +``` + +This returns: + +```sh +0x0000000000000000000000000000000000000000000000056bc75e2d63100000 +``` + +Converting the non zero characters to binary gives the decimal value of your bid (in wei - divide by 1e18 to get the value in ETH): + +```sh +cast --to-dec 56bc75e2d63100000 + +>> 100000000000000000000 // 100 ETH in wei +``` + +Now you can repeat this process, outbidding the initial bid from another address and check the `higyhest_bidder()` and `highest_bid()` to confirm. Do this a few times, then call `end_auction()` to see the value of the highest bid get transferred tot he `beneficiary_addr`. You can always check the balance of each address using: ```sh -cast send --rpc-url localhost:8545 --private-key --create $(cat output/Auction/Auction.bin) --constructor-args +cast balance
``` -Now you can interact with your contract using +Ands check whether the auction open time has expired using ```sh -cast call "method_name(args)" --rpc-url localhost:8545 | cast to_ascii +cast "check_ended()" ``` -For a reminder how to use Foundry to deploy and interact with contracts, revisit the [quickstart guide](../../quickstart/deploy_contract.md). ## Summary Congratulations! You wrote an open auction contract in Fe and deployed it to a local blockchain! -If you are using a local Anvil blockchain, you can use the ten ephemeral addresses created when the network started to simul;ate a bidding war! +If you are using a local Anvil blockchain, you can use the ten ephemeral addresses created when the network started to simulate a bidding war! By following this tutorial, you learned: @@ -255,4 +344,6 @@ By following this tutorial, you learned: - how to handle state variables - how to avoid reentrancy - how to use `ctx` to handle transaction data -- how to emit events usign `ctx.emit` \ No newline at end of file +- how to emit events usign `ctx.emit` +- how to deploy a contract with constructor arguments using Foundry +- how to interact with your contract \ No newline at end of file From 06c1a213464c48230c90586db0f4e455fa91d46f Mon Sep 17 00:00:00 2001 From: jmc <33655003+jmcook1186@users.noreply.github.com> Date: Wed, 20 Sep 2023 13:32:07 +0100 Subject: [PATCH 03/11] fix typos and fe -> rust for snippets --- docs/src/user-guide/tutorials/auction.md | 66 ++++++++++++------------ 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/docs/src/user-guide/tutorials/auction.md b/docs/src/user-guide/tutorials/auction.md index dba9b4e33..b804ae038 100644 --- a/docs/src/user-guide/tutorials/auction.md +++ b/docs/src/user-guide/tutorials/auction.md @@ -2,11 +2,11 @@ This tutorial aims to implement a simple auction contract in Fe. Along the way you will learn some foundational Fe concepts. -An open auction is one where prices are determined in real time by live bidding. The winner is the participant who has made the highest bid at the time the auction ends. +An open auction is one where prices are determined in real-time by live bidding. The winner is the participant who has made the highest bid at the time the auction ends. ## The auction rules -To run an open auction, you need an item for sale, a seller, a pool of buyers and a deadlien after which no more bids will be recognized. In this tutorial we will not have an item per se, the buyers are simply bidding to win! The highest bidder is provably crowned the winner, and the value of their bid is passed to the beneficiary. Bidders can also withdraw their bids at any time. +To run an open auction, you need an item for sale, a seller, a pool of buyers and a deadline after which no more bids will be recognized. In this tutorial we will not have an item per se, the buyers are simply bidding to win! The highest bidder is provably crowned the winner, and the value of their bid is passed to the beneficiary. Bidders can also withdraw their bids at any time. ## Get Started @@ -21,7 +21,7 @@ You will also need [Foundry](https://getfoundry.sh) installed to follow the depl ## Writing the Contract -You can see the entire contract [here](../example_contracts/auction_contract.md). You can refer back to this any time to check implementation details. +You can see the entire contract [here](../example_contracts/auction_contract.md). You can refer back to this at any time to check implementation details. ### Defining the `Contract` and initializing variables @@ -52,15 +52,15 @@ The variables that expect numbers are given the `u256` type. This is an unsigned The `ended` variable will be used to check whether the auction is live or not. If it has finished `ended` will be set to `true`. There are only two possible states for this, so it makes sense to declare it as a `bool` - i.e. true/false. -The `pending returns` variable is a mapping between N keys and N values, with user addresses as the keys and their bids as values. For this, a `Map` type is used. In Fe, you define the types for the key and value in the Map definition - in this case it is `Map`. Keys can be any `numeric` type, `address`, `boolean` or `unit`. +The `pending returns` variable is a mapping between N keys and N values, with user addresses as the keys and their bids as values. For this, a `Map` type is used. In Fe, you define the types for the key and value in the Map definition - in this case, it is `Map`. Keys can be any `numeric` type, `address`, `boolean` or `unit`. -Now you should decide which of these variable will have values that are known at the time the contract is instantiated. It makes sense to set the `beneficiary` right away, so you can add that to the constructor arguments. +Now you should decide which of these variables will have values that are known at the time the contract is instantiated. It makes sense to set the `beneficiary` right away, so you can add that to the constructor arguments. -The other thing to consider here is *how* the contract will keep track of time. On its own, the contract has no concept of time. However, the contract does have access to the current block timestamp which is measured in seconds since the Unix epoch (Jan 1st 1970). This can be used to measure the time elapsed in a smart contract. In this contract you can use this concept to set a deadline on the auction. By passing a length of time in seconds to the constructor, you can then add that value to the current block timestamp and create a deadline for bidding to end. Therefore, you should add a `bidding_time` argument to the constructor. Its type can be `u256`. +The other thing to consider here is *how* the contract will keep track of time. On its own, the contract has no concept of time. However, the contract does have access to the current block timestamp which is measured in seconds since the Unix epoch (Jan 1st 1970). This can be used to measure the time elapsed in a smart contract. In this contract, you can use this concept to set a deadline on the auction. By passing a length of time in seconds to the constructor, you can then add that value to the current block timestamp and create a deadline for bidding to end. Therefore, you should add a `bidding_time` argument to the constructor. Its type can be `u256`. When you have implemented all this, your contract should look like this: -```fe +```rust contract Auction { // states auction_end_time: u256 @@ -78,7 +78,7 @@ contract Auction { } ``` -Notice that the constructor receive values for `bidding_time` and `beneficiary_addr` and uses the to initialize the contract's `auction_end_time` and `beneficiary` variables. +Notice that the constructor receives values for `bidding_time` and `beneficiary_addr` and uses them to initialize the contract's `auction_end_time` and `beneficiary` variables. The other thing to notice about the constructor is that there are two additional arguments passed to the constructor: `must self` and `ctx: Context`. @@ -91,7 +91,7 @@ Here, you are not only using `self` but you are prepending it with `mut`. `mut` #### Context -Context is used to track deadlines for requests. It gives the contract the ability to cancel functions that have taken too long to run, and they provide a structure for storing values that are scoped to a particular request so that they can be accessed anywhere in the call chain. This will be familiar to Go and Rust developers, as Context is often used in the same way in those languages and is frequently employed when working with external API calls. It is conventional to name the context object `ctx`. +Context is used to track deadlines for requests. It gives the contract the ability to cancel functions that have taken too long to run, and provides a structure for storing values that are scoped to a particular request so that they can be accessed anywhere in the call chain. This will be familiar to Go and Rust developers, as Context is often used in the same way in those languages and is frequently employed when working with external API calls. It is conventional to name the context object `ctx`. Read more on [Context in Go](https://www.makeuseof.com/go-contexts/) Read more on [Context in Rust](https://docs.rs/ctx/latest/ctx/) @@ -105,7 +105,7 @@ Now that you have your contract constructor and state variables, you can impleme This logic can be implemented as follows: -```fe +```rust pub fn bid(mut self, mut ctx: Context) { if ctx.block_timestamp() > self.auction_end_time { revert AuctionAlreadyEnded() @@ -123,16 +123,16 @@ pub fn bid(mut self, mut ctx: Context) { } ``` -The method first checks that the current block timestamp is not later than the contract's `aution_end_time` variable. If it *is* later, then the contract reverts. This is triggered using the []`revert`](../spec/statements/revert.md) keyword. The `revert` can accept a struct that becomes encoded as [revert data](https://github.com/ethereum/EIPs/issues/838). Here you can just revert without any arguments. Add the following definition somehwere in `Auction.fe` outside the main contract definition: +The method first checks that the current block timestamp is not later than the contract's `aution_end_time` variable. If it *is* later, then the contract reverts. This is triggered using the []`revert`](../spec/statements/revert.md) keyword. The `revert` can accept a struct that becomes encoded as [revert data](https://github.com/ethereum/EIPs/issues/838). Here you can just revert without any arguments. Add the following definition somewhere in `Auction.fe` outside the main contract definition: -```fe +```rust struct AuctionAlreadyEnded { } ``` -The next check is whether the incoming bid exceeds the current highest bid. If not, the bid has failed and it may as well revert. We can repeat the same logic as for `AuctionAlreadyEnded`. We can also report the current highest bid in the revert message to help the user to reprice if they want to. Add the following to `auction.fe`: +The next check is whether the incoming bid exceeds the current highest bid. If not, the bid has failed and it may as well revert. We can repeat the same logic as for `AuctionAlreadyEnded`. We can also report the current highest bid in the revert message to help the user reprice if they want to. Add the following to `auction.fe`: -``` +```rust struct BidNotHighEnough { pub highest_bid: u256 } @@ -140,11 +140,11 @@ struct BidNotHighEnough { > Notice that the value being checked is `msg.value` which is included in the `ctx` object. `ctx` is where you can access incoming transaction data. -Next, if the incoming transaction *is* the highest bid, you need to track how much the sender shoudl receive as a payout if their bid ends up being exceeded by another user (i.e. if they get outbid, they get their ETH back). To do this, you add a key-value pair to the `pending_returns` mapping, with the user address as the key and the transaction amount as the value. Both of these come from `ctx` in the form of `msg.sender` and `msg.value`. +Next, if the incoming transaction *is* the highest bid, you need to track how much the sender should receive as a payout if their bid ends up being exceeded by another user (i.e. if they get outbid, they get their ETH back). To do this, you add a key-value pair to the `pending_returns` mapping, with the user address as the key and the transaction amount as the value. Both of these come from `ctx` in the form of `msg.sender` and `msg.value`. -Finally, if the incoming bid *is* the highest, you can emit an event. Events are useful because they provide a cheap way to return data from a contract as they use logs instead of contract storage. Unlike other smart contract languages there is no `emit` keyword or `Event` type. Instead, you trigger an event by calling the `emit` method on the `ctx` object. You can pass this method a struct that defines the emitted message. You can add the following struct for this event: +Finally, if the incoming bid *is* the highest, you can emit an event. Events are useful because they provide a cheap way to return data from a contract as they use logs instead of contract storage. Unlike other smart contract languages, there is no `emit` keyword or `Event` type. Instead, you trigger an event by calling the `emit` method on the `ctx` object. You can pass this method a struct that defines the emitted message. You can add the following struct for this event: -```fe +```rust struct HighestBidIncreased { #indexed pub bidder: address @@ -156,11 +156,11 @@ You have now implemented all the logic to handle a bid! ### Withdrawing -A previous high-bidder will want to retrieve their ETH from the contract so they can either walk-away for bid again. You therefore need to create a `withdraw` methodn that the user can call. The function will lookup the user address in `pending_returns`. if there is a non-zero value associated with the user's address, the contract should send that amount back to the sender's address. It is important to first update the value in `pending_returns` and *then* send the ETH to the user, otherwise you are exposing a [re-entrancy](https://www.certik.com/resources/blog/3K7ZUAKpOr1GW75J2i0VHh-what-is-a-reentracy-attack) vulnerability (where a user can repeatedlyt call the contract and receibve the ETH multiple times). +A previous high-bidder will want to retrieve their ETH from the contract so they can either walk away or bid again. You therefore need to create a `withdraw` method that the user can call. The function will lookup the user address in `pending_returns`. If there is a non-zero value associated with the user's address, the contract should send that amount back to the sender's address. It is important to first update the value in `pending_returns` and *then* send the ETH to the user, otherwise you are exposing a [re-entrancy](https://www.certik.com/resources/blog/3K7ZUAKpOr1GW75J2i0VHh-what-is-a-reentracy-attack) vulnerability (where a user can repeatedly call the contract and receive the ETH multiple times). Add the following to the contract to implement the `withdraw` method: -```fe +```rust pub fn withdraw(mut self, mut ctx: Context) -> bool { let amount: u256 = self.pending_returns[ctx.msg_sender()] @@ -174,25 +174,25 @@ pub fn withdraw(mut self, mut ctx: Context) -> bool { ### End the auction -Fianlly, you need to add a way to end the auction. This will check whether the biddign period is over, and if it is, automaticaly trigger the payment to the beneficiary and emit the address of the winner in an event. +Finally, you need to add a way to end the auction. This will check whether the bidding period is over, and if it is, automatically trigger the payment to the beneficiary and emit the address of the winner in an event. -First, check the auction is not still live - if the auction is live you cannot end it early. If an attempt to end the auction early is made, it shoudl revert using a `AuctionNotYetEnded` struct, which can look as follows: +First, check the auction is not still live - if the auction is live you cannot end it early. If an attempt to end the auction early is made, it should revert using a `AuctionNotYetEnded` struct, which can look as follows: -```fe +```rust struct AuctionNotYetEnded { } ``` You should also check whether the auction was *already* ended by a previous valid call to this method. In this case, revert with a `AuctionEndAlreadyCalled` struct: -```fe +```rust struct AuctionEndAlreadyCalled {} ``` If the auction is still live, you can end it. First set `self.ended` to `true` to update the contract state. Then emit the event using `ctx.emit()`. Then, send the ETH to the beneficiary. Again, the order is important - you should always send value last to protect against re-entrancy. Your method can look as follows: -```fe +```rust pub fn action_end(mut self, mut ctx: Context) { if ctx.block_timestamp() <= self.auction_end_time { revert AuctionNotYetEnded() @@ -207,16 +207,16 @@ pub fn action_end(mut self, mut ctx: Context) { } ``` -Congratulations! You just write an open auction contract in Fe! +Congratulations! You just wrote an open auction contract in Fe! ## View functions -To help us test the copntract without having to decode transaction logs, we can add some simple functions to the contract that simply report the current values for some key state variables (specifically, `highest_bidder`, `highest_bid` and `ended`). This will allow a user to use `eth_call` to query these values in the contract. `eth_call` is used for functions that do not update the state of the blockchain and costs no gas because the queries can be performed on local data. +To help test the contract without having to decode transaction logs, you can add some simple functions to the contract that simply report the current values for some key state variables (specifically, `highest_bidder`, `highest_bid` and `ended`). This will allow a user to use `eth_call` to query these values in the contract. `eth_call` is used for functions that do not update the state of the blockchain and costs no gas because the queries can be performed on local data. You can add the following functions to the contract: -```fe +```rust pub fn check_highest_bidder(mut self, ctx: Context) -> address { return self.highest_bidder; } @@ -234,7 +234,7 @@ pub fn check_ended(mut self, ctx: Context) -> bool { Your contract is now ready to use! Compile it using -```fe +```sh fe build auction.fe ``` @@ -246,7 +246,7 @@ Start a local blockchain to deploy your contract to: anvil ``` -There are constructor arguments (`bidding_time: u256`, `beneficiary_addr: address`) that have to be added to the contract bytecode so that the contract is instantiated with your desired values. To add constructor arguments you can encode them intoi bytecode and append them to the contract bytecode. +There are constructor arguments (`bidding_time: u256`, `beneficiary_addr: address`) that have to be added to the contract bytecode so that the contract is instantiated with your desired values. To add constructor arguments you can encode them into bytecode and append them to the contract bytecode. First, hex encode the value you want to pass to `bidding_time`. In this case, we will use a value of 10: @@ -308,7 +308,7 @@ This returns: 0x0000000000000000000000000000000000000000000000056bc75e2d63100000 ``` -Converting the non zero characters to binary gives the decimal value of your bid (in wei - divide by 1e18 to get the value in ETH): +Converting the non-zero characters to binary gives the decimal value of your bid (in wei - divide by 1e18 to get the value in ETH): ```sh cast --to-dec 56bc75e2d63100000 @@ -316,13 +316,13 @@ cast --to-dec 56bc75e2d63100000 >> 100000000000000000000 // 100 ETH in wei ``` -Now you can repeat this process, outbidding the initial bid from another address and check the `higyhest_bidder()` and `highest_bid()` to confirm. Do this a few times, then call `end_auction()` to see the value of the highest bid get transferred tot he `beneficiary_addr`. You can always check the balance of each address using: +Now you can repeat this process, outbidding the initial bid from another address and check the `higyhest_bidder()` and `highest_bid()` to confirm. Do this a few times, then call `end_auction()` to see the value of the highest bid get transferred to the `beneficiary_addr`. You can always check the balance of each address using: ```sh cast balance
``` -Ands check whether the auction open time has expired using +And check whether the auction open time has expired using ```sh cast "check_ended()" @@ -344,6 +344,6 @@ By following this tutorial, you learned: - how to handle state variables - how to avoid reentrancy - how to use `ctx` to handle transaction data -- how to emit events usign `ctx.emit` +- how to emit events using `ctx.emit` - how to deploy a contract with constructor arguments using Foundry - how to interact with your contract \ No newline at end of file From acefcb624bbf87b16a12bcc0414674a048aa9f94 Mon Sep 17 00:00:00 2001 From: jmc <33655003+jmcook1186@users.noreply.github.com> Date: Wed, 20 Sep 2023 13:44:16 +0100 Subject: [PATCH 04/11] fix linter Signed-off-by: jmc <33655003+jmcook1186@users.noreply.github.com> --- docs/src/user-guide/tutorials/auction.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/user-guide/tutorials/auction.md b/docs/src/user-guide/tutorials/auction.md index b804ae038..7af82277a 100644 --- a/docs/src/user-guide/tutorials/auction.md +++ b/docs/src/user-guide/tutorials/auction.md @@ -36,13 +36,13 @@ contract Auction { To run the auction you will need several state variables, some of which can be initialized at the time the contract is instantiated. You will need to track the address of the beneficiary so you know who to pay out to. You will also need to keep track of the highest bidder, and the amount they have bid. You will also need to keep track of how much each specific address has sent into the contract, so you can refund them the right amount if they decide to withdraw. You will also need a flag that tracks whether or not the auction has ended. The following list of variables will suffice: -```fe +```sh auction_end_time: u256 beneficiary: address highest_bidder: address highest_bid: u256 pending_returns: Map -ended bool +ended: bool ``` Notice that variables are named using camel case (lower case, underscore separated, `like_this`). From 57049148fa52b58d3adab5c3513a01e10cfa0064 Mon Sep 17 00:00:00 2001 From: jmc <33655003+jmcook1186@users.noreply.github.com> Date: Mon, 25 Sep 2023 13:47:56 +0100 Subject: [PATCH 05/11] apply changes from review Signed-off-by: jmc <33655003+jmcook1186@users.noreply.github.com> --- .../example_contracts/auction_contract.md | 6 +++--- docs/src/user-guide/tutorials/auction.md | 20 ++++++++++--------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/docs/src/user-guide/example_contracts/auction_contract.md b/docs/src/user-guide/example_contracts/auction_contract.md index 8dc04adcc..79f088dff 100644 --- a/docs/src/user-guide/example_contracts/auction_contract.md +++ b/docs/src/user-guide/example_contracts/auction_contract.md @@ -83,15 +83,15 @@ contract Auction { ctx.send_value(to: self.beneficiary, wei: self.highest_bid) } - pub fn check_highest_bidder(mut self, ctx: Context) -> address { + pub fn check_highest_bidder(mut self) -> address { return self.highest_bidder; } - pub fn check_highest_bid(mut self, ctx: Context) -> u256 { + pub fn check_highest_bid(mut self) -> u256 { return self.highest_bid; } - pub fn check_ended(mut self, ctx: Context) -> bool { + pub fn check_ended(mut self) -> bool { return self.ended; } } diff --git a/docs/src/user-guide/tutorials/auction.md b/docs/src/user-guide/tutorials/auction.md index 7af82277a..f9dfe7508 100644 --- a/docs/src/user-guide/tutorials/auction.md +++ b/docs/src/user-guide/tutorials/auction.md @@ -80,7 +80,7 @@ contract Auction { Notice that the constructor receives values for `bidding_time` and `beneficiary_addr` and uses them to initialize the contract's `auction_end_time` and `beneficiary` variables. -The other thing to notice about the constructor is that there are two additional arguments passed to the constructor: `must self` and `ctx: Context`. +The other thing to notice about the constructor is that there are two additional arguments passed to the constructor: `mut self` and `ctx: Context`. #### self @@ -91,12 +91,11 @@ Here, you are not only using `self` but you are prepending it with `mut`. `mut` #### Context -Context is used to track deadlines for requests. It gives the contract the ability to cancel functions that have taken too long to run, and provides a structure for storing values that are scoped to a particular request so that they can be accessed anywhere in the call chain. This will be familiar to Go and Rust developers, as Context is often used in the same way in those languages and is frequently employed when working with external API calls. It is conventional to name the context object `ctx`. +Context is used to gate access to certain features including emitting logs, creating contracts, reading messages and transferring ETH. It is conventional to name the context object `ctx`. The `Context` object needs to be passed as the *first* parameter to a function unless the function also takes `self`, in which case the `Context` object should be passed as the second parameter. `Context` must be expicitly made mutable if it will invoke functions that changes the blockchain data, whereas an immutable reference to `Context` can be used where read-only access to the blockchain is needed. -Read more on [Context in Go](https://www.makeuseof.com/go-contexts/) -Read more on [Context in Rust](https://docs.rs/ctx/latest/ctx/) +Read more on [Context in Fe](https://github.com/ethereum/fe/issues/558) -In Fe contracts `ctx` will also be used to track transaction and blockchain data including `msg.sender`, `msg.value`, `block.timestamp` etc. +In Fe contracts `ctx` is where you can find transaction data such as `msg.sender`, `msg.value`, `block.timestamp` etc. ### Bidding @@ -123,7 +122,7 @@ pub fn bid(mut self, mut ctx: Context) { } ``` -The method first checks that the current block timestamp is not later than the contract's `aution_end_time` variable. If it *is* later, then the contract reverts. This is triggered using the []`revert`](../spec/statements/revert.md) keyword. The `revert` can accept a struct that becomes encoded as [revert data](https://github.com/ethereum/EIPs/issues/838). Here you can just revert without any arguments. Add the following definition somewhere in `Auction.fe` outside the main contract definition: +The method first checks that the current block timestamp is not later than the contract's `aution_end_time` variable. If it *is* later, then the contract reverts. This is triggered using the [`revert`](../spec/statements/revert.md) keyword. The `revert` can accept a struct that becomes encoded as [revert data](https://github.com/ethereum/EIPs/issues/838). Here you can just revert without any arguments. Add the following definition somewhere in `Auction.fe` outside the main contract definition: ```rust struct AuctionAlreadyEnded { @@ -172,6 +171,9 @@ pub fn withdraw(mut self, mut ctx: Context) -> bool { } ``` +>**Note** that in this case `mut` is used with `ctx` because `send_value` is making changes to the blockchain (it is moving ETH from one address to another). + + ### End the auction Finally, you need to add a way to end the auction. This will check whether the bidding period is over, and if it is, automatically trigger the payment to the beneficiary and emit the address of the winner in an event. @@ -217,15 +219,15 @@ To help test the contract without having to decode transaction logs, you can add You can add the following functions to the contract: ```rust -pub fn check_highest_bidder(mut self, ctx: Context) -> address { +pub fn check_highest_bidder(mut self) -> address { return self.highest_bidder; } -pub fn check_highest_bid(mut self, ctx: Context) -> u256 { +pub fn check_highest_bid(mut self) -> u256 { return self.highest_bid; } -pub fn check_ended(mut self, ctx: Context) -> bool { +pub fn check_ended(mut self) -> bool { return self.ended; } ``` From d9d046ae09539539dd746600cfb3c2e9ee14b44c Mon Sep 17 00:00:00 2001 From: jmc <33655003+jmcook1186@users.noreply.github.com> Date: Mon, 25 Sep 2023 13:49:04 +0100 Subject: [PATCH 06/11] add newsfragment Signed-off-by: jmc <33655003+jmcook1186@users.noreply.github.com> --- newsfragments/930.doc.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/930.doc.md diff --git a/newsfragments/930.doc.md b/newsfragments/930.doc.md new file mode 100644 index 000000000..e7cc7dcd3 --- /dev/null +++ b/newsfragments/930.doc.md @@ -0,0 +1 @@ +Added a new tutorial: Open Auction \ No newline at end of file From 7d39601f7ea34865a72fff433cc99a42a36b63ab Mon Sep 17 00:00:00 2001 From: jmc <33655003+jmcook1186@users.noreply.github.com> Date: Mon, 25 Sep 2023 13:53:08 +0100 Subject: [PATCH 07/11] fix link Signed-off-by: jmc <33655003+jmcook1186@users.noreply.github.com> --- docs/src/user-guide/tutorials/auction.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/user-guide/tutorials/auction.md b/docs/src/user-guide/tutorials/auction.md index f9dfe7508..568d5c7eb 100644 --- a/docs/src/user-guide/tutorials/auction.md +++ b/docs/src/user-guide/tutorials/auction.md @@ -122,7 +122,7 @@ pub fn bid(mut self, mut ctx: Context) { } ``` -The method first checks that the current block timestamp is not later than the contract's `aution_end_time` variable. If it *is* later, then the contract reverts. This is triggered using the [`revert`](../spec/statements/revert.md) keyword. The `revert` can accept a struct that becomes encoded as [revert data](https://github.com/ethereum/EIPs/issues/838). Here you can just revert without any arguments. Add the following definition somewhere in `Auction.fe` outside the main contract definition: +The method first checks that the current block timestamp is not later than the contract's `aution_end_time` variable. If it *is* later, then the contract reverts. This is triggered using the [`revert`](../../spec/statements/revert.md) keyword. The `revert` can accept a struct that becomes encoded as [revert data](https://github.com/ethereum/EIPs/issues/838). Here you can just revert without any arguments. Add the following definition somewhere in `Auction.fe` outside the main contract definition: ```rust struct AuctionAlreadyEnded { From 44e540954704ad79acc6ffa5d9f26c8acd710a63 Mon Sep 17 00:00:00 2001 From: jmc <33655003+jmcook1186@users.noreply.github.com> Date: Mon, 25 Sep 2023 14:16:17 +0100 Subject: [PATCH 08/11] add newline to fix linter Signed-off-by: jmc <33655003+jmcook1186@users.noreply.github.com> --- newsfragments/930.doc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/newsfragments/930.doc.md b/newsfragments/930.doc.md index e7cc7dcd3..2a1e42cd5 100644 --- a/newsfragments/930.doc.md +++ b/newsfragments/930.doc.md @@ -1 +1 @@ -Added a new tutorial: Open Auction \ No newline at end of file +Added a new tutorial: Open Auction From 1318e7c22827386fd0b075b70c0af754f89e613f Mon Sep 17 00:00:00 2001 From: Joseph Cook <33655003+jmcook1186@users.noreply.github.com> Date: Wed, 27 Sep 2023 15:39:28 +0100 Subject: [PATCH 09/11] use cast send --create to deploy contract w constructor args --- docs/src/user-guide/tutorials/auction.md | 30 +++++++----------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/docs/src/user-guide/tutorials/auction.md b/docs/src/user-guide/tutorials/auction.md index 568d5c7eb..3ebf30dc6 100644 --- a/docs/src/user-guide/tutorials/auction.md +++ b/docs/src/user-guide/tutorials/auction.md @@ -45,14 +45,14 @@ pending_returns: Map ended: bool ``` -Notice that variables are named using camel case (lower case, underscore separated, `like_this`). +Notice that variables are named using snake case (lower case, underscore separated, `like_this`). [Addresses](../../spec/type_system/types/address.md) have their own type in Fe - it represents 20 hex-encoded bytes as per the Ethereum specification. The variables that expect numbers are given the `u256` type. This is an unsigned integer of length 256 bits. There are other [choices for integers](../../spec/type_system/types/numeric.md) too, with both signed and unsigned integers between 8 and 256 bits in length. The `ended` variable will be used to check whether the auction is live or not. If it has finished `ended` will be set to `true`. There are only two possible states for this, so it makes sense to declare it as a `bool` - i.e. true/false. -The `pending returns` variable is a mapping between N keys and N values, with user addresses as the keys and their bids as values. For this, a `Map` type is used. In Fe, you define the types for the key and value in the Map definition - in this case, it is `Map`. Keys can be any `numeric` type, `address`, `boolean` or `unit`. +The `pending_returns` variable is a mapping between N keys and N values, with user addresses as the keys and their bids as values. For this, a `Map` type is used. In Fe, you define the types for the key and value in the Map definition - in this case, it is `Map`. Keys can be any `numeric` type, `address`, `boolean` or `unit`. Now you should decide which of these variables will have values that are known at the time the contract is instantiated. It makes sense to set the `beneficiary` right away, so you can add that to the constructor arguments. @@ -219,15 +219,15 @@ To help test the contract without having to decode transaction logs, you can add You can add the following functions to the contract: ```rust -pub fn check_highest_bidder(mut self) -> address { +pub fn check_highest_bidder(self) -> address { return self.highest_bidder; } -pub fn check_highest_bid(mut self) -> u256 { +pub fn check_highest_bid(self) -> u256 { return self.highest_bid; } -pub fn check_ended(mut self) -> bool { +pub fn check_ended(self) -> bool { return self.ended; } ``` @@ -258,22 +258,10 @@ cast --to_hex(10) >> 0xa // this is 10 in hex ``` -Ethereum addresses are already hex, so there is no further encoding required. The following command will take the constructor function and the hex-encoded arguments and concatenate them into a contiguous hex string, and save it to `constructor_args.txt`. +Ethereum addresses are already hex, so there is no further encoding required. The following command will take the constructor function and the hex-encoded arguments and concatenate them into a contiguous hex string and then deploy the contract with the constructor arguments. ```sh -cast abi-encode "__init__(uint256,address)" "0xa" "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720" > constructor_args.txt -``` - -Now append these constructor argument bytes to the bytecode generated by `fe build` (not that the `cut -c3` command removes the leading `0x` from the bytes being appended) - -```sh -cat constructor_args.txt | cut -c3- >> output/Auction/Auction.bin -``` - -Now deploy the contract to Anvil - -```sh -cast send --rpc-url localhost:8545 --private-key --create $(cat output/SimpleOpenAuction/SimpleOpenAuction.bin) +cast send --from --private-key --create $(cat output/Auction/Auction.bin) $(cast abi-encode "__init__(uint256,address)" 0xa 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720) ``` You will see the contract address reported in your terminal. @@ -318,7 +306,7 @@ cast --to-dec 56bc75e2d63100000 >> 100000000000000000000 // 100 ETH in wei ``` -Now you can repeat this process, outbidding the initial bid from another address and check the `higyhest_bidder()` and `highest_bid()` to confirm. Do this a few times, then call `end_auction()` to see the value of the highest bid get transferred to the `beneficiary_addr`. You can always check the balance of each address using: +Now you can repeat this process, outbidding the initial bid from another address and check the `highest_bidder()` and `highest_bid()` to confirm. Do this a few times, then call `end_auction()` to see the value of the highest bid get transferred to the `beneficiary_addr`. You can always check the balance of each address using: ```sh cast balance
@@ -348,4 +336,4 @@ By following this tutorial, you learned: - how to use `ctx` to handle transaction data - how to emit events using `ctx.emit` - how to deploy a contract with constructor arguments using Foundry -- how to interact with your contract \ No newline at end of file +- how to interact with your contract From 9d717aeec3edd1e94088b7afe5db327a4dd60b62 Mon Sep 17 00:00:00 2001 From: Joseph Cook <33655003+jmcook1186@users.noreply.github.com> Date: Wed, 27 Sep 2023 15:45:45 +0100 Subject: [PATCH 10/11] make `self` immutable in view funcs --- docs/src/user-guide/example_contracts/auction_contract.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/user-guide/example_contracts/auction_contract.md b/docs/src/user-guide/example_contracts/auction_contract.md index 79f088dff..7f8bd077b 100644 --- a/docs/src/user-guide/example_contracts/auction_contract.md +++ b/docs/src/user-guide/example_contracts/auction_contract.md @@ -83,16 +83,16 @@ contract Auction { ctx.send_value(to: self.beneficiary, wei: self.highest_bid) } - pub fn check_highest_bidder(mut self) -> address { + pub fn check_highest_bidder(self) -> address { return self.highest_bidder; } - pub fn check_highest_bid(mut self) -> u256 { + pub fn check_highest_bid(self) -> u256 { return self.highest_bid; } - pub fn check_ended(mut self) -> bool { + pub fn check_ended(self) -> bool { return self.ended; } } -``` \ No newline at end of file +``` From 7d9294f955d06b41b50d6aa88a9e25ff371835cd Mon Sep 17 00:00:00 2001 From: jmc <33655003+jmcook1186@users.noreply.github.com> Date: Wed, 27 Sep 2023 15:59:49 +0100 Subject: [PATCH 11/11] replace rust tags with fe where possible Signed-off-by: jmc <33655003+jmcook1186@users.noreply.github.com> --- docs/src/user-guide/example_contracts/index.md | 2 ++ docs/src/user-guide/tutorials/auction.md | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/src/user-guide/example_contracts/index.md b/docs/src/user-guide/example_contracts/index.md index 2f14ff89c..19deff169 100644 --- a/docs/src/user-guide/example_contracts/index.md +++ b/docs/src/user-guide/example_contracts/index.md @@ -1 +1,3 @@ # Example Contracts + +- [Simple open auction](./auction_contract.md) \ No newline at end of file diff --git a/docs/src/user-guide/tutorials/auction.md b/docs/src/user-guide/tutorials/auction.md index 3ebf30dc6..288c48231 100644 --- a/docs/src/user-guide/tutorials/auction.md +++ b/docs/src/user-guide/tutorials/auction.md @@ -60,7 +60,7 @@ The other thing to consider here is *how* the contract will keep track of time. When you have implemented all this, your contract should look like this: -```rust +```fe contract Auction { // states auction_end_time: u256 @@ -124,14 +124,14 @@ pub fn bid(mut self, mut ctx: Context) { The method first checks that the current block timestamp is not later than the contract's `aution_end_time` variable. If it *is* later, then the contract reverts. This is triggered using the [`revert`](../../spec/statements/revert.md) keyword. The `revert` can accept a struct that becomes encoded as [revert data](https://github.com/ethereum/EIPs/issues/838). Here you can just revert without any arguments. Add the following definition somewhere in `Auction.fe` outside the main contract definition: -```rust +```fe struct AuctionAlreadyEnded { } ``` The next check is whether the incoming bid exceeds the current highest bid. If not, the bid has failed and it may as well revert. We can repeat the same logic as for `AuctionAlreadyEnded`. We can also report the current highest bid in the revert message to help the user reprice if they want to. Add the following to `auction.fe`: -```rust +```fe struct BidNotHighEnough { pub highest_bid: u256 } @@ -143,7 +143,7 @@ Next, if the incoming transaction *is* the highest bid, you need to track how mu Finally, if the incoming bid *is* the highest, you can emit an event. Events are useful because they provide a cheap way to return data from a contract as they use logs instead of contract storage. Unlike other smart contract languages, there is no `emit` keyword or `Event` type. Instead, you trigger an event by calling the `emit` method on the `ctx` object. You can pass this method a struct that defines the emitted message. You can add the following struct for this event: -```rust +```fe struct HighestBidIncreased { #indexed pub bidder: address @@ -180,14 +180,14 @@ Finally, you need to add a way to end the auction. This will check whether the b First, check the auction is not still live - if the auction is live you cannot end it early. If an attempt to end the auction early is made, it should revert using a `AuctionNotYetEnded` struct, which can look as follows: -```rust +```fe struct AuctionNotYetEnded { } ``` You should also check whether the auction was *already* ended by a previous valid call to this method. In this case, revert with a `AuctionEndAlreadyCalled` struct: -```rust +```fe struct AuctionEndAlreadyCalled {} ```