Welcome to the documentation for our confidential storage extension to the Ethereum Virtual Machine (EVM). This extension introduces confidential storage capabilities, allowing developers to handle sensitive data securely within smart contracts. By building upon the existing EVM infrastructure, we've added minimal changes to ensure ease of adoption and maintain compatibility.
This documentation highlights the differences from Cancun's Ethereum version to focus on the new features introduced by Seismic's Mercury version. We recommend familiarizing yourself with the standard Ethereum documentation alongside this guide.
Indeed, this work wouldn’t have been possible without an incredible, nearly decade-long effort to build Solidity. As such, we strongly encourage the reader to explore their work and documentation.
This is experimental software, thread with caution.
- New Shielded Types
- Storage Behavior
- Restrictions and Caveats
- Casting and Type Conversion
- New Instructions
- Arrays and Collections
- Best Practices
- Conclusion
- Feedback
We introduce four new types called shielded types:
suint
: Shielded unsigned integer.sint
: Shielded signed integer.saddress
: Shielded address.sbool
: Shielded bool.
These types function similarly to their unshielded counterparts (uint
, int
, address
and bool
) but are designed to handle confidential data securely within the smart contract's storage.
contract ConfidentialWallet {
suint256 confidentialBalance;
saddress confidentialOwner;
constructor(suint256 _initialBalance, saddress _owner) {
confidentialBalance = _initialBalance;
confidentialOwner = _owner;
}
function addFunds(suint256 amount) private {
confidentialBalance += amount;
}
// Shielded public interface for balance inquiries
function getConfidentialBalance(saddress caller) public view returns (suint256) {
require(caller == confidentialOwner, "Unauthorized access");
return confidentialBalance;
}
// Securely transfer funds from this wallet to another shielded address
function confidentialTransfer(suint256 amount, saddress recipient) public {
require(msg.sender == confidentialOwner, "Only the owner can transfer");
require(confidentialBalance >= amount, "Insufficient balance");
confidentialBalance -= amount;
// `recipient` would use a shielded receive function to handle incoming funds
// This line represents a private operation that modifies shielded storage
// in another confidential contract instance.
ConfidentialWallet(recipient).addFunds(amount);
}
}
-
Whole Slot Consumption: Shielded types consume an entire storage slot. This design choice ensures that a storage slot is entirely private or public, avoiding mixed storage types within a single slot.
-
Future Improvements: We plan to support slot packing for shielded types in future updates. Until then, developers can use inline assembly to achieve slot packing manually if necessary.
contract RegularStorage {
struct RegularStruct {
uint64 a; // Slot 0 (packed)
uint128 b; // Slot 0 (packed)
uint64 c; // Slot 0 (packed)
}
RegularStruct regularData;
/*
Storage Layout:
- Slot 0: [a | b | c]
*/
}
contract ShieldedStorage {
struct ShieldedStruct {
suint64 a; // Slot 0
suint128 b; // Slot 1
suint64 c; // Slot 2
}
ShieldedStruct shieldedData;
/*
Storage Layout:
- Slot 0: [a]
- Slot 1: [b]
- Slot 2: [c]
*/
}
-
Shielded variables cannot be declared as
public
. This restriction prevents accidental exposure of confidential data.suint256 public confidentialNumber; // This will cause a compilation error
-
Shielded types cannot be used as constants or immutables. Constants and Immutables are embedded in the contract bytecode, which is publicly accessible.
suint256 constant CONFIDENTIAL_CONSTANT = 42; // Not allowed
- Be cautious when using literals and enums with shielded types. They can inadvertently leak information if not handled properly.
- Using shielded integers as exponents in exponentiation operations can leak information through gas usage, as gas cost scales with the exponent value.
- Calling
.min()
and.max()
on shielded integers can reveal information about their values.
-
Shielded types cannot be emitted in events, as this would expose confidential data.
-
Currently: Although native event encryption isn'y supported, developers may use the encrypt and decrypt precompiles at addresses 102/103 (see example) to secure event data.
-
Future Improvements: We plan to support encrypted events, enabling the emission of shielded types without compromising confidentiality or developer experience.
event ConfidentialEvent(suint256 confidentialData); // Not allowed
- Shielded types and their unshielded counterparts do not support implicit casting.
uint256 publicNumber = 100;
suint256 confidentialNumber = suint256(publicNumber); // Explicit casting required`
- To cast an
saddress
to apayable
address, use the following pattern:
address payable pay = payable(saddress(SomethingCastableToAnSaddress));`
We introduce two new EVM instructions to handle confidential storage:
- Purpose: Stores shielded values in marked confidential storage slots.
- Behavior: Sets the storage slot as confidential during the store operation.
- Purpose: Retrieves shielded values from marked confidential or uninitialized storage slots.
- Behavior: Only accesses storage slots marked as confidential.
-
Flagged Storage: We introduce
FlaggedStorage
to tag storage slots as public or private based on the store instructions (SSTORE
for public,CSTORE
for confidential). -
Access Control:
- Public Storage: Can be stored and loaded using
SSTORE
andSLOAD
. - Confidential Storage: Must be stored using
CSTORE
and loaded usingCLOAD
.
- Public Storage: Can be stored and loaded using
-
Flexibility: Storage slots are not permanently fixed as public or private. Developers can manage access rights using inline assembly if needed. Otherwise, the compiler will take care of it.
- Arrays of shielded types are supported, and their metadata (e.g., length of a dynamic array) are also stored in confidential storage.
suint256[] private confidentialDynamicArray;
-
As such, when interfacing with shielded arrays, we've conserved Solidity rules and just transposed them by using shielded types:
- The index should be a Shielded Integer.
- The declared length should be a Shielded Integer.
- The returned length is a Shielded Integer.
- Pushed values should be consistant with the shielded array underlying type.
- Currently, shielded arrays only work with the shielded types (
suint
,sint
,saddress
andsbool
). - Shielded
bytes
orstring
arrays are not yet supported. - It is very likely that some of our intermediary representation is not strictly correct, which would lead into less optimized code as IR is fundamental to optimization passes.
- Mappings using shielded types for keys and/or values are supported. In such cases, the storage operations will employ the confidential instructions (CLOAD/CSTORE) accordingly.
- Avoid Public Exposure: Never expose shielded variables through public getters or events.
- Careful with Gas Usage: Be mindful of operations where gas cost can vary based on shielded values (e.g., loops, exponentiation).
- Encrypt Calldata: Not only must shielded immutable variables be initialized with encrypted calldata, but all functions accepting shielded types should use encrypted calldata.
- Manual Slot Packing: If slot packing is necessary, use inline assembly carefully to avoid introducing vulnerabilities.
- Review Compiler Warnings: Pay attention to compiler warnings related to shielded types to prevent accidental leaks.
This extension enhances the EVM by introducing confidential storage capabilities, allowing developers to handle sensitive data securely. By understanding the new shielded types, instructions, and associated caveats, you can leverage these features to build more secure smart contracts.
We encourage you to refer to the standard Ethereum documentation for foundational concepts and use this guide to understand the differences and new functionalities introduced.
We also welcome external contributions to this repository.
The upstream repository lives here. This fork is up-to-date with it through commit ab55807
. You can see this by viewing the develop branch on this repository.
You can view all of our changes vs. upstream on this pull request. The sole purpose of this PR is display our diff; it will never be merged in to the main branch of this repo.
We welcome your feedback on this documentation. If you have suggestions or encounter any issues, please contact our support team or contribute to our documentation repository.