What is Fe?
Fe is the next generation smart contract language for Ethereum.
Fe is a smart contract language that strives to make developing Ethereum smart contract development safer, simpler and more fun.
Smart contracts are programs executed by a computer embedded into Ethereum clients known as the Ethereum Virtual Machine (EVM). The EVM executes bytecode instructions that are not human readable. Therefore, developers use higher-level languages that compiles to EVM bytecode.
Fe is one of these languages.
Why Fe?
Fe aims to make writing secure smart contract code a great experience. With Fe, writing safe code feels natural and fun.
Fe shares similar syntax with the popular languages Rust and Python, easing the learning curve for new users. It also implements the best features from Rust to limit dynamic behaviour while also maximizing expressiveness, meaning you can write clean, readable code without sacrificing compile time guarantees.
Fe is:
- statically typed
- expressive
- compiled using Yul
- built to a detailed language specification
- able to limit dynamic behaviour
- rapidly evolving!
Who is Fe for?
Fe is for anyone that develops using the EVM!
Fe compiles to EVM bytecode that can be deployed directly onto Ethereum and EVM-equivalent blockchains.
Fe's syntax will feel familiar to Rust and Python developers.
Here's what a minimal contract looks like in Fe:
contract GuestBook {
messages: Map<address, String<100>>
pub fn sign(mut self, ctx: Context, book_msg: String<100>) {
self.messages[ctx.msg_sender()] = book_msg
}
}
What problems does Fe solve?
One of the pain points with smart contract languages is that there can be ambiguities in how the compiler translates the human readable code into EVM bytecode. This can lead to security flaws and unexpected behaviours.
The details of the EVM can also cause the higher level languages to be less intuitive and harder to master than some other languages. These are some of the pain points Fe aims to solve. By striving to maximize both human readability and bytecode predictability, Fe will provide an enhanced developer experience for everyone working with the EVM.
Get Started
You can read much more information about Fe in these docs. If you want to get building, you can begin with our Quickstart guide.
You can also get involved in the Fe community by contributing code or documentation to the project Github or joining the conversation on Discord. Learn more on our Contributing page.
Quickstart
Let's get started with Fe!
In this section you will learn how to write and deploy your first contract.
Download and install Fe
Before you dive in, you need to download and install Fe. For this quickstart you should simply download the binary from fe-lang.org.
Then change the name and file permissions:
mv fe_amd64 fe
chmod +x fe
Now you are ready to do the quickstart tutorial!
For more detailed information on installing Fe, or to troubleshoot, see the Installation page in our user guide.
Write your first Fe contract
Now that we have the compiler installed let's write our first contract. A contract contains the code that will be deployed to the Ethereum blockchain and resides at a specific address.
The code of the contract dictates how:
- it manipulates its own state
- interacts with other contracts
- exposes external APIs to be called from other contracts or users
To keep things simple we will just write a basic guestbook where people can leave a message associated with their Ethereum address.
Note: Real code would not instrument the Ethereum blockchain in such a way as it is a waste of precious resources. This code is for demo purposes only.
Create a guest_book.fe
file
Fe code is written in files ending on the .fe
file extension. Let's create a file guest_book.fe
and put in the following content.
contract GuestBook {
messages: Map<address, String<100>>
}
Here we're using a map
to associate messages with Ethereum addresses.
The messages will simply be a string
of a maximum length of 100
written as String<100>
.
The addresses are represented by the builtin address
type.
Execute ./fe build guest_book.fe
to compile the file. The compiler tells us that it compiled our contract and that it has put the artifacts into a subdirectory called output
.
Compiled guest_book.fe. Outputs in `output`
If we examine the output
directory we'll find a subdirectory GuestBook
with a GuestBook_abi.json
and a GuestBook.bin
file.
├── fe
├── guest_book.fe
└── output
└── GuestBook
├── GuestBook_abi.json
└── GuestBook.bin
The GuestBook_abi.json
is a JSON representation that describes the binary interface of our contract but since our contract doesn't yet expose anything useful its content for now resembles an empty array.
The GuestBook.bin
is slightly more interesting containing what looks like a gibberish of characters which in fact is the compiled binary contract code written in hexadecimal characters.
We don't need to do anything further yet with these files that the compiler produces but they will become important when we get to the point where we want to deploy our code to the Ethereum blockchain.
Add a method to sign the guest book
Let's focus on the functionality of our world changing application and add a method to sign the guestbook.
contract GuestBook {
messages: Map<address, String<100>>
pub fn sign(mut self, ctx: Context, book_msg: String<100>) {
self.messages[ctx.msg_sender()] = book_msg
}
}
In Fe, every method that is defined without the pub
keyword becomes private. Since we want people to interact with our contract and call the sign
method we have to prefix it with pub
.
Let's recompile the contract again and see what happens.
Failed to write output to directory: `output`. Error: Directory 'output' is not empty. Use --overwrite to overwrite.
Oops, the compiler is telling us that the output
directory is a non-empty directory and plays it safe by asking us if we are sure that we want to overwrite it. We have to use the --overwrite
flag to allow the compiler to overwrite what is stored in the output
directory.
Let's try it again with ./fe build guest_book.fe --overwrite
.
This time it worked and we can also see that the GuestBook_abi.json
has become slightly more interesting.
[
{
"name": "sign",
"type": "function",
"inputs": [
{
"name": "book_msg",
"type": "bytes100"
}
],
"outputs": []
}
]
Since our contract now has a public sign
method the corresponding ABI has changed accordingly.
Add a method to read a message
To make the guest book more useful we will also add a method get_msg
to read entries from a given address.
contract GuestBook {
messages: Map<address, String<100>>
pub fn sign(mut self, ctx: Context, book_msg: String<100>) {
self.messages[ctx.msg_sender()] = book_msg
}
pub fn get_msg(self, addr: address) -> String<100> {
return self.messages[addr]
}
}
However, we will hit another error as we try to recompile the current code.
Unable to compile guest_book.fe.
error: value must be copied to memory
┌─ guest_book.fe:10:14
│
8 │ return self.messages[addr]
│ ^^^^^^^^^^^^^^^^^^^ this value is in storage
│
= Hint: values located in storage can be copied to memory using the `to_mem` function.
= Example: `self.my_array.to_mem()`
When we try to return a reference type such as an array from the storage of the contract we have to explicitly copy it to memory using the to_mem()
function.
Note: In the future Fe will likely introduce immutable storage pointers which might affect these semantics.
The code should compile fine when we change it accordingly.
contract GuestBook {
messages: Map<address, String<100>>
pub fn sign(mut self, ctx: Context, book_msg: String<100>) {
self.messages[ctx.msg_sender()] = book_msg
}
pub fn get_msg(self, addr: address) -> String<100> {
return self.messages[addr].to_mem()
}
}
Congratulations! You finished your first little Fe project. 👏 In the next chapter we will learn how to deploy our code and tweak it a bit further.
Deploy your contract.
Prerequisites
You should have compiled the GuestBook
contract and have both Guestbook_abi.json
and GuestBook.bin
available in your outputs
folder.
If you don't have any of these components, please revisit Write your first contract.
Introduction
When you develop smart contracts it is common to test them on local blockchains first because they are quick and easy to create and it doesn't matter if you make mistakes - there is nothing of real value secured by the blockchain as it only exists on your computer. Later, you can deploy your contract on a public test network to see how it behaves in a more realistic environment where other developers are also testing their code. Finally, when you are very confident that your contract is ready, you can deploy to Ethereum Mainnet (or one of its live Layer-2 networks). Once the contract is deployed on a "live" network, you are handling assets with real-world value!
In this guide, you will deploy your contract to a local blockchain. This will be an "ephemeral" blockchain, meaning it is completely destroyed every time you shut it down and recreated from scratch every time you start it up - it won't save its state when you shut it down. The benefit of this is quick and easy development, and you don't need to find test ETH to pay gas fees. Later in the guide, you will learn how to deploy to a live public test network too.
Your developer environment
Everything in this tutorial can be done by sending JSON data directly to an Ethereum node. However, this is often awkward and error-prone, so a rich ecosystem of tooling has been developed to allow developers to interact with Ethereum in familiar languages or using abstractions that simplify the process.
In this guide, you will use Foundry which is a very lightweight set of command-line tools for managing smart contract development. If you already have Foundry installed, head straight to the next section. If you need to install Foundry, head to getfoundry.sh and follow the installation steps.
Note: If you are a seasoned smart contract developer, feel free to follow the tutorial using your own toolchain.
Deploying to a local network
Foundry has its own local network called Anvil. You can use it to create a local blockchain on your computer. Open a terminal and run the following very simple command:
anvil
You will see some ASCII art and configuration details in the terminal. Anvil creates a set of accounts that you can use on this network. The account addresses and private keys are displayed in the console (never use these accounts to interact with any live network). You will also see a line reading listening on 127.0.0.1:8545
. This indicates that your local node is listening for HTTP traffic on your local network on port 8545 - this is important because this is how you will send the necessary information to your node so that it can be added to the blockchain, and how you will interact with the contract after it is deployed.
Note: Anvil needs to keep running throughout this tutorial - if you close the terminal your blockchain will cease to exist. Once Anvil has started, open a new terminal tab/window to run the rest of the commands in this guide.
Making the deployment transaction
In the previous guide you wrote the following contract, and compiled it using ./fe build guest_book.fe --overwrite
to obtain the contract bytecode. This compilation stage converts the human-readable Fe code into a format that can be efficiently executed by Ethereum's embedded computer, known as the Ethereum Virtual Machine (EVM). The bytecode is stored at an address on the blockchain. The contract functions are invoked by sending instructions in a transaction to that address.
contract GuestBook {
messages: Map<address, String<100>>
pub fn sign(mut self, ctx: Context, book_msg: String<100>) {
self.messages[ctx.msg_sender()] = book_msg
}
pub fn get_msg(self, addr: address) -> String<100> {
return self.messages[addr].to_mem()
}
}
To make the deployment, we will need to send a transaction to your node via its exposed HTTP port (8545
).
The following command deploys the Guestbook contract to your local network. Grab the private key of one of your accounts from the information displayed in the terminal running Anvil.
cast send --rpc-url localhost:8545 --private-key <your-private-key> --create $(cat output/GuestBook/GuestBook.bin)
Here's what the response was at the time of writing this tutorial.
blockHash 0xcee9ff7c0b57822c5f6dd4fbd3a7e9eadb594b84d770f56f393f137785a52702
blockNumber 1
contractAddress 0x5FbDB2315678afecb367f032d93F642f64180aa3
cumulativeGasUsed 196992
effectiveGasPrice 4000000000
gasUsed 196992
logs []
logsBloom 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
root
status 1
transactionHash 0x3fbde2a994bf2dec8c11fb0390e9d7fbc0fa1150f5eab8f33c130b4561052622
transactionIndex 0
type 2
This response tells you that your contract has been deployed to the blockchain. The transaction was included in block number 1, and the address it was deployed to is provided in the contractAddress
field - you need this address to interact with the contract.
Note: Don't assume responses to be identical when following the tutorial. Due to the nature of the blockchain environment the content of the responses will always differ slightly.
Signing the guest book
Now that the contract is deployed to the blockchain, you can send a transaction to sign it with a custom message. You will sign it from the same address that was used to deploy the contract, but there is nothing preventing you from using any account for which you have the private key (you could experiment by signing from all ten accounts created by Anvil, for example).
The following command will send a transaction to call sign(string)
on our freshly deployed Guestbook contract sitting at address 0x810cbd4365396165874c054d01b1ede4cc249265
with the message "We <3 Fe".
cast send --rpc-url http://localhost:8545 --private-key <your-private-key> <contract-address> "sign(string)" '"We <3 Fe"'
The response will look approximately as follows:
blockHash 0xb286898484ae737d22838e27b29899b327804ec45309e47a75b18cfd7d595cc7
blockNumber 2
contractAddress
cumulativeGasUsed 36278
effectiveGasPrice 3767596722
gasUsed 36278
logs []
logsBloom 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
root
status 1
transactionHash 0x309bcea0a77801c15bb7534beab9e33dcb613c93cbea1f12d7f92e4be5ecab8c
transactionIndex 0
type 2
Reading the signatures
The get_msg(address)
API allows you to read the messages added to the guestbook for a given address. It will give us a response of 100 zero bytes for any address that hasn't yet signed the guestbook.
Since reading the messages doesn't change any state within the blockchain, you don't have to send an actual transaction. Instead, you can perform a call against the local state of the node that you are querying.
To do that run:
$ cast call --rpc-url http://localhost:8545 <contract-address> "get_msg(address)" <your-account-address-that-signed-the-guestbook>
Notice that the command doesn't need to provide a private key simply because we are not sending an actual transaction.
The response arrives in the form of hex-encoded bytes padded with zeroes:
0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000087765203c33204665000000000000000000000000000000000000000000000000
Foundry provides a built-in method to convert this hex string into human-readable ASCII. You can do this as follows:
cast to_ascii "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000087765203c33204665000000000000000000000000000000000000000000000000"
or simply pipe the output of the cast call
to to_ascii
to do the query and conversion in a single command:
cast call --rpc-url https://rpc.sepolia.org <contract-address> "get_msg(address)" <your-account-address-that-signed-the-guestbook> | cast --to-ascii
Either way, the response will be the message you passed to the sign(string)
function.
"We <3 Fe"
Congratulations! You've deployed real Fe code to a local blockchain! 🤖
Deploying to a public test network
Now you have learned how to deploy your contract to a local blockchain, you can consider deploying it to a public test network too. For more complex projects this can be very beneficial because it allows many users to interact with your contract, simulates real network conditions and allows you to interact with other existing contracts on the network. However, to use a public testnet you need to obtain some of that testnet's gas token.
In this guide you will use the Sepolia test network, meaning you will need some SepoliaETH. SepoliaETH has no real-world value - it is only required to pay gas fees on the network. If you don't have any SepoliaETH yet, you can request some SepoliaETH from one of the faucets that are listed on the ethereum.org website.
IMPORTANT: It is good practice to never use an Ethereum account for a testnet that is also used for the actual Ethereum mainnet.
Assuming you have some SepoliaETH, you can repeat the steps from the local blockchain example, however, instead of pointing Foundry to the RPC endpoint for your Anvil node, you need to point it to a node connected to the Sepolia network. There are several options for this:
- If you run your own node, connect it to the Sepolia network and let it sync. make sure you expose an http port or enable IPC transport.
- You can use an RPC provider such as Alchemy or Infura
- You can use an open public node such as
https://rpc.sepolia.org
.
Whichever method you choose, you will have an RPC endpoint for a node connected to Sepolia. You can replace the http://localhost:8545
in the commands with your new endpoint. For example, to deploy the contract using the open public endpoint:
cast send --rpc-url https://rpc.sepolia.org --private-key <your-private-key> --create $(cat output/GuestBook/GuestBook.bin)
Now you have deployed the contract to a public network and anyone can interact with it.
To demonstrate, you can check out previous versions of this contract deployed on Sepolia in the past:
address | link | |
---|---|---|
deploy tx hash | 0x31b41a4177d7eb66f5ea814959b2c147366b6323f17b6f7060ecff424b58df76 | etherscan |
contract address | 0x810cbd4365396165874C054d01B1Ede4cc249265 | etherscan |
Note that calling the sign(string)
function will cost you some SepoliaETH because the function changes the state of the blockchain (it adds a message to the contract storage). However, get_msg(address)
does not cost any gas because it is a simple lookup in the node's local database.
Congratulations! You've deployed real Fe code to a live network 🤖
Summary
Well done!
You have now written and compiled a Fe contract and deployed it to both a local blockchain and a live public test network! You also learned how to interact with the deployed contract using transactions and calls.
Here's some ideas for what you could do next:
- Experiment with different developer tooling
- Get more comfortable with Foundry by exploring the documentation
- Repeat the steps in this guide but for a more complex contract - be creative!
- Continue to the Using Fe pages to explore Fe more deeply.
User guide
Welcome to the Fe user guide!
Here you can find information about how to use Fe to develop smart contracts.
Read more about:
- Installing Fe
- organizing your code using projects
We are still building this section of the site, but you can expect to find other materials such as reference documentation, project examples and walkthrough guides here soon!
Installation
At this point Fe is available for Linux and MacOS natively but can also be installed on Windows via WSL.
Note: If you happen to be a Windows developer, consider getting involved and help us to support the Windows platform natively. Here would be a good place to start.
On a computer with MacOS and an ARM chip, you need to install Rosetta, Apple's x86-to-ARM translator, to be able to run the executable.
/usr/sbin/softwareupdate --install-rosetta --agree-to-license
Package managers
Fe can be installed from Homebrew. Homebrew is available for Mac, Linux and Windows (via WSL). The following command installs Fe and exposes it as fe
without any further configuration necessary:
brew install fe-lang/tap/fe
Download the compiler
Fe is distributed via a single executable file linked from the home page. In the future we will make sure it can be installed through a variety of popular package managers such as apt
.
Depending on your operating system, the file that you download is either named fe_amd64
or fe_mac
.
Note: We will rename the file to
fe
and assume that name for the rest of the guide. In the future when Fe can be installed via other mechanisms we can assumefe
to become the canonical command name.
Add permission to execute
In order to be able to execute the Fe compiler we will have to make the file executable. This can be done by navigating to the directory where the file is located and executing chmod + x <filename>
(e.g. chmod +x fe
).
After we have set the proper permissions we should be able to run ./fe
and an output that should be roughly comparable to:
fe 0.21.0-alpha
The Fe Developers <snakecharmers@ethereum.org>
An implementation of the Fe smart contract language
USAGE:
fe_amd64_latest <SUBCOMMAND>
OPTIONS:
-h, --help Print help information
-V, --version Print version information
SUBCOMMANDS:
build Build the current project
check Analyze the current project and report errors, but don't build artifacts
help Print this message or the help of the given subcommand(s)
new Create new fe project
Building from source
You can also build Fe from the source code provided in our Github repository. To do this you will need to have Rust installed. Then, clone the github repository using:
git clone https://github.com/ethereum/fe.git
Depending on your environment you may need to install some additional packages before building the Fe binary, specifically libboost-all-dev
, libclang
and cmake
. For example, on a Linux system:
sudo apt-get update &&\
apt-get install libboost-all-dev &&\
apt-get install libclang-dev &&\
apt-get install cmake
Navigate to the folder containing the Fe source code.
cd fe
Now, use Rust to build the Fe binary. To run Fe, you need to build using solc-backend
.
cargo build -r --feature solc-backend
You will now find your Fe binary in /target/release
. Check the build with:
./target/release/fe --version
If everything worked, you should see the Fe version printed to the terminal:
fe 0.24.0
You can run the built-in tests using:
cargo test --workspace --features solc-backend
Editor support & Syntax highlighting
Fe is a new language and editor support is still in its early days. However, basic syntax highlighting is available for Visual Studio Code via this VS Code extension.
In Visual Studio Code open the extension sidebar (Ctrl-Shift-P / Cmd-Shift-P, then "Install Extension") and search for fe-lang.code-ve
. Click on the extension and then click on the Install
button.
We are currently working on a Language Server Protocol (LSP), which in the future will enable more advanced editor features such as code completion, go-to definition and refactoring.
Fe projects
A project is a collection of files containing Fe code and configuration data. Often, smart contract development can become too complex to contain all the necessary code inside a single file. In these cases, it is useful to organize your work into multiple files and directories. This allows you to group thematically linked code and selectively import the code you need when you need it.
Creating a project
You can start a project using the new
subcommand:
$ fe new <my_project>
This will generate a template project containing the following:
- A
src
directory containing two .fe files. - A
fe.toml
manifest with basic project info and some local project imports.
Manifest
The fe.toml
file is known as a manifest. The manifest is written in TOML format. The purpose of this file is to provide all the metadata that is required for the project to compile. The file begins with definitions for the project name and version, then the project dependencies are listed under a heading [dependencies]
. Dependencies are files in the local filesystem that are required for your project to run.
For example:
name="my-project"
version = "1.0"
[dependencies]
dependency_1 = "../lib"
You can also specify which version of a particular dependency you want to use, using curly braces:
name="my-project"
version = "1.0"
[dependencies]
dependency_1 = {path = "../lib", version = "1.0"}
Project modes
There are two project modes: main
and lib
.
Main projects can import libraries and have code output.
Libraries on the other hand cannot import main projects and do not have code outputs. Their purpose is to be imported into other projects.
The mode of a project is determined automatically by the presence of either src/main.fe
or src/lib.fe
.
Importing
You can import code from external files with the following syntax:
#![allow(unused)] fn main() { use utils::get_42 }
This will import the get_42
function from the file utils.fe
.
You can also import using a custom name/alias:
#![allow(unused)] fn main() { use utils::get_42 as get_42 }
Tests
The templates created using fe new
include a simple test demonstrating the test syntax.
To write a unit test, create a function with a name beginning with test_
. The function should instantiate your contract and call the contract function you want to test. You can use assert
to check that the returned value matches an expected value.
For example, to test the say_hello
function on Contract
which is expected to return the string "hello"
:
#![allow(unused)] fn main() { fn test_contract(mut ctx: Context) { let contract: Contract = Contract.create(ctx, 0) assert main.say_hello() == "hello" } }
You can run all the tests in a project by running the following command:
fe test <project-root>
You will receive test results directly to the console.
Running your project
Once you have created a project, you can run the usual Fe CLI subcommands against the project path.
Tutorials
Welcome to the Tutorials section. We will be adding walkthrough guides for example Fe projects here!
For now, you can get started with:
Watch this space for more tutorials coming soon!
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 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
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 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 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. You can refer back to this at 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:
#![allow(unused)] fn main() { 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:
auction_end_time: u256
beneficiary: address
highest_bidder: address
highest_bid: u256
pending_returns: Map<address, u256>
ended: bool
Notice that variables are named using snake case (lower case, underscore separated, like_this
).
Addresses 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 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<address, u256>
. 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.
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:
contract Auction {
// states
auction_end_time: u256
beneficiary: address
highest_bidder: address
highest_bid: u256
pending_returns: Map<address, u256>
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 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: mut 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 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 Fe
In Fe contracts ctx
is where you can find transaction data such as 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:
#![allow(unused)] fn main() { 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
keyword. The revert
can accept a struct that becomes encoded as revert data. Here you can just revert without any arguments. Add the following definition somewhere in Auction.fe
outside the main contract definition:
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
:
struct BidNotHighEnough {
pub highest_bid: u256
}
Notice that the value being checked is
msg.value
which is included in thectx
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 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:
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 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 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:
#![allow(unused)] fn main() { 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 } }
Note that in this case
mut
is used withctx
becausesend_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.
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:
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:
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:
#![allow(unused)] fn main() { 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 wrote an open auction contract in Fe!
View functions
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:
#![allow(unused)] fn main() { pub fn check_highest_bidder(self) -> address { return self.highest_bidder; } pub fn check_highest_bid(self) -> u256 { return self.highest_bid; } pub fn check_ended(self) -> bool { return self.ended; } }
Build and deploy the contract
Your contract is now ready to use! Compile it using
fe build auction.fe
You will find the contract ABI and bytecode in the newly created outputs
directory.
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 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:
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 then deploy the contract with the constructor arguments.
cast send --from <your-address> --private-key <your-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.
Now you can interact with your contract. Start by sending an initial bid, let's say 100 ETH. For contract address 0x700b6A60ce7EaaEA56F065753d8dcB9653dbAD35
:
cast send 0x700b6A60ce7EaaEA56F065753d8dcB9653dbAD35 "bid()" --value "100ether" --private-key <your-private-key> --from 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720
You can check whether this was successful by calling the check_highest_bidder()
function:
cast call 0x700b6A60ce7EaaEA56F065753d8dcB9653dbAD35 "check_highest_bidder()"
You will see a response looking similar to:
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:
cast call 0x700b6A60ce7EaaEA56F065753d8dcB9653dbAD35 "check_highest_bid()"
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):
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 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:
cast balance <address>
And check whether the auction open time has expired using
cast <contract-address> "check_ended()"
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 simulate a bidding war!
By following this tutorial, you learned:
- basic Fe types, such as
bool
,address
,map
andu256
- 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 using
ctx.emit
- how to deploy a contract with constructor arguments using Foundry
- how to interact with your contract
Example Contracts
// 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 Auction {
// states
auction_end_time: u256
beneficiary: address
highest_bidder: address
highest_bid: u256
pending_returns: Map<address, u256>
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 auction_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)
}
pub fn check_highest_bidder(self) -> address {
return self.highest_bidder;
}
pub fn check_highest_bid(self) -> u256 {
return self.highest_bid;
}
pub fn check_ended(self) -> bool {
return self.ended;
}
}
Useful external links
There are not many resources for Fe outside of the official documentation at this time. This section lists useful links to external resources.
Tools
Projects
- Bountiful - Bug bounty platform written in Fe, live on Mainnet
- Simple DAO - A Simple DAO written in Fe - live on Mainnet and Optimism
Hackathon projects
These are community projects written in Fe at various hackathons.
-
Fixed-Point Numerical Library - A fixed-point number representation and mathematical operations tailored for Fe. It can be used in financial computations, scientific simulations, and data analysis.
-
p256verifier - Secp256r1 (a.k.a p256) curve signature verifier which allows for verification of a P256 signature in fe.
-
Account Storage with Efficient Sparse Merkle Trees - Efficient Sparse Merkle Trees in Fe! SMTs enable inclusion and exclusion proofs for the entire set of Ethereum addresses.
-
Tic Tac Toe - An implementation of the classic tic tac toe game in Fe with a Python frontend.
-
Fecret Santa - Fecret Santa is an onchain Secret Santa event based on a "chain": gift a collectible (ERC721 or ERC1155) to the last Santa and you'll be the next to receive a gift!
-
Go do it - A commitment device to help you achieve your goals.
-
Powerbald - On chain lottery written in Fe
-
sspc-flutter-fe - Stupid Simple Payment Channel written in Fe
Others
-
Fe standard library - The Fe standard library comes bundled with the compiler but it is also a useful example for real world Fe code.
Blog posts
- Fe or Solidity, which is better? by Ahmed Castro
Videos
-
Fe or Solidity, which is better? by Ahmed Castro
-
Fe o Solidity, ¿cuál es es mejor? by Ahmed Castro
Development
Read how to become a Fe developer.
Build and test
Please make sure Rust is installed.
Basic
The following commands only build the Fe -> Yul compiler components.
- build the CLI:
cargo build
- test:
cargo test --workspace
Full
The Fe compiler depends on the Solidity compiler for transforming Yul IR to EVM bytecode. We currently use solc-rust to perform this. In order to compile solc-rust, the following must be installed on your system:
- cmake
- boost(1.65+)
- libclang
brew install boost
Once these have been installed, you may run the full build. This is enabled using the solc-backend feature.
- build the CLI:
cargo build --features solc-backend
- test:
cargo test --workspace --features solc-backend
Release
Versioning
Make sure that version follows semver rules e.g (0.23.0
).
Generate Release Notes
Prerequisite: Release notes are generated with towncrier. Ensure to have towncrier
installed and the command is available.
Run make notes version=<version>
where <version>
is the version we are generating the release notes for e.g. 0.23.0
.
Example:
make notes version=0.23.0
Examine the generated release notes and if needed perform and commit any manual changes.
Generate the release
Run make release version=<version>
.
Example:
make release version=0.23.0
This will also run the tests again as the last step because some of them may need to be adjusted because of the changed version number.
Tag and push the release
Prerequisite: Make sure the central repository is configured as upstream
, not origin
.
After the tests are adjusted run make push-tag
to create the tag and push it to Github.
Manually edit the release on GitHub
Running the previous command will push a new tag to Github and cause the CI to create a release with the Fe binaries attached. We may want to edit the release afterwards to put in some verbiage about the release.
Updating Docs & Website
A release of a new Fe compiler should usually go hand in hand with updating the website and documentation. For one, the front page of fe-lang.org links to the download of the compiler but won't automatically pick up the latest release without a fresh deployment. Furthermore, if code examples and other docs needed to be updated along with compiler changes, these updates are also only reflected online when the site gets redeployed. This is especially problematic since our docs do currently not have a version switcher to view documentation for different compiler versions (See GitHub issue #543).
Preview the sites locally
Run make serve-website
and visit http://0.0.0.0:8000 to preview it locally. Ensure the front page displays the correct compiler version for download and that the docs render correctly.
Deploy website & docs
Prerequisite: Make sure the central repository is configured as upstream
, not origin
.
Run make deploy-website
and validate that fe-lang.org renders the updated sites (Can take up to a few minutes).
Fe Standard Library
The standard library includes commonly used algorithms and data structures that come bundled as part of the language.
Precompiles
Precompiles are EVM functions that are prebuilt and optimized as part of the Fe standard library. There are currently nine precompiles available in Fe. The first four precompiles were defined in the original Ethereum whitepaper (ec_recover
, SHA2_256
, ripemd_160
, identity)
. Four more were added during the Byzantium fork (mod_exp
, ec_add
, ec_mul
and ec_pairing
). A final precompile, blake2f
was added in EIP-152 during the Istanbul fork.
The nine precompiles available in the Fe standard library are:
These precompiles are imported as follows:
use std::precompiles
ec_recover
ec_recover
is a cryptographic function that retrieves a signer's address from a signed message. It is the fundamental operation used for verifying signatures in Ethereum. Ethereum uses the Elliptic Curve Digital Signature Algorithm (ECDSA) for verifying signatures. This algorithm uses two parameters, r
and s
. Ethereum's implementation also uses an additional 'recovery identifier' parameter, v
, which is used to identify the correct elliptic curve point from those that can be calculated from r
and s
alone.
Parameters
hash
: the hash of the signed message,u256
v
: the recovery identifier, a number in the range 27-30,u256
r
: elliptic curve parameter,u256
s
: elliptic curve parameter,u256
Returns
ec_recover
returns an address.
Function signature
pub fn ec_recover(hash: u256, v: u256, r: u256, s: u256) -> address
Example
let result: address = precompiles::ec_recover(
hash: 0x456e9aea5e197a1f1af7a3e85a3212fa4049a3ba34c2289b4c860fc0b0c64ef3,
v: 28,
r: 0x9242685bf161793cc25603c231bc2f568eb630ea16aa137d2664ac8038825608,
s: 0x4f8ae3bd7535248d0bd448298cc2e2071e56992d0774dc340c368ae950852ada
)
SHA2_256
SHA2_256
is a hash function. a hash function generates a unique string of characters of fixed length from arbitrary input data.
Parameters
buf
: a sequence of bytes to hash,MemoryBuffer
Returns
SHA2_256
returns a hash as a u256
Function signature
pub fn sha2_256(buf input_buf: MemoryBuffer) -> u256
Example
let buf: MemoryBuffer = MemoryBuffer::from_u8(value: 0xff)
let result: u256 = precompiles::sha2_256(buf)
ripemd_160
ripemd_160
is a hash function that is rarely used in Ethereum, but is included in many crypto libraries as it is used in Bitcoin core.
Parameters
input_buf
: a sequence of bytes to hash,MemoryBuffer
Returns
ripemd_160
returns a hash as a u256
Function signature
pub fn ripemd_160(buf input_buf: MemoryBuffer) -> u256
Example
let buf: MemoryBuffer = MemoryBuffer::from_u8(value: 0xff)
let result: u256 = precompiles::ripemd_160(buf)
identity
identity
is a function that simply echoes the input of the function as its output. This can be used for efficient data copying.
Parameters
input_buf
: a sequence of bytes to hash,MemoryBuffer
Returns
identity
returns a sequence of bytes, MemoryBuffer
Function signature
pub fn identity(buf input_buf: MemoryBuffer) -> MemoryBuffer
Example
let buf: MemoryBuffer = MemoryBuffer::from_u8(value: 0x42)
let mut result: MemoryBufferReader = precompiles::identity(buf).reader()
mod_exp
mod_exp
is a modular exponentiation function required for elliptic curve operations.
Parameters
- b:
MemoryBuffer
: the base (i.e. the number being raised to a power),MemoryBuffer
- e:
MemoryBuffer
: the exponent (i.e. the powerb
is raised to),MemoryBuffer
- m:
MemoryBuffer
: the modulus,MemoryBuffer
- b_size:
u256
: the length ofb
in bytes,u256
- e_size:
u256
: the length ofe
in bytes,u256
- m_size:
u256
: then length ofm
in bytes,u256
Returns
mod_exp
returns a sequence of bytes, MemoryBuffer
Function signature
pub fn mod_exp(
b_size: u256,
e_size: u256,
m_size: u256,
b: MemoryBuffer,
e: MemoryBuffer,
m: MemoryBuffer,
) -> MemoryBuffer
Example
let mut result: MemoryBufferReader = precompiles::mod_exp(
b_size: 1,
e_size: 1,
m_size: 1,
b: MemoryBuffer::from_u8(value: 8),
e: MemoryBuffer::from_u8(value: 9),
m: MemoryBuffer::from_u8(value: 10),
).reader()
ec_add
ec_add
does point addition on elliptic curves.
Parameters
x1
: x-coordinate 1,u256
y1
: y coordinate 1,u256
x2
: x coordinate 2,u256
y2
: y coordinate 2,u256
Function signature
pub fn ec_add(x1: u256, y1: u256, x2: u256, y2: u256)-> (u256,u256)
Returns
ec_add
returns a a tuple of u256
, (u256, u256)
.
Example
let (x, y): (u256, u256) = precompiles::ec_add(x1: 1, y1: 2, x2: 1, y2: 2)
ec_mul
ec_mul
is for multiplying elliptic curve points.
Parameters
x
: x-coordinate,u256
y
: y coordinate,u256
s
: multiplier,u256
Function signature
pub fn ec_mul(x: u256, y: u256, s: u256)-> (u256,u256)
Returns
ec_mul
returns a a tuple of u256
, (u256, u256)
.
Example
let (x, y): (u256, u256) = precompiles::ec_mul(
x: 1,
y: 2,
s: 2
)
ec_pairing
ec_pairing
does elliptic curve pairing - a form of encrypted multiplication.
Parameters
input_buf
: sequence of bytes representing the result of the elliptic curve operation(G1 * G2) ^ k
, asMemoryBuffer
Returns
ec_pairing
returns a bool
indicating whether the pairing is satisfied (true
) or not (false
).
Example
let mut input_buf: MemoryBuffer = MemoryBuffer::new(len: 384)
let mut writer: MemoryBufferWriter = buf.writer()
writer.write(value: 0x2cf44499d5d27bb186308b7af7af02ac5bc9eeb6a3d147c186b21fb1b76e18da)
writer.write(value: 0x2c0f001f52110ccfe69108924926e45f0b0c868df0e7bde1fe16d3242dc715f6)
writer.write(value: 0x1fb19bb476f6b9e44e2a32234da8212f61cd63919354bc06aef31e3cfaff3ebc)
writer.write(value: 0x22606845ff186793914e03e21df544c34ffe2f2f3504de8a79d9159eca2d98d9)
writer.write(value: 0x2bd368e28381e8eccb5fa81fc26cf3f048eea9abfdd85d7ed3ab3698d63e4f90)
writer.write(value: 0x2fe02e47887507adf0ff1743cbac6ba291e66f59be6bd763950bb16041a0a85e)
writer.write(value: 0x0000000000000000000000000000000000000000000000000000000000000001)
writer.write(value: 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd45)
writer.write(value: 0x1971ff0471b09fa93caaf13cbf443c1aede09cc4328f5a62aad45f40ec133eb4)
writer.write(value: 0x091058a3141822985733cbdddfed0fd8d6c104e9e9eff40bf5abfef9ab163bc7)
writer.write(value: 0x2a23af9a5ce2ba2796c1f4e453a370eb0af8c212d9dc9acd8fc02c2e907baea2)
writer.write(value: 0x23a8eb0b0996252cb548a4487da97b02422ebc0e834613f954de6c7e0afdc1fc)
assert precompiles::ec_pairing(buf)
}
blake_2f
blake_2f
is a compression algorithm for the cryptographic hash function BLAKE2b
. It takes as an argument the state vector h
, message block vector m
, offset counter t
, final block indicator flag f
, and number of rounds rounds
. The state vector provided as the first parameter is modified by the function.
Parameters
h
: the state vector,Array<u64, 8>
m
: message block vector,Array<u64, 16>
t
: offset counter,Array<u64, 2>
f
: boolrounds
: number of rounds of mixing,u32
Returns
blake_2f
returns a modified state vector, Array<u64, 8>
Function signature
pub fn blake_2f(rounds: u32, h: Array<u64, 8>, m: Array<u64, 16>, t: Array<u64, 2>, f: bool) -> Array<u64, 8>
Example
let result: Array<u64, 8> = precompiles::blake_2f(
rounds: 12,
h: [
0x48c9bdf267e6096a,
0x3ba7ca8485ae67bb,
0x2bf894fe72f36e3c,
0xf1361d5f3af54fa5,
0xd182e6ad7f520e51,
0x1f6c3e2b8c68059b,
0x6bbd41fbabd9831f,
0x79217e1319cde05b,
],
m: [
0x6162630000000000,
0x0000000000000000,
0x0000000000000000,
0x0000000000000000,
0x0000000000000000,
0x0000000000000000,
0x0000000000000000,
0x0000000000000000,
0x0000000000000000,
0x0000000000000000,
0x0000000000000000,
0x0000000000000000,
0x0000000000000000,
0x0000000000000000,
0x0000000000000000,
0x0000000000000000,
],
t: [
0x0300000000000000,
0x0000000000000000,
],
f: true
)
Fe Language Specification
Notation
Grammar
The following notations are used by the Lexer and Syntax grammar snippets:
Notation | Examples | Meaning |
---|---|---|
CAPITAL | KW_IF | A token produced by the lexer |
ItalicCamelCase | Item | A syntactical production |
string | x , while , * | The exact character(s) |
\x | \n, \r, \t, \0 | The character represented by this escape |
x? | pub ? | An optional item |
x* | OuterAttribute* | 0 or more of x |
x+ | MacroMatch+ | 1 or more of x |
xa..b | HEX_DIGIT1..6 | a to b repetitions of x |
| | u8 | u16 , Block | Item | Either one or another |
[ ] | [b B ] | Any of the characters listed |
[ - ] | [a -z ] | Any of the characters in the range |
~[ ] | ~[b B ] | Any characters, except those listed |
~string | ~\n , ~*/ | Any characters, except this sequence |
( ) | (, Parameter) | Groups items |
Lexical Structure
Keywords
Fe divides keywords into two categories:
Strict keywords
These keywords can only be used in their correct contexts. They cannot be used as the identifiers.
Lexer:
KW_AS :as
KW_BREAK :break
KW_CONST :const
KW_CONTINUE :continue
KW_CONST :contract
KW_FN :fn
KW_ELSE :else
KW_ENUM :enum
KW_EVENT :event
KW_FALSE :false
KW_FOR :for
KW_IDX :idx
KW_IF :if
KW_IN :in
KW_LET :let
KW_MATCH :match
KW_MUT :mut
KW_NONPAYABLE :nonpayable
KW_PAYABLE :payable
KW_PUB :pub
KW_RETURN :return
KW_REVERT :revert
KW_SELFVALUE :self
KW_STRUCT :struct
KW_TRUE :true
KW_USE :use
KW_WHILE :while
KW_ADDRESS :address
Reserved keywords
These keywords aren't used yet, but they are reserved for future use. They have the same restrictions as strict keywords. The reasoning behind this is to make current programs forward compatible with future versions of Fe by forbidding them to use these keywords.
Lexer:
KW_ABSTRACT :abstract
KW_ASYNC :async
KW_AWAIT :await
KW_DO :do
KW_EXTERNAL :external
KW_FINAL :final
KW_IMPL :impl
KW_MACRO :macro
KW_OVERRIDE :override
KW_PURE :pure
KW_SELFTYPE :Self
KW_STATIC :static
KW_SUPER :super
KW_TRAIT :trait
KW_TYPE :type
KW_TYPEOF :typeof
KW_VIEW :view
KW_VIRTUAL :virtual
KW_WHERE :where
KW_YIELD :yield
Identifiers
Lexer:
IDENTIFIER_OR_KEYWORD :
[a
-z
A
-Z
] [a
-z
A
-Z
0
-9
_
]*
|_
[a
-z
A
-Z
0
-9
_
]+ Except a strict or reserved keyword
An identifier is any nonempty ASCII string of the following form:
Either
- The first character is a letter.
- The remaining characters are alphanumeric or
_
.
Or
- The first character is
_
. - The identifier is more than one character.
_
alone is not an identifier. - The remaining characters are alphanumeric or
_
.
Tokens
NEWLINE
A token that represents a new line.
Literals
A literal is an expression consisting of a single token, rather than a sequence of tokens, that immediately and directly denotes the value it evaluates to, rather than referring to it by name or some other evaluation rule. A literal is a form of constant expression, so is evaluated (primarily) at compile time.
Examples
Strings
Example | Characters | Escapes | |
---|---|---|---|
String | "hello" | ASCII subset | Quote & ASCII |
ASCII escapes
Name | |
---|---|
\n | Newline |
\r | Carriage return |
\t | Tab |
\\ | Backslash |
Quote escapes
Name | |
---|---|
\" | Double quote |
Numbers
Number literals* | Example |
---|---|
Decimal integer | 98_222 |
Hex integer | 0xff |
Octal integer | 0o77 |
Binary integer | 0b1111_0000 |
*
All number literals allow _
as a visual separator: 1_234
Boolean literals
Lexer
BOOLEAN_LITERAL :
true
|false
String literals
Lexer
STRING_LITERAL :
"
(
PRINTABLE_ASCII_CHAR
| QUOTE_ESCAPE
| ASCII_ESCAPE
)*"
PRINTABLE_ASCII_CHAR :
Any ASCII character between0x1F
and0x7E
QUOTE_ESCAPE :
\"
ASCII_ESCAPE :
|\n
|\r
|\t
|\\
A string literal is a sequence of any characters that are in the set of printable ASCII characters as well as a set of defined escape sequences.
Line breaks are allowed in string literals.
Integer literals
Lexer
INTEGER_LITERAL :
( DEC_LITERAL | BIN_LITERAL | OCT_LITERAL | HEX_LITERAL )DEC_LITERAL :
DEC_DIGIT (DEC_DIGIT|_
)*BIN_LITERAL :
0b
(BIN_DIGIT|_
)* BIN_DIGIT (BIN_DIGIT|_
)*OCT_LITERAL :
0o
(OCT_DIGIT|_
)* OCT_DIGIT (OCT_DIGIT|_
)*HEX_LITERAL :
0x
(HEX_DIGIT|_
)* HEX_DIGIT (HEX_DIGIT|_
)*BIN_DIGIT : [
0
-1
]OCT_DIGIT : [
0
-7
]DEC_DIGIT : [
0
-9
]HEX_DIGIT : [
0
-9
a
-f
A
-F
]
An integer literal has one of four forms:
- A decimal literal starts with a decimal digit and continues with any mixture of decimal digits and underscores.
- A hex literal starts with the character sequence
U+0030
U+0078
(0x
) and continues as any mixture (with at least one digit) of hex digits and underscores. - An octal literal starts with the character sequence
U+0030
U+006F
(0o
) and continues as any mixture (with at least one digit) of octal digits and underscores. - A binary literal starts with the character sequence
U+0030
U+0062
(0b
) and continues as any mixture (with at least one digit) of binary digits and underscores.
Examples of integer literals of various forms:
123 // type u256
0xff // type u256
0o70 // type u256
0b1111_1111_1001_0000 // type u256
0b1111_1111_1001_0000i64 // type u256
Comments
Lexer
LINE_COMMENT :
//
*
Items
Visibility and Privacy
These two terms are often used interchangeably, and what they are attempting to convey is the answer to the question "Can this item be used at this location?"
Fe knows two different types of visibility for functions and state variables: public
and private
. Visibility of private
is the default and is used if no other visibility is specified.
Public: External functions are part of the contract interface, which means they can be called from other contracts and via transactions.
Private: Those functions and state variables can only be accessed internally from within the same contract. This is the default visibility.
For example, this is a function that can be called externally from a transaction:
pub fn answer_to_life_the_universe_and_everything() -> u256 {
return 42
}
Top-level definitions in a Fe source file can also be specified as pub
if the file exists within the context of an Ingot. Declaring a definition as pub
enables other modules within an Ingot to use
the definition.
For example, given an Ingot with the following structure:
example_ingot
└── src
├── ding
│ └── dong.fe
└── main.fe
With ding/dong.fe
having the following contents:
pub struct Dang {
pub my_address: address
pub my_u256: u256
pub my_i8: i8
}
Then main.fe
can use the Dang
struct since it is pub
-qualified:
use ding::dong::Dang
contract Foo {
pub fn hot_dang() -> Dang {
return Dang(
my_address: 8,
my_u256: 42,
my_i8: -1
)
}
}
Structs
Syntax
Struct :
struct
IDENTIFIER{
StructField*
StructMethod*
}
StructField :
pub
? IDENTIFIER:
TypeStructMethod :
Function
A struct is a nominal struct type defined with the keyword struct
.
An example of a struct
item and its use:
struct Point {
pub x: u256
pub y: u256
}
fn pointy_stuff() {
let mut p: Point = Point(x: 10, y: 11)
let px: u256 = p.x
p.x = 100
}
Builtin functions:
abi_encode()
encodes the struct as an ABI tuple and returns the encoded data as a fixed-size byte array that is equal in size to the encoding.
Traits
Syntax
Trait :
trait
IDENTIFIER{
TraitMethod*
}
TraitMethod :
fn
IDENTIFIER
(
FunctionParameters?)
FunctionReturnType?;
A trait is a collection of function signatures that a type can implement. Traits are implemented for specific types through separate implementations. A type can implement a trait by providing a function body for each of the trait's functions. Traits can be used as type bounds for generic functions to restrict the types that can be used with the function.
All traits define an implicit type parameter Self
that refers to "the type that is implementing this interface".
Example of the Min
trait from Fe's standard library:
pub trait Min {
fn min() -> Self;
}
Example of the i8
type implementing the Min
trait:
impl Min for i8 {
fn min() -> Self {
return -128
}
}
Example of a function restricting a generic parameter to types implementing the Compute
trait:
pub trait Compute {
fn compute(self) -> u256;
}
struct Example {
fn do_something<T: Compute>(val: T) -> u256 {
return val.compute()
}
}
Enum
Syntax
Enumeration :
enum
IDENTIFIER{
EnumField*
EnumMethod*
}
EnumField :
IDENTIFIER | IDENTIFIER(
TupleElements?)
EnumMethod :
Function
An enum, also referred to as enumeration is a simultaneous definition of a nominal Enum type, that can be used to create or pattern-match values of the corresponding type.
Enumerations are declared with the keyword enum
.
An example of an enum
item and its use:
enum Animal {
Dog
Cat
Bird(BirdType)
pub fn bark(self) -> String<10> {
match self {
Animal::Dog => {
return "bow"
}
Animal::Cat => {
return "meow"
}
Animal::Bird(BirdType::Duck) => {
return "quack"
}
Animal::Bird(BirdType::Owl) => {
return "hoot"
}
}
}
}
enum BirdType {
Duck
Owl
}
fn f() {
let barker: Animal = Animal::Dog
barker.bark()
}
Type aliases
Syntax
TypeAlias :
type
IDENTIFIER=
Type
A type alias defines a new name for an existing type. Type aliases are
declared with the keyword type
.
For example, the following defines the type BookMsg
as a synonym for the type
u8[100]
, a sequence of 100
u8
numbers which is how sequences of bytes are represented in Fe.
type BookMsg = Array<u8, 100>
Contracts
Syntax
Contract :
contract
IDENTIFIER{
ContractMember*
_}
ContractMember:
Visibility?
(
ContractField
| Function
| Struct
| Enum
)Visibility :
pub
?ContractField :
IDENTIFIER:
Type
A contract is a piece of executable code stored at an address on the blockchain. See Appendix A. in the Yellow Paper for more info. Contracts can be written in high level languages, like Fe, and then compiled to EVM bytecode for deployment to the blockchain.
Once the code is deployed to the blockchain, the contract's functions can be invoked by sending a transaction to the contract address (or a call
, for functions that do not modify blockchain data).
In Fe, contracts are defined in files with .fe
extensions and compiled using fe build
.
A contract is denoted using the contract
keyword. A contract definition adds a new contract type to the module. This contract type may be used for calling existing contracts with the same interface or initializing new contracts with the create methods.
An example of a contract
:
struct Signed {
pub book_msg: String<100>
}
contract GuestBook {
messages: Map<address, String<100>>
pub fn sign(mut self, mut ctx: Context, book_msg: String<100>) {
self.messages[ctx.msg_sender()] = book_msg
ctx.emit(Signed(book_msg: book_msg))
}
pub fn get_msg(self, addr: address) -> String<100> {
return self.messages[addr].to_mem()
}
}
Multiple contracts can be compiled from a single .fe
contract file.
pragma
An optional pragma
statement can be placed at the beginning of a contract. They are used to enable developers to express that certain code is meant to be compiled with a specific compiler version such that non-matching compiler versions will reject it.
Read more on pragma
State variables
State variables are permanently stored in the contract storage on the blockchain. State variables must be declared inside the contract body but outside the scope of any individual contract function.
pub contract Example {
some_number: u256
_some_string: String<100>
}
Contract functions
Functions are executable blocks of code. Contract functions are defined inside the body of a contract, but functions defined at module scope (outside of any contract) can be called from within a contract as well.
Individual functions can be called internally or externally depending upon their visibility (either private
or public
).
Functions can modify either (or neither) the contract instance or the blockchain. This can be inferred from the function signature by the presence of combinations of mut
, self
and Context
. If a function modifies the contract instance it requires mut self
as its first argument. If a function modifies the blockchain it requires Context
as an argument.
Read more on functions.
The __init__()
function
The __init__
function is a special contract function that can only be called at contract deployment time. It is mostly used to set initial values to state variables upon deployment. In other contexts, __init__()
is commonly referred to as the constructor
function.
pub contract Example {
_some_number: u256
_some_string: String<100>
pub fn __init__(mut self, number: u256, string: String<100>) {
self._some_number=number;
self._some_string=string;
}
}
It is not possible to call __init__
at runtime.
Structs
Structs might also exist inside a contract file. These are declared outside of the contract body and are used to define a group of variables that can be used for some specific purpose inside the contract. In Fe structs are also used to represent an Event
or an Error
.
Read more on structs.
Functions
Constant size values stored on the stack or in memory can be passed into and returned by functions.
Syntax
Function :
FunctionQualifiersfn
IDENTIFIER
(
FunctionParameters?)
FunctionReturnType?
{
FunctionStatements*
}
FunctionQualifiers :
pub
?FunctionStatements :
ReturnStatement
| VariableDeclarationStatement
| AssignStatement
| AugmentedAssignStatement
| ForStatement
| WhileStatement
| IfStatement
| AssertStatement
| BreakStatement
| ContinueStatement
| RevertStatement
| ExpressionFunctionParameters :
self
? |self,
? FunctionParam (,
FunctionParam)*,
?FunctionParam :
FunctionParamLabel? IDENTIFIER:
TypesFunctionParamLabel :
_ | IDENTIFIERFunctionReturnType :
->
Types
A function definition consists of name and code block along with an optional
list of parameters and return value. Functions are declared with the
keyword fn
. Functions may declare a set of input parameters,
through which the caller passes arguments into the function, and
the output type of the value the function will return to its caller
on completion.
When referred to, a function yields a first-class value of the corresponding zero-sized function type, which when called evaluates to a direct call to the function.
A function header prepends a set or curly brackets {...}
which contain the function body.
For example, this is a simple function:
fn add(x: u256, y: u256) -> u256 {
return x + y
}
Functions can be defined inside of a contract, inside of a struct, or at the "top level" of a module (that is, not nested within another item).
Example:
fn add(_ x: u256, _ y: u256) -> u256 {
return x + y
}
contract CoolCoin {
balance: Map<address, u256>
fn transfer(mut self, from sender: address, to recipient: address, value: u256) -> bool {
if self.balance[sender] < value {
return false
}
self.balance[sender] -= value
self.balance[recipient] += value
return true
}
pub fn demo(mut self) {
let ann: address = 0xaa
let bob: address = 0xbb
self.balance[ann] = 100
let bonus: u256 = 2
let value: u256 = add(10, bonus)
let ok: bool = self.transfer(from: ann, to: bob, value)
}
}
Function parameters have optional labels. When a function is called, the arguments must be labeled and provided in the order specified in the function definition.
The label of a parameter defaults to the parameter name; a different label can be specified by adding an explicit label prior to the parameter name. For example:
fn encrypt(msg cleartext: u256, key: u256) -> u256 {
return cleartext ^ key
}
fn demo() {
let out: u256 = encrypt(msg: 0xdecafbad, key: 0xfefefefe)
}
Here, the first parameter of the encrypt
function has the label msg
,
which is used when calling the function, while the parameter name is
cleartext
, which is used inside the function body. The parameter name
is an implementation detail of the function, and can be changed without
modifying any function calls, as long as the label remains the same.
When calling a function, a label can be omitted when the argument is a variable with a name that matches the parameter label. Example:
let msg: u256 = 0xdecafbad
let cyf: u256 = encrypt(msg, key: 0x1234)
A parameter can also be specified to have no label, by using _
in place of a
label in the function definition. In this case, when calling the function, the
corresponding argument must not be labeled. Example:
fn add(_ x: u256, _ y: u256) -> u256 {
return x + y
}
fn demo() {
let sum: u256 = add(16, 32)
}
Functions defined inside of a contract or struct may take self
as a
parameter. This gives the function the ability to read and write contract
storage or struct fields, respectively. If a function takes self
as a parameter, the function must be called via self
. For example:
let ok: bool = self.transfer(from, to, value)
self
is expected to come first parameter in the function's parameter list.
Functions can also take a Context
object which gives access to EVM features that read or write
blockchain and transaction data. Context
is expected to be first in the function's parameter list
unless the function takes self
, in which case Context
should come second.
Context
Context
is used frequently in Fe smart contracts. It is used to gate access to EVM features for reading and modifyng the blockchain.
Rationale
Smart contracts execute on the Ethereum Virtual Machine (EVM). The EVM exposes features that allow smart contracts to query or change some of the blockchain data, for example emitting logs that are included in transaction receipts, creating contracts, obtaining the current block number and altering the data stored at certain addresses.
To make Fe maximally explicit and as easy as possible to audit, these functions are gated behind a Context
object. This is passed as an argument to functions, making it clear whether a function interacts with EVM features from the function signature alone.
For example, the following function looks pure from its signature (i.e. it is not expected to alter any blockchain data) but in reality it does modify the blockchain (by emitting a log).
pub fn looks_pure_but_isnt() {
// COMPILE ERROR
block_number()
}
Using Context
to control access to EVM functions such as emit
solves this problem by requiring an instance of Context
to be passed explicitly to the function, making it clear from the function signature that the function executes some blockchain interaction. The function above, rewritten using Context
, looks as follows:
pub fn uses_context(ctx: Context) -> u256 {
return ctx.block_number()
}
The Context object
The Context object gates access to features such as:
- emitting logs
- creating contracts
- transferring ether
- reading message info
- reading block info
The Context
object needs to be passed as a parameter to the function. The Context
object has a defined location in the parameter list. It is the first parameter unless the function also takes self
. Context
or self
appearing at any other position in the parameter list causes a compile time error.
The Context object is automatically injected when a function is called externally but it has to be passed explicitly when the function is called from another Fe function e.g.
// The context object is automatically injected when this is called externally
pub fn multiply_block_number(ctx: Context) -> u256 {
// but it has to be passed along in this function call
return retrieves_blocknumber(ctx) * 1000
}
fn retrieves_blocknumber(ctx: Context) -> u256 {
return ctx.block_number()
}
Context mutability
All functionality that modifies the blockchain such as creating logs or contracts or transferring ether would require a mutable Context
reference whereas read-only access such as ctx.block_number()
does not need require a mutable reference to the context. To pass a mutable Context
object, prepend the object name with mut
in the function definition, e.g.:
struct SomeEvent{
}
pub fn mutable(mut ctx: Context) {
ctx.emit(SomeEvent())
}
ABI conformity
The use of Context
enables tighter rules and extra clarity compared wth the existing function categories in the ABI, especially when paired with self
. The following table shows how combinations of self
, mut self
, Context
and mut Context
map to ABI function types.
Category | Characteristics | Fe Syntax | ABI |
---|---|---|---|
Pure | Can only operate on input arguments and not produce any information besides its return value. Can not take self and therefore has no access to things that would make it impure | foo(val: u256) | pure |
Read Contract | Reading information from the contract instance (broad definition includes reading constants from contract code) | foo(self) | view |
Storage Writing | Writing to contract storage (own or that of other contracts) | foo(mut self) | payable or nonpayable |
Context Reading | Reading contextual information from the blockchain (msg , block etc) | foo(ctx: Context) | view |
Context Modifying | Emitting logs, transferring ether, creating contracts | foo(ctx: mut Context) | payable or nonpayable |
Read Contract & Context | Reading information from the contract instance and Context | foo(self, ctx:Context) | view |
Read Contract & write Context | Reading information from the contract instance and modify Context | foo(self, ctx: mut Context) | view |
Storage Writing & read Context | Writing to contract storage and read from Context | foo(mut self, ctx: Context) | payable or nonpayable |
Storage Writing & write Context | Writing to contract storage and Context | foo(mut self, ctx: mut Context) | payable or nonpayable |
This means Fe has nine different categories of function that can be derived from the function signatures that map to four different ABI types.
Examples
msg_sender and msg_value
Context
includes information about inbound transactions. For example, the following function receives ether and adds the sender's address and the
transaction value to a mapping. No blockchain data is being changed, so Context
does not need to be mutable.
#![allow(unused)] fn main() { // assumes existence of state variable named 'ledger' with type Map<address, u256> pub fn add_to_ledger(mut self, ctx: Context) { self.ledger[ctx.msg_sender()] = ctx.msg_value(); } }
Transferring ether
Transferring ether modifies the blockchain state, so it requires access to a mutable Context
object.
pub fn send_ether(mut ctx: Context, _addr: address, amount: u256) {
ctx.send_value(to: _addr, wei: amount)
}
create/create2
Creating a contract via create
/create2
requires access to a mutable Context
object because it modifies the blockchain state data:
#![allow(unused)] fn main() { pub fn creates_contract(ctx: mut Context): ctx.create2(...) }
block number
Reading block chain information such as the current block number requires Context
(but does not require it to be mutable).
pub fn retrieves_blocknumber(ctx: Context) {
ctx.block_number()
}
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. self
gives access to constants from the contract code and state variables from contract storage.
Note: Here we focus on functions defined inside a contract, giving access to contract storage; however,
self
can also be used to read and writestruct
fields where functions are defined insidestructs
.
If a function takes self as a parameter, the function must be called via self. For example:
#![allow(unused)] fn main() { let ok: bool = self.transfer(from, to, value) }
Mutability
self
is immutable and can be used for read-only operations on the contract storage (or struct
fields). In order to write to the contract storage, you must use mut self
. This makes the contract instance mutable and allows the contract storage to be updated.
Examples
Reading contract storage
contract example {
value: u256;
pub fn check_value(self) -> u256 {
return self.value;
}
}
Writing contract storage
contract example {
value: u256;
pub fn update_value(mut self) {
self.value += 1;
}
}
Statements
- pragma Statement
- const Statement
- let Statement
- Assignment Statement
- Augmenting Assignment Statement
- revert Statement
- return Statemetn
- if Statement
- for Statement
- while Statement
- break Statement
- continue Statement
- match Statement
- assert Statement
pragma
statement
Syntax
PragmaStatement :
pragma
VersionRequirementVersionRequirement :Following the semver implementation by cargo
The pragma statement is denoted with the keyword pragma
. Evaluating a pragma
statement will cause the compiler to reject compilation if the version of the compiler does not conform to the given version requirement.
An example of a pragma
statement:
pragma ^0.1.0
The version requirement syntax is identical to the one that is used by cargo (more info).
const
statement
Syntax
ConstStatement :
const
IDENTIFIER:
Type=
Expression
A const
statement introduces a named constant value. Constants are either directly inlined wherever they are used or loaded from the contract code depending on their type.
Example:
const TEN: u256 = 10
const HUNDO: u256 = TEN * TEN
contract Foo {
pub fn bar() -> u256 {
return HUNDO
}
}
let
statement
Syntax
LetStatement :
let
IDENTIFIER | TupleTarget:
Type=
ExpressionTupleTarget :
(
TupleTargetItem (,
TupleTargetItem) +)
TupleTargetItem :
IDENTIFIER | TupleTarget
A let
statement introduces a new set of variables. Any variables introduced by a variable declaration are visible from the point of declaration until the end of the enclosing block scope.
Note: Support for nested tuples isn't yet implemented but can be tracked via this GitHub issue.
Example:
contract Foo {
pub fn bar() {
let val1: u256 = 1
let (val2):(u256) = (1,)
let (val3, val4):(u256, bool) = (1, false)
let (val5, val6, (val7, val8)):(u256, bool, (u256, u256)) = (1, false, (2, 4))
}
}
Assignment statement
Syntax
AssignmentStatement :
Expression=
Expression
An assignment statement moves a value into a specified place. An assignment statement consists of an expression that holds a mutable place, followed by an equals sign (=) and a value expression.
Example:
contract Foo {
some_array: Array<u256, 10>
pub fn bar(mut self) {
let mut val1: u256 = 10
// Assignment of stack variable
val1 = 10
let mut values: (u256, u256) = (1, 2)
// Assignment of tuple item
values.item0 = 3
// Assignment of storage array slot
self.some_array[5] = 1000
}
}
Augmenting Assignment statement
Syntax
AssignmentStatement :
Expression=
Expression
| Expression+=
Expression
| Expression-=
Expression
| Expression%=
Expression
| Expression**=
Expression
| Expression<<=
Expression
| Expression>>=
Expression
| Expression|=
Expression
| Expression^=
Expression
| Expression&=
Expression
Augmenting assignment statements combine arithmetic and logical binary operators with assignment statements.
An augmenting assignment statement consists of an expression that holds a mutable place, followed by one of the arithmetic or logical binary operators, followed by an equals sign (=) and a value expression.
Example:
fn example() -> u8 {
let mut a: u8 = 1
let b: u8 = 2
a += b
a -= b
a *= b
a /= b
a %= b
a **= b
a <<= b
a >>= b
a |= b
a ^= b
a &= b
return a
}
revert
statement
Syntax
RevertStatement :
revert
Expression?
The revert statement is denoted with the keyword revert
. Evaluating a revert
statement will cause to revert all state changes made by the call and return with an revert error to the caller. A revert statement may be followed by an expression that evaluates to a struct in which case the struct is encoded as revert data as defined by EIP-838.
An example of a revert
statement without revert data:
contract Foo {
fn transfer(self, to: address, value: u256) {
if not self.in_whitelist(addr: to) {
revert
}
// more logic here
}
fn in_whitelist(self, addr: address) -> bool {
return false
}
}
An example of a revert
statement with revert data:
struct ApplicationError {
pub code: u8
}
contract Foo {
pub fn transfer(self, to: address, value: u256) {
if not self.in_whitelist(addr: to) {
revert ApplicationError(code: 5)
}
// more logic here
}
fn in_whitelist(self, addr: address) -> bool {
return false
}
}
return
statement
Syntax
ReturnStatement :
return
Expression?
The return statement is denoted with the keyword return
. A return
statement leaves the current function call with a return value which is either the value of the evaluated expression (if present) or ()
(unit type) if return
is not followed by an expression explicitly.
An example of a return
statement without explicit use of an expression:
contract Foo {
fn transfer(self, to: address, value: u256) {
if not self.in_whitelist(to) {
return
}
}
fn in_whitelist(self, to: address) -> bool {
// revert used as placeholder for actual logic
revert
}
}
The above can also be written in a slightly more verbose form:
contract Foo {
fn transfer(self, to: address, value: u256) -> () {
if not self.in_whitelist(to) {
return ()
}
}
fn in_whitelist(self, to: address) -> bool {
// revert used as placeholder for actual logic
revert
}
}
if
statement
Syntax
IfStatement :
if
Expression{
(Statement | Expression)+
}
(else
{
(Statement | Expression)+
}
)?
Example:
contract Foo {
pub fn bar(val: u256) -> u256 {
if val > 5 {
return 1
} else {
return 2
}
}
}
The if
statement is used for conditional execution.
for
statement
Syntax
ForStatement :
for
IDENTIFIERin
Expression{
(Statement | Expression)+
}
A for
statement is a syntactic construct for looping over elements provided by an array type.
An example of a for
loop over the contents of an array:
Example:
contract Foo {
pub fn bar(values: Array<u256, 10>) -> u256 {
let mut sum: u256 = 0
for i in values {
sum = sum + i
}
return sum
}
}
while
statement
Syntax
WhileStatement :
while
Expression{
(Statement | Expression)+
}
A while
loop begins by evaluation the boolean loop conditional expression. If the loop conditional expression evaluates to true
, the loop body block executes, then control returns to the loop conditional expression. If the loop conditional expression evaluates to false
, the while
expression completes.
Example:
contract Foo {
pub fn bar() -> u256 {
let mut sum: u256 = 0
while sum < 10 {
sum += 1
}
return sum
}
}
break
statement
Syntax
BreakStatement :
break
The break
statement can only be used within a for
or while
loop and causes the immediate termination of the loop.
If used within nested loops the break
statement is associated with the innermost enclosing loop.
An example of a break
statement used within a while
loop.
contract Foo {
pub fn bar() -> u256 {
let mut sum: u256 = 0
while sum < 10 {
sum += 1
if some_abort_condition() {
break
}
}
return sum
}
fn some_abort_condition() -> bool {
// some complex logic
return true
}
}
An example of a break
statement used within a for
loop.
contract Foo {
pub fn bar(values: Array<u256, 10>) -> u256 {
let mut sum: u256 = 0
for i in values {
sum = sum + i
if some_abort_condition() {
break
}
}
return sum
}
fn some_abort_condition() -> bool {
// some complex logic
return true
}
}
continue
statement
Syntax
ContinueStatement :
continue
The continue
statement can only be used within a for
or while
loop and causes the immediate termination of the current iteration, returning control to the loop head.
If used within nested loops the continue
statement is associated with the innermost enclosing loop.
An example of a continue
statement used within a while
loop.
contract Foo {
pub fn bar() -> u256 {
let mut sum: u256 = 0
while sum < 10 {
sum += 1
if some_skip_condition() {
continue
}
}
return sum
}
fn some_skip_condition() -> bool {
// some complex logic
return true
}
}
An example of a continue
statement used within a for
loop.
contract Foo {
pub fn bar(values: Array<u256, 10>) -> u256 {
let mut sum: u256 = 0
for i in values {
sum = sum + i
if some_skip_condition() {
continue
}
}
return sum
}
fn some_skip_condition() -> bool {
// some complex logic
return true
}
}
match
statement
Syntax
MatchStatement :
match
Expression{
( Pattern=>
{
Statement*}
)+
}
Pattern :
PatternElem (|
PatternElem )*PatternElem :
IDENTIFIER | BOOLEAN_LITERAL |_
|..
| Path |
Path(
TuplePatterns?)
|(
TuplePatterns?)
|
Path{
StructPatterns?}
TuplePatterns :
Pattern (,
Pattern )*StructPatterns :
Field (,
Field)*(,
..
)?Field :
IDENTIFIER:
Pattern
A match
statements compares expression
with patterns, then executes body of the matched arm.
Example:
enum MyEnum {
Unit
Tuple(u32, u256, bool)
}
contract Foo {
pub fn bar(self) -> u256 {
let val: MyEnum = MyEnum::Tuple(1, 10, false)
return self.eval(val)
}
fn eval(self, val: MyEnum) -> u256 {
match val {
MyEnum::Unit => {
return 0
}
MyEnum::Tuple(.., false) => {
return 1
}
MyEnum::Tuple(a, b, true) => {
return u256(a) + b
}
}
}
}
assert
statement
Syntax
AssertStatement :
assert
Expression (,
Expression)?
The assert
statement is used express invariants in the code. It consists of a boolean expression optionally followed by a comma followed by a string expression.
If the boolean expression evaluates to false
, the code reverts with a panic code of 0x01
. In the case that the first expression evaluates to false
and a second string expression is given, the code reverts with the given string as the error code.
Warning:
The current implementation of assert
is under active discussion and likely to change.
An example of a assert
statement without the optional message:
contract Foo {
fn bar(val: u256) {
assert val > 5
}
}
An example of a assert
statement with an error message:
contract Foo {
fn bar(val: u256) {
assert val > 5, "Must be greater than five"
}
}
Expressions
- Call expressions
- Tuple expressions
- List expressions
- Struct expressions
- Index expressions
- Attribute expressions
- Name expressions
- Name expressions
- Literal expressions
- Arithmetic Operators
- Comparison Operators
- Boolean Operators
- Unary Operators
Call expressions
Syntax
CallExpression :
Expression GenericArgs?(
CallParams?)
GenericArgs :
<
IDENTIFIER | INTEGER_LITERAL (,
IDENTIFIER | INTEGER_LITERAL)*>
CallParams :
CallArg (,
CallArg )*,
?CallArg :
(CallArgLabel=
)? ExpressionCallArgLabel :
IDENTIFIERLabel must correspond to the name of the called function argument at the given position. It can be omitted if parameter name and the name of the called function argument are equal.
A call expression calls a function. The syntax of a call expression is an expression, followed by a parenthesized comma-separated list of call arguments. Call arguments are expressions, and must be labeled and provided in the order specified in the function definition. If the function eventually returns, then the expression completes.
Example:
contract Foo {
pub fn demo(self) {
let ann: address = 0xaa
let bob: address = 0xbb
self.transfer(from: ann, to: bob, 25)
}
pub fn transfer(self, from: address, to: address, _ val: u256) {}
}
Tuple expressions
Syntax
TupleExpression :
(
TupleElements?)
TupleElements :
( Expression,
)+ Expression?
A tuple expression constructs tuple values.
The syntax for tuple expressions is a parenthesized, comma separated list of expressions, called the tuple initializer operands. The number of tuple initializer operands is the arity of the constructed tuple.
1-ary tuple expressions require a comma after their tuple initializer operand to be disambiguated with a parenthetical expression.
Tuple expressions without any tuple initializer operands produce the unit tuple.
For other tuple expressions, the first written tuple initializer operand initializes the field item0
and subsequent operands initializes the next highest field.
For example, in the tuple expression (true, false, 1)
, true
initializes the value of the field item0
, false
field item1
, and 1
field item2
.
Examples of tuple expressions and their types:
Expression | Type |
---|---|
() | () (unit) |
(0, 4) | (u256, u256) |
(true, ) | (bool, ) |
(true, -1, 1) | (bool, i256, u256) |
A tuple field can be accessed via an attribute expression.
Example:
contract Foo {
pub fn bar() {
// Creating a tuple via a tuple expression
let some_tuple: (u256, bool) = (1, false)
// Accessing the first tuple field via the `item0` field
baz(input: some_tuple.item0)
}
pub fn baz(input: u256) {}
}
List expressions
Syntax
ListExpression :
[
ListElements?]
ListElements :
Expression (,
Expression)*,
?
A list expression constructs array values.
The syntax for list expressions is a parenthesized, comma separated list of expressions, called the list initializer operands. The number of list initializer operands must be equal to the static size of the array type. The types of all list initializer operands must conform to the type of the array.
Examples of tuple expressions and their types:
Expression | Type |
---|---|
[1, self.get_number()] | u256[2] |
[true, false, false] | bool[3] |
An array item can be accessed via an index expression.
Example:
contract Foo {
fn get_val3() -> u256 {
return 3
}
pub fn baz() {
let val1: u256 = 2
// A list expression
let foo: Array<u256, 3> = [1, val1, get_val3()]
}
}
Struct expressions
TBW
Index expressions
Syntax
IndexExpression :
Expression[
Expression]
Array and Map types can be indexed by by writing a square-bracket-enclosed expression after them. For arrays, the type of the index key has to be u256
whereas for Map types it has to be equal to the key type of the map.
Example:
contract Foo {
balances: Map<address, u256>
pub fn baz(mut self, mut values: Array<u256, 10>) {
// Assign value at slot 5
values[5] = 1000
// Read value at slot 5
let val1: u256 = values[5]
// Assign value for address zero
self.balances[address(0)] = 10000
// Read balance of address zero
let bal: u256 = self.balances[address(0)]
}
}
Attribute expressions
Syntax
AttributeExpression :
Expression.
IDENTIFIER
An attribute expression evaluates to the location of an attribute of a struct, tuple or contract.
The syntax for an attribute expression is an expression, then a .
and finally an identifier.
Examples:
struct Point {
pub x: u256
pub y: u256
}
contract Foo {
some_point: Point
some_tuple: (bool, u256)
fn get_point() -> Point {
return Point(x: 100, y: 500)
}
pub fn baz(some_point: Point, some_tuple: (bool, u256)) {
// Different examples of attribute expressions
let bool_1: bool = some_tuple.item0
let x1: u256 = some_point.x
let point1: u256 = get_point().x
let point2: u256 = some_point.x
}
}
Name expressions
Syntax
NameExpression :
IDENTIFIER
A name expression resolves to a local variable.
Example:
contract Foo {
pub fn baz(foo: u256) {
// name expression resolving to the value of `foo`
foo
}
}
Path expressions
Syntax
PathExpression :
IDENTIFIER (::
IDENTIFIER )*
A name expression resolves to a local variable.
Example:
contract Foo {
pub fn baz() {
// CONST_VALUE is defined in another module `my_mod`.
let foo: u32 = my_mod::CONST_VALUE
}
}
Literal expressions
Syntax
LiteralExpression :
| STRING_LITERAL
| INTEGER_LITERAL
| BOOLEAN_LITERAL
A literal expression consists of one of the literal forms described earlier. It directly describes a number, string or boolean value.
"hello" // string type
5 // integer type
true // boolean type
Arithmetic Operators
Syntax
ArithmeticExpression :
Expression+
Expression
| Expression-
Expression
| Expression*
Expression
| Expression/
Expression
| Expression%
Expression
| Expression**
Expression
| Expression&
Expression
| Expression|
Expression
| Expression^
Expression
| Expression<<
Expression
| Expression>>
Expression
Binary operators expressions are all written with infix notation. This table summarizes the behavior of arithmetic and logical binary operators on primitive types.
Symbol | Integer |
---|---|
+ | Addition |
- | Subtraction |
* | Multiplication |
/ | Division* |
% | Remainder |
** | Exponentiation |
& | Bitwise AND |
| | Bitwise OR |
^ | Bitwise XOR |
<< | Left Shift |
>> | Right Shift |
* Integer division rounds towards zero.
Here are examples of these operators being used.
3 + 6 == 9
6 - 3 == 3
2 * 3 == 6
6 / 3 == 2
5 % 4 == 1
2 ** 4 == 16
12 & 25 == 8
12 | 25 == 29
12 ^ 25 == 21
212 << 1 == 424
212 >> 1 == 106
Comparison Operators
Syntax
ComparisonExpression :
Expression==
Expression
| Expression!=
Expression
| Expression>
Expression
| Expression<
Expression
| Expression>=
Expression
| Expression<=
Expression
Symbol | Meaning |
---|---|
== | Equal |
!= | Not equal |
> | Greater than |
< | Less than |
>= | Greater than or equal to |
<= | Less than or equal to |
Here are examples of the comparison operators being used.
123 == 123
23 != -12
12 > 11
11 >= 11
11 < 12
11 <= 11
Boolean Operators
Syntax
BooleanExpression :
Expressionor
Expression
| Expressionand
Expression
The operators or
and and
may be applied to operands of boolean type. The or
operator denotes logical 'or', and the and
operator denotes logical 'and'.
Example:
const x: bool = false or true // true
Unary Operators
Syntax
UnaryExpression :
not
Expression
|-
Expression
|~
Expression
The unary operators are used to negate expressions. The unary -
(minus) operator yields the negation of its numeric argument. The unary ~
(invert) operator yields the bitwise inversion of its integer argument. The unary not
operator yields the inversion of its boolean argument.
Example:
fn f() {
let x: bool = not true // false
let y: i256 = -1
let z: i256 = i256(~1) // -2
}
Type System
Types
Every variable, item, and value in a Fe program has a type. The _ of a value defines the interpretation of the memory holding it and the operations that may be performed on the value.
Built-in types are tightly integrated into the language, in nontrivial ways that are not possible to emulate in user-defined types. User-defined types have limited capabilities.
The list of types is:
- Data types
- Other types:
Boolean type
The bool
type is a data type which can be either true
or false
.
Example:
let x: bool = true
Contract types
An contract type is the type denoted by the name of an contract
item.
A value of a given contract type carries the contract's public interface as
attribute functions. A new contract value can be created by either casting
an address to a contract type or by creating a new contract using the type
attribute functions create
or create2
.
Example:
contract Foo {
pub fn get_my_num() -> u256 {
return 42
}
}
contract FooFactory {
pub fn create2_foo(mut ctx: Context) -> address {
// `0` is the value being sent and `52` is the address salt
let foo: Foo = Foo.create2(ctx, 0, 52)
return address(foo)
}
}
Numeric types
The unsigned integer types consist of:
Type | Minimum | Maximum |
---|---|---|
u8 | 0 | 28-1 |
u16 | 0 | 216-1 |
u32 | 0 | 232-1 |
u64 | 0 | 264-1 |
u128 | 0 | 2128-1 |
u256 | 0 | 2256-1 |
The signed two's complement integer types consist of:
Type | Minimum | Maximum |
---|---|---|
i8 | -(27) | 27-1 |
i16 | -(215) | 215-1 |
i32 | -(231) | 231-1 |
i64 | -(263) | 263-1 |
i128 | -(2127) | 2127-1 |
i256 | -(2255) | 2255-1 |
Tuple Types
Tuple types are a family of structural types1 for heterogeneous lists of other types.
The syntax for a tuple type is a parenthesized, comma-separated list of types.
A tuple type has a number of fields equal to the length of the list of types.
This number of fields determines the arity of the tuple.
A tuple with n
fields is called an n-ary tuple.
For example, a tuple with 2 fields is a 2-ary tuple.
Fields of tuples are named using increasing numeric names matching their position in the list of types.
The first field is item0
.
The second field is item1
.
And so on.
The type of each field is the type of the same position in the tuple's list of types.
For convenience and historical reasons, the tuple type with no fields (()
) is often called unit or the unit type.
Its one value is also called unit or the unit value.
Some examples of tuple types:
()
(also known as the unit or zero-sized type)(u8, u8)
(bool, i32)
(i32, bool)
(different type from the previous example)
Values of this type are constructed using a tuple expression. Furthermore, various expressions will produce the unit value if there is no other meaningful value for it to evaluate to. Tuple fields can be accessed via an attribute expression.
Structural types are always equivalent if their internal types are equivalent.
Array types
Syntax
ArrayType :
Array<Type, INTEGER_LITERAL>
An array is a fixed-size sequence of N
elements of type T
. The array type
is written as Array<T, N>
. The size is an integer literal.
Arrays are either stored in storage or memory but are never stored directly on the stack.
Examples:
contract Foo {
// An array in storage
bar: Array<u8, 10>
fn do_something() {
// An array in memory
let values: Array<u256, 3> = [10, 100, 100]
}
}
All elements of arrays are always initialized, and access to an array is always bounds-checked in safe methods and operators.
Struct types
A struct type is the type denoted by the name of an struct
item.
A struct
type is a heterogeneous product of other types, called the
fields of the type.
New instances of a struct
can be constructed with a struct expression.
Struct types are either stored in storage or memory but are never stored directly on the stack.
Examples:
struct Rectangle {
pub width: u256
pub length: u256
}
contract Example {
// A Rectangle in storage
area: Rectangle
fn do_something() {
let length: u256 = 20
// A rectangle in memory
let square: Rectangle = Rectangle(width: 10, length)
}
}
All fields of struct types are always initialized.
The data layout of a struct
is not part of its external API and may be changed in any release.
The fields of a struct
may be qualified by visibility modifiers, to allow
access to data in a struct outside a module.
Enum types
An enum type is the type denoted by the name of an enum
item.
Address Type
The address
type represents a 20 byte Ethereum address.
Example:
contract Example {
// An address in storage
someone: address
fn do_something() {
// A plain address (not part of a tuple, struct etc) remains on the stack
let dai_contract: address = 0x6b175474e89094c44da98b954eedeac495271d0f
}
}
Map type
The type Map<K, V>
is used to associate key values with data.
The following types can be used as key:
The values can be of any type including other maps, structs, tuples or arrays.
Example:
contract Foo {
bar: Map<address, Map<address, u256>>
baz: Map<address, Map<u256, bool>>
pub fn read_bar(self, a: address, b: address) -> u256 {
return self.bar[a][b]
}
pub fn write_bar(mut self, a: address, b: address, value: u256) {
self.bar[a][b] = value
}
pub fn read_baz(self, a: address, b: u256) -> bool {
return self.baz[a][b]
}
pub fn write_baz(mut self, a: address, b: u256, value: bool) {
self.baz[a][b] = value
}
}
String Type
A value of type String<N>
represents a sequence of unsigned bytes with the assumption that the data is valid UTF-8.
The String<N>
type is generic over a constant value that has to be an integer literal. That value N
constraints the maximum number of bytes that are available for storing the string's characters.
Note that the value of N
does not restrict the type to hold exactly that number of bytes at all times which means that a type such as String<10>
can hold a short word such as "fox"
but it can not hold a full sentence such as "The brown fox jumps over the white fence"
.
Example:
contract Foo {
fn bar() {
let single_byte_string: String<1> = "a"
let longer_string: String<100> = "foo"
}
}
Unit type
TBW
Function Types
TBW
Data Layout
There are three places where data can be stored on the EVM:
- stack: 256-bit values placed on the stack that are loaded using
DUP
operations. - storage: 256-bit address space where 256-bit values can be stored. Accessing higher storage slots does not increase gas cost.
- memory: 256-bit address space where 256-bit values can be stored. Accessing higher memory slots increases gas cost.
Each data type can be stored in these locations. How data is stored is described in this section.
Stack
The following can be stored on the stack:
- base type values
- pointers to reference type values
The size of each value stored on the stack must not exceed 256 bits. Since all base types are less than or equal to 256 bits in size, we store them on the stack. Pointers to values stored in memory or storage may also be stored on the stack.
Example:
fn f() {
let foo: u256 = 42 // foo is stored on the stack
let bar: Array<u256, 100> = [0; 100] // bar is a memory pointer stored on the stack
}
Storage
All data types can be stored in storage.
Constant size values in storage
Storage pointers for constant size values are determined at compile time.
Example:
contract Cats {
population: u256 // assigned a static location by the compiler
}
The value of a base type in storage is found by simply loading the value from storage at the given pointer.
To find an element inside of a sequence type, the relative location of the element is added to the given pointer.
Maps in storage
Maps are not assigned pointers, because they do not have a location in storage. They are instead assigned a nonce that is used to derive the location of keyed values during runtime.
Example:
contract Foo {
bar: Map<address, u256> // bar is assigned a static nonce by the compiler
baz: Map<address, Map<address, u256>> // baz is assigned a static nonce by the compiler
}
The expression bar[0x00]
would resolve to the hash of both bar's nonce and the key value
.i.e. keccak256(<bar nonce>, 0x00)
. Similarly, the expression baz[0x00][0x01]
would resolve to
a nested hash i.e. keccak256(keccak256(<baz nonce>, 0x00), 0x01)
.
The to_mem
function
Reference type values can be copied from storage and into memory using the to_mem
function.
Example:
let my_array_var: Array<u256, 10> = self.my_array_field.to_mem()
Memory
Only sequence types can be stored in memory.
The first memory slot (0x00
) is used to keep track of the lowest available memory slot. Newly
allocated segments begin at the value given by this slot. When more memory has been allocated,
the value stored in 0x00
is increased.
We do not free memory after it is allocated.
Sequence types in memory
Sequence type values may exceed the 256-bit stack slot size, so we store them in memory and reference them using pointers kept on the stack.
Example:
fn f() {
let foo: Array<u256, 100> = [0; 100] // foo is a pointer that references 100 * 256 bits in memory.
}
To find an element inside of a sequence type, the relative location of the element is added to the given pointer.
Contributing
You can contribute to Fe!
Ways to contribute:
1. Reporting or fixing issues.
If you find problems with Fe you can report them to the development team on the project Github.
You are also welcome to work on existing issues, especially any tagged good first issue
on the project issue board.
2. Improving the docs.
We always appreciate improvements to the project documentation. This could be fixing any bugs you find, adding some detail to a description or explanation or developing a new user guide.
To add to the docs you can fork the Fe Github repository and make updates in the /docs
directory. You can build and serve locally using mdbook build && mdbook serve
. Then, when you are happy with your changes you can raise a pull request to the main repository.
3. Developing Fe
You are also welcome to work on Fe itself. There are many opportunities to help build the language, for example working on the compiler or the language specification, adding tests or developing tooling.
It is a good idea to connect with the existing Fe community to find out what are the priority areas that need attention and to discuss your idea in context before putting time into it. You can find Fe developers on the Discord or you can use the Github issue board.
Please note that there has been a substantial amount of work done on the
fe-v2
branch of the repository that includes breaking changes. When mergedfe-v2
will revert new contributions based onmaster
.To make your work v2 ready you can build off the
fe-v2
branch. It is recommended to seek out issues taggedv2
in the Github Issue board.
4. Community engagement
We appreciate help answering questions on the Discord and other platforms such as Stack Exchange or Twitter.
Please note that this project has a Code of Conduct. By participating in this project — in the issues, pull requests, or Discord channel — you agree to abide by its terms.
Processes
Reporting issues
To report an issue, please use the Github issue board. When reporting issues, please mention the following details:
- Fe version.
- your operating system.
- the steps required to reproduce the issue
- actual vs expected behaviour
- any error messages or relevant logs
- the specific source code where the issue originates
The appropriate place for technical discussions about the language itself is the Fe Discord.
Rasing Pull Requests
Please fork the Fe repository and raise pull requests against the master
branch.
Your commit messages should be concise and explain the changes made. Your pull request description should explain why the changes were made and list the specific changes. If you have to pull in changes from master
to your fork (e.g. to resolve merge conflicts), please use git rebase
rather than git merge
.
New features should be accompanied by appropriate tests.
Finally, please make sure you respect the coding style for this project.
Thank you for contributing to Fe!
Release Notes
🖥️ Download Binaries 📄 Draft Spec ℹ️ Getting Started
Fe is moving fast. Read up on all the latest improvements.
0.26.0 "Zircon" (2023-11-03)
Features
-
Give option to produce runtime bytecode as compilation artifact
Previously, the compiler could only produce the bytecode that is used for the deployment of the contract. Now it can also produce the runtime bytecode which is the bytecode that is saved to storage.
Being able to obtain the runtime bytecode is useful for contract verification.
To obtain the runtime bytecode use the
runtime-bytecode
option of the--emit
flag (multiple options allowed).Example Output:
- mycontract.bin (bytecode for deployment)
- mycontract.runtime.bin (runtime bytecode) (#947)
-
New
verify
command to verify onchain contracts against local source code.People need to be able to verify that a deployed contract matches the source code that the author claims was used to deploy it. Previously, there was no simple way to achieve this.
These are the steps to verify a contract with the
verify
command:- Obtain the project's source code locally.
- Ensure it is the same source code that was used to deploy the contract. (e.g. check out a specific tag)
- From the project directory run
fe verify <contract-address> <json-rpc-url>
Example:
$ fe verify 0xf0adbb9ed4135d1509ad039505bada942d18755f https://example-eth-mainnet-rpc.com It's a match!✨ Onchain contract: Address: 0xf0adbb9ed4135d1509ad039505bada942d18755f Bytecode: 0x60008..76b90 Local contract: Contract name: SimpleDAO Source file: /home/work/ef/simple_dao/fe_contracts/simpledao/src/main.fe Bytecode: 0x60008..76b90 Hint: Run with --verbose to see the contract's source code.
(#948)
Improved Documentation
- Added a new page on EVM precompiles (#944)
0.25.0 "Yoshiokaite" (2023-10-26)
Features
-
Use the project root as default path for
fe test
Just run
fe test
from any directory of the project. (#913) -
Completed
std::buf::MemoryBuffer
refactor. (#917) -
Allow filtering tests to run via
fe test --filter <some-filter
E.g. Running
fe test --filter foo
will run all tests that containfoo
in their name. (#919) -
Logs for successfully ran tests can be printed with the
--logs
parameter.example:
// test_log.fe use std::evm::log0 use std::buf::MemoryBuffer struct MyEvent { pub foo: u256 pub baz: bool pub bar: u256 } #test fn test_log(mut ctx: Context) { ctx.emit(MyEvent(foo: 42, baz: false, bar: 26)) unsafe { log0(buf: MemoryBuffer::new(len: 42)) } }
$ fe test --logs test_log.fe executing 1 test in test_log: test_log ... passed test_log produced the following logs: MyEvent emitted by 0x0000…002a with the following parameters [foo: 2a, baz: false, bar: 1a] Log { address: 0x000000000000000000000000000000000000002a, topics: [], data: b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x1a\0\0\0\0\0\0\0\0\0\0" } 1 test passed; 0 tests failed; 1 test executed
Note: Logs are not collected for failing tests. (#933)
-
Adds 'functions' section to docs with information on
self
andContext
. (#937)
Bugfixes
-
Yul codegen was failing to include string literals used in test assertions. This resulted in a compiler error.
Example:
#test fn foo() { assert false, "oops" }
The example code above was failing to compile, but now it compiles and executes as expected. (#926)
Improved Documentation
- Added a new tutorial: Open Auction (#930)
0.24.0 "Xenotime" (2023-08-10)
Features
-
Added support for project manifests and project dependencies.
Example:
my_project ├── fe.toml └── src └── main.fe
# fe.toml name = "my_project" version = "1.0" [dependencies] my_lib = { path = "../path/to/my_lib", version = "1.0" } my_other_lib = "../path/to/my_other_lib"
Note: The current implementation supports circular dependencies. (#908)
Performance improvements
MemoryBuffer
now allocates an extra 31 bytes. This removes the need for runtime checks and bitshifting needed to ensure safe writing to aMemoryBuffer
's region. (#898)
Improved Documentation
- Link to vs-code extension in Quickstart Guide (#910)
0.23.0 "Wiluite" (2023-06-01)
Features
-
Fixed an issue where generic parameters that were
mut
could not be satisfied at callsite.For instance, the following code would previously cause a compile error but now works as expected:
#![allow(unused)] fn main() { struct Runner { pub fn run<T: Computable>(self, mut _ val: T) -> u256 { return val.compute(val: 1000) } } contract Example { pub fn run_test(self) { let runner: Runner = Runner(); let mut mac: Mac = Mac(); assert runner.run(mac) == 1001 } } }
(#865)
-
The
ctx
parameter can now be passed into test functions.example:
#test fn my_test(ctx: Context) { assert ctx.block_number() == 0 }
(#880)
-
The following has been added to the standard library:
Memory buffer abstraction
example:
use std::buf::{MemoryBuffer, MemoryBufferReader, MemoryBufferWriter} use std::traits::Max #test fn test_buf_rw() { let mut buf: MemoryBuffer = MemoryBuffer::new(len: 161) let mut writer: MemoryBufferWriter = buf.writer() let mut reader: MemoryBufferReader = buf.reader() writer.write(value: 42) writer.write(value: 42) writer.write(value: 26) writer.write(value: u8(26)) writer.write(value: u256::max()) writer.write(value: u128::max()) writer.write(value: u64::max()) writer.write(value: u32::max()) writer.write(value: u16::max()) writer.write(value: u8::max()) writer.write(value: u8(0)) assert reader.read_u256() == 42 assert reader.read_u256() == 42 assert reader.read_u256() == 26 assert reader.read_u8() == 26 assert reader.read_u256() == u256::max() assert reader.read_u128() == u128::max() assert reader.read_u64() == u64::max() assert reader.read_u32() == u32::max() assert reader.read_u16() == u16::max() assert reader.read_u8() == u8::max() assert reader.read_u8() == 0 }
Precompiles
example:
use std::precompiles use std::buf::{MemoryBuffer, MemoryBufferReader, MemoryBufferWriter} #test fn test_ec_recover() { let result: address = precompiles::ec_recover( hash: 0x456e9aea5e197a1f1af7a3e85a3212fa4049a3ba34c2289b4c860fc0b0c64ef3, v: 28, r: 0x9242685bf161793cc25603c231bc2f568eb630ea16aa137d2664ac8038825608, s: 0x4f8ae3bd7535248d0bd448298cc2e2071e56992d0774dc340c368ae950852ada ) assert result == address(0x7156526fbd7a3c72969b54f64e42c10fbb768c8a) }
ctx.raw_call()
example:
use std::buf::{ RawCallBuffer, MemoryBufferReader, MemoryBufferWriter } use std::evm contract Foo { pub unsafe fn __call__() { if evm::call_data_load(offset: 0) == 42 { evm::mstore(offset: 0, value: 26) evm::return_mem(offset: 0, len: 32) } else if evm::call_data_load(offset: 0) == 26 { revert } } } #test fn test_raw_call(mut ctx: Context) { let foo: Foo = Foo.create(ctx, 0) let mut buf: RawCallBuffer = RawCallBuffer::new( input_len: 32, output_len: 32 ) let mut writer: MemoryBufferWriter = buf.writer() writer.write(value: 42) assert ctx.raw_call(addr: address(foo), value: 0, buf) let mut reader: MemoryBufferReader = buf.reader() assert reader.read_u256() == 26 assert not ctx.raw_call(addr: address(foo), value: 0, buf) }
(#885)
Bugfixes
-
Fixed an ICE when using aggregate types with aggregate type fields in public functions
This code would previously cause an ICE:
struct Tx { pub data: Array<u8, 320> } contract Foo { pub fn bar(mut tx: Tx) {} }
(#867)
-
Fixed a regression where the compiler would not reject a method call on a struct in storage.
E.g. the follwing code should be rejected as it is missing a
to_mem()
call:struct Bar { pub x: u256 pub fn get_x(self) -> u256{ return self.x } } contract Foo { bar: Bar pub fn __init__(mut self) { self.bar = Bar( x: 2 ) } fn yay(self) { self.bar.get_x() } }
The compiler will now reject the code and suggest a
to_mem()
before callingget_x()
. (#881)
0.22.0 "Vulcanite" (2023-04-05)
This is the first non-alpha release of Fe. Read our announcement for more details.
Features
-
Support for tests.
example:
#test fn my_test() { assert 26 + 16 == 42 }
Tests can be executed using the
test
subcommand.example:
$ fe test foo.fe
(#807) -
Fixed broken trait orphan rule
Fe has an orphan rule for Traits similar to Rust's that requires that either the trait or the type that we are implementing the trait for are located in the same ingot as the
impl
. This rule was implemented incorrectly so that instead of requiring them to be in the same ingot, they were required to be in the same module. This change fixes this so that the orphan rule is enforced correctly. (#863) -
address
values can now be specified with a number literal, with no explicit cast. So instead oflet t: address = address(0xfe)
, one can now writelet t: address = 0xfe
. This also means that it's possible to defineconst
addresses:const SOME_KNOWN_CONTRACT: address = 0xfefefefe
(#864)
Bugfixes
-
Fixed resolving of generic arguments to associated functions.
For example, this code would previously crash the compiler:
#![allow(unused)] fn main() { ... // This function doesn't take self pub fn run_static<T: Computable>(_ val: T) -> u256 { return val.compute(val: 1000) } ... // Invoking it would previously crash the compiler Runner::run_static(Mac()) ... }
(#861)
Improved Documentation
- Changed the Deployment tutorial to use foundry and the Sepolia network (#853)
0.21.0-alpha (2023-02-28)
Features
-
Support for
Self
typeWith this change
Self
(with capitalS
) can be used to refer to the enclosing type in contracts, structs, impls and traits.E.g.
trait Min { fn min() -> Self; } impl Min for u8 { fn min() -> u8 { // Both `u8` or `Self` are valid here return 0 } }
Usage:
u8::min()
(#803) -
Added
Min
andMax
traits to the std library. The std library implements the traits for all numeric types.Example
use std::traits::{Min, Max} ... assert u8::min() < u8::max() ``` ([#836](https://github.com/ethereum/fe/issues/836))
-
Upgraded underlying solc compiler to version
0.8.18
Bugfixes
- the release contains minor bugfixes
0.20.0-alpha (2022-12-05)
Features
-
Removed the
event
type as well as theemit
keyword. Instead thestruct
type now automatically implements theEmittable
trait and can be emitted viactx.emit(..)
.Indexed fields can be annotated via the
#indexed
attribute.E.g.
struct Signed { book_msg: String<100> } contract GuestBook { messages: Map<address, String<100>> pub fn sign(mut self, mut ctx: Context, book_msg: String<100>) { self.messages[ctx.msg_sender()] = book_msg ctx.emit(Signed(book_msg)) } }
(#717)
-
Allow to call trait methods on types when trait is in scope
So far traits were only useful as bounds for generic functions. With this change traits can also be used as illustrated with the following example:
trait Double { fn double(self) -> u256; } impl Double for (u256, u256) { fn double(self) -> u256 { return (self.item0 + self.item1) * 2 } } contract Example { pub fn run_test(self) { assert (0, 1).double() == 2 } }
If a call turns out to be ambigious the compiler currently asks the user to disambiguate via renaming. In the future we will likely introduce a syntax to allow to disambiguate at the callsite. (#757)
-
Allow contract associated functions to be called via
ContractName::function_name()
syntax. (#767) -
Add
enum
types andmatch
statement.enum
can now be defined, e.g.,pub enum MyEnum { Unit Tuple(u32, u256, bool) fn unit() -> MyEnum { return MyEnum::Unit } }
Also,
match
statement is introduced, e.g.,pub fn eval_enum() -> u256{ match MyEnum { MyEnum::Unit => { return 0 } MyEnum::Tuple(a, _, false) => { return u256(a) } MyEnum::Tuple(.., true) => { return u256(1) } } }
For now, available patterns are restricted to
- Wildcard(
_
), which matches all patterns:_
- Named variable, which matches all patterns and binds the value to make the value usable in the arm. e.g.,
a
,b
andc
inMyEnum::Tuple(a, b, c)
- Boolean literal(
true
andfalse
) - Enum variant. e.g.,
MyEnum::Tuple(a, b, c)
- Tuple pattern. e.g.,
(a, b, c)
- Struct pattern. e.g.,
MyStruct {x: x1, y: y1, b: true}
- Rest pattern(
..
), which matches the rest of the pattern. e.g.,MyEnum::Tuple(.., true)
- Or pattern(|). e.g., MyEnum::Unit | MyEnum::Tuple(.., true)
Fe compiler performs the exhaustiveness and usefulness checks for
match
statement.
So the compiler will emit an error when all patterns are not covered or an unreachable arm are detected. (#770) - Wildcard(
-
Changed comments to use
//
instead of#
(#776) -
Added the
mut
keyword, to mark things as mutable. Any variable or function parameter not markedmut
is now immutable.contract Counter { count: u256 pub fn increment(mut self) -> u256 { // `self` is mutable, so storage can be modified self.count += 1 return self.count } } struct Point { pub x: u32 pub y: u32 pub fn add(mut self, _ other: Point) { self.x += other.x self.y += other.y // other.x = 1000 // ERROR: `other` is not mutable } } fn pointless() { let origin: Point = Point(x: 0, y: 0) // origin.x = 10 // ERROR: origin is not mutable let x: u32 = 10 // x_coord = 100 // ERROR: `x_coord` is not mutable let mut y: u32 = 0 y = 10 // OK let mut p: Point = origin // copies `origin` p.x = 10 // OK, doesn't modify `origin` let mut q: Point = p // copies `p` q.x = 100 // doesn't modify `p` p.add(q) assert p.x == 110 }
Note that, in this release, primitive type function parameters can't be
mut
. This restriction might be lifted in a future release.For example:
fn increment(mut x: u256) { // ERROR: primitive type parameters can't be mut x += 1 }
(#777)
-
The contents of the
std::prelude
module (currently just theContext
struct) are now automaticallyuse
d by every module, souse std::context::Context
is no longer required. (#779) -
When the Fe compiler generates a JSON ABI file for a contract, the "stateMutability" field for each function now reflects whether the function can read or modify chain or contract state, based on the presence or absence of the
self
andctx
parameters, and whether those parameters aremut
able.If a function doesn't take
self
orctx
, it's "pure". If a function takesself
orctx
immutably, it can read state but not mutate state, so it's a "view" If a function takesmut self
ormut ctx
, it can mutate state, and is thus marked "payable".Note that we're following the convention set by Solidity for this field, which isn't a perfect fit for Fe. The primary issue is that Fe doesn't currently distinguish between "payable" and "nonpayable" functions; if you want a function to revert when Eth is sent, you need to do it manually (eg
assert ctx.msg_value() == 0
). (#783) -
Trait associated functions
This change allows trait functions that do not take a
self
parameter. The following demonstrates a possible trait associated function and its usage:trait Max { fn max(self) -> u8; } impl Max for u8 { fn max() -> u8 { return u8(255) } } contract Example { pub fn run_test(self) { assert u8::max() == 255 } }
(#805)
Bugfixes
-
Fix issue where calls to assiciated functions did not enforce visibility rules.
E.g the following code should be rejected but previously wasn't:
struct Foo { fn do_private_things() { } } contract Bar { fn test() { Foo::do_private_things() } }
With this change, the above code is now rejected because
do_private_things
is notpub
. (#767) -
Padding on
bytes
andstring
ABI types is zeroed out. (#769) -
Ensure traits from other modules or even ingots can be implemented (#773)
-
Certain cases where the compiler would not reject pure functions being called on instances are now properly rejected. (#775)
-
Reject calling
to_mem()
on primitive types in storage (#801) -
Disallow importing private type via
use
The following was previously allowed but will now error:
use foo::PrivateStruct
(#815)
0.19.1-alpha "Sunstone" (2022-07-06)
Features
-
Support returning nested struct.
Example:
pub struct InnerStruct { pub inner_s: String<10> pub inner_x: i256 } pub struct NestedStruct { pub inner: InnerStruct pub outer_x: i256 } contract Foo { pub fn return_nested_struct() -> NestedStruct { ... } }
(#635)
-
Made some small changes to how the
Context
object is used.ctx
is not required when casting an address to a contract type. Eglet foo: Foo = Foo(address(0))
ctx
is required when calling an external contract function that requires ctx
Example:
use std::context::Context // see issue #679 contract Foo { pub fn emit_stuff(ctx: Context) { emit Stuff(ctx) # will be `ctx.emit(Stuff{})` someday } } contract Bar { pub fn call_foo_emit_stuff(ctx: Context) { Foo(address(0)).emit_stuff(ctx) } } event Stuff {}
(#703)
-
Braces! Fe has abandoned python-style significant whitespace in favor of the trusty curly brace.
In addition,
elif
is now spelledelse if
, and thepass
statement no longer exists.Example:
pub struct SomeError {} contract Foo { x: u8 y: u16 pub fn f(a: u8) -> u8 { if a > 10 { let x: u8 = 5 return a + x } else if a == 0 { revert SomeError() } else { return a * 10 } } pub fn noop() {} }
(#707)
-
traits and generic function parameter
Traits can now be defined, e.g:
trait Computable { fn compute(self, val: u256) -> u256; }
The mechanism to implement a trait is via an
impl
block e.g:struct Linux { pub counter: u256 pub fn get_counter(self) -> u256 { return self.counter } pub fn something_static() -> u256 { return 5 } } impl Computable for Linux { fn compute(self, val: u256) -> u256 { return val + Linux::something_static() + self.get_counter() } }
Traits can only appear as bounds for generic functions e.g.:
struct Runner { pub fn run<T: Computable>(self, _ val: T) -> u256 { return val.compute(val: 1000) } }
Only
struct
functions (notcontract
functions) can have generic parameters. Therun
method ofRunner
can be called with any type that implementsComputable
e.g.contract Example { pub fn generic_compute(self) { let runner: Runner = Runner(); assert runner.run(Mac()) == 1001 assert runner.run(Linux(counter: 10)) == 1015 } }
(#710)
-
Generate artifacts for all contracts of an ingot, not just for contracts that are defined in
main.fe
(#726) -
Allow using complex type as array element type.
Example:
contract Foo { pub fn bar() -> i256 { let my_array: Array<Pair, 3> = [Pair::new(1, 0), Pair::new(2, 0), Pair::new(3, 0)] let sum: i256 = 0 for pair in my_array { sum += pair.x } return sum } } struct Pair { pub x: i256 pub y: i256 pub fn new(_ x: i256, _ y: i256) -> Pair { return Pair(x, y) } }
(#730)
-
The
fe
CLI now has subcommands:fe new myproject
- creates a new project structurefe check .
- analyzes fe source code and prints errorsfe build .
- builds a fe project (#732) -
Support passing nested struct types to public functions.
Example:
pub struct InnerStruct { pub inner_s: String<10> pub inner_x: i256 } pub struct NestedStruct { pub inner: InnerStruct pub outer_x: i256 } contract Foo { pub fn f(arg: NestedStruct) { ... } }
(#733)
-
Added support for repeat expressions (
[VALUE; LENGTH]
).e.g.
let my_array: Array<bool, 42> = [bool; 42]
Also added checks to ensure array and struct types are initialized. These checks are currently performed at the declaration site, but will be loosened in the future. (#747)
Bugfixes
-
Fix a bug that incorrect instruction is selected when the operands of a comp instruction are a signed type. (#734)
-
Fix issue where a negative constant leads to an ICE
E.g. the following code would previously crash the compiler but shouldn't:
const INIT_VAL: i8 = -1 contract Foo { pub fn init_bar() { let x: i8 = INIT_VAL } }
(#745)
-
Fix a bug that causes ICE when nested if-statement has multiple exit point.
E.g. the following code would previously crash the compiler but shouldn't:
pub fn foo(self) { if true { if self.something { return } } if true { if self.something { return } } }
(#749)
0.18.0-alpha "Ruby" (2022-05-27)
Features
- Added support for parsing of attribute calls with generic arguments (e.g.
foo.bar<Baz>()
). (#719)
Bugfixes
- Fix a regression where the
stateMutability
field would not be included in the generated ABI (#722) - Fix two regressions introduced in
0.17.0
- Properly lower right shift operation to yul's
sar
if operand is signed type - Properly lower negate operation to call
safe_sub
(#723)
- Properly lower right shift operation to yul's
0.17.0-alpha "Quartz" (2022-05-26)
Features
-
Support for underscores in numbers to improve readability e.g.
100_000
.Example
let num: u256 = 1000_000_000_000
(#149)
-
Optimized access of struct fields in storage (#249)
-
Unit type
()
is now ABI encodable (#442) -
Temporary default
stateMutability
topayable
in ABIThe ABI metadata that the compiler previously generated did not include the
stateMutability
field. This piece of information is important for tooling such as hardhat because it determines whether a function needs to be called with or without sending a transaction.As soon as we have support for
mut self
andmut ctx
we will be able to derive that information from the function signature. In the meantime we now default topayable
. (#705)
Bugfixes
-
Fixed a crash caused by certain memory to memory assignments.
E.g. the following code would previously lead to a compiler crash:
my_struct.x = my_struct.y
(#590)
-
Reject unary minus operation if the target type is an unsigned integer number.
Code below should be reject by
fe
compiler:contract Foo: pub fn bar(self) -> u32: let unsigned: u32 = 1 return -unsigned pub fn foo(): let a: i32 = 1 let b: u32 = -a
(#651)
-
Fixed crash when passing a struct that contains an array
E.g. the following would previously result in a compiler crash:
struct MyArray: pub x: Array<i32, 2> contract Foo: pub fn bar(my_arr: MyArray): pass
(#681)
-
reject infinite size struct definitions.
Fe
structs
having infinite size due to recursive definitions were not rejected earlier and would cause ICE in the analyzer since they were not properly handled. Nowstructs
having infinite size are properly identified by detecting cycles in the dependency graph of the struct field definitions and an error is thrown by the analyzer. (#682) -
Return instead of revert when contract is called without data.
If a contract is called without data so that no function is invoked, we would previously
revert
but that would leave us without a way to send ETH to a contract so instead it will cause areturn
now. (#694) -
Resolve compiler crash when using certain reserved YUL words as struct field names.
E.g. the following would previously lead to a compiler crash because
numer
is a reserved keyword in YUL.struct Foo: pub number: u256 contract Meh: pub fn yay() -> Foo: return Foo(number:2)
(#709)
0.16.0-alpha (2022-05-05)
Features
- Change static function call syntax from
Bar.foo()
toBar::foo()
(#241) - Added support for retrieving the base fee via
ctx.base_fee()
(#503)
Bugfixes
- Resolve functions on structs via path (e.g.
bi::ba::bums()
) (#241)
0.15.0-alpha (2022-04-04)
Features
-
Labels are now required on function arguments. Labels can be omitted if the argument is a variable with a name that matches the label, or if the function definition specifies that an argument should have no label. Functions often take several arguments of the same type; compiler-checked labels can help prevent accidentally providing arguments in the wrong order.
Example:
contract CoolCoin: balance: Map<address, i256> loans: Map<(address, address), i256> pub fn demo(self, ann: address, bob: address): let is_loan: bool = false self.give(from: ann, to: bob, 100, is_loan) fn transfer(self, from sender: address, to recipient: address, _ val: u256, is_loan: bool): self.cred[sender] -= val self.cred[recipient] += val if is_loan: self.loans[(sender, recipient)] += val
Note that arguments must be provided in the order specified in the function definition.
A parameter's label defaults to the parameter name, but can be changed by specifying a different label to the left of the parameter name. Labels should be clear and convenient for the caller, while parameter names are only used in the function body, and can thus be longer and more descriptive. In the example above, we choose to use
sender
andrecipient
as identifiers in the body offn transfer
, but use labelsfrom:
andto:
.In cases where it's ideal to not have labels, e.g. if a function takes a single argument, or if types are sufficient to differentiate between arguments, use
_
to specify that a given parameter has no label. It's also fine to require labels for some arguments, but not others.Example:
fn add(_ x: u256, _ y: u256) -> u256: return x + y contract Foo: fn transfer(self, _ to: address, wei: u256): pass pub fn demo(self): transfer(address(0), wei: add(1000, 42))
(#397)
Bugfixes
- The region of memory used to compute the slot of a storage map value was not being allocated. (#684)
0.14.0-alpha (2022-03-02)
Features
-
Events can now be defined outside of contracts.
Example:
event Transfer: idx sender: address idx receiver: address value: u256 contract Foo: fn transferFoo(to: address, value: u256): emit Transfer(sender: msg.sender, receiver: to, value) contract Bar: fn transferBar(to: address, value: u256): emit Transfer(sender: msg.sender, receiver: to, value)
(#80)
-
The Fe standard library now includes a
std::evm
module, which provides functions that perform low-level evm operations. Many of these are markedunsafe
, and thus can only be used inside of anunsafe
function or anunsafe
block.Example:
use std::evm::{mstore, mload} fn memory_shenanigans(): unsafe: mstore(0x20, 42) let x: u256 = mload(0x20) assert x == 42
The global functions
balance
andbalance_of
have been removed; these can now be called asstd::evm::balance()
, etc. The global functionsend_value
has been ported to Fe, and is now available asstd::send_value
. (#629) -
Support structs that have non-base type fields in storage.
Example:
struct Point: pub x: u256 pub y: u256 struct Bar: pub name: String<3> pub numbers: Array<u256, 2> pub point: Point pub something: (u256, bool) contract Foo: my_bar: Bar pub fn complex_struct_in_storage(self) -> String<3>: self.my_bar = Bar( name: "foo", numbers: [1, 2], point: Point(x: 100, y: 200), something: (1, true), ) # Asserting the values as they were set initially assert self.my_bar.numbers[0] == 1 assert self.my_bar.numbers[1] == 2 assert self.my_bar.point.x == 100 assert self.my_bar.point.y == 200 assert self.my_bar.something.item0 == 1 assert self.my_bar.something.item1 # We can change the values of the array self.my_bar.numbers[0] = 10 self.my_bar.numbers[1] = 20 assert self.my_bar.numbers[0] == 10 assert self.my_bar.numbers[1] == 20 # We can set the array itself self.my_bar.numbers = [1, 2] assert self.my_bar.numbers[0] == 1 assert self.my_bar.numbers[1] == 2 # We can change the values of the Point self.my_bar.point.x = 1000 self.my_bar.point.y = 2000 assert self.my_bar.point.x == 1000 assert self.my_bar.point.y == 2000 # We can set the point itself self.my_bar.point = Point(x=100, y=200) assert self.my_bar.point.x == 100 assert self.my_bar.point.y == 200 # We can change the value of the tuple self.my_bar.something.item0 = 10 self.my_bar.something.item1 = false assert self.my_bar.something.item0 == 10 assert not self.my_bar.something.item1 # We can set the tuple itself self.my_bar.something = (1, true) assert self.my_bar.something.item0 == 1 assert self.my_bar.something.item1 return self.my_bar.name.to_mem()
(#636)
-
Features that read and modify state outside of contracts are now implemented on a struct named "Context".
Context
is included in the standard library and can be imported withuse std::context::Context
. Instances ofContext
are created by calls to public functions that declare it in the signature or by unsafe code.Basic example:
use std::context::Context contract Foo: my_num: u256 pub fn baz(ctx: Context) -> u256: return ctx.block_number() pub fn bing(self, new_num: u256) -> u256: self.my_num = new_num return self.my_num contract Bar: pub fn call_baz(ctx: Context, foo_addr: address) -> u256: # future syntax: `let foo = ctx.load<Foo>(foo_addr)` let foo: Foo = Foo(ctx, foo_addr) return foo.baz() pub fn call_bing(ctx: Context) -> u256: # future syntax: `let foo = ctx.create<Foo>(0)` let foo: Foo = Foo.create(ctx, 0) return foo.bing(42)
Example with
__call__
and unsafe block:use std::context::Context use std::evm contract Foo: pub fn __call__(): unsafe: # creating an instance of `Context` is unsafe let ctx: Context = Context() let value: u256 = u256(bar(ctx)) # return `value` evm::mstore(0, value) evm::return_mem(0, 32) fn bar(ctx: Context) -> address: return ctx.self_address()
(#638)
-
Features
Support local constant
Example:
contract Foo: pub fn bar(): const LOCAL_CONST: i32 = 1
Support constant expression
Example:
const GLOBAL: i32 = 8 contract Foo: pub fn bar(): const LOCAL: i32 = GLOBAL * 8
Support constant generics expression
Example:
const GLOBAL: u256= 8 const USE_GLOBAL: bool = false type MY_ARRAY = Array<i32, { GLOBAL / 4 }> contract Foo: pub fn bar(): let my_array: Array<i32, { GLOBAL if USE_GLOBAL else 4 }>
Bug fixes
Fix ICE when constant type is mismatch
Example:
const GLOBAL: i32 = "FOO" contract Foo: pub fn bar(): let FOO: i32 = GLOBAL
Fix ICE when assigning value to constant twice
Example:
const BAR: i32 = 1 contract FOO: pub fn bar(): BAR = 10
(#649)
-
Argument label syntax now uses
:
instead of=
. Example:struct Foo: x: u256 y: u256 let x: MyStruct = MyStruct(x: 10, y: 11) # previously: MyStruct(x = 10, y = 11)
(#665)
-
Support module-level
pub
modifier, now default visibility of items in a module is private.Example:
# This constant can be used outside of the module. pub const PUBLIC:i32 = 1 # This constant can NOT be used outside of the module. const PRIVATE: i32 = 1
(#677)
Internal Changes - for Fe Contributors
-
- Source files are now managed by a (salsa)
SourceDb
. ASourceFileId
now corresponds to a salsa-internedFile
with a path. File content is a salsa input function. This is mostly so that the future (LSP) language server can update file content when the user types or saves, which will trigger a re-analysis of anything that changed. - An ingot's set of modules and dependencies are also salsa inputs, so that when the user adds/removes a file or dependency, analysis is rerun.
- Standalone modules (eg a module compiled with
fe fee.fe
) now have a fake ingot parent. Each Ingot has an IngotMode (Lib, Main, StandaloneModule), which is used to disallowingot::whatever
paths in standalone modules, and to determine the correct root module file. parse_module
now always returns anast::Module
, and thus aModuleId
will always exist for a source file, even if it contains fatal parse errors. If the parsing fails, the body will end with aModuleStmt::ParseError
node. The parsing will stop at all but the simplest of syntax errors, but this at least allows partial analysis of source file with bad syntax.ModuleId::ast(db)
is now a query that parses the module's file on demand, rather than the AST being interned into salsa. This makes handling parse diagnostics cleaner, and removes the up-front parsing of every module at ingot creation time. (#628)
- Source files are now managed by a (salsa)
0.13.0-alpha (2022-01-31)## 0.13.0-alpha (2022-01-31)
Features
-
Support private fields on structs
Public fields now need to be declared with the
pub
modifier, otherwise they default to private fields. If a struct contains private fields it can not be constructed directly except from within the struct itself. The recommended way is to implement a methodnew(...)
as demonstrated in the following example.struct House: pub price: u256 pub size: u256 vacant: bool pub fn new(price: u256, size: u256) -> House return House(price=price, size=size, vacant=true) contract Manager: house: House pub fn create_house(price: u256, size: u256): self.house = House::new(price, size) let can_access_price: u256 = self.house.price # can not access `self.house.vacant` because the field is private
(#214)
-
Support non-base type fields in structs
Support is currently limited in two ways:
- Structs with complex fields can not be returned from public functions
- Structs with complex fields can not be stored in storage (#343)
-
Addresses can now be explicitly cast to u256. For example:
fn f(addr: address) -> u256: return u256(addr)
(#621)
-
A special function named
__call__
can now be defined in contracts.The body of this function will execute in place of the standard dispatcher when the contract is called.
example (with intrinsics):
contract Foo: pub fn __call__(self): unsafe: if __calldataload(0) == 1: __revert(0, 0) else: __return(0, 0)
(#622)
Bugfixes
-
Fixed a crash that happend when using a certain unprintable ASCII char (#551)
-
The argument to
revert
wasn't being lowered by the compiler, meaning that somerevert
calls would cause a compiler panic in later stages. For example:const BAD_MOJO: u256 = 0xdeaddead struct Error: code: u256 fn fail(): revert Error(code = BAD_MOJO)
(#619)
-
Fixed a regression where an empty list expression (
[]
) would lead to a compiler crash. (#623) -
Fixed a bug where int array elements were not sign extended in their ABI encodings. (#633)
0.12.0-alpha (2021-12-31)## 0.12.0-alpha (2021-12-31)
Features
-
Added unsafe low-level "intrinsic" functions, that perform raw evm operations. For example:
fn foo(): unsafe: __mtore(0, 5000) assert __mload(0) == 5000
The functions available are exactly those defined in yul's "evm dialect": https://docs.soliditylang.org/en/v0.8.11/yul.html#evm-dialect but with a double-underscore prefix. Eg
selfdestruct
->__selfdestruct
.These are intended to be used for implementing basic standard library functionality, and shouldn't typically be needed in normal contract code.
Note: some intrinsic functions don't return a value (eg
__log0
); using these functions in a context that assumes a return value of unit type (eglet x: () = __log0(a, b)
) will currently result in a compiler panic in the yul compilation phase. (#603) -
Added an out of bounds check for accessing array items. If an array index is retrieved at an index that is not within the bounds of the array it now reverts with
Panic(0x32)
. (#606)
Bugfixes
-
Ensure ternary expression short circuit.
Example:
contract Foo: pub fn bar(input: u256) -> u256: return 1 if input > 5 else revert_me() fn revert_me() -> u256: revert return 0
Previous to this change, the code above would always revert no matter which branch of the ternary expressions it would resolve to. That is because both sides were evaluated and then one side was discarded. With this change, only the branch that doesn't get picked won't get evaluated at all.
The same is true for the boolean operations
and
andor
. (#488)
Internal Changes - for Fe Contributors
-
Added a globally available dummy std lib.
This library contains a single
get_42
function, which can be called usingstd::get_42()
. Once low-level intrinsics have been added to the language, we can deleteget_42
and start adding useful code. (#601)
0.11.0-alpha "Karlite" (2021-12-02)
Features
-
Added support for multi-file inputs.
Implementation details:
Mostly copied Rust's crate system, but use the the term ingot instead of crate.
Below is an example of an ingot's file tree, as supported by the current implementation.
`-- basic_ingot `-- src |-- bar | `-- baz.fe |-- bing.fe |-- ding | |-- dang.fe | `-- dong.fe `-- main.fe
There are still a few features that will be worked on over the coming months:
- source files accompanying each directory module (e.g.
my_mod.fe
) - configuration files and the ability to create library ingots
- test directories
- module-level
pub
modifier (all items in a module are public) mod
statements (all fe files in the input tree are public modules)
These things will be implemented in order of importance over the next few months. (#562)
- source files accompanying each directory module (e.g.
-
The syntax for array types has changed to match other generic types. For example,
u8[4]
is now writtenArray<u8, 4>
. (#571) -
Functions can now be defined on struct types. Example:
struct Point: x: u64 y: u64 # Doesn't take `self`. Callable as `Point.origin()`. # Note that the syntax for this will soon be changed to `Point::origin()`. pub fn origin() -> Point: return Point(x=0, y=0) # Takes `self`. Callable on a value of type `Point`. pub fn translate(self, x: u64, y: u64): self.x += x self.y += y pub fn add(self, other: Point) -> Point: let x: u64 = self.x + other.x let y: u64 = self.y + other.y return Point(x, y) pub fn hash(self) -> u256: return keccak256(self.abi_encode()) pub fn do_pointy_things(): let p1: Point = Point.origin() p1.translate(5, 10) let p2: Point = Point(x=1, y=2) let p3: Point = p1.add(p2) assert p3.x == 6 and p3.y == 12
(#577)
Bugfixes
-
Fixed a rare compiler crash.
Example:
let my_array: i256[1] = [-1 << 1]
Previous to this fix, the given example would lead to an ICE. (#550)
-
Contracts can now
create
an instance of a contract defined later in a file. This issue was caused by a weakness in the way we generated yul. (#596)
Internal Changes - for Fe Contributors
-
File IDs are now attached to
Span
s. (#587) -
The fe analyzer now builds a dependency graph of source code "items" (functions, contracts, structs, etc). This is used in the yulgen phase to determine which items are needed in the yul (intermediate representation) output. Note that the yul output is still cluttered with utility functions that may or may not be needed by a given contract. These utility functions are defined in the yulgen phase and aren't tracked in the dependency graph, so it's not yet possible to filter out the unused functions. We plan to move the definition of many of these utility functions into fe; when this happens they'll become part of the dependency graph and will only be included in the yul output when needed.
The dependency graph will also enable future analyzer warnings about unused code. (#596)
0.10.0-alpha (2021-10-31)
Features
-
Support for module level constants for base types
Example:
const TEN = 10 contract pub fn do_moon_math(self) -> u256: return 4711 * TEN
The values of base type constants are always inlined. (#192)
-
Encode revert errors for ABI decoding as
Error(0x103)
notPanic(0x99)
(#492) -
Replaced
import
statements withuse
statements.Example:
use foo::{bar::*, baz as baz26}
Note: this only adds support for parsing
use
statements. (#547) -
Functions can no be defined outside of contracts. Example:
fn add_bonus(x: u256) -> u256: return x + 10 contract PointTracker: points: Map<address, u256> pub fn add_points(self, user: address, val: u256): self.points[user] += add_bonus(val)
(#566)
-
Implemented a
send_value(to: address, value_in_wei: u256)
function.The function is similar to the
sendValue
function by OpenZeppelin with the differences being that:-
It reverts with
Error(0x100)
instead ofError("Address: insufficient balance")
to safe more gas. -
It uses
selfbalance()
instead ofbalance(address())
to safe more gas -
It reverts with
Error(0x101)
instead ofError("Address: unable to send value, recipient may have reverted")
also to safe more gas. (#567)
-
-
Added support for
unsafe
functions andunsafe
blocks within functions. Note that there's currently no functionality within Fe that requires the use ofunsafe
, but we plan to add built-inunsafe
functions that perform raw evm operations which will only callable within anunsafe
block or function. (#569) -
Added
balance()
andbalance_of(account: address)
methods. (#572) -
Added support for explicit casting between numeric types.
Example:
let a: i8 = i8(-1) let a1: i16 = i16(a) let a2: u16 = u16(a1) assert a2 == u16(65535) let b: i8 = i8(-1) let b1: u8 = u8(b) let b2: u16 = u16(b1) assert b2 == u16(255)
Notice that Fe allows casting between any two numeric types but does not allow to change both the sign and the size of the type in one step as that would leave room for ambiguity as the example above demonstrates. (#576)
Bugfixes
-
Adjust numeric values loaded from memory or storage
Previous to this fix numeric values that were loaded from either memory or storage were not properly loaded on the stack which could result in numeric values not treated as intended.
Example:
contract Foo: pub fn bar() -> i8: let in_memory: i8[1] = [-3] return in_memory[0]
In the example above
bar()
would not return-3
but253
instead. (#524) -
Propagate reverts from external contract calls.
Before this fix the following code to
should_revert()
orshould_revert2()
would succeed even though it clearly should not.contract A: contract_b: B pub fn __init__(contract_b: address): self.contract_b = B(contract_b) pub fn should_revert(): self.contract_b.fail() pub fn should_revert2(): self.contract_b.fail_with_custom_error() struct SomeError: pass contract B: pub fn fail(): revert pub fn fail_with_custom_error(): revert SomeError()
With this fix the revert errors are properly passed upwards the call hierachy. (#574)
-
Fixed bug in left shift operation.
Example:
Let's consider the value
1
as anu8
which is represented as the following 256 bit item on the EVM stack00..|00000001|
. A left shift of8
bits (val << 8
) turns that into00..01|00000000|
.Previous to this fix this resulted in the compiler taking
256
as the value for theu8
when clearly256
is not even in the range ofu8
anymore. With this fix the left shift operations was fixed to properly "clean up" the result of the shift so that00..01|00000000|
turns into00..00|00000000|
. (#575) -
Ensure negation is checked and reverts with over/underflow if needed.
Example:
The minimum value for an
i8
is-128
but the maximum value of ani8
is127
which means that negating-128
should lead to an overflow since128
does not fit into ani8
. Before this fix, negation operations where not checked for over/underflow resulting in returning the oversized value. (#578)
Internal Changes - for Fe Contributors
-
In the analysis stage, all name resolution (of variable names, function names, type names, etc used in code) now happens via a single
resolve_name
pathway, so we can catch more cases of name collisions and log more helpful error messages. (#555) -
Added a new category of tests: differential contract testing.
Each of these tests is pased on a pair of contracts where one implementation is written in Fe and the other one is written in Solidity. The implementations should have the same public APIs and are assumed to always return identical results given equal inputs. The inputs are randomly generated using
proptest
and hence are expected to discover unknown bugs. (#578)
0.9.0-alpha (2021-09-29)
Features
-
The
self
variable is no longer implicitly defined in code blocks. It must now be declared as the first parameter in a function signature.Example:
contract Foo: my_stored_num: u256 pub fn bar(self, my_num: u256): self.my_stored_num = my_num pub fn baz(self): self.bar(my_pure_func()) pub fn my_pure_func() -> u256: return 42 + 26
(#520)
-
The analyzer now disallows defining a type, variable, or function whose name conflicts with a built-in type, function, or object.
Example:
error: type name conflicts with built-in type ┌─ compile_errors/shadow_builtin_type.fe:1:6 │ 1 │ type u256 = u8 │ ^^^^ `u256` is a built-in type
(#539)
Bugfixes
- Fixed cases where the analyzer would correctly reject code, but would panic instead of logging an error message. (#534)
- Non-fatal parser errors (eg missing parentheses when defining a function that takes no arguments:
fn foo:
) are no longer ignored if the semantic analysis stage succeeds. (#535) - Fixed issue #531 by adding a
$
to the front of lowered tuple names. (#546)
Internal Changes - for Fe Contributors
- Implemented pretty printing of Fe AST. (#540)
0.8.0-alpha "Haxonite" (2021-08-31)
Features
-
Support quotes, tabs and carriage returns in string literals and otherwise restrict string literals to the printable subset of the ASCII table. (#329)
-
The analyzer now uses a query-based system, which fixes some shortcomings of the previous implementation.
- Types can now refer to other types defined later in the file. Example:
type Posts = Map<PostId, PostBody> type PostId = u256 type PostBody = String<140>
- Duplicate definition errors now show the location of the original definition.
- The analysis of each function, type definition, etc happens independently, so an error in one doesn't stop the analysis pass. This means fe can report more user errors in a single run of the compiler. (#468)
-
Function definitions are now denoted with the keyword fn instead of def. (#496)
-
Variable declarations are now preceded by the
let
keyword. Example:let x: u8 = 1
. (#509) -
Implemented support for numeric unary invert operator (
~
) (#526)
Bugfixes
-
Calling
self.__init__()
now results in a nice error instead of a panic in the yul compilation stage. (#468) -
Fixed an issue where certain expressions were not being moved to the correct location. (#493)
-
Fixed an issue with a missing return statement not properly detected.
Previous to this fix, the following code compiles but it should not:
contract Foo: pub fn bar(val: u256) -> u256: if val > 1: return 5
With this change, the compiler rightfully detects that the code is missing a
return
orrevert
statement after theif
statement since it is not guaranteed that the path of execution always follows the arm of theif
statement. (#497) -
Fixed a bug in the analyzer which allowed tuple item accessor names with a leading 0, resulting in an internal compiler error in a later pass. Example:
my_tuple.item001
. These are now rejected with an error message. (#510) -
Check call argument labels for function calls.
Previously the compiler would not check any labels that were used when making function calls on
self
or external contracts.This can be especially problematic if gives developers the impression that they could apply function arguments in any order as long as they are named which is not the case.
contract Foo: pub fn baz(): self.bar(val2=1, doesnt_even_exist=2) pub fn bar(val1: u256, val2: u256): pass
Code as the one above is now rightfully rejected by the compiler. (#517)
Improved Documentation
-
Various improvements and bug fixes to both the content and layout of the specification. (#489)
-
Document all remaining statements and expressions in the spec.
Also added a CI check to ensure code examples in the documentation are validated against the latest compiler. (#514)
Internal Changes - for Fe Contributors
- Separated Fe type traits between crates. (#485)
0.7.0-alpha "Galaxite" (2021-07-27)
Features
-
Enable the optimizer by default. The optimizer can still be disabled by supplying
--optimize=false
as an argument. (#439) -
The following checks are now performed while decoding data:
- The size of the encoded data fits within the size range known at compile-time.
- Values are correctly padded.
- unsigned integers, addresses, and bools are checked to have correct left zero padding
- the size of signed integers are checked
- bytes and strings are checked to have correct right padding
- Data section offsets are consistent with the size of preceding values in the data section.
- The dynamic size of strings does not exceed their maximum size.
- The dynamic size of byte arrays (
u8[n]
) is equal to the size of the array. (#440)
-
Type aliases can now include tuples. Example:
type InternetPoints = (address, u256)
(#459)
-
Revert with custom errors
Example:
struct PlatformError: code: u256 pub fn do_something(): revert PlatformError(code=4711)
Error encoding follows Solidity which is based on EIP-838. This means that custom errors returned from Fe are fully compatible with Solidity. (#464)
-
- The builtin value
msg.sig
now has typeu256
. - Removed the
bytes[n]
type. The typeu8[n]
can be used in its placed and will be encoded as a dynamically-sized, but checked, bytes component. (#472)
- The builtin value
-
Encode certain reverts as panics.
With this change, the following reverts are encoded as
Panic(uint256)
with the following panic codes:0x01
: An assertion that failed and did not specify an error message0x11
: An arithmetic expression resulted in an over- or underflow0x12
: An arithmetic expression divided or modulo by zero
The panic codes are aligned with the panic codes that Solidity uses. (#476)
Bugfixes
-
Fixed a crash when trying to access an invalid attribute on a string.
Example:
contract Foo: pub fn foo(): "".does_not_exist
The above now yields a proper user error. (#444)
-
Ensure
String<N>
type is capitalized in error messages (#445) -
Fixed ICE when using a static string that spans over multiple lines.
Previous to this fix, the following code would lead to a compiler crash:
contract Foo: pub fn return_with_newline() -> String<16>: return "foo balu"
The above code now works as intended. (#448)
-
Fixed ICE when using a tuple declaration and specifying a non-tuple type. Fixed a second ICE when using a tuple declaration where the number of target items doesn't match the number of items in the declared type. (#469)
Internal Changes - for Fe Contributors
-
- Cleaned up ABI encoding internals.
- Improved yulc panic formatting. (#472)
0.6.0-alpha "Feldspar" (2021-06-10)
Features
-
Support for
pragma
statementExample:
pragma ^0.1.0
(#361) -
Add support for tuple destructuring
Example:
my_tuple: (u256, bool) = (42, true) (x, y): (u256, bool) = my_tuple
(#376)
-
- Call expression can now accept generic arguments
- Replace
stringN
toString<N>
Example:
s: String<10> = String<10>("HI")
(#379)
-
- Many analyzer errors now include helpful messages and underlined code.
- Event and struct constructor arguments must now be labeled and in the order specified in the definition.
- The analyzer now verifies that the left-hand side of an assignment is actually assignable. (#398)
-
Types of integer literal are now inferred, rather than defaulting to
u256
.contract C: fn f(x: u8) -> u16: y: u8 = 100 # had to use u8(100) before z: i8 = -129 # "literal out of range" error return 1000 # had to use `return u16(1000)` before fn g(): self.f(50)
Similar inference is done for empty array literals. Previously, empty array literals caused a compiler crash, because the array element type couldn't be determined.
contract C: fn f(xs: u8[10]): pass fn g(): self.f([])
(Note that array length mismatch is still a type error, so this code won't actually compile.) (#429)
-
The Map type name is now capitalized. Example:
contract GuestBook: guests: Map<address, String<100>>
(#431)
-
Convert all remaining errors to use the new advanced error reporting system (#432)
-
Analyzer throws an error if
__init__
is not public. (#435)
Internal Changes - for Fe Contributors
- Refactored front-end "not implemented" errors into analyzer errors and removed questionable variants. Any panic is now considered to be a bug. (#437)
0.5.0-alpha (2021-05-27)
Features
-
Add support for hexadecimal/octal/binary numeric literals.
Example:
value_hex: u256 = 0xff value_octal: u256 = 0o77 value_binary: u256 = 0b11
(#333)
-
Added support for list expressions.
Example:
values: u256[3] = [10, 20, 30] # or anywhere else where expressions can be used such as in a call sum: u256 = self.sum([10, 20, 30])
(#388)
-
Contracts, events, and structs can now be empty.
e.g.
event MyEvent: pass ... contract MyContract: pass ... struct MyStruct: pass
(#406)
-
External calls can now handle dynamically-sized return types. (#415)
Bugfixes
- The analyzer will return an error if a tuple attribute is not of the form
item<index>
. (#401)
Improved Documentation
- Created a landing page for Fe at https://fe-lang.org (#394)
- Provide a Quickstart chapter in Fe Guide (#403)
Internal Changes - for Fe Contributors
-
Using insta to validate Analyzer outputs. (#387)
-
Analyzer now disallows using
context.add_
methods to update attributes. (#392) -
()
now represents a distinct type internally called the unit type, instead of an empty tuple.The lowering pass now does the following: Valueless return statements are given a
()
value and functions without a return value are given explicit()
returns. (#406) -
Add CI check to ensure fragment files always end with a new line (#4711)
0.4.0-alpha (2021-04-28)
Features
-
Support for revert messages in assert statements
E.g
assert a == b, "my revert statement"
The provided string is abi-encoded as if it were a call to a function
Error(string)
. For example, the revert string"Not enough Ether provided."
returns the following hexadecimal as error return data:0x08c379a0 // Function selector for Error(string) 0x0000000000000000000000000000000000000000000000000000000000000020 // Data offset 0x000000000000000000000000000000000000000000000000000000000000001a // String length 0x4e6f7420656e6f7567682045746865722070726f76696465642e000000000000 // String data
(#288)
-
Added support for augmented assignments.
e.g.
contract Foo: pub fn add(a: u256, b: u256) -> u256: a += b return a pub fn sub(a: u256, b: u256) -> u256: a -= b return a pub fn mul(a: u256, b: u256) -> u256: a *= b return a pub fn div(a: u256, b: u256) -> u256: a /= b return a pub fn mod(a: u256, b: u256) -> u256: a %= b return a pub fn pow(a: u256, b: u256) -> u256: a **= b return a pub fn lshift(a: u8, b: u8) -> u8: a <<= b return a pub fn rshift(a: u8, b: u8) -> u8: a >>= b return a pub fn bit_or(a: u8, b: u8) -> u8: a |= b return a pub fn bit_xor(a: u8, b: u8) -> u8: a ^= b return a pub fn bit_and(a: u8, b: u8) -> u8: a &= b return a
(#338)
-
A new parser implementation, which provides more helpful error messages with fancy underlines and code context. (#346)
-
Added support for tuples with base type items.
e.g.
contract Foo: my_num: u256 pub fn bar(my_num: u256, my_bool: bool) -> (u256, bool): my_tuple: (u256, bool) = (my_num, my_bool) self.my_num = my_tuple.item0 return my_tuple
(#352)
Bugfixes
-
Properly reject invalid emit (#211)
-
Properly tokenize numeric literals when they start with 0 (#331)
-
Reject non-string assert reasons as type error (#335)
-
Properly reject code that creates a circular dependency when using
create
orcreate2
.Example, the following code is now rightfully rejected because it tries to create an instance of
Foo
from within theFoo
contract itself.contract Foo: pub fn bar()->address: foo:Foo=Foo.create(0) return address(foo)
(#362)
Internal Changes - for Fe Contributors
- AST nodes use
String
s instead of&str
s. This way we can perform incremental compilation on the AST. (#332) - Added support for running tests against solidity fixtures. Also added tests that cover how solidity encodes revert reason strings. (#342)
- Refactoring of binary operation type checking. (#347)
0.3.0-alpha "Calamine" (2021-03-24)
Features
-
Add over/underflow checks for multiplications of all integers (#271)
-
Add full support for empty Tuples. (#276)
All functions in Fe implicitly return an empty Tuple if they have no other return value. However, before this change one was not able to use the empty Tuple syntax
()
explicitly.With this change, all of these are treated equally:
contract Foo: pub fn explicit_return_a1(): return pub fn explicit_return_a2(): return () pub fn explicit_return_b1() ->(): return pub fn explicit_return_b2() ->(): return () pub fn implicit_a1(): pass pub fn implicit_a2() ->(): pass
-
The JSON ABI builder now supports structs as both input and output. (#296)
-
Make subsequently defined contracts visible.
Before this change:
# can't see Bar contract Foo: ... # can see Foo contract Bar: ...
With this change the restriction is lifted and the following becomes possible. (#298)
contract Foo: bar: Bar pub fn external_bar() -> u256: return self.bar.bar() contract Bar: foo: Foo pub fn external_foo() -> u256: return self.foo.foo()
-
Perform checks for divison operations on integers (#308)
-
Support for msg.sig to read the function identifier. (#311)
-
Perform checks for modulo operations on integers (#312)
-
Perform over/underflow checks for exponentiation operations on integers (#313)
Bugfixes
-
Properly reject
emit
not followed by an event invocation (#212) -
Properly reject octal number literals (#222)
-
Properly reject code that tries to emit a non-existing event. (#250)
Example that now produces a compile time error:
emit DoesNotExist()
-
Contracts that create other contracts can now include
__init__
functions.See https://github.com/ethereum/fe/issues/284 (#304)
-
Prevent multiple types with same name in one module. (#317)
Examples that now produce compile time errors:
type bar = u8 type bar = u16
or
struct SomeStruct: some_field: u8 struct SomeStruct: other: u8
or
contract SomeContract: some_field: u8 contract SomeContract: other: u8
Prevent multiple fields with same name in one struct.
Example that now produces a compile time error:
struct SomeStruct: some_field: u8 some_field: u8
Prevent variable definition in child scope when name already taken in parent scope.
Example that now produces a compile time error:
pub fn bar(): my_array: u256[3] sum: u256 = 0 for i in my_array: sum: u256 = 0
-
The CLI was using the overwrite flag to enable Yul optimization.
i.e.
# Would both overwrite output files and run the Yul optimizer. $ fe my_contract.fe --overwrite
Using the overwrite flag now only overwrites and optimization is enabled with the optimize flag. (#320)
-
Ensure analyzer rejects code that uses return values for
__init__
functions. (#323)An example that now produces a compile time error:
contract C: pub fn __init__() -> i32: return 0
-
Properly reject calling an undefined function on an external contract (#324)
Internal Changes - for Fe Contributors
- Added the Uniswap demo contracts to our testing fixtures and validated their behaviour. (#179)
- IDs added to AST nodes. (#315)
- Failures in the Yul generation phase now panic; any failure is a bug. (#327)
0.2.0-alpha "Borax" (2021-02-27)
Features
-
Add support for string literals.
Example:
fn get_ticker_symbol() -> string3: return "ETH"
String literals are stored in and loaded from the compiled bytecode. (#186)
-
The CLI now compiles every contract in a module, not just the first one. (#197)
Sample compiler output with all targets enabled:
output |-- Bar | |-- Bar.bin | |-- Bar_abi.json | `-- Bar_ir.yul |-- Foo | |-- Foo.bin | |-- Foo_abi.json | `-- Foo_ir.yul |-- module.ast `-- module.tokens
-
Add support for string type casts (#201)
Example:
val: string100 = string100("foo")
-
Add basic support for structs. (#203)
Example:
struct House: price: u256 size: u256 vacant: bool contract City: pub fn get_price() -> u256: building: House = House(300, 500, true) assert building.size == 500 assert building.price == 300 assert building.vacant return building.price
-
Added support for external contract calls. Contract definitions now add a type to the module scope, which may be used to create contract values with the contract's public functions as callable attributes. (#204)
Example:
contract Foo: pub fn build_array(a: u256, b: u256) -> u256[3]: my_array: u256[3] my_array[0] = a my_array[1] = a * b my_array[2] = b return my_array contract FooProxy: pub fn call_build_array( foo_address: address, a: u256, b: u256, ) -> u256[3]: foo: Foo = Foo(foo_address) return foo.build_array(a, b)
-
Add support for
block
,msg
,chain
, andtx
properties: (#208)block.coinbase: address block.difficulty: u256 block.number: u256 block.timestamp: u256 chain.id: u256 msg.value: u256 tx.gas_price: u256 tx.origin: address
(Note that
msg.sender: address
was added previously.)Example:
fn post_fork() -> bool: return block.number > 2675000
-
The CLI now panics if an error is encountered during Yul compilation. (#218)
-
Support for contract creations.
Example of
create2
, which takes avalue
and addresssalt
as parameters.contract Foo: pub fn get_my_num() -> u256: return 42 contract FooFactory: pub fn create2_foo() -> address: # value and salt foo: Foo = Foo.create2(0, 52) return address(foo)
Example of
create
, which just takes avalue
parameter.contract Foo: pub fn get_my_num() -> u256: return 42 contract FooFactory: pub fn create_foo() -> address: # value and salt foo: Foo = Foo.create(0) return address(foo)
Note: We do not yet support init parameters. (#239)
-
Support updating individual struct fields in storage. (#246)
Example:
pub fn update_house_price(price: u256): self.my_house.price = price
-
Implement global
keccak256
method. The method expects one parameter ofbytes[n]
and returns the hash as anu256
. In a future versionkeccak256
will most likely be moved behind an import so that it has to be imported (e.g.from std.crypto import keccak256
). (#255)Example:
pub fn hash_single_byte(val: bytes[1]) -> u256: return keccak256(val)
-
Require structs to be initialized using keyword arguments.
Example:
struct House: vacant: bool price: u256
Previously,
House
could be instantiated asHouse(true, 1000000)
. With this change it is required to be instantiated likeHouse(vacant=true, price=1000000)
This ensures property assignment is less prone to get mixed up. It also makes struct initialization visually stand out more from function calls. (#260)
-
Implement support for boolean
not
operator. (#264)Example:
if not covid_test.is_positive(person): allow_boarding(person)
-
Do over/underflow checks for additions (SafeMath).
With this change all additions (e.g
x + y
) for signed and unsigned integers check for over- and underflows and revert if necessary. (#265) -
Added a builtin function
abi_encode()
that can be used to encode structs. The return type is a fixed-size array of bytes that is equal in size to the encoding. The type system does not support dynamically-sized arrays yet, which is why we used fixed. (#266)Example:
struct House: price: u256 size: u256 rooms: u8 vacant: bool contract Foo: pub fn hashed_house() -> u256: house: House = House( price=300, size=500, rooms=u8(20), vacant=true ) return keccak256(house.abi_encode())
-
Perform over/underflow checks for subtractions (SafeMath). (#267)
With this change all subtractions (e.g
x - y
) for signed and unsigned integers check for over- and underflows and revert if necessary. -
Support for the boolean operations
and
andor
. (#270)Examples:
contract Foo: pub fn bar(x: bool, y: bool) -> bool: return x and y
contract Foo: pub fn bar(x: bool, y: bool) -> bool: return x or y
Support for
self.address
.This expression returns the address of the current contract.
Example:
contract Foo: pub fn bar() -> address: return self.address
Bugfixes
-
Perform type checking when calling event constructors
Previously, the following would not raise an error even though it should:
contract Foo: event MyEvent: val_1: string100 val_2: u8 pub fn foo(): emit MyEvent("foo", 1000)
Wit this change, the code fails with a type error as expected. (#202)
-
Fix bug where compilation of contracts without public functions would result in illegal YUL. (#219)
E.g without this change, the following doesn't compile to proper YUL
contract Empty: lonely: u256
-
Ensure numeric literals can't exceed 256 bit range. Previously, this would result in a non user friendly error at the YUL compilation stage. With this change it is caught at the analyzer stage and presented to the user as a regular error. (#225)
-
Fix crash when return is used without value.
These two methods should both be treated as returning
()
pub fn explicit_return(): return pub fn implicit(): pass
Without this change, the
explicit_return
crashes the compiler. (#261)
Internal Changes - for Fe Contributors
- Renamed the fe-semantics library to fe-analyzer. (#207)
- Runtime testing utilities. (#243)
- Values are stored more efficiently in storage. (#251)
0.1.0-alpha "Amethyst" (2021-01-20)
WARNING: This is an alpha version to share the development progress with developers and enthusiasts. It is NOT yet intended to be used for anything serious. At this point Fe is missing a lot of features and has a lot of bugs instead.
This is the first alpha release and kicks off our release schedule which will be one release every month in the future. Since we have just started tracking progress on changes, the following list of changes is incomplete, but will appropriately document progress between releases from now on.
Features
-
Added support for
for loop
, allows iteration over static arrays. (#134) -
Enforce bounds on numeric literals in type constructors.
For instance calling
u8(1000)
ori8(-250)
will give an error because the literals1000
and-250
do not fit intou8
ori8
. (#145) -
Added builtin copying methods
clone()
andto_mem()
to reference types. (#155)usage:
# copy a segment of storage into memory and assign the new pointer my_mem_array = self.my_sto_array.to_mem() # copy a segment of memory into another segment of memory and assign the new pointer my_other_mem_array = my_mem_array.clone()
-
Support emitting JSON ABI via
--emit abi
. The default value of--emit
is nowabi,bytecode
. (#160) -
Ensure integer type constructor reject all expressions that aren't a numeric literal. For instance, previously the compiler would not reject the following code even though it could not be guaranteed that
val
would fit into anu16
.pub fn bar(val: u8) -> u16: return u16(val)
Now such code is rejected and integer type constructor do only work with numeric literals such as
1
or-3
. (#163) -
Support for ABI decoding of all array type. (#172)
-
Support for value assignments in declaration.
Previously, this code would fail:
another_reference: u256[10] = my_array
As a workaround declaration and assignment could be split apart.
another_reference: u256[10] another_reference = my_array
With this change, the shorter declaration with assignment syntax is supported. (#173)
Improved Documentation
- Point to examples in the README (#162)
- Overhaul README page to better reflect the current state of the project. (#177)
- Added descriptions of the
to_mem
andclone
functions to the spec. (#195)
Internal Changes - for Fe Contributors
- Updated the Solidity backend to v0.8.0. (#169)
- Run CI tests on Mac and support creating Mac binaries for releases. (#178)
Contributor Covenant Code of Conduct
Our Pledge
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
Our Standards
Examples of behavior that contributes to a positive environment for our community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
- Focusing on what is best not just for us as individuals, but for the overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
Scope
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at piper@pipermerriam.com. All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
1. Correction
Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
Consequence: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
2. Warning
Community Impact: A violation through a single incident or series of actions.
Consequence: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
3. Temporary Ban
Community Impact: A serious violation of community standards, including sustained inappropriate behavior.
Consequence: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
4. Permanent Ban
Community Impact: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
Consequence: A permanent ban from any sort of public interaction within the community.
Attribution
This Code of Conduct is adapted from the Contributor Covenant, version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by Mozilla's code of conduct enforcement ladder.
For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.