Browse Source

feat: add examples for principal keywords, as-contract (#604)

master-legacy
Aaron Blankstein 5 years ago
committed by GitHub
parent
commit
5c6f14e954
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 209
      _core/smart/principals.md

209
_core/smart/principals.md

@ -36,6 +36,32 @@ is a multiple of 10, otherwise returning a 400 error code.
(err u400)))
```
Clarity provides an additional variable to help smart contracts
authenticate a transaction sender. The keyword `contract-caller`
returns the principal that _called_ the current contract. If an
inter-contract call occurred, `contract-caller` returns the last
contract in the stack of callers. For example, suppose there are three
contracts A, B, and C, each with an `invoke` function such that
`A::invoke` calls `B::invoke` and `B::invoke` calls `C::invoke`.
When a user Bob issues a transaction that calls `A::invoke`, the value
of `contract-caller` in each successive invoke function's body would change:
```
in A::invoke, contract-caller = Bob
in B::invoke, contract-caller = A
in C::invoke, contract-caller = B
```
This allows contracts to make assertions and perform authorization
checks using not only the `tx-sender` (which in this example, would
always be "Bob"), but also using the `contract-caller`. This could be
used to ensure that a particular function is only ever called directly
and never called via an inter-contract call (by asserting that
`tx-sender` and `contract-caller` are equal). We provide an example of
a two different types of authorization checks in the rocket ship
example below.
## Smart contracts as principals
Smart contracts themselves are principals and are represented by the
@ -100,3 +126,186 @@ In this example, the public function `claim-from-faucet`:
Unlike other principals, there is no private key associated with a
smart contract. As it lacks a private key, a Clarity smart contract
cannot broadcast a signed transaction on the blockchain.
## Example: Authorization checks
The interactions between `tx-sender`, `contract-caller` and
`as-contract` are subtle, but are important when performing
authorization checks in a contract. In this example contract,
we'll show two different kinds of authorization checks
a contract may wish to perform, and then walk through how
different ways in which contract functions may be called
will pass or fail those checks.
This contract defines a "rocket-ship" non-fungible-token
that a principal may own and manage the authorized pilots.
Pilots are principals that are allowed to "fly" the
rocket ship.
This contract performs two different authorization checks:
1. Before a ship is allowed to fly, the contract checks whether or not
the transaction was created and signed by an authorized pilot. A
pilot, could, for example, call another contract, which then calls
the "fly-ship" public function on the pilot's behalf.
2. Before modifying the allowed-pilots for a given rocket ship, the
contract checks that the transaction was signed by the owner of the
rocket ship. Furthermore, the contract requires that this function
be called _directly_ by the ship's owner, rather than through a
inter-contract-call.
The second type of check is more restrictive than the first check,
and is helpful for guarding very sensitive routines --- it
protects users from unknowingly calling a function on a malicious
contract that subsequently tries to call sensitive functions on
another contract.
```scheme
;;
;; rockets-base.clar
;;
(define-non-fungible-token rocket-ship uint)
;; a map from rocket ships to their allowed
;; pilots
(define-map allowed-pilots
((rocket-ship uint)) ((pilots (list 10 principal))))
;; implementing a contains function via fold
(define-private (contains-check
(y principal)
(to-check { p: principal, result: bool }))
(if (get result to-check)
to-check
{ p: (get p to-check),
result: (is-eq (get p to-check) y) }))
(define-private (contains (x principal) (find-in (list 10 principal)))
(get result (fold contains-check find-in
{ p: x, result: false })))
(define-read-only (is-my-ship (ship uint))
(is-eq (some tx-sender) (nft-get-owner? rocket-ship ship)))
;; this function will print a message
;; (and emit an event) if the tx-sender was
;; an authorized flyer.
;;
;; here we use tx-sender, because we want
;; to allow the user to let other contracts
;; fly the ship on behalf of users
(define-public (fly-ship (ship uint))
(let ((pilots (default-to
(list)
(get pilots (map-get? allowed-pilots { rocket-ship: ship })))))
(if (contains tx-sender pilots)
(begin (print "Flew the rocket-ship!")
(ok true))
(begin (print "Tried to fly without permission!")
(ok false)))))
;;
;; Authorize a new pilot.
;;
;; here we want to ensure that this function
;; was called _directly_ by the user by
;; checking that tx-sender and contract-caller are equal.
;; if any other contract is in the call stack, contract-caller
;; would be updated to a different principal.
;;
(define-public (authorize-pilot (ship uint) (pilot principal))
(begin
;; sender must equal caller: an intermediate contract is
;; not issuing this call.
(asserts! (is-eq tx-sender contract-caller) (err u1))
;; sender must own the rocket ship
(asserts! (is-eq (some tx-sender)
(nft-get-owner? rocket-ship ship)) (err u2))
(let ((prev-pilots (default-to
(list)
(get pilots (map-get? allowed-pilots { rocket-ship: ship })))))
;; don't add a pilot already in the list
(asserts! (not (contains pilot prev-pilots)) (err u3))
;; append to the list, and check that it is less than
;; the allowed maximum
(match (as-max-len? (append prev-pilots pilot) u10)
next-pilots
(ok (map-set allowed-pilots {rocket-ship: ship} {pilots: next-pilots}))
;; too many pilots already
(err u4)))))
```
### Extending functionality: Multi-flyer contract
The authorization scheme for `fly-ship` allows pilots to fly rocket-ships from
other contracts. This allows other contracts to provide new functionality built
around calling that function.
For example, we can create a contract that calls `fly-ship`
for multiple rocket-ships in a single transaction:
```
;;
;; rockets-multi.clar
;;
(define-private (call-fly (ship uint))
(unwrap! (contract-call? .rockets-base fly-ship ship) false))
;; try to fly all the ships, returning a list of whether
;; or not we were able to fly the supplied ships
(define-public (fly-all (ships (list 10 uint)))
(ok (map call-fly ships)))
```
### Authorization for Contract-Owned Assets
The check in `authorize-pilot` protects users from malicious contracts,
but how would such a scheme support contract-owned assets? This is what the
`as-contract` function is used for. The `as-contract` function executes
the supplied closure as if the sender of the transaction was the current
contract, rather than the user -- it does this by updating `tx-sender`
to the current contract principal. We can use this to, for example, create
a smart contract rocket-ship-line:
```
;;
;; rockets-ship-line.clar
;;
(define-constant line-ceo 'SP19Y6GRV9X778VFS1V8WT2H502WFK33XZXADJWZ)
(define-data-var employed-pilots (list 10 principal) (list))
;; This function will:
;; * check that it is called by the line-ceo
;; * check that the rocket is owned by the contract
;; * authorize each employed pilot to the ship
(define-public (add-managed-rocket (ship uint))
(begin
;; only the ceo may call this function!
(asserts! (is-eq tx-sender contract-caller line-ceo) (err u1))
;; start executing as the contract
(as-contract (begin
;; make sure the contract owns the ship!
(asserts! (contract-call? .rockets-base is-my-ship ship) (err u2))
;; register all of our pilots on the ship
(add-pilots-to ship)))))
;; add all the pilots to a ship using fold --
;; the fold checks the return type of previous calls,
;; skipping subsequent contract-calls if one fails.
(define-private (add-pilot-via-fold (pilot principal) (prior-result (response uint uint)))
(let ((ship (try! prior-result)))
(try! (contract-call? .rockets-base authorize-pilot ship pilot))
(ok ship)))
(define-private (add-pilots-to (ship uint))
(fold add-pilot-via-fold (var-get employed-pilots) (ok ship)))
```
In order for the contract to add each pilot to a new ship, the contract must
call `authorize-pilot`. However, the contract wants to perform this action on
behalf of a ship the _contract_ owns, not the transaction sender. To do this,
the contract uses `as-contract`.

Loading…
Cancel
Save