|
| 1 | +# ADR: Smart Contract Return Value |
| 2 | + |
| 3 | +## Date |
| 4 | + |
| 5 | +2025-05-02 |
| 6 | +Last status update: 2025-05-02 |
| 7 | + |
| 8 | +## Status |
| 9 | + |
| 10 | +- [ ] Proposed |
| 11 | +- [x] Accepted |
| 12 | +- [ ] Rejected |
| 13 | +- [ ] Deprecated |
| 14 | +- [ ] Superseded by ADR #X |
| 15 | + |
| 16 | +### Implementation Status |
| 17 | + |
| 18 | +- [x] Planned |
| 19 | +- [ ] In Development |
| 20 | +- [ ] Implemented |
| 21 | +- [ ] Verified |
| 22 | +- [ ] Discontinued |
| 23 | + |
| 24 | +## People |
| 25 | + |
| 26 | +- **Decision-makers**: KP |
| 27 | +- **Consulted**: @dedok, @andor0 |
| 28 | +- **Informed**: |
| 29 | + - [x] @rnbta |
| 30 | + - [x] @zotho |
| 31 | + |
| 32 | +## Context |
| 33 | + |
| 34 | +A PolkaVM program is expected to return a numeric value, such as a `u64`, as shown below. |
| 35 | + |
| 36 | +```rust |
| 37 | +extern "C" fn main() -> u64 { |
| 38 | +``` |
| 39 | + |
| 40 | +The smart contracts' `main()` function must follow this convention and have the same signature. |
| 41 | + |
| 42 | +Currently, in the QF Network Smart Contracts platform, the return value is used as the computation result handler from the smart contract to the `pallet-qf-polkavm` runtime module. And it is stored on-chain after each call. |
| 43 | + |
| 44 | +```rust |
| 45 | +let result: u64 = instance.call_typed_and_get_result::<u64, ()>(&mut state, "main", ()); |
| 46 | + |
| 47 | +let (result, not_enough_gas, trap) = match result { |
| 48 | + Err(CallError::NotEnoughGas) => (None, true, false), |
| 49 | + Err(CallError::Trap) => (None, false, true), |
| 50 | + Err(_) => Err(Error::<T>::PolkaVMModuleExecutionFailed)?, |
| 51 | + Ok(res) => (Some(res), false, false), |
| 52 | +}; |
| 53 | + |
| 54 | +// snip |
| 55 | + |
| 56 | +ExecutionResult::<T>::insert( |
| 57 | + (&contract_address, version, &who), |
| 58 | + ExecResult { |
| 59 | + result, |
| 60 | + not_enough_gas, |
| 61 | + trap, |
| 62 | + gas_before: gas_limit, |
| 63 | + gas_after: instance.gas(), |
| 64 | + }, |
| 65 | +); |
| 66 | +``` |
| 67 | + |
| 68 | +## Problem |
| 69 | + |
| 70 | +### Current Implementation |
| 71 | + |
| 72 | +- Each smart contract call writes the return value to the on-chain storage. |
| 73 | +- Existing implementation limits future support for cross-contract calls, return values handing over the oh-chain storage is inefficient. |
| 74 | +- Even read-only smart contract's functions require a transaction to get the computation result. |
| 75 | + |
| 76 | +## Decision |
| 77 | + |
| 78 | +### Proposed Solution |
| 79 | + |
| 80 | +#### Alternative 1: Primary |
| 81 | + |
| 82 | +- Remove the saving of smart contract return values. |
| 83 | + 1. Remove `ExecutionResult` storage item (with its data deletion using a migration). |
| 84 | + 1. Update `execute` extrinsic removing `ExecutionResult` usage. |
| 85 | + 1. Update affected smart contract examples. |
| 86 | +- Reserve the return value for future use (see Alternative 2, Alternative 3). |
| 87 | +- Consider read-only[^1] [^2] functions invocation without a transaction (through the "dry run"[^3]) separately. |
| 88 | + 1. Blockchain data, thus the contract state, is available for UIs over RPC. Keys are known in advance as well as the value decoding algorithm. |
| 89 | + 1. Currently all smart contracts state is written to the pallet storage item and can be easily retrieved over the Polkadot/Substrate Portal RPC without an additional UI. See <https://youtu.be/Xs_uBU86XIE?si=MvOd0-qqw13-nSkL>. |
| 90 | + |
| 91 | +**Pros:** |
| 92 | + |
| 93 | +- Prevents unnecessary on-chain storage growth and unifies smart contract data writing (writes are defined within the contract body). |
| 94 | +- Doesn't prevent efficient cross-contract calls implementation with return value exchange over the non-persistent memory. |
| 95 | +- Defers the decision on how to utilize return values until it is actually needed. |
| 96 | + |
| 97 | +#### Implementation Notes |
| 98 | + |
| 99 | +- Live networks, such as the QF Network Testnet, may require a migration to remove the `ExecutionResult` storage item. |
| 100 | +- Existing examples will need to be updated to remove reliance on return value storage. |
| 101 | + |
| 102 | +### Alternative 2: return value is an error code |
| 103 | + |
| 104 | +### Alternative 3: return value is a pointer to a larger struct |
| 105 | + |
| 106 | +### Comparison |
| 107 | + |
| 108 | +#### Polkadot SDK's `pallet-contracts` |
| 109 | + |
| 110 | +The runtime may receive a `Vec<u8>` as the return value from a smart contract call. |
| 111 | + |
| 112 | +```rust |
| 113 | +// https://github.com/paritytech/polkadot-sdk/blob/c2174ff3505eaa1ea1eb047b784a0ed7afe1cbd7/substrate/frame/contracts/src/primitives.rs#L112 |
| 114 | +pub struct ExecReturnValue { |
| 115 | + /// Flags passed along by `seal_return`. Empty when `seal_return` was never called. |
| 116 | + pub flags: ReturnFlags, |
| 117 | + /// Buffer passed along by `seal_return`. Empty when `seal_return` was never called. |
| 118 | + pub data: Vec<u8>, |
| 119 | +} |
| 120 | +``` |
| 121 | + |
| 122 | +This value is not persisted on-chain, but is instead written to WASM sandbox memory. |
| 123 | + |
| 124 | +```rust |
| 125 | +// https://github.com/paritytech/polkadot-sdk/blob/c2174ff3505eaa1ea1eb047b784a0ed7afe1cbd7/substrate/frame/contracts/src/wasm/runtime.rs#L1048-L1057 |
| 126 | +if let Ok(output) = &call_outcome { |
| 127 | + self.write_sandbox_output( |
| 128 | + memory, |
| 129 | + output_ptr, |
| 130 | + output_len_ptr, |
| 131 | + &output.data, |
| 132 | + true, |
| 133 | + |len| Some(RuntimeCosts::CopyToContract(len)), |
| 134 | + )?; |
| 135 | +} |
| 136 | +``` |
| 137 | + |
| 138 | +A dry run, implemented via the [pallet-contracts::ContractsApi runtime API](https://github.com/paritytech/polkadot-sdk/blob/c2174ff3505eaa1ea1eb047b784a0ed7afe1cbd7/substrate/frame/contracts/src/lib.rs#L1937), is used for state queries and getters. This approach is demonstrated in [ink-examples](https://github.com/use-ink/ink-examples/blob/c72b4cb0d6cfd7229a4c441b789d86c07e995451/flipper/frontend/src/App.tsx#L162) and the [use-inkathon](https://github.com/scio-labs/use-inkathon/blob/496538322521bcb49b454be7bd05ca40ef9e2aaf/src/helpers/contractCall.ts#L18) library. |
| 139 | + |
| 140 | +#### Polkadot SDK's `pallet-revive` |
| 141 | + |
| 142 | +The runtime handles smart contract function return values similarly to `pallet-contracts`. Notably, host functions exposed to the smart contract - which must also return a numeric value - are treated either as a `ReturnCode` or as a value containing the actual output. |
| 143 | + |
| 144 | +```rust |
| 145 | +// https://github.com/paritytech/polkadot-sdk/blob/c2174ff3505eaa1ea1eb047b784a0ed7afe1cbd7/substrate/frame/revive/uapi/src/lib.rs#L125 |
| 146 | +pub struct ReturnCode(u32); |
| 147 | + |
| 148 | +// https://github.com/paritytech/polkadot-sdk/blob/c2174ff3505eaa1ea1eb047b784a0ed7afe1cbd7/substrate/frame/revive/uapi/src/host/riscv64.rs#L39 |
| 149 | +pub fn set_storage( |
| 150 | + flags: u32, |
| 151 | + key_ptr: *const u8, |
| 152 | + key_len: u32, |
| 153 | + value_ptr: *const u8, |
| 154 | + value_len: u32, |
| 155 | +) -> ReturnCode; |
| 156 | + |
| 157 | +// snip |
| 158 | + |
| 159 | +pub fn gas_price() -> u64; |
| 160 | +``` |
| 161 | + |
| 162 | +## References |
| 163 | + |
| 164 | +[^1]: Solidity view functions, <https://docs.soliditylang.org/en/latest/contracts.html#view-functions>. |
| 165 | +[^2]: ink! immutable functions, <https://use.ink/docs/v5/basics/mutating-values#mutable-and-immutable-functions>. |
| 166 | +[^3]: Dry-run via RPC, <https://use.ink/docs/v5/getting-started/calling-your-contract/#dry-run-via-rpc>. |
0 commit comments