-
Notifications
You must be signed in to change notification settings - Fork 316
/
Copy pathcap-0058.md
300 lines (211 loc) · 19.8 KB
/
cap-0058.md
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
## Preamble
```
CAP: 0058
Title: Constructors for Soroban contracts
Working Group:
Owner: Dmytro Kozhevin <@dmkozh>
Authors: Dmytro Kozhevin <@dmkozh>
Consulted:
Status: Draft
Created: 2024-07-17
Discussion: https://github.com/stellar/stellar-protocol/discussions/1501
Protocol version: 22
```
## Simple Summary
Introduce 'constructor' feature for Soroban smart contracts: a mechanism that
ensures that the contract will run custom initialization logic atomically
when it's first created.
## Working Group
As specified in the Preamble.
## Motivation
Constructor support improves usability of Soroban for the contract and SDK developers:
- A contract with a constructor is guaranteed to always be initialized, so there is never a need to ensure that initialization at run time, thus reducing the contract size, CPU usage, and potentially the contract storage needs
- Using constructors makes it harder for developers to accidentally make their contracts prone to front-running the initialization (in case if factory is not being used), thus improving security
- Constructors are supported in other smart contract frameworks/languages (such as Solidity), which both makes Soroban more compatible with the new SDKs (see [discussion](https://github.com/stellar/stellar-protocol/discussions/1501)), and also is more intuitive for developer on-boarding
- Constructors make contract instantiation cheaper and faster, as only a single transaction is necessary vs multiple transactions or a factory (that requires an additional contract invocation)
### Goals Alignment
This CAP is aligned with the following Stellar Network Goals:
- The Stellar Network should make it easy for developers of Stellar projects to create highly usable products.
## Abstract
This CAP introduces the set of the necessary changes to support defining the constructors and executing them, specifically:
- Reserve a new special contract function `__constructor` that may only be called by the Soroban host environment. The environment will call `__constructor` function if and only if the contract is being created from a Wasm exporting that function
- Introduce a new host function `create_contract_with_constructor` in Soroban environment that allows constracts to instantiate other contracts with constructors
- Introduce a new `HostFunction` XDR variant `HOST_FUNCTION_TYPE_CREATE_CONTRACT_V2` that acts in the same way as `HOST_FUNCTION_TYPE_CREATE_CONTRACT`, but also allows users to specify the constructor arguments
- Introduce a new `SorobanAuthorizedFunction` XDR variant `SOROBAN_AUTHORIZED_FUNCTION_TYPE_CREATE_CONTRACT_V2_HOST_FN` that allows users to sign the authorization payload corresponding to `HOST_FUNCTION_TYPE_CREATE_CONTRACT_V2` host function calls
## Specification
### XDR Changes
```
---
Stellar-transaction.x | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)
diff --git a/Stellar-transaction.x b/Stellar-transaction.x
index 87dd32d..7da2f1d 100644
--- a/Stellar-transaction.x
+++ b/Stellar-transaction.x
@@ -476,7 +476,8 @@ enum HostFunctionType
{
HOST_FUNCTION_TYPE_INVOKE_CONTRACT = 0,
HOST_FUNCTION_TYPE_CREATE_CONTRACT = 1,
- HOST_FUNCTION_TYPE_UPLOAD_CONTRACT_WASM = 2
+ HOST_FUNCTION_TYPE_UPLOAD_CONTRACT_WASM = 2,
+ HOST_FUNCTION_TYPE_CREATE_CONTRACT_V2 = 3
};
enum ContractIDPreimageType
@@ -503,6 +504,14 @@ struct CreateContractArgs
ContractExecutable executable;
};
+struct CreateContractArgsV2
+{
+ ContractIDPreimage contractIDPreimage;
+ ContractExecutable executable;
+ // Arguments of the contract's constructor.
+ SCVal constructorArgs<>;
+};
+
struct InvokeContractArgs {
SCAddress contractAddress;
SCSymbol functionName;
@@ -517,12 +526,15 @@ case HOST_FUNCTION_TYPE_CREATE_CONTRACT:
CreateContractArgs createContract;
case HOST_FUNCTION_TYPE_UPLOAD_CONTRACT_WASM:
opaque wasm<>;
+case HOST_FUNCTION_TYPE_CREATE_CONTRACT_V2:
+ CreateContractArgsV2 createContractV2;
};
enum SorobanAuthorizedFunctionType
{
SOROBAN_AUTHORIZED_FUNCTION_TYPE_CONTRACT_FN = 0,
- SOROBAN_AUTHORIZED_FUNCTION_TYPE_CREATE_CONTRACT_HOST_FN = 1
+ SOROBAN_AUTHORIZED_FUNCTION_TYPE_CREATE_CONTRACT_HOST_FN = 1,
+ SOROBAN_AUTHORIZED_FUNCTION_TYPE_CREATE_CONTRACT_V2_HOST_FN = 2
};
union SorobanAuthorizedFunction switch (SorobanAuthorizedFunctionType type)
@@ -531,6 +543,8 @@ case SOROBAN_AUTHORIZED_FUNCTION_TYPE_CONTRACT_FN:
InvokeContractArgs contractFn;
case SOROBAN_AUTHORIZED_FUNCTION_TYPE_CREATE_CONTRACT_HOST_FN:
CreateContractArgs createContractHostFn;
+case SOROBAN_AUTHORIZED_FUNCTION_TYPE_CREATE_CONTRACT_V2_HOST_FN:
+ CreateContractArgsV2 createContractV2HostFn;
};
struct SorobanAuthorizedInvocation
--
```
### Semantics
#### Constructor function
Every Wasm that exports `__constructor` function is considered to have a constructor. `__constructor` function may take an arbitrary number of arbitrary `SCVal`(XDR)/`Val`(Soroban host) arguments (0 arguments are supported as well).
The constructor has the following semantics from the Soroban host standpoint:
- For any Wasm with Soroban environment version less than 22, `__constructor` function is treated as any other unused Soroban 'reserved' function (any contract function that has name starting with double `_`), i.e. it can be exported, but can not be invoked. These contracts are considered to not have any constructor, even past protocol 22.
- Keep in mind, that it is not possible to upload Wasm with v22 environment prior to v22 protocol upgrade, thus it is guaranteed that for any given Wasm uploaded on-chain either all of, or none of the instances have constructor support (depending on the env version)
- For any Wasm with Soroban environment version 22 or greater `__constructor` is treated as constructor function:
- When a new contract is created from that Wasm, the environment calls `__constructor` with user-provided arguments immediately after creating a contract instance entry in the storage (without returning control to the caller)
- If `__constructor` hasn't finished successfully (i.e. if VM traps, or a function returns an error), creation function fails and all the changes are rolled back
- If `__constructor` succeeds, but returns a non-error value that is not `Val::Void` (unit-type return value in Rust), then then the creation function is considered to have failed as well
- If a contract with constructor is instantiated by a function that doesn't allow specifying the constructor arguments, then constuctor is called with 0 arguments.
- If a contract without constructor has any constructor arguments passed to it (i.e. >=1 arguments), the creation function fails
Other than the semantics described above, `__constructor` behaves as a normal contract function that can manipulate contract storage, do cross-contract calls etc.
`__constructor` must return `Val::VOID` (in the Rust SDK that is equivalent to returning no value). Returning any value that is not `Val::VOID` will result in error and contract won't be instantiated.
##### 'Default' constructor
The semantics of contracts that don't have a constructor (i.e. those created before protocol 22 and those that simply don't have `__constructor` defined) can be significantly simplified by treating them as having a 'default' constructor that accepts no arguments and performs no operations. This streamlines the user experience as it makes calling the contracts without constructor to behave exactly the same as contracts with a 0-argument constructor. That's why, for example, it is possible to instantiate a constructor-less contract with a function that expects constructor arguments, but only as long as 0 arguments are passed (passing >0 arguments to the 'default' constructor would cause it to fail).
##### Interaction with contract updates
Notice, that the above section refers only to creation of the contracts. Every contract may only be created just once (via `create_contract` host function or its `InvokeHostFunctionOp` counterpart). When the contract has its code updated it is not considered created and thus constructor won't be called.
This behavior has an important logical consequences: while the protocol guarantees that the constructor has been called if it was present in the _initial_ Wasm the contract has been created with, it does not guarantee that the constructor present in the _current_ Wasm of the contract has been called.
#### `create_contract_with_constructor` host function
A new function, `create_contract_with_constructor`, with export name `e` in module `l` ('ledger' module) is added to the Soroban environment's exported interface.
The `env.json` in `rs-soroban-env` will be modified as so:
```json
{
"export": "e",
"name": "create_contract_with_constructor",
"args": [
{
"name": "deployer",
"type": "AddressObject"
},
{
"name": "wasm_hash",
"type": "BytesObject"
},
{
"name": "salt",
"type": "BytesObject"
},
{
"name": "constructor_args",
"type": "VecObject"
}
],
"return": "AddressObject",
"docs": "Creates the contract instance on behalf of `deployer`. Created contract must be created from a Wasm that has a constructor. `deployer` must authorize this call via Soroban auth framework, i.e. this calls `deployer.require_auth` with respective arguments. `wasm_hash` must be a hash of the contract code that has already been uploaded on this network. `salt` is used to create a unique contract id. `constructor_args` are forwarded into created contract's constructor (`__constructor`) function. Returns the address of the created contract.",
"min_supported_protocol": 22
}
```
The `deployer` (`AddressObject`), `wasm_hash` (`BytesObject`), and `salt` (`BytesObject`) semantics have exactly the same semantics as for the existing `create_contract` host function. `constructor_args` is a host vector of `Val`s containing the arguments to use in constructor.
The function creates a contract from Wasm and calls its constructor with the provided `constructor_args`. Contracts with no constructor may be created by this function as well, but no arguments have to be provided.
#### `HOST_FUNCTION_TYPE_CREATE_CONTRACT_V2` host function
The new variant of the XDR `HostFunction` for the `InvokeHostFunctionOp` (`HOST_FUNCTION_TYPE_CREATE_CONTRACT_V2` defined in XDR changes section) allows users to create contracts both with and without constructors via a single transaction. The function is only supported from protocol 22 onwards.
Contracts can be created from any valid Wasm on-chain with this function, i.e. pre-22 Wasms are supported as well and are always treated as contracts with a default constructor (even if they happened to have `__constructor` function defined before).
`constructorArgs` argument of `CreateContractArgsV2` is a vector of arguments to pass to the constructor function.
The host function implementation is routed to `create_contract_with_constructor` host function.
The 'legacy' `HOST_FUNCTION_TYPE_CREATE_CONTRACT` XDR host function will be preserved in protocol 22 for the sake of backwards compatibility. It will only work with the contracts that don't have constructors defined.
#### Authorization support
`SOROBAN_AUTHORIZED_FUNCTION_TYPE_CREATE_CONTRACT_V2_HOST_FN` allows users to authorize both `HOST_FUNCTION_TYPE_CREATE_CONTRACT_V2` (when called directly from `InvokeHostFunctionOp`), and `create_contract`/`create_contract_with_constructor` host functions starting from protocol 22.
For `HOST_FUNCTION_TYPE_CREATE_CONTRACT_V2` exactly the same `CreateContractArgsV2` structure as in the `HostFunction` definition has to be authorized.
For `create_contract` calls the respective XDR with 0 `constructorArgs` has to be authorized, and for `create_contract_with_constructor` `constructorArgs` has to be set to respective respective vector of arguments.
Starting from protocol 22 `SOROBAN_AUTHORIZED_FUNCTION_TYPE_CREATE_CONTRACT_HOST_FN` will be deprecated and no longer accepted for authorizing any contract creation calls (including `HOST_FUNCTION_TYPE_CREATE_CONTRACT`).
##### Authorization context for custom accounts
Custom accounts must be aware of the context that they are authorizing. Thus we extend `AuthorizationContext` data structure that is passed to the special `__check_auth` function by adding a new enum variant `CreateContractWithCtorHostFn` to it (defined using the `contracttype` syntax of Soroban SDK):
```rust
#[contracttype]
struct CreateContractWithConstructorHostFnContext {
executable: ContractExecutable,
salt: BytesN<32>,
constructor_args: Vec<Val>,
}
#[contracttype]
enum AuthorizationContext {
Contract(ContractAuthorizationContext),
CreateContractHostFn(CreateContractHostFnContext),
CreateContractWithCtorHostFn(CreateContractWithConstructorHostFnContext),
}
```
`CreateContractWithCtorHostFn` is used to represent authorizing creating a contract with non-zero constructor arguments. Contracts that have a constructor that takes no arguments are still represented by `CreateContractHostFn` variant, which improves the backwards compatibility with the existing custom accounts.
Constructors with non-empty arguments can't be supported by the existing custom accounts that care about the context without potentially compromising their assumptions about the context, so if they try to strictly parse the new context variant they will fail. That will be the case for all the custom accounts built with the Soroban Rust SDK, as it fails in case if it can't parse the input argument. Note, that these custom accounts will still be able to authorize regular contract invocations and creating contracts that have the default or 0-argument constructor.
##### 'Deep' invoker authorization
The specification above only refers to externally provided authorization. Soroban also has a capability of authorizing 'deep' (i.e. non-direct) cross-contract calls on behalf of the current contract (invoker). We add support for authorizing creating contracts with constructor by extending the `InvokerContractAuthEntry` enum with the new variant `CreateContractWithCtorHostFn`:
```rust
#[contracttype]
enum InvokerContractAuthEntry {
Contract(SubContractInvocation),
CreateContractHostFn(CreateContractHostFnContext),
CreateContractWithCtorHostFn(CreateContractWithConstructorHostFnContext),
}
```
`CreateContractWithConstructorHostFnContext` is exactly the same struct as defined in the section above.
Since there are no signatures involved, we can provide more flexible authorization rules, specifically:
- Contracts that have a default constructor or a 0-argument constructor can be authorized either by authorizing the respective `CreateContractHostFn` entry, or by authorizing `CreateContractWithCtorHostFn` entry with 0 constructor arguments.
- Contracts that have a constructor with more than 0 arguments must be authorized with the respective `CreateContractWithCtorHostFn` entry.
This way the existing contract may still authorize creation of the contracts with 'trivial' constructors, but must be updated in order to authorize contracts that have non-trivial constructors.
## Design Rationale
### Constructor vs Factory
Factory contracts can be used as an alternative to constructors and they are already supported by the protocol. However, factory contracts don't provide a guarantee that every contract instance has been initialized via a factory.
In theory, we could standardize certain generic factory contract, build it into the protocol and provide initialization guarantees. While that approach would allow us to avoid adding new interfaces, it is much less straightforward to use - some (constructor-less) contracts would still be created via `create_contract`/`HOST_FUNCTION_TYPE_CREATE_CONTRACT`, while other(constructor-enabled) contracts would be created via `call`/`HOST_FUNCTION_TYPE_INVOKE_CONTRACT`.
### Only a single constructor function is allowed
Allowing to overload constructors would be pretty tricky as Wasm doesn't support overloading functions.
If 'overload' behavior is necessary, it can be implemented in custom fashion via e.g. using a single `enum` argument with different variants containing different sets of arguments.
### Returning custom values from constructor is not allowed
The constructor return value has no semantics from the perspective of this CAP, so in theory we could allow contracts to return arbitrary values from constructors and just discard them after calling the constructor. However, this might lead to incorrect developer expectations. For example, a developer might return a custom 'error-code' as integer, or a boolean flag and expect that it is respected by the Soroban host. This is an unlikely case, but since we don't want to make any potentially incorrect assumptions about behavior of the user-defined contracts, we explicitly disallow returning any non-error values besides `Val::VOID`.
Note, that returning an error-type value (`Val::Error`) has the same consequences as returning any other custom value (i.e. the constructor is considered to have failed and contract creation is aborted), even though semantically these cases are different (the constructor failed vs constructor succeeded, but returned an unsupported value).
### Constructor is not called when Wasm is updated
Contract re-initialization is risky for a lot of cases, e.g. for contracts with complex ownership model (DAO), multisig smart wallets, or contracts that have a part of state assumed to be constant. For all such scenarios a malicious or non-malicious buggy interaction may break the important invariants, used to revert contract to no longer state or even simply break it for everyone.
While in some cases additional initialization may be required after Wasm has been updated, we deem the risks of allowing it to be higher than the benefits for such cases. Unlike initialization, atomicity is not as important, because the authorization priviliges don't change between the call to update the contract and the call to re-initialize it. Thus a programmatic, contract-specific solution should be feasible.
### `SOROBAN_AUTHORIZED_FUNCTION_TYPE_CREATE_CONTRACT_HOST_FN` deprecation
In order to prevent signature malleability, an auth payload has to match to a specific action (e.g. we can't just accept both v1 and v2 payloads to authorize `CREATE_CONTRACT_HOST_FN`). So `SOROBAN_AUTHORIZED_FUNCTION_TYPE_CREATE_CONTRACT_HOST_FN` has to be used in some very specific contexts and it's not immediately obvious for the user why in certain context v1 has to be used vs why v2 should be used. That's why for the sake of clarity we deprecate `SOROBAN_AUTHORIZED_FUNCTION_TYPE_CREATE_CONTRACT_HOST_FN` in protocol 22.
Since most of the Soroban interactions happen via RPC simulation, we don't expect significant client discruption. However, the RPC clients will have to update their XDR definitions in order to be able to handle the new varint of the auth payload.
There is also a small chance that a transaction gets rejected when being applied right after the upgrade, but that's an improbable and one-time event (contract instantiations are much more rare than regular contract interactions).
## Security Concerns
Constructors ensure atomic contract initialization that has to be authorized by the deployer of the contract (via `SOROBAN_AUTHORIZED_FUNCTION_TYPE_CREATE_CONTRACT_V2_HOST_FN` authorization payload). Thus, deployed contract can't have their initialization to be frontrun.
Executing `__constructor` function from within host allows contracts to execute arbitrary logic while being called from a host function. However, the risk surface is not really increased compared to regular contract calls, as the caller acknowledges that constructor will be called, similarly to how the caller acknowledges that a different contract's method is going to be called.
## Test Cases
To be implemented. The tests should cover the newly introduced host function and the invariants described in the 'Semantics' section.
## Implementation
To be implemented.