Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,26 @@ It is encouraged that you develop an initial prototype during the application ph
* Read this book: https://www.marabu.dev/blockchain-foundations.pdf


---

## Smart Contracts

MiniChain supports fully-functional smart contracts written directly in Python!
The execution engine uses `sys.settrace` for precise **Gas Metering** (charging 1 gas per executed opcode) and `multiprocessing` for **Sandboxed Execution** to ensure network security.

### Writing a Contract
Comment on lines +110 to +111

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add blank line before heading for better readability.

Markdown best practice requires blank lines surrounding headings.

📝 Proposed fix
 The execution engine uses `sys.settrace` for precise **Gas Metering** (charging 1 gas per executed opcode) and `multiprocessing` for **Sandboxed Execution** to ensure network security.
+
 ### Writing a Contract
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 111-111: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 110 - 111, Add a blank line before the "### Writing a
Contract" heading to follow Markdown best practices and improve readability;
update the README.md by inserting an empty line immediately above the "###
Writing a Contract" heading so the heading is separated from the preceding
paragraph or content.

Source: Linters/SAST tools

Smart contracts in MiniChain have access to a persistent `storage` dictionary and a `msg` dictionary containing transaction context (`sender`, `value`, `data`).

Check out the `/examples` directory for tutorials:
- `examples/counter.py` - A basic state mutation example.
- `examples/stablecoin.py` - A minimal ERC-20 style fungible token.
- `examples/dex.py` - An Automated Market Maker (AMM) using the constant product formula (x * y = k).

### Interacting via CLI
Comment on lines +118 to +119

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add blank line before heading for better readability.

Markdown best practice requires blank lines surrounding headings.

📝 Proposed fix
 - `examples/dex.py` - An Automated Market Maker (AMM) using the constant product formula (x * y = k).
+
 ### Interacting via CLI
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
### Interacting via CLI
- `examples/dex.py` - An Automated Market Maker (AMM) using the constant product formula (x * y = k).
### Interacting via CLI
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 119-119: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 118 - 119, Add a single blank line before the "###
Interacting via CLI" heading in README.md to follow Markdown best practices and
improve readability; locate the heading string "### Interacting via CLI" and
insert one empty line immediately above it.

Source: Linters/SAST tools

Start the interactive node using `python main.py` and use the following commands:
1. **Deploy:** `deploy <filepath> [amount] [gas_limit]`
2. **Call:** `call <contract_address> <payload> [amount] [gas_limit]`

---

## Tech Stack
Expand Down
37 changes: 37 additions & 0 deletions examples/counter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Counter Smart Contract Example
#
# This is a simple counter contract designed to demonstrate the basic
# structure of smart contracts in MiniChain.
#
# Available built-ins in the MiniChain Sandbox:
# - `storage`: A dictionary persisting state across executions.
# - `msg`: A dictionary containing transaction info:
# - `msg['sender']` : The address of the caller.
# - `msg['value']` : The amount of coins attached to the call.
# - `msg['data']` : The payload string.
#
# Available functions: range(), len(), min(), max(), abs(), str(), bool(), float(), int(), list(), dict(), tuple(), sum()
#
# NOTE: The sandbox does NOT allow imports, print(), or any double-underscore methods.

if msg['data'] == 'increment':
# Retrieve the current counter value, defaulting to 0 if it doesn't exist
current_value = storage.get('counter', 0)

# Increment the counter
storage['counter'] = current_value + 1

elif msg['data'] == 'decrement':
current_value = storage.get('counter', 0)
storage['counter'] = current_value - 1

elif msg['data'] == 'reset':
# You can restrict who can reset the counter by checking the sender!
# (Just an example, anyone can call this one)
storage['counter'] = 0

else:
# If the payload doesn't match any known command, raise an exception.
# This will fail the transaction and refund the 'amount' to the sender,
# but the network will keep the 'fee' as gas.
raise Exception("Unknown command. Valid commands: increment, decrement, reset")
90 changes: 90 additions & 0 deletions examples/dex.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# MiniSwap (DEX) Smart Contract Example
#
# This contract implements a minimal Automated Market Maker (AMM)
# using the x * y = k constant product formula.
# It trades the native MiniChain coin (msg['value']) against a minted DEX Token.
#
# Valid Payloads:
# - 'init' (Must send initial native coins to provide liquidity)
# - 'buy' (Sends native coins, receives DEX tokens)
# - 'sell:<amount>' (Sells DEX tokens, receives native coins)
#
# Note: Since native coins sent to the contract are automatically added to the
# contract's balance by the state manager BEFORE execution, msg['value'] is already
# inside the contract's physical balance.

if msg['data'] == 'init':
# Initialize the liquidity pool
if storage.get('k') is not None:
raise Exception("Already initialized")
if msg['value'] <= 0:
raise Exception("Must provide initial native coin liquidity")

# We will arbitrarily mint 1000 DEX tokens to match the initial coin liquidity
storage['native_reserve'] = msg['value']
storage['token_reserve'] = 1000
storage['k'] = storage['native_reserve'] * storage['token_reserve']

# Give the initial tokens to the creator
storage[msg['sender']] = 1000

elif msg['data'] == 'buy':
# User sends native coins to buy DEX tokens
if storage.get('k') is None:
raise Exception("Not initialized")
if msg['value'] <= 0:
raise Exception("Must send coins to buy tokens")

# Calculate how many tokens to give using x * y = k
# (native_reserve + msg['value']) * (token_reserve - tokens_out) = k
new_native_reserve = storage['native_reserve'] + msg['value']
new_token_reserve = storage['k'] // new_native_reserve

tokens_out = storage['token_reserve'] - new_token_reserve
if tokens_out <= 0:
raise Exception("Not enough tokens to dispense")

# Update reserves
storage['native_reserve'] = new_native_reserve
storage['token_reserve'] = new_token_reserve
Comment on lines +38 to +49

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Integer division breaks constant product invariant.

The buy logic uses integer division at line 41 (storage['k'] // new_native_reserve), which truncates and violates the x * y = k invariant. Repeated trades can drain value through rounding manipulation. The same issue exists in the sell path at line 74.

For an educational example, consider documenting this limitation in comments, or use a rounding strategy that preserves k (e.g., always round reserves in favor of the pool). Production AMMs typically use fixed-point arithmetic or maintain k strictly.

🧰 Tools
🪛 Ruff (0.15.15)

[error] 40-40: Undefined name storage

(F821)


[error] 40-40: Undefined name msg

(F821)


[error] 41-41: Undefined name storage

(F821)


[error] 43-43: Undefined name storage

(F821)


[warning] 45-45: Create your own exception

(TRY002)


[warning] 45-45: Avoid specifying long messages outside the exception class

(TRY003)


[error] 48-48: Undefined name storage

(F821)


[error] 49-49: Undefined name storage

(F821)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/dex.py` around lines 38 - 49, The buy path uses integer division
when computing new_token_reserve (storage['k'] // new_native_reserve), which
truncates and breaks the constant-product invariant; change the calculation to
preserve k by using exact/fixed-point division or a rounding strategy that
always favors the pool (e.g., compute new_token_reserve = floor(k /
new_native_reserve) only where floor reduces user payout, or use
decimal/fraction arithmetic), then compute tokens_out = storage['token_reserve']
- new_token_reserve accordingly; make the same change in the sell path (the
symmetric calculation around storage['k'] and
new_native_reserve/new_token_reserve) and add a brief comment explaining the
chosen rounding strategy to prevent draining via repeated trades (refer to
storage['k'], new_token_reserve, tokens_out and the buy/sell logic in
examples/dex.py).


# Credit tokens to buyer
sender = msg['sender']
storage[sender] = storage.get(sender, 0) + tokens_out

elif msg['data'].startswith('sell:'):
# User sells DEX tokens to get native coins back
if storage.get('k') is None:
raise Exception("Not initialized")

parts = msg['data'].split(':')
tokens_to_sell = int(parts[1])
Comment on lines +60 to +61

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add error handling for payload parsing.

Line 61 calls int(parts[1]) without a try/except, which raises ValueError if the payload format is invalid (e.g., sell:abc). While the contract will fail safely (charging gas), a clearer error message improves user experience.

🛡️ Proposed fix
     parts = msg['data'].split(':')
-    tokens_to_sell = int(parts[1])
+    try:
+        tokens_to_sell = int(parts[1])
+    except (ValueError, IndexError):
+        raise Exception("Invalid sell format. Use: sell:<amount>")
🧰 Tools
🪛 Ruff (0.15.15)

[error] 60-60: Undefined name msg

(F821)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/dex.py` around lines 60 - 61, The payload parsing assumes
msg['data'] splits into at least two parts and that parts[1] converts to int;
add defensive validation around msg['data'] -> parts (check length >=2) and wrap
the int(parts[1]) conversion in a try/except to catch ValueError, then produce a
clear error response/log (e.g., return or raise a descriptive error mentioning
the invalid payload) instead of letting a raw ValueError bubble up; update the
code around parts, tokens_to_sell and any caller handling to use the validated
tokens_to_sell.


sender = msg['sender']
sender_tokens = storage.get(sender, 0)
if sender_tokens < tokens_to_sell:
raise Exception("Insufficient token balance")

# Deduct tokens from user
storage[sender] -= tokens_to_sell

# Calculate how many native coins to give using x * y = k
# (token_reserve + tokens_to_sell) * (native_reserve - coins_out) = k
new_token_reserve = storage['token_reserve'] + tokens_to_sell
new_native_reserve = storage['k'] // new_token_reserve

coins_out = storage['native_reserve'] - new_native_reserve
if coins_out <= 0:
raise Exception("Not enough coins to dispense")

# Update reserves
storage['native_reserve'] = new_native_reserve
storage['token_reserve'] = new_token_reserve

# Wait! In MiniChain, smart contracts cannot arbitrarily initiate outgoing transactions yet.
# To properly implement 'sell', the contract engine would need a 'transfer_out' API.
# For now, we will just record their native coin balance in storage.
storage[f"{sender}_native_credit"] = storage.get(f"{sender}_native_credit", 0) + coins_out

else:
raise Exception("Unknown command.")
41 changes: 41 additions & 0 deletions examples/stablecoin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Stablecoin (ERC-20 style) Smart Contract Example
#
# This contract implements a minimal fungible token.
#
# Valid Payloads:
# - 'mint:<amount>'
# - 'transfer:<recipient_address>:<amount>'

if msg['data'].startswith('mint:'):
# In a real contract, you would restrict this to an owner address!
# For this example, anyone can mint tokens to themselves.
amount = int(msg['data'].split(':')[1])
if amount <= 0:
raise Exception("Amount must be positive")
Comment on lines +9 to +14

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add error handling for payload parsing.

Line 12 calls int(msg['data'].split(':')[1]) without a try/except, which can raise ValueError (invalid integer) or IndexError (missing colon). While the contract will fail safely (charging gas), a clearer error message improves user experience.

🛡️ Proposed fix
 if msg['data'].startswith('mint:'):
     # In a real contract, you would restrict this to an owner address!
     # For this example, anyone can mint tokens to themselves.
-    amount = int(msg['data'].split(':')[1])
+    try:
+        amount = int(msg['data'].split(':')[1])
+    except (ValueError, IndexError):
+        raise Exception("Invalid mint format. Use: mint:<amount>")
     if amount <= 0:
🧰 Tools
🪛 Ruff (0.15.15)

[error] 9-9: Undefined name msg

(F821)


[error] 12-12: Undefined name msg

(F821)


[warning] 14-14: Create your own exception

(TRY002)


[warning] 14-14: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/stablecoin.py` around lines 9 - 14, The mint payload parsing uses
int(msg['data'].split(':')[1]) without protection; wrap the parsing in a
try/except around the 'mint:' branch (the code that sets amount from
msg['data']), catch ValueError and IndexError, and raise a clear Exception like
"Invalid mint payload, expected 'mint:<positive-integer>'" so callers get a
helpful error; then proceed to the existing amount <= 0 check as before.


sender = msg['sender']
storage[sender] = storage.get(sender, 0) + amount
storage['total_supply'] = storage.get('total_supply', 0) + amount

elif msg['data'].startswith('transfer:'):
parts = msg['data'].split(':')
if len(parts) != 3:
raise Exception("Invalid transfer format")

to_address = parts[1]
amount = int(parts[2])

if amount <= 0:
raise Exception("Amount must be positive")
Comment on lines +20 to +29

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add error handling for amount parsing.

Line 26 calls int(parts[2]) without a try/except, which can raise ValueError if the amount is not a valid integer (e.g., transfer:addr:abc). While the contract will fail safely (charging gas), a clearer error message improves user experience.

🛡️ Proposed fix
     to_address = parts[1]
-    amount = int(parts[2])
+    try:
+        amount = int(parts[2])
+    except ValueError:
+        raise Exception("Invalid amount format. Must be an integer.")
     
     if amount <= 0:
🧰 Tools
🪛 Ruff (0.15.15)

[error] 20-20: Undefined name msg

(F821)


[error] 21-21: Undefined name msg

(F821)


[warning] 23-23: Create your own exception

(TRY002)


[warning] 23-23: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 29-29: Create your own exception

(TRY002)


[warning] 29-29: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/stablecoin.py` around lines 20 - 29, The transfer amount parsing
currently does int(parts[2]) without validation which can raise ValueError;
update the transfer handling (the branch that checks
msg['data'].startswith('transfer:') and uses parts, to_address and amount) to
wrap the int conversion in a try/except, catching ValueError and raising a clear
Exception/ValueError like "Invalid transfer amount: must be an integer, got
'<value>'", then retain the existing amount <= 0 check to reject non-positive
amounts; ensure the error message includes the original parts[2] for easier
debugging.


sender = msg['sender']
sender_balance = storage.get(sender, 0)

if sender_balance >= amount:
storage[sender] -= amount
storage[to_address] = storage.get(to_address, 0) + amount
else:
raise Exception("Insufficient token balance")

else:
raise Exception("Unknown command. Valid commands: mint:<amount>, transfer:<to>:<amount>")
Loading