#smart-contracts #contract #control-flow #crypto #language #ethereum #low-level

bin+lib meplang

一种对智能合约控制流有完全控制的 EVM 低级语言

3 个版本

0.1.8 2024 年 7 月 4 日
0.1.7 2024 年 1 月 6 日
0.1.6 2023 年 11 月 2 日

#45 in #control-flow

Download history 117/week @ 2024-07-01 3/week @ 2024-07-08 54/week @ 2024-07-29

54 个月下载量

MIT/Apache

120KB
2.5K SLoC

Meplang - 一种 EVM 低级语言

Meplang 是一种生成 EVM 字节码的低级编程语言。它为需要对其智能合约中的控制流有完全控制的开发者而设计。

Meplang 是一种低级语言,并不适合复杂智能合约的开发。对于这类开发,建议使用更高级的语言,如 Solidity 或 Yul。

请注意,Meplang 的工作仍在进行中,用户在部署之前应始终验证输出字节码是否符合预期。

安装

  1. 在您的机器上安装 Rust

  2. 运行以下命令从源代码构建 Meplang 编译器

cargo install --git https://github.com/meppent/meplang.git

要从源代码更新,再次运行相同的命令。

你好,世界!

这是一个简单的 Meplang 合约示例,它以字节形式返回 "Hello World!"

contract HelloWorld {
    block main {
        // copy the bytes into memory
        push(hello_world.size) push(hello_world.pc) push(0x) codecopy

        // return them
        push(hello_world.size) push(0x) return 
    }

    block hello_world {
        // "Hello World!" as bytes
        0x48656c6c6f20576f726c6421
    }
}

要编译保存为 hello_world.mep 的合约,运行以下命令

meplang compile -contract HelloWorld -input hello_world.mep

或简化的版本

meplang compile -c HelloWorld -i hello_world.mep

这将打印终端中的运行时字节码。要导出编译工件(包括运行时字节码),请使用参数 -o-output

meplang compile -c HelloWorld -i hello_world.mep -o hello_world.json

部署字节码

编译生成智能合约的运行时字节码。要获取部署合约,请使用辅助合约,并将其编译

contract Constructor {
    block main {
        // copy the bytes into memory
        push(deployed.size) push(deployed.pc) push(0x) codecopy

        // return them
        push(deployed.size) push(0x) return 
    }

    block deployed {
        &Deployed.code
    }
}

// the contract that will be deployed
contract Deployed {
    block main {
        // ...
    }
}

编译合约 Constructor 以获取合约 Deployed 的部署字节码。

基本语法

  • 使用关键字 contract 声明 合约。可以在单个文件中定义多个合约。一个合约可以在块中使用 &Contract.code 复制另一个合约的运行时字节码。
  • 在合约中使用关键字 block 声明一个块。块可以定义为 抽象(见后文),在 block 前使用关键字 abstract。合约的第一个操作码来自必要的块 main(或带有属性 #[main] 的块)。
  • 使用关键字 const 在合约中声明一个常量。常量只能用在块内的函数 push 中。
contract BalanceGetter {
    const BALANCE_OF_SELECTOR = 0x70a08231;
    const WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;

    #[assume(msize = 0x00)]
    #[assume(returndatasize = 0x00)]
    block main {
        push(BALANCE_OF_SELECTOR) push(0x) mstore // mem[0x1c..0x20] = 0x70a08231
        #[assume(msize = 0x20)]
        address push(0x20) mstore // mem[0x20..0x40] = address(this)
        #[assume(msize = 0x40)]
    
        // mem[0x00..0x20] = WETH.call{value: 0, gas: gas()}(mem[0x1c..0x20])
        //                 = WETH.balanceOf(address(this))
        push(0x20) push(0x) push(0x24) push(0x1c) push(0x) push(WETH) gas call
        #[assume(returndatasize = 0x20)]

        // the contract's balance in WETH is stored at mem[0x00..0x20]

        push(0x20) push(0x) return
    }
}
  • 在块内,可以使用任何操作码 除了 PUSH1 到 PUSH32 操作码(PUSH0 是允许的)。也可以直接使用原始字节码。可以使用函数 push 来推送一个值,该函数可以接受十六进制字面量、常量、非抽象 块程序计数器或大小作为参数。只有 push 函数内的值才会被编译器优化。
contract Contract {
    const MAGIC_NUMBER = 0xff;

    #[assume(msize = 0x00)]
    block main {
        push(MAGIC_NUMBER) push(0x) mstore
        #[assume(msize = 0x20)]

        push(0x20) // can be replaced by the opcode `msize` during the compilation
        0x6020     // won't be changed at the compilation

        push(end_block.size) // will be replaced by the actual size of the block `end_block`
        push(end_block.pc)   // will be replaced by the actual pc of the beginning of the block `end_block`
        jump
    }

    block end_block {
        jumpdest // do not forget to begin with jumpdest if we can jump on this block
        push(0x) push(0x) return
    }
}
  • 非抽象 块可以在另一个块中使用操作符 * 至多复制一次。一个 抽象 块可以在其他块中使用操作符 & 复制任意次数。因此,我们无法引用 抽象 块的 pcsize,因为它可能在字节码中出现多次,并且每次编译可能不同。
contract Contract {
    #[assume(msize = 0x00)]
    block main {
        callvalue &shift_right_20_bytes // will most certainly be compiled `callvalue push1 0x20 shr`

        push(0x) push(0x) mstore
        #[assume(msize = 0x20)]

        callvalue &shift_right_20_bytes // will most certainly be compiled `callvalue msize shr` because we assumed msize = 0x20.
        *end_block
    }

    abstract block shift_right_20_bytes {
        push(0x20) shr
    }

    block end_block {
        // no jumpdest here because we do not jump on this block, we copy it
        push(0x) push(0x) return
    } 
}
  • 存在许多 属性 以指导编译器。它们使用语法 #[ATTRIBUTE] 在合约、块或块内的行上声明。现有属性列表如下
    • assume 告诉编译器从这一点开始,一个操作码将推送一个定义的值到栈上。然后编译器可以用这些假设替换一些 push 操作码。
    • clear_assume 用于清除之前的假设。
    • main 如果主块没有命名为 main,则可以标记此属性。
    • last 告诉编译器该块必须放在字节码的末尾。
    • keep 告诉编译器即使该块未使用,也必须在字节码的某处保留该块。

更多合约示例可以在文件夹 examples 中找到。

未来功能

  • assert 属性可以对块程序计数器或合约大小施加条件。
  • 启发式算法以提高编译优化。
  • 合约继承。

依赖

~12MB
~197K SLoC