Skip to content

Instantly share code, notes, and snippets.

@kopcho
Last active January 11, 2025 00:39
Show Gist options
  • Save kopcho/8adb173e97e569987f55646244906d10 to your computer and use it in GitHub Desktop.
Save kopcho/8adb173e97e569987f55646244906d10 to your computer and use it in GitHub Desktop.
# Convex Lisp Smart Contract FAQ: Dutch Auction Debugging and Best Practices
; vim: syntax=clojure
# Convex Lisp Smart Contract FAQ: Dutch Auction Debugging and Best Practices
This FAQ documents the debugging process and best practices learned while developing a Dutch Auction smart contract in Convex Lisp. It covers both Convex Lisp-specific issues and general smart contract design considerations.
## I. Asset Handling and Ownership
1. **FAQ: Do I need to perform a separate ownership check before transferring an NFT in a smart contract function?**
**Answer:** Not if you are using `asset/accept`. The `asset/accept` function implicitly checks ownership before transferring an asset. If the caller does not own the asset, `asset/accept` will fail. Using this is the recommended approach. A separate check is only required if you need to perform actions based on the owner *before* the transfer.
**Incorrect (redundant):**
```convex
(defn ^:callable buy [auction-id]
(let [auction (get auctions auction-id)]
(when (not (validate-ownership (:nft-id auction))) ; Redundant
(fail :NOT-NFT-OWNER))
(asset/accept *address* *caller* (:nft-id auction))))
```
**Correct:**
```convex
(defn ^:callable buy [auction-id]
(let [auction (get auctions auction-id)
accept-result (asset/accept *address* *caller* (:nft-id auction))]
(when (:error accept-result)
(fail :NFT-ACCEPT-FAILED accept-result))))
```
2. **FAQ: How do I transfer an asset to the contract in the `create` function?**
**Answer:** The `asset/transfer` function is used. The caller (`*caller*`) must own the asset *before* calling the `create` function. The transfer should happen *before* any auction data is stored in the contract's state.
**Incorrect (transferring to self):**
```convex
(defn ^:callable create [nft-id ...]
(let [auction {...}]
(set! auctions (assoc auctions auction-id auction))
(asset/transfer *caller* *caller* nft-id))) ; Incorrect
```
**Correct:**
```convex
(defn ^:callable create [nft-id ...]
(let [transfer-result (asset/transfer *caller* *address* nft-id)]
(when (:error transfer-result)
(fail :NFT-TRANSFER-FAILED transfer-result))
(let [auction {...}]
(set! auctions (assoc auctions auction-id auction)))))
```
## II. Error Handling
3. **FAQ: What's the best way to handle errors in Convex Lisp smart contracts?**
**Answer:** Use the `fail` form. `fail` aborts the current transaction and rolls back any state changes. This prevents partial execution and ensures data consistency.
**Incorrect (returning error maps):**
```convex
(defn ^:callable my-function []
(if (some-condition)
{:error :some-error} ; Incorrect
true))
```
**Correct:**
```convex
(defn ^:callable my-function []
(when (some-condition)
(fail :SOME-ERROR)) ; Correct
true)
```
4. **FAQ: Should I use `assert` for error checking in Convex smart contracts?**
**Answer:** While `assert` can be useful for internal debugging during development, `fail` is the recommended approach for production smart contracts. `assert` will halt execution but may not always trigger a full transaction rollback in all contexts.
## III. Function Definitions and Scope
5. **FAQ: What's the difference between `defn` and `defn-` in Convex smart contracts?**
**Answer:** In standard Clojure, `defn-` denotes a private function. However, in Convex smart contracts, this distinction is not relevant in the same way. All functions within a contract's namespace are effectively callable (if marked `^:callable`). Therefore, you should use `defn` for all function definitions in Convex smart contracts.
**Incorrect:**
```convex
(defn- my-helper-function [x] ...) ; Incorrect
```
**Correct:**
```convex
(defn my-helper-function [x] ...) ; Correct
```
## IV. Return Values
6. **FAQ: What is the recommended way to return values from Convex Lisp smart contract functions?**
**Answer:** Successful functions should return the result of the operation directly or `true`. Avoid returning maps like `{:success true}`. For error conditions, use `fail`.
**Incorrect:**
```convex
(defn ^:callable my-function []
(if (some-condition)
{:success true} ; Incorrect
{:error :some-error}))
```
**Correct:**
```convex
(defn ^:callable my-function []
(when (some-condition)
(fail :SOME-ERROR))
true) ; Correct
```
## V. Imports
7. **FAQ: How do I import NFT-related functions in a Convex smart contract?**
**Answer:** The correct import path for NFT functions within the `convex.asset` library is `asset.nft.tokens`.
**Incorrect:**
```convex
(import convex.nft :as nft) ; Incorrect
```
**Correct:**
```convex
(import asset.nft.tokens :as nft) ; Correct
```
## VI. State Updates
8. **FAQ: How should I handle state updates in functions that also perform asset transfers or payments?**
**Answer:** All state updates and transfer/payment operations must be performed within a single `set!` operation to ensure atomicity. This prevents race conditions and ensures that the contract's state remains consistent.
**Incorrect (non-atomic update):**
```convex
(defn ^:callable buy [auction-id]
...
(set! auctions ...) ; First state update
(asset/transfer ...) ; Transfer happens later
(accept ...)
...)
```
**Correct (atomic update):**
```convex
(defn ^:callable buy [auction-id]
...
(let [fee (* price AUCTION-FEE-PERCENTAGE)
seller-payout (- price fee)
accept-result (asset/accept *address* *caller* (:nft-id auction))]
(when (:error accept-result)
(fail :NFT-ACCEPT-FAILED accept-result))
(set! auctions
(assoc auctions auction-id
(assoc auction :active false :final-price price :winner *caller*))) ; Atomic update
(accept price)
(when (> offer price)
(transfer *caller* (- offer price)))
(transfer (:seller auction) seller-payout)
(transfer *address* fee)
(log :AUCTION-ENDED auction-id *caller* price (:nft-id auction))
true))
```
## VII. General Smart Contract Logic and Design (Dutch Auction Specific)
9. **FAQ: Why is it crucial to transfer the NFT to the contract *before* creating the auction data?**
**Answer:** The contract must own the NFT before it can manage the auction. If the auction data is created first, there's a window of time where the auction exists, but the contract doesn't own the NFT, leading to potential inconsistencies or vulnerabilities.
10. **FAQ: Why is atomic state update important in the `buy` function?**
**Answer:** The `buy` function must update the auction state (marking it as ended, setting the winner) *and* perform the asset transfer and payment acceptance within a single atomic operation. This prevents race conditions where two users might try to buy the same NFT simultaneously.
11. **FAQ: Why is it important to handle errors from `asset/transfer` and `asset/accept`?**
**Answer:** These functions can fail for various reasons (e.g., insufficient permissions, incorrect asset ID). It's crucial to check their return values and handle any errors appropriately using `fail`. Failing to do so can lead to unexpected behavior and potentially exploitable vulnerabilities.
12. **FAQ: Why was `validate-ownership` removed from `buy` and `cancel` functions?**
**Answer:** The `asset/accept` function already performs an implicit ownership check. If the contract does not own the NFT, `asset/accept` will fail. Therefore, a separate `validate-ownership` check is redundant and adds unnecessary complexity.
13. **FAQ: Why is it important to have separate `create` and `buy` functions?**
**Answer:** These are distinct operations with different actors and purposes. `create` is
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment