Introduction

Zink is a powerful toolchain revolutionizing smart contract development on Ethereum, it is designed to bridge the gap between WebAssembly and the Ethereum Virtual Machine.

At its core, zinkc, the Zink compiler, takes WASM bytecode as the source code and transforms it into EVM bytecode for seamless execution on the Ethereum blockchain. This optimization ensures efficient and reliable execution of smart contracts on Ethereum.

This guide is intended to serve a number of purposes and within you’ll find:

…and more! The source for this guide lives on GitHub and contributions are welcome!

Tutorial

This tutorial walks through creating a simple add-two program and compiles it with description how everything works.

For the overall instructions:

# Install zink toolchain
cargo install zinkup

# Create project
elko new add-two

# Compile it
cd add-two && elko build
ls target/add-two.bin

Creating Zink Project

For creating a zink project, we need to install the zink toolchain zinkup from crates.io first, the package manager elko will be installed along with other tools:

cargo install zinkup
elko -h

Now, let’s create a zink project:

elko new my-awesome-contract
Created zink project `my-awesome-contract`

the Zink projects are based on the cargo projects, you can install dependencies you need with cargo, the basic Cargo.toml will be like:

# ...

[lib]
crate-type = [ "cdylib" ]

[dependencies]
zink = "0.1.0"

# ...

open my-awesome-contract/src/lib.rs

//! my-awesome-project
#![no_std]

// For the panic handler.
#[cfg(not(test))]
extern crate zink;

/// Adds two numbers together.
#[no_mangle]
pub extern "C" fn addition(x: u64, y: u64) -> u64 {
    x + y
}

you’ll see a standard WASM library in rust:

  1. #![no_std] means we don’t need the std library in this project.
  2. extern crate zink is for importing the panic handler from library zink for this project.
  3. #[no_mangle] is for exporting function addition to WASM, and this will be one the methods of your contracts.

Compiling Zink Project

We have got a zink project after creating-zink-project, now it’s time to compile it to EVM bytecode!

# Enter our project
cd my-awesome-project

# Build the project
elko build

# Check the outputs
ls target/zink
my-awesome-project.wasm my-awesome-project.bin

you’ll see a my-awesome-project.bin file under target/zink, and that’s it!

How it works?

first, elko compiles the cargo project to WASM with:

cargo b --target wasm32-unknown-unknown --release

then, there will be some logic inside elko, running wasm-opt for our output WASM binary

# if you have wasm-opt installed on your machine, you can try the same
mkdir -p target/zink
wasm-opt -O4 target/wasm32-unknown/unknown/release/my-awesome-project.wasm -o target/zink/my-awesome-project.wasm

finally we use zinkc to compile the wasm to EVM bytecode:

# For reproducing it in your command line
zinkc target/zink/my-awesome-project.wasm
mv my-awesome-project.bin target/zink

Future plans (TODO)

  1. Generate the ABI as well.
  2. Add command for deploying the bytecode to EVM chain with RPC endpoints.
  3. Test suite

Examples

This chapter provides various zink examples in rust:

nameknowledgesdescription
add-twoparamsbasic program in zink
fibonaccicalls, recursion, if-blockrecursion implementation
logloglog APIs
selectwasm built-in functionsprogram with extra instruction select from WASM
storagestoragestorage operations

AddTwo

//! Addition example.
#![no_std]

// for the panic handler.
#[cfg(not(test))]
extern crate zink;

/// Adds two numbers together.
#[no_mangle]
pub extern "C" fn addition(x: u64, y: u64) -> u64 {
    x + y
}

A basic addition program in zink

(module
    (func (param i32) (param i32) (result i32)
    (local.get 0)
    (local.get 1)
    (i32.add)
    )
)

Requires:

  • Get params from locals
  • Process basic operand
  • Return data from the result type
6000356020350160005260206000f3

Fibonacci

//! fibonacci example.
#![no_std]

// for the panic handler.
#[cfg(not(test))]
extern crate zink;

/// Calculates the nth fibonacci number.
#[no_mangle]
pub extern "C" fn fibonacci(n: usize) -> usize {
    recursion(n)
}

/// Calculates the nth fibonacci number using recursion.
#[no_mangle]
pub extern "C" fn recursion(n: usize) -> usize {
    if n < 2 {
        n
    } else {
        recursion(n - 1) + recursion(n - 2)
    }
}

A recursion example, complex in bytecode

(module
  (type (;0;) (func (param i32) (result i32)))
  (func (;0;) (type 0) (param i32) (result i32)
    local.get 0
    call 1)
  (func (;1;) (type 0) (param i32) (result i32)
    (local i32)
    local.get 0
    i32.const 2
    i32.ge_u
    if  ;; label = @1
      loop  ;; label = @2
        local.get 0    ;; 1
        i32.const 1    ;; 2
        i32.sub        ;; 1
        call 1         ;; 1
        local.get 1    ;; 2
        i32.add        ;; 1
        local.set 1    ;; 0
        local.get 0    ;; 1
        i32.const 2    ;; 2
        i32.sub        ;; 1
        local.tee 0    ;; 1
        i32.const 1    ;; 2
        i32.gt_u       ;; 1
        br_if 0 (;@2;) ;; 2 -> 0
      end
    end
    local.get 0
    local.get 1
    i32.add))

A more complex implementation of locals ( params + local variables) is introduced in this example, control flow br_if and loop are compiled as well.

600035586010565b60005260206000f35b906000816002600190031015603d575b8160019003586010565b8101905081600290038092506001106020575b8181019150509060040156

Log

//! Addition example.
#![no_std]

// for the panic handler.
#[cfg(not(test))]
extern crate zink;

use zink::Event;

/// A `Ping` event.
#[derive(Event)]
struct Ping;

#[no_mangle]
pub extern "C" fn log1() {
    Ping.log1(b"pong");
}

The log API of zink is derived by the trait Event which provides methods log0, log1, log2, log3, log4. We current only supports static bytes in this API.

(module
  (type (;0;) (func))
  (type (;1;) (func (param i32 i32 i32 i32)))
  (import "evm" "log1" (func (;1;) (type 1)))
  (import "env" "memory" (memory (;0;) 17))
  (func (;1;) (type 0)
    i32.const 1048576
    i32.const 4
    i32.const 1048580
    i32.const 4
    call 0)
  (export "log1" (func 1))
  (data (;0;) (i32.const 1048576) "Pingpong"))

The static byte array will be compiled to the data section of wasm, zinkc gets it from the data section then process it to the logging interfaces.

63706f6e676350696e6760005260206000a15f5ff3

Select

//! if-else example.
#![no_std]

// for the panic handler.
#[cfg(not(test))]
extern crate zink;

/// Simple if-else condition
#[no_mangle]
pub extern "C" fn if_else(x: u64, y: u64) -> u64 {
    if x > y {
        x
    } else {
        y
    }
}

Code block selecting value with if-else will be compiled to instruction select in WASM

(module
  (type (;0;) (func (param i64 i64) (result i64)))
  (func $if_else (type 0) (param i64 i64) (result i64)
    local.get 0
    local.get 1
    local.get 0
    local.get 1
    i64.gt_u
    select))

Since EVM bytecode doesn’t have similar instruction, we have to implement it ourselves, the solution is introduce a select function in the extra code section provided by zink compiler, jump to there and jump back just like calling a real function.

60003560203560003560203510589190601c575b60005260206000f35b5060060156

Storage

// for the panic handler.
#[cfg(not(test))]
extern crate zink;

use zink::Storage;

/// It gets expanded to 'Counter' struct
/// that implements zink::Storage trait
/// (::set and ::get)
///
/// Storage key is taken based on macro order
/// (e.g this macro is first and only in this project,
/// so it will take 0x0 contract storage key)
#[zink::storage]
pub type Counter = i32;

/// Set value to storage and get it
#[no_mangle]
pub unsafe extern "C" fn set_and_get(value: i64) -> i64 {
    Counter::set(value);
    Counter::get()
}

Simple storage IO for numbers.

(module
  (type (;0;) (func (param i64) (result i64)))
  (type (;1;) (func (param i64 i64)))
  (import "zink" "sload" (func (;0;) (type 0)))
  (import "zink" "sstore" (func (;1;) (type 1)))
  (func (type 0) (param i64) (result i64)
        i64.const 0
        local.get 0
        call 1
        i64.const 0
        call 0))

Set and get number parameter with storage here.

60006000355891601b565b600058906021565b60005260206000f35b55600501565b549060050156

Command Line Tool

The zink toolchain are gathered in zinkup

You can install all of the components directly with:

cargo install zinkup

For installing only specified binaries:

cargo install zinkup --features elko,zinkc

Available binaries:

NameDescription
elkoZink's package manager
zinkcThe zink compiler

elko - Zink’s package manager

Installation

cargo install zinkup --features elko

Usages

elko
Package manager of zink.

Usage: elko [OPTIONS] <COMMAND>

Commands:
  new    Create a new zink project
  build  Build zink project to EVM bytecode
  help   Print this message or the help of the given subcommand(s)

Options:
  -v, --verbose...  Verbose mode (-v, -vv, -vvv, etc.)
  -h, --help        Print help
  -V, --version     Print version

The zink compiler

Installation

cargo install zinkup --features zinkc

Usage

The Zink Compiler

Usage: zinkc [OPTIONS] <INPUT>

Arguments:
  <INPUT>  The path of the wasm file

Options:
  -o, --output <OUTPUT>  Write output to <filename>
  -v, --verbose...       Verbose mode (-v, -vv, -vvv, etc.)
  -h, --help             Print help
  -V, --version          Print version

Style Guide

This style guide is for writing zink projects with rust as the source language.

We don’t need any styles for now, just writing anything you want, and the zinkc compiler will handle the rest of everything.

For the futures plans of events, selector, storage and ... plz keep tuned, they will be implemented in v0.2.0 : )

Compiler

The chapter illustrates the design of the zink compiler, so mostly, we are talking about wat and EVM bytecode Mnemonic here:

Arithmetic

The arithmetic operators have a lot of differences between WASM and EVM bytecode, all of the operand requires the order of the stack are reserved…

Sub, Div, Mod and Bitwise Operand

i32.const 2       ;; PUSH1 0x02
i32.const 1       ;; PUSH1 0x01
sub               ;; SWAP1
                  ;; SUB

This SWAP1 sticks to all of these reversed order instructions, will introduce macros to optimize it in v0.3.0.

Comparison

The order of comparison are reversed as well, but however, they are paired!

i32.const 1    ;; PUSH1 0x01
i32.const 0    ;; PUSH1 0x00
gt             ;; LT

This is insane, but works perfectly, don’t think too much about it, focus on if the output is 0 or 1 ;)

Calls

The function calls are compiled by order in zink, the first call in order (index 0) will be the main function ( to be updated ).

Functions

There are only internal functions and external functions in zink project for now.

Internal Functions

The parameters of the internal functions will be queued to the locals of the them and taking the first n stack for storing them.

External Functions

Same as internal functions, will be updated once have the design of selector in v0.2.0

Extended Functions

We have also introduces extended functions inside the compiler for complete the difference between EVM bytecode and WASM, see the implementation select as example.

Main Function

You may never meet this because it is embedded in the compiled bytecode, but it is the entry of zink programs.

It takes parameters from the calldata by order, for loading 32-byte parameters, it will process

// parameter 1
PUSH1 0x00
calldataload

// parameter 2
PUSH1 0x20
calldataload

Layout

Each function in zink is started with JUMPDEST in the layout of the bytecode for the insane jumping…

Each function call’s stack starts with PC which stores the last active program counter for the program for jumping back to the main process since the callee functions could be called by any functions but not only one.

There is a tricky problem that how to detect the last pc before jumping, for solving this, zinkc registers the original PC to the jump table when meeting jumps and relocates them after compiling all functions.

Example Addition

(module
  (func (export "main") (param i32) (param i32) (result i32)
    (call $add (local.get 0) (local.get 1))
  )

  (func $add (param i32 i32) (result i32)
    (local.get 0)
    (local.get 1)
    (i32.add)
  )
)

Let’s assume we are calling an add function with parameters 1, 1 and now we are at the first byte right before it:

/* 0x00 */  PUSH1 0x01    // push the first parameter on the stack
/* 0x02 */  PUSH1 0x01    // push the second  parameter on the stack
/*      */                //
/*      */                //
/* 0x04 */  pc            // the first byte before calling the callee function
/*      */                //
/*      */                //
/* 0x05 */  PUSH1 0x42    // This 0x42 will be reloacted by `zinkc`
/*      */                //
/*      */                //
/* 0x07 */  jump          // jump to the callee function
/*      */                //
/*      */                //
/* 0x08 */  jumpdest      // the pc for jumping back from the callee function.
/*      */                //
/*      */                // the rest logic of the main process.
/*      */                //
/*      */                //
/* 0x42 */  jumpdest      // the first byte of the callee function
/*      */                //
/*      */                //
/* 0x43 */  add           // for the current stack: [PC, 0x02]
/*      */                //
/*      */                //
/* 0x44 */  SWAP1         // shift the stored PC to the top of the stack
/*      */                //
/*      */                //
/* 0x45 */  PUSH1 0x04    // the jumpdest is the original pc + 4 bcz we have
/* 0x47 */  add           // `push1`, `0x42`, `jump`, `jumpdest` queued after
/*      */                // `pc`.
/*      */                //
/*      */                //
/* 0x48 */  jump          // This 0x07 will be reloacted by `zinkc`

Control Flow

EVM doesn’t have instructions for the custom control flows, however zink implements them with JUMPI and JUMP, which includes:

  • if
  • block
  • loop
  • else
  • select
  • br
  • br_if
  • br_table

If-Else

The beginning of an if construct with an implicit then block, plus and else block.

The basic logic is, if non-zero, enter the if block, otherwise jump to the else block or the end of the if condition.

(if (result i32)
  (local.get 0)
  (then (i32.const 7))
  (else (i32.const 8)))

The expected result is

InputResult
17
08

so in the compiled bytecode, the code snippet above will be

PUSH1 0x00       // Load the params at 0x00
calldataload

iszero           // if is zero, jump to 0x0c, the else block.
PUSH1 0x0c
jumpi

push1 0x07       // if is non-zero, enters the if block.
                 // push 0x07 on stack.

PUSH1 0x0f       // jump to the end of the else block.
jump

jumpdest         // destination of the else block, push 0x08
push1 0x08       // on stack.


jumpdest         // the end of the else block.


PUSH1 0x00       // pack the result and return...
mstore
PUSH1 0x20
PUSH1 0x00
return

Select

The select (0x1B) instruction comes from WebAssembly, it selects one of its first two operands based on whether its third operand is zero or not.

Simple rust conditions in rust will be compiled to select.

pub extern "C" fn if_else(x: u64, y: u64) -> u64 {
    if x > y {
        x
    } else {
        y
    }
}

As we can see in the example above, we simply returns the bigger number from the 2 parameters, the logic in the two blocks of if-else is explicit direct, that will be compiled to select.

(module
  (type (;0;) (func (param i64 i64) (result i64)))
  (func $if_else (type 0) (param i64 i64) (result i64)
    local.get 0
    local.get 1
    local.get 0
    local.get 1
    i64.gt_u
    select))

Since EVM doesn’t have instruction like select, we need to provide it ourselves in our implementation like an external function, if zero pop the value on the top of the stack.

const SELECT: [OpCode; 6] = [
    OpCode::JUMPDEST,
    OpCode::POP,
    OpCode::PUSH1,
    OpCode::Data(0x06),
    OpCode::ADD,
    OpCode::JUMP,
];

In the compiled code, we need to combine this function select with jumpi in EVM.

PUSH1 0x00      // Load the parameters.
calldataload

PUSH1 0x20
calldataload

PUSH1 0x00
calldataload

PUSH1 0x20
calldataload

lt               // Compiled to `lt` because of the result of this
                 // instruction is oppsited between EVM and WASM.

pc
swap2            // shift
swap1
PUSH1 0x1c
jumpi            // `jumpi` for the if condition.
JUMPDEST

PUSH1 0x00       // Returns the value.
mstore
PUSH1 0x20
PUSH1 0x00
return

JUMPDEST         // Function select starts here.
pop
PUSH1 0x06
add
jump

Locals

There are two usages of locals in zink.

  1. The parameters of functions are loaded as locals.
  2. local defined variables in functions.
fn paramslocal variables
stacklocals[..n]locals[n..]

Function Parameters

Let’s go through the add-two example:

(module
    (func (param (;0;) i32) (param (;1;) i32) (result i32)
    (local.get 0)
    (local.get 1)
    (i32.add)
    )
)

(param (;0;) i32) and (param (;1;) i32) will be pushed to the function locals with index 0 and index 1 with their type i32 recorded.

zinkc gets the defined local at index 0 when reaching (local.get 0), at index 1 for (local.get 1), for example, for (local.get 0), it will be translated to:

push1 0x00
calldataload

for (local.get 1), that would be

push1 0x20
calldataload

You may have problem why we PUSH1 0x20 while getting local at index 1, the answer is that this offset is calculated by the size of the parameters.

The CALLDATALOAD operator has stack input i and output data[i] while data[i] is a 32-byte value starting from the given offset of the calldata, so the minimal size of our types will be 32-byte, therefore, we align all types sizes to 32-byte in zinkc.

WARN: We don’t care about the originals offset of the parameters in WASM bcz we will serialize them into our locals and calculate the offsets on our own when need anyway.

typesizealigned size
i32[u8;4][u8;32]
i64[u8;8][u8;32]

It is a waste of resources but sadly this is also how EVM works ))

Local Variables

The locals variables will take the stack items right after the function parameters, for example:

(func (result i32)
  (local i32)
  i32.const 42
  local.set 0
  local.get 0)

In the program above, we set and get 42 to local variable 0 and returns it.

While compiling this function, zinkc will push local variable 0 on the stack with an initializing value 0 first, getting with dup and setting with swap and drop.

PUSH1 0x00       // initializing value 0 for local 0
                 //
                 //
PUSH1 0x28       // push value 42 on stack, the current stack is [0, 42]
                 //
                 //
SWAP1            // swap the value on the top of the stack to local 0 and
DROP             // drop the previous value of it for cleaning stack, `swapn`
                 // is calculated by `zinkc`. current stack: [42]
                 //
                 //
DUP1             // dup the value of local 0 and push it on the top of the
                 // stack, `dupn` is calculated by `zinkc`.
                 //
                 //
DROP             // clean the stack before returning results.

As we can see, the usages of get and set is verbose with swap and dup, it is for adapting any usages but not necessary for all of them, however, we will introduce optimizer for this in v0.4.0!

Recursion

The main function of zink project doesn’t support recursion for now :)

The callee functions support, plz see fibonacci for example.

Stability

This project is still under active development, please DO NOT use it in production.

versiondescription
v0.1.0The MVP of the zink project.

v0.1.0 - MVP

The MVP of the zink project, provides various tools for developing EVM contracts with rust and WASM.

Binaries

namedescription
elkoZink’s package manager, can create and build zink project.
zinkcThe zink compiler, can compile simple wasm to EVM bytecode.

Components

namedescription
zinkgenZink code generator
zinkcZink compiler
zinkRust library for developing program with zink
zintBasic test utils including evm wrapper for testing usages
zinkupZink toolchain installer

Functionality

We provide basic functionalities in v0.1.0 to verify thoughts, the final target of it is example fibonaaci, which means, everything used in the fibonacci example now works!

For supporting nearly everything, plz keep tuned for v0.3.0.

Arithmetic

add, sub, mul are available now, plus all comparison operand like gt, lt, ge, le, bitwise also have implementations operators like shr require the order of the stack will have bugs.

Locals

The compilation of locals currently works without any hardcode, ideally, we don’t need to refactor it in the future!

Calls

Same as locals, works without any hardcode, but some logic related to the jump table need to be refactored after introducing selector.

Control Flow

if, else, block, loop, br_if now works without any hardcode, need to add br_table, select… to align wasm MVP in the future releases.

Security

TBS

Mapping of Instructions

Type Conversions

WASM have i32, i64, f32, f64 as number types while EVM bytecode only supports arithmetic operations for 256-bits integers.

TODO: Add more risk conditions.

Stack Operations

TBA

Memory Operations

The memory related operations in WASM are dangerous for Zink’s implementation.

WASM is using 32-bits offsets from the MVP spec while EVM is using 256-bits offsets, so it may cause memory overwrite problems.

The instructions need to be checked:

  • i32.store
  • i64.store
  • f32.store
  • f64.store
  • i32.store8
  • i64.store8
  • i32.store16
  • i64.store16
  • memory.size
  • memory.grow

TODO: check if it is possible to manage this issue with handling memory.size and memory.grow in a proper way.

Benchmarks

This chapter terms to record the benchmarks of zink projects comparing with solidity and vyper.

We are not going to compare with yul or huff since we are on different level, however, if zink is even faster than yul or huff in some cases, don’t be surprised, we are born with high performance.

NOTE

since we haven’t implemented the selector logic yet, the bytecode of zink is a bit shorter, similar in the gas cost, the main reason that zink contracts cost less gas is that we are currently missing conditions for situations that are more complex, we will get a more actual answer of these after the release of v0.2.0.

Fibonacci

Benchmarks for fibonacci.

Vyper is not included since it doesn’t support cyclic function call :(

Gas Cost

fib(n)ZinkSolidity@0.8.21
0110614
1110614
22621322
34142030
47183446
511745570

Runtime Code

zinksolidity
1461052

zink

//! Zink fibonacci recursion

#[no_mangle]
pub extern "C" fn fib(n: usize) -> usize {
    if n < 2 {
        n
    } else {
        recursion(n - 1) + recursion(n - 2)
    }
}
600035586010565b60005260206000f35b906000816002600190031015603d575b8160019003586010565b8101905081600290038092506001106020575b8181019150509060040156

solidity

/**
 * Solidity fibonacci recursion
 **/

function fib(uint n) public view returns (uint) {
  if (n < 2) {
    return n;
  } else {
    return fib(n - 1) + fib(n - 2);
  }
}
608060405234801561001057600080fd5b506004361061002b5760003560e01c8063c6c2ea1714610030575b600080fd5b61004a600480360381019061004591906100ea565b610060565b6040516100579190610126565b60405180910390f35b60006002821015610073578190506100aa565b6100886002836100839190610170565b610060565b61009d6001846100989190610170565b610060565b6100a791906101a4565b90505b919050565b600080fd5b6000819050919050565b6100c7816100b4565b81146100d257600080fd5b50565b6000813590506100e4816100be565b92915050565b600060208284031215610100576100ff6100af565b5b600061010e848285016100d5565b91505092915050565b610120816100b4565b82525050565b600060208201905061013b6000830184610117565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600061017b826100b4565b9150610186836100b4565b925082820390508181111561019e5761019d610141565b5b92915050565b60006101af826100b4565b91506101ba836100b4565b92508282019050808211156101d2576101d1610141565b5b9291505056fea2646970667358221220f28552ff642c48025f3617233333427ae50d06ce8b168d3e3e9c18f0cf9bc34d64736f6c63430008120033

Log

Zink only supports static byte array for log arguments for now, see issue #129 for more details.

ZinkVyper@0.3.9Solidity@0.8.21
Gas Cost103127772894
Runtime Code42632774

zink

//! Addition example.
#![no_std]

// for the panic handler.
#[cfg(not(test))]
extern crate zink;

use zink::Event;

/// A `Ping` event.
///
/// TODO: generate this with proc-macro.
struct Ping;

/// TODO: generate this with proc-macro.
impl Event for Ping {
    const NAME: &'static [u8] = b"Ping";
}

#[no_mangle]
pub extern "C" fn log1() {
    Ping.log1(b"pong");
}
63706f6e676350696e6760005260206000a15f5ff3

vyper

event Ping:
  name: String[4]
  topic1: String[4]

@external
def l():
  log Ping("Ping", "pong")
6003361161000c57610127565b5f3560e01c3461012b5763ece866b98118610125577fcf8d08d4ab9d61004e3c20715af5b44c3badc3d3f41ddccbedbef447355ebff460408060c05260046040527f50696e670000000000000000000000000000000000000000000000000000000060605260408160c00181516020830160208301815181525050808252508051806020830101601f825f03163682375050601f19601f8251602001011690509050810190508060e05260046080527f706f6e670000000000000000000000000000000000000000000000000000000060a05260808160c00181516020830160208301815181525050808252508051806020830101601f825f03163682375050601f19601f82516020010116905090508101905060c0a1005b505b5f5ffd5b5f80fda165767970657283000309000b

solidity

// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;

contract Test {
  event Ping(string name, string topic);

  function l() public {
    emit Ping("Ping", "pong");
  }
}
608060405234801561001057600080fd5b506004361061002b5760003560e01c8063ece866b914610030575b600080fd5b61003861003a565b005b7fcf8d08d4ab9d61004e3c20715af5b44c3badc3d3f41ddccbedbef447355ebff46040516100679061011a565b60405180910390a1565b600082825260208201905092915050565b7f50696e6700000000000000000000000000000000000000000000000000000000600082015250565b60006100b8600483610071565b91506100c382610082565b602082019050919050565b7f706f6e6700000000000000000000000000000000000000000000000000000000600082015250565b6000610104600483610071565b915061010f826100ce565b602082019050919050565b60006040820190508181036000830152610133816100ab565b90508181036020830152610146816100f7565b905091905056fea26469706673582212201af2a7b1c1d4743d1d089d3eaa1bbd6ecb4186fc95a43320108b18882d7a6dfc64736f6c63430008120033

Storage

Result for a simple storage IO.

Have to say Vyper is super on this even it contains the logic of function selector!

Gas Cost

ZinkVyper@0.3.9Solidity@0.8.21
222372234527738

The gas costs here are measured by transaction cost + execution cost, for example, the transaction of this function in solidity is 24120, and 2916 for the execution cost, 27738 in total.

Since revm doesn’t support this and we haven’t implemented the constructor yet, we don’t have the separated costs for zink for now ))

Issues: zink-lang/zink#102, bluealloy/revm#619

Runtime Code

zinkvypersolidity
42204724

zink

/// TODO: generate this storage interface with proc macro.
struct Counter;

impl Counter {
    fn get() -> i64 {
        unsafe { sload(0) }
    }

    fn set(value: i64) {
        unsafe { sstore(0, value) }
    }
}

/// Set value to the storage and get it.
#[no_mangle]
pub unsafe extern "C" fn set_and_get(value: i64) -> i64 {
    Counter::set(value);
    Counter::get()
}
6000600035589155600058905460005260206000f3

vyper

n: public(int256)

@external
def sg(_n: int256) -> int256:
  self.n = _n
  return self.n
6003361161000c57610051565b5f3560e01c3461005557632e52d606811861002c575f5460405260206040f35b63da48b556811861004f5760243610610055576004355f555f5460405260206040f35b505b5f5ffd5b5f80fda165767970657283000309000b

solidity

pragma solidity >=0.7.0 <0.9.0;

contract Storage {
  int public number;

  function sg(int n) public returns (int) {
    number = n;
    return number;
  }
}
608060405234801561001057600080fd5b50600436106100365760003560e01c80638381f58a1461003b578063da48b55614610059575b600080fd5b610043610089565b60405161005091906100bb565b60405180910390f35b610073600480360381019061006e9190610107565b61008f565b60405161008091906100bb565b60405180910390f35b60005481565b6000816000819055506000549050919050565b6000819050919050565b6100b5816100a2565b82525050565b60006020820190506100d060008301846100ac565b92915050565b600080fd5b6100e4816100a2565b81146100ef57600080fd5b50565b600081359050610101816100db565b92915050565b60006020828403121561011d5761011c6100d6565b5b600061012b848285016100f2565b9150509291505056fea264697066735822122052e14a565911c984f75788fb539e44d7692065628b2042665fc4abfc95e680d264736f6c63430008120033

Contributing

We’re excited to work on Zink together with you! this guide should help you get up and running with Zink development!

Join Our Chat

We chat about Zink development on telegram – join us!

If you’re having building Zink, aren’t sure why a test is failing, or have any other questions, feel free to ask on telegram.

As always, you’re more than welcome to open an issue too!

Join or Support the Team

Zink is developed by @clearloop with his spare time for now, really need more hands for the revolution : )

My handle on telegram is @clearloop as well, if you’d like to support this project or join the team, dm me plz!

Architecture of Zink

Compiler

The path of the compiler crate is /compiler, as its name, it’s the zink compiler zinkc, currently just a wrapper of zingen, the codegen library.

So if you want to contribute to the compiler, the code inside /compiler and /codegen will be interested for you!

Zink

Located at /zink, it is a rust library for compiling cargo project to zink program with provided apis, selector, events…any sugar or asm macro for zink will be embedded in this library.

Test utils

/zint is the testing library for zink projects, it is currently just a wrapper of evm, we need really a lot of features in this in v0.3.0.

Building

This section describes everything required to build and run zink.

Prerequisites

Before we can actually build Zink, we’ll need to make sure these things are installed first.

The Rust toolchain

Install the Rust toolchain here. This includes rustup, cargo, rustc, etc…

Add target wasm32-unknown-unknown

rustup target add wasm32-unknown-unknown

This is required for compiling our rust projects to wasm.

Build the zinkc CLI

cd cli
cargo b -p zinkup --release --features zinkc

The built executable will be located at target/release/zinkc.

Build examples

cd examples
cargo b --release

The built wasm binaries will be localted at examples/target/wasm32-unknown-unknown/realease/*.wasm, then, you can you zinkc to compile them to EVM bytecode!

Testing

This section describes how to run Zink’s tests and add new tests.

Before continuing, make sure you can build Zink successfully. Can’t run the tests if you can’t build it!

Running All Tests

cargo test --all

Adding New Tests for the Compiler

At the current stage, we are lack of the tests of the compiler, the tests of it are gathered in compiler/tests.

Each file under compiler/tests are named by instruction, for example add.rs, it includes the tests related to instruction ADD.

For adding a new test for compiler/tests/add.rs, we need to write a wasm program for it first, for example, the wasm program of the params test of ADD is located at compiler/wat/i32add/params.wat.

(module
    (func (param i32) (param i32) (result i32)
    (local.get 0)
    (local.get 1)
    (i32.add)
    )
)

In compiler/tests/add.rs:

#[test]
fn params() -> Result<()> {
    let bytecode = common::load("i32add", "params")?;

    // add(1, 2)
    let input = [1.to_bytes32(), 2.to_bytes32()].concat();
    let info = EVM::run(&bytecode, &input);

    assert_eq!(info.ret, [3.to_bytes32()].concat());
    Ok(())
}

We use common::load("i32add", "params") to load wat file to EVM bytecode from compiler/tests/wat/i32add/params.wat, execute with the EVM provided by zint, compare if the result is as expected, that’s it!

Appendix

The following sections contain reference material you may find useful in your Zink journey.

A - Optimizations

The optimizations of Zink projects now are mainly benefited from the optimizer of wasm – wasm-opt, for the details of it please check Binary Optimizations.