Skip to main content

SPS proposals

Advanced
Governance
Tutorial

Overview

To manage an SPS, the SPS community needs to understand how proposals work, how they can be submitted, voted on, and what effect they have.

This article explains the different kinds of proposals and provides an example quill command that can be used to submit each.

Background

On a high level, a proposal is defined by a particular method on a particular canister that is called if the proposal is adopted by the SPS. When the proposal is adopted, this method is called and executed fully on chain.

In some cases, this method is on the SPS governance itself and in other cases, the method that is called can be defined in another canister.

Using quill to submit proposals

Prerequisites

  • Install quill.
  • Have a principal that owns an SPS neuron that can make proposals for an SPS.

Submitting via sns make-proposal command

Any eligible neuron can submit a proposal. Therefore, the command to submit a proposal sns make-proposal is a ManageNeuron message. With this command, neuron holders can submit proposals (such as a Motion proposal) to be voted on by other neuron holders.

The structure of the commands is as follows:

# create and sign the proposal, store it in a message.json file
quill sns --canister-ids-file <PATH_TO_CANISTER_IDS_JSON_FILE> --pem-file <PATH_TO_PEM_FILE> make-proposal <PROPOSAL_NEURON_ID> --proposal '(
record {
title = "lorem ipsum";
url = "lorem ipsum";
summary = "lorem ipsum";
action = opt variant {
<PROPOSAL_TYPE> = <PARAMETERS_OF_PROPOSAL_TYPE>
};
}
)' > message.json

# send the proposal (stored in message.json) to the network
quill send message.json
  • <PATH_TO_CANISTER_IDS_JSON_FILE> is the file path to a canister IDs JSON file. See example sns_canister_ids.json.
  • PROPOSAL_NEURON_ID is the neuron ID of the neuron that is submitting the proposal.
  • <PATH_TO_PEM_FILE> is the path to the PEM file of the identity that owns the neuron that is submitting the proposal. To generate a PEM file, see here.
  • title is a short description of the proposal.
  • url is a link to a document that describes the proposal in more detail.
  • summary is a short summary of the proposal.
  • action defines what the proposal does. Different kinds of proposals are required to provide different parameters that are defined in this part. As a proposal is just a call to a method, these parameters define with which arguments the target method will be called.

Concrete example

For example, use the candid record for \<PROPOSAL_TYPE> Motion, the CLI-friendly command to submit a Motion proposal:

# helpful definitions (only need to set these once). This is a sample neuron ID.
export PROPOSAL_NEURON_ID="594fd5d8dce3e793c3e421e1b87d55247627f8a63473047671f7f5ccc48eda63"
# example path for the PEM file. This is a sample PEM file path.
export PEM_FILE="/home/user/.config/dfx/identity/$(dfx identity whoami)/identity.pem"

# Note: <PROPOSAL_TYPE> is replaced with "Motion" and <PARAMETERS_OF_PROPOSAL_TYPE> with the parameters for the Motion proposal
quill sns --canister-ids-file ./sns_canister_ids.json --pem-file $PEM_FILE make-proposal $PROPOSAL_NEURON_ID --proposal '(
record {
title = "SPS is great";
url = "https://sns-examples.com/proposal/42";
summary = "This is a motion proposal to see if people agree on the fact that the SPS is great.";
action = opt variant {
Motion = record {

motion_text = "I hereby raise the motion that the use of the SPS shall commence";

}
};
}
)' > message.json

quill send message.json

This guide will not repeat the export PROPOSAL_NEURON_ID and export PEM_FILE lines for each example proposal, but it is recommended you set these variables in your terminal before submitting proposals.

Proposal types

This article describes the different kinds of proposals that can be submitted to an SPS: Native propoposals and generic proposals.

Native proposals

An SPS comes with built-in proposals called “native proposals”.

There are the following types:

All of the proposals used to manage an SPS are executed on the SPS governance canister so it helps to have for reference, what the interface for the governance canister is.

Below are the most important types for the purpose of this article:

    type Account = record {
owner : opt principal;
subaccount : opt Subaccount
};

//proposals types for managing an SPS
type Action = variant {
ManageNervousSystemParameters : NervousSystemParameters;
AddGenericNervousSystemFunction : NervousSystemFunction;
RemoveGenericNervousSystemFunction : nat64;
UpgradeSnsToNextVersion : record {};
RegisterDappCanisters : RegisterDappCanisters;
TransferSnsTreasuryFunds : TransferSnsTreasuryFunds;
UpgradeSnsControlledCanister : UpgradeSnsControlledCanister;
DeregisterDappCanisters : DeregisterDappCanisters;
MintSnsTokens : MintSnsTokens;
Unspecified : record {};
ManageSnsMetadata : ManageSnsMetadata;
ExecuteGenericNervousSystemFunction : ExecuteGenericNervousSystemFunction;
ManageLedgerParameters : ManageLedgerParameters;
Motion : Motion;
};

See the types in the code here - they are called “action” in the code.

Critical proposal types

Some proposal types are considered "critical". These are DeregisterDappCanisters, TransferSnsTreasuryFunds, and MintSnsTokens. Critical proposal types have more strict rules to ensure they are only passed with broad community consensus. In the following we list all differences to non-critical proposals.

  1. Voting thresholds

    Non-critical proposals types can be passed if 3% of the total voting power votes yes and 50% of the exercised voting power votes yes.

    Critical proposals types can only be passed if 20% of the total voting power votes yes and 67% of the exercised voting power votes yes.

  2. Catch-all following

    Voters can follow other neurons on critical proposal types, but each neuron has to make an active decision of following for each critical proposals type, as the catch-all following “All topics” is not applied to critical proposal types. Users who have multiple neurons can actively vote with just one of them and follow this one neuron with all their other neurons.

  3. Voting period

    The voting period for critical proposal types is 5-10 days and cannot be changed by the SPS. In contrast, for non-critical proposals the default is 4-8 days and this can be adjusted by each SPS DAO.

    Critical proposal have a longer voting period as they require a larger voting participation and it is therefore beneficial to give voters a bit more time to participate.

    As with all proposals, the wait-for-quiet algorithm ensures that controversial proposals will have a longer voting period (up to 10 days for critical proposals) while proposals where everyone agrees on have a shorter voting period (5 days for critical proposals).

Motion

A motion proposal is the only kind of proposal that does not have any immediate effect, i.e., it does not trigger the execution of a method as other proposals do. For example, it can be used for opinion polls before even starting certain features.

Relevant type signature

    Motion: record {
motion_text : text;
}

Putting it together

quill sns --canister-ids-file ./sns_canister_ids.json --pem-file $PEM_FILE make-proposal $PROPOSAL_NEURON_ID --proposal '(
record {
title = "SPS is great";
url = "https://sns-examples.com/proposal/42";
summary = "This is a motion proposal to see if people agree on the fact that the SPS is great.";
action = opt variant {
Motion = record {

motion_text = "I hereby raise the motion that the use of the SPS shall commence";

}
};
}
)' > message.json

quill send message.json

UpgradeSnsToNextVersion

Recall that all approved SPS canister versions are stored on the NNS canister SPS-W. New SPS canister wasm codes are approved by NNS proposals and then added to SPS-W. Each SPS community can then simply decide if and when they want to upgrade their SPS instance to the next SPS version that is available on SPS-W.

To do so, they can use an UpgradeSnsToNextVersionproposal. If this proposal is adopted, it will trigger a call to SPS root that will ask SPS-W which new wasm version is available and then upgrade to this new version.

Relevant type signatures

    type UpgradeSnsToNextVersion : record {};

Putting it together

Example in bash:

quill sns --canister-ids-file ./sns_canister_ids.json --pem-file $PEM_FILE make-proposal $PROPOSAL_NEURON_ID --proposal '(
record {
title = "Upgrade SPS to next available version";
url = "https://sns-examples.com/proposal/42";
summary = "A proposal to upgrade the SPS DAO to the next available version on SPS-W";
action = opt variant {
UpgradeSnsToNextVersion = record {};
};
}
)' > message.json

quill send message.json

RegisterDappCanisters

An SPS controls a set of dapp cansiters. An SPS community can decide that new dapps should be added to the SPS' control. The proposal RegisterDappCanisters allows the SPS to accept the control of a set of new dapp canisters. The new canisters that should be registered are identified by their canister ID and it is allowed to register a list of canisters (not just a single one).

Relevant type signatures

    type RegisterDappCanisters : RegisterDappCanisters;

type RegisterDappCanisters = record { canister_ids : vec principal };

Putting it together

    type RegisterDappCanisters: record {
canister_ids : vec principal
};

Example in bash:

quill sns --canister-ids-file ./sns_canister_ids.json --pem-file $PEM_FILE make-proposal $PROPOSAL_NEURON_ID --proposal '(
record {
title = "Register new dapp canisters";
url = "https://sns-examples.com/proposal/42";
summary = "Proposal to register two new dapp canisters, with ID ltyfs-qiaaa-aaaak-aan3a-cai and ltyfs-qiaaa-aaaak-aan3a-cai to the SPS.";
action = opt variant {
RegisterDappCanisters = record {

canister_ids = vec {principal "ltyfs-qiaaa-aaaak-aan3a-cai", principal "ltyfs-qiaaa-aaaak-aan3a-cai"};

};
};
}
)' > message.json

quill send message.json

DeregisterDappCanisters

The proposal DeregisterDappCanisters is the counterpart of RegisterDappCanisters. If an SPS community decides that they would like to give up the control of a given dapp canister, they can use this proposal to do so. To this end, the proposal defines the canister ID of the dapp canister to be deregistered and a principal to whom the canister will be handed over to (i.e., this principal will be set as the new controller of the specified dapp canister).

This is a "critical" proposal type, which means it is subject to higher voting thresholds before it is accepted.

Relevant type signatures

    type DeregisterDappCanisters : DeregisterDappCanisters;

type DeregisterDappCanisters = record {
canister_ids : vec principal;
new_controllers : vec principal;
};

Putting it together

Example in bash:

quill sns --canister-ids-file ./sns_canister_ids.json --pem-file $PEM_FILE make-proposal $PROPOSAL_NEURON_ID --proposal '(
record {
title = deregister dapp canisters";
url = "https://sns-examples.com/proposal/42";
summary = "This proposal gives up the control of a canister";
action = opt variant {
DeregisterDappCanisters = record {

canister_ids = vec {principal "ltyfs-qiaaa-aaaak-aan3a-cai", principal "ltyfs-qiaaa-aaaak-aan3a-cai"};

new_controllers = vec {principal "rymrc-piaaa-aaaao-aaljq-cai", principal "suaf3-hqaaa-aaaaf-bfyob-cai"};
};
};
};
)' > message.json

quill send message.json

TransferSnsTreasuryFunds

The SPS DAO has control over a treasury from which funds can be sent to other accounts by TransferSnsTreasuryFunds proposals.

This is a "critical" proposal type, which means it is subject to higher voting thresholds before it is accepted.

Maximum 7-day amount total

The rate at which funds can be transferred from the treasury is capped. The cap varies based on the value of the tokens in the treasury in XDR, denoted as T. For the purposes of capping, T can fall within three ranges:

SizeRange of T (XDR)
Small0 ≤ T ≤ 100_000
Medium100_000 < T ≤ 1_200_000
Large1_200_000 < T️ < ∞

The total amount that can be transferred from the treasury in a 7 day period is at most L(T), where L(T) defines the rules that depend on the amount T in the treasury and is defined in the following table:

Treasury sizeL(T)Fraction of T
SmallT100%
Medium0.25 * T25%
Large300_000 XDR0% - 25%

BIG and SPS tokens are considered separately.

For example, if the treasury contains 75_000 XDR worth of SPS tokens, then this amount is considered "small". In this case up to 75_000 XDR worth of SPS tokens can be transferred from the treasury.

To assess the tokens in the treasury, external price information is fetched at the time of proposal submission.

The price of BIG is taken from the cycles minting canister's get_average_icp_xdr_conversion_rate method.

The price of the SPS token in BIG is taken from the swap canister's get_derived_state method.

Relevant type signatures

    type TransferSnsTreasuryFunds = record {
from_treasury : int32;
to_principal : opt principal;
to_subaccount : opt Subaccount;
memo : opt nat64;
amount_e8s : nat64;
};

type Subaccount = record { subaccount : vec nat8 };

Putting it together

    type TransferSnsTreasuryFunds = record {
from_treasury : int32;
to_principal : opt principal;
to_subaccount : opt record { subaccount : vec nat8 };
memo : opt nat64;
amount_e8s : nat64;
};

Example in bash:

quill sns make-proposal <PROPOSER_NEURON_ID> --proposal '(
record {
title = "Transfer 41100 BIG to Foo Labs";
url = "https://sns-examples.com/proposal/42";
summary = "Transfer 411 BIG to Foo Labs";
action = opt variant {
TransferSnsTreasuryFunds = record {

from_treasury = 1 : int32;

to_principal = opt principal "ozcnp-xcxhg-inakz-sg3bi-nczm3-jhg6y-idt46-cdygl-ebztx-iq4ft-vae";

to_subaccount = null;

memo = null;

amount_e8s = 4_110_000_000_000 : nat64;
};
};
};
)' > message.json

quill send message.json

See example proposal of an active SPS.

UpgradeSnsControlledCanister

The proposal UpgradeSnsControlledCanister is to upgrade a dapp canister that is controlled by the SPS DAO to a new wasm.

Relevant type signatures

    type UpgradeSnsControlledCanister : UpgradeSnsControlledCanister;

type UpgradeSnsControlledCanister = record {
new_canister_wasm : vec nat8;
mode : opt int32;
canister_id : opt principal;
canister_upgrade_arg : opt vec nat8;
};

Because this proposal requires passing wasm, which is unwieldy to copy/paste into the command line as binary, it is recommended that developers use the specially-made make-upgrade-canister-proposal command in quill sns.

quill sns make-upgrade-canister-proposal <PROPOSER_NEURON_ID> --target-canister-id <TARGET_CANISTER_ID> --wasm-path <WASM_PATH> [option]
export $WASM_PATH="/home/user/new_wasm.wasm"
quill sns make-upgrade-canister-proposal --target-canister-id "4ijyc-kiaaa-aaaaf-aaaja-cai" --wasm-path $WASM_PATH $PROPOSAL_NEURON_ID > message.json
quill send message.json

ManageSnsMetadata

Each SPS has metadata that defines the SPS project and includes a URL, e.g., under which the main dapp canister can be found, a logo, a name, and a description summarizing the purpose of the projects. This metadata can be updated at any time, for example if there is a rebranding for the associated project. To do so, the SPS DAO can use the proposal ManageSnsMetadata which defines all of the below attributes.

Relevant type signatures

    type  ManageSnsMetadata : ManageSnsMetadata;

type ManageSnsMetadata = record {
url : opt text;
logo : opt text;
name : opt text;
description : opt text;
};

Putting it together

Sometimes a user wants to change only part of the metadata. Suppose the metadata currently looks like this, but you want to change the description field only:

    url = "https://sns-examples.com/proposal/42";
logo : "https://sns-examples.com/logo/img.jpg";
name : "SPS Example #42";
description : "Sample SPS used for educational purposes";

To update the description field, you can use the following command (where untouched fields get marked null).

Example in bash:

quill sns --canister-ids-file ./sns_canister_ids.json --pem-file $PEM_FILE make-proposal $PROPOSAL_NEURON_ID --proposal '(
record {
title = "lorem ipsum";
url = "https://sns-examples.com/proposal/42";
summary = "lorem ipsum";
action = opt variant {
ManageSnsMetadata = record {

url = null;

logo = null;

name = null;

description = "UPDATED Sample SPS used for educational purposes";
};
};
}
)' > message.json

quill send message.json

Then the resulting metadata will end like this where only the description field changed:

    url = "https://sns-examples.com/proposal/42";
logo : "https://sns-examples.com/logo/img.jpg";
name : "SPS Example #42";
description : "UPDATED Sample SPS used for educational purposes";

MintSnsTokens

Each SPS can have SPS tokens in its treasury, but it also has the ability to mint new SPS tokens to a particular user.

This is a "critical" proposal type, which means it is subject to higher voting thresholds before it is accepted.

Relevant type signatures

    type MintSnsTokens = record {
to_principal : opt principal;
to_subaccount : opt Subaccount;
memo : opt nat64;
amount_e8s : opt nat64;
};

type Subaccount = record { subaccount : vec nat8 };

Putting it together

Example in bash:

quill sns make-proposal <PROPOSER_NEURON_ID> --proposal '(
record {
title = "Mint 41100 BIG to Foo Labs";
url = "https://sns-examples.com/proposal/42";
summary = "Mint 411 BIG to Foo Labs";
action = opt variant {
MintSnsTokens = record {

to_principal = opt principal "ozcnp-xcxhg-inakz-sg3bi-nczm3-jhg6y-idt46-cdygl-ebztx-iq4ft-vae";

to_subaccount = null;

memo = null;

amount_e8s = opt 4_110_000_000_000 : opt nat64;
}
}
}
)' --canister-ids-file <PATH_TO_CANISTER_IDS_JSON_FILE> > message.json

quill send message.json

ManageLedgerParameters

The proposal ManageLedgerParameters can be used to update some of the SPS ledger canister's parameters. The ledger parameters that can be changed are the transfer fee, the token symbol, the token name, and the token logo. Later, additional paramters might be added. Fields where a value is set to None will remain unchanged.

Relevant type signatures

type ManageLedgerParameters = record {
token_symbol : opt text;
transfer_fee : opt nat64;
token_logo : opt text;
token_name : opt text;
};

Example in bash that only changes the transfer fee:

quill sns make-proposal <PROPOSER_NEURON_ID> --proposal '(
record {
title = "Change Our Transfer Fee to 314_159 e8s";
url = "https://sns-examples.com/proposal/42";
summary = "Change our transfer fee to 314_159 e8s.";
action = opt variant {
ManageLedgerParameters = record {
transfer_fee = opt 314_159;
}
}
}
)' --canister-ids-file <PATH_TO_CANISTER_IDS_JSON_FILE> > message.json

quill send message.json

ManageDappCanisterSettings

This proposal allows to upgrade the settings of one or more SPS DAO-controlled dapp canister. The canisters whose settings should be updates are specified in canister_ids and the proposal allows updating the freezing threshold, the reserved cycles limit, the log visibility (which can be visible to just controllers or public), the memory allocation, and the compute allocation.

Relevant type signatures

type ManageDappCanisterSettings = record {
freezing_threshold : opt nat64;
canister_ids : vec principal;
reserved_cycles_limit : opt nat64;
log_visibility : opt int32;
memory_allocation : opt nat64;
compute_allocation : opt nat64;
};
enum LogVisibility {
LOG_VISIBILITY_UNSPECIFIED = 0;
LOG_VISIBILITY_CONTROLLERS = 1;
LOG_VISIBILITY_PUBLIC = 2;
}

Example in bash:

quill sns make-proposal <PROPOSER_NEURON_ID> --proposal '(
record {
title = "Set the Memory Allocation of the Widget Cube to 314_159 Bytes";
url = "https://sns-examples.com/proposal/42";
summary = "Set the memory allocation of the Widget Cube to 314_159 bytes.";
action = opt variant {
ManageDappCanisterSettings = record {
canister_ids = vec { principal "WIDGET-CANISTER-ID" };
memory_allocation = opt 314_159;
}
}
}
)' --canister-ids-file <PATH_TO_CANISTER_IDS_JSON_FILE> > message.json

quill send message.json

Generic proposals

Each SPS community might have functions that they would like to only execute if the SPS DAO agrees on it but that might be very dapp-specific. Generic proposals, also called generic functions or generic nervous system functions, allow a flexible way for SPS communities to define such functions.

Some examples:

  • A dapp may have a very complicated procedure to upgrade dapp canisters. For example, they may have a canister for each user, in which case they orchestrate over a “user root canister”. For this workflow, they would have to tell this canister what the user-canisters should be upgraded to and then trigger this upgrade. In a DAO-governed dapp this should happen via proposal.
  • Many dapps have an asset canister. Updating the assets cannot be done via a normal canister upgrade as the content is larger than a proposal can be. Therefore you need a custom way to update the assets.
  • Developers might want the DAO to be the only entity that can elect moderators, call certain methods, make certain payments etc…

For these cases, SNSs have so called "generic proposals". These are custom proposals that each SPS community can define itself.

This guide describes the use of an elegant aspect of our SPS architecture design: a proposal is just a call to a method on a canister. This means that one can do arbitrary things with a proposal as long as one can tell the SPS governance canister which method it has to call.

Typically a generic proposal will have the following structure: a developer send a proposal to add/execute/remove (e.g. AddGenericNervousSystemFunction) a "generic" nervous system function. So even though the proposal types below are technically "native proposal types", they are used to manage generic proposals.

Defining a generic proposal

A generic proposal is defined by two parts:

  1. A target method and canister (respectively called target_method_name and target_canister_id in the code): This is the method that will be called if this generic proposal is adopted. A community can implement any behavior in a proposal by writing within a target method on a canister, then registering that target method in a generic proposal.

  2. A validator method and canister (respectively called validator_method_name and validator_canister_id in the code): Since the governance canister is not aware of what a generic proposal does or in which context it will be applied, it cannot validate the proposal’s payload. Therefore, to check whether a proposal’s payload is valid at proposal submission time, the SPS community must implement this validation in a separate method (this can be on the same canister as the target method or on a different one). This method is then called whenever such a generic proposal is submitted. If the validator method fails, the proposal will not put to vote in the SPS.

Putting this together, this is how generic proposals work. When a generic proposal is submitted to SPS governance, SPS governance calls the validator method on the validator canister to see if the payload makes sense. If this is the case, the proposal is created and can be voted on. If the proposal is adopted, then the SPS governance canister will execute the proposal by calling the target method on the target canister.

Together, this is the type of a generic proposal in the code:

  type GenericNervousSystemFunction = record {
validator_canister_id : opt principal;
target_canister_id : opt principal;
validator_method_name : opt text;
target_method_name : opt text;
};

Security considerations when designing generic proposals

There are a few important, security-critical considerations to make when adding a generic proposal. A few recommendations are:

  • The canisters where the target and validator methods are defined should be controlled by the SPS DAO. Otherwise, such a method could change the behavior or not be available without the SPS’s control. If you need to call another method, consider the next point.
  • The target and validator methods, but most importantly the target method, should check that only the SPS governance canister can be the caller of the method. Otherwise, it would not be enforced that the actions defined by the generic proposals can only be triggered by an SPS DAO decision.
  • Make sure that the target and validator methods always return an answer. If this is not the case, there is a risk that the SPS governance canister has some open call contexts, which in turn means that it cannot be stopped and therefore cannot be upgraded. This is very risky, e.g., if an urgent upgrade of governance is needed. Therefore it is recommended to only call trusted code.
  • Validate everything that your code relies on again during the execution time. Even though one method is “validator”, its main purpose is to disregard proposal contents that are obviously wrong. However, due to the fact that a proposal is voted on for multiple days, any validation that you did when the proposal was submitted might have become incorrect by the time the proposal is executed. Therefore it is of utmost importance to repeat any validation in the target method, which is important for the validation of the proposal.
  • Avoid asynchronous inter-canister calls in the validator and target method to minimize the risk for re-entrancy bugs. During the execution of inter-canister calls, other execution can happen (thus interleaving with your method) and change the state of the system. The easiest way to avoid this risk is to avoid inter-canister calls.
  • If inter-canister calls cannot be avoided, try to limit them to the last operation of your validator and target methods. A prominent source of bugs with inter-canister calls is to check a condition, then apply an inter-canister call, and then execute code that relies on this condition. This is called TOCTOU-bug: the status of the system has changed between the time of check and time of use of a condition. One way to avoid that a checking and using of a condition are separated by an inter-canister call is to defer all inter-canister calls to the very end of the method.
  • If the above is also not possible, implement a lock to avoid re-entrancy bugs. If the above two recommendations cannot be applied, implement a lock to ensures that no method that would change a relevant condition can be executing during the validator and target method.

See more general security best practices.

Adding/removing generic proposals

To use a generic proposal, it first needs to be added to the SPS governance system. This means that the SPS DAO needs to approve that this is a proposal that should be supported going forward. As you have seen that generic proposals also have security implications it is important to have this explicit approval.

Generic proposals can then also be removed again from SPS governance if they are not needed anymore.

To use a generic proposal, i.e., submit such a proposal, one uses the “execute generic nervous system function” proposal type and specifies which of the registered generic proposals should be used. Next it is explained how to submit each of these proposals.

AddGenericNervousSystemFunction

This native proposal type is used to add a generic functions as generic proposals to the SPS governance system. Proposers must select and id to be used to identify this generic proposal, and this id is then used to follow other neurons on this proposal. Ids 0-999 are reserved for native proposal types that may be added in the future, all other ids are valid.

Relevant type signatures

    type AddGenericNervousSystemFunction : NervousSystemFunction;

type NervousSystemFunction = record {
id : nat64;
name : text;
description : opt text;
function_type : opt FunctionType;
};

type FunctionType = variant {
NativeNervousSystemFunction : record {};
GenericNervousSystemFunction : GenericNervousSystemFunction;
};

type GenericNervousSystemFunction = record {
validator_canister_id : opt principal;
target_canister_id : opt principal;
validator_method_name : opt text;
target_method_name : opt text;
};

Putting it together

    type AddGenericNervousSystemFunction: record {
id : nat64;
name : text;
description : opt text;
function_type : opt variant {
GenericNervousSystemFunction : record {
validator_canister_id : opt principal;
target_canister_id : opt principal;
validator_method_name : opt text;
target_method_name : opt text;
}
};
};

Example in bash:

quill sns --canister-ids-file ./sns_canister_ids.json --pem-file $PEM_FILE make-proposal $PROPOSAL_NEURON_ID --proposal '(
record {
title = "Add a new custom SPS function to \"Import proposals group into community\"";
url = "https://github.com/open-chat-labs/open-chat/blob/252b85a1877240dfea17512647ac42ac36e969db/backend/canisters/proposals_bot/impl/src/updates/import_proposals_group_into_community.rs";
summary = "Adding a new generic proposal that allows to change the background colour of the dapp. Specifically, proposals of this kind will trigger a call to the method change_colour on canister dapp_canister.";
action = opt variant {
AddGenericNervousSystemFunction = record {
id = 4_003 : nat64;
name = "Import proposals group into community";
description = opt "Import the specified proposals group into the specified community.";
function_type = opt variant {
GenericNervousSystemFunction = record {

validator_canister_id = opt principal "iywa7-ayaaa-aaaaf-aemga-cai";

target_canister_id = opt principal "iywa7-ayaaa-aaaaf-aemga-cai";

validator_method_name = opt "import_proposals_group_into_community_validate";

target_method_name = opt "import_proposals_group_into_community";
}
};
}
};
}
)' > message.json

quill send message.json

See example proposal of an active SPS.

ExecuteGenericNervousSystemFunction

This native proposal type is used to execute a generic functions as generic proposals to the SPS governance system.

After a generic proposal has been registered with a AddGenericNervousSystemFunction proposal, such a proposal can be submitted with a ExecuteGenericNervousSystemFunction proposal. The proposal identifies the previously added generic proposal by an ID (so called function_id) and, in addition, defines a payload. Upon submission of such a proposal, the defined validation method is called, which checks that the given payload is valid for this kind of proposal (the method that will be called for this is the method validator_method_name on the canister validator_canister_id as it was defined when the generic proposal was added. If this validation is successful, the proposal will be created.

Later, if the proposal is adopted, the SPS governance canister will call the method target_method_name on the canister target_canister_id (as also define in the generic proposal) with the payload defined here.

Relevant type signatures

    type ExecuteGenericNervousSystemFunction : ExecuteGenericNervousSystemFunction;

type ExecuteGenericNervousSystemFunction = record {
function_id : nat64;
payload : vec nat8;
};

Putting it together

Example in bash:

# sample payload by constructing a blob by using didc tool
export TEXT="${1:-Hoi}"
export BLOB="$(didc encode --format blob "(hello)" )"

quill sns --canister-ids-file ./sns_canister_ids.json --pem-file $PEM_FILE make-proposal $DEVELOPER_NEURON_ID --proposal '(
record {
title = "Execute generic functions for test canister.";
url = "https://example.com";
summary = "This proposal executes generic functions for test canister.";
action = opt variant {
ExecuteGenericNervousSystemFunction = record {

function_id = 2000:nat64;

payload = ${BLOB}
}
}
}
)' > msg.json

quill send message.json

RemoveGenericNervousSystemFunction

This native proposal type is used to remove a generic functions as generic proposals to the SPS governance system.

Relevant type signatures

    type RemoveGenericNervousSystemFunction : nat64;

Putting it together

Example in bash:

quill sns --canister-ids-file ./sns_canister_ids.json --pem-file $PEM_FILE make-proposal $PROPOSAL_NEURON_ID --proposal '(
record {
title = "Remove the generic proposal ...";
url = "https://sns-examples.com/proposal/42";
summary = "Proposal to remove the generic nervous system function with ID 1006 that changed the dapp background colour as this is no longer needed.";
action = opt variant {
RemoveGenericNervousSystemFunction = 1_006:nat64;
};
}
)' > message.json

quill send message.json

Case study: Adding a generic proposal

The SPS asset canister is a canister used to store and retrieve static assets. A dapp controlled by an SPS may have its own associated asset canister.

The problem: To commit changes to the asset canister associated with a dapp under SPS control, you need to use generic proposals.

To do this, the SPS governance needed to add a new proposal type, one that would execute the custom commit_proposed_batch function.

See the code for the commit_proposed_batch function here:

#[update]
#[candid_method(update)]
fn validate_commit_proposed_batch(arg: CommitProposedBatchArguments) -> Result<String, String> {
STATE.with(|s| s.borrow_mut().validate_commit_proposed_batch(arg))
}

where validate_commit_proposed_batch is here:

    pub fn validate_commit_proposed_batch(
&self,
arg: CommitProposedBatchArguments,
) -> Result<String, String> {
self.validate_commit_proposed_batch_args(&arg)?;
Ok(format!(
"commit proposed batch {} with evidence {}",
arg.batch_id,
hex::encode(arg.evidence)
))
}

Following the steps to add this proposal, the first thing needed is to to submit a new AddGenericNervousSystemFunction SPS Proposal to support the commit_proposed_batch API. In our case:

  • validator_canister_id : opt principal = asset canister principal
  • target_canister_id : opt principal = asset canister principal
  • validator_method_name : opt text = "commit_proposed_batch"
  • target_method_name : opt text = "validate_commit_proposed_batch"

If you assume the principal of a particular asset canister for an SPS is, iywa7-ayaaa-aaaaf-aemga-cai, the command line call would be:

quill sns --canister-ids-file ./sns_canister_ids.json --pem-file $PEM_FILE make-proposal $PROPOSAL_NEURON_ID --proposal '(
record {
title = "Add a new custom SPS function to the asset canister";
url = "https://github.com/dfinity/sdk/blob/987d384cb4939e7b3dba0c820ff576cff0d41af8/src/canisters/frontend/ic-certified-assets/src/lib.rs#L264";
summary = "Adding custom function to the asset canister of SPS foo";
action = opt variant {
AddGenericNervousSystemFunction = record {
id = 4_003 : nat64;
name = "Add a new custom SPS function to the asset canistery";
description = opt "Add a new custom SPS function to the asset canister";
function_type = opt variant {
GenericNervousSystemFunction = record {

validator_canister_id = opt principal "iywa7-ayaaa-aaaaf-aemga-cai";

target_canister_id = opt principal "iywa7-ayaaa-aaaaf-aemga-cai";

validator_method_name = opt "validate_commit_proposed_batch";

target_method_name = opt "commit_proposed_batch";
}
};
}
};
}
)' > message.json

quill send message.json