Decoding a struct from provider.getStorageAt #2373
-
I have a struct struct MyStruct { that is stored in contract memory as: Is there a way to decode this back into a struct with ethers? I've tried: |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 3 replies
-
I've also been wondering the same thing. I couldn't find a single example of decoding a packed storage slot value anywhere, either for ethers.js, web3.js or otherwise. Closest article I found was here but they don't show their work for the packed slot example. Seems like a pretty important thing to be able to do easily to audit smart contracts that make certain storage variables internal/private, which are often important to the behavior of the contract. I couldn't get this to work with const { ethers } = require('ethers')
let encodedUints = ethers.utils.defaultAbiCoder.encode(
['uint32', 'uint32'],
[1, 2]
)
console.log(encodedUints)
// 0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002
console.log(
ethers.utils.defaultAbiCoder.decode(['uint32', 'uint32'], encodedUints)
)
// [ 1, 2 ] The closest thing I found related to packed values was the ethers.utils.solidityPack function, but there's no corresponding decode/unpack function. In my case I spent the better part of a day trying to figure out how to decode some of the internal variables Chainlink uses in their ETH/USD price feed aggregator contract, like the const ETH_USD_PRICE_FEED_CONTRACT_ADDRESS = '0x37bC7498f4FF12C19678ee8fE19d713b87F6a9e6'
const THRESHOLD_STORAGE_SLOT = 42
let storageSlotValue = await provider.getStorageAt(
ETH_USD_PRICE_FEED_CONTRACT_ADDRESS,
THRESHOLD_STORAGE_SLOT
)
// Location of threshold variable is in a packed internal struct:
/*
struct HotVars {
bytes16 latestConfigDigest;
uint40 latestEpochAndRound;
uint8 threshold;
uint32 latestAggregatorRoundId;
}
HotVars internal s_hotVars;
*/
// s_hotVars storage slot value
// 0x000000000000000081170a0001ff2c029f88d5f42679b75c4a0244716f695ef3
// Since we know slot values are 32 bytes and the hex representation is 64 characters, each byte is 2 characters long
// So bytes16 -> 16 bytes -> 32 characters
// uint40 -> 40 bits / 8 bits = 5 bytes -> 10 characters
// uint8 -> 8 bits / 8 bits = 1 byte -> 2 characters
// uint32 -> 32 bits / 8 bits = 4 bytes -> 8 characters
// segmented from right to left by types bytes16, uint40, uint8, uint32
// 0x000000000000|00008117|0a|0001ff2c02|9f88d5f42679b75c4a0244716f695ef3
// template '0x0000000000000000000000000000000000000000000000000000000000000000'
// latestConfigDigest = 0x00000000000000000000000000000000
// value = '0x000000000000000000000000000000009f88d5f42679b75c4a0244716f695ef3'
// types = ['bytes16']
// latestEpochAndRound = 33500162
// value = '0x0000000000000000000000000000000000000000000000000000000001ff2c02'
// types = ['uint40']
// threshold = 10
value = '0x000000000000000000000000000000000000000000000000000000000000000a'
types = ['uint8']
// latestAggregatorRoundId = 33047
// value = '0x0000000000000000000000000000000000000000000000000000000000008117'
// types = ['uint32']
console.log(ethers.utils.defaultAbiCoder.decode(types, value))
// [ 10 ] Anyhow that's certainly error prone and it would be better to write a function to do this. I'm surprised there appear to be no direct utility functions for this built in to these tools, as it's fairly common to encounter this scenario, and they're the only tools I see recommended whenever someone asks how to read storage values directly from a deployed smart contract. Even if you're not purposefully trying to obfuscate the code to introduce a vulnerability, the default way smart contracts are written still makes it quite difficult to understand exactly how many deployed contracts work by only exposing public state variables that users/viewers are meant to query, while hiding private/internal variables that are meant to be used for implementation details. The value of a private/internal state variable can completely change how a contract works, so IMO it should be just as easy to query private/internal variables, just while having them scoped under a private/internal/hidden namespace (or something like a dropdown for explorer interfaces like etherscan) to maintain the clean public user interface. All in all, you have to 1. find the exact smart contract source code, often across multiple files, 2. copy them all over and compile them through Remix or a command line tool to find out the exact storage slot of the variable you're looking for (its incredibly time consuming and error prone try to count storage slots manually for even smaller sized contracts), 3. run some code to query for the storage slot value, and then on top of that, 4. manually parse the value or write custom code to decode it if it's a packed slot. Really puts a dent in the common perception that "anyone can view and audit the smart contracts" to figure out what they do. If you're a non-technical user, forget it. Anyways maybe there is actually an easy way to do this and It's just my relative ignorance at the moment. I think I'll open a stack overflow question about this to hopefully get some further input from the community. |
Beta Was this translation helpful? Give feedback.
-
For very specific situations, you may be able to create a function that decides a storage slot, but that could easily break between instances of Solidity. The packing solidity uses internally is baked into the contract at compile time, which is one reason giving access to another contract’s storage could be dangerous. Two versions of solidity may store the internal representation differently, or even the same version of solidity could (theoretically) store different internal representations based on the optimizer settings. For example, the way solidity stores bytes and strings in storage is completely unrelated to how bytes are encoded as ABI; since most strings are short, special sentinel values and byte offsets are used so that strings 31 bytes or less are crammed into a single storage slot, while ABI coding would require 2. And future versions of solidity are free to change up the game however they see fit, as storage is intended to be more or less private. So it is very discouraged to try writing any generic function to decode this data; a better solution is to add a method to the contract that acts as a getter that returns the necessary struct, as this means the contract (at compile time) will correctly account for any optimizer and version quirks when unpacking the data from storage. Does that make sense? |
Beta Was this translation helpful? Give feedback.
I've also been wondering the same thing. I couldn't find a single example of decoding a packed storage slot value anywhere, either for ethers.js, web3.js or otherwise. Closest article I found was here but they don't show their work for the packed slot example. Seems like a pretty important thing to be able to do easily to audit smart contracts that make certain storage variables internal/private, which are often important to the behavior of the contract.
I couldn't get this to work with
decode
as it appears to assume the hex value is unpacked, whereas in reality some slot values are packed when neighboring data types are small enough, like in your example above. I discovered the default e…