diff --git a/pages/developers/intelligent-contracts/features/interacting-with-intelligent-contracts.mdx b/pages/developers/intelligent-contracts/features/interacting-with-intelligent-contracts.mdx index 8ccd0629..1c7fd6ec 100644 --- a/pages/developers/intelligent-contracts/features/interacting-with-intelligent-contracts.mdx +++ b/pages/developers/intelligent-contracts/features/interacting-with-intelligent-contracts.mdx @@ -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) +``` + +## 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}") +``` + +## 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") +``` + +### ❌ 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)) +``` +