Core Concepts: Integrations

Taking Actions On Behalf of Users

Ferum is a platform other financial applications can be built on and this requires having the ability to take actions on behalf of users.

Protocol Registration

To start, protocols must register using the ferum::platform::register_protocol. This returns a ProtocolCapability. Protocols should store this capability in a top level struct defined by the protocol.

The ProtocolCapability is then used to get an AccountIdentifier via ferum::platform::get_account_identifier. This identifier uniquely identifies a market account and can be used to open one on behalf of the user.

Opening a market account will return a MarketAccountKey, which is used to take various actions.

Orders placed by protocols on behalf of users can only be cancelled by the protocol that placed them. Ferum trades will settle into the market account that placed the order.

Here is an example of a project called quicksilver registering as a protocol and then placing on order for a user:

struct FerumCapabilityStore has key {
    cap: ProtocolCapability,
}

public entry fun example_register(signer: &signer) {
    assert!(address_of(signer) == @quicksilver, 0);
    let cap = ferum::platform::register_protocol(signer);
    move_to(signer, FerumCapabilityStore{
        cap,
    });
}

public entry fun example_open_account_for_user(user: &signer, ...) {
    let cap = &borrow_global<FerumCapabilityStore>(@quicksilver).cap;
    let accID = ferum::platform::get_account_identifier(user, cap);
    let _mak = ferum::market::open_market_account(user, vector[accID]);
    // Can optionally store the above MarketAccountKey
}

public entry fun example_place_order_for_user(user: &signer, ...) {
    // If the MarketAccountKey isn't stored anywhere, it can be derived again.
    let cap = &borrow_global<FerumCapabilityStore>(@quicksilver).cap;
    let accID = ferum::platform::get_account_identifier(user, cap);
    let mak = ferum::market::get_market_account_key(user, vector[accID]);
    
    ferum::market::add_order(user, mak, ...);
}

Resource Accounts

More complex use cases will require more fine grained control over an account by a protocol. For example, margin trading requires the protocol to loan assets to a user and potentially liquidate them if their loan to value (LTV) ratio rises above a limit.

To enable these use cases, protocols can use Aptos resource accounts. A resource account is an account created using an address and a seed. A protocol can create resource accounts that it has fully custody over. These accounts can then be used to place orders on Ferum.

Since the user won't have access to funds in the resource account, protocols can implement fine grained controls over assets (ex: if a user's account exceeds an LTV ratio, the user is liquidated).

Here is a reference example for this pattern:

// Protocol Config struct.
struct Config has key {
    // The signing capability used to generate accounts.
    // This capability itself is associated with a resource account
    // that is created when the app is initialized. We generate user 
    // accounts using this resource account to ensure that only the protocol 
    // has control over the user accounts.
    adminSigningCap: account::SignerCapability,
    // Used as part of the seed to generate accounts for users. 
    // Each time a user creates an account, this is incremented, ensuring
    // that each user has a unique account.
    nonce: u128,
}

// Represents a user's account.
struct Account has key {
    // The address of the user this account belongs to.
    owner: address,
    // The signing capability used to take actions as this account.
    signingCap: account::SignerCapability,
}

public fun create_user_account(): SignerCapability acquires Config {
    let cfg = borrow_global_mut<Config>(@admin);
    let adminSigner = &account::create_signer_with_capability(&cfg.adminSigningCap);
    
    // Create user account using admin resource account.
    let seed = bcs::to_bytes(&@perimeter);
    vector::append(&mut seed, bcs::to_bytes(&cfg.nonce));
    cfg.nonce = cfg.nonce + 1;
    vector::append(&mut seed, USER_ACCOUNT_SALT);
    let (_, signingCap) = account::create_resource_account(adminSigner, seed);
    signingCap
}

// Called by users to create an account.
public entry fun register(owner: &signer) {
    let ownerAddr = address_of(owner);
    assert!(!exists<Account>(ownerAddr), ERR_ALREADY_REGISTERED);
    let signingCap = create_user_account();
    move_to(owner, Account{
        owner: ownerAddr,
        signingCap,
    });
}

// Helper to get user account signer.
public fun get_user_account_signer(owner: address): signer acquires Account {
    let accountCap = &borrow_global<Account>(owner).signingCap;
    account::create_signer_with_capability(accountCap)
}

// Let users trade on the APT/USDF market using their user account.
public entry fun trade(
    owner: &signer,
    side: u8,
    type: u8,
    price: u64,
    qty: u64,
) {
    let ownerAddr = address_of(owner);
    let accountSigner = get_user_account_signer(ownerAddr);
    let clientOrderID = 0;
    // Ferum will settle this trade directly into the resource 
    // account's market account.
    add_order<AptosCoin, USDF>(
        &accountSigner,
        side,
        type,
        price,
        qty,
        clientOrderID,
    );
}

Last updated