Skip to content

Commit e393bb1

Browse files
authored
ADR on Smart contract return value (#357)
* Quick draft for a quick review * Reflect feedback, add prior work overview * More implementation details * Remove "dry run" implementation proposal * Reference "dry run" definition * Follow the template * Add read only function references
1 parent 86125ac commit e393bb1

File tree

1 file changed

+166
-0
lines changed

1 file changed

+166
-0
lines changed
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
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

Comments
 (0)