Skip to content

Commit

Permalink
[move][evm] Experiments for EVM Move Programming Model
Browse files Browse the repository at this point in the history
This PR contains some rough experiments for a possible programming model for EVM in Move. Check the README.md for details.

Closes: #10104
  • Loading branch information
wrwg authored and bors-libra committed Jan 6, 2022
1 parent c2688f9 commit 5af341e
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 3 deletions.
10 changes: 10 additions & 0 deletions language/evm/examples/Move.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "EvmExamples"
version = "1.5.0"

[addresses]
Std = "0x1"
Evm = "0x2"

[dependencies]
MoveStdlib = { local = "../../move-stdlib" }
21 changes: 21 additions & 0 deletions language/evm/examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
**Work in Progress**

This directory contains (a growing set of) examples of "Move-on-EVM", a programming model in Move for EVM.

- [Token.move](./sources/Token.move) contains an implementation of ERC20 which is intended to be compliant and callable
from other EVM code (Move or otherwise).
- [Faucet.move](./sources/Faucet.move) contains the faucet example from the Ethereum book.

The basic programming model is as follows:

- The module [Evm.move](./sources/Evm.move) contains the API of a Move contract to the EVM. It encapsulates access to
the transaction context and other EVM builtins.
- In the current model, *each Move EVM contract has its own isolated address space*. That is, `borrow_global` et. al
work on memory private to this contract. This reflects the setup of the EVM most naturally, where storage between
contracts cannot be shared apart from via accessor contract functions.
- In order to allow a contract to store to its own private memory, there is currently a pseudo function
`Evm::sign(addr)` which allows a contract to convert any address into a signer for its own private memory. Eventually,
we may want to remove the requirement to have a signer for move_to in the EVM context.
- Move EVM contracts use attributes to indicate the usage of structs for storage and events, and for functions to be
callable from other contracts. It is expected that there is some codegen of Move from these attributes. Specifically,
functions marked as `callable` have a generated API for cross-contract EVM call and delegate invocations.
27 changes: 27 additions & 0 deletions language/evm/examples/sources/Evm.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/// Module which provides access to EVM functionality, including information about the executing transaction.
////
/// This currently only represents a basic subset of what we may want to expose.
module Evm::Evm {

/// Returns the address of the executing contract.
public native fun self(): address;

/// Returns the address of the transaction sender.
public native fun sender(): address;

/// If this is a payable transaction, returns the value (in Wei) associated with it.
/// TODO: need u256
public native fun value(): u128;

/// Returns the balance, in Wei, of any account.
public native fun balance(addr: address): u128;

/// Transfers the given amount to the target account.
public native fun transfer(addr: address, amount: u128);

/// Emits an event. The type passed for `E` must be annotated with #[event].
public native fun emit<E>(e: E);

/// Creates a signer for the contract's address space.
public native fun sign(addr: address): &signer;
}
57 changes: 57 additions & 0 deletions language/evm/examples/sources/Faucet.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#[contract]
/// Faucet example in the Ethereum book.
module 0x42::Faucet {
use Evm::Evm::{sender, value, self, sign, balance, transfer, emit};
use Std::Errors;

#[storage]
struct State has key {
owner: address,
}

#[event]
struct WithdrawalEvent {
to: address,
amount: u128
}

#[event]
struct DepositEvent {
from: address,
amount: u128
}

#[create]
public fun create() {
move_to<State>(sign(self()), State{owner: sender()})
}

#[delete]
public fun delete() acquires State {
let state = borrow_global<State>(self());
assert!(sender() == state.owner, Errors::requires_address(0));
}

#[receive, payable]
public fun receive() {
emit(DepositEvent{from: sender(), amount: value()})
}

#[callable]
public fun withdraw(amount: u128) acquires State {
let state = borrow_global<State>(self());

// Don't allow to withdraw from self.
assert!(state.owner != self(), Errors::invalid_argument(0));

// Limit withdrawal amount
assert!(amount <= 100, Errors::invalid_argument(0));

// Funds must be available.
assert!(balance(self()) >= amount, Errors::limit_exceeded(0));

// Transfer funds
transfer(sender(), amount);
emit(WithdrawalEvent{to: sender(), amount})
}
}
145 changes: 145 additions & 0 deletions language/evm/examples/sources/Token.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
#[contract]
/// An implementation of ERC20.
module Evm::ERC20 {
use Evm::Evm::{sender, self, sign};
use Std::Errors;
use Std::Vector;

#[storage]
/// Represents the state of this contract. This is located at `borrow_global<State>(self())`.
struct State has key {
decimals: u8,
total_supply: u128,
}

#[storage]
/// Represents the state of an account managed by this contract, located at
/// `borrow_global<Account>(address)`. Notice that the storage of this resource
/// is private to this contract, as each EVM contract manages its own Move address space.
struct Account has key {
/// The balance value.
value: u128,
/// The allowances this account has granted to other specified accounts.
allowances: vector<Allowance>
}

/// How much a spender is allowed to use.
struct Allowance has store {
spender: address,
amount: u128,
}

#[create]
/// Constructor of this contract.
public fun create(initial_amount: u128, decimals: u8) {
// Initial state of contract
move_to<State>(sign(self()), State{decimals, total_supply: initial_amount});

// Initialize senders balance with initial amount
move_to<Account>(sign(sender()), Account{value: initial_amount, allowances: vector[]});
}

#[callable, view]
/// Returns the total supply of the token.
public fun total_supply(): u128 acquires State {
borrow_global<State>(self()).total_supply
}

#[callable, view]
/// Returns the balance of an account.
public fun balance_of(owner: address): u128 acquires Account {
borrow_global<Account>(owner).value
}

#[callable, view]
/// Returns the allowance an account owner has approved for the given spender.
public fun allowance(owner: address, spender: address): u128 acquires Account {
let allowances = &borrow_global<Account>(owner).allowances;
let i = index_of_allowance(allowances, spender);
if (i < Vector::length(allowances)) {
Vector::borrow(allowances, i).amount
} else {
0
}
}

#[callable]
/// Approves that the spender can spent the given amount on behalf of the calling account.
public fun approve(spender: address, amount: u128) acquires Account {
create_account_if_not_present(sender());
let allowances = &mut borrow_global_mut<Account>(sender()).allowances;
mut_allowance(allowances, spender).amount = amount
}

#[callable]
/// Transfers the amount from the sending account to the given account
public fun transfer(to: address, amount: u128) acquires Account {
assert!(sender() != to, Errors::invalid_argument(0));
do_transfer(sender(), to, amount)
}


#[callable]
/// Transfers the amount on behalf of the `from` account to the given account.
/// This evaluates and adjusts the allowance.
public fun transfer_from(from: address, to: address, amount: u128) acquires Account {
let allowances = &mut borrow_global_mut<Account>(from).allowances;
let allowance = mut_allowance(allowances, sender());
assert!(allowance.amount >= amount, Errors::limit_exceeded(0));
allowance.amount = allowance.amount - amount;
do_transfer(from, to, amount)
}

/// Helper function to perform a transfer of funds.
fun do_transfer(from: address, to: address, amount: u128) acquires Account {
create_account_if_not_present(from);
create_account_if_not_present(to);
let from_acc = borrow_global_mut<Account>(from);
assert!(from_acc.value >= amount, Errors::limit_exceeded(0));
from_acc.value = from_acc.value - amount;
let to_acc = borrow_global_mut<Account>(to);
to_acc.value = to_acc.value + amount;
}

/// Helper function to find the index of an existing allowance. Returns length of the passed
/// vector if not present.
fun index_of_allowance(allowances: &vector<Allowance>, spender: address): u64 {
let i = 0;
let l = Vector::length(allowances);
while (i < l) {
if (Vector::borrow(allowances, i).spender == spender) {
return i
};
i = i + 1;
};
return l
}

/// Helper function to return a mut ref to the allowance of a spender.
fun mut_allowance(allowances: &mut vector<Allowance>, spender: address): &mut Allowance {
let i = index_of_allowance(allowances, spender);
if (i == Vector::length(allowances)) {
Vector::push_back(allowances, Allowance{spender, amount: 0})
};
Vector::borrow_mut(allowances, i)
}

/// Helper function to create an account with a zero balance and no allowances.
fun create_account_if_not_present(owner: address) {
if (!exists<Account>(owner)) {
move_to<Account>(sign(owner), Account{value: 0, allowances: vector[]})
}
}

// ==============================================================================================================
// The following APIs will be automatically generated from the #[callable] function attributes. They
// constitute the EVM level contract API.

public native fun call_total_supply(contract: address): u128;
public native fun call_allowance(contract: address, owner: address, spender: address): u128;
public native fun call_approve(contract: address, spender: address, amount: u128);
public native fun call_transfer(contract: address, to: address, amount: u128);
public native fun call_transfer_from(contract: address, from: address, to: address, amount: u128);

// ... and the same as delegate_XXXX APIs?
}
6 changes: 3 additions & 3 deletions x.toml
Original file line number Diff line number Diff line change
Expand Up @@ -301,9 +301,9 @@ mark-changed = ["transaction-builder-generator"]
post-rule = "skip-rules"

[[determinator.path-rule]]
# On changes of diem-framework or move-stdlib, rerun the tests in move-prover.
globs = ["diem-move/diem-framework/**/*", "language/move-stdlib/**/*"]
mark-changed = ["move-prover"]
# Ignore files in the language/evm/examples tree (for now)
globs = ["language/evm/examples/**/*"]
mark-changed = []

[[determinator.path-rule]]
# On changes to files related to VS Code, build the move-analyzer package.
Expand Down

0 comments on commit 5af341e

Please sign in to comment.