Skip to main content

SNS asset canisters

Advanced
Governance
Tutorial

The asset canister provides users with a way to store and retrieve static assets from a canister deployed on ICP. Generally, asset canisters are used to serve HTML, CSS, or JavaScript assets, which are typically part of a dapp's frontend. For this reason, the asset canister is also referred to as the frontend canister. For purposes of this guide, it will be referred to as the asset canister.

In the context of the SNS, a dapp's associated asset canister serves the frontend assets related to the dapp and may also include a frontend to the SNS DAO, e.g., through which users can vote on governance proposals.

The contents of the asset canister can be configured prior to the launch of the SNS. If configured correctly, any changes afterwards must be made by a principal with the Prepare permission. Principals with this Prepare permission can make a batch of changes to the asset canister and then 'lock' those changes. To have those changes applied, a proposal must be submitted. Anyone can submit the proposal for the 'locked' changes. Once changes are proposed, they can be voted on by the SNS DAO. If approved, the SNS governance canister is the only one that can commit the approved changes. This configuration assures that changes to the asset canister are only made by approved proposals. These changes are referred to as 'updates' to the asset canister in the remainder of this document.

This section is relevant if your project contains an asset canister and describes how you can test handing over control of an asset canister to an SNS.

In this document, the term upgrade refers to deploying an upgraded version of a canister's WebAssembly module.

The term update refers to changing or updating the assets stored within an asset canister.

Asset canister example

An example of an SNS asset canister is canister sqbzf-5aaaa-aaaam-aavya-cai, which is an asset canister part of the Dragginz Dapp SNS.

Deploying an asset canister

The general overview of deploying an asset canister during an SNS launch is as follows:

  • First, the asset canister must be created with or upgraded to a Wasm file from dfx 0.15.2+.
  • Then, developers should use revoke_permission to remove their own permissions (especially Commit) that allow them to update the assets arbitrarily.
  • Then, after launching the SNS, the SNS's function should be registered to commit proposed changes.
  • Last, using upgrade arguments, updated permissions can be set. This is only possible after the SNS canisters have been successfully deployed and initialized because before the launch the principal id of SNS governance is not known yet
    • The SNS governance canister is given Commit permissions. With Commit, SNS governance may apply a batch of proposed asset updates to the assets served by the asset canister.
    • To give certain individuals the permission to upload changes to the asset canister that can then be put to vote, these principals can be granted Prepare permissions. Changes created using Prepare permissions must be approved through a proposal before they are applied to the asset canister.
    • To facilitate permission management via custom SNS proposals, it can make sense to grant SNS governance the ManagePermissions permission. With this permission, it is not necessary to upgrade the asset canister every time permissions are supposed to change.

To deploy an asset canister, any canister in the dfx.json file can be set as "type": "assets", and dfx will automatically generate the required files, such as an assets.wasm.gz file and a candid/assets.did file once the canister has been built.

An example of configuring an asset canister within the dfx.json file can be found below:

    "assets": {
"source": [],
"type": "assets"
}

Deploying locally for testing

To deploy your asset canister locally for testing purposes, the following command can be used:

dfx deploy assets

Deploying on the mainnet

To deploy to the mainnet, you will need a wallet that contains cycles. For more information on cycles wallets, please see here.

To deploy your asset canister to the mainnet, the following command can be used:

dfx deploy assets --network ic

Configuring an asset canister's permissions

When configuring an asset canister, a set of permissions that contains a whitelist of principals must be created. This whitelist details who is allowed to submit changes that update assets in the asset canister. Principals that are allowed to submit changes are given the Prepare permission.

Once a principal with Prepare permissions submits changes to the asset canister, these changes are set in a 'locked' state. Then, anyone can submit a proposal that proposes the 'locked' changes be applied; there is no permission necessary to submit this proposal.

Only principals with Commit rights may apply proposed changes.

Once the asset canister has been handed over to the SNS, only the governance canister should have Commit rights, and principals in the whitelist should have Prepare rights. The developer who configured and deployed the SNS should have their permissions removed prior to the SNS launch.

Granting permissions

To grant a principal permission within an asset canister, the following command can be used:

dfx canister call --network ic <canister-id> grant_permission '(record {permission = variant {<Permission: Commit / Prepare / ManagePermissions >}; to_principal = principal "<principal>"})'

Revoking permissions

To revoke a principal's permission within an asset canister, the following command can be used:

dfx canister call --network ic <canister-id>  revoke_permission '(record {permission = variant {<Permission: Commit / Prepare / ManagePermissions >}; of_principal = principal "<principal>"})'

Listing permissions

To list permissions for an asset canister, the following command can be used:

dfx canister call --network ic <canister-id> list_permitted '(record {permission = variant {<Permission>}})'

For example, to list all principals with the Commit permission:

dfx canister call --network ic oa7fk-maaaa-aaaam-abgka-cai list_permitted '(record {permission = variant {Commit}})'

To list all principals with the Prepare permission:

dfx canister call --network ic oa7fk-maaaa-aaaam-abgka-cai list_permitted '(record {permission = variant {Prepare}})'

Once the asset canister has been deployed and permissions have been configured, the SNS decentralization swap can be started. You can learn more about launching an SNS here

Overwriting permissions

Upgrade arguments can be used by the permissions lists to set new values, regardless of the current state of the permissions.

The upgrade argument is of type (AssetCanisterArgs), where the contained types are:

  type AssetCanisterArgs = variant {
Init: InitArgs;
Upgrade: UpgradeArgs;
};
type InitArgs = record {};
type UpgradeArgs = record {
set_permissions: opt SetPermissions;
};
/// Sets the list of principals granted each permission.
type SetPermissions = record {
prepare: vec principal;
commit: vec principal;
manage_permissions: vec principal;
};

Therefore, to set the permissions such that SNS governance has ManagePermissions and Commit permissions and principal1 has Prepare permissions, the upgrade argument would look like this:

  (variant {
Upgrade = record {
set_permissions = opt record {
prepare = vec { principal "<principal1>"; };
commit = vec { principal "<SNS governance>"; };
manage_permissions = vec { principal "<SNS governance>"; };
}
}
})

SNS GenericNervousSystemFunctions

Generic proposals are a way for each SNS to define custom proposals. To commit changes to the asset canister, such generic proposals are used.

To use a generic proposal, first this new proposal type has to be "registered" in the SNS. To do so, one uses a AddGenericNervousSystemFunction proposal.

To submit a new AddGenericNervousSystemFunction SNS Proposal to support the commit_proposed_batch API, the target canister id should be the asset canister (that is updated in the update steps below) and the target function is commit_proposed_batch. The validate function should be validate_commit_proposed_batch.

The ExecuteGenericNervousSystemFunction SNS proposal is used to execute the newly registered proposal. To submit an ExecuteGenericNervousSystemFunction SNS Proposal with the output from dfx deploy FRONTEND_CANISTER_NAME --network ic --by-proposal see update steps below.

Submitting an SNS proposal to upgrade an asset canister

Once an asset canister is under the control of the SNS, any changes must be done via an SNS proposal.

Before submitting an SNS proposal to update an asset canister, assure that the asset canister has been upgraded (by proposal) to use the Wasm bundled with dfx 0.14.1+.

  • Step 1: To submit an SNS proposal, first have a principal with Prepare permission run:

dfx deploy FRONTEND_CANISTER_NAME --network ic --by-proposal

The output will contain something like this:

Proposed commit of batch 2 with evidence e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855. Either commit it by proposal, or delete it.
  • Step 2: Encode the evidence and batch number.

The evidence bytes must be Candid-encoded bytes. These are the arguments you will submit with the ExecuteGenericNervousSystemFunction proposal.

EVIDENCE_STRING=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
BATCH_ID=123
didc encode "(record{ batch_id = $BATCH_ID : nat; evidence = blob \"$(echo $EVIDENCE_STRING | sed 's/../\\&/g')\"; })"
  • Step 3: To ensure that others would be able to verify the evidence in the proposal, have someone else clone the dapp repo and run:

dfx deploy FRONTEND_CANISTER_NAME --network ic --compute-evidence

The computed evidence should match the evidence from step 1.

  • Step 4: Submit a new proposal to commit the batch, using the APIs below:

Use these asset canister APIs in the proposal:

  type CommitProposedBatchArguments = record {
batch_id: BatchId;
evidence: blob;
};
type ValidationResult = variant { Ok : text; Err : text };

validate_commit_proposed_batch: (CommitProposedBatchArguments) -> (ValidationResult);
commit_proposed_batch: (CommitProposedBatchArguments) -> ();

If the proposal is rejected, the preparer should use this new asset canister API:

  type DeleteBatchArguments = record {
batch_id: BatchId;
};
delete_batch: (DeleteBatchArguments) -> ();

Asset canister upgrade proposal example

Below is an example script that can be run to setup the necessary proposals to upgrade an asset canister:

#!/bin/bash

# Set current directory to the scripts root
SCRIPT=$(readlink -f "$0")
SCRIPT_DIR=$(dirname "$SCRIPT")
cd $SCRIPT_DIR

TITLE="Update Frontend Canister Permissions."
SUMMARY="Update the frontend canister permissions, adding commit permissions for the governance canister."
URL="https://example.com"

CANISTER_NAME=example_frontend
NETWORK="ic"

# Get the target canister id
TARGET_CANISTER_ID=$(dfx -qq canister --network $NETWORK id $CANISTER_NAME)

dfx identity use ic_admin
OWNER_IDENTITY=$(dfx identity whoami)
PEM_FILE="$(readlink -f "$HOME/.config/dfx/identity/${OWNER_IDENTITY}/identity.pem")"
PROPOSER_NEURON_ID=be1741c21c35da7e386d2ccf59a3d5d14c97d9b82b36045f4a8d4c336864f6dc

UPGRADE_ARG="(opt variant {
Upgrade = record {
set_permissions = opt record {
prepare = vec {
principal \"detjl-sqaaa-aaaaq-aacqa-cai\";
principal \"4jijx-ekel7-4t2kx-32cyf-wzo3t-i4tas-qsq4k-ujnug-oxke7-o5aci-eae\"
};
commit = vec { principal \"detjl-sqaaa-aaaaq-aacqa-cai\"};
manage_permissions = vec { principal \"detjl-sqaaa-aaaaq-aacqa-cai\"}
}
}
})"

echo UPGRADE_ARG
# Make the proposal using quill
quill sns --canister-ids-file ../../utils/sns_canister_ids.json --pem-file $PEM_FILE make-upgrade-canister-proposal --title "$TITLE" --url "$URL" --summary "$SUMMARY" $PROPOSER_NEURON_ID --target-canister-id $TARGET_CANISTER_ID --wasm-path '../../../.dfx/ic/canisters/example_frontend/assetstorage.wasm.gz' --canister-upgrade-arg "$UPGRADE_ARG" > msg.json
quill send msg.json
rm -f msg.json

Then, deploy the asset canister to be upgraded via proposal:

dfx deploy example_frontend --network ic --by-proposal

Lastly, submit the proposal to upgrade the asset canister:

#!/bin/bash

# Set current directory to the scripts root
SCRIPT=$(readlink -f "$0")
SCRIPT_DIR=$(dirname "$SCRIPT")
cd $SCRIPT_DIR

TITLE="Update Frontend Canister."
SUMMARY="Update frontend canister with evidence batch id 1."
URL="https://example.com"

EVIDENCE_STRING=55d25c6fb1d28bff2f1f5df99c39d5b50f60d97e892caad03933e2622af4a797
BATCH_ID=1
FUNCTION_ID=24000

# Submit the proposal

../../utils/make_custom_function_proposal_frontend.sh $FUNCTION_ID "$TITLE" "$SUMMARY" "$URL" "$EVIDENCE_STRING" "$BATCH_ID" "commit_proposed_batch"