Codegen for fully working ERC20 demo along with everything that was blocking it#1178
Codegen for fully working ERC20 demo along with everything that was blocking it#1178sbillig merged 11 commits intoargotorg:masterfrom
Conversation
9016525 to
780c724
Compare
There was a problem hiding this comment.
Pull request overview
This PR introduces comprehensive support for compiling a fully working ERC20 token contract to EVM bytecode. The implementation includes a new StorageMap type for key-value storage, several new intrinsics (keccak, caller, revert, addr_of), and significant compiler improvements to handle pointer type monomorphization and nested struct support.
Key Changes:
- Added
StorageMap<K, V>for Ethereum persistent storage with standard Solidity mapping layout - Implemented new intrinsics:
keccak256,caller(),revert(), andaddr_of() - Refactored pointer handling to use
MemPtr/StorPtrmarker types instead of runtimeAddressSpaceenum - Added
FieldPtrfor nested struct field access via pointer arithmetic - Implemented receiver address space tracking during monomorphization for proper method specialization
Reviewed changes
Copilot reviewed 51 out of 51 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| library/core/src/storage_map.fe | New StorageMap type for persistent key-value storage |
| library/core/src/ptr.fe | Refactored to Ptr trait with MemPtr/StorPtr marker types |
| library/core/src/option.fe | Fixed enum variant qualification (Self::None) |
| library/core/src/intrinsic.fe | Added keccak, caller, revert, addr_of intrinsics |
| library/core/src/enum_repr.fe | Updated to use Ptr trait with type parameters |
| crates/mir/src/monomorphize.rs | Added receiver_space tracking for method monomorphization |
| crates/mir/src/lower/*.rs | Implemented FieldPtr for nested structs and aggregate copying |
| crates/mir/src/ir.rs | Added AddressSpaceKind, FieldPtrOrigin, Revert terminator |
| crates/mir/src/dedup.rs | Fixed helper deduplication to only dedupe single instances |
| crates/codegen/src/yul/emitter/*.rs | Added codegen for new intrinsics and FieldPtr |
| crates/contract-harness/src/lib.rs | Added deploy() method for init bytecode execution |
| Test fixtures | Comprehensive tests for ERC20, nested structs, storage maps |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
|
|
||
| fn storage_slot(self, key: K) -> u256 { | ||
| // keccak256(key ++ slot) - standard Solidity mapping layout |
There was a problem hiding this comment.
The comment describes the Solidity mapping layout as keccak256(key ++ slot), but the implementation actually does keccak256(key ++ addr_of(self)) where addr_of(self) is written at position key_len instead of being concatenated. This appears correct for the Solidity layout, but the comment could be clearer. The actual layout is: the key is written at position 0, then the slot/address is written immediately after at position key_len, and then keccak256 is computed over the full key_len + 32 bytes.
| let mut iterations: usize = 0; | ||
| while let Some(func_idx) = self.worklist.pop_front() { | ||
| iterations += 1; | ||
| if iterations > 100_000 { | ||
| panic!("monomorphization worklist exceeded 100k iterations; possible cycle"); | ||
| } |
There was a problem hiding this comment.
The infinite loop protection using a hardcoded iteration limit of 100,000 is a reasonable safeguard, but the panic message could be more helpful by including diagnostic information such as the number of instances created or the last function being processed. This would help developers debug what caused the cycle.
| self.allowances.get(key: allowance_key(owner, spender)) | ||
| } | ||
|
|
||
| fn approve(mut self, owner: u256, spender: u256, value: u256) { |
There was a problem hiding this comment.
The approve implementation updates self.allowances directly to value without considering the existing allowance, which exposes the standard ERC20 allowance front‑running issue. An attacker who can front‑run a transaction that intends to change an allowance (e.g., from 10 to 20) can first spend the old allowance via transfer_from, after which the victim’s approve still sets the new higher value, effectively allowing the attacker to spend more than intended. To mitigate this, either require that existing allowances are first set to zero before a non‑zero update, or provide increase_allowance/decrease_allowance style APIs that adjust the current allowance instead of overwriting it blindly.
| fn approve(mut self, owner: u256, spender: u256, value: u256) { | |
| fn approve(mut self, owner: u256, spender: u256, value: u256) { | |
| let current = self.allowances.get(key: allowance_key(owner, spender)) | |
| if value != 0 && current != 0 { | |
| revert(0, 0) | |
| } |
b2737eb to
8ecb434
Compare
This PR introduces several foundational features that collectively enable compiling a complete, working ERC20 token contract to EVM bytecode.
Highlights
A fully working ERC20 token is now compilable end-to-end. The demo contract includes:
(address, address)New Features
StorageMap
A new
StorageMap<K, V>type in the core library provides key-value storage backed by Ethereum's persistent storage.@sbillig This isn't yet the nice Map abstraction that we eventually want but I didn't want to increase the scope of this PR any further.
New Intrinsics
keccak: Compute Keccak-256 hashescaller(): Accessmsg.senderequivalent for authorization checksrevert: Abort execution and revert state changesaddr_of: Extract the underlying address (memory offset or storage slot) from a pointer valueCompiler Improvements
Pointer Type Monomorphization
Storage vs. memory pointer decisions are now resolved during monomorphization rather than at codegen time. This produces cleaner, more predictable code paths and enables proper handling of storage-backed data structures like
StorageMap.Nested Struct Support
The MIR layer now correctly handles nested structs through
FieldPtroperations that compute byte offsets for accessing inner fields. This allows expressions likeouter.inner.valueto compile correctly by performing pointer arithmetic without loading intermediate struct values. This is probably not what we want long term but since this is touching the topic of references I wanted to keep it simple and leave the rest to @sbilligBug Fixes
Testing
The ERC20 contract compiles to valid bytecode and passes integration tests that exercise the full contract lifecycle including deployment, minting, transfers, and allowance management.
TLDR
fe/crates/codegen/tests/fixtures/erc20.fe
Lines 37 to 169 in b2737eb