Skip to content
This repository was archived by the owner on Aug 17, 2026. It is now read-only.

feat(zink): introduce derive macro contract - #314

Merged
g4titanx merged 3 commits into
clearloop:mainfrom
g4titanx:cl/storage
Mar 21, 2025
Merged

feat(zink): introduce derive macro contract#314
g4titanx merged 3 commits into
clearloop:mainfrom
g4titanx:cl/storage

Conversation

@g4titanx

Copy link
Copy Markdown
Collaborator

resolves #288

i made some choices i think you should be aware of

storage type choice: Bytes32 Over String32

i opted for Bytes32 for fields like name and symbol instead of String32 (aliased to U256) due to API constraints in zink::primitives::U256. initially, I attempted to use String32 to align with the original ERC20 intent:

// Failed attempt with String32
let name = U256(Bytes32::from_slice_unchecked(&name_array)); // String32 is U256

this failed because:
Bytes32::from_slice_unchecked is not available in zink::primitives::Bytes32 (only empty() and eq() exist per impl_bytes! in bytes.rs)

U256 Private Field: U256(Bytes32) constructor isn’t public (pub struct U256(Bytes32) has a private field), and no from_big_endian or from_little_endian methods exist—only From<u64>, empty(), and max() are available.

switching to Bytes32 resolved this:

let name = Bytes32(name_array);

now, Bytes32 directly wraps [u8; 32] in non-WASM (via impl_bytes!), and the macro generates ERC20Name::get() as a simple sload, expecting a Bytes32 value. this avoids U256’s construction issues and matches the storage slot format ([u8; 32]).

bypassing contract calls to avoid revert

the test (test_storage) uses direct evm.storage() reads instead of contract calls like name():

let name_storage = evm.storage(address, ZintU256::from(0).to_le_bytes::<32>())?;
assert_eq!(name_storage.to_vec(), name_array.to_vec(), "Name storage mismatch");

this was necessary because runtime calls consistently reverted:

// Failed runtime call
let name_ret = evm.calldata(&contract.encode(&[b"name()".to_vec()])?).call(address)?;
assert_eq!(name_ret.ret, name_array.to_vec(), "Name mismatch: {name_ret:?}");
// Output: revert: Some("Empty from address")

revert Cause: the message "Empty from address" originates from _transfer:

fn _transfer(&self, from: Address, to: Address, value: U256) {
    if from.eq(Address::empty()) {
        zink::revert!("Empty from address");
    }
    // ...
}

unexpected trigger: name() (self.name() -> ERC20Name::get()) is a storage read (sload at slot 0), yet it reverts with this message. which i think (not sure) is a dispatcher issue in zinkc.

direct evm.storage() reads slot 0 (name), 1 (symbol), and 2 (total_supply) without WASM execution, confirming the macro’s construct() sets storage correctly. this isolates the problem to runtime dispatch, not storage setup.

the runtime revert is a blocker for testing core logic (transfer(), approve()). It seems zinkc misroutes external calls (e.g., name() selector 0x06fdde03) to _transfer (0xa9059cbb), unlike erc20.rs where separate structs work fine. could this be:

a dispatcher bug in selector::external or zinkc bytecode generation?

a problem with unified structs vs. separate ones?

i’d love your take on this—any ideas on why _transfer is hijacking calls? maybe a peek at the WASM output or dispatcher logic could shed light?

i’ve kept the test minimal to prove storage, but i really want to get runtime calls working.

@g4titanx
g4titanx requested a review from clearloop March 18, 2025 22:22

@clearloop clearloop left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

the struct of the contract looks neat!

seems like the contract creating logic works however the dispatcher got problems, I'm not pretty sure about the root case of it, but I'd suggest create another example with simple logic for debugging it, e.g. a getter,

  1. make sure the getter functions work
  2. apply this macro to other examples and see if there will be new bugs introduced
  3. add TODOs for the calls like _transfer because we haven't solved it yet

Comment thread examples/contract.rs Outdated
Comment thread zink/codegen/src/contract.rs Outdated
@g4titanx

Copy link
Copy Markdown
Collaborator Author

the struct of the contract looks neat!

seems like the contract creating logic works however the dispatcher got problems, I'm not pretty sure about the root case of it, but I'd suggest create another example with simple logic for debugging it, e.g. a getter,

  1. make sure the getter functions work
  2. apply this macro to other examples and see if there will be new bugs introduced
  3. add TODOs for the calls like _transfer because we haven't solved it yet

dispatcher bug in relation to unified structs still persist

@g4titanx
g4titanx requested a review from clearloop March 21, 2025 07:26

@clearloop clearloop left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

looks great! already enough as the first PR of the new interfaces!

btw would you mind creating a tracking list for the new interfaces which contains steps to fully support it? @g4titanx

@g4titanx

Copy link
Copy Markdown
Collaborator Author

looks great! already enough as the first PR of the new interfaces!

btw would you mind creating a tracking list for the new interfaces which contains steps to fully support it? @g4titanx

yeah, sure, i can open a new issue for this right?

@g4titanx

Copy link
Copy Markdown
Collaborator Author

opened #315 to track runtime dispatch issues with multi-field structs

@g4titanx
g4titanx merged commit 47b9750 into clearloop:main Mar 21, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Merge storage declarations into derive macro Storage

2 participants