From 368844ad2a28ec07848e3c0169cf2b83b579a2e8 Mon Sep 17 00:00:00 2001 From: Aaron Buchwald Date: Mon, 23 Nov 2020 20:46:35 -0500 Subject: Add native asset precompiled contracts for apricot release --- contracts/arc20.sol | 150 ++++++++++++++++++++++++++++++++++++++ contracts/nativeAssets.sol | 20 +++++ core/vm/contracts.go | 86 +++++++++++----------- core/vm/contracts_stateful.go | 165 ++++++++++++++++++++++++++++++++++++++++++ core/vm/evm.go | 22 +++--- core/vm/interpreter.go | 12 +++ core/vm/jump_table.go | 11 +++ core/vm/opcodes.go | 1 + eth/backend.go | 5 +- examples/arc20/main.go | 93 ++++++++++++++++++++++++ examples/multicoin/main.go | 17 +++-- params/config.go | 67 ++++++++++++++++- params/protocol_params.go | 2 + plugin/evm/export_tx.go | 3 +- plugin/evm/import_tx.go | 3 +- plugin/evm/vm.go | 9 +++ 16 files changed, 597 insertions(+), 69 deletions(-) create mode 100644 contracts/arc20.sol create mode 100644 contracts/nativeAssets.sol create mode 100644 core/vm/contracts_stateful.go create mode 100644 examples/arc20/main.go diff --git a/contracts/arc20.sol b/contracts/arc20.sol new file mode 100644 index 0000000..a7fc997 --- /dev/null +++ b/contracts/arc20.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.0 <0.8.0; + +import {NativeAssets} from "./NativeAssets.sol"; + +contract ARC20 { + + mapping (address => uint256) private _balances; + mapping(address => mapping(address => uint256)) private _allowances; + + uint256 private _assetID; + + uint256 private _totalSupply; + + string private _name; + string private _symbol; + uint8 private _decimals; + + constructor(string memory name_, string memory symbol_, uint8 decimals_, uint256 assetID_) public { + _name = name_; + _symbol = symbol_; + _decimals = decimals_; + _assetID = assetID_; + } + + /** + * @dev Returns the name of the token. + */ + function name() public view returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to represent the token. + */ + function decimals() public view returns (uint8) { + return _decimals; + } + + /** + * @dev Returns the total supply of `assetID` currently held by + * this contract. + */ + function totalSupply() public view returns (uint256) { + return _totalSupply; + } + + /** + * @dev Returns the balance of `account` held in this contract. + */ + function balanceOf(address account) public view returns (uint256) { + return _balances[account]; + } + + // Withdrawal/Deposit functionality + + /** + * @dev Acknowledges the receipt of some amount of an Avalanche Native Token + * into the contract implementing this interface. + */ + function deposit() public { + uint256 updatedBalance = NativeAssets.assetBalance(address(this), _assetID); + uint256 depositAmount = updatedBalance - _totalSupply; + assert(depositAmount >= 0); + + _balances[msg.sender] += depositAmount; + _totalSupply = updatedBalance; + emit Deposit(msg.sender, depositAmount); + } + + /** + * @dev Emitted when `value` tokens are deposited from `depositor` + */ + event Deposit(address indexed depositor, uint256 value); + + /** + * @dev Withdraws `value` of the underlying asset to the contract + * caller. + */ + function withdraw(uint256 value) public { + require(_balances[msg.sender] >= value, "Insufficient funds for withdrawal"); + + _balances[msg.sender] -= value; + _totalSupply -= value; + + NativeAssets.assetCall(msg.sender, _assetID, value, ""); + emit Withdrawal(msg.sender, value); + } + + /** + * @dev Emitted when `value` tokens are withdrawn to `withdrawer` + */ + event Withdrawal(address indexed withdrawer, uint256 value); + + /** + * @dev Returns the `assetID` of the underlying asset this contract handles. + */ + function assetID() external view returns (uint256) { + return _assetID; + } + + event Transfer(address indexed from, address indexed to, uint256 value); + + function transfer(address to, uint256 value) public returns (bool success) { + require(_balances[msg.sender] >= value, "insufficient balance for transfer"); + + _balances[msg.sender] -= value; // deduct from sender's balance + _balances[to] += value; // add to recipient's balance + emit Transfer(msg.sender, to, value); + return true; + } + + event Approval(address indexed owner, address indexed spender, uint256 value); + + function approve(address spender, uint256 value) + public + returns (bool success) + { + _allowances[msg.sender][spender] = value; + emit Approval(msg.sender, spender, value); + return true; + } + + function transferFrom(address from, address to, uint256 value) + public + returns (bool success) + { + require(value <= _balances[from], "From address has insufficient balance to transfer"); + require(value <= _allowances[from][msg.sender], "Insufficient allowance granted to sender"); + + _balances[from] -= value; + _balances[to] += value; + _allowances[from][msg.sender] -= value; + emit Transfer(from, to, value); + return true; + } + + function allowance(address owner, address spender) public view returns (uint256) { + return _allowances[owner][spender]; + } +} diff --git a/contracts/nativeAssets.sol b/contracts/nativeAssets.sol new file mode 100644 index 0000000..0656271 --- /dev/null +++ b/contracts/nativeAssets.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.0 <0.8.0; + +library NativeAssets { + address constant balanceAddr = 0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF; + address constant transferAddr = 0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE; + + function assetBalance(address addr, uint256 assetID) public returns (uint256) { + (bool success, bytes memory data) = balanceAddr.call(abi.encodePacked(addr, assetID)); + require(success, "assetBalance failed"); + return abi.decode(data, (uint256)); + } + + function assetCall(address addr, uint256 assetID, uint256 assetAmount, bytes memory callData) public returns (bytes memory) { + (bool success, bytes memory data) = transferAddr.call(abi.encodePacked(addr, assetID, assetAmount, callData)); + require(success, "assetCall failed"); + return data; + } +} diff --git a/core/vm/contracts.go b/core/vm/contracts.go index 9827bac..6fc6334 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -44,61 +44,61 @@ type PrecompiledContract interface { // PrecompiledContractsHomestead contains the default set of pre-compiled Ethereum // contracts used in the Frontier and Homestead releases. -var PrecompiledContractsHomestead = map[common.Address]PrecompiledContract{ - common.BytesToAddress([]byte{1}): &ecrecover{}, - common.BytesToAddress([]byte{2}): &sha256hash{}, - common.BytesToAddress([]byte{3}): &ripemd160hash{}, - common.BytesToAddress([]byte{4}): &dataCopy{}, +var PrecompiledContractsHomestead = map[common.Address]StatefulPrecompiledContract{ + common.BytesToAddress([]byte{1}): newWrappedPrecompiledContract(&ecrecover{}), + common.BytesToAddress([]byte{2}): newWrappedPrecompiledContract(&sha256hash{}), + common.BytesToAddress([]byte{3}): newWrappedPrecompiledContract(&ripemd160hash{}), + common.BytesToAddress([]byte{4}): newWrappedPrecompiledContract(&dataCopy{}), } // PrecompiledContractsByzantium contains the default set of pre-compiled Ethereum // contracts used in the Byzantium release. -var PrecompiledContractsByzantium = map[common.Address]PrecompiledContract{ - common.BytesToAddress([]byte{1}): &ecrecover{}, - common.BytesToAddress([]byte{2}): &sha256hash{}, - common.BytesToAddress([]byte{3}): &ripemd160hash{}, - common.BytesToAddress([]byte{4}): &dataCopy{}, - common.BytesToAddress([]byte{5}): &bigModExp{}, - common.BytesToAddress([]byte{6}): &bn256AddByzantium{}, - common.BytesToAddress([]byte{7}): &bn256ScalarMulByzantium{}, - common.BytesToAddress([]byte{8}): &bn256PairingByzantium{}, +var PrecompiledContractsByzantium = map[common.Address]StatefulPrecompiledContract{ + common.BytesToAddress([]byte{1}): newWrappedPrecompiledContract(&ecrecover{}), + common.BytesToAddress([]byte{2}): newWrappedPrecompiledContract(&sha256hash{}), + common.BytesToAddress([]byte{3}): newWrappedPrecompiledContract(&ripemd160hash{}), + common.BytesToAddress([]byte{4}): newWrappedPrecompiledContract(&dataCopy{}), + common.BytesToAddress([]byte{5}): newWrappedPrecompiledContract(&bigModExp{}), + common.BytesToAddress([]byte{6}): newWrappedPrecompiledContract(&bn256AddByzantium{}), + common.BytesToAddress([]byte{7}): newWrappedPrecompiledContract(&bn256ScalarMulByzantium{}), + common.BytesToAddress([]byte{8}): newWrappedPrecompiledContract(&bn256PairingByzantium{}), } // PrecompiledContractsIstanbul contains the default set of pre-compiled Ethereum // contracts used in the Istanbul release. -var PrecompiledContractsIstanbul = map[common.Address]PrecompiledContract{ - common.BytesToAddress([]byte{1}): &ecrecover{}, - common.BytesToAddress([]byte{2}): &sha256hash{}, - common.BytesToAddress([]byte{3}): &ripemd160hash{}, - common.BytesToAddress([]byte{4}): &dataCopy{}, - common.BytesToAddress([]byte{5}): &bigModExp{}, - common.BytesToAddress([]byte{6}): &bn256AddIstanbul{}, - common.BytesToAddress([]byte{7}): &bn256ScalarMulIstanbul{}, - common.BytesToAddress([]byte{8}): &bn256PairingIstanbul{}, - common.BytesToAddress([]byte{9}): &blake2F{}, +var PrecompiledContractsIstanbul = map[common.Address]StatefulPrecompiledContract{ + common.BytesToAddress([]byte{1}): newWrappedPrecompiledContract(&ecrecover{}), + common.BytesToAddress([]byte{2}): newWrappedPrecompiledContract(&sha256hash{}), + common.BytesToAddress([]byte{3}): newWrappedPrecompiledContract(&ripemd160hash{}), + common.BytesToAddress([]byte{4}): newWrappedPrecompiledContract(&dataCopy{}), + common.BytesToAddress([]byte{5}): newWrappedPrecompiledContract(&bigModExp{}), + common.BytesToAddress([]byte{6}): newWrappedPrecompiledContract(&bn256AddIstanbul{}), + common.BytesToAddress([]byte{7}): newWrappedPrecompiledContract(&bn256ScalarMulIstanbul{}), + common.BytesToAddress([]byte{8}): newWrappedPrecompiledContract(&bn256PairingIstanbul{}), + common.BytesToAddress([]byte{9}): newWrappedPrecompiledContract(&blake2F{}), } // PrecompiledContractsYoloV1 contains the default set of pre-compiled Ethereum // contracts used in the Yolo v1 test release. -var PrecompiledContractsYoloV1 = map[common.Address]PrecompiledContract{ - common.BytesToAddress([]byte{1}): &ecrecover{}, - common.BytesToAddress([]byte{2}): &sha256hash{}, - common.BytesToAddress([]byte{3}): &ripemd160hash{}, - common.BytesToAddress([]byte{4}): &dataCopy{}, - common.BytesToAddress([]byte{5}): &bigModExp{}, - common.BytesToAddress([]byte{6}): &bn256AddIstanbul{}, - common.BytesToAddress([]byte{7}): &bn256ScalarMulIstanbul{}, - common.BytesToAddress([]byte{8}): &bn256PairingIstanbul{}, - common.BytesToAddress([]byte{9}): &blake2F{}, - common.BytesToAddress([]byte{10}): &bls12381G1Add{}, - common.BytesToAddress([]byte{11}): &bls12381G1Mul{}, - common.BytesToAddress([]byte{12}): &bls12381G1MultiExp{}, - common.BytesToAddress([]byte{13}): &bls12381G2Add{}, - common.BytesToAddress([]byte{14}): &bls12381G2Mul{}, - common.BytesToAddress([]byte{15}): &bls12381G2MultiExp{}, - common.BytesToAddress([]byte{16}): &bls12381Pairing{}, - common.BytesToAddress([]byte{17}): &bls12381MapG1{}, - common.BytesToAddress([]byte{18}): &bls12381MapG2{}, +var PrecompiledContractsYoloV1 = map[common.Address]StatefulPrecompiledContract{ + common.BytesToAddress([]byte{1}): newWrappedPrecompiledContract(&ecrecover{}), + common.BytesToAddress([]byte{2}): newWrappedPrecompiledContract(&sha256hash{}), + common.BytesToAddress([]byte{3}): newWrappedPrecompiledContract(&ripemd160hash{}), + common.BytesToAddress([]byte{4}): newWrappedPrecompiledContract(&dataCopy{}), + common.BytesToAddress([]byte{5}): newWrappedPrecompiledContract(&bigModExp{}), + common.BytesToAddress([]byte{6}): newWrappedPrecompiledContract(&bn256AddIstanbul{}), + common.BytesToAddress([]byte{7}): newWrappedPrecompiledContract(&bn256ScalarMulIstanbul{}), + common.BytesToAddress([]byte{8}): newWrappedPrecompiledContract(&bn256PairingIstanbul{}), + common.BytesToAddress([]byte{9}): newWrappedPrecompiledContract(&blake2F{}), + common.BytesToAddress([]byte{10}): newWrappedPrecompiledContract(&bls12381G1Add{}), + common.BytesToAddress([]byte{11}): newWrappedPrecompiledContract(&bls12381G1Mul{}), + common.BytesToAddress([]byte{12}): newWrappedPrecompiledContract(&bls12381G1MultiExp{}), + common.BytesToAddress([]byte{13}): newWrappedPrecompiledContract(&bls12381G2Add{}), + common.BytesToAddress([]byte{14}): newWrappedPrecompiledContract(&bls12381G2Mul{}), + common.BytesToAddress([]byte{15}): newWrappedPrecompiledContract(&bls12381G2MultiExp{}), + common.BytesToAddress([]byte{16}): newWrappedPrecompiledContract(&bls12381Pairing{}), + common.BytesToAddress([]byte{17}): newWrappedPrecompiledContract(&bls12381MapG1{}), + common.BytesToAddress([]byte{18}): newWrappedPrecompiledContract(&bls12381MapG2{}), } // RunPrecompiledContract runs and evaluates the output of a precompiled contract. diff --git a/core/vm/contracts_stateful.go b/core/vm/contracts_stateful.go new file mode 100644 index 0000000..3ef7a9e --- /dev/null +++ b/core/vm/contracts_stateful.go @@ -0,0 +1,165 @@ +package vm + +import ( + "errors" + "fmt" + "math/big" + + "github.com/ava-labs/coreth/params" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + "github.com/holiman/uint256" +) + +// PrecompiledContractsApricot contains the default set of pre-compiled Ethereum +// contracts used in the Istanbul release and the stateful precompiled contracts +// added for the Avalanche Apricot release. +// Apricot is incompatible with the YoloV1 Release since it does not include the +// BLS12-381 Curve Operations added to the set of precompiled contracts +var PrecompiledContractsApricot = map[common.Address]StatefulPrecompiledContract{ + common.BytesToAddress([]byte{1}): newWrappedPrecompiledContract(&ecrecover{}), + common.BytesToAddress([]byte{2}): newWrappedPrecompiledContract(&sha256hash{}), + common.BytesToAddress([]byte{3}): newWrappedPrecompiledContract(&ripemd160hash{}), + common.BytesToAddress([]byte{4}): newWrappedPrecompiledContract(&dataCopy{}), + common.BytesToAddress([]byte{5}): newWrappedPrecompiledContract(&bigModExp{}), + common.BytesToAddress([]byte{6}): newWrappedPrecompiledContract(&bn256AddIstanbul{}), + common.BytesToAddress([]byte{7}): newWrappedPrecompiledContract(&bn256ScalarMulIstanbul{}), + common.BytesToAddress([]byte{8}): newWrappedPrecompiledContract(&bn256PairingIstanbul{}), + common.BytesToAddress([]byte{9}): newWrappedPrecompiledContract(&blake2F{}), + common.HexToAddress("0x0100000000000000000000000000000000000000"): &deprecatedContract{msg: "hardcoded genesis contract has been deprecated"}, + common.HexToAddress("0xffffffffffffffffffffffffffffffffffffff"): &nativeAssetBalance{gasCost: params.AssetBalanceApricot}, + common.HexToAddress("0xfffffffffffffffffffffffffffffffffffffe"): &nativeAssetCall{gasCost: params.AssetCallApricot}, +} + +// StatefulPrecompiledContract is the interface for executing a precompiled contract +// This wraps the PrecompiledContracts native to Ethereum and allows adding in stateful +// precompiled contracts to support native Avalanche asset transfers. +type StatefulPrecompiledContract interface { + // Run executes a precompiled contract in the current state + // assumes that it has already been verified that [caller] can + // transfer [value]. + Run(evm *EVM, caller ContractRef, addr common.Address, value *big.Int, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) +} + +// wrappedPrecompiledContract implements StatefulPrecompiledContract by wrapping stateless native precompiled contracts +// in Ethereum. +type wrappedPrecompiledContract struct { + p PrecompiledContract +} + +func newWrappedPrecompiledContract(p PrecompiledContract) StatefulPrecompiledContract { + return &wrappedPrecompiledContract{p: p} +} + +// Run ... +func (w *wrappedPrecompiledContract) Run(evm *EVM, caller ContractRef, addr common.Address, value *big.Int, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + evm.Transfer(evm.StateDB, caller.Address(), addr, value) + return RunPrecompiledContract(w.p, input, suppliedGas) +} + +// nativeAssetBalance is a precompiled contract used to retrieve the native asset balance +type nativeAssetBalance struct { + gasCost uint64 +} + +// Run implements StatefulPrecompiledContract +func (b *nativeAssetBalance) Run(evm *EVM, caller ContractRef, addr common.Address, value *big.Int, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + // input: encodePacked(address 20 bytes, assetID 32 bytes) + if value.Sign() != 0 { + return nil, suppliedGas, errors.New("cannot transfer value to native asset balance pre-compiled contract") + } + + if suppliedGas < b.gasCost { + return nil, 0, ErrOutOfGas + } + remainingGas = suppliedGas - b.gasCost + + if len(input) != 52 { + return nil, remainingGas, fmt.Errorf("input to native asset balance must be 52 bytes, containing address [20 bytes] and assetID [32 bytes], but found length: %d", len(input)) + } + address := common.BytesToAddress(input[:20]) + assetID := new(common.Hash) + assetID.SetBytes(input[20:52]) + + res, overflow := uint256.FromBig(evm.StateDB.GetBalanceMultiCoin(address, *assetID)) + log.Info("nativeAssetBalance", "address", address, "assetID", assetID.Hex(), "res", res, "overflow", overflow) + if overflow { + return nil, remainingGas, errors.New("balance overflow") + } + return common.LeftPadBytes(res.Bytes(), 32), remainingGas, nil +} + +// nativeAssetCall atomically transfers a native asset to a recipient address as well as calling that +// address +type nativeAssetCall struct { + gasCost uint64 +} + +// Run implements StatefulPrecompiledContract +func (c *nativeAssetCall) Run(evm *EVM, caller ContractRef, addr common.Address, value *big.Int, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + // input: encodePacked(address 20 bytes, assetID 32 bytes, assetAmount 32 bytes, callData variable length bytes) + if suppliedGas < c.gasCost { + return nil, 0, ErrOutOfGas + } + remainingGas = suppliedGas - c.gasCost + + if readOnly { + return nil, remainingGas, errors.New("cannot execute native asset transfer within read only call") + } + + if len(input) < 84 { + return nil, remainingGas, fmt.Errorf("missing inputs to native asset transfer, expected input of length at least 84, but found %d", len(input)) + } + to := common.BytesToAddress(input[:20]) + assetID := new(common.Hash) + assetID.SetBytes(input[20:52]) + assetAmount := new(big.Int).SetBytes(input[52:84]) + callData := input[84:] + log.Info("nativeAssetCall", "input", fmt.Sprintf("0x%x", input), "to", to.Hex(), "assetID", assetID.Hex(), "assetAmount", assetAmount, "callData", fmt.Sprintf("0x%x", callData)) + + mcerr := evm.Context.CanTransferMC(evm.StateDB, caller.Address(), to, assetID, assetAmount) + if mcerr == 1 { + log.Info("Insufficient balance for mc transfer") + return nil, remainingGas, ErrInsufficientBalance + } else if mcerr != 0 { + log.Info("incompatible account for mc transfer") + return nil, remainingGas, ErrIncompatibleAccount + } + + snapshot := evm.StateDB.Snapshot() + + if !evm.StateDB.Exist(to) { + log.Info("Creating account entry", "address", to.Hex()) + remainingGas -= params.CallNewAccountGas + evm.StateDB.CreateAccount(to) + } + + // Send [value] to [to] address + evm.Transfer(evm.StateDB, caller.Address(), to, value) + evm.TransferMultiCoin(evm.StateDB, caller.Address(), to, assetID, assetAmount) + ret, remainingGas, err = evm.Call(caller, to, callData, remainingGas, big.NewInt(0)) + log.Info("Finished TransferMultiCoin and call", "return", ret, "remainingGas", remainingGas, "error", err) + + // When an error was returned by the EVM or when setting the creation code + // above we revert to the snapshot and consume any gas remaining. Additionally + // when we're in homestead this also counts for code storage gas errors. + if err != nil { + evm.StateDB.RevertToSnapshot(snapshot) + if err != ErrExecutionReverted { + remainingGas = 0 + } + // TODO: consider clearing up unused snapshots: + //} else { + // evm.StateDB.DiscardSnapshot(snapshot) + } + return ret, remainingGas, err +} + +type deprecatedContract struct { + msg string +} + +// Run ... +func (d *deprecatedContract) Run(evm *EVM, caller ContractRef, addr common.Address, value *big.Int, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + return nil, suppliedGas, errors.New(d.msg) +} diff --git a/core/vm/evm.go b/core/vm/evm.go index 54c9c7f..ab04790 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -34,19 +34,23 @@ var emptyCodeHash = crypto.Keccak256Hash(nil) type ( // CanTransferFunc is the signature of a transfer guard function - CanTransferFunc func(StateDB, common.Address, *big.Int) bool + CanTransferFunc func(StateDB, common.Address, *big.Int) bool + // CanTransferMCFunc is the signature of a transfer native asset function CanTransferMCFunc func(StateDB, common.Address, common.Address, *common.Hash, *big.Int) int // TransferFunc is the signature of a transfer function - TransferFunc func(StateDB, common.Address, common.Address, *big.Int) + TransferFunc func(StateDB, common.Address, common.Address, *big.Int) + // TransferMCFunc is the signature of a transfer native asset function TransferMCFunc func(StateDB, common.Address, common.Address, *common.Hash, *big.Int) // GetHashFunc returns the n'th block hash in the blockchain // and is used by the BLOCKHASH EVM op code. GetHashFunc func(uint64) common.Hash ) -func (evm *EVM) precompile(addr common.Address) (PrecompiledContract, bool) { - var precompiles map[common.Address]PrecompiledContract +func (evm *EVM) precompile(addr common.Address) (StatefulPrecompiledContract, bool) { + var precompiles map[common.Address]StatefulPrecompiledContract switch { + case evm.chainRules.IsApricot: + precompiles = PrecompiledContractsApricot case evm.chainRules.IsYoloV1: precompiles = PrecompiledContractsYoloV1 case evm.chainRules.IsIstanbul: @@ -222,7 +226,6 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas } evm.StateDB.CreateAccount(addr) } - evm.Transfer(evm.StateDB, caller.Address(), addr, value) // Capture the tracer start/end events in debug mode if evm.vmConfig.Debug && evm.depth == 0 { @@ -233,8 +236,9 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas } if isPrecompile { - ret, gas, err = RunPrecompiledContract(p, input, gas) + ret, gas, err = p.Run(evm, caller, addr, value, input, gas, false) } else { + evm.Transfer(evm.StateDB, caller.Address(), addr, value) // Initialise a new contract and set the code that is to be used by the EVM. // The contract is a scoped environment for this execution context only. code := evm.StateDB.GetCode(addr) @@ -371,7 +375,7 @@ func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte, // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { - ret, gas, err = RunPrecompiledContract(p, input, gas) + ret, gas, err = p.Run(evm, caller, addr, value, input, gas, false) } else { addrCopy := addr // Initialise a new contract and set the code that is to be used by the EVM. @@ -407,7 +411,7 @@ func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []by // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { - ret, gas, err = RunPrecompiledContract(p, input, gas) + ret, gas, err = p.Run(evm, caller, addr, big0, input, gas, false) } else { addrCopy := addr // Initialise a new contract and make initialise the delegate values @@ -451,7 +455,7 @@ func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte evm.StateDB.AddBalance(addr, big0) if p, isPrecompile := evm.precompile(addr); isPrecompile { - ret, gas, err = RunPrecompiledContract(p, input, gas) + ret, gas, err = p.Run(evm, caller, addr, big0, input, gas, true) } else { // At this point, we use a copy of address. If we don't, the go compiler will // leak the 'contract' to the outer scope, and make allocation for 'contract' diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index a114a8d..1292650 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -26,6 +26,7 @@ import ( ) var ( + // BuiltinAddr ... BuiltinAddr = common.Address{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -106,6 +107,8 @@ func NewEVMInterpreter(evm *EVM, cfg Config) *EVMInterpreter { if cfg.JumpTable[STOP] == nil { var jt JumpTable switch { + case evm.chainRules.IsApricot: + jt = apricotInstructionSet case evm.chainRules.IsYoloV1: jt = yoloV1InstructionSet case evm.chainRules.IsIstanbul: @@ -146,6 +149,15 @@ func NewEVMInterpreter(evm *EVM, cfg Config) *EVMInterpreter { // considered a revert-and-consume-all-gas operation except for // ErrExecutionReverted which means revert-and-keep-gas-left. func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (ret []byte, err error) { + // TODO remove after Apricot network upgrade. + // [BuiltinAddr] is overridden by a precompile in Apricot. + // This makes any call to [BuiltinAddr] a no-op once the network + // upgrade occurs. Then in the upgrade after the hard fork, this + // code can be safely removed. + // This can't occur before the hard fork because it would change + // the behavior of upgraded nodes before the hard fork occurs + // since there is no mechanism to change this code based on the + // block height. if contract.Address() == BuiltinAddr { self := AccountRef(contract.Caller()) if _, ok := contract.caller.(*Contract); ok { diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index 9b538d4..fc12f93 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -49,6 +49,7 @@ type operation struct { } var ( + apricotInstructionSet = newApricotInstructionSet() frontierInstructionSet = newFrontierInstructionSet() homesteadInstructionSet = newHomesteadInstructionSet() tangerineWhistleInstructionSet = newTangerineWhistleInstructionSet() @@ -1043,3 +1044,13 @@ func newFrontierInstructionSet() JumpTable { }, } } + +// newApricotInstructionSet returns a new instruction set +// compatible with the Apricot release, which deprecates +// added instructions: CALLEX and BALANCEMC +func newApricotInstructionSet() JumpTable { + instructionSet := newFrontierInstructionSet() + instructionSet[CALLEX] = nil + instructionSet[BALANCEMC] = nil + return instructionSet +} diff --git a/core/vm/opcodes.go b/core/vm/opcodes.go index c5097f8..cc9518e 100644 --- a/core/vm/opcodes.go +++ b/core/vm/opcodes.go @@ -208,6 +208,7 @@ const ( SWAP ) +// Constants supporting native asset operations const ( BALANCEMC = 0xcd //EMC = 0xce diff --git a/eth/backend.go b/eth/backend.go index 9fb8f38..e26cbb3 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -28,7 +28,6 @@ import ( "github.com/ava-labs/coreth/consensus" "github.com/ava-labs/coreth/consensus/clique" "github.com/ava-labs/coreth/consensus/dummy" - "github.com/ava-labs/coreth/consensus/ethash" "github.com/ava-labs/coreth/core" "github.com/ava-labs/coreth/core/bloombits" "github.com/ava-labs/coreth/core/rawdb" @@ -152,7 +151,7 @@ func New(stack *node.Node, config *Config, chainDb: chainDb, eventMux: stack.EventMux(), accountManager: stack.AccountManager(), - engine: CreateConsensusEngine(stack, chainConfig, &config.Ethash, config.Miner.Notify, config.Miner.Noverify, chainDb, cb), + engine: CreateConsensusEngine(cb), closeBloomHandler: make(chan struct{}), networkID: config.NetworkId, gasPrice: config.Miner.GasPrice, @@ -265,7 +264,7 @@ func makeExtraData(extra []byte) []byte { } // CreateConsensusEngine creates the required type of consensus engine instance for an Ethereum service -func CreateConsensusEngine(stack *node.Node, chainConfig *params.ChainConfig, config *ethash.Config, notify []string, noverify bool, db ethdb.Database, cb *dummy.ConsensusCallbacks) consensus.Engine { +func CreateConsensusEngine(cb *dummy.ConsensusCallbacks) consensus.Engine { return dummy.NewDummyEngine(cb) } diff --git a/examples/arc20/main.go b/examples/arc20/main.go new file mode 100644 index 0000000..6bac290 --- /dev/null +++ b/examples/arc20/main.go @@ -0,0 +1,93 @@ +package main + +import ( + "context" + "crypto/ecdsa" + "fmt" + "math/big" + + "github.com/ava-labs/coreth/params" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" +) + +var ( + uri = "http://127.0.0.1:9650/ext/bc/C/rpc" + nativeAssetCallAddr = common.BytesToAddress([]byte{11}) + nativeAssetBalanceAddr = common.BytesToAddress([]byte{10}) + chainID = new(big.Int).SetUint64(43112) + privateKey *ecdsa.PrivateKey + address common.Address + erc20 common.Address + assetID *big.Int + amount *big.Int + gasLimit = uint64(700000) + gasPrice = params.MinGasPrice +) + +func init() { + pk, err := crypto.HexToECDSA("da777cd656c8760a7d378ae04d7dd0cd7a703c450c84e6c2faa886ca97517df7") + if err != nil { + panic(err) + } + privateKey = pk + address = crypto.PubkeyToAddress(privateKey.PublicKey) + erc20 = common.HexToAddress("0xea75d59faF258F1fdf2b94F158e54D7ad44359B6") + aID, success := new(big.Int).SetString("21556002304173297996004673107070813226981923092188601867247360128943231977558", 10) + if !success { + panic("Failed to read assetID") + } + assetID = aID + amount = new(big.Int).SetUint64(100) +} + +// createDepositCallData creates the callData argument to nativeAssetTransfer to move [amount] +// of [assetID] to [erc20] address and call the deposit function with signature "deposit()" +func createDepositCallData(erc20 common.Address, assetID, amount *big.Int) []byte { + // erc20 addr, assetID, assetAmount, callData + signatureHash := crypto.Keccak256([]byte("deposit()")) + fmt.Printf("signatureHash: 0x%x\n", signatureHash) + functionSignature := signatureHash[:4] + data := make([]byte, 0, 84) + data = append(data, erc20.Bytes()...) + data = append(data, common.LeftPadBytes(assetID.Bytes(), 32)...) + data = append(data, common.LeftPadBytes(amount.Bytes(), 32)...) + data = append(data, functionSignature...) // Add this back in to trigger call to deposit + fmt.Printf("deposit callData: 0x%x\n", data) + return data +} + +// createDepositTransaction creates a transaction to deposit native asset funds in [erc20] +func createDepositTransaction(nonce uint64, erc20 common.Address, assetID, amount *big.Int, gasLimit uint64, gasPrice *big.Int) *types.Transaction { + callData := createDepositCallData(erc20, assetID, amount) + return types.NewTransaction(nonce, nativeAssetCallAddr, new(big.Int), gasLimit, gasPrice, callData) +} + +func main() { + client, err := ethclient.Dial(uri) + if err != nil { + panic(err) + } + ctx := context.Background() + nonce, err := client.NonceAt(ctx, address, nil) + if err != nil { + panic(err) + } + fmt.Printf("Creating deposit transaction from: %s, erc20 address: %s, assetID: %d, amount: %d, nonce: %d\n", address.Hex(), erc20.Hex(), assetID, amount, nonce) + // Create and sign deposit transaction from account that has been funded with sufficient AVAX to + // pay gas costs and sufficient amount of the native asset to make the deposit + tx := createDepositTransaction(nonce, erc20, assetID, amount, gasLimit, gasPrice) + signer := types.NewEIP155Signer(chainID) + signedTx, err := types.SignTx(tx, signer, privateKey) + if err != nil { + panic(err) + } + // Send the signed transaction to the client + if err := client.SendTransaction(ctx, signedTx); err != nil { + panic(err) + } + txHash := signedTx.Hash() + fmt.Printf("txHash: %s\n", txHash.Hex()) +} diff --git a/examples/multicoin/main.go b/examples/multicoin/main.go index 35158a8..c2257e3 100644 --- a/examples/multicoin/main.go +++ b/examples/multicoin/main.go @@ -5,6 +5,15 @@ import ( "encoding/json" "flag" "fmt" + "go/build" + "math/big" + "os" + "os/signal" + "path/filepath" + "strings" + "syscall" + "time" + "github.com/ava-labs/coreth" "github.com/ava-labs/coreth/core" "github.com/ava-labs/coreth/core/types" @@ -18,14 +27,6 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rlp" - "go/build" - "math/big" - "os" - "os/signal" - "path/filepath" - "strings" - "syscall" - "time" ) func checkError(err error) { diff --git a/params/config.go b/params/config.go index e5ec64b..1e14c40 100644 --- a/params/config.go +++ b/params/config.go @@ -53,6 +53,49 @@ var CheckpointOracles = map[common.Hash]*CheckpointOracleConfig{ } var ( + // AvalancheMainnetChainID ... + AvalancheMainnetChainID = big.NewInt(43114) + // AvalancheFujiChainID ... + AvalancheFujiChainID = big.NewInt(43113) +) + +var ( + // AvalancheApricotMainnetChainConfig is the configuration for Avalanche Main Network + AvalancheApricotMainnetChainConfig = &ChainConfig{ + ChainID: AvalancheMainnetChainID, + HomesteadBlock: big.NewInt(0), + DAOForkBlock: big.NewInt(0), + DAOForkSupport: true, + EIP150Block: big.NewInt(0), + EIP150Hash: common.HexToHash("0x2086799aeebeae135c246c65021c82b4e15a2c451340993aacfd2751886514f0"), + EIP155Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + MuirGlacierBlock: big.NewInt(0), + ApricotBlock: big.NewInt(0), // TODO specify correct block height + } + + // AvalancheApricotFujiChainConfig is the configuration for the Fuji Test Network + AvalancheApricotFujiChainConfig = &ChainConfig{ + ChainID: AvalancheFujiChainID, + HomesteadBlock: big.NewInt(0), + DAOForkBlock: big.NewInt(0), + DAOForkSupport: true, + EIP150Block: big.NewInt(0), + EIP150Hash: common.HexToHash("0x2086799aeebeae135c246c65021c82b4e15a2c451340993aacfd2751886514f0"), + EIP155Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + MuirGlacierBlock: big.NewInt(0), + ApricotBlock: big.NewInt(0), // TODO specify correct block height + } + + // TODO update local network genesis to enable Apricot + // MainnetChainConfig is the chain parameters to run a node on the main network. MainnetChainConfig = &ChainConfig{ ChainID: big.NewInt(1), @@ -239,16 +282,16 @@ var ( // // This configuration is intentionally not using keyed fields to force anyone // adding flags to the config to also have to set these fields. - AllEthashProtocolChanges = &ChainConfig{big.NewInt(1337), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, nil, nil, new(EthashConfig), nil} + AllEthashProtocolChanges = &ChainConfig{big.NewInt(1337), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, nil, nil, nil, new(EthashConfig), nil} // AllCliqueProtocolChanges contains every protocol change (EIPs) introduced // and accepted by the Ethereum core developers into the Clique consensus. // // This configuration is intentionally not using keyed fields to force anyone // adding flags to the config to also have to set these fields. - AllCliqueProtocolChanges = &ChainConfig{big.NewInt(1337), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, nil, nil, nil, &CliqueConfig{Period: 0, Epoch: 30000}} + AllCliqueProtocolChanges = &ChainConfig{big.NewInt(1337), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, nil, nil, nil, nil, &CliqueConfig{Period: 0, Epoch: 30000}} - TestChainConfig = &ChainConfig{big.NewInt(1), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, nil, nil, new(EthashConfig), nil} + TestChainConfig = &ChainConfig{big.NewInt(1), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, nil, nil, nil, new(EthashConfig), nil} TestRules = TestChainConfig.Rules(new(big.Int)) ) @@ -323,6 +366,8 @@ type ChainConfig struct { YoloV1Block *big.Int `json:"yoloV1Block,omitempty"` // YOLO v1: https://github.com/ethereum/EIPs/pull/2657 (Ephemeral testnet) EWASMBlock *big.Int `json:"ewasmBlock,omitempty"` // EWASM switch block (nil = no fork, 0 = already activated) + ApricotBlock *big.Int `json:"apricotBlock,omitempty"` // Apricot switch block (nil = no fork, 0 = already activated) + // Various consensus engines Ethash *EthashConfig `json:"ethash,omitempty"` Clique *CliqueConfig `json:"clique,omitempty"` @@ -358,7 +403,7 @@ func (c *ChainConfig) String() string { default: engine = "unknown" } - return fmt.Sprintf("{ChainID: %v Homestead: %v DAO: %v DAOSupport: %v EIP150: %v EIP155: %v EIP158: %v Byzantium: %v Constantinople: %v Petersburg: %v Istanbul: %v, Muir Glacier: %v, YOLO v1: %v, Engine: %v}", + return fmt.Sprintf("{ChainID: %v Homestead: %v DAO: %v DAOSupport: %v EIP150: %v EIP155: %v EIP158: %v Byzantium: %v Constantinople: %v Petersburg: %v Istanbul: %v, Muir Glacier: %v, YOLO v1: %v, Apricot: %v, Engine: %v}", c.ChainID, c.HomesteadBlock, c.DAOForkBlock, @@ -372,6 +417,7 @@ func (c *ChainConfig) String() string { c.IstanbulBlock, c.MuirGlacierBlock, c.YoloV1Block, + c.ApricotBlock, engine, ) } @@ -438,6 +484,12 @@ func (c *ChainConfig) IsEWASM(num *big.Int) bool { return isForked(c.EWASMBlock, num) } +// IsApricot returns whether num represents a block number after the Apricot fork +func (c *ChainConfig) IsApricot(num *big.Int) bool { + return isForked(c.ApricotBlock, num) +} + +// TODO review how this works and see if it will work for a live transition // CheckCompatible checks whether scheduled fork transitions have been imported // with a mismatching chain configuration. func (c *ChainConfig) CheckCompatible(newcfg *ChainConfig, height uint64) *ConfigCompatError { @@ -477,6 +529,10 @@ func (c *ChainConfig) CheckConfigForkOrder() error { {name: "istanbulBlock", block: c.IstanbulBlock}, {name: "muirGlacierBlock", block: c.MuirGlacierBlock, optional: true}, {name: "yoloV1Block", block: c.YoloV1Block}, + // TODO figure out how Apricot fits in here. + // Precompiled contracts start from other end so they are compatible + // but it may be incompatible (and it was before Apricot as well) due + // to how the instruction sets are created } { if lastFork.name != "" { // Next one must be higher number @@ -610,6 +666,8 @@ type Rules struct { IsHomestead, IsEIP150, IsEIP155, IsEIP158 bool IsByzantium, IsConstantinople, IsPetersburg, IsIstanbul bool IsYoloV1 bool + // Avalanche Releases + IsApricot bool } // Rules ensures c's ChainID is not nil. @@ -629,5 +687,6 @@ func (c *ChainConfig) Rules(num *big.Int) Rules { IsPetersburg: c.IsPetersburg(num), IsIstanbul: c.IsIstanbul(num), IsYoloV1: c.IsYoloV1(num), + IsApricot: c.IsApricot(num), } } diff --git a/params/protocol_params.go b/params/protocol_params.go index 3536c83..fdfb9a5 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -95,6 +95,8 @@ const ( ExtcodeHashGasConstantinople uint64 = 400 // Cost of EXTCODEHASH (introduced in Constantinople) ExtcodeHashGasEIP1884 uint64 = 700 // Cost of EXTCODEHASH after EIP 1884 (part in Istanbul) SelfdestructGasEIP150 uint64 = 5000 // Cost of SELFDESTRUCT post EIP 150 (Tangerine) + AssetBalanceApricot uint64 = 20 // Cost of calling PrecompiledContract AssetBalance (Apricot) + AssetCallApricot uint64 = 40 // Cost of calling PrecompiledContract AssetCall (Apricot) // EXP has a dynamic portion depending on the size of the exponent ExpByteFrontier uint64 = 10 // was set to 10 in Frontier diff --git a/plugin/evm/export_tx.go b/plugin/evm/export_tx.go index 2735573..2ebfeff 100644 --- a/plugin/evm/export_tx.go +++ b/plugin/evm/export_tx.go @@ -240,8 +240,8 @@ func (vm *VM) newExportTx( func (tx *UnsignedExportTx) EVMStateTransfer(vm *VM, state *state.StateDB) error { addrs := map[[20]byte]uint64{} for _, from := range tx.Ins { - log.Info("crosschain C->X", "addr", from.Address, "amount", from.Amount) if from.AssetID == vm.ctx.AVAXAssetID { + log.Info("crosschain C->X", "from", from.Address, "amount", from.Amount, "assetID", "AVAX") amount := new(big.Int).Mul( new(big.Int).SetUint64(from.Amount), x2cRate) if state.GetBalance(from.Address).Cmp(amount) < 0 { @@ -250,6 +250,7 @@ func (tx *UnsignedExportTx) EVMStateTransfer(vm *VM, state *state.StateDB) error state.SubBalance(from.Address, amount) } else { amount := new(big.Int).SetUint64(from.Amount) + log.Info("crosschain C->X", "from", from.Address, "amount", from.Amount, "assetID", from.AssetID) if state.GetBalanceMultiCoin(from.Address, common.Hash(from.AssetID)).Cmp(amount) < 0 { return errInsufficientFunds } diff --git a/plugin/evm/import_tx.go b/plugin/evm/import_tx.go index 1ec394c..d6b4b7d 100644 --- a/plugin/evm/import_tx.go +++ b/plugin/evm/import_tx.go @@ -270,12 +270,13 @@ func (vm *VM) newImportTx( // accounts accordingly with the imported EVMOutputs func (tx *UnsignedImportTx) EVMStateTransfer(vm *VM, state *state.StateDB) error { for _, to := range tx.Outs { - log.Info("crosschain X->C", "addr", to.Address, "amount", to.Amount) if to.AssetID == vm.ctx.AVAXAssetID { + log.Info("crosschain X->C", "to", to.Address, "amount", to.Amount, "assetID", "AVAX") amount := new(big.Int).Mul( new(big.Int).SetUint64(to.Amount), x2cRate) state.AddBalance(to.Address, amount) } else { + log.Info("crosschain X->C", "to", to.Address, "amount", to.Amount, "assetID", to.AssetID) amount := new(big.Int).SetUint64(to.Amount) state.AddBalanceMultiCoin(to.Address, common.Hash(to.AssetID), amount) } diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index c9bdad9..fc571b9 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -239,6 +239,15 @@ func (vm *VM) Initialize( return err } + // Set the Apricot Fork Block on the ChainConfig + // TODO switch to correct block number of the fork + switch { + case g.Config.ChainID.Cmp(params.AvalancheMainnetChainID) == 0: + g.Config.ApricotBlock = nil + case g.Config.ChainID.Cmp(params.AvalancheFujiChainID) == 0: + g.Config.ApricotBlock = nil + } + vm.acceptedDB = prefixdb.New([]byte(acceptedPrefix), db) vm.chainID = g.Config.ChainID -- cgit v1.2.3