rustsol
is a tool designed to generate Rust storage bindings for Ethereum smart contracts.
These bindings can be used to retrieve storage slots, offsets, and sizes of all stored variables within a contract, including objects stored in mappings and arrays.
When initialized with a slots_getter
, the generated structures can be used to conveniently access the values of these storage variables.
Get storage position of a contract variable:
let contract = generated_contract::UniswapV3Pool::new();
let (slot, offset, size_bytes) = contract.observations.at(42).tickCumulative.position();
println!("slot={}, offset={}, size_bytes={}", slot, offset, size_bytes);
// Output:
// slot=50, offset=4, size_bytes=7
Get storage value of a contract variable using provided slots getter:
let contract = generated_contract::UniswapV3Pool::new();
contract.set_slots_getter(my_slots_getter);
let tick_value = contract.ticks.at(-92110).get_value().unwrap();
println!("{:?}", tick_value);
// Output (prettified):
// TickInfoValue {
// liquidityGross: 398290794261,
// liquidityNet: 398290794261,
// feeGrowthOutside0X128: 0x0000000000000000000000000000000000000b73d798604f1b0cd4f1d544c646_U256,
// feeGrowthOutside1X128: 0x000000000000000000000000000000f2a960acbe8891e526c025b819077f15ae_U256,
// tickCumulativeOutside: 622572156443,
// secondsPerLiquidityOutsideX128: 0x0000000000000000000000000000000000000001e576ee66a9d9f002e36fad4c_U256,
// secondsOutside: 1623419923,
// initialized: true
// }
The generated structs from the bindings will look similar to the following:
pub struct UniswapV3Pool {
__slot: U256,
__slots_getter: Option<Arc<dyn SlotsGetter>>,
pub slot0: UniswapV3PoolSlot0,
pub feeGrowthGlobal0X128: Primitive<32, U256>,
pub feeGrowthGlobal1X128: Primitive<32, U256>,
pub protocolFees: UniswapV3PoolProtocolFees,
pub liquidity: Primitive<16, u128>,
pub ticks: Mapping<i32, TickInfo>,
pub tickBitmap: Mapping<i16, Primitive<32, U256>>,
pub positions: Mapping<U256, PositionInfo>,
pub observations: StaticArray<2097120, OracleObservation>,
}
pub struct UniswapV3PoolSlot0Value {
pub sqrtPriceX96: U256,
pub tick: i32,
pub observationIndex: u16,
pub observationCardinality: u16,
pub observationCardinalityNext: u16,
pub feeProtocol: u8,
pub unlocked: bool,
}
From crates.io:
cargo install rustsol
From source code:
git clone <this repo>
cd rustsol
cargo install --path rustsol
To generate storage bindings for a contract, you first need the contract's storage layout in JSON format.
For Solidity contracts, this can be generated using the solc
compiler.
rustsol
provides a simple Python script to assist with this process.
Set up the Python environment
python -m venv .venv
source .venv/bin/activate
pip install -r pytools/requirements.txt
and run the script
cd pytools
python storage_layout.py
Modify this script to generate the storage layout for the contracts you are interested in.
To generate bindings, you can use the following command:
rustsol generate_storage_bindings your_solc_output.json contract_path contract_name generated_contract.rs
Or with cargo:
cargo run -- generate_storage_bindings your_solc_output.json contract_path contract_name generated_contract.rs
To understand what contract_path
and contract_name
mean, let's look at an example storage layout JSON:
{
"contracts": {
"UniswapV3Pool.sol": {
"UniswapV3Pool": {
"storageLayout": {
...
}
}
}
}
}
Here, UniswapV3Pool.sol
is the contract path, and UniswapV3Pool
is the contract name.
For the provided example, it would be:
rustsol generate_storage_bindings example/solc_output_uniswap3pool.json UniswapV3Pool.sol UniswapV3Pool example/src/generated_contract_uniswap3pool.rs
After running the above command, check the generated file generated_contract_uniswap3pool.rs
in the example/src
directory.
See example/src/run_*.rs
examples for bindings usage.
Bindings can also be generated from Rust using rustsol
as a library.
See the example/src/generate.rs
script in the example
directory:
cargo run -p example --bin generate
Solidity variable types are mapped to rustsol
binding types.
This mapping is shown in the table below.
Here, value_type
corresponds to a (possibly nested) rustsol
binding type, such as Primitive
or Mapping
.
The native_type
is a type to which the actual slot values are converted, such as u64
, bool
and U256
.
Note that the keys of Mappings
are also native types.
Solidity Type | Generated Rust Type |
---|---|
all integer types, bool, enum, address, contract, small bytes1..32 |
Primitive<byte_size, native_type> |
string, bytes | Bytes<String> , Bytes<Vec<u8>> |
static arrays | StaticArray<byte_size, value_type> |
dynamic arrays | DynamicArray<value_type> |
mapping | Mapping<key_native_type, value_type> |
struct | CustomNamedStructWithCorrespondingFields |
Note that all enums are mapped to Primitive<byte_size, U256>
.
It would be better to use native Rust enums as the native_type
instead of the generic U256
,
but this requires deeper Solidity contract analysis,
as information about enum field names is not available from the storage layout.
Variable values obtained with get_value()
are converted to native types according to the table below.
rustsol Type |
Generated Rust Type |
---|---|
Primitive<_, native_type> |
native_type |
Bytes<String> |
String |
Bytes<Vec<u8>> |
Vec<u8> |
StaticArray<value_type> |
Vec<recursive native type of value_type> |
DynamicArray<value_type> |
Vec<recursive native type of value_type> |
Mapping |
Mapping<key_native_type, value_type> getter |
SomeStruct |
SomeStructValue |
Note that Mapping
types are actually mapped to the same Mapping
.
This is because there is no way to get all elements of a Solidity mapping.
Therefore, we return a value getter instead of a concrete value.
This project is licensed under the MIT License.