Xian Smart Contract Development - Cursor Rules
XIAN is the currency of the Xian blockchain. Never mention TAU or Lamden.
Contract Structure
Basic Structure
- Smart contracts are written in native Python without transpilation
- Contract names must follow the pattern:
^con_[a-z][a-z0-9_]*$ - Contract names must start with 'con_' prefix (except system contracts like 'currency')
- Contract names must be lowercase, only contain letters, numbers and underscores after prefix
- Contract names must be max 64 characters
Naming Conventions
- You cannot use '_' as a prefix for variables or functions (e.g.,
_private_varis not allowed) - Follow standard Python naming conventions otherwise
- Use descriptive names for clarity
- A contract can not be deployed by another contract
Function Types
@exportdecorator defines public functions callable by any user or contract@constructdecorator defines initialization function executed once at contract submission (optional)- Functions without decorators are private and can only be called by the contract itself
- Functions with
@exportcan call private functions internally
Constructor Arguments
- Optional arguments can be provided to the
@constructfunction - Initial state can be setup using these arguments
State Management
Variable
Variableis a way to define a singular state variable in the contract- Use
variable.set(value)to modify - Use
variable.get()to retrieve
my_var = Variable()
@construct
def seed():
my_var.set(0) # Initialize variable
@export
def increment():
my_var.set(my_var.get() + 1)
Hash
Hashis a key-value store for the contract- Default value can be specified with
Hash(default_value=0) - Access through dictionary-like syntax:
hash[key] = valueandhash[key] - Supports nested keys with tuple:
hash[key1, key2] = value
my_hash = Hash(default_value=0)
@export
def set_value(key: str, value: int):
my_hash[key] = value
@export
def get_value(key: str):
return my_hash[key]
Illegal Delimiters
":" and "." cannot be used in Variable or Hash keys.
Foreign State Access
ForeignHashprovides read-only access to a Hash from another contractForeignVariableprovides read-only access to a Variable from another contract
token_balances = ForeignHash(foreign_contract='con_my_token', foreign_name='balances')
foundation_owner = ForeignVariable(foreign_contract='foundation', foreign_name='owner')
Context Variables
ctx.caller
- The identity of the person or contract calling the function
- Changes when a contract calls another contract's function
- Used for permission checks in token contracts
ctx.signer
- The top-level user who signed the transaction
- Remains constant throughout transaction execution
- Only used for security guards/blacklisting, not for account authorization
ctx.this
- The identity/name of the current contract
- Never changes
- Useful when the contract needs to refer to itself
ctx.owner
- Owner of the contract, optional field set at time of submission
- Only the owner can call exported functions if set
- Can be changed with
ctx.owner = new_owner
ctx.entry
- Returns tuple of (contract_name, function_name) of the original entry point
- Helps identify what contract and function initiated the call chain
Built-in Variables
Time and Blockchain Information
now- Returns the current datetimeblock_num- Returns the current block number, useful for block-dependent logicblock_hash- Returns the current block hash, can be used as a source of randomness
Example usage:
@construct
def seed():
submission_time = Variable()
submission_block_num = Variable()
submission_block_hash = Variable()
# Store blockchain state at contract creation
submission_time.set(now)
submission_block_num.set(block_num)
submission_block_hash.set(block_hash)
Imports and Contract Interaction
Importing Contracts
- Use
importlib.import_module(contract_name)for dynamic contract imports - Static contract imports can be done with
import <contract_name> - Only use 'import' syntax for contracts, not for libraries or Python modules
- Trying to import standard libraries will not work within a contract (they're automatically available)
- Dynamic imports are preferred when the contract name is determined at runtime
- Can enforce interface with
importlib.enforce_interface() - NEVER import anything other than a contract.
- ALL contracting libraries are available globally
- NEVER IMPORT importlib. It is already available globally.
@export
def interact_with_token(token_contract: str, recipient: str, amount: float):
token = importlib.import_module(token_contract)
# Define expected interface
interface = [
importlib.Func('transfer', args=('amount', 'to')),
importlib.Var('balances', Hash)
]
# Enforce interface
assert importlib.enforce_interface(token, interface)
# Call function on other contract
token.transfer(amount=amount, to=recipient)
Error Handling
Assertions
- Use
assertstatements for validation and error checking - Include error messages:
assert condition, "Error message"
No Try/Except
- Exception handling with try/except is not allowed
- Use conditional logic with if/else statements instead
# DO NOT USE:
try:
result = 100 / value
except:
result = 0
# CORRECT APPROACH:
assert value != 0, "Cannot divide by zero"
result = 100 / value
# OR
if value == 0:
result = 0
else:
result = 100 / value
Prohibited Built-ins
getattris an illegal built-in function and must not be used- Other Python built-ins may also be restricted for security reasons
Modules
Random
- Seed RNG with
random.seed() - Generate random integers with
random.randint(min, max)
Datetime
- Available by default without importing
- Compare timestamps with standard comparison operators
- Use the built-in
nowvariable for current time
Crypto
- Provides cryptographic functionality using the PyNaCl library under the hood
- Employs the Ed25519 signature scheme for digital signatures
- Main function is
verifyfor signature validation
# Verify a signature
is_valid = crypto.verify(vk, msg, signature)
# Returns True if the signature is valid for the given message and verification key
Example usage in a contract:
@export
def verify_signature(vk: str, msg: str, signature: str):
# Use the verify function to check if the signature is valid
is_valid = crypto.verify(vk, msg, signature)
# Return the result of the verification
return is_valid
Hashlib
- Xian provides a simplified version of hashlib with a different API than Python's standard library
- Does not require setting up an object and updating it with bytes
- Functions directly accept and return hexadecimal strings
# Hash a hex string with SHA3 (256 bit)
hash_result = hashlib.sha3("68656c6c6f20776f726c64") # hex for "hello world"
# If not a valid hex string, it will encode the string to bytes first
text_hash = hashlib.sha3("hello world")
# SHA256 works the same way (SHA2 256-bit, used in Bitcoin)
sha256_result = hashlib.sha256("68656c6c6f20776f726c64")
Testing
Setting Up Tests
- Use Python's unittest framework
- Client available via
from contracting.client import ContractingClient - Flush client before and after each test
Setting Test Environment
- Pass environment variables like
now(datetime) in a dictionary
from contracting.stdlib.bridge.time import Datetime
env = {"now": Datetime(year=2021, month=1, day=1, hour=0)}
result = self.some_contract.some_fn(some_arg=some_value, environment=env)
Specifying Signer
- Specify the signer when calling contract functions in tests
result = self.some_contract.some_fn(some_arg=some_value, signer="some_signer")
Events
Defining Events
- Use
LogEventto define events at the top level of a contract - Each event has a name and a schema of parameters with their types
- Set
idx: Truefor parameters that should be indexed for querying
TransferEvent = LogEvent(
event="Transfer",
params={
"from": {'type': str, 'idx': True},
"to": {'type': str, 'idx': True},
"amount": {'type': (int, float, decimal)}
}
)
ApprovalEvent = LogEvent(
event="Approval",
params={
"owner": {'type': str, 'idx': True},
"spender": {'type': str, 'idx': True},
"amount": {'type': (int, float, decimal)}
}
)
Emitting Events
- Call the event variable as a function and pass a dictionary of parameter values
- All parameters defined in the event schema must be provided
- Event parameters must match the specified types
@export
def transfer(amount: float, to: str):
sender = ctx.caller
# ... perform transfer logic ...
# Emit the transfer event
TransferEvent({
"from": sender,
"to": to,
"amount": amount
})
Testing Events
- Use
return_full_output=Truewhen calling contract functions in tests to capture events - Access events in the result dictionary's 'events' key
- Assert on event types and parameters in tests
# In your test function
result = self.contract.transfer(
amount=100,
to="recipient",
signer="sender",
return_full_output=True
)
# Verify events
events = result['events']
assert len(events) == 1
assert events[0]['event'] == 'Transfer'
assert events[0]['from'] == 'sender'
assert events[0]['to'] == 'recipient'
assert events[0]['amount'] == 100
Common Event Types
- Transfer: When value moves between accounts
- Approval: When spending permissions are granted
- Mint/Burn: When tokens are created or destroyed
- StateChange: When significant contract state changes
- ActionPerformed: When important contract actions execute
Smart Contract Testing Best Practices
Test Structure
- Use Python's unittest framework for structured testing
- Create a proper test class that inherits from
unittest.TestCase - Implement
setUpandtearDownmethods to isolate tests - Define the environment and chain ID in setUp for consistent testing
class TestMyContract(unittest.TestCase):
def setUp(self):
# Bootstrap the environment
self.chain_id = "test-chain"
self.environment = {"chain_id": self.chain_id}
self.deployer_vk = "test-deployer"
# Initialize the client
self.client = ContractingClient(environment=self.environment)
self.client.flush()
# Load and submit the contract
with open('path/to/my_contract.py') as f:
code = f.read()
self.client.submit(code, name="my_contract", constructor_args={"owner": self.deployer_vk})
# Get contract instance
self.contract = self.client.get_contract("my_contract")
def tearDown(self):
# Clean up after each test
self.client.flush()
Test Organization
- Group tests by functionality using descriptive method names
- Follow the Given-When-Then pattern for clear test cases
- Test both positive paths and error cases
- Define all variables within the test, not in setUp
- Define all variables and parameters used by a test WITHIN THE TEST, not within setUp
- This ensures test isolation and prevents unexpected side effects between tests
def test_transfer_success(self):
# GIVEN a sender with balance
sender = "alice"
self.contract.balances[sender] = 1000
# WHEN a transfer is executed
result = self.contract.transfer(amount=100, to="bob", signer=sender)
# THEN the balances should be updated correctly
self.assertEqual(self.contract.balances["bob"], 100)
self.assertEqual(self.contract.balances[sender], 900)
Testing for Security Vulnerabilities
1. Authorization and Access Control
- Test that only authorized users can perform restricted actions
- Verify that contract functions check
ctx.callerorctx.signerappropriately
def test_change_metadata_unauthorized(self):
# GIVEN a non-operator trying to change metadata
with self.assertRaises(Exception):
self.contract.change_metadata(key="name", value="NEW", signer="attacker")
2. Replay Attack Protection
- Test that transaction signatures cannot be reused
- Verify nonce mechanisms or one-time-use permits
def test_permit_double_spending(self):
# GIVEN a permit already used once
self.contract.permit(owner="alice", spender="bob", value=100, deadline=deadline,
signature=signature)
# WHEN the permit is used again
# THEN it should fail
with self.assertRaises(Exception):
self.contract.permit(owner="alice", spender="bob", value=100,
deadline=deadline, signature=signature)
3. Time-Based Vulnerabilities
- Test behavior around time boundaries (begin/end dates)
- Test with different timestamps using the environment parameter
def test_time_sensitive_function(self):
# Test with time before deadline
env = {"now": Datetime(year=2023, month=1, day=1)}
result = self.contract.some_function(signer="alice", environment=env)
self.assertTrue(result)
# Test with time after deadline
env = {"now": Datetime(year=2024, month=1, day=1)}
with self.assertRaises(Exception):
self.contract.some_function(signer="alice", environment=env)
4. Balance and State Checks
- Verify state changes after operations
- Test for correct balance updates after transfers
- Ensure state consistency through complex operations
def test_transfer_balances(self):
# Set initial balances
self.contract.balances["alice"] = 1000
self.contract.balances["bob"] = 500
# Perform transfer
self.contract.transfer(amount=300, to="bob", signer="alice")
# Verify final balances
self.assertEqual(self.contract.balances["alice"], 700)
self.assertEqual(self.contract.balances["bob"], 800)
5. Signature Validation
- Test with valid and invalid signatures
- Test with modified parameters to ensure signatures aren't transferable
def test_signature_validation(self):
# GIVEN a properly signed message
signature = wallet.sign_msg(msg)
# WHEN using the correct parameters
result = self.contract.verify_signature(msg=msg, signature=signature,
public_key=wallet.public_key)
# THEN verification should succeed
self.assertTrue(result)
# BUT when using modified parameters
with self.assertRaises(Exception):
self.contract.verify_signature(msg=msg+"tampered", signature=signature,
public_key=wallet.public_key)
6. Edge Cases and Boundary Conditions
- Test with zero values, max values, empty strings
- Test operations at time boundaries (exactly at deadline)
- Test with invalid inputs and malformed data
def test_edge_cases(self):
# Test with zero amount
with self.assertRaises(Exception):
self.contract.transfer(amount=0, to="receiver", signer="sender")
# Test with negative amount
with self.assertRaises(Exception):
self.contract.transfer(amount=-100, to="receiver", signer="sender")
7. Capturing and Verifying Events
- Use
return_full_output=Trueto capture events - Verify event emissions and their parameters
def test_event_emission(self):
# GIVEN a setup for transfer
sender = "alice"
receiver = "bob"
amount = 100
self.contract.balances[sender] = amount
# WHEN executing with return_full_output
result = self.contract.transfer(
amount=amount,
to=receiver,
signer=sender,
return_full_output=True
)
# THEN verify the event was emitted with correct parameters
self.assertIn('events', result)
events = result['events']
self.assertEqual(len(events), 1)
event = events[0]
self.assertEqual(event['event'], 'Transfer')
self.assertEqual(event['data_indexed']['from'], sender)
self.assertEqual(event['data_indexed']['to'], receiver)
self.assertEqual(event['data']['amount'], amount)
Common Exploits to Test For
Reentrancy
- Test that state is updated before external calls
- Verify operations complete atomically
def test_no_reentrancy_vulnerability(self):
# Set up the attack scenario (if possible with Xian)
# Verify state is properly updated before any external calls
# For example, check that balances are decreased before tokens are sent
# Verify proper operation ordering in the contract
Integer Overflow/Underflow
- Test with extremely large numbers
- Test arithmetic operations at boundaries
def test_integer_boundaries(self):
# Set a large balance
self.contract.balances["user"] = 10**20
# Test with large transfers
result = self.contract.transfer(amount=10**19, to="receiver", signer="user")
# Verify results are as expected
self.assertEqual(self.contract.balances["user"], 9*10**19)
self.assertEqual(self.contract.balances["receiver"], 10**19)
Front-Running Protection
- Test mechanisms that prevent frontrunning (e.g., commit-reveal)
- Test deadline-based protections
def test_front_running_protection(self):
# Test with deadlines to ensure transactions expire
deadline = Datetime(year=2023, month=1, day=1)
current_time = Datetime(year=2023, month=1, day=2) # After deadline
with self.assertRaises(Exception):
self.contract.time_sensitive_operation(
param1="value",
deadline=str(deadline),
environment={"now": current_time}
)
Authorization Bypass
- Test authorization for all privileged operations
- Try to access functions with different signers
def test_authorization_checks(self):
# Test admin functions with non-admin signers
with self.assertRaises(Exception):
self.contract.admin_function(param="value", signer="regular_user")
# Test with proper authorization
result = self.contract.admin_function(param="value", signer="admin")
self.assertTrue(result)
Best Practices Summary
- Test both positive and negative paths
- Test permissions and authorization thoroughly
- Use environment variables to test time-dependent behavior
- Verify event emissions using
return_full_output=True - Test against potential replay attacks and signature validation
- Check edge cases and boundary conditions
- Verify state consistency after operations
- Test for common security vulnerabilities