Access control

Managing permissions and access control with principal functions.

Principal functions in Clarity are essential tools for implementing robust access control and permission management in smart contracts. These functions allow developers to identify, authenticate, and authorize different entities interacting with the contract, ensuring that only the right parties can perform specific actions or access certain data.

Why these functions matter

Clarity's principal functions are designed with blockchain-specific considerations in mind:

  1. Identify and authenticate users and contracts interacting with your smart contract.
  2. Implement role-based access control for different contract functions.
  3. Ensure that only authorized entities can perform certain actions or access specific data.
  4. Create multi-signature schemes for enhanced security.

Key elements for access control

1. asserts!

What: A function that checks a condition and throws an error if it's not met.

Why: Essential for enforcing access control rules and validating conditions.

When: Use when you need to ensure a condition is true before proceeding with a function.

Best practices:

  • Use asserts! to enforce access control rules and validate conditions.
  • Consider using asserts! in combination with other principal functions for robust access control.

Example use case: Using asserts! to check if a user has sufficient balance before performing a transfer.

;; Define a map to store user balances
(define-map Balances principal uint)

;; Function to transfer tokens
(define-public (transfer (amount uint) (recipient principal))
  (let
    (
      (senderBalance (default-to u0 (map-get? Balances tx-sender)))
    )
    ;; Assert that the sender has sufficient balance
    (asserts! (>= senderBalance amount) (err u1))
    
    ;; If assertion passes, proceed with the transfer
    (map-set Balances tx-sender (- senderBalance amount))
    (map-set Balances recipient (+ (default-to u0 (map-get? Balances recipient)) amount))
    (ok true)
  )
)

;; Function to check balance
(define-read-only (get-balance (user principal))
  (default-to u0 (map-get? Balances user))
)

2. tx-sender

What: A keyword that represents the current transaction sender.

Why: Important for identifying who is calling a contract function.

When: Use when you need to check permissions or record actions associated with the caller.

Best practices:

  • Always validate tx-sender before performing sensitive operations.
  • Don't rely solely on tx-sender for complex authentication schemes.

Example use case: Restricting a function to be called only by the contract owner.

(define-data-var contractOwner principal tx-sender)

(define-public (restricted-function)
  (begin
    (asserts! (is-eq tx-sender (var-get contractOwner)) (err u1))
    ;; Function logic here
    (ok true)
  )
)

3. contract-caller

What: A keyword that represents the immediate caller of the current contract.

Why: Allows for more granular control in contract-to-contract interactions.

When: Use when your contract might be called by other contracts and you need to distinguish between the original sender and the immediate caller.

Best practices:

  • Use in conjunction with tx-sender for comprehensive access control.
  • Be cautious of potential confusion between tx-sender and contract-caller in complex call chains.

Example use case: Implementing a whitelist for contracts allowed to call a function.

(define-map AllowedCallers principal bool)

(define-public (whitelisted-function)
  (begin
    (asserts! (default-to false (map-get? AllowedCallers contract-caller)) (err u2))
    ;; Function logic here
    (ok true)
  )
)

4. is-eq

What: Checks if two values are equal.

Why: Crucial for comparing principals and implementing access control logic.

When: Use when you need to verify if a caller matches a specific principal or if two principals are the same.

Best practices:

  • Use for exact matching of principals.
  • Consider using in combination with other checks for more robust authentication.

Example use case: Multi-signature functionality requiring approval from specific principals.

(define-constant APPROVER_ONE 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5)
(define-constant APPROVER_TWO 'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG)
  
(define-public (approve-transaction (transactionId uint))
  (begin
    (asserts! (or (is-eq tx-sender APPROVER_ONE) (is-eq tx-sender APPROVER_TWO)) (err u3))
    ;; Approval logic here
    (ok true)
  )
)

Practical example: simple governance contract

Let's implement a basic governance contract that demonstrates role-based access control using principal functions. This contract will have an owner, administrators, and regular members, each with different permissions.

;; Define maps to store roles
(define-map Administrators principal bool)
(define-map Members principal bool)

;; Define data variables
(define-data-var contractOwner principal tx-sender)
(define-data-var proposalCounter uint u0)

;; Define a map to store proposals
(define-map Proposals
  uint
  {
    title: (string-ascii 50),
    proposer: principal,
    votesFor: uint,
    votesAgainst: uint
  }
)

;; Function to add an administrator (only owner can do this)
(define-public (add-administrator (newAdmin principal))
  (begin
    (asserts! (is-eq tx-sender (var-get contractOwner)) (err u1))
    (ok (map-set Administrators newAdmin true))
  )
)

;; Function to add a member (only Administrators can do this)
(define-public (add-member (newMember principal))
  (begin
    (asserts! (default-to false (map-get? Administrators contract-caller)) (err u2))
    (ok (map-set Members newMember true))
  )
)

;; Function to create a proposal (only members can do this)
(define-public (create-proposal (title (string-ascii 50)))
  (let
    (
      (proposalId (var-get proposalCounter))
    )
    (asserts! (default-to false (map-get? Members tx-sender)) (err u3))
    (map-set Proposals proposalId
      {
        title: title,
        proposer: tx-sender,
        votesFor: u0,
        votesAgainst: u0
      })
    (var-set proposalCounter (+ proposalId u1))
    (ok proposalId)
  )
)

;; Function to vote on a proposal (only members can do this)
(define-public (vote (proposalId uint) (voteFor bool))
  (let
    (
      (proposal (unwrap! (map-get? Proposals proposalId) (err u4)))
    )
    (asserts! (default-to false (map-get? Members tx-sender)) (err u5))
    (if voteFor
      (map-set Proposals proposalId (merge proposal { votesFor: (+ (get votesFor proposal) u1) }))
      (map-set Proposals proposalId (merge proposal { votesAgainst: (+ (get votesAgainst proposal) u1) }))
    )
    (ok true)
  )
)

;; Function to transfer ownership (only current owner can do this)
(define-public (transfer-ownership (newOwner principal))
  (begin
    (asserts! (is-eq tx-sender (var-get contractOwner)) (err u6))
    (var-set contractOwner newOwner)
    (ok true)
  )
)

Conclusion

Principal functions in Clarity provide powerful tools for implementing secure and flexible access control in smart contracts. By understanding when and how to use these functions, developers can create robust permission systems, ensuring that only authorized entities can perform specific actions or access certain data. Always consider the specific security requirements of your application when implementing access control mechanisms using these principal functions.