DApp-dApp invocations for Ride

Ilya Smagin
Waves Protocol
Published in
3 min readDec 10, 2020

--

As Waves Blockchain matures, we are adding more programmable features to Ride. One such feature for the upcoming release of Waves Node version 1.3 is cross-dapp invocations. After all, staking/unstaking USDNs is a valuable part of income from AMMs, automatic hedging might be necessary for others, oracles want to collect fees for their services, and, finally, how can a fellow developer get-rich-fast(I mean, really fast) without flashloans?

Why is it important?

A successful ecosystem contains many synergetic components. If we look at the present day, the building blocks of a successful ecosystem are #defi components: liquidity providers, AMMs, oracles. These components need to be composable. The cross-dapp interaction is the tool providing this composition in a way that allows the complexity to be controlled — not just for today.

How it works?

Let’s say you are implementing an AMM which accepts USDN as liquidity, which you want to stake immediately and unstake upon request:

@Callable(inv)
func provideLiquidity() = {
if(inv.payments[0].assetId != NEUTRINOID)
then throw("bad token")
else
let amt = inv.payments[0].amount
strict stakingResult = invoke(
NEUTRINODAPP, # dapp address
'lockNeutrino', # dapp function
[], # passed arguments
inv.payments # attached payments
)
...
([
# updating dapp state
],
0 # returning something meaningful to the invoker
)

Looks intuitive to me. Mind the newstrict keyword introduced — more details below.

API Changes

The key API change is the signature of @Callable function. Starting STDLIB_VERSION 5 , not only the dapp can update state/do payouts, but also return meaningful value to the caller: the return type changes from [Action] to

([Action], Object)

Where the latter part of the tuple is the return value. The invoke function signature goes as follows:

invoke: (Address, String,[Object],[AttachedPayment]) => Object

When dApp invokes another dApp, the result is to be matched:

strict invokeResult = invoke(...)
match invokeResult {
case i:Int => ...
case _ => throw("unexpected invokeResult")
}

‘strict’ keyword

The necessity to introduce new keyword comes from the laziness of ride language, and, in turn, the order “parts” of “script” are calculated. It has never been a problem so far since the computations were side-effectless(No write effects happened during the execution of a script).

Problem: Imagine we have a script that grabs a balance, invokes dapp, then grabs the balance again:

...
let balanceBefore = wavesBalance(this)
let autotrade = invoke('swopfi-waves-usdn', 'swap', [10 waves])
let balanceAfter = wavesBalance(this)
if(balanceAfter < balanceBefore) then ... else ...

Since let is lazy, the computations happen not at the time of definition, but at the time of the first access to the variable. e.g. first, balanceAfter then balanceBefore , and no autotrade will be processed at all!

Solution: we cheated by making strict a macro which ensures variable of the block is materialised before the expression of the block:

strict a = ...     # 'strict' macro
a + b

is just

let a = ...
if(a!=a) then throw("fatal") else # inserted by macro
a + b

in the bytecode.

...
strict balanceBefore = wavesBalance(this)
strict autotrade = invoke('swopfi-waves-usdn', 'swap', 10 waves)
strict balanceAfter = wavesBalance(this)
if(balanceAfter < balanceBefore) then ... else ...

Now all the computations happen in the exact order of the definitions.

TL;DR: With the upcoming release of Waves Node 1.3, we’re adding dapp-to-dapp interaction within Ride.

--

--

Ilya Smagin
Waves Protocol

Head of Development for Waves Smart Contracts