Helper function to restrict contract calls

Implement access control to ensure functions can only be called by users, not other contracts


;; Check if caller is a standard principal (user wallet)
(define-private (is-standard-principal-call)
(is-none (get name (unwrap! (principal-destruct? contract-caller) false)))
)
;; Public function restricted to direct user calls
(define-public (user-only-function (amount uint))
(begin
(asserts! (is-standard-principal-call) (err u401))
;; Function logic here
(ok true)
)
)

Use cases

  • Preventing contract-to-contract reentrancy attacks
  • Ensuring human-initiated transactions for governance
  • Restricting token minting to direct user actions
  • Protecting admin functions from automated calls

Key concepts

Principal types in Clarity:

  • Standard principals: User wallets (SP/ST addresses)
  • Contract principals: Deployed contracts (address.contract-name)
  • contract-caller: The immediate caller of the current function
  • tx-sender: The original transaction initiator

Complete access control example

(define-constant ERR_UNAUTHORIZED (err u401))
(define-constant ERR_CONTRACT_CALL (err u402))
;; Check if caller is the tx-sender (no intermediary contracts)
(define-private (is-direct-call)
(is-eq contract-caller tx-sender)
)
;; Check if caller is a user (not a contract)
(define-private (is-user-call)
(is-none (get name (unwrap! (principal-destruct? contract-caller) false)))
)
;; Restrict to direct user calls only
(define-public (mint-tokens (recipient principal) (amount uint))
(begin
;; Must be called directly by a user
(asserts! (is-direct-call) ERR_CONTRACT_CALL)
(asserts! (is-user-call) ERR_UNAUTHORIZED)
;; Mint logic here
(ft-mint? my-token amount recipient)
)
)
;; Allow contract calls but log them
(define-public (transfer-with-logging (amount uint) (recipient principal))
(let ((is-contract-call (not (is-user-call))))
;; Log contract calls for transparency
(if is-contract-call
(print { event: "contract-transfer", from: contract-caller, amount: amount })
(print { event: "user-transfer", from: tx-sender, amount: amount })
)
(ft-transfer? my-token amount tx-sender recipient none)
)
)

Admin access patterns

(define-constant CONTRACT_OWNER tx-sender)
;; Owner-only function
(define-public (admin-function (new-fee uint))
(begin
(asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_UNAUTHORIZED)
(asserts! (is-user-call) ERR_CONTRACT_CALL)
(var-set protocol-fee new-fee)
(ok true)
)
)
;; Multi-signature admin control
(define-map admins principal bool)
(define-map approvals { action: (string-ascii 50), nonce: uint } uint)
(define-data-var approval-threshold uint u2)
(define-public (propose-action (action (string-ascii 50)))
(let ((nonce (var-get action-nonce)))
(asserts! (is-user-call) ERR_CONTRACT_CALL)
(asserts! (default-to false (map-get? admins tx-sender)) ERR_UNAUTHORIZED)
(map-set approvals { action: action, nonce: nonce } u1)
(ok nonce)
)
)
Security consideration

While restricting contract calls can prevent certain attacks, it may also limit composability. Consider your protocol's needs carefully before implementing these restrictions.

Flexible access control

;; Allow specific contracts
(define-map allowed-contracts principal bool)
(define-private (is-allowed-caller)
(or
(is-user-call)
(default-to false (map-get? allowed-contracts contract-caller))
)
)
;; Admin can whitelist contracts
(define-public (add-allowed-contract (contract principal))
(begin
(asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_UNAUTHORIZED)
(map-set allowed-contracts contract true)
(ok true)
)
)
;; Function with flexible access
(define-public (flexible-function (value uint))
(begin
(asserts! (is-allowed-caller) ERR_UNAUTHORIZED)
;; Function logic
(ok value)
)
)

Testing access control

;; Test helper functions
(define-read-only (check-caller-type)
{
is-user: (is-user-call),
is-direct: (is-direct-call),
caller: contract-caller,
sender: tx-sender
}
)
;; Analyze any principal
(define-read-only (analyze-principal (p principal))
(match (principal-destruct? p)
details {
type: (if (is-some (get name details)) "contract" "user"),
version: (get version details),
hash: (get hash-bytes details),
name: (get name details)
}
{
type: "invalid",
version: u0,
hash: 0x,
name: none
}
)
)