DEP 0002: Smart Contract Composability
status: deprecated
Current Situation
When creating the DAO, we needed to invent the concept of protocol owned liquidity in DarkFi. Without this, in order to have on chain DAO treasuries, the DarkFi blockchain would have to recognize funds held by both the money and DAO contracts as valid. This introduces a security risk if there is an error in the DAO contracts.
Additionally it means that liquidity could only be held by the DAO contract, and any liquidity held by other contracts would have to be recognized by the consensus as valid. This would restrict the protocols that could work with on chain liquidity to a small hardcoded subset due to security.
Motivated by the desire to enable protocol owned liquidity, we created the concept in
money::transfer()
of the spend_hook
.
Firstly a quick recap of how money::transfer()
works. During the mint phase of creating
coins, we construct a coin C = hash(…, spend_hook, user_data)
. The …
contains coin data such
as value, token ID and other attributes. During the burn phase we produce a deterministic
unlinkable nullifier.
This is a more general ZK concept of committing to several attributes, and then later either
full on revealing them or more specifically applying constraints to the attributes.
To enable protocol owned liquidity, we introduced the coin attributes spend_hook
and
user_data
, motivated by these desires:
- Generalize protocol-owned liquidity enabling any third party to write contracts that own liquidity.
- Stronger security model for on chain liquidity by only depending on the money contract when composed with contracts like the DAO.
When a coin is spent, the spend_hook
is revealed publicly. The money::transfer()
call
enforces that the subsequent contract called in the tx matches the spend_hook
.
In our example, spend_hook = DAO
, and then our tx will have two calls:
[money::transfer(), DAO::exec()]
. When spending a coin where the spend_hook = DAO
,
then money::transfer()
will check the next contract in the tx will match the spend_hook
.
Now you might ask some questions:
- Here we are listing
DAO
, but actually we need a stricter check that the call isDAO::exec()
and not some other DAO method call. - We need to enforce which DAO we are operating on.
This is where the user_data
is used. We can commit to several things including which function
is called in the contract. Since in the DAO, only DAO::exec()
can be composed, we just sidestep
this and enforce that when a tx has two calls, then the DAO one must be DAO::exec()
. We then
use the user_data
to store the DAO bulla.
Motivation: Limitations of Current Approach
The current approach enables contracts to own liquidity, which is how we can have DAO on chain treasuries. We have the ability for contracts to directly call other contracts. However this calling mechanism is static.
We desire now to generalize the DAO calling mechanism, so any contract could be called.
Currently DAO::exec()
deserializes the money::transfer()
calldata, and then enforces its
checks on it inside wasm. These checks are hardcoded.
It would be very useful if instead this data or code were to be dynamic. Therefore a DAO
proposal could be called, not to call money::transfer()
but instead to call another contract.
This system would then be generic and usable with other contracts, such as an algorithmic
streaming contract making calls on a ZK NFT.
Proposal: Introspective Params
The current ContractCall
struct looks like:
pub struct ContractCall {
/// ID of the contract invoked
pub contract_id: ContractId,
/// Call data passed to the contract
pub data: Vec<u8>,
}
We propose to change the data
field to this:
pub struct ContractCall {
/// ID of the contract invoked
pub contract_id: ContractId,
/// Named call data passed to the contract
pub data: HashMap<String, Vec<u8>>,
}
This way contracts can query each other's calldata in a dynamic compatible way.
Some part of the params for a contract may be specific to that contract. Another part
might be generic, which shares the same struct with multiple other contracts.
This enables contracts to query an interface from another contract's calldata,
deserialize that data and work with it, without having to hardcode a dependency,
e.g DAO::exec()
hardcoding a dependency on money::transfer()
params.
Note on Auth Modules
An alternative approach is introducing the concept of auth modules. So for example, with the DAO, a user could deploy their own contract on chain with specific logic, then make a proposal to execute that contract. We could also supply our own auth module with hardcoded branching support for several common contract types.
However while this may be desirable in some cases where complex logic in DAO proposals are required, it presents several downsides:
- The supplied auth module will hardcode support for a few contract types and not be properly generic.
- User deployed contracts could be expensive and error prone.
- For efficiency the DAO would probably end up hardcoding support for several contract types directly, as well as other composable contracts.