Last active
January 11, 2025 00:39
-
-
Save kopcho/8adb173e97e569987f55646244906d10 to your computer and use it in GitHub Desktop.
# Convex Lisp Smart Contract FAQ: Dutch Auction Debugging and Best Practices
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
; 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