cap-0046-02.md
February 27, 2024 ยท View on GitHub
Preamble
CAP: 0046-02 (formerly 0047)
Title: Smart Contract Lifecycle
Working Group:
Owner: Siddharth Suresh <@sisuresh>
Authors: Siddharth Suresh <@sisuresh>, Dmytro Kozhevin <@dmkozh>, Jay Geng <@jayz22>
Consulted: Graydon Hoare <@graydon>, Jon Jove <@jonjove>, Leigh McCulloch <@leighmcculloch>, Nicolas Barry <@MonsieurNicolas>
Status: Final
Created: 2022-05-02
Discussion:
Protocol version: 20
Simple Summary
This proposal defines the structure of smart contracts on Stellar and specifies how users can create them.
Motivation and Goals Alignment
See the Soroban overview CAP.
Abstract
Users need a way to manage smart contracts on the network. This CAP allows users to deploy the smart contracts to the network and specifies the supported contract code kinds.
Specification
XDR
See the XDR diffs in the Soroban overview CAP, specifically those referring to
HOST_FUNCTION_TYPE_CREATE_CONTRACT and
HOST_FUNCTION_TYPE_UPLOAD_CONTRACT_WASM.
Semantics
Contract structure
This defines the terms we use in the following sections without going into their design and implementation details.
Contract source
Contract source can be thought of as a 'class' of a contract. Multiple contracts can share the same source, but have their own state. Thanks to that sharing capability, we can reduce the amount of duplication in ledger and only store unique contract sources.
This CAP defines two possible kinds of contract sources:
- Wasm source: a blob of Wasm code that is stored in a separate ledger entry and is deduplicated based on contents. This is uploaded to ledger by the users.
- Built-in contract: this is a 'source' compiled into host directly that has a protocol-defined interface and behavior.
Contract executable
The Contract executable contains a pointer to the Wasm source or a tag of a built-in contract.
Contract instance
Contract instance can be thought of as an instance of the contract 'class'. Contract instance consists of:
- A
ContractExecutable. - An optional
SCMap*for users to store data that should be tied to the lifetime of the instance (./cap-0046-02.md).
A contract instance may own an arbitrary amount of ledger entries attributed to its identifier. Contracts that share the same source in no way may influence each other; from the perspective of a contract invoker there is no difference between calling the contracts with the same or different source references (besides the possible contract-defined behavior differences).
Contract identifier preimage type
ENVELOPE_TYPE_CONTRACT_ID is the HashIDPreimage type used for contract
identifiers. This unique tag is what ensures that there are no collisions with
other hashes in the protocol. It contains the Hash of the networkID to
ensure that every network has unique set of contract identifiers, along with a
ContractIDPreimage, which is a union that supports both
CONTRACT_ID_PREIMAGE_FROM_ADDRESS and CONTRACT_ID_PREIMAGE_FROM_ASSET.
CONTRACT_ID_PREIMAGE_FROM_ADDRESS: built from an anSCAddressand the user-specifieduint256salt.CONTRACT_ID_PREIMAGE_FROM_ASSET: built from a StellarAssetstructure.
Uploading Wasm sources using InvokeHostFunctionOp
Wasm contract sources can be uploaded to the network without instantiating a
contract via InvokeHostFunctionOp(defined in CAP-0046-04)
with HOST_FUNCTION_TYPE_UPLOAD_CONTRACT_WASM host function type in
hostFunction.
This function accepts opaque wasm<> that contains the Wasm
contract code.
Uploaded contracts are stored in ContractCodeEntry ledger entries. These
entries are keyed by the hash of the Wasm used to upload
them.
The contract upload host function will compute the hash of
the Wasm and check if such a contract code already exists. If
the entry exists, the operation will immediately succeed. If it doesn't, the
new ContractCodeEntry will be created.
Core does not perform any validation on the uploaded contract code, besides checking its size.
Max contract size setting
The maximum Wasm contract size will be introduced as a ConfigSettingEntry(see
CAP-0046-09 for details on config entries).
It is set during the protocol version upgrade using a new ConfigSettingEntry,
with configSettingID == CONFIG_SETTING_CONTRACT_MAX_SIZE_BYTES, and
contractMaxSizeBytes == 65536. The valid values for
contractMaxSizeBytes are [5000, UINT32_MAX] (inclusive).
Instantiating contracts using InvokeHostFunctionOp
Contracts can be instantiated via InvokeHostFunctionOp with
HOST_FUNCTION_TYPE_CREATE_CONTRACT host function type.
The function accepts CreateContractArgs struct that defines the input for
building the contract identifier preimage (contractIDPreimage field) and the
contract executable reference (executable field).
InvokeHostFunctionOp's auth vector will also require a
SorobanAuthorizationEntry with credentials that match the address in
CreateContractArgs used to derive the contractID (if the preimage is
CONTRACT_ID_PREIMAGE_FROM_ASSET, then no auth is required).rootInvocation
should be set to a SorobanAuthorizedInvocation where function is of type
SOROBAN_AUTHORIZED_FUNCTION_TYPE_CREATE_CONTRACT_HOST_FN.
function.createContractHostFn should be set to the CreateContractArgs used
under the HOST_FUNCTION_TYPE_CREATE_CONTRACT host function mentioned above.
The executable and identifier arguments are normally independent of each other
with an exception: identifiers that are built from
CONTRACT_ID_PREIMAGE_FROM_ASSET may only be used in conjunction with built-in
token contract source. This handles the special case of instantiating token
contracts corresponding to the classic Stellar assets (see more details in
CAP-0046-06).
The host builds the actual contract identifier by computing SHA-256 of the
HashIDPreimage corresponding to the contractIDPreimage. If the contract
identifier already exists, the operation fails.
If the identifier is new, the host will create a new Persistent
ContractDataEntry from CAP-0046-05 with a
SCV_LEDGER_KEY_CONTRACT_INSTANCE key value. The value of the entry is
ScContractInstance that either refers to the Wasm code entry or to a built-in
contract (according to the value of the executable field in CreateContractArgs).
Instantiating a contract from a contract
Factory contracts are quite popular already on other networks, so this CAP adds functionality to support them.
The following host functions are provided to instantiate contracts and upload Wasm:
// Uploads the Wasm. Returns the SHA-256 hash of the Wasm code.
fn upload_wasm(wasm: Bytes) -> Bytes
// Creates a Wasm instance using the deployer, SHA-256 hash of the Wasm, and a user specified salt.
// Returns the Address of the newly created contract.
fn create_contract(deployer: Address, wasm_hash: Bytes, salt:Bytes) -> Address
// Creates a Stellar Asset Contract for the XDR serialized asset passed in. Returns the Address
// for the newly created contract.
fn create_asset_contract(serialized_asset: Bytes) -> Address
The contractIDs for the contracts created with create_contract and
create_asset_contract are derived from CONTRACT_ID_PREIMAGE_FROM_ADDRESS and
CONTRACT_ID_PREIMAGE_FROM_ASSET respectively.
Similar to how contract creation through HOST_FUNCTION_TYPE_CREATE_CONTRACT
requires authorization mentioned
above, invoking the
create_contract host function requires authorization as well.
Updating a contracts code
We also provide a host function that allows contract instances to update the
Wasm executable by first uploading the new code, and then calling
update_current_contract_wasm with the hash of the newly uploaded Wasm.The
update happens only after the current contract invocation has successfully
finished, so this can be safely called in the middle of a function.
// Updated the current contracts Wasm executable.
fn update_current_contract_wasm(wasm_hash: Bytes)
Invoking a contract using InvokeHostFunctionOp
Contracts can be invoked via InvokeHostFunctionOp with
HOST_FUNCTION_TYPE_INVOKE_CONTRACT host function type.
The function accepts InvokeContractArgs struct that consists of the contractAddress, functionName and the args array.
The auth vector must be properly filled with the required credentials and the correct invocation details. Refer to Soroban Authorization Framework for details.
If the invocation is successful, the return value ScVal along with ContractEvents will be
used to construct a InvokeHostFunctionSuccessPreImage, from which the hash is computed and
returned in the InvokeHostFunctionResult.
If the invocation fails, the InvokeHostFunctionResult will contain the proper error code
indicating the failure reason.
The return value as well as all events (ContractEvents and DiagnosticEvents) will be included in the TransactionMeta, see cap-0046-08 for details.
Invoking a contract from a contract
CAP-0046-03 specifies the host functions that can be used for cross-contract invocations.
The auth vector must be properly filled with the required credentials and the correct invocation details. Refer to Soroban Authorization Framework for details.
Extend and restore contract
Both the contract instance and the contract code entry are persistent ledger entries which have finite, pre-specified TTLs.
We provide two additional operations ExtendFootprintTTLOp and RestoreFootprintOp for extending TTL and restoring entries.
See State Archival Interface Cap for more details on archival semantics and operations for extending and restoring entries.
Design Rationale
There are no built in controls for contracts
Controls like pausing invocation or mutability for all or a subset of a contract should be put into a contract itself. Leaving it to the contract writer is a much more general solution than baking it into the protocol. The downside is this is more error prone and will take more space since the same logic will be implemented multiple times.
ContractDataEntry has no owner associated with it
The entity that creates the ContractDataEntry that contains the contract code
is not tied to it in any way. This allows for contract management and
authorization to be handled in the contract using whichever custom mechanism
the contract creator chooses.
ContractCodeEntry has no owner associated with it
Contract source code entries with the Wasm code don't have any ownership. Anyone can upload contract sources to the ledger and then anyone can use them. This encourages sharing the contract code and allows contracts that use it to be sure that their implementation can't unexpectedly change.
Contracts cannot be deleted, and can only be updated through the update_current_contract_wasm host function
The contract code reference is stored in a ContractDataEntry, but the host
functions in CAP-0046-05 to set,
update, or delete ContractDataEntry should trap if they are used on contract
code.
Malicious contracts
The validators do not have a mechanism to ban specific contracts. Any kind of targeted banning mechanism can be worked around quite easily by creating new accounts and contracts.
Maximum Wasm contract code size is configurable
The maximum contract size will be set during the protocol upgrade, and can be updated by the validators. This allows to adjust the contract sizes depending on the demand and network load requirements.
ContractIDs are deterministic
Pulling contractIDs from LedgerHeader.idPool would be easier but it would make
parallelizing contract creation more difficult in the future. It's also more
difficult to determine what the contractID will be since the id pool would be
used by offers and other contracts. This CAP uses a Hash instead as the
contractID.
With this CAP we provide several ways of building the contractID preimages that can be reproduced off-chain and then used to address the contracts that may or may not exist (for example, some general contracts like tokens or AMMs).
Security Concerns
The security concerns from CAP-0046 (https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046.md#security-concerns) apply here as well.