Skip to content
Pre-Release: Fe is under active development. This documentation covers the upcoming release. Follow progress on GitHub

Why Effects Matter

Effects provide significant benefits for smart contract development. They make code safer, more testable, and easier to reason about.

Every function declares exactly what it can access:

fn transfer(from: u256, to: u256, amount: u256) uses (mut balances: Balances) {
// Can ONLY modify Balances
// Cannot access Allowances, Config, or anything else
}

This makes security audits easier. You know a function’s blast radius just by reading its signature.

Functions only get the capabilities they need:

// This function can only read
fn get_balance(account: u256) -> u256 uses (balances: Balances) {
balances.data.get(account)
}
// This function can read AND write
fn set_balance(account: u256, amount: u256) uses (mut balances: Balances) {
balances.data.set(account, amount)
}

A bug in get_balance cannot corrupt state because it lacks mutation capability.

Unlike languages where any function might access global state, Fe functions can only access what they declare:

// In Fe, this function signature guarantees
// it cannot access any external state
fn calculate_fee(amount: u256, rate: u256) -> u256 {
amount * rate / 10000
}

If a function has no uses clause, it’s a pure computation.

Effects make mocking straightforward:

pub struct Config { pub fee_rate: u256 }
pub struct Balances { pub credited: u256 }
pub struct Logger { pub count: u256 }
impl Balances {
pub fn new() -> Self { Balances { credited: 0 } }
pub fn credit(mut self, amount: u256) { self.credited = amount }
}
impl Logger {
pub fn new() -> Self { Logger { count: 0 } }
pub fn log_payment(mut self, amount: u256, fee: u256) {
let _ = (amount, fee)
self.count += 1
}
}
fn process_payment(amount: u256) uses (config: Config, mut balances: Balances, mut logger: Logger) {
let fee = amount * config.fee_rate / 10000
balances.credit(amount - fee)
logger.log_payment(amount, fee)
}
// In tests, provide mock effects:
fn test_payment() {
let config = Config { fee_rate: 100 } // 1% fee
let mut balances = Balances::new()
let mut logger = Logger::new()
with (Config = config, Balances = balances, Logger = logger) {
process_payment(1000)
}
// Assert on mock state
assert(balances.credited == 990, "credited check")
assert(logger.count == 1, "log count check")
}

Test functions in isolation by providing only the effects they need:

pub struct Balances { pub balance: u256 }
impl Balances {
pub fn get(self, account: u256) -> u256 {
let _ = account
self.balance
}
}
fn validate_transfer(from: u256, amount: u256) -> bool uses (balances: Balances) {
balances.get(from) >= amount
}
fn test_validate_transfer() {
let balances = Balances { balance: 100 }
with (Balances = balances) {
assert(validate_transfer(1, 50) == true, "should allow 50")
assert(validate_transfer(1, 150) == false, "should reject 150")
}
}

Pure functions (no effects) need no setup at all:

fn calculate_shares(amount: u256, total: u256, supply: u256) -> u256 {
if total == 0 {
amount
} else {
amount * supply / total
}
}

Tests can call pure functions directly without any setup:

fn test_calculate_shares() {
// No setup needed, just call the function
assert(calculate_shares(100, 1000, 500) == 50, "shares calc 1")
assert(calculate_shares(100, 0, 0) == 100, "shares calc 2")
}
// Just by reading this signature, you know:
// - It needs caller context
// - It reads token store
// - It modifies balances
// - It writes to event log
fn transfer(to: u256, amount: u256)
uses (ctx: Ctx, tokens: TokenStore, mut balances: Balances, mut log: EventLog)
{
}

Effects create explicit dependency graphs:

fn inner1() uses (a: A) {
}
fn inner2() uses (b: B) {
}
fn outer() uses (a: A, b: B) {
inner1() // Uses effect A
inner2() // Uses effect B
}

You can trace exactly how effects flow through your code.

When refactoring, the compiler catches missing effects:

pub struct Config { pub multiplier: u256 }
// Before: function was pure
fn calculate_v1(x: u256) -> u256 {
x * 2
}
// After: now needs Config
fn calculate_v2(x: u256) -> u256 uses (config: Config) {
x * config.multiplier
}
// Compiler catches any call site that doesn't provide Config

All effect checking happens at compile time:

  • No runtime overhead for effect tracking
  • Missing effects are caught before deployment
  • Effect mismatches are compilation errors
fn risky_operation() uses (mut state: CriticalState) {
}
// This would cause a compiler error:
// fn caller() {
// risky_operation() // Error: CriticalState not available
// }
// Must declare the effect to call risky_operation
fn valid_caller() uses (mut state: CriticalState) {
risky_operation()
}

This error appears at compile time, not when the contract is deployed or called.

AspectWithout EffectsWith Effects
DependenciesHidden in implementationVisible in signature
Security auditMust read all codeCheck signatures first
TestingComplex mockingSimple effect injection
RefactoringRuntime errorsCompile-time errors
Principle of least privilegeManual disciplineCompiler enforced

Effects transform implicit dependencies into explicit, compiler-checked declarations:

  1. Security: Limit what each function can access
  2. Testability: Mock effects easily for isolated tests
  3. Reasoning: Understand code from signatures alone
  4. Safety: Catch errors at compile time, not runtime

For smart contracts where security is critical and bugs are costly, effects provide essential guarantees that make your code safer and more maintainable.