From 4e0354dc272fb89466d6bbb2ee200a7398d1f7d1 Mon Sep 17 00:00:00 2001 From: Yusrah Mohammed Date: Thu, 21 May 2026 03:47:58 +0100 Subject: [PATCH 1/3] feat: add restricted ERC20 token project --- erc20_token/.gitignore | 5 + erc20_token/README.md | 46 ++++++++ erc20_token/Scarb.lock | 24 ++++ erc20_token/Scarb.toml | 52 +++++++++ erc20_token/snfoundry.toml | 11 ++ erc20_token/src/checks.cairo | 32 ++++++ erc20_token/src/errors.cairo | 7 ++ erc20_token/src/events.cairo | 15 +++ erc20_token/src/interfaces.cairo | 25 ++++ erc20_token/src/lib.cairo | 8 ++ erc20_token/src/storage.cairo | 1 + erc20_token/src/token.cairo | 154 +++++++++++++++++++++++++ erc20_token/tests/test_contract.cairo | 160 ++++++++++++++++++++++++++ 13 files changed, 540 insertions(+) create mode 100644 erc20_token/.gitignore create mode 100644 erc20_token/README.md create mode 100644 erc20_token/Scarb.lock create mode 100644 erc20_token/Scarb.toml create mode 100644 erc20_token/snfoundry.toml create mode 100644 erc20_token/src/checks.cairo create mode 100644 erc20_token/src/errors.cairo create mode 100644 erc20_token/src/events.cairo create mode 100644 erc20_token/src/interfaces.cairo create mode 100644 erc20_token/src/lib.cairo create mode 100644 erc20_token/src/storage.cairo create mode 100644 erc20_token/src/token.cairo create mode 100644 erc20_token/tests/test_contract.cairo diff --git a/erc20_token/.gitignore b/erc20_token/.gitignore new file mode 100644 index 0000000..4096f8b --- /dev/null +++ b/erc20_token/.gitignore @@ -0,0 +1,5 @@ +target +.snfoundry_cache/ +snfoundry_trace/ +coverage/ +profile/ diff --git a/erc20_token/README.md b/erc20_token/README.md new file mode 100644 index 0000000..8b718ef --- /dev/null +++ b/erc20_token/README.md @@ -0,0 +1,46 @@ +# Restricted ERC20 Token (Starknet) + +A simple ERC20 token on Starknet with transfer limits and admin controls. Built with Cairo and [Scarb](https://docs.swmansion.com/scarb/). + +## Features + +| Feature | Description | +|---------|-------------| +| **ERC20** | `transfer`, `approve`, `transfer_from`, balances, supply | +| **Transfer cap** | Each transfer must be ≤ `max_limit` (default **10,000**) | +| **Admin** | Set at deploy via constructor — not the UDC deployer | +| **Revoke** | User who approved can cancel their allowance (`revoke`) — [revoke.cash](https://revoke.cash) style | +| **Admin burn** | Owner can destroy tokens from any address | +| **Update limit** | Owner can change `max_limit` | + + + +## Build & test + +```bash +scarb build +scarb test +``` + +## Deploy (constructor args) + +Pass addresses and token settings in this order: + +1. `admin` — admin address (stored as `owner`) +2. `recipient` — initial token holder +3. `name` — token name (felt252) +4. `symbol` — ticker (felt252) +5. `decimals` — e.g. `18` +6. `initial_supply` — tokens minted to `recipient` + +Contract module name for declare: **`ERC20Token`** + +## Who can call what + +| Function | Caller | +|----------|--------| +| `transfer`, `approve`, `revoke` | Any token holder | +| `transfer_from` | Approved spender | +| `burn`, `set_max_limit` | Admin (`owner`) | +| `get_owner`, `balance_of`, … | Anyone (read-only) | + diff --git a/erc20_token/Scarb.lock b/erc20_token/Scarb.lock new file mode 100644 index 0000000..989516c --- /dev/null +++ b/erc20_token/Scarb.lock @@ -0,0 +1,24 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "erc20_token" +version = "0.1.0" +dependencies = [ + "snforge_std", +] + +[[package]] +name = "snforge_scarb_plugin" +version = "0.59.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:871fba677c03b66a1bf40815dac0ab1b385eb1b9be6e6c3cf2ad9788eeb2b6bb" + +[[package]] +name = "snforge_std" +version = "0.59.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:3620924fa08bd2d740b2b5b01ef86c8dab3d4b9c2206387c8dbdc8d2ec15133e" +dependencies = [ + "snforge_scarb_plugin", +] diff --git a/erc20_token/Scarb.toml b/erc20_token/Scarb.toml new file mode 100644 index 0000000..e9285a2 --- /dev/null +++ b/erc20_token/Scarb.toml @@ -0,0 +1,52 @@ +[package] +name = "erc20_token" +version = "0.1.0" +edition = "2024_07" + +# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html + +[dependencies] +starknet = "2.17.0" + +[dev-dependencies] +snforge_std = "0.59.0" +assert_macros = "2.17.0" + +[[target.starknet-contract]] +sierra = true + +[scripts] +test = "snforge test" + +[tool.scarb] +allow-prebuilt-plugins = ["snforge_std"] + +# Visit https://foundry-rs.github.io/starknet-foundry/appendix/scarb-toml.html for more information + +# [tool.snforge] # Define `snforge` tool section +# exit_first = true # Stop tests execution immediately upon the first failure +# fuzzer_runs = 1234 # Number of runs of the random fuzzer +# fuzzer_seed = 1111 # Seed for the random fuzzer + +# [[tool.snforge.fork]] # Used for fork testing +# name = "SOME_NAME" # Fork name +# url = "http://your.rpc.url" # Url of the RPC provider +# block_id.tag = "latest" # Block to fork from (block tag) + +# [[tool.snforge.fork]] +# name = "SOME_SECOND_NAME" +# url = "http://your.second.rpc.url" +# block_id.number = "123" # Block to fork from (block number) + +# [[tool.snforge.fork]] +# name = "SOME_THIRD_NAME" +# url = "http://your.third.rpc.url" +# block_id.hash = "0x123" # Block to fork from (block hash) + +# [profile.dev.cairo] # Configure Cairo compiler +# unstable-add-statements-code-locations-debug-info = true # Should be used if you want to use coverage +# unstable-add-statements-functions-debug-info = true # Should be used if you want to use coverage/profiler +# inlining-strategy = "avoid" # Should be used if you want to use coverage + +# [features] # Used for conditional compilation +# enable_for_tests = [] # Feature name and list of other features that should be enabled with it diff --git a/erc20_token/snfoundry.toml b/erc20_token/snfoundry.toml new file mode 100644 index 0000000..686c2ab --- /dev/null +++ b/erc20_token/snfoundry.toml @@ -0,0 +1,11 @@ +# Visit https://foundry-rs.github.io/starknet-foundry/appendix/snfoundry-toml.html +# and https://foundry-rs.github.io/starknet-foundry/projects/configuration.html for more information + +# [sncast.default] # Define a profile name +# url = "https://api.zan.top/public/starknet-sepolia/rpc/v0_10" # Url of the RPC provider +# accounts-file = "../account-file" # Path to the file with the account data +# account = "mainuser" # Account from `accounts_file` or default account file that will be used for the transactions +# keystore = "~/keystore" # Path to the keystore file +# wait-params = { timeout = 300, retry-interval = 10 } # Wait for submitted transaction parameters +# block-explorer = "Voyager" # Block explorer service used to display links to transaction details +# show-explorer-links = true # Print links pointing to pages with transaction details in the chosen block explorer diff --git a/erc20_token/src/checks.cairo b/erc20_token/src/checks.cairo new file mode 100644 index 0000000..3b15dca --- /dev/null +++ b/erc20_token/src/checks.cairo @@ -0,0 +1,32 @@ +use core::integer::u256; +use core::traits::Into; + +use crate::errors; + +pub fn assert_within_limit(amount: felt252, max_limit: felt252) { + let amt: u256 = amount.into(); + let limit: u256 = max_limit.into(); + if amt > limit { + assert(false, errors::OVER_LIMIT); + } +} + +pub fn assert_enough_balance(balance: felt252, amount: felt252) { + let bal: u256 = balance.into(); + let amt: u256 = amount.into(); + if bal < amt { + assert(false, errors::LOW_BALANCE); + } +} + +pub fn assert_enough_allowance(allowed: felt252, amount: felt252) { + let allow: u256 = allowed.into(); + let amt: u256 = amount.into(); + if allow < amt { + assert(false, errors::LOW_ALLOWANCE); + } +} + +pub fn assert_is_owner(caller: starknet::ContractAddress, owner: starknet::ContractAddress) { + assert(caller == owner, errors::NOT_OWNER); +} diff --git a/erc20_token/src/errors.cairo b/erc20_token/src/errors.cairo new file mode 100644 index 0000000..20a38c0 --- /dev/null +++ b/erc20_token/src/errors.cairo @@ -0,0 +1,7 @@ +pub const OVER_LIMIT: felt252 = 'Over limit'; +pub const FROM_ZERO: felt252 = 'From zero'; +pub const TO_ZERO: felt252 = 'To zero'; +pub const SPENDER_ZERO: felt252 = 'Spender zero'; +pub const LOW_BALANCE: felt252 = 'Low balance'; +pub const LOW_ALLOWANCE: felt252 = 'Low allowance'; +pub const NOT_OWNER: felt252 = 'Not owner'; diff --git a/erc20_token/src/events.cairo b/erc20_token/src/events.cairo new file mode 100644 index 0000000..e3bcffe --- /dev/null +++ b/erc20_token/src/events.cairo @@ -0,0 +1,15 @@ +use starknet::ContractAddress; + +#[derive(Copy, Drop, starknet::Event)] +pub struct Transfer { + pub from: ContractAddress, + pub to: ContractAddress, + pub value: felt252, +} + +#[derive(Copy, Drop, starknet::Event)] +pub struct Approval { + pub owner: ContractAddress, + pub spender: ContractAddress, + pub value: felt252, +} diff --git a/erc20_token/src/interfaces.cairo b/erc20_token/src/interfaces.cairo new file mode 100644 index 0000000..46bc13c --- /dev/null +++ b/erc20_token/src/interfaces.cairo @@ -0,0 +1,25 @@ +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IERC20 { + fn get_name(self: @TContractState) -> felt252; + fn get_symbol(self: @TContractState) -> felt252; + fn get_decimals(self: @TContractState) -> u8; + fn get_total_supply(self: @TContractState) -> felt252; + fn balance_of(self: @TContractState, account: ContractAddress) -> felt252; + fn allowance( + self: @TContractState, owner: ContractAddress, spender: ContractAddress, + ) -> felt252; + fn get_owner(self: @TContractState) -> ContractAddress; + fn transfer(ref self: TContractState, recipient: ContractAddress, amount: felt252); + fn transfer_from( + ref self: TContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: felt252, + ); + fn approve(ref self: TContractState, spender: ContractAddress, amount: felt252); + fn revoke(ref self: TContractState, spender: ContractAddress); + fn burn(ref self: TContractState, from: ContractAddress, amount: felt252); + fn set_max_limit(ref self: TContractState, new_limit: felt252); +} diff --git a/erc20_token/src/lib.cairo b/erc20_token/src/lib.cairo new file mode 100644 index 0000000..6a2197e --- /dev/null +++ b/erc20_token/src/lib.cairo @@ -0,0 +1,8 @@ +pub mod checks; +pub mod errors; +pub mod events; +pub mod interfaces; +pub mod storage; +pub mod token; + +pub use interfaces::IERC20; diff --git a/erc20_token/src/storage.cairo b/erc20_token/src/storage.cairo new file mode 100644 index 0000000..e39d53c --- /dev/null +++ b/erc20_token/src/storage.cairo @@ -0,0 +1 @@ +pub const MAX_LIMIT: felt252 = 10_000; diff --git a/erc20_token/src/token.cairo b/erc20_token/src/token.cairo new file mode 100644 index 0000000..5b4ad25 --- /dev/null +++ b/erc20_token/src/token.cairo @@ -0,0 +1,154 @@ +#[starknet::contract] +pub mod ERC20Token { + use core::num::traits::Zero; + use starknet::get_caller_address; + use starknet::ContractAddress; + use starknet::storage::{ + Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, + StoragePointerWriteAccess, + }; + + use crate::checks::{ + assert_enough_allowance, assert_enough_balance, assert_is_owner, assert_within_limit, + }; + use crate::errors; + use crate::events::{Approval, Transfer}; + use crate::storage::MAX_LIMIT; + + #[storage] + struct Storage { + owner: ContractAddress, + name: felt252, + symbol: felt252, + decimals: u8, + total_supply: felt252, + max_limit: felt252, + balances: Map::, + allowances: Map::<(ContractAddress, ContractAddress), felt252>, + } + + #[event] + #[derive(Copy, Drop, starknet::Event)] + enum Event { + Transfer: Transfer, + Approval: Approval, + } + + #[constructor] + fn constructor( + ref self: ContractState, + admin: ContractAddress, + recipient: ContractAddress, + name: felt252, + symbol: felt252, + decimals: u8, + initial_supply: felt252, + ) { + self.owner.write(admin); + self.name.write(name); + self.symbol.write(symbol); + self.decimals.write(decimals); + self.max_limit.write(MAX_LIMIT); + self.total_supply.write(initial_supply); + self.balances.write(recipient, initial_supply); + } + + #[abi(embed_v0)] + impl ERC20Impl of crate::interfaces::IERC20 { + fn get_name(self: @ContractState) -> felt252 { + self.name.read() + } + + fn get_symbol(self: @ContractState) -> felt252 { + self.symbol.read() + } + + fn get_decimals(self: @ContractState) -> u8 { + self.decimals.read() + } + + fn get_total_supply(self: @ContractState) -> felt252 { + self.total_supply.read() + } + + + fn balance_of(self: @ContractState, account: ContractAddress) -> felt252 { + self.balances.read(account) + } + + fn allowance( + self: @ContractState, owner: ContractAddress, spender: ContractAddress, + ) -> felt252 { + self.allowances.read((owner, spender)) + } + + fn get_owner(self: @ContractState) -> ContractAddress { + self.owner.read() + } + + fn transfer(ref self: ContractState, recipient: ContractAddress, amount: felt252) { + assert_within_limit(amount, self.max_limit.read()); + + let sender = get_caller_address(); + assert(sender.is_non_zero(), errors::FROM_ZERO); + // assert(recipient.is_non_zero(), errors::TO_ZERO); + + let bal = self.balances.read(sender); + assert_enough_balance(bal, amount); + + self.balances.write(sender, bal - amount); + self.balances.write(recipient, self.balances.read(recipient) + amount); + self.emit(Event::Transfer(Transfer { from: sender, to: recipient, value: amount })); + } + + fn transfer_from( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: felt252, + ) { + assert_within_limit(amount, self.max_limit.read()); + + let caller = get_caller_address(); + let allowed = self.allowances.read((sender, caller)); + assert_enough_allowance(allowed, amount); + self.allowances.write((sender, caller), allowed - amount); + + let bal = self.balances.read(sender); + assert_enough_balance(bal, amount); + + self.balances.write(sender, bal - amount); + self.balances.write(recipient, self.balances.read(recipient) + amount); + self.emit(Event::Transfer(Transfer { from: sender, to: recipient, value: amount })); + } + + fn approve(ref self: ContractState, spender: ContractAddress, amount: felt252) { + assert(spender.is_non_zero(), errors::SPENDER_ZERO); + + let owner = get_caller_address(); + self.allowances.write((owner, spender), amount); + self.emit(Event::Approval(Approval { owner, spender, value: amount })); + } + + fn revoke(ref self: ContractState, spender: ContractAddress) { + let owner = get_caller_address(); + self.allowances.write((owner, spender), 0); + self.emit(Event::Approval(Approval { owner, spender, value: 0 })); + } + + fn burn(ref self: ContractState, from: ContractAddress, amount: felt252) { + assert_is_owner(get_caller_address(), self.owner.read()); + + let bal = self.balances.read(from); + assert_enough_balance(bal, amount); + + self.balances.write(from, bal - amount); + self.total_supply.write(self.total_supply.read() - amount); + } + + fn set_max_limit(ref self: ContractState, new_limit: felt252) { + assert_is_owner(get_caller_address(), self.owner.read()); + self.max_limit.write(new_limit); + } + } +} diff --git a/erc20_token/tests/test_contract.cairo b/erc20_token/tests/test_contract.cairo new file mode 100644 index 0000000..471b969 --- /dev/null +++ b/erc20_token/tests/test_contract.cairo @@ -0,0 +1,160 @@ +use snforge_std::{ + ContractClassTrait, DeclareResultTrait, declare, start_cheat_caller_address, + stop_cheat_caller_address, +}; +use starknet::ContractAddress; + +use erc20_token::interfaces::{IERC20Dispatcher, IERC20DispatcherTrait}; +use erc20_token::storage::MAX_LIMIT; + +fn admin() -> ContractAddress { + 'admin'.try_into().unwrap() +} + +fn user() -> ContractAddress { + 'user'.try_into().unwrap() +} + +fn spender() -> ContractAddress { + 'spender'.try_into().unwrap() +} + +fn deploy( + admin_addr: ContractAddress, + recipient: ContractAddress, + initial_supply: felt252, +) -> ContractAddress { + let contract = declare("ERC20Token").unwrap().contract_class(); + let mut calldata = array![ + admin_addr.into(), + recipient.into(), + 'TestToken', + 'TTK', + 18, + initial_supply, + ]; + let (address, _) = contract.deploy(@calldata).unwrap(); + address +} + +#[test] +fn constructor_sets_state() { + let token = deploy(admin(), user(), 50_000); + let dispatcher = IERC20Dispatcher { contract_address: token }; + + assert(dispatcher.get_name() == 'TestToken', 'name'); + assert(dispatcher.get_symbol() == 'TTK', 'symbol'); + assert(dispatcher.get_decimals() == 18, 'decimals'); + assert(dispatcher.get_total_supply() == 50_000, 'supply'); + assert(dispatcher.get_owner() == admin(), 'owner'); + assert(dispatcher.balance_of(user()) == 50_000, 'recipient balance'); + assert(dispatcher.balance_of(admin()) == 0, 'admin balance'); +} + +#[test] +fn transfer_moves_tokens() { + let token = deploy(admin(), user(), 10_000); + let dispatcher = IERC20Dispatcher { contract_address: token }; + + start_cheat_caller_address(token, user()); + dispatcher.transfer(admin(), 1000); + stop_cheat_caller_address(token); + + assert(dispatcher.balance_of(user()) == 9000, 'sender'); + assert(dispatcher.balance_of(admin()) == 1000, 'recipient'); +} + +#[test] +#[should_panic(expected: ('Over limit',))] +fn transfer_over_max_limit_panics() { + let token = deploy(admin(), user(), 50_000); + let dispatcher = IERC20Dispatcher { contract_address: token }; + + start_cheat_caller_address(token, user()); + dispatcher.transfer(admin(), MAX_LIMIT + 1); + stop_cheat_caller_address(token); +} + +#[test] +#[should_panic(expected: ('Low balance',))] +fn transfer_insufficient_balance_panics() { + let token = deploy(admin(), user(), 100); + let dispatcher = IERC20Dispatcher { contract_address: token }; + + start_cheat_caller_address(token, user()); + dispatcher.transfer(admin(), 101); + stop_cheat_caller_address(token); +} + +#[test] +fn approve_and_transfer_from() { + let token = deploy(admin(), user(), 10_000); + let dispatcher = IERC20Dispatcher { contract_address: token }; + + start_cheat_caller_address(token, user()); + dispatcher.approve(spender(), 3000); + stop_cheat_caller_address(token); + + assert(dispatcher.allowance(user(), spender()) == 3000, 'allowance'); + + start_cheat_caller_address(token, spender()); + dispatcher.transfer_from(user(), admin(), 2000); + stop_cheat_caller_address(token); + + assert(dispatcher.balance_of(user()) == 8000, 'owner balance'); + assert(dispatcher.balance_of(admin()) == 2000, 'recipient balance'); + assert(dispatcher.allowance(user(), spender()) == 1000, 'remaining allowance'); +} + +#[test] +fn revoke_clears_allowance() { + let token = deploy(admin(), user(), 10_000); + let dispatcher = IERC20Dispatcher { contract_address: token }; + + start_cheat_caller_address(token, user()); + dispatcher.approve(spender(), 500); + dispatcher.revoke(spender()); + stop_cheat_caller_address(token); + + assert(dispatcher.allowance(user(), spender()) == 0, 'revoked'); +} + +#[test] +fn owner_burn_reduces_balance_and_supply() { + let token = deploy(admin(), user(), 10_000); + let dispatcher = IERC20Dispatcher { contract_address: token }; + + start_cheat_caller_address(token, admin()); + dispatcher.burn(user(), 2500); + stop_cheat_caller_address(token); + + assert(dispatcher.balance_of(user()) == 7500, 'balance'); + assert(dispatcher.get_total_supply() == 7500, 'supply'); +} + +#[test] +fn owner_can_update_max_limit() { + let token = deploy(admin(), user(), 10_000); + let dispatcher = IERC20Dispatcher { contract_address: token }; + + start_cheat_caller_address(token, admin()); + dispatcher.set_max_limit(500); + stop_cheat_caller_address(token); + + start_cheat_caller_address(token, user()); + dispatcher.transfer(admin(), 500); + stop_cheat_caller_address(token); + + assert(dispatcher.balance_of(admin()) == 500, 'transfer at new limit'); +} + +#[test] +#[should_panic(expected: ('Not owner',))] +fn non_owner_cannot_burn() { + let token = deploy(admin(), user(), 10_000); + let dispatcher = IERC20Dispatcher { contract_address: token }; + + start_cheat_caller_address(token, user()); + dispatcher.burn(user(), 1); + stop_cheat_caller_address(token); +} From 39784007670868ff68d415ee637c46f1bf2e6e11 Mon Sep 17 00:00:00 2001 From: Yusrah Mohammed Date: Thu, 21 May 2026 13:59:31 +0100 Subject: [PATCH 2/3] updated the token contract --- erc20_token/README.md | 5 +- erc20_token/src/checks.cairo | 31 ++++------ erc20_token/src/errors.cairo | 7 --- erc20_token/src/events.cairo | 8 +-- erc20_token/src/interfaces.cairo | 21 +++---- erc20_token/src/lib.cairo | 1 - erc20_token/src/storage.cairo | 2 +- erc20_token/src/token.cairo | 82 +++++++++++++++++---------- erc20_token/tests/test_contract.cairo | 48 ++++++++++++---- 9 files changed, 117 insertions(+), 88 deletions(-) delete mode 100644 erc20_token/src/errors.cairo diff --git a/erc20_token/README.md b/erc20_token/README.md index 8b718ef..b354200 100644 --- a/erc20_token/README.md +++ b/erc20_token/README.md @@ -28,8 +28,8 @@ Pass addresses and token settings in this order: 1. `admin` — admin address (stored as `owner`) 2. `recipient` — initial token holder -3. `name` — token name (felt252) -4. `symbol` — ticker (felt252) +3. `name` — token name (`ByteArray`) +4. `symbol` — ticker (`ByteArray`) 5. `decimals` — e.g. `18` 6. `initial_supply` — tokens minted to `recipient` @@ -43,4 +43,3 @@ Contract module name for declare: **`ERC20Token`** | `transfer_from` | Approved spender | | `burn`, `set_max_limit` | Admin (`owner`) | | `get_owner`, `balance_of`, … | Anyone (read-only) | - diff --git a/erc20_token/src/checks.cairo b/erc20_token/src/checks.cairo index 3b15dca..982a4d2 100644 --- a/erc20_token/src/checks.cairo +++ b/erc20_token/src/checks.cairo @@ -1,32 +1,21 @@ -use core::integer::u256; -use core::traits::Into; - -use crate::errors; - -pub fn assert_within_limit(amount: felt252, max_limit: felt252) { - let amt: u256 = amount.into(); - let limit: u256 = max_limit.into(); - if amt > limit { - assert(false, errors::OVER_LIMIT); +pub fn assert_within_limit(amount: u256, max_limit: u256) { + if amount > max_limit { + assert(false, 'Over limit'); } } -pub fn assert_enough_balance(balance: felt252, amount: felt252) { - let bal: u256 = balance.into(); - let amt: u256 = amount.into(); - if bal < amt { - assert(false, errors::LOW_BALANCE); +pub fn assert_enough_balance(balance: u256, amount: u256) { + if balance < amount { + assert(false, 'Low balance'); } } -pub fn assert_enough_allowance(allowed: felt252, amount: felt252) { - let allow: u256 = allowed.into(); - let amt: u256 = amount.into(); - if allow < amt { - assert(false, errors::LOW_ALLOWANCE); +pub fn assert_enough_allowance(allowed: u256, amount: u256) { + if allowed < amount { + assert(false, 'Low allowance'); } } pub fn assert_is_owner(caller: starknet::ContractAddress, owner: starknet::ContractAddress) { - assert(caller == owner, errors::NOT_OWNER); + assert(caller == owner, 'Not owner'); } diff --git a/erc20_token/src/errors.cairo b/erc20_token/src/errors.cairo deleted file mode 100644 index 20a38c0..0000000 --- a/erc20_token/src/errors.cairo +++ /dev/null @@ -1,7 +0,0 @@ -pub const OVER_LIMIT: felt252 = 'Over limit'; -pub const FROM_ZERO: felt252 = 'From zero'; -pub const TO_ZERO: felt252 = 'To zero'; -pub const SPENDER_ZERO: felt252 = 'Spender zero'; -pub const LOW_BALANCE: felt252 = 'Low balance'; -pub const LOW_ALLOWANCE: felt252 = 'Low allowance'; -pub const NOT_OWNER: felt252 = 'Not owner'; diff --git a/erc20_token/src/events.cairo b/erc20_token/src/events.cairo index e3bcffe..33782a3 100644 --- a/erc20_token/src/events.cairo +++ b/erc20_token/src/events.cairo @@ -1,15 +1,15 @@ use starknet::ContractAddress; -#[derive(Copy, Drop, starknet::Event)] +#[derive(Drop, starknet::Event)] pub struct Transfer { pub from: ContractAddress, pub to: ContractAddress, - pub value: felt252, + pub value: u256, } -#[derive(Copy, Drop, starknet::Event)] +#[derive(Drop, starknet::Event)] pub struct Approval { pub owner: ContractAddress, pub spender: ContractAddress, - pub value: felt252, + pub value: u256, } diff --git a/erc20_token/src/interfaces.cairo b/erc20_token/src/interfaces.cairo index 46bc13c..addf268 100644 --- a/erc20_token/src/interfaces.cairo +++ b/erc20_token/src/interfaces.cairo @@ -2,24 +2,25 @@ use starknet::ContractAddress; #[starknet::interface] pub trait IERC20 { - fn get_name(self: @TContractState) -> felt252; - fn get_symbol(self: @TContractState) -> felt252; + fn get_name(self: @TContractState) -> ByteArray; + fn get_symbol(self: @TContractState) -> ByteArray; fn get_decimals(self: @TContractState) -> u8; - fn get_total_supply(self: @TContractState) -> felt252; - fn balance_of(self: @TContractState, account: ContractAddress) -> felt252; + fn get_total_supply(self: @TContractState) -> u256; + fn balance_of(self: @TContractState, account: ContractAddress) -> u256; fn allowance( self: @TContractState, owner: ContractAddress, spender: ContractAddress, - ) -> felt252; + ) -> u256; fn get_owner(self: @TContractState) -> ContractAddress; - fn transfer(ref self: TContractState, recipient: ContractAddress, amount: felt252); + fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256); fn transfer_from( ref self: TContractState, sender: ContractAddress, recipient: ContractAddress, - amount: felt252, + amount: u256, ); - fn approve(ref self: TContractState, spender: ContractAddress, amount: felt252); + fn approve(ref self: TContractState, spender: ContractAddress, amount: u256); fn revoke(ref self: TContractState, spender: ContractAddress); - fn burn(ref self: TContractState, from: ContractAddress, amount: felt252); - fn set_max_limit(ref self: TContractState, new_limit: felt252); + fn mint(ref self: TContractState, recipient: ContractAddress, amount: u256); + fn burn(ref self: TContractState, from: ContractAddress, amount: u256); + fn set_max_limit(ref self: TContractState, new_limit: u256); } diff --git a/erc20_token/src/lib.cairo b/erc20_token/src/lib.cairo index 6a2197e..604c613 100644 --- a/erc20_token/src/lib.cairo +++ b/erc20_token/src/lib.cairo @@ -1,5 +1,4 @@ pub mod checks; -pub mod errors; pub mod events; pub mod interfaces; pub mod storage; diff --git a/erc20_token/src/storage.cairo b/erc20_token/src/storage.cairo index e39d53c..5838de0 100644 --- a/erc20_token/src/storage.cairo +++ b/erc20_token/src/storage.cairo @@ -1 +1 @@ -pub const MAX_LIMIT: felt252 = 10_000; +pub const MAX_LIMIT: u256 = 10_000; diff --git a/erc20_token/src/token.cairo b/erc20_token/src/token.cairo index 5b4ad25..bb03689 100644 --- a/erc20_token/src/token.cairo +++ b/erc20_token/src/token.cairo @@ -1,8 +1,7 @@ #[starknet::contract] pub mod ERC20Token { use core::num::traits::Zero; - use starknet::get_caller_address; - use starknet::ContractAddress; + use starknet::{ContractAddress, get_caller_address}; use starknet::storage::{ Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, StoragePointerWriteAccess, @@ -11,24 +10,27 @@ pub mod ERC20Token { use crate::checks::{ assert_enough_allowance, assert_enough_balance, assert_is_owner, assert_within_limit, }; - use crate::errors; use crate::events::{Approval, Transfer}; use crate::storage::MAX_LIMIT; #[storage] struct Storage { owner: ContractAddress, - name: felt252, - symbol: felt252, + name: ByteArray, + symbol: ByteArray, decimals: u8, - total_supply: felt252, - max_limit: felt252, - balances: Map::, - allowances: Map::<(ContractAddress, ContractAddress), felt252>, + total_supply: u256, + max_limit: u256, + balances: Map::, + allowances: Map::<(ContractAddress, ContractAddress), u256>, + } + + fn zero_address() -> ContractAddress { + 0.try_into().unwrap() } #[event] - #[derive(Copy, Drop, starknet::Event)] + #[derive(Drop, starknet::Event)] enum Event { Transfer: Transfer, Approval: Approval, @@ -37,29 +39,34 @@ pub mod ERC20Token { #[constructor] fn constructor( ref self: ContractState, - admin: ContractAddress, + owner: ContractAddress, recipient: ContractAddress, - name: felt252, - symbol: felt252, + name: ByteArray, + symbol: ByteArray, decimals: u8, - initial_supply: felt252, + initial_supply: u256, ) { - self.owner.write(admin); + self.owner.write(owner); self.name.write(name); self.symbol.write(symbol); self.decimals.write(decimals); self.max_limit.write(MAX_LIMIT); self.total_supply.write(initial_supply); self.balances.write(recipient, initial_supply); + self.emit( + Event::Transfer(Transfer { + from: zero_address(), to: recipient, value: initial_supply, + }), + ); } #[abi(embed_v0)] impl ERC20Impl of crate::interfaces::IERC20 { - fn get_name(self: @ContractState) -> felt252 { + fn get_name(self: @ContractState) -> ByteArray { self.name.read() } - fn get_symbol(self: @ContractState) -> felt252 { + fn get_symbol(self: @ContractState) -> ByteArray { self.symbol.read() } @@ -67,18 +74,18 @@ pub mod ERC20Token { self.decimals.read() } - fn get_total_supply(self: @ContractState) -> felt252 { + fn get_total_supply(self: @ContractState) -> u256 { self.total_supply.read() } - fn balance_of(self: @ContractState, account: ContractAddress) -> felt252 { + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { self.balances.read(account) } fn allowance( self: @ContractState, owner: ContractAddress, spender: ContractAddress, - ) -> felt252 { + ) -> u256 { self.allowances.read((owner, spender)) } @@ -86,12 +93,12 @@ pub mod ERC20Token { self.owner.read() } - fn transfer(ref self: ContractState, recipient: ContractAddress, amount: felt252) { + fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) { assert_within_limit(amount, self.max_limit.read()); let sender = get_caller_address(); - assert(sender.is_non_zero(), errors::FROM_ZERO); - // assert(recipient.is_non_zero(), errors::TO_ZERO); + assert(sender.is_non_zero(), 'From zero'); + assert(recipient.is_non_zero(), 'To zero'); let bal = self.balances.read(sender); assert_enough_balance(bal, amount); @@ -105,7 +112,7 @@ pub mod ERC20Token { ref self: ContractState, sender: ContractAddress, recipient: ContractAddress, - amount: felt252, + amount: u256, ) { assert_within_limit(amount, self.max_limit.read()); @@ -122,8 +129,8 @@ pub mod ERC20Token { self.emit(Event::Transfer(Transfer { from: sender, to: recipient, value: amount })); } - fn approve(ref self: ContractState, spender: ContractAddress, amount: felt252) { - assert(spender.is_non_zero(), errors::SPENDER_ZERO); + fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) { + assert(spender.is_non_zero(), 'Spender zero'); let owner = get_caller_address(); self.allowances.write((owner, spender), amount); @@ -132,21 +139,36 @@ pub mod ERC20Token { fn revoke(ref self: ContractState, spender: ContractAddress) { let owner = get_caller_address(); - self.allowances.write((owner, spender), 0); - self.emit(Event::Approval(Approval { owner, spender, value: 0 })); + self.allowances.write((owner, spender), Zero::zero()); + self.emit(Event::Approval(Approval { owner, spender, value: Zero::zero() })); + } + + fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256) { + assert_is_owner(get_caller_address(), self.owner.read()); + assert(recipient.is_non_zero(), 'To zero'); + + self.total_supply.write(self.total_supply.read() + amount); + self.balances.write(recipient, self.balances.read(recipient) + amount); + self.emit( + Event::Transfer(Transfer { + from: zero_address(), to: recipient, value: amount, + }), + ); } - fn burn(ref self: ContractState, from: ContractAddress, amount: felt252) { + fn burn(ref self: ContractState, from: ContractAddress, amount: u256) { assert_is_owner(get_caller_address(), self.owner.read()); + assert(from.is_non_zero(), 'From zero'); let bal = self.balances.read(from); assert_enough_balance(bal, amount); self.balances.write(from, bal - amount); self.total_supply.write(self.total_supply.read() - amount); + self.emit(Event::Transfer(Transfer { from, to: zero_address(), value: amount })); } - fn set_max_limit(ref self: ContractState, new_limit: felt252) { + fn set_max_limit(ref self: ContractState, new_limit: u256) { assert_is_owner(get_caller_address(), self.owner.read()); self.max_limit.write(new_limit); } diff --git a/erc20_token/tests/test_contract.cairo b/erc20_token/tests/test_contract.cairo index 471b969..8a1c5b6 100644 --- a/erc20_token/tests/test_contract.cairo +++ b/erc20_token/tests/test_contract.cairo @@ -2,6 +2,7 @@ use snforge_std::{ ContractClassTrait, DeclareResultTrait, declare, start_cheat_caller_address, stop_cheat_caller_address, }; +use core::serde::Serde; use starknet::ContractAddress; use erc20_token::interfaces::{IERC20Dispatcher, IERC20DispatcherTrait}; @@ -22,17 +23,18 @@ fn spender() -> ContractAddress { fn deploy( admin_addr: ContractAddress, recipient: ContractAddress, - initial_supply: felt252, + initial_supply: u256, ) -> ContractAddress { let contract = declare("ERC20Token").unwrap().contract_class(); - let mut calldata = array![ - admin_addr.into(), - recipient.into(), - 'TestToken', - 'TTK', - 18, - initial_supply, - ]; + let name: ByteArray = "TestToken"; + let symbol: ByteArray = "TTK"; + let mut calldata = array![]; + admin_addr.serialize(ref calldata); + recipient.serialize(ref calldata); + name.serialize(ref calldata); + symbol.serialize(ref calldata); + 18_u8.serialize(ref calldata); + initial_supply.serialize(ref calldata); let (address, _) = contract.deploy(@calldata).unwrap(); address } @@ -42,8 +44,8 @@ fn constructor_sets_state() { let token = deploy(admin(), user(), 50_000); let dispatcher = IERC20Dispatcher { contract_address: token }; - assert(dispatcher.get_name() == 'TestToken', 'name'); - assert(dispatcher.get_symbol() == 'TTK', 'symbol'); + assert(dispatcher.get_name() == "TestToken", 'name'); + assert(dispatcher.get_symbol() == "TTK", 'symbol'); assert(dispatcher.get_decimals() == 18, 'decimals'); assert(dispatcher.get_total_supply() == 50_000, 'supply'); assert(dispatcher.get_owner() == admin(), 'owner'); @@ -119,6 +121,19 @@ fn revoke_clears_allowance() { assert(dispatcher.allowance(user(), spender()) == 0, 'revoked'); } +#[test] +fn owner_can_mint_from_zero_address() { + let token = deploy(admin(), user(), 10_000); + let dispatcher = IERC20Dispatcher { contract_address: token }; + + start_cheat_caller_address(token, admin()); + dispatcher.mint(admin(), 2_500); + stop_cheat_caller_address(token); + + assert(dispatcher.balance_of(admin()) == 2_500, 'minted balance'); + assert(dispatcher.get_total_supply() == 12_500, 'minted supply'); +} + #[test] fn owner_burn_reduces_balance_and_supply() { let token = deploy(admin(), user(), 10_000); @@ -158,3 +173,14 @@ fn non_owner_cannot_burn() { dispatcher.burn(user(), 1); stop_cheat_caller_address(token); } + +#[test] +#[should_panic(expected: ('Not owner',))] +fn non_owner_cannot_mint() { + let token = deploy(admin(), user(), 10_000); + let dispatcher = IERC20Dispatcher { contract_address: token }; + + start_cheat_caller_address(token, user()); + dispatcher.mint(user(), 1); + stop_cheat_caller_address(token); +} From 88d51c3f1094ebbcf45dd15c2d86f55bf66657e3 Mon Sep 17 00:00:00 2001 From: Yusrah Mohammed Date: Thu, 21 May 2026 18:40:34 +0100 Subject: [PATCH 3/3] feat: adding my assignment --- cairo_program/src/hello_world.cairo | 2 +- cairo_program/src/short_string.cairo | 2 +- starknet-agentic | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) create mode 160000 starknet-agentic diff --git a/cairo_program/src/hello_world.cairo b/cairo_program/src/hello_world.cairo index 1739c6d..77754c4 100644 --- a/cairo_program/src/hello_world.cairo +++ b/cairo_program/src/hello_world.cairo @@ -1,6 +1,6 @@ #[executable] fn main() { - let x: felt252 = 32; + let x: u8 = 32; println!("x is: {}", x); println!("Hello, World!"); } diff --git a/cairo_program/src/short_string.cairo b/cairo_program/src/short_string.cairo index 6674470..6e85931 100644 --- a/cairo_program/src/short_string.cairo +++ b/cairo_program/src/short_string.cairo @@ -1,5 +1,5 @@ #[executable] fn main() { - let bootcamp_name: felt252 = 'Bootcamp 6.0'; + let bootcamp_name: ByteArray = "Bootcamp 6.0"; println!("bootcamp name is: {}", bootcamp_name); } diff --git a/starknet-agentic b/starknet-agentic new file mode 160000 index 0000000..dd8f4e8 --- /dev/null +++ b/starknet-agentic @@ -0,0 +1 @@ +Subproject commit dd8f4e8b2143f2787e38a0946e1c4fe3bf6137c5