Created
December 29, 2025 11:11
-
-
Save ngmachado/89cd7e68e138d902e04205cd343312c7 to your computer and use it in GitHub Desktop.
RFC - ERC20 in Ora
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ERC20 Token Implementation in Ora | |
| // Using refinement types and error unions | |
| // Error declarations | |
| error InsufficientBalance(required: u256, available: u256); | |
| error InsufficientAllowance(required: u256, available: u256); | |
| error InvalidAddress; | |
| error InvalidAmount; | |
| error TransferToZeroAddress; | |
| contract ERC20Token { | |
| // Storage variables (explicit @storage region) | |
| storage var totalSupply: u256; // @storage: persistent contract storage | |
| storage balances: map[address, u256]; // @storage: persistent storage map | |
| storage allowances: map[address, map[address, u256]]; // @storage: nested storage map | |
| // Events (logs) | |
| log Transfer(from: address, to: address, amount: u256); | |
| log Approval(owner: address, spender: address, amount: u256); | |
| // Constructor - runs once at deployment | |
| pub fn init(initialSupply: MinValue<u256, 0>) { | |
| // initialSupply: @calldata (function parameter, read-only) | |
| let deployer: NonZeroAddress = std.msg.sender(); // @stack: std.msg.sender() returns NonZeroAddress (guaranteed non-zero by EVM, so NonZeroAddress <: address) | |
| totalSupply = initialSupply; // @storage = @calldata: allowed (calldata -> storage) | |
| balances[deployer] = initialSupply; // @storage[@stack] = @calldata: storage map write (NonZeroAddress <: address) | |
| log Transfer(std.constants.ZERO_ADDRESS, deployer, initialSupply); // constants are @stack, NonZeroAddress <: address | |
| } | |
| // Get total supply | |
| pub fn getTotalSupply() -> u256 { | |
| return totalSupply; // @storage -> @stack: storage read returns stack value | |
| } | |
| // Get balance of an account | |
| pub fn balanceOf(account: address) -> u256 { | |
| // account: @calldata (function parameter) | |
| return balances[account]; // @storage[@calldata] -> @stack: storage map read returns stack value | |
| } | |
| // Transfer tokens from sender to recipient | |
| // Uses refinement types to ensure non-negative amounts and non-zero recipient | |
| pub fn transfer(recipient: NonZeroAddress, amount: MinValue<u256, 0>) -> !bool | InsufficientBalance { | |
| // recipient: @calldata (function parameter, NonZeroAddress - guaranteed non-zero at compile time) | |
| // amount: @calldata (function parameter with refinement type) | |
| let sender: NonZeroAddress = std.msg.sender(); // @stack: std.msg.sender() returns NonZeroAddress (guaranteed non-zero by EVM) | |
| let sender_balance: u256 = balances[sender]; // @stack: storage map read (@storage[@stack]) returns stack value | |
| // No zero address check needed - NonZeroAddress guarantees recipient != 0 | |
| // Check sufficient balance (runtime check required - sender_balance is from storage) | |
| if (sender_balance < amount) { // @stack < @calldata: allowed comparison | |
| return error.InsufficientBalance(amount, sender_balance); // error values are @stack | |
| } | |
| // After the check, we know sender_balance >= amount | |
| // We can use refinement type to make subtraction type-safe | |
| // Note: sender_balance is u256, but we know it's >= amount (MinValue<u256, 0>) | |
| // The subtraction is safe because we've verified sender_balance >= amount | |
| // Perform transfer | |
| balances[sender] = sender_balance - amount; // @storage[@stack] = @stack: storage map write (safe: sender_balance >= amount verified above) | |
| let recipient_balance: u256 = balances[recipient]; // @stack: storage map read (@storage[@calldata]) returns stack value | |
| balances[recipient] = recipient_balance + amount; // @storage[@calldata] = @stack: storage map write | |
| log Transfer(sender, recipient, amount); // all values are @stack | |
| return true; // @stack: return value | |
| } | |
| // Approve spender to spend tokens | |
| pub fn approve(spender: NonZeroAddress, amount: MinValue<u256, 0>) -> !bool { | |
| // spender: @calldata (function parameter, NonZeroAddress - guaranteed non-zero at compile time) | |
| // amount: @calldata (function parameter with refinement type) | |
| let owner: NonZeroAddress = std.msg.sender(); // @stack: std.msg.sender() returns NonZeroAddress (guaranteed non-zero by EVM) | |
| // No zero address check needed - NonZeroAddress guarantees spender != 0 | |
| allowances[owner][spender] = amount; // @storage[@stack][@calldata] = @calldata: nested storage map write (NonZeroAddress <: address) | |
| log Approval(owner, spender, amount); // all values are @stack | |
| return true; // @stack: return value | |
| } | |
| // Get allowance | |
| pub fn allowance(owner: address, spender: address) -> u256 { | |
| // owner: @calldata (function parameter) | |
| // spender: @calldata (function parameter) | |
| return allowances[owner][spender]; // @storage[@calldata][@calldata] -> @stack: nested storage map read returns stack value | |
| } | |
| // Transfer from one address to another using allowance | |
| pub fn transferFrom(sender: address, recipient: NonZeroAddress, amount: MinValue<u256, 0>) -> !bool | InsufficientBalance | InsufficientAllowance { | |
| // sender: @calldata (function parameter, can be any address including zero) | |
| // recipient: @calldata (function parameter, NonZeroAddress - guaranteed non-zero at compile time) | |
| // amount: @calldata (function parameter with refinement type) | |
| let spender: NonZeroAddress = std.msg.sender(); // @stack: std.msg.sender() returns NonZeroAddress (guaranteed non-zero by EVM) | |
| // No zero address check needed for recipient - NonZeroAddress guarantees recipient != 0 | |
| // Note: sender can still be zero address (e.g., for contract-to-contract transfers) | |
| // Check allowance | |
| let current_allowance: u256 = allowances[sender][spender]; // @stack: nested storage map read (@storage[@calldata][@stack]) returns stack value | |
| if (current_allowance < amount) { // @stack < @calldata: allowed comparison | |
| return error.InsufficientAllowance(amount, current_allowance); // @stack: error value | |
| } | |
| // Check sender balance (runtime check required - sender_balance is from storage) | |
| let sender_balance: u256 = balances[sender]; // @stack: storage map read (@storage[@calldata]) returns stack value | |
| if (sender_balance < amount) { // @stack < @calldata: allowed comparison | |
| return error.InsufficientBalance(amount, sender_balance); // @stack: error value | |
| } | |
| // After the check, we know sender_balance >= amount and current_allowance >= amount | |
| // Subtractions are safe because we've verified the conditions above | |
| // Update allowance | |
| allowances[sender][spender] = current_allowance - amount; // @storage[@calldata][@stack] = @stack: nested storage map write (safe: current_allowance >= amount verified above) | |
| // Perform transfer | |
| balances[sender] = sender_balance - amount; // @storage[@calldata] = @stack: storage map write (safe: sender_balance >= amount verified above) | |
| let recipient_balance: u256 = balances[recipient]; // @stack: storage map read (@storage[@calldata]) returns stack value | |
| balances[recipient] = recipient_balance + amount; // @storage[@calldata] = @stack: storage map write | |
| log Transfer(sender, recipient, amount); // all values are @stack | |
| return true; // @stack: return value | |
| } | |
| // Helper function to safely transfer with error handling | |
| pub fn safeTransfer(recipient: NonZeroAddress, amount: MinValue<u256, 0>) { | |
| // recipient: @calldata (function parameter, NonZeroAddress - guaranteed non-zero at compile time) | |
| // amount: @calldata (function parameter with refinement type) | |
| try { | |
| let result = transfer(recipient, amount); // @stack: function call returns stack value (error union) | |
| // Transfer succeeded - result is true (@stack) | |
| } catch (e) { | |
| // Handle transfer errors | |
| // e: @stack (error value from error union) | |
| // e can be InsufficientBalance (TransferToZeroAddress removed - compile-time guarantee) | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment