-
Notifications
You must be signed in to change notification settings - Fork 295
Problem: no stateful precompiled contract for bank #837
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| uint256Type, _ := abi.NewType("uint256", "", nil) | ||
Check warningCode 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 | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Check warning
Code scanning / gosec
Returned error is not propagated up the stack.