Quickstart
Learn how to test your Clarity smart contracts thoroughly using Rendezvous property-based testing.
This tutorial walks you through testing a DeFi lending contract with Rendezvous. You’ll see how Rendezvous uncovers subtle vulnerabilities that traditional unit tests might miss, and learn how to design property-based tests that help expose real bugs.
What You'll Learn
You will test a simplified DeFi lending contract that allows users to deposit STX and borrow against those deposits. This contract hides a subtle bug that passes all example-based tests, but fails when running Rendezvous property-based testing.
You’ll learn to:
Write property-based tests directly in Clarity
Catch real vulnerabilities using randomized, stateful test runs
Replay and fix failing test sequences deterministically
Note: This example is adapted from the stx-defi contract in the hirosystems/clarity-examples repository.
Prerequisites
Before you begin, make sure you have:
Node.js (version >= 20)
Clarinet installed (installation guide)
Step 1: Create a New Clarinet Project
Open your terminal and create a new Clarinet project:
This creates a new directory with the basic Clarinet structure:
Step 2: Add Rendezvous
Add Rendezvous to your project:
Verify the installation:
Step 3: Add the Lending Contract
Add a new contract to the Clarinet project:
Open contracts/stx-defi.clar and add this Clarity code:
What this contract does:
Users can
depositSTX into the protocolUsers can
borrowup to 50% of their deposit valueThe contract tracks deposits and loans for each user
Step 4: Write Some Unit Tests
Let's first write some example-based unit tests.
Open tests/stx-defi.test.ts and add these example-based unit tests:
Install dependencies and run the tests:
Looking good! ✅ (or so it seems...)
The main functions and state of the contract are now covered by tests. Line coverage is probably high as well. Looks great, right? But here's the thing: example-based tests only verify the examples you thought of. Let's see if the contract holds up under Rendezvous property-based testing.
Step 5: Add Rendezvous Property-Based Tests
Rendezvous lets you test a broader range of inputs, not just specific examples. Let's see how to write your first property-based test and why it matters.
Create the Test File
Create the Rendezvous test file:
Add an Ice-Breaker Test
Before writing any meaningful properties, it's a good idea to check that Rendezvous can run. Add a simple "always-true" test to verify your setup. Open contracts/stx-defi.tests.clar and add an always-true test:
Check if Rendezvous can execute the test:
Expected output:
If you see similar output, your setup works. You're ready to write a real property-based test.
Define a Borrowing Property
You want to test that borrowing always updates the loan amount correctly:
At this stage, the test will likely fail. This is an important learning moment: Rendezvous runs your tests in a stateful, randomized environment that simulates real contract interactions.
How Rendezvous Executes Property Tests
Rendezvous:
Injects all property-based tests directly into the deployed contract.
Detects all public
test-*functions automatically.Generates a random sequence to call each test.
Produces random argument values for each function parameter.
Randomly selects senders from settings/Devnet.toml.
Randomly advances Bitcoin and Stacks block heights during testing.
Accumulates state across test calls instead of resetting each time.
Discards test cases where preconditions fail, returning (ok false).
This design allows you to test your contract in realistic, varied scenarios that a simple/example-based unit test could never reach.
Why the First Test Fails
The test likely failed because the borrow call failed—the contract wasn't in a suitable state. Rendezvous allows you to discard test cases when preconditions aren't met (wrong state, invalid arguments, caller, height, etc.). In our case, borrow will fail for one of these reasons:
no deposits were made
the generated amount argument is non-positive (u0)
the generated amount argument is more than the allowed borrow value
To fix this, you need to simulate deposits and add discard logic.
Let's address them one by one.
Handle Preconditions
First, you need deposits. You can create a helper function that Rendezvous will pick up during property-based testing runs. This helper will allow deposits to be created so other tests can check properties that require deposits:
Next, add discard logic to the borrow test. A test is discarded when it returns (ok false). Wrap the core test logic in a conditional that checks for invalid preconditions (the three cases listed above) and returns (ok false) to discard those cases:
The test discards invalid cases: when amount is u0, or when the new total loan would exceed half the deposit (which also covers cases with no deposits).
Now the test only runs when valid preconditions are met.
Run Rendezvous and Catch the Bug
Start a new property-based testing run:
Rendezvous will probably catch the bug in the very first run, showing output like this:
The output shows a failure: (err "Loan amount not updated correctly"). The contract isn't tracking loan amounts correctly.
Note: The output includes a seed (
1880056597) you can use to reproduce this exact sequence.
You can also stop at the first failure:
Step 6: Identify and Fix the Borrow Bug
After taking a closer look at the lending contract, the bug is in this line of the borrow function:
Change the line to correctly accumulate loans:
Re-run Rendezvous with the Same Seed
Re-run with the same seed, to find out if you completely fixed the bug for that random sequence of events:
Output:
The bug is fixed! The contract now correctly tracks cumulative loans.
Run Multiple Random Sequences
Test additional random sequences (each run generates a new random sequence):
Run more tests to increase confidence (default is 100 runs):
Rendezvous caught the bug and you successfully fixed it! 🎯
Step 7: Understand the Bug
What was the bug?
Rendezvous discovered that when a user borrows multiple times, only the most recent borrow amount is recorded.
The bug means the contract doesn't track cumulative borrows correctly. When a user borrows multiple times, only the most recent borrow amount is recorded, not the total. The existing loan amount (current-loan) is completely ignored!
Why did example-based unit tests miss this?
The unit tests passed because they only tested single borrow scenarios. Look back at the unit test:
When there's only one borrow, (+ amount) and (+ current-loan amount) produce the same result because the initial loan is u0.
Rendezvous caught the bug by:
Randomly generating test sequences
Calling
borrowmultiple times with different amountsVerifying the property holds for ALL sequences
This is the power of using Rendezvous!
What You Learned
You've successfully:
✅ Created a simple DeFi lending contract
✅ Wrote traditional unit tests that passed but missed a critical bug
✅ Wrote your first Rendezvous property-based test
✅ Discovered how Rendezvous catches bugs through random stateful testing
✅ Fixed the bug and verified the fix
✅ Understood the difference between stateless example-based and stateful property-based testing
The Key Insight
Example-based tests check specific examples. Property-based tests check a much broader range of inputs.
Example-based tests ask:
"Does this work for input A?"
"Does this work for input B?"
Property-based tests ask:
"Does this ALWAYS work?"
"Can I find ANY input that breaks this?"
Rendezvous explores your contract's state space automatically, finding edge cases you might never think to test manually.
Real-World Impact
This bug in a production DeFi protocol would allow users to:
Deposit 1000 STX
Borrow 500 STX (maximum allowed)
Borrow another 500 STX (should fail, but succeeds due to bug)
Total borrowed: 1000 STX with only 500 STX recorded
User only needs to repay 500 STX despite borrowing 1000 STX
This would drain the protocol's funds — a critical vulnerability caught by Rendezvous in seconds.
Example Implementation
You can see a complete step-by-step implementation of this tutorial with commit-by-commit progress in the rendezvous-tutorial repository (view commits).
Next Steps
Now that you understand the power of Rendezvous, explore:
More examples: Study other smart contracts in the Examples Chapter of the Rendezvous Docs
Your own contracts: Apply Rendezvous to your projects and find bugs before they reach production
Get Involved
Found this tutorial useful? Star the Rendezvous repository on GitHub to show your support!
Have questions, found a bug, or want to contribute? We'd love to hear from you:
Open an issue on GitHub
Reach out with questions or feedback
Share your findings — contribute examples of bugs you've caught to show others how powerful advanced testing techniques can be
Last updated
Was this helpful?
