-
Notifications
You must be signed in to change notification settings - Fork 141
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Auto-unlock with Safes #26
Conversation
Is it right to read it like: "Token contract creates a Promise with a call to the |
Co-Authored-By: Alexey <alexey@fckt.dev>
More like "Token contract creates a safe that creates a Promise towards the contract itself with a call to the |
I think it's worth discussing the relationship of (un)lock and adjusting allowance. IMO you have a cleaner implementation where only
|
@robrobbins
There are 2 transactions getting executed in parallel, where |
@zmanian also linked to https://agoric.com/documentation/zoe/guide/ for a different approach of cross-contract value interaction. |
Also this may be of interest https://github.com/cosmos/ics/tree/master/ideation/ics-027-direct-interchain-exchange |
@evgenykuzyakov Do you believe this NEP should remain open or should we close it in favor of #122? |
Hi @evgenykuzyakov! Since we haven't heard back for two months, we are closing this NEP. If you or anyone is interested in revisiting it in the future, please submit a new NEP. |
Summary
Introducing a new concept of safes that allows to securely lock some data from a contract with
automatic unlock mechanism.
Motivation
There are a few NEPs that proposed solutions to address the cross-shard communication problem.
For example when an decentralized exchange tries to swap 2 tokens without owning them.
One solution is to introduce locks with automatic unlock capabilities, but without explicitly exposing the locked structure.
While it solves the issue for a simple exchange use-case. It has some limitations and complexity for non trivial use cases.
It might lead to an unexpected behavior when a token is behind the proxy contract.
This proposal is to introduce explicit locked data storage which we call a
safe
that can't be copied and always resolved at the end.It's a familiar concept for an asynchronous development similar to guards.
When a guard is released, the destructor (or Drop in Rust) is called and the lock can be resolved.
Guide-level explanation
We introduce a new concept which we call a
safe
.Example:
When a decentralized exchange tries to move some tokens, it first has to acquire and lock the funds.
alice
.unlock
on itself.dex
.Now Dex has this safe from the token contract.
Dex can read the content of the safe and assert the content is correct.
transfer
on the token contract and pass this safe with this promise.400
to the new owner.400
by the transferred amount. E.g.dex
.Transfer has completed successfully, but
dex
may want to do more transfers. It's safe to drop the safe now.NOTE, that the promise is always called even if the content of the safe was fully used.
It's because the promise is fully prepaid during the creation of the safe.
alice
.Reference-level explanation
Runtime API
Introducing new Runtime API to handle safes:
API to create safes and read/write content
Passing safes
If you don't return or pass a safe, then this safe will be dropped at the end of the contract execution.
Receiving safes
Safes can be received in two ways:
Low-level contract example
This is still pseudo-code. But it should highlight how safes work.
E.g. this code don't use registers and assumes core functions return vectors.
On access to safes from multiple actions.
Since safes are passed to a promise and not to a particular function call.
Let's say a promise contains 2 function calls.
All safe(s) will be given to the first function call. If the function call doesn't consume a safe, it will be passed towards the next function call.
Once the safe reaches the last action and the safe is not consumed by the last action, the safe will be dropped.
If the content of the safe is modified by the contract during one of the function call, but then the next action fails. The content of the safe
is reverted to the original content, the content before the first action has started.
Runtime internal implementation
To handle safes properly we need the following:
promise_attach_safe
)safe_return
).Tracking safes
The easiest option to handle safes is to accumulate all safes at the beginning of action receipt processing.
Same way we accumulate input_data, we can accumulate safes with content. Let's introduce
Safe
data structure:Then we add a new vector of
all_safes
intoapply_action_receipt
within a Runtime.This vector allows us to drop all safes with the original content in case any action fails during
processing of this
ActionReceipt
.Processing safes with actions
Each action will receive a mutable reference to
input_safes
andpromise_results_safes
.If any action fails, then we don't care about
input_safes
andpromise_results_safes
anymore, becausewe'll just drop safes from
original_safes
.A function call on an account that owns a safe may update the content of the safe.
A function call can also consume some safes from either vector, or create new safes and add them to
input_safes
.Newly created safes that are not consumed will be passed to the next action as an input, so it can act the safe if needed.
The content of the safes will be handled through
RuntimeExt
crate.Inside a VMLogic, we'll track safes the following way.
Handling of returned safes:
safe_return
call will panic immediately.NOTE: Even though the safe can be attached to the promise, with multiple outgoing dependencies, you can do this by
attaching it directly instead of relying on
safe_return
.consumed_safes_idxs
andreturned_safes_idxs
. NOTE: if the outgoing dependencies are empty, the safe will not be returnedanywhere, so it effectively will be dropped after this action.
We also need to update
Promise
enum to indicate safe resolving promise:RuntimeExt
needs the following methods:Need to add the following fields to the
ActionResult
:Collecting safes after successful execution of a Function Call action:
VMOutcome
should contain the following fields fromVMLogic
:new_safes_idxs
consumed_safes_idxs
returned_safes_idxs
RuntimeExt
should returnmut safes
back toRuntime
.Runtime
should do the following:safes
by removing allconsumed_safes_idxs
:returned_safes
inActionResult
.new_safes_idxs
toinput_safes_idxs
.input_safes_idxs
andpromise_results_safes_idxs
by retaining only safeindices that were not consumed and remapping the old indices to the new indices.
Merging
ActionResult
:ActionResult
, all oldreturned_safes
should be moved to olddropped_safes
.The reason for this is there shouldn't be any
outgoing_dependencies
in the oldActionResult
.Because the old action was not the last action and only the last action can have
outgoing_dependencies
.ActionResult
result isErr
, all newreturned_safes
should be moved to newdropped_safes
.dropped_safes
are added after olddropped_safes
.Update
ActionReceipt
andDataReceipt
Need to add one field to
ActionReceipt
:Also need to add one field to
DataReceipt
:Resolving safes at the end.
If the
ActionResult
result isErr
:original_safes
If the
ActionResult
result isOk
, we have safes in the following fields:returned_safes
anddropped_safes
in theActionResult
Receipt
in theActionResult
safes
that were not consumed and should be dropped.return_data
isPromiseIndex
allreturned_safes
should be moved todropped_safes
.outgoing_dependencies
allreturned_safes
should be moved todropped_safes
.Handling
returned_safes
forOk
with exactly 1 outgoing dependency:returned_safes
tosafes
from the outgoingDataReceipt
.Dropping safes:
Receipt
with aDataReceipt
.receiver_id
issafe.owner_id
.data_id
issafe.data_id
.data
is theSome(safe.content)
.