status | flip | authors | sponsor | updated |
---|---|---|---|---|
implemented |
92 |
Bastian Müller ([email protected]) |
Bastian Müller ([email protected]) |
2023-08-15 |
The goal of this proposal is to improve how Cadence provides access to accounts. The proposed API aims to make account access simpler and safer, by enabling and encouraging the principle of least authority (PoLA) for account access.
In Cadence, access to accounts is provided through the built-in types AuthAccount
and PublicAccount
:
AuthAccount
provides write access to an account,
whereas PublicAccount
only provides read access.
Any program may request read access to an account using the built-in function getAccount
.
Scripts may request write access to an account using the built-in function getAuthAccount
.
A transaction declaration may request write access to zero or more accounts,
through the parameter list of a transaction
declaration's prepare
block.
For example, a transaction declaration which requests access to two accounts may be written as:
transaction {
prepare(signer1: AuthAccount, signer2: AuthAccount) {
/* ... */
}
}
The account API has evolved a lot since it was originally introduced, and several new features were added.
This makes the AuthAccount
type very powerful today:
Having access to the account grants access to everything in the account: storage, contracts, keys, capabilities, etc.
Often, programs do not need full access to an account, and at the same time, it is not possible for developers to only request reduced access. For example, if a transaction transfers tokens, it only needs access to the account's storage, and does not need, nor should have the possibility, to manage contracts, keys, etc.
Cadence has recently gained a powerful new language feature which allows specifying access control in a declarative manner: entitlements, which can be used to improve this situation.
In addition, FLIP 89 is proposing further improvements to access control.
The changes that are proposed in this FLIP affect all Cadence programs which work with accounts, mostly transactions.
Users will benefit from the improved safety that this proposal results in: Cadence developers will be able to be more precise about the account access their programs require, which will in turn give users confidence that transactions they sign will only have limited access, just enough for the transaction to work, and not more. User agents, like wallets, will be able to analyze transactions and communicate to users what kind of account access the user will grant to the application.
The proposal consists of several changes to the existing account API.
The account types AuthAccount
and PublicAccount
are refactored into a single Account
type.
The functionality of both types intentionally overlaps:
The PublicAccount
type only contains/exposes the read-only functionality,
whereas the AuthAccount
type contains/exposes both read and write functionality.
When the two types were introduced, having separate types was the only means to restrict access to a set of functionality.
Today, references and entitlements can be used to implement the access control pattern.
The full type definition of the new Account
type can be found in the separate subsection below.
Several fine-grained entitlements for individual operations (nested functions) and coarse-grained entitlements for groups of operations (nested types) are introduced.
This allows developers to request access to specific account operations.
For example, the fine-grained entitlement AddContract
is required to deploy a new contract,
i.e. to call the contracts.add
function, and the coarse-grained entitlement Contracts
grants
access to all contract management operations, e.g. to contracts.update
.
Similar entitlements are added for other individual management operations and categories of operations,
e.g. the fine-grained AddKey
entitlement is added for allowing to add a key,
and the coarse-grained Keys
entitlement is added to allow any key management operation.
Entitlement mappings are introduced to propagate entitlements to the account to nested types.
For example, a reference to an account with the fine-grained AddContract
or coarse-grained Contracts
entitlement is propagated to the nested Account.Contracts
type:
transaction {
prepare(signer: auth(AddContract) &Account) {
let contracts: auth(AddContract) &Account = signer.contracts
contracts.add(/* ... omitted ... */)
}
}
As the field Account.Contracts
has the access modifier access(AccountMapping)
,
the AccountMapping
entitlement map is used when accessing the field.
As AccountMapping
includes the built-in identity mapping,
the entitlement of AddContract
on the Account
is mapped as-is to AddContract
,
and the add
function, which requires entitlement AddContract
, can be called.
For a full list of entitlements and entitlement mappings,
see the full type definition of Account
below.
This proposal depends on an upcoming FLIP which will propose the idea of identity mapping:
A common use-case for entitlement mappings is to propagate entitlements from the outer to nested types as-is.
For example, given a reference account: auth(AddContract) &Account
,
an access of account.contracts
should also result in auth(AddContract) &Account
, instead of just &Account
.
This can already be expressed today,
by adding entitlement mapping entries like AddContract -> AddContract
for each entitlement.
However, this is quite verbose, especially when many fine-grained entitlements are provided and should be granted transitively.
Instead of exposing the account API directly through an owned Account
type,
the API is exposed through a reference type, &Account
.
This makes the semantics clearer and avoids confusion:
The types already internally have reference semantics,
i.e. multiple copies refer to the same account,
copying an AuthAccount
or PublicAccount
value does not create a new account.
As a result, PublicAccount
is equivalent to &Account
, an unauthorized reference,
i.e., one that does not have any entitlements, and therefore can only access read-only functionality.
Further, AuthAccount
is equivalent to auth(Storage, Contracts, Keys, Inbox, Capabilities) &Account
,
an authorized reference which grants all entitlements, i.e., and therefore can access all functionality
(read and write operations).
This has a nice side-effect: More powerful references are longer and a warning sign.
The minimal type &Account
is the easiest to write and only grants read access,
and developers have to go out of their way to define more powerful references.
This allows developers to request specific access in a transaction.
For example, a transaction which deploys a new contract can use the type auth(AddContract) &Account
in the parameter list of the prepare
block of the transaction.
This ensures that the transaction is only allowed to deploy a contract,
and is not able to perform any other write operations on the account,
e.g. it cannot transfer an object, add a new key, etc.
Contracts have an implicit field named account
which grants the contract full access to the account the contract is deployed to.
The field of the type is changed from AuthAccount
to auth(Storage, Contracts, Keys, Inbox, Capabilities) &Account
.
A future FLIP could maybe allow contracts to override the type of the field,
which would allow developers to restrict the access a contract has to the account.
This proposal depends on FLIP 89,
which proposes that field access on references returns a reference.
For example, given a reference account: &Account
,
accessing the field contracts
results in another reference &Account.Contracts
(instead of the field's type, just Account.Contracts
).
Account capabilities already leverage account references:
The type of an account capability is Capability<&AuthAccount>
.
Just like for the non-reference type AuthAccount
,
the equivalent of the reference type &AuthAccount
will be auth(Storage, Contracts, Keys, Inbox, Capabilities) &Account
.
Over time, the functionality exposed through the AuthAccount
type has grown significantly.
Today, the type grants management access to several parts of the account, such as storage, keys, contracts, capabilities, etc.
Originally, all functionality was exposed directly in the type itself,
which resulted in a large number of unrelated fields and functions.
Previous FLIPs addressed this by grouping related functionality into nested sub-types.
For example, the key and contract management APIs were grouped into the Keys
and Contracts
sub-types.
New functionality, such as the capability inbox and capability controller APIs, were added immediately using this pattern,
i.e. as the Inbox
and Capabilities
sub-types.
What remains in the account types are the storage-related functions and fields,
such as save
, load
, storageUsed
, and storageCapacity
.
Those storage-related functions and fields are refactored into a new sub-type Storage
,
and exposed through a new storage
field.
When Account Linking was added to Cadence,
the #allowAccountLinking
pragma was temporarily added to allow user agents (e.g. wallets)
to detect that a transaction requests the ability to create a new account link.
This proposal obsoletes the pragma – the same functionality, statically requesting/granting access to creating an account capability, can now be expressed through the type system.
For example, a transaction can only issue a new account capability controller
if the transaction requests the fine-grained IssueAccountCapabilityController
or coarse-grained Capabilities
entitlement.
See the Examples section to see how a transaction which issues an account capability controller would look like.
access(all)
struct Account {
/// The address of the account.
access(all)
let address: Address
/// The FLOW balance of the default vault of this account.
access(all)
let balance: UFix64
/// The FLOW balance of the default vault of this account that is available to be moved.
access(all)
let availableBalance: UFix64
/// The storage of the account.
access(AccountMapping)
let storage: Account.Storage
/// The contracts deployed to the account.
access(AccountMapping)
let contracts: Account.Contracts
/// The keys assigned to the account.
access(AccountMapping)
let keys: Account.Keys
/// The inbox allows bootstrapping (sending and receiving) capabilities.
access(AccountMapping)
let inbox: Account.Inbox
/// The capabilities of the account.
access(AccountMapping)
let capabilities: Account.Capabilities
access(all)
struct Storage {
/// The current amount of storage used by the account in bytes.
access(all)
let used: UInt64
/// The storage capacity of the account in bytes.
access(all)
let capacity: UInt64
/// All public paths of this account.
access(all)
let publicPaths: [PublicPath]
/// All storage paths of this account.
access(all)
let storagePaths: [StoragePath]
/// Saves the given object into the account's storage at the given path.
///
/// Resources are moved into storage, and structures are copied.
///
/// If there is already an object stored under the given path, the program aborts.
///
/// The path must be a storage path, i.e., only the domain `storage` is allowed.
access(Storage | SaveValue)
fun save<T: Storable>(_ value: T, to: StoragePath)
/// Reads the type of an object from the account's storage which is stored under the given path,
/// or nil if no object is stored under the given path.
///
/// If there is an object stored, the type of the object is returned without modifying the stored object.
///
/// The path must be a storage path, i.e., only the domain `storage` is allowed.
access(all)
view fun type(at path: StoragePath): Type?
/// Loads an object from the account's storage which is stored under the given path,
/// or nil if no object is stored under the given path.
///
/// If there is an object stored,
/// the stored resource or structure is moved out of storage and returned as an optional.
///
/// When the function returns, the storage no longer contains an object under the given path.
///
/// The given type must be a supertype of the type of the loaded object.
/// If it is not, the function panics.
///
/// The given type must not necessarily be exactly the same as the type of the loaded object.
///
/// The path must be a storage path, i.e., only the domain `storage` is allowed.
access(Storage | LoadValue)
fun load<T: Storable>(from: StoragePath): T?
/// Returns a copy of a structure stored in account storage under the given path,
/// without removing it from storage,
/// or nil if no object is stored under the given path.
///
/// If there is a structure stored, it is copied.
/// The structure stays stored in storage after the function returns.
///
/// The given type must be a supertype of the type of the copied structure.
/// If it is not, the function panics.
///
/// The given type must not necessarily be exactly the same as the type of the copied structure.
///
/// The path must be a storage path, i.e., only the domain `storage` is allowed.
access(all)
view fun copy<T: AnyStruct>(from: StoragePath): T?
/// Returns true if the object in account storage under the given path satisfies the given type,
/// i.e. could be borrowed using the given type.
///
/// The given type must not necessarily be exactly the same as the type of the borrowed object.
///
/// The path must be a storage path, i.e., only the domain `storage` is allowed.
access(all)
view fun check<T: Any>(from: StoragePath): Bool
/// Returns a reference to an object in storage without removing it from storage.
///
/// If no object is stored under the given path, the function returns nil.
/// If there is an object stored, a reference is returned as an optional,
/// provided it can be borrowed using the given type.
/// If the stored object cannot be borrowed using the given type, the function panics.
///
/// The given type must not necessarily be exactly the same as the type of the borrowed object.
///
/// The path must be a storage path, i.e., only the domain `storage` is allowed
access(Storage | BorrowValue)
view fun borrow<T: &Any>(from: StoragePath): T?
/// Iterate over all the public paths of an account,
/// passing each path and type in turn to the provided callback function.
///
/// The callback function takes two arguments:
/// 1. The path of the stored object
/// 2. The runtime type of that object
///
/// Iteration is stopped early if the callback function returns `false`.
///
/// The order of iteration is undefined.
///
/// If an object is stored under a new public path,
/// or an existing object is removed from a public path,
/// then the callback must stop iteration by returning false.
/// Otherwise, iteration aborts.
///
access(all)
fun forEachPublic(_ function: fun(PublicPath, Type): Bool)
/// Iterate over all the stored paths of an account,
/// passing each path and type in turn to the provided callback function.
///
/// The callback function takes two arguments:
/// 1. The path of the stored object
/// 2. The runtime type of that object
///
/// Iteration is stopped early if the callback function returns `false`.
///
/// If an object is stored under a new storage path,
/// or an existing object is removed from a storage path,
/// then the callback must stop iteration by returning false.
/// Otherwise, iteration aborts.
access(all)
fun forEachStored(_ function: fun (StoragePath, Type): Bool)
}
access(all)
struct Contracts {
/// The names of all contracts deployed in the account.
access(all)
let names: [String]
/// Adds the given contract to the account.
///
/// The `code` parameter is the UTF-8 encoded representation of the source code.
/// The code must contain exactly one contract or contract interface,
/// which must have the same name as the `name` parameter.
///
/// All additional arguments that are given are passed further to the initializer
/// of the contract that is being deployed.
///
/// The function fails if a contract/contract interface with the given name already exists in the account,
/// if the given code does not declare exactly one contract or contract interface,
/// or if the given name does not match the name of the contract/contract interface declaration in the code.
///
/// Returns the deployed contract.
access(Contracts | AddContract)
fun add(
name: String,
code: [UInt8]
): DeployedContract
/// Updates the code for the contract/contract interface in the account.
///
/// The `code` parameter is the UTF-8 encoded representation of the source code.
/// The code must contain exactly one contract or contract interface,
/// which must have the same name as the `name` parameter.
///
/// Does **not** run the initializer of the contract/contract interface again.
/// The contract instance in the world state stays as is.
///
/// Fails if no contract/contract interface with the given name exists in the account,
/// if the given code does not declare exactly one contract or contract interface,
/// or if the given name does not match the name of the contract/contract interface declaration in the code.
///
/// Returns the deployed contract for the updated contract.
access(Contracts | UpdateContract)
fun update(name: String, code: [UInt8]): DeployedContract
/// Returns the deployed contract for the contract/contract interface with the given name in the account, if any.
///
/// Returns nil if no contract/contract interface with the given name exists in the account.
access(all)
view fun get(name: String): DeployedContract?
/// Removes the contract/contract interface from the account which has the given name, if any.
///
/// Returns the removed deployed contract, if any.
///
/// Returns nil if no contract/contract interface with the given name exists in the account.
access(Contracts | RemoveContract)
fun remove(name: String): DeployedContract?
/// Returns a reference of the given type to the contract with the given name in the account, if any.
///
/// Returns nil if no contract with the given name exists in the account,
/// or if the contract does not conform to the given type.
access(all)
view fun borrow<T: &Any>(name: String): T?
}
access(all)
struct Keys {
/// Adds a new key with the given hashing algorithm and a weight.
///
/// Returns the added key.
access(Keys | AddKey)
fun add(
publicKey: PublicKey,
hashAlgorithm: HashAlgorithm,
weight: UFix64
): AccountKey
/// Returns the key at the given index, if it exists, or nil otherwise.
///
/// Revoked keys are always returned, but they have `isRevoked` field set to true.
access(all)
view fun get(keyIndex: Int): AccountKey?
/// Marks the key at the given index revoked, but does not delete it.
///
/// Returns the revoked key if it exists, or nil otherwise.
access(Keys | RevokeKey)
fun revoke(keyIndex: Int): AccountKey?
/// Iterate over all unrevoked keys in this account,
/// passing each key in turn to the provided function.
///
/// Iteration is stopped early if the function returns `false`.
///
/// The order of iteration is undefined.
access(all)
fun forEach(_ function: fun(AccountKey): Bool)
/// The total number of unrevoked keys in this account.
access(all)
let count: UInt64
}
access(all)
struct Inbox {
/// Publishes a new Capability under the given name,
/// to be claimed by the specified recipient.
access(Inbox | PublishInboxCapability)
fun publish(_ value: Capability, name: String, recipient: Address)
/// Unpublishes a Capability previously published by this account.
///
/// Returns `nil` if no Capability is published under the given name.
///
/// Errors if the Capability under that name does not match the provided type.
access(Inbox | UnpublishInboxCapability)
fun unpublish<T: &Any>(_ name: String): Capability<T>?
/// Claims a Capability previously published by the specified provider.
///
/// Returns `nil` if no Capability is published under the given name,
/// or if this account is not its intended recipient.
///
/// Errors if the Capability under that name does not match the provided type.
access(Inbox | ClaimInboxCapability)
fun claim<T: &Any>(_ name: String, provider: Address): Capability<T>?
}
access(all)
struct Capabilities {
/// The storage capabilities of the account.
access(CapabilitiesMapping)
let storage: Account.StorageCapabilities
/// The account capabilities of the account.
access(CapabilitiesMapping)
let account: Account.AccountCapabilities
/// Returns the capability at the given public path.
/// Returns nil if the capability does not exist,
/// or if the given type is not a supertype of the capability's borrow type.
access(all)
view fun get<T: &Any>(_ path: PublicPath): Capability<T>?
/// Borrows the capability at the given public path.
/// Returns nil if the capability does not exist, or cannot be borrowed using the given type.
/// The function is equivalent to `get(path)?.borrow()`.
access(all)
view fun borrow<T: &Any>(_ path: PublicPath): T?
/// Publish the capability at the given public path.
///
/// If there is already a capability published under the given path, the program aborts.
///
/// The path must be a public path, i.e., only the domain `public` is allowed.
access(Capabilities | PublishCapability)
fun publish(_ capability: Capability, at: PublicPath)
/// Unpublish the capability published at the given path.
///
/// Returns the capability if one was published at the path.
/// Returns nil if no capability was published at the path.
access(Capabilities | UnpublishCapability)
fun unpublish(_ path: PublicPath): Capability?
}
access(all)
struct StorageCapabilities {
/// Get the storage capability controller for the capability with the specified ID.
///
/// Returns nil if the ID does not reference an existing storage capability.
access(Capabilities | StorageCapabilities | GetStorageCapabilityController)
view fun getController(byCapabilityID: UInt64): &StorageCapabilityController?
/// Get all storage capability controllers for capabilities that target this storage path
access(Capabilities | StorageCapabilities | GetStorageCapabilityController)
view fun getControllers(forPath: StoragePath): [&StorageCapabilityController]
/// Iterate over all storage capability controllers for capabilities that target this storage path,
/// passing a reference to each controller to the provided callback function.
///
/// Iteration is stopped early if the callback function returns `false`.
///
/// If a new storage capability controller is issued for the path,
/// an existing storage capability controller for the path is deleted,
/// or a storage capability controller is retargeted from or to the path,
/// then the callback must stop iteration by returning false.
/// Otherwise, iteration aborts.
access(Capabilities | StorageCapabilities | GetStorageCapabilityController)
fun forEachController(
forPath: StoragePath,
_ function: fun(&StorageCapabilityController): Bool
)
/// Issue/create a new storage capability.
access(Capabilities | StorageCapabilities | IssueStorageCapabilityController)
fun issue<T: &Any>(_ path: StoragePath): Capability<T>
}
access(all)
struct AccountCapabilities {
/// Get capability controller for capability with the specified ID.
///
/// Returns nil if the ID does not reference an existing account capability.
access(Capabilities | AccountCapabilities | GetAccountCapabilityController)
view fun getController(byCapabilityID: UInt64): &AccountCapabilityController?
/// Get all capability controllers for all account capabilities.
access(Capabilities | AccountCapabilities | GetAccountCapabilityController)
view fun getControllers(): [&AccountCapabilityController]
/// Iterate over all account capability controllers for all account capabilities,
/// passing a reference to each controller to the provided callback function.
///
/// Iteration is stopped early if the callback function returns `false`.
///
/// If a new account capability controller is issued for the account,
/// or an existing account capability controller for the account is deleted,
/// then the callback must stop iteration by returning false.
/// Otherwise, iteration aborts.
access(Capabilities | AccountCapabilities | GetAccountCapabilityController)
fun forEachController(_ function: fun(&AccountCapabilityController): Bool)
/// Issue/create a new account capability.
access(Capabilities | AccountCapabilities | IssueAccountCapabilityController)
fun issue<T: &Account>(): Capability<T>
}
}
/* Storage entitlements */
entitlement Storage
entitlement SaveValue
entitlement LoadValue
entitlement BorrowValue
/* Contract entitlements */
entitlement Contracts
entitlement AddContract
entitlement UpdateContract
entitlement RemoveContract
/* Key entitlements */
entitlement Keys
entitlement AddKey
entitlement RevokeKey
/* Inbox entitlements */
entitlement Inbox
entitlement PublishInboxCapability
entitlement UnpublishInboxCapability
entitlement ClaimInboxCapability
/* Capability entitlements */
entitlement Capabilities
entitlement StorageCapabilities
entitlement AccountCapabilities
entitlement PublishCapability
entitlement UnpublishCapability
entitlement GetStorageCapabilityController
entitlement IssueStorageCapabilityController
entitlement GetAccountCapabilityController
entitlement IssueAccountCapabilityController
/* Entitlement mappings */
entitlement mapping AccountMapping {
/*
Identity is a built-in mapping which implicitly maps E -> E, for any entitlement E.
A FLIP will propose this feature separately.
*/
include Identity
Storage -> SaveValue
Storage -> LoadValue
Storage -> BorrowValue
Contracts -> AddContract
Contracts -> UpdateContract
Contracts -> RemoveContract
Keys -> AddKey
Keys -> RevokeKey
Inbox -> PublishInboxCapability
Inbox -> UnpublishInboxCapability
Inbox -> ClaimInboxCapability
Capabilities -> StorageCapabilities
Capabilities -> AccountCapabilities
}
entitlement mapping CapabilitiesMapping {
include Identity
StorageCapabilities -> GetStorageCapabilityController
StorageCapabilities -> IssueStorageCapabilityController
AccountCapabilities -> GetAccountCapabilityController
AccountCapabilities -> IssueAccountCapabilityController
}
The function getAccount
allows accessing the public portion of an account
in any context, in transactions and scripts.
The function currently returns a PublicAccount
value, and has the signature:
fun getAccount(_ address: Address): PublicAccount
The return type of the function is changed from PublicAccount
to the equivalent &Account
,
i.e., the function is changed to have the following signature:
fun getAccount(_ address: Address): &Account
The function getAuthAccount
allows accessing the authorized portion of an account in a script.
The function currently returns an AuthAccount
value, and has the signature:
fun getAuthAccount(_ address: Address): AuthAccount
The return type of the function is changed from AuthAccount
to a type parameter,
i.e., the function is changed to have the following signature:
fun getAuthAccount<T: &Account>(_ address: Address): T
This allows "summoning" any account type needed. For example, to get access to the storage of account at address 0x1:
let ref = getAuthAccount<auth(Storage) &Account>(0x1)
All contracts have a a pre-declared field account
, which currently has the type AuthAccount
.
The field's type is changed to auth(Storage, Contracts, Keys, Inbox, Capabilities) &Account
.
The constructor function fun AuthAccount(payer: AuthAccount): AuthAccount
creates a new account,
given an account which is charged the cost for the creation.
This function is replaced with the new function
fun Account(payer: auth(BorrowValue | Storage) &Account): auth(Storage, Contracts, Keys, Inbox, Capabilities) &Account
.
Like before, it creates a new account, but now returns a fully-entitled reference to the new account.
Like before, an account must pay for the creation,
but given that the charging only requires borrowing a fungible token vault from storage,
only those entitlements are necessary.
This proposal depends on several language features: references, entitlements, entitlement mappings, field access on references, etc. As the majority of Cadence programs interact with accounts, the majority of developers will use this new account API.
Usage of the API will likely be unsurprising to developers, but understanding why and how the API works will require an understanding of those language features.
None
None
The effort required to implement the proposed changes is about a month of work. In addition, existing stored data must be migrated, which will require additional work to implement a storage migration.
Cadence programs, especially transactions, should follow the principle of least authority (PoLA): They should only request the least amount of entitlements for an account that are necessary to perform the intended operation on the account.
Fine-grained account entitlements should be preferred over coarse-grained account entitlements.
Fully entitled account references should be avoided, as they are very powerful, and unlikely to be necessary for the majority of use-cases.
Today, a transaction which deploys a contract is likely written as:
transaction {
prepare(signer: AuthAccount) {
signer.contracts.add(name: "MyContract", code: [/* code */])
}
}
With this FLIP implemented, the same transaction can now be written as follows:
transaction {
prepare(signer: auth(AddContract) &Account) {
signer.contracts.add(name: "MyContract", code: [/* code */])
}
}
Note the change in the parameter list of the prepare
block:
Instead of requesting access to the whole account, only the AddContract
entitlement is requested,
which means that the contracts.add
function is available,
while other operations on the account, like adding a key (keys.add
), are unavailable.
Today, a transaction which using the linking-based capability API is likely written as:
#allowAccountLinking
transaction {
prepare(signer: AuthAccount) {
signer.linkAccount(/private/account)
}
}
With the new Capability Controllers API it is likely written as:
transaction {
prepare(signer: AuthAccount) {
signer.capabilities.account.issue<&AuthAccount>()
}
}
With this FLIP implemented, the same transaction can now be written as follows:
transaction {
prepare(signer: auth(IssueAccountCapabilityController) &Account) {
signer.capabilities.account.issue<&Account>()
}
}
Note the change in the parameter list of the prepare
block:
Instead of requesting access to the whole account, and annotating the transaction with a special pragma,
only the IssueAccountCapabilityController
entitlement is requested,
which means that the capabilities.account.issue
function is available,
while other operations on the account, like adding a key (keys.add
), are unavailable.
In addition, note how the issue
function takes a type parameter.
Currently, the only possible type is &AuthAccount
,
which allows the capability to perform all operations on the account.
With this FLIP, the type is any subtype of &Account
.
That means by "default", the capability is not authorized to perform any operations on the account,
and the issuer may choose to grant certain coarse or fine-grained entitlements as needed.
For example, it is possible to issue a new account capability which is able to access the account's storage, but not perform other operations, like adding keys or contracts:
signer.capabilities.account.issue<auth(Storage) &Account>()
The proposal is not backwards-compatible, it is a breaking change.
Existing programs which use the account API, i.e. the AuthAccount
and PublicAccount
,
will need to be migrated to the new Account
type.
Existing stored data will be migrated automatically:
- The type
PublicAccount
will be migrated to&Account
. - The type
AuthAccount
will be migrated toauth(Storage, Contracts, Keys, Inbox, Capabilities) &Account
.
To provide better backwards compatibility and reduce the amount of broken programs,
PublicAccount
and AuthAccount
could stay in the language as type aliases for their respective equivalents.
However, this is intentionally not proposed:
Not providing aliases forces developers to update their programs and encourages them to follow PoLA,
ultimately resulting in a better, and especially safer experience for users.
The majority of Cadence developers will be impacted by the change and will need to update their programs, especially transactions. The required changes should be minimal: Statements can very often stay as-is, the majority of changes will be for type annotations, e.g. in transactions. That will require developers to determine what set of entitlements need to be requested. In return, safety for users will improve, as they will be able to better understand what operations may be performed by a transaction they sign.
None
It is still unclear how entitlements should be named.
This proposal, along with FLIP 86, is the first case where built-in entitlements are added to Cadence. As such, the FLIP is setting a precedent for the naming of entitlements in general.
For now, entitlements are named in verb form, in upper camel-case. That can be easily changed.