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!
modularity.cairo
By bernardstanislas
modularity.cairo
- 44