description |
---|
Guide to creating secure dApps on Sapphire |
import DocCard from '@theme/DocCard'; import {findSidebarItem} from '@site/src/sidebarUtils';
This page mainly describes the differences between Sapphire and Ethereum since there are a number of excellent tutorials on developing for Ethereum. If you don't know where to begin, the Hardhat tutorial, Solidity docs, and Emerald dApp tutorial are great places to start. You can continue following this guide once you've set up your development environment and have deployed your contract to a non-confidential EVM network (e.g., Ropsten, Emerald).
The Oasis Network consists of the consensus layer and a number of ParaTimes. ParaTimes are independent replicated state machines that settle transactions using the consensus layer (to learn more, check the Oasis Network Overview). Sapphire is a ParaTime which implements the Ethereum Virtual Machine (EVM).
The minimum and also expected block time in Sapphire is 6 seconds. Any Sapphire transaction will require at least this amount of time to be executed, and probably no more.
ParaTimes, Sapphire included, are not allowed to directly access your tokens stored in consensus layer accounts. You will need to deposit tokens from your consensus account to Sapphire. Consult the Manage your Tokens chapter to learn more.
Sapphire is deployed on Testnet and Mainnet chains. Testnet should be considered unstable software and may also have its state wiped at any time. As the name implies, only use Testnet for testing unless you're testing how angry your users get when state is wiped.
:::danger Never deploy production services on Testnet
Because Testnet state can be wiped in the future, you should never deploy a production service on Testnet! Just don't do it!
Also note that while Testnet does use proper TEEs, due to experimental software and different security parameters, confidentiality of Sapphire on Testnet is not guaranteed -- all transactions and state published on the Sapphire Testnet should be considered public.
:::
:::tip
For testing purposes, visit our Testnet faucet to obtain some TEST which you can then use on the Sapphire Testnet to pay for gas fees. The faucet supports sending TEST both to your consensus layer address or to your address inside the ParaTime.
:::
For development and testing, you can run a local instance of the entire Sapphire stack.
Sapphire is generally compatible with Ethereum, the EVM, and all the user and developer tooling that you are used to. In addition to confidentiality features, you get a few extra benefits including the ability to generate private entropy, and make signatures on-chain. An example of a dApp that uses both is an HSM contract that generates an Ethereum wallet and signs transactions sent to it via transactions.
There are also a few breaking changes compared to Ethereum though, but we think that you'll quickly grasp them:
- Encrypted Contract State
- End-to-End Encrypted Transactions and Calls
from
Address is Zero for Unsigned Calls- Override
receive
andfallback
when Funding the Contract - Instant Finality
Read below to learn more about them. Otherwise, Sapphire is like Emerald, a fast, cheap Ethereum.
The contract state is only visible to the contract that wrote it. With respect
to the contract API, it's as if all state variables are declared as private
,
but with the further restriction that not even full nodes can read the values.
Public or access-controlled values are provided instead through explicit
getters.
Calling eth_getStorageAt()
will return zero.
Transactions and calls are end-to-end encrypted into the contract. Only the caller and the contract can see the data sent to/received from the ParaTime. This ends up defeating some utility of block explorers, however.
The status of the transaction is public and so are the error code, the revert message and logs (emitted events).
The from
address using of calls is derived from a signature attached to the
call. Unsigned calls have their sender set to the zero address. This allows
contract authors to write getters that release secrets to authenticated callers
(e.g. by checking the msg.sender
value), but without requiring a transaction
to be posted on-chain.
In Ethereum, you can fund a contract by sending Ether along the transaction in two ways:
- a transaction must call a payable function in the contract, or
- not calling any specific function (i.e. empty calldata). In this case,
the payable
receive()
and/orfallback()
functions need to be defined in the contract. If no such functions exist, the transaction will revert.
The behavior described above is the same in Sapphire when using EVM transactions to fund a contract.
However, the Oasis Network also uses Oasis-native transactions such as a
deposit to a ParaTime account or a transfer. In this case, you will be able to
fund the contract's account even though the contract may not implement payable
receive()
or fallback()
! Or, if these functions do exist, they will not
be triggered. You can send such Oasis-native transactions by using the Oasis
CLI for example.
The Oasis Network is a proof of stake network where 2/3+ of the validator nodes need to verify each block in order to consider it final. However, in Ethereum the signatures of those validator nodes can be submitted minutes after the block is proposed, which makes the block proposal mechanism independent of the validation, but adds uncertainty if and when will the proposed block actually be finalized.
In the Oasis Network, the 2/3+ of signatures need to be provided immediately after the block is proposed and the network will halt, until the required number signatures are provided. This means that you can rest assured that any validated block is final. As a consequence, the cross-chain bridges are more responsive yet safe on the Oasis Network.
Once ROSE tokens are deposited into Sapphire, it should be painless for users to begin using dApps. To achieve this ideal user experience, we have to modify the dApp a little, but it's made simple by our compatibility library, @oasisprotocol/sapphire-paratime.
There are compatibility layers in other languages, which may be found in the repo.
Sapphire is compatible with popular self-custodial wallets including MetaMask, Ledger, Brave, and so forth. You can also use libraries like Ethers, Viem, and Wagmi to create programmatic wallets. In general, if it generates secp256k1 signatures, it'll work just fine.
Sapphire is programmable using any language that targets the EVM, such as Solidity, Fe or Vyper. If you prefer to use an Ethereum framework like Hardhat or Foundry, you can also use those with Sapphire; all you need to do is set your Web3 gateway URL. You can find the details of the Oasis Sapphire Web3 endpoints here.
The figure above illustrates the flow of a confidential smart contract transaction on Sapphire.
Transactions and calls must be encrypted and signed for maximum security. The @oasisprotocol/sapphire-paratime npm package will make your life easy. It'll handle cryptography and signing for you.
You should be aware that taking actions based on the value of private data may leak the private data through side channels like time spent, gas use and accessed memory locations. If you need to branch on private data, you should in most cases ensure that both branches exhibit the same time/gas and storage patterns.
You can also make confidential smart contract calls on Sapphire. If you
use msg.sender
for access control in your contract, the call must be
signed, otherwise msg.sender
will be zeroed. On the other hand, set the
from
address to all zeros, if you want to avoid annoying signature popups in
the user's wallet for calls that do not need to be signed. The JS library will
do this for you.
:::note
Inside the smart contract code, there is no way of knowing whether the client's call data were originally encrypted or not.
:::
The Sapphire state model is like Ethereum's except for all state being encrypted and not accessible to anyone except the contract. The contract, executing in an active (attested) Oasis compute node is the only entity that can request its state encryption key from the Oasis key manager. Both the keys and values of the items stored in state are encrypted, but the size of either is not hidden. Your app may need to pad state items to a constant length, or use other obfuscation. Observers may also be able to infer computation based on storage access patterns, so you may need to obfuscate that, too. See Security chapter for more recommendations.
:::danger Contract state leaks a fine-grained access pattern
Contract state is backed by an encrypted key-value store. However, the trace of encrypted records is leaked to the compute node. As a concrete example, an ERC-20 token transfer would leak which encrypted record is for the sender's account balance and which is for the receiver's account balance. Such a token would be traceable from sender address to receiver address. Obfuscating the storage access patterns may be done by using an ORAM implementation.
:::
Contract state may be made available to third parties through logs/events, or explicit getters.
Contract logs/events (e.g., those emitted by the Solidity emit
keyword)
are exactly like Ethereum. Data contained in events is not encrypted.
Precompiled contracts are available to help you encrypt data that you can
then pack into an event, however.
:::danger Unmodified contracts may leak state through logs
Base contracts like those provided by OpenZeppelin often emit logs containing
private information. If you don't know they're doing that, you might undermine
the confidentiality of your state. As a concrete example, the ERC-20 spec
requires implementers to emit an event Transfer(from, to, amount)
, which is
obviously problematic if you're writing a confidential token. What you can
do instead is fork that contract and remove the offending emissions.
:::
<DocCard item={findSidebarItem('/node/run-your-node/paratime-client-node')} /> <DocCard item={findSidebarItem('/node/web3')} />