├── .gitignore ├── BACKGROUND.md ├── LICENSE ├── README.md ├── contracts ├── access │ └── OwnedClaimable.sol ├── token │ ├── ERC20.sol │ ├── ERC721 │ │ ├── ERC721.sol │ │ └── IERC721TokenReceiver.sol │ └── WFIL.sol └── utils │ ├── CallNative.sol │ └── FilAddress.sol ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules -------------------------------------------------------------------------------- /BACKGROUND.md: -------------------------------------------------------------------------------- 1 | ## Background Info 2 | 3 | The FEVM extends Filecoin's FVM to support Ethereum-style accounts and smart contracts. For the most part, the FEVM and EVM are very similar. However, there are a few key differences one should consider before blindly redeploying EVM-native smart contracts to Filecoin. This section details crucial background information about the FEVM to keep in mind when deploying to Filecoin. 4 | 5 | First, a quick glossary. I will use the following terms: 6 | 7 | ***Actors*** are blocks of Rust code compiled to wasm bytecode. These exist at distinct addresses in Filecoin, and carry out certain functions. 8 | 9 | When you deploy a Solidity smart contract to Filecoin, the FVM deploys a new EVM actor for you, assigns it a new address, and stores the EVM bytecode in the EVM actor's state. Like all actors, the EVM actor is written in rust, and contains an implementation of an EVM interpreter. Basically, it runs the contract's bytecode when invoked. 10 | 11 | ***EVM-type actor*** will refer to actors that have counterparts on Ethereum. Specifically: 12 | 13 | * ***Eth contract*** will refer to a smart contract deployed to Filecoin. Again, these are represented internally as "EVM actors," but for sanity's sake I'm going to call them Eth contracts. 14 | * ***Eth account*** will refer to an Ethereum EOA account. These are directly analogous to Ethereum's EOAs and have very similar properties. 15 | 16 | ***Filecoin-native actor*** or ***non-EVM actor*** will refer to actors that do not exist in Ethereum. This can refer to EOAs like BLS/SECPK actors, as well as builtin contract-like actors like the Miner, Multisig, and Market actors. 17 | 18 | --- 19 | 20 | ### 1. Gas 21 | 22 | FEVM execution gas is completely different than EVM gas. The FEVM is embedded into a larger wasm runtime, which supports running code from both EVM-type actors, as well as Filecoin-native actors. All actors (including EVM-type actors) are written in rust, compiled to wasm bytecode. This bytecode is injected with gas metering that allows the Filecoin runtime to consume execution gas as the wasm executes. 23 | 24 | Rather than come up with a conversion between the EVM's gas-per-instruction and Filecoin's metered wasm, the FEVM has abandoned the EVM's gas semantics in favor of using the existing metered wasm to handle execution gas. As a result, the numerical values required for FEVM execution will look very different from EVM gas values. 25 | 26 | Additional quirks of FEVM gas: 27 | 28 | * The gas required to execute an Eth contract depends in small part on its bytecode size, as the first operation performed is to load the bytecode from state. 29 | * The `INVALID` opcode does NOT consume all available gas as it does in Ethereum. In the FEVM, it behaves identically to `REVERT`, except that it cannot return data. 30 | * When calling precompiles, the passed in gas value is ignored. Execution will not consume more gas than you have available, but it is NOT possible to restrict the gas consumed by a precompile. 31 | 32 | ### 2. Addresses 33 | 34 | The FEVM does not exist in isolation. On Ethereum, smart contracts and EOAs only ever call (or are called by) other smart contracts and EOAs. In the FEVM, Eth contracts and accounts can interact with each other, but they can also interact with Filecoin-native actors. 35 | 36 | Eth contracts must support these other actor types in order to be fully compatible, as well as to avoid unintended issues. The most important information to understand is that Filecoin has multiple address types. With the addition of the FEVM, Filecoin has 5 different native address types: 37 | 38 | * *f0 - ID address*: On creation, actors are assigned a sequential `uint64` actor id. Every actor (including Eth contracts and accounts) will have an actor id. 39 | * *f1 - SECP256K1 address*: 20 byte pubkey hashes that correspond to SECP256K1 EOAs. 40 | * *f2 - Actor address*: 20 byte hashes used as a "reorg-safe" address scheme. All actors have an f2 address. 41 | * *f3 - BLS address*: 48 byte pubkeys that correspond to BLS EOAs. 42 | * *f4 - Delegated address*: An extensible addressing format that is currently only used by Eth contracts and accounts. See [FIP-0048](https://github.com/filecoin-project/FIPs/blob/master/FIPS/fip-0048.md) and [FIP-0055](https://github.com/filecoin-project/FIPs/blob/master/FIPS/fip-0055.md) for more info. The f4 format will likely be expanded in future upgrades to incorporate other actor types. 43 | 44 | For the most part, Filecoin's native actors can be referenced by their actor id (f0 address). Actor ids are just `uint64` values, which means they are small enough to fit within Solidity's 20-byte `address` type. To provide support for non-EVM actors as first-class citizens, the FEVM recognizes a special format for ID addresses that fits into the 20-byte Solidity `address` type. 45 | 46 | The format is `0xff`, followed by 11 empty bytes, followed by the `uint64` actor id. Here are two examples: 47 | 48 | ```solidity 49 | // A Solidity address that contains the id "5" looks like: 50 | address constant ID_ADDRESS_FIVE = 0xff00000000000000000000000000000000000005; 51 | // The largest possible actor id (uint64.max) looks like: 52 | address constant MAX_ID_ADDRESS = 0xFf0000000000000000000000FFfFFFFfFfFffFfF; 53 | ``` 54 | 55 | For the most part, you can use ID addresses to refer to non-EVM actors. **However, ALWAYS keep in mind** that Eth contracts and accounts have BOTH a standard Ethereum address, AND an ID address. The following details the behavior of various EVM operations in relation to these different actors and address formats. 56 | 57 | #### Calling other actors 58 | 59 | 1. *Eth contract calls an EVM-type actor*: BOTH the Ethereum-style address and ID address function the exact same way. i.e. whether your contract is calling an Eth contract or Eth account, the address format used doesn't matter. Here's an example: 60 | 61 | ```solidity 62 | using FilAddress for *; 63 | 64 | interface ERC20 { 65 | function transfer(address, uint) public returns (bool); 66 | } 67 | 68 | address constant SOME_ETH_CONTRACT = address(0xc6e2459991BfE27cca6d86722F35da23A1E4Cb97); 69 | 70 | // Call ERC20.transfer on the Eth contract 71 | function doCallA() public { 72 | ERC20(SOME_ETH_CONTRACT).transfer(msg.sender, 100); 73 | } 74 | 75 | // Call ERC20.transfer on the Eth contract, except target its id address 76 | // Note that the call is performed identically! Under the hood, the runtime 77 | // is converting the Eth format to its ID equivalent before calling, anyway. 78 | // ... so, we can use either. 79 | function doCallB() public { 80 | // Get the ID address equivalent 81 | (bool success, uint64 actorID) = SOME_ETH_CONTRACT.getActorID(); 82 | require(success); 83 | address contractIDAddr = actorID.toIDAddress(); 84 | 85 | // Call ERC20.transfer - works the same as in doCallA! 86 | ERC20(contractIDAddr).transfer(msg.sender, 100); 87 | } 88 | ``` 89 | 90 | 2. *Eth contract calls a BLS/SECPK actor*: Use the ID address. These behave pretty much like an Eth account. Calls to these actors will always succeed (assuming sufficient gas, balance, stack depth, etc). 91 | 92 | ```solidity 93 | address constant SOME_ETH_ACCOUNT = address(0xc6e2459991BfE27cca6d86722F35da23A1E4Cb97); 94 | // The ID address of some BLS actor 95 | address constant SOME_BLS_ACCOUNT = address(0xff000000000000000000000000000000BEEFBEEF); 96 | 97 | // Call the Eth account and transfer some funds 98 | function doCallA() public { 99 | SOME_ETH_ACCOUNT.call{value: 100}(""); 100 | } 101 | 102 | // Call the BLS account and transfer some funds 103 | function doCallB() public { 104 | SOME_BLS_ACCOUNT.call{value: 100}(""); 105 | } 106 | ``` 107 | 108 | 3. *Eth contract calls another Filecoin-native actor*: Use the ID address. However, note that a plain call will only work for some actor types. Many actors (like the Miner actor) export methods that can be called by Eth contracts through a special FEVM precompile (either the `call_actor` or `call_actor_id` precompile). This library doesn't handle the specific interfaces these actors export. Take a look at [`Zondax/filecoin-solidity`](https://github.com/Zondax/filecoin-solidity/) if your contract needs to call native actor methods. 109 | 110 | #### Getting called by other actors (aka "who is `msg.sender`?") 111 | 112 | `msg.sender` will either be an ID address, or the standard Ethereum address format. You can use this information to help determine what actor type is calling your contract: 113 | 114 | * If `msg.sender` is in the standard Ethereum address format, you were called by an EVM-type actor (i.e. an Eth contract or account) 115 | * If `msg.sender` is an ID address, you were called by a non-EVM actor (i.e. some other Filecoin-native actor). 116 | 117 | #### Getting information about other actors 118 | 119 | * Eth contracts should have the same `EXTCODESIZE`, `EXTCODEHASH`, and `EXTCODECOPY` values as they do in Ethereum. Note, too, that you can check these with either the Ethereum address OR ID address. They behave the same. 120 | * Account-type actors (BLS, SECP, and Eth account actors) have: 121 | * `EXTCODESIZE == 0` 122 | * `EXTCODEHASH == keccak256("")` 123 | * `EXTCODECOPY` will copy zeroes 124 | * Nonexistent actors have: 125 | * `EXTCODESIZE == 0` 126 | * `EXTCODEHASH == 0` 127 | * `EXTCODECOPY` will copy zeroes 128 | * Other non-EVM actors have: 129 | * `EXTCODESIZE == 1` 130 | * `EXTCODEHASH == keccak256(abi.encodePacked(0xFE))` 131 | * `EXTCODECOPY` will copy 0xFE, then zeroes. 132 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2023 Alex Wade and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included 14 | in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## fevmate 2 | 3 | *Libraries, mixins, and other Solidity building blocks for use with Filecoin's FEVM.* 4 | 5 | This library borrows heavily from popular security-centric Solidity libraries like [solmate](https://github.com/transmissions11/solmate) and [OpenZeppelin](https://github.com/OpenZeppelin/openzeppelin-contracts), while including FEVM-specific tweaks to safely support Filecoin-native features. 6 | 7 | **Use these libraries at your own risk!** The FEVM is a brand new system and likely has many kinks that will be uncovered by real-world use. FEVM-specific design patterns will emerge over time as things break and are fixed. The contracts provided here are an attempt to safeguard against some of these, but are by no means complete. Do your own research, understand the system you're deploying to, test thoroughly, and above all else - be careful! 8 | 9 | This library is in heavy WIP as I extend support and testing for popular Solidity contracts. Note that a big TODO is to add testing for currently-implemented contracts. I've tested a lot of these manually, but am still working on unit testing. Please write your own tests if you end up using any of these. 10 | 11 | If you'd like me to consider adding support for your favorite Solidity library, please open an issue! The following contracts are finalized: 12 | 13 | **Access** 14 | 15 | * [`OwnedClaimable.sol`](./contracts/access/OwnedClaimable.sol): Ownable-style access control, implemented using a two-step role transferrance pattern as this should be safer and more future-proof in the FEVM. 16 | 17 | **Tokens** 18 | 19 | Standard token contracts, implemented using address normalization for token transfers and approvals, as well as balance and allowance queries. 20 | 21 | * [`ERC20.sol`](./contracts/token/ERC20.sol) 22 | * [`ERC721.sol`](./contracts/token/ERC721/ERC721.sol) 23 | * [`WFIL.sol`](./contracts/token/WFIL.sol): The version here is the canonical WFIL deployed by GLIF to Filecoin mainnet. Contract address: [`0x60E1773636CF5E4A227d9AC24F20fEca034ee25A`](https://filfox.info/en/address/0x60E1773636CF5E4A227d9AC24F20fEca034ee25A) 24 | 25 | **Utilities** 26 | 27 | * [`FilAddress.sol`](./contracts/utils/FilAddress.sol): Utilities for all things related to Solidity's `address` type. Helps implement address normalization, as well as convert between actor ids and evm addresses (and vice-versa). 28 | 29 | ### Usage 30 | 31 | `npm i fevmate` 32 | 33 | ... then import within your Solidity files! For example: 34 | 35 | ```solidity 36 | import "fevmate/contracts/utils/FilAddress.sol"; 37 | 38 | contract YourContract { 39 | 40 | using FilAddress for *; 41 | } 42 | ``` 43 | 44 | ### Tests 45 | 46 | The FEVM doesn't have a good option for running tests locally against the FEVM. Since the whole point of fevmate is to handle FEVM-specific behavior, it really needs to be fully tested on the FEVM, rather than partially tested with an existing EVM framework. 47 | 48 | This is going to remain a big TODO until a suitable test framework exists, but I've started writing basic tests in a forked version of the `ref-fvm` repo. You can find those tests here: [`wadealexc/ref-fvm/solidity-tests`](https://github.com/wadealexc/ref-fvm/tree/387de6febe6d2784c8f4ba538088cda5d8e3ff63/tools/solidity-tests). 49 | 50 | These are far from perfect, but at least allow me to test basic behavior. Actually, it seems there were some issues in FilAddress - hence the 1.0.2 release! 51 | 52 | ### Design Patterns 53 | 54 | ***This section assumes you have read [BACKGROUND.md](./BACKGROUND.md). If you haven't, please go do that.*** 55 | 56 | fevmate uses the following patterns: 57 | 58 | * *Address normalization* 59 | * *Two-step role transferrance* 60 | * *No hardcoded gas values* 61 | 62 | #### Address Normalization 63 | 64 | *TL;DR: When in doubt, use [`FilAddress.normalize`](./contracts/utils/FilAddress.sol#L43) on `address` input. If you take nothing else away from this document, please do this!* 65 | 66 | As a refresher, both Eth contracts and accounts have both a standard Ethereum address, as well as an id address. The two addresses can be used interchangably for `call`-type operations, as well as for `extcodesize/hash/copy`. 67 | 68 | However, when an EVM-type actor calls a contract, `msg.sender` is ALWAYS in the standard Ethereum format. 69 | 70 | To illustrate why this is such a big deal, let's use a minimalist ERC20 contract as an example: 71 | 72 | ```solidity 73 | pragma solidity ^0.8.17; 74 | 75 | contract SmolERC20 { 76 | 77 | mapping(address => uint) public balanceOf; 78 | 79 | // Transfer tokens to an account 80 | function transfer(address _to, uint _amt) public returns (bool) { 81 | balanceOf[msg.sender] -= _amt; 82 | balanceOf[_to] += _amt; 83 | return true; 84 | } 85 | } 86 | ``` 87 | 88 | Imagine a user with an Eth account is transferred tokens to their ID address. This may not seem like an issue, given that ID addresses behave the same in many situations - the user can give out their ID address to receive FIL, and ID addresses can be used to call Eth contracts and accounts. 89 | 90 | However, when the user calls transfer to move their tokens, they appear to have no balance! The contract uses `msg.sender` to look up their balance, which is NOT the ID address to which their tokens were transferred. The ID and Ethereum addresses may be equivalent in many places, but when an EVM-type actor calls an Eth contract, the `msg.sender` will always be their Ethereum address. 91 | 92 | One solution to this might be to reject token transfers to ID addresses. However, this prevents use of the contract by non-EVM actors, as BLS and SECPK actors MUST use the ID address format! 93 | 94 | --- 95 | 96 | Instead, contracts should *normalize address input wherever possible.* 97 | 98 | When your contract is given an address (for example, via function parameters), before you do anything with it - check if the address is in the ID format. If it is, try to convert it to a standard Eth format. The FEVM exposes a special precompile for this: `lookup_delegated_address` checks if an actor id has a corresponding f4 address. 99 | 100 | If you're not able to perform a conversion, you can use the address as-is; it may belong to a BLS/SECPK or other non-EVM actor. 101 | 102 | This library provides [`FilAddress.normalize`](./contracts/utils/FilAddress.sol#L43) as a convenience method for these operations, which performs a conversion if possible, and does nothing otherwise. 103 | 104 | --- 105 | 106 | Here's the same minimalist ERC20 contract, this time using address normalization: 107 | 108 | ```solidity 109 | pragma solidity ^0.8.17; 110 | 111 | import "fevmate/contracts/utils/FilAddress.sol"; 112 | 113 | contract SmolERC20 { 114 | 115 | using FilAddress for *; 116 | 117 | mapping(address => uint) balances; 118 | 119 | // Transfer tokens to an account 120 | function transfer(address _to, uint _amt) public returns (bool) { 121 | // Attempt to convert destination to Eth address 122 | // _to is unchanged if no conversion occurs 123 | _to = _to.normalize(); 124 | 125 | balances[msg.sender] -= _amt; 126 | balances[_to] += _amt; 127 | return true; 128 | } 129 | 130 | // Balance lookup should also normalize inputs 131 | function balanceOf(address _a) public view returns (uint) { 132 | return balances[_a.normalize()]; 133 | } 134 | } 135 | ``` 136 | 137 | In this version, if tokens are transferred to an ID address, the `normalize` method first checks to see if there is a corresponding Eth address. If there is, we use that instead. Otherwise, the address is returned unchanged. 138 | 139 | #### Two-step Role Transferrance 140 | 141 | Address normalization is one way to ensure Eth contracts and accounts have a canonical format in your smart contracts. 142 | 143 | Another good way is to ignore address formats entirely in favor of requiring the destination address to call into the contract. This isn't efficient or user-friendly enough for things like token transfers, but is a great method to ensure infrequently-performed operations are simple and future-proof. 144 | 145 | For example, the classic `Ownable.sol` role transfer pattern looks like this: 146 | 147 | ```solidity 148 | pragma solidity ^0.8.17; 149 | 150 | contract Ownable { 151 | 152 | address owner; 153 | 154 | function transferOwnership(address _newOwner) public { 155 | owner = _newOwner; 156 | } 157 | } 158 | ``` 159 | 160 | Following address normalization, we could just ensure that `_newOwner` is normalized before being assigned to the `owner` variable. And while this should work, it adds unnecessary complexity to a method that needs to function perfectly, the first time, forever. 161 | 162 | The primary property address normalization wants to enforce is that every address is resolved to its "`msg.sender` format." In essence, "can this `address` execute smart contract functions?" The downside of `normalize` is that it requires a liberal amount of assembly, and even calls a FEVM precompile to perform an address lookup. 163 | 164 | We can answer the same question without all the complexity, by making role transfers a two-step process. A role transfer first designates a "pending" user to receive the role. The transfer is only completed after the "pending" user calls the corresponding "accept" method. 165 | 166 | Address format isn't checked anywhere, but by ensuring the pending user can call the "accept" function, we know the address is in its "`msg.sender` format." Also, the huge decrease in complexity means this method should remain compatible with any future Filecoin network upgrade! 167 | 168 | This library provides [`OwnedClaimable.sol`](./contracts/access/OwnedClaimable.sol) as a mixin to help implement two-step role transfers. 169 | 170 | #### No hardcoded gas values 171 | 172 | When porting smart contracts to the FEVM, make sure that: 173 | 174 | * The code does NOT hardcode gas values anywhere. 175 | * The code does NOT use Solidity's `address.send` or `address.transfer` 176 | * The code does NOT rely on the `INVALID` opcode or precompile gas restriction described in [BACKGROUND.md](./BACKGROUND.md). 177 | 178 | In all cases, you should forward ALL gas to the callee. If you need reentrancy protection, use a `ReentrancyGuard` like the ones provided by [OpenZeppelin](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/ReentrancyGuard.sol) or [solmate](https://github.com/transmissions11/solmate/blob/main/src/utils/ReentrancyGuard.sol). 179 | -------------------------------------------------------------------------------- /contracts/access/OwnedClaimable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import "../utils/FilAddress.sol"; 5 | 6 | /** 7 | * @author fevmate (https://github.com/wadealexc/fevmate) 8 | * @notice Two-step owner transferrance mixin. Unlike many fevmate contracts, 9 | * no methods here normalize address inputs - so it is possible to transfer 10 | * ownership to an ID address. However, the acceptOwnership method enforces 11 | * that the pending owner address can actually be the msg.sender. 12 | * 13 | * This should mean it's possible for other Filecoin actor types to hold the 14 | * owner role - like BLS/SECP account actors. 15 | */ 16 | abstract contract OwnedClaimable { 17 | 18 | using FilAddress for *; 19 | 20 | error Unauthorized(); 21 | error InvalidAddress(); 22 | 23 | /*////////////////////////////////////// 24 | OWNER INFO 25 | //////////////////////////////////////*/ 26 | 27 | address public owner; 28 | address pendingOwner; 29 | 30 | /*////////////////////////////////////// 31 | EVENTS 32 | //////////////////////////////////////*/ 33 | 34 | event OwnershipTransferred(address indexed oldOwner, address indexed newOwner); 35 | event OwnershipPending(address indexed currentOwner, address indexed pendingOwner); 36 | 37 | /*////////////////////////////////////// 38 | CONSTRUCTOR 39 | //////////////////////////////////////*/ 40 | 41 | constructor(address _owner) { 42 | if (_owner == address(0)) revert InvalidAddress(); 43 | // normalize _owner to avoid setting an EVM actor's ID address as owner 44 | owner = _owner.normalize(); 45 | 46 | emit OwnershipTransferred(address(0), owner); 47 | } 48 | 49 | /*////////////////////////////////////// 50 | OWNABLE METHODS 51 | //////////////////////////////////////*/ 52 | 53 | modifier onlyOwner() virtual { 54 | if (msg.sender != owner) revert Unauthorized(); 55 | _; 56 | } 57 | 58 | /** 59 | * @notice Allows the current owner to revoke the owner role, locking 60 | * any onlyOwner functions. 61 | * 62 | * Note: this method requires that there is not currently a pending 63 | * owner. To revoke ownership while there is a pending owner, the 64 | * current owner must first set a new pending owner to address(0). 65 | * Alternatively, the pending owner can claim ownership and then 66 | * revoke it. 67 | */ 68 | function revokeOwnership() public virtual onlyOwner { 69 | if (pendingOwner != address(0)) revert Unauthorized(); 70 | owner = address(0); 71 | 72 | emit OwnershipTransferred(msg.sender, address(0)); 73 | } 74 | 75 | /** 76 | * @notice Works like most 2-step ownership transfer methods. The current 77 | * owner can call this to set a new pending owner. 78 | * 79 | * Note: the new owner address is NOT normalized - it is stored as-is. 80 | * This is safe, because the acceptOwnership method enforces that the 81 | * new owner can make a transaction as msg.sender. 82 | */ 83 | function transferOwnership(address _newOwner) public virtual onlyOwner { 84 | pendingOwner = _newOwner; 85 | 86 | emit OwnershipPending(msg.sender, _newOwner); 87 | } 88 | 89 | /** 90 | * @notice Used by the pending owner to accept the ownership transfer. 91 | * 92 | * Note: If this fails unexpectedly, check that the pendingOwner is not 93 | * an ID address. The pending owner address should match the pending 94 | * owner's msg.sender address. 95 | */ 96 | function acceptOwnership() public virtual { 97 | if (msg.sender != pendingOwner) revert Unauthorized(); 98 | 99 | // Transfer ownership and set pendingOwner to 0 100 | address oldOwner = owner; 101 | owner = msg.sender; 102 | delete pendingOwner; 103 | 104 | emit OwnershipTransferred(oldOwner, msg.sender); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /contracts/token/ERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import "../utils/FilAddress.sol"; 5 | 6 | /** 7 | * @author fevmate (https://github.com/wadealexc/fevmate) 8 | * @notice ERC20 mixin for the FEVM. This contract implements the ERC20 9 | * standard, with additional safety features for the FEVM. 10 | * 11 | * All methods attempt to normalize address input. This means that if 12 | * they are provided ID addresses as input, they will attempt to convert 13 | * these addresses to standard Eth addresses. 14 | * 15 | * This is an important consideration when developing on the FEVM, and 16 | * you can read about it more in the README. 17 | */ 18 | abstract contract ERC20 { 19 | 20 | using FilAddress for *; 21 | 22 | /*////////////////////////////////////// 23 | TOKEN INFO 24 | //////////////////////////////////////*/ 25 | 26 | string public name; 27 | string public symbol; 28 | uint8 public decimals; 29 | 30 | /*////////////////////////////////////// 31 | ERC-20 STORAGE 32 | //////////////////////////////////////*/ 33 | 34 | uint public totalSupply; 35 | 36 | mapping(address => uint) balances; 37 | mapping(address => mapping(address => uint)) allowances; 38 | 39 | /*////////////////////////////////////// 40 | EVENTS 41 | //////////////////////////////////////*/ 42 | 43 | event Transfer(address indexed from, address indexed to, uint256 amount); 44 | event Approval(address indexed owner, address indexed spender, uint256 amount); 45 | 46 | /*////////////////////////////////////// 47 | CONSTRUCTOR 48 | //////////////////////////////////////*/ 49 | 50 | constructor ( 51 | string memory _name, 52 | string memory _symbol, 53 | uint8 _decimals 54 | ) { 55 | name = _name; 56 | symbol = _symbol; 57 | decimals = _decimals; 58 | } 59 | 60 | /*////////////////////////////////////// 61 | ERC-20 METHODS 62 | //////////////////////////////////////*/ 63 | 64 | function transfer(address _to, uint _amount) public virtual returns (bool) { 65 | // Attempt to convert destination to Eth address 66 | _to = _to.normalize(); 67 | 68 | balances[msg.sender] -= _amount; 69 | balances[_to] += _amount; 70 | 71 | emit Transfer(msg.sender, _to, _amount); 72 | return true; 73 | } 74 | 75 | function transferFrom(address _owner, address _to, uint _amount) public virtual returns (bool) { 76 | // Attempt to convert owner and destination to Eth addresses 77 | _owner = _owner.normalize(); 78 | _to = _to.normalize(); 79 | 80 | // Reduce allowance for spender. If allowance is set to the 81 | // max value, we leave it alone. 82 | uint allowed = allowances[_owner][msg.sender]; 83 | if (allowed != type(uint).max) 84 | allowances[_owner][msg.sender] = allowed - _amount; 85 | 86 | balances[_owner] -= _amount; 87 | balances[_to] += _amount; 88 | 89 | emit Transfer(_owner, _to, _amount); 90 | return true; 91 | } 92 | 93 | function approve(address _spender, uint _amount) public virtual returns (bool) { 94 | // Attempt to convert spender to Eth address 95 | _spender = _spender.normalize(); 96 | 97 | allowances[msg.sender][_spender] = _amount; 98 | 99 | emit Approval(msg.sender, _spender, _amount); 100 | return true; 101 | } 102 | 103 | /*////////////////////////////////////// 104 | ERC-20 GETTERS 105 | //////////////////////////////////////*/ 106 | 107 | function balanceOf(address _a) public virtual view returns (uint) { 108 | return balances[_a.normalize()]; 109 | } 110 | 111 | function allowance(address _owner, address _spender) public virtual view returns (uint) { 112 | return allowances[_owner.normalize()][_spender.normalize()]; 113 | } 114 | 115 | /*////////////////////////////////////// 116 | MINT/BURN INTERNAL METHODS 117 | //////////////////////////////////////*/ 118 | 119 | function _mint(address _to, uint _amount) internal virtual { 120 | // Attempt to convert to Eth address 121 | _to = _to.normalize(); 122 | 123 | totalSupply += _amount; 124 | balances[_to] += _amount; 125 | 126 | emit Transfer(address(0), _to, _amount); 127 | } 128 | 129 | function _burn(address _from, uint _amount) internal virtual { 130 | // Attempt to convert to Eth address 131 | _from = _from.normalize(); 132 | 133 | balances[_from] -= _amount; 134 | totalSupply -= _amount; 135 | 136 | emit Transfer(_from, address(0), _amount); 137 | } 138 | } -------------------------------------------------------------------------------- /contracts/token/ERC721/ERC721.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import "../../utils/FilAddress.sol"; 5 | import "./IERC721TokenReceiver.sol"; 6 | 7 | /** 8 | * @author fevmate (https://github.com/wadealexc/fevmate) 9 | * @notice ERC721 mixin for the FEVM. This contract implements the ERC721 10 | * standard, with additional safety features for the FEVM. 11 | * 12 | * All methods attempt to normalize address input. This means that if 13 | * they are provided ID addresses as input, they will attempt to convert 14 | * these addresses to standard Eth addresses. 15 | * 16 | * This is an important consideration when developing on the FEVM, and 17 | * you can read about it more in the README. 18 | */ 19 | abstract contract ERC721 { 20 | 21 | using FilAddress for *; 22 | 23 | error Unauthorized(); 24 | error UnsafeReceiver(); 25 | error NullOwner(); 26 | 27 | /*////////////////////////////////////// 28 | TOKEN INFO 29 | //////////////////////////////////////*/ 30 | 31 | string public name; 32 | string public symbol; 33 | 34 | /*////////////////////////////////////// 35 | ERC-721 STORAGE 36 | //////////////////////////////////////*/ 37 | 38 | // Maps tokenId to owner address 39 | mapping(uint => address) tokenOwners; 40 | // Maps owner address to token count 41 | mapping(address => uint) ownerBalances; 42 | 43 | // Maps tokenId to approved address 44 | mapping(uint => address) tokenApprovals; 45 | // Maps owner address to operator approvals 46 | mapping(address => mapping(address => bool)) operatorApprovals; 47 | 48 | /*////////////////////////////////////// 49 | EVENTS 50 | //////////////////////////////////////*/ 51 | 52 | event Transfer(address indexed from, address indexed to, uint indexed tokenId); 53 | event Approval(address indexed owner, address indexed spender, uint indexed tokenId); 54 | event ApprovalForAll(address indexed owner, address indexed spender, bool isApproved); 55 | 56 | /*////////////////////////////////////// 57 | CONSTRUCTOR 58 | //////////////////////////////////////*/ 59 | 60 | constructor( 61 | string memory _name, 62 | string memory _symbol 63 | ) { 64 | name = _name; 65 | symbol = _symbol; 66 | } 67 | 68 | /*////////////////////////////////////// 69 | ERC-721 METHODS 70 | //////////////////////////////////////*/ 71 | 72 | function transferFrom(address _owner, address _to, uint _tokenId) public virtual { 73 | // Attempt to convert owner and destination to Eth addresses 74 | _owner = _owner.normalize(); 75 | _to = _to.normalize(); 76 | 77 | // Ensure the _owner is the owner of _tokenId, and 78 | // Ensure msg.sender is allowed to transfer _tokenId 79 | if ( 80 | _owner != ownerOf(_tokenId) || 81 | ( 82 | msg.sender != _owner && 83 | !isApprovedForAll(_owner, msg.sender) && 84 | msg.sender != getApproved(_tokenId) 85 | ) 86 | ) revert Unauthorized(); 87 | 88 | if (_to == address(0)) revert UnsafeReceiver(); 89 | 90 | unchecked { 91 | ownerBalances[_owner]--; 92 | ownerBalances[_to]++; 93 | } 94 | 95 | tokenOwners[_tokenId] = _to; 96 | delete tokenApprovals[_tokenId]; 97 | 98 | emit Transfer(_owner, _to, _tokenId); 99 | } 100 | 101 | function safeTransferFrom(address _owner, address _to, uint _tokenId) public virtual { 102 | // transferFrom will normalize input 103 | transferFrom(_owner, _to, _tokenId); 104 | 105 | // Check receiver. Only _owner needs to be normalized here, since: 106 | // - msg.sender is already normalized by default 107 | // - _to is getting called, which behaves identically for ID / Eth addresses 108 | _checkSafeReceiver(_to, msg.sender, _owner.normalize(), _tokenId, ""); 109 | } 110 | 111 | function safeTransferFrom(address _owner, address _to, uint _tokenId, bytes calldata _data) public virtual { 112 | // transferFrom will normalize input 113 | transferFrom(_owner, _to, _tokenId); 114 | 115 | // Check receiver. Only _owner needs to be normalized here, since: 116 | // - msg.sender is already normalized by default 117 | // - _to is getting called, which behaves identically for ID / Eth addresses 118 | _checkSafeReceiver(_to, msg.sender, _owner.normalize(), _tokenId, _data); 119 | } 120 | 121 | function approve(address _spender, uint _tokenId) public virtual { 122 | // Attempt to convert spender to Eth address 123 | _spender = _spender.normalize(); 124 | 125 | // No need to normalize, since we're reading from storage 126 | // and we only store normalized addresses 127 | address owner = ownerOf(_tokenId); 128 | if (msg.sender != owner && !isApprovedForAll(owner, msg.sender)) revert Unauthorized(); 129 | 130 | tokenApprovals[_tokenId] = _spender; 131 | emit Approval(owner, _spender, _tokenId); 132 | } 133 | 134 | function setApprovalForAll(address _operator, bool _isApproved) public virtual { 135 | // Attempt to convert operator to Eth address 136 | _operator = _operator.normalize(); 137 | 138 | operatorApprovals[msg.sender][_operator] = _isApproved; 139 | 140 | emit ApprovalForAll(msg.sender, _operator, _isApproved); 141 | } 142 | 143 | /*////////////////////////////////////// 144 | ERC-721 GETTERS 145 | //////////////////////////////////////*/ 146 | 147 | function tokenURI(uint _tokenId) public virtual view returns (string memory); 148 | 149 | function balanceOf(address _owner) public virtual view returns (uint) { 150 | // Attempt to convert owner to Eth address 151 | _owner = _owner.normalize(); 152 | 153 | if (_owner == address(0)) revert NullOwner(); 154 | 155 | return ownerBalances[_owner]; 156 | } 157 | 158 | function ownerOf(uint _tokenId) public virtual view returns (address) { 159 | address owner = tokenOwners[_tokenId]; 160 | if (owner == address(0)) revert NullOwner(); 161 | return owner; 162 | } 163 | 164 | function getApproved(uint _tokenId) public virtual view returns (address) { 165 | return tokenApprovals[_tokenId]; 166 | } 167 | 168 | function isApprovedForAll(address _owner, address _spender) public virtual view returns (bool) { 169 | return operatorApprovals[_owner.normalize()][_spender.normalize()]; 170 | } 171 | 172 | /*////////////////////////////////////// 173 | ERC-165 GETTERS 174 | //////////////////////////////////////*/ 175 | 176 | function supportsInterface(bytes4 _interfaceId) public virtual view returns (bool) { 177 | return 178 | _interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165 179 | _interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721 180 | _interfaceId == 0x5b5e139f; // ERC165 Interface ID for ERC721Metadata 181 | } 182 | 183 | /*////////////////////////////////////// 184 | MINT/BURN INTERNAL METHODS 185 | //////////////////////////////////////*/ 186 | 187 | function _mint(address _to, uint _tokenId) internal virtual { 188 | // Attempt to normalize destination 189 | _to = _to.normalize(); 190 | 191 | if (_to == address(0)) revert UnsafeReceiver(); 192 | if (tokenOwners[_tokenId] != address(0)) revert Unauthorized(); 193 | 194 | ownerBalances[_to]++; 195 | tokenOwners[_tokenId] = _to; 196 | 197 | emit Transfer(address(0), _to, _tokenId); 198 | } 199 | 200 | function _burn(uint _tokenId) internal virtual { 201 | address owner = ownerOf(_tokenId); 202 | 203 | ownerBalances[owner]--; 204 | delete tokenOwners[_tokenId]; 205 | delete tokenApprovals[_tokenId]; 206 | 207 | emit Transfer(owner, address(0), _tokenId); 208 | } 209 | 210 | function _safeMint(address _to, uint _tokenId) internal virtual { 211 | _mint(_to, _tokenId); 212 | 213 | // Check receiver. No normalization is needed: 214 | // - msg.sender is already normalized by default 215 | // - _to is getting called, which behaves identically for ID / Eth addresses 216 | // - address(0) doesn't need normalization 217 | _checkSafeReceiver(_to, msg.sender, address(0), _tokenId, ""); 218 | } 219 | 220 | function _safeMint(address _to, uint _tokenId, bytes memory _data) internal virtual { 221 | _mint(_to, _tokenId); 222 | 223 | // Check receiver. No normalization is needed: 224 | // - msg.sender is already normalized by default 225 | // - _to is getting called, which behaves identically for ID / Eth addresses 226 | // - address(0) doesn't need normalization 227 | _checkSafeReceiver(_to, msg.sender, address(0), _tokenId, _data); 228 | } 229 | 230 | /** 231 | * @notice This method does NOT normalize inputs. Ensure addresses are 232 | * normalized before calling this method. 233 | */ 234 | function _checkSafeReceiver(address _to, address _operator, address _from, uint _tokenId, bytes memory _data) internal { 235 | // Native actors (like the miner) will have a codesize of 1 236 | // However, they'd still need to return the magic value for 237 | // this to succeed. 238 | if ( 239 | _to.code.length != 0 && 240 | IERC721TokenReceiver(_to).onERC721Received(_operator, _from, _tokenId, _data) != 241 | IERC721TokenReceiver.onERC721Received.selector 242 | ) revert UnsafeReceiver(); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /contracts/token/ERC721/IERC721TokenReceiver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | interface IERC721TokenReceiver { 5 | function onERC721Received( 6 | address, 7 | address, 8 | uint, 9 | bytes calldata 10 | ) external returns (bytes4); 11 | } -------------------------------------------------------------------------------- /contracts/token/WFIL.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import "./ERC20.sol"; 5 | import "../utils/FilAddress.sol"; 6 | import "../access/OwnedClaimable.sol"; 7 | 8 | /** 9 | * @author fevmate (https://github.com/wadealexc/fevmate) 10 | * @notice Wrapped filecoin implementation, using ERC20-FEVM mixin. 11 | */ 12 | contract WFIL is ERC20("Wrapped FIL", "WFIL", 18), OwnedClaimable { 13 | 14 | using FilAddress for *; 15 | 16 | error TimelockActive(); 17 | 18 | /*////////////////////////////////////// 19 | WFIL STORAGE 20 | //////////////////////////////////////*/ 21 | 22 | // Timelock for 6 months after contract is deployed 23 | // Applies only to recoverDeposit. See comments there for info 24 | uint public immutable recoveryTimelock = block.timestamp + 24 weeks; 25 | 26 | /*////////////////////////////////////// 27 | EVENTS 28 | //////////////////////////////////////*/ 29 | 30 | event Deposit(address indexed from, uint amount); 31 | event Withdrawal(address indexed to, uint amount); 32 | 33 | /*////////////////////////////////////// 34 | CONSTRUCTOR 35 | //////////////////////////////////////*/ 36 | 37 | constructor(address _owner) OwnedClaimable(_owner) {} 38 | 39 | /*////////////////////////////////////// 40 | WFIL METHODS 41 | //////////////////////////////////////*/ 42 | 43 | /** 44 | * @notice Fallback function - Fil transfers via standard address.call 45 | * will end up here and trigger the deposit function, minting the caller 46 | * with WFIL 1:1. 47 | * 48 | * Note that transfers of value via the FVM's METHOD_SEND bypass bytecode, 49 | * and will not credit the sender with WFIL in return. Please ensure you 50 | * do NOT send the contract Fil via METHOD_SEND - always use InvokeEVM. 51 | * 52 | * For more information on METHOD_SEND, see recoverDeposit below. 53 | */ 54 | receive() external payable virtual { 55 | deposit(); 56 | } 57 | 58 | /** 59 | * @notice Deposit Fil into the contract, and mint WFIL 1:1. 60 | */ 61 | function deposit() public payable virtual { 62 | _mint(msg.sender, msg.value); 63 | 64 | emit Deposit(msg.sender, msg.value); 65 | } 66 | 67 | /** 68 | * @notice Burns _amount WFIL from caller's balance, and transfers them 69 | * the unwrapped Fil 1:1. 70 | * 71 | * Note: The fund transfer used here is address.call{value: _amount}(""), 72 | * which does NOT work with the FVM's builtin Multisig actor. This is 73 | * because, under the hood, address.call acts like a message to an actor's 74 | * InvokeEVM method. The Multisig actor does not implement this method. 75 | * 76 | * This is a known issue, but we've decided to keep the method as-is, 77 | * because it's likely that the Multisig actor is eventually upgraded to 78 | * support this method. Even though a Multisig actor cannot directly 79 | * withdraw, it is still possible for Multisigs to deposit, transfer, 80 | * etc WFIL. So, if your Multisig actor needs to withdraw, you can 81 | * transfer your WFIL to another contract, which can perform the 82 | * withdrawal for you. 83 | * 84 | * (Though Multisig actors are not supported, BLS/SECPK/EthAccounts 85 | * and EVM contracts can use this method normally) 86 | */ 87 | function withdraw(uint _amount) public virtual { 88 | _burn(msg.sender, _amount); 89 | 90 | emit Withdrawal(msg.sender, _amount); 91 | 92 | payable(msg.sender).sendValue(_amount); 93 | } 94 | 95 | /** 96 | * @notice Used by owner to unstick Fil that was directly transferred 97 | * to the contract without triggering the deposit/receive functions. 98 | * When called, _amount stuck Fil is converted to WFIL on behalf of 99 | * the passed-in _depositor. 100 | * 101 | * This method ONLY converts Fil that would otherwise be permanently 102 | * lost. 103 | * 104 | * --- About --- 105 | * 106 | * In the event someone accidentally sends Fil to this contract via 107 | * FVM method METHOD_SEND (or via selfdestruct), the Fil will be 108 | * lost rather than being converted to WFIL. This is because METHOD_SEND 109 | * transfers value without invoking the recipient's code. 110 | * 111 | * If this occurs, the contract's Fil balance will go up, but no WFIL 112 | * will be minted. Luckily, this means we can calculate the number of 113 | * stuck tokens as the contract's Fil balance minus WFIL totalSupply, 114 | * and ensure we're only touching stuck tokens with this method. 115 | * 116 | * Please ensure you only ever send funds to this contract using the 117 | * FVM method InvokeEVM! This method is not a get-out-of-jail free card, 118 | * and comes with no guarantees. 119 | * 120 | * (If you're a lost EVM dev, address.call uses InvokeEVM under the 121 | * hood. So in a purely contract-contract context, you don't need 122 | * to do anything special - use address.call, or call the WFIL.deposit 123 | * method as you would normally.) 124 | */ 125 | function recoverDeposit(address _depositor, uint _amount) public virtual onlyOwner { 126 | // This method is locked for 6 months after contract deployment. 127 | // This is to give the deployers time to sort out the best/most 128 | // equitable way to recover and distribute accidentally-locked 129 | // tokens. 130 | if (block.timestamp < recoveryTimelock) revert TimelockActive(); 131 | 132 | // Calculate number of locked tokens 133 | uint lockedTokens = address(this).balance - totalSupply; 134 | require(_amount <= lockedTokens); 135 | 136 | // Normalize depositor. _mint also does this, but we want to 137 | // emit the normalized address in the Deposit event below. 138 | _depositor = _depositor.normalize(); 139 | 140 | _mint(_depositor, _amount); 141 | emit Deposit(_depositor, _amount); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /contracts/utils/CallNative.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "./FilAddress.sol"; 5 | 6 | /** 7 | * @author fevmate (https://github.com/wadealexc/fevmate) 8 | * @notice Helpers for calling actors by ID 9 | */ 10 | library CallNative { 11 | 12 | // keccak([]) 13 | bytes32 constant EVM_EMPTY_CODEHASH = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; 14 | // keccak([0xFE]) 15 | bytes32 constant FIL_NATIVE_CODEHASH = 0xbcc90f2d6dada5b18e155c17a1c0a55920aae94f39857d39d0d8ed07ae8f228b; 16 | 17 | uint64 constant MAX_RESERVED_METHOD = 1023; 18 | bytes4 constant NATIVE_METHOD_SELECTOR = 0x868e10c4; 19 | 20 | uint64 constant DEFAULT_FLAG = 0x00000000; 21 | uint64 constant READONLY_FLAG = 0x00000001; 22 | 23 | /** 24 | * @notice Call actor by ID. This method allows the target actor 25 | * to change state. If you don't want this, see the readonly 26 | * method below. 27 | */ 28 | function callActor( 29 | uint64 _id, 30 | uint64 _method, 31 | uint _value, 32 | uint64 _codec, 33 | bytes memory _data 34 | ) internal returns (bool, bytes memory) { 35 | return callHelper(false, _id, _method, _value, _codec, _data); 36 | } 37 | 38 | /** 39 | * @notice Call actor by ID, and revert if state changes occur. 40 | * This is the call_actor_id precompile equivalent of an EVM 41 | * staticcall. By passing the READONLY flag, the FVM will prevent 42 | * state changes in the same way staticcall does. 43 | * 44 | * Note: The assembly here is because the call_actor_id precompile 45 | * has to be called using delegatecall, and solc's mutability checker 46 | * won't allow me to call this method "view" if it can delegatecall. 47 | * 48 | * Having a "view" method is nice for usability, though, because users 49 | * can "read" contract methods in frontends without sending a transaction. 50 | * 51 | * ... so we trick solc into allowing the method to be marked as view. 52 | */ 53 | function callActorReadonly( 54 | uint64 _id, 55 | uint64 _method, 56 | uint64 _codec, 57 | bytes memory _data 58 | ) internal view returns (bool, bytes memory) { 59 | function(bool, uint64, uint64, uint, uint64, bytes memory) internal view returns (bool, bytes memory) callFn; 60 | function(bool, uint64, uint64, uint, uint64, bytes memory) internal returns (bool, bytes memory) helper = callHelper; 61 | assembly { callFn := helper } 62 | return callFn(true, _id, _method, 0, _codec, _data); 63 | } 64 | 65 | function callHelper( 66 | bool _readonly, 67 | uint64 _id, 68 | uint64 _method, 69 | uint _value, 70 | uint64 _codec, 71 | bytes memory _data 72 | ) private returns (bool, bytes memory) { 73 | uint64 flags = _readonly ? READONLY_FLAG : DEFAULT_FLAG; 74 | require(!_readonly || _value == 0); // sanity check - shouldn't hit this in a private method 75 | bytes memory input = abi.encode(_method, _value, flags, _codec, _data, _id); 76 | return FilAddress.CALL_ACTOR_BY_ID.delegatecall(input); 77 | } 78 | } -------------------------------------------------------------------------------- /contracts/utils/FilAddress.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | /** 5 | * @author fevmate (https://github.com/wadealexc/fevmate) 6 | * @notice Utility functions for converting between id and 7 | * eth addresses. Helps implement address normalization. 8 | * 9 | * See README for more details about how to use this when 10 | * developing for the FEVM. 11 | */ 12 | library FilAddress { 13 | 14 | // Custom errors 15 | error CallFailed(); 16 | error InvalidAddress(); 17 | error InsufficientFunds(); 18 | 19 | // Builtin Actor addresses (singletons) 20 | address constant SYSTEM_ACTOR = 0xfF00000000000000000000000000000000000000; 21 | address constant INIT_ACTOR = 0xff00000000000000000000000000000000000001; 22 | address constant REWARD_ACTOR = 0xff00000000000000000000000000000000000002; 23 | address constant CRON_ACTOR = 0xFF00000000000000000000000000000000000003; 24 | address constant POWER_ACTOR = 0xFf00000000000000000000000000000000000004; 25 | address constant MARKET_ACTOR = 0xff00000000000000000000000000000000000005; 26 | address constant VERIFIED_REGISTRY_ACTOR = 0xFF00000000000000000000000000000000000006; 27 | address constant DATACAP_TOKEN_ACTOR = 0xfF00000000000000000000000000000000000007; 28 | address constant EAM_ACTOR = 0xfF0000000000000000000000000000000000000a; 29 | 30 | // FEVM precompile addresses 31 | address constant RESOLVE_ADDRESS = 0xFE00000000000000000000000000000000000001; 32 | address constant LOOKUP_DELEGATED_ADDRESS = 0xfE00000000000000000000000000000000000002; 33 | address constant CALL_ACTOR = 0xfe00000000000000000000000000000000000003; 34 | // address constant GET_ACTOR_TYPE = 0xFe00000000000000000000000000000000000004; // (deprecated) 35 | address constant CALL_ACTOR_BY_ID = 0xfe00000000000000000000000000000000000005; 36 | 37 | // An ID address with id == 0. It's also equivalent to the system actor address 38 | // This is useful for bitwise operations 39 | address constant ZERO_ID_ADDRESS = SYSTEM_ACTOR; 40 | 41 | /** 42 | * @notice Convert ID to Eth address. Returns input if conversion fails. 43 | * 44 | * Attempt to convert address _a from an ID address to an Eth address 45 | * If _a is NOT an ID address, this returns _a 46 | * If _a does NOT have a corresponding Eth address, this returns _a 47 | * 48 | * NOTE: It is possible this returns an ID address! If you want a method 49 | * that will NEVER return an ID address, see mustNormalize below. 50 | */ 51 | function normalize(address _a) internal view returns (address) { 52 | // First, check if we have an ID address. If we don't, return as-is 53 | (bool isID, uint64 id) = isIDAddress(_a); 54 | if (!isID) { 55 | return _a; 56 | } 57 | 58 | // We have an ID address -- attempt the conversion 59 | // If there is no corresponding Eth address, return _a 60 | (bool success, address eth) = getEthAddress(id); 61 | if (!success) { 62 | return _a; 63 | } else { 64 | return eth; 65 | } 66 | } 67 | 68 | /** 69 | * @notice Convert ID to Eth address. Reverts if conversion fails. 70 | * 71 | * Attempt to convert address _a from an ID address to an Eth address 72 | * If _a is NOT an ID address, this returns _a unchanged 73 | * If _a does NOT have a corresponding Eth address, this method reverts 74 | * 75 | * This method can be used when you want a guarantee that an ID address is not 76 | * returned. Note, though, that rejecting ID addresses may mean you don't support 77 | * other Filecoin-native actors. 78 | */ 79 | function mustNormalize(address _a) internal view returns (address) { 80 | // First, check if we have an ID address. If we don't, return as-is 81 | (bool isID, uint64 id) = isIDAddress(_a); 82 | if (!isID) { 83 | return _a; 84 | } 85 | 86 | // We have an ID address -- attempt the conversion 87 | // If there is no corresponding Eth address, revert 88 | (bool success, address eth) = getEthAddress(id); 89 | if (!success) revert InvalidAddress(); 90 | return eth; 91 | } 92 | 93 | // Used to clear the last 8 bytes of an address (addr & U64_MASK) 94 | address constant U64_MASK = 0xFffFfFffffFfFFffffFFFffF0000000000000000; 95 | // Used to retrieve the last 8 bytes of an address (addr & MAX_U64) 96 | address constant MAX_U64 = 0x000000000000000000000000fFFFFFffFFFFfffF; 97 | 98 | /** 99 | * @notice Checks whether _a matches the ID address format. 100 | * If it does, returns true and the id 101 | * 102 | * The ID address format is: 103 | * 0xFF | bytes11(0) | uint64(id) 104 | */ 105 | function isIDAddress(address _a) internal pure returns (bool isID, uint64 id) { 106 | /// @solidity memory-safe-assembly 107 | assembly { 108 | // Zeroes out the last 8 bytes of _a 109 | let a_mask := and(_a, U64_MASK) 110 | 111 | // If the result is equal to the ZERO_ID_ADDRESS, 112 | // _a is an ID address. 113 | if eq(a_mask, ZERO_ID_ADDRESS) { 114 | isID := true 115 | id := and(_a, MAX_U64) 116 | } 117 | } 118 | } 119 | 120 | /** 121 | * @notice Given an Actor ID, converts it to an EVM-compatible address. 122 | * 123 | * If _id has a corresponding Eth address, we return that 124 | * Otherwise, _id is returned as a 20-byte ID address 125 | */ 126 | function toAddress(uint64 _id) internal view returns (address) { 127 | (bool success, address eth) = getEthAddress(_id); 128 | if (success) { 129 | return eth; 130 | } else { 131 | return toIDAddress(_id); 132 | } 133 | } 134 | 135 | /** 136 | * @notice Given an Actor ID, converts it to a 20-byte ID address 137 | * 138 | * Note that this method does NOT check if the _id has a corresponding 139 | * Eth address. If you want that, try toAddress above. 140 | */ 141 | function toIDAddress(uint64 _id) internal pure returns (address addr) { 142 | /// @solidity memory-safe-assembly 143 | assembly { addr := or(ZERO_ID_ADDRESS, _id) } 144 | } 145 | 146 | // An address with all bits set. Used to clean higher-order bits 147 | address constant ADDRESS_MASK = 0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF; 148 | 149 | /** 150 | * @notice Convert ID to Eth address by querying the lookup_delegated_address 151 | * precompile. 152 | * 153 | * If the actor ID corresponds to an Eth address, this will return (true, addr) 154 | * If the actor ID does NOT correspond to an Eth address, this will return (false, 0) 155 | * 156 | * --- About --- 157 | * 158 | * The lookup_delegated_address precompile retrieves the actor state corresponding 159 | * to the id. If the actor has a delegated address, it is returned using fil 160 | * address encoding (see below). 161 | * 162 | * f4, or delegated addresses, have a namespace as well as a subaddress that can 163 | * be up to 54 bytes long. This is to support future address formats. Currently, 164 | * though, the f4 format is only used to support Eth addresses. 165 | * 166 | * Consequently, the only addresses lookup_delegated_address should return have: 167 | * - Prefix: "f4" address - 1 byte - (0x04) 168 | * - Namespace: EAM actor id 10 - 1 byte - (0x0A) 169 | * - Subaddress: EVM-style address - 20 bytes - (EVM address) 170 | * 171 | * This method checks that the precompile output exactly matches this format: 172 | * 22 bytes, starting with 0x040A. 173 | * 174 | * If we get anything else, we return (false, 0x00). 175 | */ 176 | function getEthAddress(uint64 _id) internal view returns (bool success, address eth) { 177 | /// @solidity memory-safe-assembly 178 | assembly { 179 | // Call LOOKUP_DELEGATED_ADDRESS precompile 180 | // 181 | // Input: uint64 id, in standard EVM format (left-padded to 32 bytes) 182 | // 183 | // Output: LOOKUP_DELEGATED_ADDRESS returns an f4-encoded address. 184 | // For Eth addresses, the format is a 20-byte address, prefixed with 185 | // 0x040A. So, we expect exactly 22 bytes of returndata. 186 | // 187 | // Since we want to read an address from the returndata, we place the 188 | // output at memory offset 10, which means the address is already 189 | // word-aligned (10 + 22 == 32) 190 | // 191 | // NOTE: success and returndatasize checked at the end of the function 192 | mstore(0, _id) 193 | success := staticcall(gas(), LOOKUP_DELEGATED_ADDRESS, 0, 32, 10, 22) 194 | 195 | // Read result. LOOKUP_DELEGATED_ADDRESS returns raw, unpadded 196 | // bytes. Assuming we succeeded, we can extract the eth address 197 | // by reading from offset 0 and cleaning any higher-order bits: 198 | let result := mload(0) 199 | eth := and(ADDRESS_MASK, result) 200 | 201 | // Check that the returned address has the expected prefix. The 202 | // prefix is the first 2 bytes of returndata, located at memory 203 | // offset 10. 204 | // 205 | // To isolate it, shift right by the # of bits in an address (160), 206 | // and clean all but the last 2 bytes. 207 | let prefix := and(0xFFFF, shr(160, result)) 208 | if iszero(eq(prefix, 0x040A)) { 209 | success := false 210 | eth := 0 211 | } 212 | } 213 | // Checking these here because internal functions don't have 214 | // a good way to return from inline assembly. 215 | // 216 | // But, it's very important we do check these. If the output 217 | // wasn't exactly what we expected, we assume there's no eth 218 | // address and return (false, 0). 219 | if (!success || returnDataSize() != 22) { 220 | return (false, address(0)); 221 | } 222 | } 223 | 224 | /** 225 | * @notice Convert Eth address to ID by querying the resolve_address precompile. 226 | * 227 | * If the passed-in address is already in ID form, returns (true, id) 228 | * If the Eth address has no corresponding ID address, returns (false, 0) 229 | * Otherwise, the lookup succeeds and this returns (true, id) 230 | * 231 | * --- About --- 232 | * 233 | * The resolve_address precompile can resolve any fil-encoded address to its 234 | * corresponding actor ID, if there is one. This means resolve_address handles 235 | * all address protocols: f0, f1, f2, f3, and f4. 236 | * 237 | * An address might not have an actor ID if it does not exist in state yet. A 238 | * typical example of this is a public-key-type address, which can exist even 239 | * if it hasn't been used on-chain yet. 240 | * 241 | * This method is only meant to look up ids for Eth addresses, so it contains 242 | * very specific logic to correctly encode an Eth address into its f4 format. 243 | * 244 | * Note: This is essentially just the reverse of getEthAddress above, so check 245 | * the comments there for more details on f4 encoding. 246 | */ 247 | function getActorID(address _eth) internal view returns (bool success, uint64 id) { 248 | // First - if we already have an ID address, we can just return that 249 | (success, id) = isIDAddress(_eth); 250 | if (success) { 251 | return (success, id); 252 | } 253 | 254 | /// @solidity memory-safe-assembly 255 | assembly { 256 | // Convert Eth address to f4 format: 22 bytes, with prefix 0x040A. 257 | // (see getEthAddress above for more details on this format) 258 | // 259 | // We're going to pass the 22 bytes to the precompile without any 260 | // padding or length, so everything will be left-aligned. Since 261 | // addresses are right-aligned, we need to shift everything left: 262 | // - 0x040A prefix - shifted left 240 bits (30 bytes * 8 bits) 263 | // - Eth address - shifted left 80 bits (10 bytes * 8 bits) 264 | let input := or( 265 | shl(240, 0x040A), 266 | shl(80, _eth) 267 | ) 268 | // Call RESOLVE_ADDRESS precompile 269 | // 270 | // Input: Eth address in f4 format. 22 bytes, no padding or length 271 | // 272 | // Output: RESOLVE_ADDRESS returns a uint64 actor ID in standard EVM 273 | // format (left-padded to 32 bytes). 274 | // 275 | // NOTE: success and returndatasize checked at the end of the function 276 | mstore(0, input) 277 | success := staticcall(gas(), RESOLVE_ADDRESS, 0, 22, 0, 32) 278 | 279 | // Read result and clean higher-order bits, just in case. 280 | // If successful, this will be the actor id. 281 | id := and(MAX_U64, mload(0)) 282 | } 283 | // Checking these here because internal functions don't have 284 | // a good way to return from inline assembly. 285 | // 286 | // But, it's very important we do check these. If the output 287 | // wasn't exactly what we expected, we assume there's no ID 288 | // address and return (false, 0). 289 | if (!success || returnDataSize() != 32) { 290 | return (false, 0); 291 | } 292 | } 293 | 294 | /** 295 | * @notice Replacement for Solidity's address.send and address.transfer 296 | * This sends _amount to _recipient, forwarding all available gas and 297 | * reverting if there are any errors. 298 | * 299 | * If _recpient is an Eth address, this works the way you'd 300 | * expect the EVM to work. 301 | * 302 | * If _recpient is an ID address, this works if: 303 | * 1. The ID corresponds to an Eth EOA address (EthAccount actor) 304 | * 2. The ID corresponds to an Eth contract address (EVM actor) 305 | * 3. The ID corresponds to a BLS/SECPK address (Account actor) 306 | * 307 | * If _recpient is some other Filecoin-native actor, this will revert. 308 | */ 309 | function sendValue(address payable _recipient, uint _amount) internal { 310 | if (address(this).balance < _amount) revert InsufficientFunds(); 311 | 312 | (bool success, ) = _recipient.call{value: _amount}(""); 313 | if (!success) revert CallFailed(); 314 | } 315 | 316 | function returnDataSize() private pure returns (uint size) { 317 | /// @solidity memory-safe-assembly 318 | assembly { size := returndatasize() } 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fevmate", 3 | "version": "1.0.3", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "fevmate", 9 | "version": "1.0.3", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@openzeppelin/contracts": "^4.8.1" 13 | } 14 | }, 15 | "node_modules/@openzeppelin/contracts": { 16 | "version": "4.8.1", 17 | "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.8.1.tgz", 18 | "integrity": "sha512-xQ6eUZl+RDyb/FiZe1h+U7qr/f4p/SrTSQcTPH2bjur3C5DbuW/zFgCU/b1P/xcIaEqJep+9ju4xDRi3rmChdQ==" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fevmate", 3 | "version": "1.0.3", 4 | "description": "Building blocks for smart contract development on the Filecoin EVM", 5 | "files": [ 6 | "contracts/**/*.sol" 7 | ], 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/wadealexc/fevmate.git" 14 | }, 15 | "author": "Alex Wade ", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/wadealexc/fevmate/issues" 19 | }, 20 | "homepage": "https://github.com/wadealexc/fevmate#readme", 21 | "dependencies": { 22 | "@openzeppelin/contracts": "^4.8.1" 23 | }, 24 | "keywords": [ 25 | "Filecoin", 26 | "Solidity", 27 | "EVM", 28 | "FEVM" 29 | ] 30 | } 31 | --------------------------------------------------------------------------------