Cadence is a programming language designed explicitly for managing ownership of valuable digital assets, like art, collectibles, or cryptocurrencies on the blockchain. Cadence protects your digital assets with fine-grained access control, making an accidental loss or malicious duplication impossible.
With Cadence, digital assets are represented using a unique data model: resources. Resources facilitate the creation, transfer, and storage of digital assets. Combined with capability-based access control, this language is easier to learn, audit, and use than any other currently available language.
During our recent Dapper Labs audit engagement, Halborn security researcher Ferran Celades discovered a vulnerability in how the Secure Cadence update to Cadence manages resources.
Halborn subsequently put together a document outlining the best security practices that Cadence Developers should follow. The document, published on Halborn’s Github, is reproduced below.
Best Security Practices for Cadence Developers
- Developers can access fields and call functions on the referenced object using Reference. These are ephemeral values and cannot be stored. The developer should store a capability and borrow it when needed if persistence is required.
- In Cadence, users have complete control over their data and may reorganize it as they see fit. Developers should never trust the users’ account storage as users may store values in any path so that paths may store values of “unexpected” types. These values may be instances of types in contracts that the user deployed. It is advised to always borrow with the specific type that is expected or check if the value is an instance of the expected type.
- Access to AuthAccount will grant full access to the storage, keys, and contracts. Hence it is advised to avoid using AuthAccount as a function parameter unless absolutely necessary.
- Anyone can access your public capability using the getCapability function. That is why it is said not to store anything under the public capability storage unless strictly required. If something needs to be stored under public, developers should ensure only read functionality is provided by restricting its type using either a resource interface or struct interface.
- Authorized References, i.e., references with the auth keyword, allow downcasting. For example, a restricted type to its unrestricted type should only be used in some specific cases. The subtype or unrestricted type could expose functionality that was not intended to be exposed. Therefore, developers should not use authorized references when exposing functionality.
- While the developer links a capability, the link might already be present as the link function does not check if the target path is valid/exists when the capability is created. In that case, Cadence will not panic with a runtime error; instead, the link function will return nil. It is a good practice to check if the link already exists before creating it with getLinkTarget, and this function will return nil if the link does not exist.
- Whenever it is necessary to handle the case where borrowing a capability (borrow) might fail, the check function could be used to verify that the target exists and has a valid type.
- It is always advised not to blindly sign a transaction as doing so will grant access to the AuthAccount, i.e., full access to the account’s storage, keys, and contracts. This could change the deployed contracts by upgrading them with malicious statements, revoking or adding keys, transferring resources from storage, etc.
- Using capabilities allows the revocation of access by unlinking and limiting access to a single value with a specific set of functionality – access to an AuthAccount gives full access to the whole storage and key and contract management. Therefore, It is preferable to use capabilities over direct AuthAccount storage when exposing account data.
- While auditing the Cadence code, it is strictly advised to include transactions as they may contain arbitrary code, just like in contracts. In addition, they are given full access to the accounts of the transaction’s signers, i.e., the transaction is allowed to manipulate the signers’ account storage, contracts, and keys.
- Types used should always be as specific (restrictive) as possible, especially for resource types, following the principle of least privilege. When exposing functionality, developers should provide the least necessary access. Also, developers should make sure not to use authorized references, as they can be downcasted, potentially allowing a user to gain access to supposedly restricted functionality. If given a less-specific type, cast to the more specific type that is expected.
- Always prefer non-public access to the mutable state as declaring a field as pub/access(all) only protects from replacing the field’s value. However, the value itself can still be mutated if it is mutable. Remember that containers, like dictionaries, and arrays, are mutable. Such mutable state may also be nested. For example, a child may still be mutated even if its parent exposes it through a field with non-settable access.
- Ensure capabilities are not accessed by any unauthorized parties. Capabilities should not be accessible through a public field, including public dictionaries or arrays. Exposing a capability in such a way will allow anyone to borrow it and perform all the capability’s actions.
- It is recommended not to use the pub/access(all) modifier on fields and functions unless necessary. Instead, priv/access(self), access(contract), and access(account) could be used when other types in the contract or account need to have access. Refer to the design pattern to learn more.
In a nutshell, Cadence is a great programming language and can make smart contract development faster and safer simultaneously. But developers need to ensure that they are following all the security practices to make their code as hack-proof as possible.