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}))