How to design your contract to limit the amount of leaked data from storage access patterns
Coming Soon!
Full example guide on mitigating privacy risks.
Here we use a sample contract for selling a secret message to illustrate padding, gas evaporation, and side-chain attack prevention using a trusted actor to confirm transactions. In the contract we define three methods: posting a secret for sale, buying a secret, and confirming purchases. All three message types have two optional parameters: gas_target
and padding
. These will be used to help mask which message is being called.
execute
Padding and evaporation are parameters that are set for all message types.
The padding
parameter is used by the client to pad the size of message being sent to the contract. The padding
parameter is not used within the contract itself and is only necessary when using Keplr's amino format. In other cases the dapp or wallet can simply add extra space characters to the end of the json message.
We also want to pad the result sent back to the client. We can use the pad_handle_result
function from secret toolkit to format the response to a fixed block size. If you have responses that are large, you will need a sufficiently large block size to successfully mask the method being run.
After padding the result at the end of execute
entry point function, we use gas_target
along with the evaporation api functions to evaporate any remaining gas to hit the gas target. Note, we simply ignore both the padding
and the gas_target
message parameters using ..
in the match statement.
Just like we want to use padding to hide the message type when sending between the client and the contract, the number of bytes written to the chain can leak information about what is written. For example, a coin amount stored as a Uint128
will write a different number of bytes depending on the size of the number, and also in our example the secret message could leak information based on its length. For data structs we want to store on chain we usually create a stored version and create conversion methods between the unstored and stored versions. Note in the example, we use the secret toolkit space_pad
function to make all secrets stored as 256 bytes long masking the content.
Padding and evaporation help to protect privacy about what types of functions are being called in a contract and about what data is being read and written from storage. However, our contract is still vulnerable to a side chain attack if a buyer can simply purchase a secret without a trusted actor confirming the transaction. This is because a malicious actor could fork the chain and buy the secret on the forked chain without ever paying any coins on the mainnet.
In our case a trusted actor who can confirm the purchase is the seller of the secret. If all purchases must have the ConfirmPurchase
transaction performed after buying, then it is impossible for the side-chain attack to occur. Other contracts that need to implement this functionality might require a trusted third-party actor instead, for example a game contract where turns are executed on-chain.
The "potential attack vector" section in the Encryption specs of the documentation highlights most of the privacy attacks developers should consider.
The "Privacy essentials" section also quickly describes everything to take into account.
The equivalent of padding for gas_used
When a smart contract is executed, CosmWasm meters how much gas the execution consumes while the program is running. On Secret Network, this all happens within the enclave. However, once execution completes, the total amount of gas that was consumed leaves the enclave and is made public in transaction metadata.
The public gas_used
result of a contract execution message reflects the sum of costs for the actual instructions that were executed. Within a typical smart contract, most of its methods consume different amounts of gas. With SNIP-20 for example, an attacker could deduce which method a transaction executed with a high degree of certainty; create_viewing_key
vs. transfer
vs. increase_allowance
, each of which produce a distinct gas_used
amount in the public transaction metadata.
In some cases, this can leak even more information such as which code path was taken, the value of a conditional, how many iterations a loop completed, and so on.
Evaporation is a concept that was introduced to overcome the privacy risks associated with gas_used
. Evaporation refers to the practice of deliberately consuming extra gas during execution in order to pad the gas_used
amount before it leaves the enclave.
With evaporation, clients can now instruct contracts on exactly how much gas their execution should consume, yielding a consistent gas_used
across all methods.
A contract can evaporate an arbitrary amount of gas using the built-in API function gas_evaporate
. In order to evaporate any remaining gas, the check_gas
API function allows contracts to calculate the difference between the amount consumed up to that point, and the amount the client wants the contract to consume by the end of its execution:
See the evaporation example contract's source code for more details.
Note that there is no API function to get the amount of actual gas remaining. Instead, that information must come from the client. This practice ensures that users are still able to execute multiple messages in a single transaction by specifying gas limits to each contract execution individually.
The new check_gas
API function essentially gives contracts read access to CosmWasm's internal gas metering. Calling this function more than once during execution allows contracts to track changes in the amount of gas consumed. This simple feature enables a realm of interesting use cases not previously possible.
In its simplest form, gas tracking can be used as a development aid to inspect the amount of gas used by certain blocks of code, giving developers more insight and help with optimizing their gas footprint.
A more advanced use case involving gas tracking is opportunistic execution, where contracts take advantage of excess gas that would otherwise be evaporated by performing work that needs to be done anyway. That work could be anything from generic housecleaning duties, such as processing items in a queue, to more intentional duties, such as notarizing previous user actions to protect against tx-replay attacks.
To give an example, imagine an NFT auction contract that accepts bids on listed items. When a listing expires, the listed NFT should be transferred to the highest bidder. In typical contract design, the transfer would only actually happen once the winning bidder calls a method to "claim" their winnings. However, with opportunistic execution, the contract can automatically perform the transfer during any execution. For example, someone creating a viewing key could end up paying the gas fees to complete the transfer of a previous listing's NFT to its winning bidder.