Skip to content

Commit

Permalink
feat: factory contract (#96)
Browse files Browse the repository at this point in the history
* chore: small doc addition

* feat: add simple factory example

* fix: use abi(embed_v0) #91

* feat: added contract class/instance separation
  • Loading branch information
julio4 authored Oct 19, 2023
1 parent 90aa7c0 commit dcadbd1
Show file tree
Hide file tree
Showing 11 changed files with 241 additions and 4 deletions.
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Do not write directly Cairo program inside the markdown files. Instead, use code

## Adding a new Cairo program

You can add or modify examples in the `listings` directory. Each listing is a scarb project. You can use `scarb init` to create a new scarb project. Here's the minimal `Scarb.toml` configuration:
You can add or modify examples in the `listings` directory. Each listing is a scarb project. You can use `scarb init` to create a new scarb project (You can remove the generated git repository, `rm -rf .git`). Here's the minimal `Scarb.toml` configuration:

```toml
[package]
Expand Down
6 changes: 6 additions & 0 deletions listings/ch00-getting-started/counter/src/counter.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ mod SimpleCounter {
counter: u256,
}

#[constructor]
fn constructor(ref self: ContractState, init_value: u256) {
// Store initial value
self.counter.write(init_value);
}

#[generate_trait]
#[external(v0)]
impl SimpleCounter of ISimpleCounter {
Expand Down
1 change: 1 addition & 0 deletions listings/ch00-getting-started/factory/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target
8 changes: 8 additions & 0 deletions listings/ch00-getting-started/factory/Scarb.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "factory"
version = "0.1.0"

[dependencies]
starknet = ">=2.3.0-rc0"

[[target.starknet-contract]]
5 changes: 5 additions & 0 deletions listings/ch00-getting-started/factory/src/lib.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod simple_factory;
mod target;

#[cfg(test)]
mod tests;
64 changes: 64 additions & 0 deletions listings/ch00-getting-started/factory/src/simple_factory.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use starknet::{ContractAddress, ClassHash};

#[starknet::interface]
trait ICounterFactory<TContractState> {
/// Create a new counter contract from stored arguments
fn create_counter(ref self: TContractState) -> ContractAddress;

/// Create a new counter contract from the given arguments
fn create_counter_at(ref self: TContractState, init_value: u128) -> ContractAddress;

/// Update the argument
fn update_init_value(ref self: TContractState, init_value: u128);

/// Update the class hash of the Counter contract to deploy when creating a new counter
fn update_counter_class_hash(ref self: TContractState, counter_class_hash: ClassHash);
}

#[starknet::contract]
mod CounterFactory {
use starknet::{ContractAddress, ClassHash};
use starknet::syscalls::deploy_syscall;

#[storage]
struct Storage {
/// Store the constructor arguments of the contract to deploy
init_value: u128,
/// Store the class hash of the contract to deploy
counter_class_hash: ClassHash,
}

#[constructor]
fn constructor(ref self: ContractState, init_value: u128, class_hash: ClassHash) {
self.init_value.write(init_value);
self.counter_class_hash.write(class_hash);
}

#[abi(embed_v0)]
impl Factory of super::ICounterFactory<ContractState> {
fn create_counter_at(ref self: ContractState, init_value: u128) -> ContractAddress {
// Contructor arguments
let mut constructor_calldata: Array::<felt252> = array![init_value.into()];

// Contract deployment
let (deployed_address, _) = deploy_syscall(
self.counter_class_hash.read(), 0, constructor_calldata.span(), false
)
.expect('failed to deploy counter');

deployed_address
}

fn create_counter(ref self: ContractState) -> ContractAddress {
self.create_counter_at(self.init_value.read())
}

fn update_init_value(ref self: ContractState, init_value: u128) {
self.init_value.write(init_value);
}

fn update_counter_class_hash(ref self: ContractState, counter_class_hash: ClassHash) {
self.counter_class_hash.write(counter_class_hash);
}
}
}
39 changes: 39 additions & 0 deletions listings/ch00-getting-started/factory/src/target.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#[starknet::interface]
trait ISimpleCounter<TContractState> {
fn get_current_count(self: @TContractState) -> u128;
fn increment(ref self: TContractState);
fn decrement(ref self: TContractState);
}

#[starknet::contract]
mod SimpleCounter {
#[storage]
struct Storage {
// Counter variable
counter: u128,
}

#[constructor]
fn constructor(ref self: ContractState, init_value: u128) {
// Store initial value
self.counter.write(init_value);
}

#[abi(embed_v0)]
impl SimpleCounter of super::ISimpleCounter<ContractState> {
fn get_current_count(self: @ContractState) -> u128 {
self.counter.read()
}

fn increment(ref self: ContractState) {
// Store counter value + 1
let mut counter: u128 = self.counter.read() + 1;
self.counter.write(counter);
}
fn decrement(ref self: ContractState) {
// Store counter value - 1
let mut counter: u128 = self.counter.read() - 1;
self.counter.write(counter);
}
}
}
82 changes: 82 additions & 0 deletions listings/ch00-getting-started/factory/src/tests.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
mod tests {
use core::option::OptionTrait;
use core::traits::Into;
use core::traits::TryInto;
use starknet::{ContractAddress, ClassHash, contract_address_const};
use starknet::syscalls::deploy_syscall;

use factory::simple_factory::{
CounterFactory, ICounterFactoryDispatcher, ICounterFactoryDispatcherTrait
};
use factory::target::{SimpleCounter, ISimpleCounterDispatcher, ISimpleCounterDispatcherTrait};

/// Deploy a counter factory contract
fn deploy_factory(
counter_class_hash: ClassHash, init_value: u128
) -> ICounterFactoryDispatcher {
let caller_address: ContractAddress = contract_address_const::<'caller'>();
let deployed_contract_address = contract_address_const::<'market_factory'>();

let mut constructor_calldata: Array::<felt252> = array![
init_value.into(), counter_class_hash.into()
];

let (factory_address, _) = deploy_syscall(
CounterFactory::TEST_CLASS_HASH.try_into().unwrap(),
0,
constructor_calldata.span(),
false
)
.expect('failed to deploy factory');

ICounterFactoryDispatcher { contract_address: factory_address }
}

#[test]
#[available_gas(20000000)]
fn test_deploy_counter_constructor() {
let init_value = 10;

let counter_class_hash: ClassHash = SimpleCounter::TEST_CLASS_HASH.try_into().unwrap();
let factory = deploy_factory(counter_class_hash, init_value);

let counter_address = factory.create_counter();
let counter = ISimpleCounterDispatcher { contract_address: counter_address };

assert(counter.get_current_count() == init_value, 'Incorrect initial value');
}

#[test]
#[available_gas(20000000)]
fn test_deploy_counter_argument() {
let init_value = 10;
let argument_value = 20;

let counter_class_hash: ClassHash = SimpleCounter::TEST_CLASS_HASH.try_into().unwrap();
let factory = deploy_factory(counter_class_hash, init_value);

let counter_address = factory.create_counter_at(argument_value);
let counter = ISimpleCounterDispatcher { contract_address: counter_address };

assert(counter.get_current_count() == argument_value, 'Incorrect argument value');
}

#[test]
#[available_gas(20000000)]
fn test_deploy_multiple() {
let init_value = 10;
let argument_value = 20;

let counter_class_hash: ClassHash = SimpleCounter::TEST_CLASS_HASH.try_into().unwrap();
let factory = deploy_factory(counter_class_hash, init_value);

let mut counter_address = factory.create_counter();
let counter_1 = ISimpleCounterDispatcher { contract_address: counter_address };

counter_address = factory.create_counter_at(argument_value);
let counter_2 = ISimpleCounterDispatcher { contract_address: counter_address };

assert(counter_1.get_current_count() == init_value, 'Incorrect initial value');
assert(counter_2.get_current_count() == argument_value, 'Incorrect argument value');
}
}
3 changes: 2 additions & 1 deletion src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ Summary
- [Events](./ch00/basics/events.md)
- [Storing Custom Types](./ch00/basics/storing-custom-types.md)
- [Custom types in entrypoints](./ch00/basics/custom-types-in-entrypoints.md)
- [Interfaces and interacting with contracts](./ch00/interacting/interacting.md)
- [Deploy and interact with contracts](./ch00/interacting/interacting.md)
- [Factory pattern](./ch00/interacting/factory.md)
- [Contract interfaces and Traits generation](./ch00/interacting/interfaces-traits.md)
- [Calling other contracts](./ch00/interacting/calling_other_contracts.md)
- [Testing contracts](./ch00/testing/contract-testing.md)
Expand Down
31 changes: 31 additions & 0 deletions src/ch00/interacting/factory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Factory Pattern

The factory pattern is a well known pattern in object oriented programming. It provides an abstraction on how to instantiate a class.

In the case of smart contracts, we can use this pattern by defining a factory contract that have the sole responsibility of creating and managing other contracts.

## Class hash and contract instance

In Starknet, there's a separation between contract's classes and instances. A contract class serves as a blueprint, defined by the underling Cairo bytecode, contract's entrypoints, ABI and Sierra program hash. The contract class is identified by a class hash. When you want to add a new class to the network, you first need to declare it.

When deploying a contract, you need to specify the class hash of the contract you want to deploy. Each instance of a contract has their own storage regardless of the class hash.

Using the factory pattern, we can deploy multiple instances of the same contract class and handle upgrades easily.

## Minimal example

Here's a minimal example of a factory contract that deploy the `SimpleCounter` contract:

```rust
{{#include ../../../listings/ch00-getting-started/factory/src/simple_factory.cairo}}
```

<!-- This is not ready for "Open in remix" because we need multiple files -->

This factory can be used to deploy multiple instances of the `SimpleCounter` contract by calling the `create_counter` and `create_counter_at` functions.

The `SimpleCounter` class hash is stored inside the factory, and can be upgraded with the `update_counter_class_hash` function which allows to reuse the same factory contract when the `SimpleCounter` contract is upgraded.

This minimal example lacks several useful features such as access control, tracking of deployed contracts, events, ...

<!-- TODO maybe add a more complete example at the end of this section or in the `Applications examples` chapter -->
4 changes: 2 additions & 2 deletions src/ch00/interacting/interacting.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Interacting with contracts
# Deploy and interact with contracts

In this chapter, we will see how to interact with contracts.
In this chapter, we will see how to deploy and interact with contracts.

0 comments on commit dcadbd1

Please sign in to comment.