skills/multiversx/mx-ai-skills/multiversx-factory-manager

multiversx-factory-manager

SKILL.md

MultiversX Factory/Manager Pattern

Deploy, track, and upgrade child contracts from a template — the three core operations of a factory manager.

What Problem Does This Solve?

When a protocol needs to deploy multiple instances of the same contract (e.g., one per user, per market, per pool), a factory manager handles the deployment lifecycle: deploy from a template, track all instances, and upgrade them when the template changes.

When to Use

Scenario Use Factory?
Multiple instances of same contract needed Yes
Each user/entity gets their own contract Yes
Single contract serves all users No
Different contract types per deployment No — use separate deploy logic

Core Concept

Manager Contract
├── Stores template address (code to clone)
├── Deploy: creates child from template via from_source()
├── Track: registry of all deployed children
└── Upgrade: applies new template to existing children

Operation 1: Deploy

Deploy a new child contract using from_source() to clone the template:

#[endpoint(deployChild)]
fn deploy_child(
    &self,
    child_id: ManagedBuffer,
    init_arg_a: BigUint,
    init_arg_b: ManagedAddress,
) -> ManagedAddress {
    let caller = self.blockchain().get_caller();
    let template = self.template_address().get();

    let metadata = CodeMetadata::UPGRADEABLE
        | CodeMetadata::READABLE
        | CodeMetadata::PAYABLE;

    // Deploy from template
    let new_address = self.tx()
        .typed(child_proxy::ChildProxy) // ChildProxy generated from child contract's #[multiversx_sc::proxy]
        .init(init_arg_a, init_arg_b)
        .from_source(template)
        .code_metadata(metadata)
        .returns(ReturnsNewManagedAddress)
        .sync_call();

    // Register in tracking
    self.child_addresses().insert(new_address.clone());
    self.child_by_id(&child_id).set(new_address.clone());

    self.deploy_event(&caller, &child_id, &new_address);
    new_address
}

CodeMetadata Explained

Flag Effect When to Use
UPGRADEABLE Child can be upgraded later Almost always — needed for Operation 3
READABLE Other contracts can read child's storage When using cross-contract storage reads
PAYABLE Child can receive EGLD directly When child needs to accept payments
PAYABLE_BY_SC Only other SCs can pay child When child should only interact with contracts

Operation 2: Track (Registry)

#[multiversx_sc::module]
pub trait RegistryModule {
    // Template to clone from
    #[storage_mapper("templateAddress")]
    fn template_address(&self) -> SingleValueMapper<ManagedAddress>;

    // Set of all deployed child addresses
    #[storage_mapper("childAddresses")]
    fn child_addresses(&self) -> SetMapper<ManagedAddress>;

    // Lookup: ID → child address
    #[storage_mapper("childById")]
    fn child_by_id(&self, id: &ManagedBuffer) -> SingleValueMapper<ManagedAddress>;

    // Admin set (who can deploy/upgrade)
    #[storage_mapper("admins")]
    fn admins(&self) -> UnorderedSetMapper<ManagedAddress>;

    // Helper: verify address is a managed child
    fn require_managed_child(&self, address: &ManagedAddress) {
        require!(
            self.child_addresses().contains(address),
            "Not a managed child contract"
        );
    }
}

Pattern: SetMapper for the full set (iteration + contains check) + SingleValueMapper for ID-based lookup. This gives you both O(1) lookup by ID and iteration over all children.

Operation 3: Upgrade

Upgrade a single child to the current template:

#[endpoint(upgradeChild)]
fn upgrade_child(&self, child_id: ManagedBuffer) {
    self.require_admin();

    let child_address = self.child_by_id(&child_id).get();
    self.require_managed_child(&child_address);

    let metadata = CodeMetadata::UPGRADEABLE
        | CodeMetadata::READABLE
        | CodeMetadata::PAYABLE;

    self.tx()
        .to(child_address)
        .typed(child_proxy::ChildProxy) // ChildProxy generated from child contract's #[multiversx_sc::proxy]
        .upgrade()
        .code_metadata(metadata)
        .from_source(self.template_address().get())
        .upgrade_async_call_and_exit();
}

Note: When the child contract proxy is not available (e.g., template-based deployment from external code), use raw_deploy() instead of typed() for deployment.

Template Management

#[only_owner]
#[endpoint(setTemplate)]
fn set_template(&self, template_address: ManagedAddress) {
    require!(
        self.blockchain().is_smart_contract(&template_address),
        "Address is not a smart contract"
    );
    self.template_address().set(template_address);
}

Admin Hierarchy

Owner (deployer of manager)
  └── Admins (can deploy/upgrade children)
       └── Users (interact with their own child)
fn require_admin(&self) {
    let caller = self.blockchain().get_caller();
    require!(
        self.admins().contains(&caller)
            || caller == self.blockchain().get_owner_address(),
        "Not authorized"
    );
}

Anti-Patterns

1. No Registry Tracking

// WRONG — deploying without tracking makes upgrades impossible
let addr = self.tx().typed(ChildProxy).init().from_source(template).sync_call();
// addr is lost after this transaction!

2. Missing UPGRADEABLE Flag

// WRONG — child can never be upgraded
let metadata = CodeMetadata::READABLE | CodeMetadata::PAYABLE;
// Should include CodeMetadata::UPGRADEABLE

3. Upgrading Without Validation

// WRONG — no check that address is actually a managed child
fn upgrade(&self, address: ManagedAddress) {
    self.tx().to(address).typed(ChildProxy).upgrade()
        .from_source(self.template_address().get())
        .upgrade_async_call_and_exit();
}

Template

#[multiversx_sc::module]
pub trait FactoryModule {
    #[storage_mapper("templateAddress")]
    fn template_address(&self) -> SingleValueMapper<ManagedAddress>;

    #[storage_mapper("childAddresses")]
    fn child_addresses(&self) -> SetMapper<ManagedAddress>;

    #[storage_mapper("childById")]
    fn child_by_id(&self, id: &ManagedBuffer) -> SingleValueMapper<ManagedAddress>;

    #[storage_mapper("admins")]
    fn admins(&self) -> UnorderedSetMapper<ManagedAddress>;
}
Weekly Installs
7
GitHub Stars
10
First Seen
Feb 8, 2026
Installed on
opencode6
github-copilot6
codex6
kimi-cli6
gemini-cli6
amp6