modularity.cairo

Learnings from Solidity, opportunities in Starknet

# WHY

Start with why

We are building a collaboration protocol.

The center piece is the concept of bounty.

A bounty represents a task you can work on and be paid for.

The protocol is focused on the bounty lifecycle.

It needs to be modular in order to adapt to every bounty business need.

You're stuck with me for 15 minutes πŸͺ’

Solidity

What we explored to achieve modularity, main takeaways and decisions made

1.

2.

Cairo
What is really different in the end? What are the opportunities?

3.

Getting modular again

What we have thought of, what did we achieve, and the next steps

# AGENDA

Building a modular protocol

# GOAL

Composability 🧩

A protocol user must be able to easily swap technical implementations of a part or the whole of the contract business logic

Discoverability πŸ”­

Contracts must be easy to interact with, whether it is from another contract or a Dapp frontend

Interoperability πŸ”Œ

The protocol must be easy to build on, using the highest amount of standards – EIP or good practices – is a must-have

Strategy pattern

interface ICandidateApprovalStrategy {
  function approveCandidate(address) external;
}

contract Bounty {
  function approveCandidate(address candidate_) external {
    candidateApprovalStrategy.approveCandidate(candidate_);
  }
}
βœ… ABI is explicit 
βœ… Strategies can be made stateless with call delegation, and shared among bounties
❌ Strategies do not have flexibility over function signatures
❌ Strategies must hold all bounty variable declarations if use through call delegation
Caller -> Bounty -> Strategy

COMPOSABILITY​

DISCOVERABILITY

READABILITY OF THIS SLIDE

# OPTION 1
interface ICandidateApprovalStrategy {
}

contract RewardingCandidateApprovalStrategy is ICandidateApprovalStrategy {
  function approveCandidate(address bounty_, address candidate_, address erc20_) external {
  }
}
βœ… Strategy is free to expose its own function signatures
❌ Strategy has to store its own data about bounties
❌ Caller has to know all strategy addresses for a given bounty
Caller -> Strategy -> Bounty

Reverse the caller!

DISCOVERABILITY

COMPOSABILITY

# OPTION 2
# OPTION 3

EIP1820 to the rescue

A unique register where you store "the interface implementer for this address is at this address"

interface ICandidateApprovalStrategy {
}

contract Bounty {
  function constructor(address candidateApprovalStrat_) {
    erc1820.registerImplementerFor("ICandidateApprovalStrategy", candidateApprovalStrat_);
  }
}
Caller -> ERC1820Registry -> Strategy -> Bounty

DISCOVERABILITY*

COMPOSABILITY

*The ABI problem still needs to be addressed but is simple to solve with tooling

βœ…

# CAIRO

What's up Cairo?

⚰️ Inheritance

Goodbye

⚰️ InterfaceId

Welcome back

🀝 Call delegation

🀝 Function selectors

# OPPORTUNITY

Any opportunity?

You can use delegate call to invoke a function that changes a storage variable which wasn’t defined in the calling contract. In such a case, the new corresponding storage variable will be created in the calling contract [...]

🀯

🀯

🀯

Call
delegation

# PROPOSITION

Bring the best, leave the rest

πŸ«‚ Strategy pattern

πŸ«‚ Call delegation

πŸ«‚ EIP1820~ish
πŸ«‚ Fallback function

The recipe

# Bounty.cairo

@contract_interface
namespace IStrategy:
    func init(strategy_address : felt):
    end
end

@storage_var
func bounty_strategy_routing(function_selector : felt) -> (strategy : felt):
end

@constructor
func constructor(candidate_approval_strategy : felt):
    IStrategy.delegate_init(
        contract_address=application_strategy, strategy_address=candidate_approval_strategy)
    return ()
end

# -----------------

# RewardCandidateApproval.cairo

@storage_var
func bounty_strategy_routing(function_selector : felt) -> (strategy : felt):
end

@external
func init(strategy_address : felt):
    bounty_strategy_routing.write(APPROVE_CANDIDATE_SELECTOR, strategy_address)
    return ()
end

@external
func approve_candidate(candidate: felt):
end
# Bounty.cairo

[...]

@external
@raw_input
@raw_output
func __default__(selector : felt, calldata_size : felt, calldata : felt*) -> (
        retdata_size : felt, retdata : felt*):
    let (strategy) = bounty_strategy_routing.read(selector)
    assert_not_zero(strategy)

    let (retdata_size : felt, retdata : felt*) = delegate_call(
        contract_address=strategy,
        function_selector=selector,
        calldata_size=calldata_size,
        calldata=calldata)
    return (retdata_size=retdata_size, retdata=retdata)
end

# -----------------

# RewardCandidateApproval.cairo

[...]

@external
func approve_candidate(candidate: felt):
end
# THE END

Wrapping it up

βœ… Composability

Every strategy can have their own function signatures

βœ… Discoverability

The bounty ABI is the concatenation of all strategies' ABIs

🚧 Limitations

Strategies all share the same state  – the bounty's – so you might audit the code before making a choice for your bounty platform

βœ… Interoperability

This pattern is very similar to what's described in EIP2535 – the Diamond pattern – next step is to be fully compatible

@bernanstard
Β 

@onlydust_xyz

Thank you!

Made with Slides.com