Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
- (deps) [#1121](https://github.com/crypto-org-chain/cronos/pull/1121) Bump Cosmos-SDK to v0.47.5 and ibc-go to v7.2.0.
- [cronos#1014](https://github.com/crypto-org-chain/cronos/pull/1014) Support stateful precompiled contract for relayer.
- [cronos#1165](https://github.com/crypto-org-chain/cronos/pull/1165) Icaauth module is not adjusted correctly in ibc-go v7.2.0.
- [cronos#837](https://github.com/crypto-org-chain/cronos/pull/837) Support stateful precompiled contract for bank.

### Bug Fixes

Expand Down
1 change: 1 addition & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,7 @@ func New(
tracer,
evmS,
[]vm.PrecompiledContract{
cronosprecompiles.NewBankContract(app.BankKeeper, appCodec),
cronosprecompiles.NewRelayerContract(app.IBCKeeper, appCodec),
},
allKeys,
Expand Down
46 changes: 46 additions & 0 deletions integration_tests/contracts/contracts/TestBank.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: MIT
pragma solidity >0.6.6;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract TestBank is ERC20 {
address constant bankContract = 0x0000000000000000000000000000000000000064;
constructor() public ERC20("Bitcoin MAX", "MAX") {
_mint(msg.sender, 100000000000000000000000000);
}
function encodeMint(uint256 amount) internal view returns (bytes memory) {
return abi.encodeWithSignature("mint(address,uint256)", msg.sender, amount);
}
function moveToNative(uint256 amount) public {
_burn(msg.sender, amount);
(bool result, ) = bankContract.call(encodeMint(amount));
require(result, "native call");
}
function encodeBurn(uint256 amount) internal view returns (bytes memory) {
return abi.encodeWithSignature("burn(address,uint256)", msg.sender, amount);
}
function moveFromNative(uint256 amount) public {
(bool result, ) = bankContract.call(encodeBurn(amount));
require(result, "native call");
_mint(msg.sender, amount);
}
function encodeBalanceOf(address addr) internal view returns (bytes memory) {
return abi.encodeWithSignature("balanceOf(address,address)", address(this), addr);
}
function nativeBalanceOf(address addr) public returns (uint256) {
(bool result, bytes memory data) = bankContract.call(encodeBalanceOf(addr));
require(result, "native call");
return abi.decode(data, (uint256));
}
function moveToNativeRevert(uint256 amount) public {
moveToNative(amount);
revert("test");
}
function encodeTransfer(address recipient, uint256 amount) internal view returns (bytes memory) {
return abi.encodeWithSignature("transfer(address,address,uint256)", msg.sender, recipient, amount);
}
function nativeTransfer(address recipient, uint256 amount) public {
_transfer(msg.sender, recipient, amount);
(bool result, ) = bankContract.call(encodeTransfer(recipient, amount));
require(result, "native transfer");
}
}
84 changes: 84 additions & 0 deletions integration_tests/test_bank_precompiles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import pytest
import web3

from .utils import (
ADDRS,
CONTRACTS,
KEYS,
deploy_contract,
eth_to_bech32,
module_address,
send_transaction,
)


def get_balance(cli, addr, denom):
return cli.balance(eth_to_bech32(addr), denom)


def test_call(cronos):
w3 = cronos.w3
cli = cronos.cosmos_cli()
addr = ADDRS["signer1"]
keys = KEYS["signer1"]
contract = deploy_contract(w3, CONTRACTS["TestBank"], (), keys)
denom = "evm/" + contract.address

def assert_balance(tx, expect_status, amt):
balance = get_balance(cli, addr, denom)
assert balance == contract.caller.nativeBalanceOf(addr)
crc20_balance = contract.caller.balanceOf(addr)
receipt = send_transaction(w3, tx, keys)
assert receipt.status == expect_status
balance += amt
assert balance == get_balance(cli, addr, denom)
assert balance == contract.caller.nativeBalanceOf(addr)
assert crc20_balance - amt == contract.caller.balanceOf(addr)

# test mint
amt1 = 100
data = {"from": addr}
tx = contract.functions.moveToNative(amt1).build_transaction(data)
assert_balance(tx, 1, amt1)

# test exception revert
tx = contract.functions.moveToNativeRevert(amt1).build_transaction(
{"from": addr, "gas": 210000}
)
assert_balance(tx, 0, 0)

# test burn
amt2 = 50
tx = contract.functions.moveFromNative(amt2).build_transaction(data)
assert_balance(tx, 1, -amt2)

# test transfer
amt3 = 10
addr2 = ADDRS["signer2"]
tx = contract.functions.nativeTransfer(addr2, amt3).build_transaction(data)
balance = get_balance(cli, addr, denom)
assert balance == contract.caller.nativeBalanceOf(addr)
crc20_balance = contract.caller.balanceOf(addr)

balance2 = get_balance(cli, addr2, denom)
assert balance2 == contract.caller.nativeBalanceOf(addr2)
crc20_balance2 = contract.caller.balanceOf(addr2)

receipt = send_transaction(w3, tx, keys)
assert receipt.status == 1

balance -= amt3
assert balance == get_balance(cli, addr, denom)
assert balance == contract.caller.nativeBalanceOf(addr)
assert crc20_balance - amt3 == contract.caller.balanceOf(addr)

balance2 += amt3
assert balance2 == get_balance(cli, addr2, denom)
assert balance2 == contract.caller.nativeBalanceOf(addr2)
assert crc20_balance2 + amt3 == contract.caller.balanceOf(addr2)

# test transfer to blocked address
recipient = module_address("evm")
amt4 = 20
with pytest.raises(web3.exceptions.ContractLogicError):
contract.functions.nativeTransfer(recipient, amt4).build_transaction(data)
1 change: 1 addition & 0 deletions integration_tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"TestCRC20Proxy": "TestCRC20Proxy.sol",
"TestMaliciousSupply": "TestMaliciousSupply.sol",
"CosmosERC20": "CosmosToken.sol",
"TestBank": "TestBank.sol",
}


Expand Down
228 changes: 228 additions & 0 deletions x/cronos/keeper/precompiles/bank.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
package precompiles

import (
"errors"
"math/big"

sdkmath "cosmossdk.io/math"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"

errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
errortypes "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/evmos/ethermint/x/evm/types"
)

const (
EVMDenomPrefix = "evm/"
BankContractRequiredGas = 10000
)

var (
BankContractAddress = common.BytesToAddress([]byte{100})
MintMethod abi.Method
BurnMethod abi.Method
BalanceOfMethod abi.Method
TransferMethod abi.Method
)

func init() {
addressType, _ := abi.NewType("address", "", nil)

Check warning

Code scanning / gosec

Returned error is not propagated up the stack.

Returned error is not propagated up the stack.
uint256Type, _ := abi.NewType("uint256", "", nil)

Check warning

Code scanning / gosec

Returned error is not propagated up the stack.

Returned error is not propagated up the stack.
MintMethod = abi.NewMethod(
"mint", "mint", abi.Function, "", false, false, abi.Arguments{abi.Argument{
Name: "recipient",
Type: addressType,
}, abi.Argument{
Name: "amount",
Type: uint256Type,
}},
nil,
)
BurnMethod = abi.NewMethod(
"burn", "burn", abi.Function, "", false, false, abi.Arguments{abi.Argument{
Name: "recipient",
Type: addressType,
}, abi.Argument{
Name: "amount",
Type: uint256Type,
}},
nil,
)
BalanceOfMethod = abi.NewMethod(
"balanceOf", "balanceOf", abi.Function, "", false, false, abi.Arguments{abi.Argument{
Name: "token",
Type: addressType,
}, abi.Argument{
Name: "address",
Type: addressType,
}},
abi.Arguments{abi.Argument{
Name: "amount",
Type: uint256Type,
}},
)
TransferMethod = abi.NewMethod(
"transfer", "transfer", abi.Function, "", false, false, abi.Arguments{abi.Argument{
Name: "sender",
Type: addressType,
}, abi.Argument{
Name: "recipient",
Type: addressType,
}, abi.Argument{
Name: "amount",
Type: uint256Type,
}},
nil,
)
}

func EVMDenom(token common.Address) string {
return EVMDenomPrefix + token.Hex()
}

type BankContract struct {
bankKeeper types.BankKeeper
cdc codec.Codec
}

// NewBankContract creates the precompiled contract to manage native tokens
func NewBankContract(bankKeeper types.BankKeeper, cdc codec.Codec) vm.PrecompiledContract {
return &BankContract{bankKeeper, cdc}
}

func (bc *BankContract) Address() common.Address {
return BankContractAddress
}

// RequiredGas calculates the contract gas use
func (bc *BankContract) RequiredGas(input []byte) uint64 {
// TODO estimate required gas
return BankContractRequiredGas
}

func (bc *BankContract) IsStateful() bool {
return true
}

func (bc *BankContract) checkBlockedAddr(addr sdk.AccAddress) error {
to, err := sdk.AccAddressFromBech32(addr.String())
if err != nil {
return err
}
if bc.bankKeeper.BlockedAddr(to) {
return errorsmod.Wrapf(errortypes.ErrUnauthorized, "%s is not allowed to receive funds", to.String())
}
return nil
}

func (bc *BankContract) Run(evm *vm.EVM, contract *vm.Contract, readonly bool) ([]byte, error) {
// parse input
methodID := contract.Input[:4]
stateDB := evm.StateDB.(ExtStateDB)
precompileAddr := bc.Address()

switch string(methodID) {
case string(MintMethod.ID), string(BurnMethod.ID):
if readonly {
return nil, errors.New("the method is not readonly")
}
mint := string(methodID) == string(MintMethod.ID)
var method abi.Method
if mint {
method = MintMethod
} else {
method = BurnMethod
}
args, err := method.Inputs.Unpack(contract.Input[4:])
if err != nil {
return nil, errors.New("fail to unpack input arguments")
}
recipient := args[0].(common.Address)
amount := args[1].(*big.Int)
if amount.Sign() <= 0 {
return nil, errors.New("invalid amount")
}
addr := sdk.AccAddress(recipient.Bytes())
if err := bc.checkBlockedAddr(addr); err != nil {
return nil, err
}
denom := EVMDenom(contract.CallerAddress)
amt := sdk.NewCoin(denom, sdkmath.NewIntFromBigInt(amount))
err = stateDB.ExecuteNativeAction(precompileAddr, nil, func(ctx sdk.Context) error {
if err := bc.bankKeeper.IsSendEnabledCoins(ctx, amt); err != nil {
return err
}
if mint {
if err := bc.bankKeeper.MintCoins(ctx, types.ModuleName, sdk.NewCoins(amt)); err != nil {
return errorsmod.Wrap(err, "fail to mint coins in precompiled contract")
}
if err := bc.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, addr, sdk.NewCoins(amt)); err != nil {
return errorsmod.Wrap(err, "fail to send mint coins to account")
}
} else {
if err := bc.bankKeeper.SendCoinsFromAccountToModule(ctx, addr, types.ModuleName, sdk.NewCoins(amt)); err != nil {
return errorsmod.Wrap(err, "fail to send burn coins to module")
}
if err := bc.bankKeeper.BurnCoins(ctx, types.ModuleName, sdk.NewCoins(amt)); err != nil {
return errorsmod.Wrap(err, "fail to burn coins in precompiled contract")
}
}
return nil
})
if err != nil {
return nil, err
}
case string(BalanceOfMethod.ID):
args, err := BalanceOfMethod.Inputs.Unpack(contract.Input[4:])
if err != nil {
return nil, errors.New("fail to unpack input arguments")
}
token := args[0].(common.Address)
addr := args[1].(common.Address)
// query from storage
balance := big.NewInt(0)
err = stateDB.ExecuteNativeAction(precompileAddr, nil, func(ctx sdk.Context) error {
balance = bc.bankKeeper.GetBalance(ctx, sdk.AccAddress(addr.Bytes()), EVMDenom(token)).Amount.BigInt()
return err
})
return BalanceOfMethod.Outputs.Pack(balance)
case string(TransferMethod.ID):
if readonly {
return nil, errors.New("the method is not readonly")
}
args, err := TransferMethod.Inputs.Unpack(contract.Input[4:])
if err != nil {
return nil, errors.New("fail to unpack input arguments")
}
sender := args[0].(common.Address)
recipient := args[1].(common.Address)
amount := args[2].(*big.Int)
if amount.Sign() <= 0 {
return nil, errors.New("invalid amount")
}
from := sdk.AccAddress(sender.Bytes())
to := sdk.AccAddress(recipient.Bytes())
if err := bc.checkBlockedAddr(to); err != nil {
return nil, err
}
denom := EVMDenom(contract.CallerAddress)
amt := sdk.NewCoin(denom, sdkmath.NewIntFromBigInt(amount))
err = stateDB.ExecuteNativeAction(precompileAddr, nil, func(ctx sdk.Context) error {
if err := bc.bankKeeper.IsSendEnabledCoins(ctx, amt); err != nil {
return err
}
if err := bc.bankKeeper.SendCoins(ctx, from, to, sdk.NewCoins(amt)); err != nil {
return errorsmod.Wrap(err, "fail to send coins in precompiled contract")
}
return nil
})
return nil, err
default:
return nil, errors.New("unknown method")
}
return nil, nil
}