Highlighting Some Answers From the Master of Clarity Challenge

Last week we wrapped up the 10-day Master of Clarity challenge and pushed the Stacks dev community’s skills to the limit. Over the course of 10 challenges, we covered a range of topics, from NFTs and contract calls to tuples and basic functions, and some of your solutions surprised us. Let’s dive in to some of the creative problem solving from the community.

Type
Recap
Topic(s)
Clarity
Published
September 4, 2024
Author(s)
Developer Advocate
Master of Clarity brand image
Contents

In total, we had 33 devs who competed (congrats to WHOA BUDDY who won the grand prize of a limited edition keyboard), and we saw a wide range of answers in the coding challenges. In this post, we want to highlight some devs who went above and beyond in the challenge.

WHOA BUDDY Shows Many Paths to the Same Destination

In the second quiz, we posed a bug challenge to developers that read:

The following code causes an indeterminate error. Resolve the issue inside of <code-rich-text>execute<code-rich-text> by using the <code-rich-text>is-ok<code-rich-text> function.


;; This function always returns an ok response
(define-public (say-hello)
  (ok "Hello")
)

;; This function attempts to match on a response with an indeterminate err type
(define-public (execute)
  (match (say-hello)
    success (ok success)
    error (err error)
  )
)

;; Test cases
(execute) ;; Should return (ok "Hello")

This challenge was meant to force developers down a specific path for how to solve the problem. However, some developers went above and beyond to showcase different ways the problem could be solved.

One participant, WHOA BUDDY, had a particularly insightful response to this challenge, offering not just one but three different approaches to solving the problem. Each solution demonstrates a unique aspect of Clarity programming and error handling. Here’s what he submitted:

Solution 1

His first solution takes a straightforward approach with an <code-rich-text>execute-v1<code-rich-text> function:


(define-public (execute-v1)
  (ok (unwrap-panic (say-hello)))
)

This solution uses <code-rich-text>unwrap-panic<code-rich-text> to extract the value from the <code-rich-text>ok<code-rich-text> response. While simple and concise, it's worth noting that this approach would cause a panic if <code-rich-text>say-hello<code-rich-text> ever returned an error. In this test problem, it is perfectly acceptable to use this approach since we know the return value. But if the response or optional type is unknown, this solution would cause a runtime error.

Solution 2

WHOA BUDDY’s second solution follows the quiz instructions more closely, using an <code-rich-text>execute-v2<code-rich-text> function:


(define-public (execute-v2)
  (if (is-ok (say-hello))
    (say-hello)
    (err u401)
  )
)

This implementation uses the <code-rich-text>is-ok<code-rich-text> function as requested, checking the response before deciding whether to return the response directly or return a custom error code. This approach offers more control over error handling, which can be useful when you want to implement additional logic that depends on an <code-rich-text>ok<code-rich-text> response.

Solution 3

WHOA BUDDY’s final solution presents a clean and efficient <code-rich-text>execute-v3<code-rich-text> function:


(define-public (execute-v3)
  (ok (unwrap! (say-hello) (err u401)))
)

This version uses the <code-rich-text>unwrap!<code-rich-text> function, which combines the benefits of the previous two approaches. It unwraps the <code-rich-text>ok<code-rich-text> value if present, or returns a custom error if not.

These responses not only solved the problem, but also provided valuable context on when each approach might be acceptable - showcasing the flexibility of Clarity and the importance of considering different error-handling strategies in smart contract development.

0xNestor Explores the Art of Protection

In the 6th quiz, we challenged developers to implement a smart contract for a time-locked wallet. One participant, 0xnestor, provided an optimized solution that not only met the requirements but also demonstrated a deep understanding of Clarity's security features.

Let's examine some key aspects of their implementation:


(define-constant OWNER tx-sender)
(define-constant CONTRACT (as-contract tx-sender))

(define-data-var beneficiary (optional principal) none)
(define-data-var unlockHeight uint u0)

;; ...

(define-public (claim)
  (let
    (
      (currentBeneficiary (unwrap! (var-get beneficiary) ERR_NO_VALUE))
    ) 
    (asserts! (<= (var-get unlockHeight) block-height) ERR_NOT_UNLOCKED)
    (asserts! (is-eq currentBeneficiary tx-sender) ERR_NOT_AUTHORIZED)

    (var-set beneficiary none)
    (var-set unlockHeight u0)
    (as-contract (stx-transfer? (stx-get-balance CONTRACT) CONTRACT currentBeneficiary))
  )
)

(define-public (bestow (newBeneficiary principal))
  (match (var-get beneficiary) currentBeneficiary
    (if (is-eq contract-caller currentBeneficiary)
      (ok (var-set beneficiary (some newBeneficiary))) 
      ERR_NOT_AUTHORIZED
    )
    ERR_NO_VALUE
  )
)

0xNestor optimized the contract by removing the unnecessary <code-rich-text>balance<code-rich-text> variable from the provided template and instead used <code-rich-text>stx-get-balance<code-rich-text> to track the contract's balance directly. This approach is more efficient and ensures that the balance is always up-to-date.

In the <code-rich-text>claim<code-rich-text> function, 0xNestor uses <code-rich-text>tx-sender<code-rich-text> on line 7 to verify the beneficiary's identity. This is a safe implementation because the function is protected by post-conditions on line 11. Post-conditions serve as a crucial safety mechanism in this function. They allow users to set predefined conditions that must be met for the transaction to execute, protecting against unexpected asset transfers without requiring specific knowledge of the smart contract's code.

In this case, post-conditions ensure that only the intended beneficiary can successfully claim the assets, adding an extra layer of security to the claim function.

However, the next function <code-rich-text>bestow<code-rich-text> is not protected by post-conditions (since there are no asset transfers. Instead, this functionuses <code-rich-text>contract-caller<code-rich-text> for authorization. Using <code-rich-text>contract-caller<code-rich-text> ensures that even if this function is called by another contract on behalf of the user, the <code-rich-text>is-eq<code-rich-text> check would fail since it was no longer the beneficiary who called the function directly. If <code-rich-text>tx-sender<code-rich-text> were used in this scenario, it would be possible for another contract to act as beneficiary to behave in unexpected ways.

Codetagonist Uncovers a Subtle Security Flaw

In the final quiz of our Master of Clarity challenge, we tasked developers with identifying and exploiting a vulnerability in a smart contract, then proposing a fix. Codetagonist's submission stood out for its ingenuity and thorough approach to both exploiting and securing the contract.

The Vulnerability

Codetagonist identified a subtle but critical vulnerability in the original contract's <code-rich-text>change-owner<code-rich-text> function:


(define-public (change-owner (newOwner principal))
  (begin
    (asserts! (is-eq tx-sender (var-get contractOwner)) ERR_ONLY_OWNER)
    (ok (var-set contractOwner newOwner))
  )
)

As touched on earlier, the vulnerability lies in the use of <code-rich-text>tx-sender<code-rich-text> for this particular authorization. While this seems secure at first glance, it can be exploited by a malicious contract that calls this function in the context of the current owner. Let’s take a look at what that might look like.

The Exploit

Codetagonist's exploit is particularly clever because it's disguised as an innocent NFT minting contract. Here's the key part of their malicious contract:


(define-public (mint)
  (let
    (
      (tokenId (+ (var-get lastTokenId) u1))
    )
    (try! (nft-mint? FomoToken tokenId tx-sender))
    (var-set lastTokenId tokenId)
    (try! (check-minter))
    (ok tokenId)
  )
)

(define-private (check-minter)
  (if (is-eq tx-sender minter)
    (contract-call? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.contract-0 change-owner 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5)
    (ok true)
  )
)

This exploit works by:

  • Offering a seemingly innocent mint function for an NFT.
  • Hiding the malicious code in a private <code-rich-text>check-minter<code-rich-text> function.
  • When the target (the current owner of the vulnerable contract) mints an NFT, it triggers the change-owner function of the vulnerable contract.
  • Because this call is made in the context of the current owner (<code-rich-text>tx-sender<code-rich-text>), it passes the authorization check and changes the owner to the attacker's address.

The Fix

Codetagonist's proposed fix addresses the root cause of the vulnerability:


(define-public (change-owner (newOwner principal))
  (begin
    (try! (is-owner))
    (ok (var-set contractOwner newOwner))
  )
)

(define-private (is-owner)
  (ok (asserts! (is-eq (var-get contractOwner) contract-caller) ERR_ONLY_OWNER))
)

The key change is the use of <code-rich-text>contract-caller<code-rich-text> instead of <code-rich-text>tx-sender<code-rich-text> in the <code-rich-text>is-owner<code-rich-text> check. This ensures that the actual caller of the function (not just the transaction sender) is the contract owner, preventing the type of exploit demonstrated in the malicious contract.

The Tricky Truth About SIP-010 Token Transfers

We had a wide range of challenge participants, ranging from new Clarity developers to seasoned veterans, but one question around fungible tokens stumped the majority of developers, regardless of their level of experience.

In Quiz 7, we asked developers a straight-forward true or false question:

True or False: In a SIP-010 compliant fungible token contract, the transfer function can be called by any <code-rich-text>principal<code-rich-text> to transfer tokens they do not own.

Most devs replied <code-rich-text>False<code-rich-text>, but in fact this statement is <code-rich-text>True<code-rich-text>. Let’s dive into an example that demonstrates the distinction between what’s technically possible and what’s considered a typical best practice.

The Proof Is in the Code

Let's walk through a minimal SIP-010 compliant contract that allows this behavior:


(define-public (transfer (amount uint) (sender principal) (recipient principal) (memo (optional (buff 34))))
  (begin
    (try! (as-contract (ft-transfer? ContractToken amount tx-sender recipient)))
    (match memo to-print (print to-print) 0x)
    (ok true)
  )
)

The <code-rich-text>transfer<code-rich-text> function in line 3 is the key to this behavior. Notice that it doesn't include any checks to verify that the <code-rich-text>contract-caller<code-rich-text> is the same as the <code-rich-text>sender<code-rich-text>.

The reason this works is that the <code-rich-text>transfer<code-rich-text> function uses <code-rich-text>as-contract<code-rich-text> to perform the transfer. This means the contract itself is the one executing the transfer, not the caller. Since the contract owns the tokens (assuming it has tokens), the caller of the function (you) is allowed to transfer the contracts tokens.

And since the SIP-010 standard doesn't dictate the internal implementation of the function, only its interface, the answer to this question is <code-rich-text>true<code-rich-text>.

The Truth About contract-call? Costs

Another question that stumped a few of our developers, regardless of their experience level, was:

True or False: The <code-rich-text>contract-call?<code-rich-text> function has a fixed cost regardless of the complexity of the called function.

The correct answer in this case is True. Let's unpack why this one was a bit tricky.

The Fixed Cost of contract-call?

If we look at the cost functions in the <code-rich-text>cost-3.clar<code-rich-text> smart contract, we’ll see the following for <code-rich-text>contract-call?<code-rich-text>:


(define-read-only (cost_contract_call (n uint))
    (runtime u134))

This shows that the <code-rich-text>contract-call?<code-rich-text> function itself has a fixed runtime cost of 134 units, regardless of what function it's calling or how complex that function is.

When you measure the cost of a contract call in practice, you'll see widely varying costs depending on what function you're calling. But that’s because the function being called can vary widely in its cost. As one seasoned developer put it:

"In general contract-call? as a function has fixed costs. But you don't see it when you measure it. The most significant portion of the cost is from the function you called, not contract-call? itself.”

When optimizing for cost, it's crucial to focus on the efficiency of the functions you're writing, as they will typically account for the majority of the runtime costs. The fixed cost of <code-rich-text>contract-call?<code-rich-text> is generally negligible in comparison.

Thanks for Competing

Shoutout to all of the devs who participated in the challenge and a special salute to WHOA BUDDY, 0xNestor, and Codetagonist for going above and beyond in their answers. We hope you enjoyed the challenges and competing with other devs.

If you want to continue your journey into Clarity, check out our brand new docs covering Clarity functions and chat with our team in the Hiro channels on Discord.

Product updates & dev resources straight to your inbox
Your Email is in an invalid format
Checkbox is required.
Thanks for
subscribing.
Oops! Something went wrong while submitting the form.
Copy link
Mailbox
Hiro news & product updates straight to your inbox
Only relevant communications. We promise we won’t spam.

Related stories