Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,229 @@ def __init__(self, num_workers: int):
```

Child contracts are immutable after deployment. To update worker logic, redeploy through the factory.

## Complete Working Example

Here's a complete example showing a token contract interacting with another contract:

```python
from genlayer import *

@gl.contract_interface
class TokenInterface:
class View:
def balance_of(self, account: Address) -> u256: ...
def total_supply(self) -> u256: ...

class Write:
def transfer(self, to: Address, amount: u256): ...
def approve(self, spender: Address, amount: u256): ...

class TokenGatedVault(gl.Contract):
"""A vault that requires minimum token balance to access"""

token_address: Address
min_balance: u256
stored_value: u256

def __init__(self, token_addr: Address, min_bal: u256):
self.token_address = token_addr
self.min_balance = min_bal
self.stored_value = u256(0)

@gl.public.write.payable
def deposit(self):
"""Deposit GEN into the vault (requires token balance)"""
# Get token contract reference
token = TokenInterface(self.token_address)

# Check caller's token balance
balance = token.view().balance_of(gl.message.sender_address)

if balance < self.min_balance:
raise gl.UserError(f"Insufficient token balance. Need {self.min_balance}, have {balance}")

# Accept the deposit
self.stored_value += gl.message.value

@gl.public.write
def withdraw(self, amount: u256):
"""Withdraw GEN from the vault (requires token balance)"""
token = TokenInterface(self.token_address)
balance = token.view().balance_of(gl.message.sender_address)

if balance < self.min_balance:
raise gl.UserError("Insufficient token balance")

if amount > self.stored_value:
raise gl.UserError("Insufficient vault balance")

self.stored_value -= amount
gl.transfer(gl.message.sender_address, amount)
```
Comment on lines +146 to +160
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

withdraw allows any qualifying token holder to drain pooled funds.

In this “complete working example,” withdrawal is not tied to who deposited. Any address meeting min_balance can withdraw up to stored_value, which is a serious security pitfall for readers who reuse this pattern.

Suggested doc fix (track per-user balances)
 class TokenGatedVault(gl.Contract):
@@
-    stored_value: u256
+    stored_value: u256
+    balances: TreeMap[Address, u256]
@@
     def deposit(self):
@@
         self.stored_value += gl.message.value
+        sender = gl.message.sender_address
+        self.balances[sender] = self.balances.get(sender, u256(0)) + gl.message.value
@@
     def withdraw(self, amount: u256):
@@
+        sender = gl.message.sender_address
+        user_balance = self.balances.get(sender, u256(0))
+        if amount > user_balance:
+            raise gl.vm.UserError("Insufficient user balance")
         if amount > self.stored_value:
-            raise gl.UserError("Insufficient vault balance")
+            raise gl.vm.UserError("Insufficient vault balance")
         
         self.stored_value -= amount
-        gl.transfer(gl.message.sender_address, amount)
+        self.balances[sender] = user_balance - amount
+        gl.transfer(sender, amount)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@pages/developers/intelligent-contracts/features/interacting-with-intelligent-contracts.mdx`
around lines 146 - 160, The withdraw function is unsafe because it lets any
address with token balance >= min_balance withdraw from the shared stored_value;
change it to track per-user deposits and enforce sender-specific limits: add a
mapping (e.g., deposits or balances) keyed by address to record each depositor's
deposited amount when they deposit, then in withdraw (the withdraw method using
TokenInterface and token.view().balance_of and reading stored_value) check that
deposits[gl.message.sender_address] >= amount, decrement both
deposits[gl.message.sender_address] and stored_value by amount, and only then
call gl.transfer(gl.message.sender_address, amount); this ties withdrawals to
who deposited and prevents any qualifying token holder from draining pooled
funds.


## Error Handling

Always handle errors when calling other contracts:

```python
@gl.public.write
def safe_transfer(self, token_addr: Address, to: Address, amount: u256):
"""Transfer tokens with proper error handling"""
try:
token = gl.get_contract_at(token_addr)
token.emit(on='finalized').transfer(to, amount)
except gl.UserError as e:
# Handle contract-specific errors
raise gl.UserError(f"Transfer failed: {e}")
except Exception as e:
# Handle unexpected errors
raise gl.UserError(f"Unexpected error: {e}")
Comment on lines +141 to +178
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify UserError naming consistency across intelligent-contract docs
rg -n "gl\.UserError|gl\.vm\.UserError" pages/developers/intelligent-contracts/features

Repository: genlayerlabs/genlayer-docs

Length of output: 3511


🏁 Script executed:

#!/bin/bash
# Check error-handling.mdx to see how errors are documented
cat pages/developers/intelligent-contracts/features/error-handling.mdx

Repository: genlayerlabs/genlayer-docs

Length of output: 3401


🏁 Script executed:

#!/bin/bash
# Search for import statements and SDK references that clarify the API
rg -n "from genvm|import.*genvm|import.*gl" pages/developers/intelligent-contracts/features -B2 -A2 | head -100

Repository: genlayerlabs/genlayer-docs

Length of output: 52


Standardize on the documented gl.vm.UserError type across all snippets.

This page uses gl.UserError while the error-handling documentation and other reference pages use gl.vm.UserError, creating inconsistency that breaks reader confidence when cross-referencing examples.

Suggested doc fix
-raise gl.UserError("...")
+raise gl.vm.UserError("...")

-except gl.UserError as e:
+except gl.vm.UserError as e:

-with pytest.raises(gl.UserError, match="..."):
+with pytest.raises(gl.vm.UserError, match="..."):

Also applies to: lines 206, 246, 254, 271, 323

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@pages/developers/intelligent-contracts/features/interacting-with-intelligent-contracts.mdx`
around lines 141 - 178, The snippets use gl.UserError but the project standard
is gl.vm.UserError; update all occurrences to gl.vm.UserError (e.g., the raise
in the deposit/withdraw methods and the exception handlers in safe_transfer) so
the examples match the documented error type—search for symbols like withdraw,
deposit, safe_transfer and replace gl.UserError with gl.vm.UserError everywhere
(including the other referenced snippets at lines noted) to ensure consistency.

```

## Common Mistakes

### ❌ Don't: Call view() on emit()

```python
# WRONG - view() is for reading, emit() is for writing
other.view().update_status("active") # This will fail!
```

```python
# CORRECT
other.emit(on='finalized').update_status("active")
```

### ❌ Don't: Forget to check return values

```python
# WRONG - Not checking if the call succeeded
other.view().transfer(recipient, amount)
```

```python
# CORRECT - Check the return value
success = other.view().transfer(recipient, amount)
if not success:
raise gl.UserError("Transfer failed")
```
Comment on lines +197 to +207
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "interacting-with-intelligent-contracts.mdx" -type f

Repository: genlayerlabs/genlayer-docs

Length of output: 163


🏁 Script executed:

wc -l ./pages/developers/intelligent-contracts/features/interacting-with-intelligent-contracts.mdx

Repository: genlayerlabs/genlayer-docs

Length of output: 167


🏁 Script executed:

sed -n '180,220p' ./pages/developers/intelligent-contracts/features/interacting-with-intelligent-contracts.mdx

Repository: genlayerlabs/genlayer-docs

Length of output: 969


🏁 Script executed:

sed -n '1,180p' ./pages/developers/intelligent-contracts/features/interacting-with-intelligent-contracts.mdx | tail -100

Repository: genlayerlabs/genlayer-docs

Length of output: 3175


🏁 Script executed:

sed -n '1,196p' ./pages/developers/intelligent-contracts/features/interacting-with-intelligent-contracts.mdx | grep -A 20 -B 5 "view\|emit" | head -80

Repository: genlayerlabs/genlayer-docs

Length of output: 2994


The "CORRECT" example uses view() for a write operation, contradicting the page's documented guidance.

transfer() is a write method (defined in the Write interface class). The page explicitly documents that view() is for synchronous reads and emit() is for asynchronous writes. The earlier mistake example correctly enforces this pattern; the "transfer" example violates it. The Error Handling section on the same page demonstrates the correct approach: token.emit(on='finalized').transfer(to, amount).

Suggested fix
-# CORRECT - Check the return value
-success = other.view().transfer(recipient, amount)
-if not success:
-    raise gl.UserError("Transfer failed")
+# CORRECT - Use emit() for write operations
+other.emit(on='finalized').transfer(recipient, amount)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```python
# WRONG - Not checking if the call succeeded
other.view().transfer(recipient, amount)
```
```python
# CORRECT - Check the return value
success = other.view().transfer(recipient, amount)
if not success:
raise gl.UserError("Transfer failed")
```
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@pages/developers/intelligent-contracts/features/interacting-with-intelligent-contracts.mdx`
around lines 197 - 207, The "CORRECT" example incorrectly uses
other.view().transfer(...) for a write operation; replace it with the documented
asynchronous write pattern using emit(), e.g. call
other.emit(on='finalized').transfer(recipient, amount) and then check the result
(assign to success and raise gl.UserError("Transfer failed") if not success) so
the example aligns with the Write interface and the Error Handling section.


### ❌ Don't: Use accepted when order matters

```python
# WRONG - These might execute out of order or multiple times
other.emit(on='accepted').step_one()
other.emit(on='accepted').step_two()
```

```python
# CORRECT - Use finalized for sequential operations
other.emit(on='finalized').step_one()
other.emit(on='finalized').step_two()
```

## Best Practices

### 1. Use Static Typing for Better IDE Support

```python
# Preferred: Define interface upfront
@gl.contract_interface
class MyContractInterface:
class View:
def get_balance(self, addr: Address) -> u256: ...
class Write:
def update_balance(self, addr: Address, amount: u256): ...

other = MyContractInterface(contract_address)
# Now you get autocomplete and type checking!
```

### 2. Validate Contract Addresses

```python
def __init__(self, oracle_addr: Address):
# Verify the address is valid
if oracle_addr == Address("0x0000000000000000000000000000000000000000"):
raise gl.UserError("Invalid oracle address")

# Verify the contract exists (optional but recommended)
oracle = gl.get_contract_at(oracle_addr)
try:
# Try calling a view method to verify it's the right contract
oracle.view().get_version()
except:
raise gl.UserError("Oracle contract not found or incompatible")

self.oracle_address = oracle_addr
```

### 3. Handle Reentrancy

```python
class Vault(gl.Contract):
balances: TreeMap[Address, u256] = TreeMap()

@gl.public.write
def withdraw(self, amount: u256):
# Check balance FIRST
balance = self.balances[gl.message.sender_address]

if amount > balance:
raise gl.UserError("Insufficient balance")

# Update state BEFORE external call
self.balances[gl.message.sender_address] = balance - amount

# External call last (prevents reentrancy)
gl.transfer(gl.message.sender_address, amount)
```

### 4. Use Finalized for Critical Operations

```python
@gl.public.write
def execute_payment(self, recipient: Address, amount: u256):
"""Critical payment - must be finalized"""
token = gl.get_contract_at(self.token_address)

# Use 'finalized' for important operations
# This ensures the transaction won't be appealed
token.emit(on='finalized').transfer(recipient, amount)
```

## Testing Contract Interactions

Example test for contract-to-contract calls:

```python
# In your test file
def test_token_gated_vault():
# Deploy token contract first
token = deploy_contract("Token", args=[u256(1000000)])

# Deploy vault with token requirement
vault = deploy_contract("TokenGatedVault", args=[
token.address,
u256(100) # minimum balance
])

# Give user some tokens
token.transfer(user_address, u256(500))

# Test deposit with sufficient balance
vault.deposit(value=u256(1000))

assert vault.view().stored_value() == u256(1000)

# Test deposit with insufficient balance
vault2 = deploy_contract("TokenGatedVault", args=[
token.address,
u256(10000) # very high requirement
])

with pytest.raises(gl.UserError, match="Insufficient token balance"):
vault2.deposit(value=u256(1000))
```