feat: interactWrites from within the contracts
ppedziwiatr opened this issue · 1 comments
Add an option to change a state of a contract from within other contract code.
Proposed solution:
- Write a new interaction for the "calling" contract.
- perform dry-run with the new input
- fetch a log of all inner write calls performed during this dry-run (probably with the use of #21)
- for each registered inner write call - add a tag in the resulting interaction transaction:
'interactWrite': 'calleeTxId'
- Eval state of the callee contract:
- load all interactions (both "traditional" and the one with "interactWrite": "calleeTxId")
- sort the transactions
- for all the "interactWrite" transactions - load the calling contract and eval its state up to the given "interactWrite" transaction height - this effectively should cause the "innerWrite" to be called - and with the help of the state cache for the callee contract - update its current value.
Detailed description
Introduction
SmartWeave protocol currently natively does not support writes between contract - contracts can only read each others' state. This lack of interoperability is a big limitation for real-life applications - especially if you want to implement features like staking/vesting, disputes - or even a standard approve/transferFrom flow from ERC-20 tokens.
Some time ago a solution addressing this issue was proposed - Foreign Call Protocol (https://www.notion.so/Foreign-Call-Protocol-Specification-61e221e5118a40b980fcaade35a2a718).
This is a great and innovative idea that greatly enhances contracts usability, but we've identified some issues:
- Contract developers need to add FCP-specific code in the smart contract code and in its state. This causes the protocol code to be mixed with the contract's business logic. Additionally - any update or change in the protocol would probably require the code (and/or state) of all of the contracts that are utilizing the FCP, to be upgraded.
- In order to create a "write" operation between FCP-compatible contracts, users need to create two separate transactions:
invoke
operation on Contract B to add entry in theforeignCalls
state field of the Contract BreadOutbox
operation on Contract A, that underneath reads Contract's BforeignCalls
and "manually" calls Contract'a Ahandle
function for each registered 'foreignCall'
We believe that writes between contracts should be implemented on the protocol level (ie. contract source code and its state should not contain any technical details of the internal calls) and that performing a write should not require creating multiple interactions.
Solution
- Attach a new method to the SmartWeave global object (the one that is accessible from the contract's code) with signature:
function write<Input = unknown>(contractId: String, input: Input): Promise<InteractionResult>
This method allows to perform writes on other contracts.
Thecaller
of such call is always set to thetxId
of the calling contract - this allows the callee contract to decide whether call should be allowed.
The method first evaluates the target (ie. specified by thecontractTxId
parameter) contract's state up to the "current" block height (ie. block height of the interaction that is calling thewrite
method) and then applies the input (specified as the 2nd. parameter of thewrite
method). The result is memoized in cache.
This has been implemented in the Contract.dryWriteFromTx()
and ContractHandlerApi.assignWrite()
- For each newly created interaction with given contract - perform a dry run and analyze the call report of the dry-run (feature introduced in #21). This should generate a list of all inner-calls between contracts.
For each generated inner call - generate additional tag:
{'interactWrite': contractTxId}
- wherecontractTxId
is the callee contract.
This has been implemented in the Contract.writeInteraction
and InnerWritesEvaluator.eval()
.
- For each state evaluation for a given contract ("Contract A"):
- load all "direct" interactions with the contract
- load all "internalWrite" interactions with the contract (search using the tag introduced in point 4)
- concat both type of transactions and sort them according to protocol specification (i.e. using the
sortKey
) - for each interaction:
- if it is a "direct" interaction - evaluate it according to current protocol specification
- if it is an "internalWrite" interaction - load the contract specified in the "internalWrite" ("Contract B") tag and evaluate its state. This will cause the
write
(described in point 1.) method to be called. After evaluating the "Contract B" contract state - load the latest state of the "Contract A" from cache (it has been updated by thewrite
method) and move to next interaction.
This has been implemented in the DefaultStateEvaluator.doReadState()
This method is also effective for nested writes between contracts.
Alternative solution
An alternative solution has been also considered. Instead of writing only a tag with id of the calling contract (point 3. from the description above), one could write the exact input (ie. function, parameters, etc) of the call. This would have an advantage of the increased performance (as we could evaluate the "Contract A" state without the need of evaluating the "Contract B" state in the process), but would ruin the "lazy-evaluation" idea of the protocol and reduce the safety of the solution - as the Contract A would need to fully trust the calling Contract B - if the Contract B would save some unsafe data in the input - there would be no way to exclude it from the execution.
implemented in #45