๐Ÿ“˜ Best Practices Guide

dist_agent_lang (DAL) โ€” This guide uses DAL syntax and stdlib. Services use @trust, @chain, @secure; functions use fn; chain and oracle calls use the actual chain:: and oracle:: APIs. See syntax.md, attributes.md, and STDLIB_REFERENCE.md.

Comprehensive guide to writing secure, efficient, and maintainable dist_agent_lang code.


๐Ÿ“‹ Table of Contents

  1. Security Best Practices
  2. Performance and Code Organization
  3. Error Handling
  4. Testing Strategies
  5. Chain and Gas Usage
  6. Oracle Integration
  7. Multi-Chain Development
  8. Common Patterns
  9. Anti-Patterns (What to Avoid)

๐Ÿ”’ Security Best Practices

1. Use service attributes for chain and trust

โœ… DO:

@trust("hybrid")
@chain("ethereum")
@secure
service SecureService {
    balances: map<string, int> = {};
    fn transfer(to: string, amount: int) -> bool {
        let caller = chain::caller();
        if (amount <= 0 || !self.balances.contains_key(caller)) { throw "Invalid transfer"; }
        if (self.balances[caller] < amount) { throw "Insufficient balance"; }
        self.balances[caller] = self.balances[caller] - amount;
        if (!self.balances.contains_key(to)) { self.balances[to] = 0; }
        self.balances[to] = self.balances[to] + amount;
        return true;
    }
}

โŒ DON'T:

service Vulnerable {
    // No @trust, @chain, @secure โ€” chain access and auth not enforced
}

@secure enables authentication (caller must be set), reentrancy guard, and audit logging. Use @public only for read-only or unauthenticated endpoints; do not combine @secure and @public on the same service or function.

2. Validate inputs before state changes

โœ… DO:

@trust("hybrid")
@chain("ethereum")
@secure
service Token {
    balances: map<string, int> = {};
    fn transfer(to: string, amount: int) -> bool {
        if (to == "" || to == "0x0000000000000000000000000000000000000000") {
            throw "Invalid address";
        }
        if (amount <= 0) { throw "Amount must be positive"; }
        let caller = chain::caller();
        if (!self.balances.contains_key(caller) || self.balances[caller] < amount) {
            throw "Insufficient balance";
        }
        self.balances[caller] = self.balances[caller] - amount;
        if (!self.balances.contains_key(to)) { self.balances[to] = 0; }
        self.balances[to] = self.balances[to] + amount;
        return true;
    }
}

โŒ DON'T:

fn transfer(to: string, amount: int) {
    self.balances[caller] = self.balances[caller] - amount;
    self.balances[to] = self.balances[to] + amount;
}

3. Update state before external chain calls (checksโ€“effectsโ€“interactions)

โœ… DO:

@txn
@secure
fn withdraw(amount: int) {
    let caller = chain::caller();
    if (!self.pending.contains_key(caller) || self.pending[caller] < amount) {
        throw "Insufficient pending";
    }
    self.pending[caller] = self.pending[caller] - amount;
    let result = chain::call(chain_id, self.vault_address, "withdraw", { "amount": amount.to_string() });
    if (!result.contains("success")) { throw "Withdraw failed"; }
}

โŒ DON'T: Call chain::call (or any external interaction) before updating service state; that can create reentrancy risk.

4. Use events and logging for important actions

โœ… DO:

event Transfer { from: string, to: string, amount: int };

fn transfer(to: string, amount: int) {
    // ... transfer logic ...
    event Transfer { from: chain::caller(), to: to, amount: amount };
    log::info("transfer", { "to": to, "amount": amount });
    return true;
}

5. Secure oracle usage

โœ… DO:

let query = oracle::create_query("BTC/USD");
let result = oracle::fetch_with_consensus(
    ["source1", "source2", "source3"],
    query,
    0.66
);
if (result == null) { throw "Oracle consensus failed"; }
let price = result.data;
let ts = chain::get_block_timestamp(1);
if (result.timestamp != null && ts - result.timestamp > 300) {
    throw "Price too old";
}

โŒ DON'T: Rely on a single oracle source without consensus or freshness checks when handling value or critical decisions.


โšก Performance and Code Organization

1. Batch work and limit chain calls

Prefer computing totals or payloads in memory, then one or few chain::call / chain::deploy invocations. Use @limit(n) on services or functions to cap resource use.

2. Cache expensive or remote data

Use service fields to cache oracle or chain data when validity windows allow (e.g. block timestamp or TTL). Use chain::get_block_timestamp(chain_id) for time; avoid calling oracle or RPC in a tight loop.

3. Project structure

my-project/
โ”œโ”€โ”€ main.dal or lib.dal    # Entry; services and top-level fns
โ”œโ”€โ”€ core/                  # Core services (e.g. token.dal, marketplace.dal)
โ”œโ”€โ”€ tests/
โ”‚   โ””โ”€โ”€ *.test.dal        # describe/it, test::expect_*, expect_throws
โ”œโ”€โ”€ scripts/
โ”‚   โ””โ”€โ”€ deploy.sh         # Build + sign tx; see PRESIGNED_DEPLOYMENT_GUIDE.md
โ”œโ”€โ”€ docs/
โ”œโ”€โ”€ .env.example
โ””โ”€โ”€ README.md

4. Service layout

Use @trust, @chain, @secure (or @public) consistently. Put fields first, then methods; use events and log:: for important state changes. Caller identity: chain::caller().

@trust("hybrid")
@chain("ethereum", "polygon")
@secure
service WellOrganized {
    total_supply: int = 0;
    balances: map<string, int> = {};
    event Transfer { from: string, to: string, amount: int };

    fn transfer(to: string, amount: int) -> bool {
        if (amount <= 0) { throw "Invalid amount"; }
        let from = chain::caller();
        if (!self.balances.contains_key(from) || self.balances[from] < amount) {
            throw "Insufficient balance";
        }
        self.balances[from] = self.balances[from] - amount;
        if (!self.balances.contains_key(to)) { self.balances[to] = 0; }
        self.balances[to] = self.balances[to] + amount;
        event Transfer { from: from, to: to, amount: amount };
        return true;
    }

    fn balance_of(account: string) -> int {
        if (self.balances.contains_key(account)) { return self.balances[account]; }
        return 0;
    }
}

๐Ÿšจ Error Handling

1. Use descriptive messages with throw

โœ… DO:

fn withdraw(amount: int) {
    if (amount <= 0) { throw "Withdrawal amount must be greater than zero"; }
    let caller = chain::caller();
    if (!self.balances.contains_key(caller) || self.balances[caller] < amount) {
        throw "Insufficient balance";
    }
    if (self.paused) { throw "Contract is paused"; }
    self.balances[caller] = self.balances[caller] - amount;
    // ... complete withdrawal ...
}

โŒ DON'T: Use empty or vague messages: throw "Error"; โ€” be specific so callers and logs are actionable.

2. Use Result for recoverable failures

โœ… DO: Return Result<T, string> and use Ok/Err when the caller should handle failure without aborting.

fn try_transfer(to: string, amount: int) -> Result<bool, string> {
    if (to == "") { return Err("Invalid address"); }
    if (amount <= 0) { return Err("Invalid amount"); }
    let from = chain::caller();
    if (!self.balances.contains_key(from) || self.balances[from] < amount) {
        return Err("Insufficient balance");
    }
    self.balances[from] = self.balances[from] - amount;
    if (!self.balances.contains_key(to)) { self.balances[to] = 0; }
    self.balances[to] = self.balances[to] + amount;
    return Ok(true);
}

3. Handle chain and external call results

โœ… DO: Check return values from chain::call and other stdlib calls; throw or return Err on failure.

let result = chain::call(chain_id, contract_address, "withdraw", { "amount": amount.to_string() });
if (result == null || !result.contains("success")) {
    throw "External call failed: " + (result ?? "null");
}

Use try/catch when you want to handle thrown errors and continue:

try {
    self.do_risky_operation();
} catch (e) {
    log::error("operation", { "error": e });
    return false;
}

๐Ÿงช Testing Strategies

Use the three-layer approach: (1) Rust unit tests for parsing/syntax, (2) semantic validators in test::, (3) DAL test files (.test.dal) for behavior. See TESTING_QUICK_REFERENCE.md.

1. DAL test files: describe / it / expect

โœ… DO:

// tests/token.test.dal
describe("Token", fn() {
    let token;
    beforeEach(fn() {
        token = MyToken::new();
        token.deposit(chain::caller(), 1000000);
    });
    it("transfers correctly", fn() {
        token.transfer("0xRecipient", 1000);
        expect(token.balance_of("0xRecipient")).to_equal(1000);
    });
    it("fails on insufficient balance", fn() {
        expect_throws(fn() { token.transfer("0xRecipient", 2000000); }, "Insufficient balance");
    });
});

2. Semantic validation (test::)

โœ… DO: Use test::expect_* for types, ranges, and structure in DAL tests or validators.

test::expect_type(&result, "number");
test::expect_in_range(price, 0.0, 1000000.0);
test::expect_has_key(config, "chain_id");
test::expect_valid_trust_model("hybrid");
test::expect_valid_chain("ethereum");

3. Integration and Rust tests

Run full flows with dal run file.test.dal. For parser/AST and example-file validation, use cargo test (e.g. cargo test --test example_tests).


โ›ฝ Chain and Gas Usage

1. Use chain stdlib for gas and cost

โœ… DO: Use chain::estimate_gas(chain_id, operation) and chain::get_gas_price(chain_id) (or chain::get_current_gas_price(chain_id)) to compare chains or warn users.

let eth_gas = chain::estimate_gas(1, "transfer");
let poly_gas = chain::estimate_gas(137, "transfer");
let eth_price = chain::get_gas_price(1);
let poly_price = chain::get_gas_price(137);
if (poly_gas * poly_price < eth_gas * eth_price) {
    log::info("gas", "Polygon is cheaper for this operation");
}

2. Batch chain operations

Prefer one chain::call with batched args (or a contract that batches on-chain) over many small calls. Use chain::get_chain_config(chain_id) and chain::get_supported_chains() when building multi-chain flows.

3. Pre-signed deployment

For real contract deployment, use pre-signed transactions (see PRESIGNED_DEPLOYMENT_GUIDE.md); avoid putting private keys in DAL.


๐Ÿ”ฎ Oracle Integration

1. Multi-source consensus

โœ… DO: Use oracle::create_query and oracle::fetch_with_consensus with multiple sources and a threshold.

let query = oracle::create_query("ETH/USD");
let result = oracle::fetch_with_consensus(
    ["source1", "source2", "source3"],
    query,
    0.66
);
if (result == null) { throw "Oracle consensus failed"; }
self.eth_price = result.data;
self.last_update = chain::get_block_timestamp(1);

2. Freshness and verification

โœ… DO: Check age with chain::get_block_timestamp(chain_id); use oracle::verify(data, signature) when signatures are available. Reject or refresh when data is too old (e.g. > 300 seconds for prices).


๐ŸŒ Multi-Chain Development

1. Declare supported chains

โœ… DO: Use @chain("ethereum", "polygon", ...) and pass chain_id explicitly in chain:: calls so the same service can run on multiple chains.

@trust("hybrid")
@chain("ethereum", "polygon", "arbitrum")
@secure
service MultiChain {
    fn transfer(chain_id: int, to: string, amount: int) -> bool {
        let caller = chain::caller();
        if (self.balances[caller] < amount) { throw "Insufficient balance"; }
        self.balances[caller] = self.balances[caller] - amount;
        if (!self.balances.contains_key(to)) { self.balances[to] = 0; }
        self.balances[to] = self.balances[to] + amount;
        return true;
    }
}

2. Chain-specific logic

โœ… DO: Use chain::get_chain_config(chain_id) and chain::get_gas_price(chain_id) (or chain::get_current_gas_price(chain_id)) when you need chain-dependent behavior (e.g. gas pricing, RPC, explorer).

fn get_gas_cost(chain_id: int) -> float {
    let gas = chain::estimate_gas(chain_id, "transfer");
    let price = chain::get_gas_price(chain_id);
    return gas * price;
}

๐ŸŽฏ Common Patterns

1. Pull over push for withdrawals

โœ… DO: Update pending state first, then perform the external transfer or chain call so reentrancy cannot drain more than the cleared amount.

pending: map<string, int> = {};
fn withdraw() {
    let caller = chain::caller();
    let amount = 0;
    if (self.pending.contains_key(caller)) { amount = self.pending[caller]; }
    if (amount <= 0) { throw "No funds to withdraw"; }
    self.pending[caller] = 0;
    let result = chain::call(chain_id, vault, "withdraw", { "amount": amount.to_string() });
    if (!result.contains("success")) { throw "Withdraw failed"; }
}

2. Pause / admin guard

โœ… DO: Use a paused field and an owner/caller check; throw at the start of sensitive methods when paused or when caller is not allowed.

paused: bool = false;
owner: string = "0x...";
fn pause() {
    if (chain::caller() != self.owner) { throw "Not owner"; }
    self.paused = true;
}
fn critical_operation() {
    if (self.paused) { throw "Contract is paused"; }
    // ...
}

โŒ Anti-Patterns (What to Avoid)

1. Don't use origin/caller for auth without validation

Use chain::caller() for the current caller and validate against a stored owner or capability; don't rely on unvalidated identity for privileged actions.

2. Don't ignore chain or oracle return values

โŒ DON'T: Call chain::call or oracle::fetch and ignore the result. โœ… DO: Check the return value and throw or return Err when the call failed or returned invalid data.

3. Don't use block or time for randomness

โŒ DON'T: Use chain::get_block_timestamp or block-derived values as the sole source of randomness for value-bearing logic (manipulable). โœ… DO: Use an oracle or verifiable randomness source (e.g. oracle::fetch with a VRF source) when you need randomness.

4. Don't put secrets in source

Never put private keys or long-lived secrets in DAL source or in constructor args. Use env, config, or a secure deploy pipeline; see PRESIGNED_DEPLOYMENT_GUIDE.md.


๐Ÿ“š Additional Resources


Next: API Reference โ†’